class SlotRecord {
/**
- * @var object database result row, as a raw object
+ * @var object database result row, as a raw object. Callbacks are supported for field values,
+ * to enable on-demand emulation of these values. This is primarily intended for use
+ * during schema migration.
*/
private $row;
* @return SlotRecord
*/
public static function newInherited( SlotRecord $slot ) {
+ // Sanity check - we can't inherit from a Slot that's not attached to a revision.
+ $slot->getRevision();
+ $slot->getOrigin();
+ $slot->getAddress();
+
+ // NOTE: slot_origin and content_address are copied from $slot.
return self::newDerived( $slot, [
- 'slot_inherited' => true,
'slot_revision_id' => null,
] );
}
$row = [
'slot_id' => null, // not yet known
'slot_revision_id' => null, // not yet known
- 'slot_inherited' => 0, // not inherited
+ 'slot_origin' => null, // not yet known, will be set in newSaved()
'content_size' => null, // compute later
'content_sha1' => null, // compute later
'slot_content_id' => null, // not yet known, will be set in newSaved()
/**
* Constructs a complete SlotRecord for a newly saved revision, based on the incomplete
* proto-slot. This adds information that has only become available during saving,
- * particularly the revision ID and content address.
+ * particularly the revision ID, content ID and content address.
*
* @param int $revisionId the revision the slot is to be associated with (field slot_revision_id).
* If $protoSlot already has a revision, it must be the same.
- * @param int $contentId the ID of the row in the content table describing the content
+ * @param int|null $contentId the ID of the row in the content table describing the content
* referenced by $contentAddress (field slot_content_id).
* If $protoSlot already has a content ID, it must be the same.
* @param string $contentAddress the slot's content address (field content_address).
SlotRecord $protoSlot
) {
Assert::parameterType( 'integer', $revisionId, '$revisionId' );
- Assert::parameterType( 'integer', $contentId, '$contentId' );
+ // TODO once migration is over $contentId must be an integer
+ Assert::parameterType( 'integer|null', $contentId, '$contentId' );
Assert::parameterType( 'string', $contentAddress, '$contentAddress' );
if ( $protoSlot->hasRevision() && $protoSlot->getRevision() !== $revisionId ) {
);
}
- if ( $protoSlot->hasAddress() && $protoSlot->getContentId() !== $contentId ) {
+ if ( $protoSlot->hasContentId() && $protoSlot->getContentId() !== $contentId ) {
throw new LogicException(
"Mismatching content ID $contentId: "
. "The slot already has content row {$protoSlot->getContentId()} associated."
);
}
- if ( $protoSlot->isInherited() && !$protoSlot->hasAddress() ) {
- throw new InvalidArgumentException(
- "An inherited blob should have a content address!"
- );
+ if ( $protoSlot->isInherited() ) {
+ if ( !$protoSlot->hasAddress() ) {
+ throw new InvalidArgumentException(
+ "An inherited blob should have a content address!"
+ );
+ }
+ if ( !$protoSlot->hasField( 'slot_origin' ) ) {
+ throw new InvalidArgumentException(
+ "A saved inherited slot should have an origin set!"
+ );
+ }
+ $origin = $protoSlot->getOrigin();
+ } else {
+ $origin = $revisionId;
}
return self::newDerived( $protoSlot, [
'slot_revision_id' => $revisionId,
'slot_content_id' => $contentId,
+ 'slot_origin' => $origin,
'content_address' => $contentAddress,
] );
}
Assert::parameterType( 'object', $row, '$row' );
Assert::parameterType( 'Content|callable', $content, '$content' );
- Assert::parameter(
- property_exists( $row, 'slot_id' ),
- '$row->slot_id',
- 'must exist'
- );
Assert::parameter(
property_exists( $row, 'slot_revision_id' ),
'$row->slot_revision_id',
'must exist'
);
- Assert::parameter(
- property_exists( $row, 'slot_inherited' ),
- '$row->slot_inherited',
- 'must exist'
- );
Assert::parameter(
property_exists( $row, 'slot_content_id' ),
'$row->slot_content_id',
'$row->model_name',
'must exist'
);
+ Assert::parameter(
+ property_exists( $row, 'slot_origin' ),
+ '$row->slot_origin',
+ 'must exist'
+ );
+ Assert::parameter(
+ !property_exists( $row, 'slot_inherited' ),
+ '$row->slot_inherited',
+ 'must not exist'
+ );
+ Assert::parameter(
+ !property_exists( $row, 'slot_revision' ),
+ '$row->slot_revision',
+ 'must not exist'
+ );
$this->row = $row;
$this->content = $content;
* @return bool whether this record contains the given field
*/
private function hasField( $name ) {
+ if ( isset( $this->row->$name ) ) {
+ // if the field is a callback, resolve first, then re-check
+ if ( !is_string( $this->row->$name ) && is_callable( $this->row->$name ) ) {
+ $this->getField( $name );
+ }
+ }
+
return isset( $this->row->$name );
}
return $this->getIntField( 'slot_revision_id' );
}
+ /**
+ * Returns the revision ID of the revision that originated the slot's content.
+ *
+ * @return int
+ */
+ public function getOrigin() {
+ return $this->getIntField( 'slot_origin' );
+ }
+
/**
* Whether this slot was inherited from an older revision.
*
+ * If this SlotRecord is already attached to a revision, this returns true
+ * if the slot's revision of origin is the same as the revision it belongs to.
+ *
+ * If this SlotRecord is not yet attached to a revision, this returns true
+ * if the slot already has an address.
+ *
* @return bool
*/
public function isInherited() {
- return $this->getIntField( 'slot_inherited' ) !== 0;
+ if ( $this->hasRevision() ) {
+ return $this->getRevision() !== $this->getOrigin();
+ } else {
+ return $this->hasAddress();
+ }
}
/**
return $this->hasField( 'content_address' );
}
+ /**
+ * Whether this slot has an origin (revision ID that originated the slot's content.
+ *
+ * @since 1.32
+ *
+ * @return bool
+ */
+ public function hasOrigin() {
+ return $this->hasField( 'slot_origin' );
+ }
+
+ /**
+ * Whether this slot has a content ID. Slots will have a content ID if their
+ * content has been stored in the content table. While building a new revision,
+ * SlotRecords will not have an ID associated.
+ *
+ * @since 1.32
+ *
+ * @return bool
+ */
+ public function hasContentId() {
+ return $this->hasField( 'slot_content_id' );
+ }
+
/**
* Whether this slot has revision ID associated. Slots will have a revision ID associated
* only if they were loaded as part of an existing revision. While building a new revision,
return \Wikimedia\base_convert( sha1( $blob ), 16, 36, 31 );
}
+ /**
+ * Returns true if $other has the same content as this slot.
+ * The check is performed based on the model, address size, and hash.
+ * Two slots can have the same content if they use different content addresses,
+ * but if they have the same address and the same model, they have the same content.
+ * Two slots can have the same content if they belong to different
+ * revisions or pages.
+ *
+ * Note that hasSameContent() may return false even if Content::equals returns true for
+ * the content of two slots. This may happen if the two slots have different serializations
+ * representing equivalent Content. Such false negatives are considered acceptable. Code
+ * that has to be absolutely sure the Content is really not the same if hasSameContent()
+ * returns false should call getContent() and compare the Content objects directly.
+ *
+ * @since 1.32
+ *
+ * @param SlotRecord $other
+ * @return bool
+ */
+ public function hasSameContent( SlotRecord $other ) {
+ if ( $other === $this ) {
+ return true;
+ }
+
+ if ( $this->getModel() !== $other->getModel() ) {
+ return false;
+ }
+
+ if ( $this->hasAddress()
+ && $other->hasAddress()
+ && $this->getAddress() == $other->getAddress()
+ ) {
+ return true;
+ }
+
+ if ( $this->getSize() !== $other->getSize() ) {
+ return false;
+ }
+
+ if ( $this->getSha1() !== $other->getSha1() ) {
+ return false;
+ }
+
+ return true;
+ }
+
}