Requests for comment/Multi-Content Revisions

This page is a full description for "Multi-Content Revisions", an RFC tracked as T107595.

Multi-Content Revision support (also MCR or MuCoRe) allows multiple content "slots" per revisions (comprising multiple "streams" per page). Each slot has a specific role (revision+role are unique), and typically each slot has a different content model. Some slots (called "primary" slots) are user editable, while others (called "derived") are maintained automatically. Please refer to the Glossary for a definition of the terms used in this document.

On an abstract level, the need that drives MCR is allowing a joined history (and atomic edits) of multiple documents that are logically bound to a page. For example, a template page may have additional "slots" that contain the template documentation, associated CSS, and a schema for the parameters. With MCR, all parts of the page (logical documents or "content objects") can be edited at once, creating a single new revision in a shared page history.

On the technical level, this translates to introducing a level of indirection between the revision and the content object, so multiple content objects can be managed per page revision. The concept of a wiki page changes from a sequence of revisions of a single document to a sequence of revisions of multiple documents (streams of slots). For simplicity and compatibility, each revision must have a "main" slot that corresponds to what is without MCR the singular page content.

Introducing MCR support does not change how content is stored (it continues to be treated as blobs, and use the text table or External Storage), although the PHP interface for blob storage will likely need to be refactored. Introducing MCR also does not change how derived index information, such as the imagelinks or page_props table, are managed and used. MCR does change how and where content meta-data is stored (the content model, format, hash, etc).

System Design
A top-down view of the service architecture for MCR support: More detailed design documents can be found on the following sub-pages:
 * Application logic uses a RevisionLookup service to access revision (resp page) content. Some of the database level code currently in WikiPage and Revision will move there.
 * Application logic uses a PageUpdateController to create new revisions (to update pages). Much of the update logic currently in WikiPage will go there.
 * The existing Revision class will continue to provide read and write access to the main slot, implemented based on RevisionLookup and PageUpdateController.
 * RevisionLookup uses a RevisionSlotLookup to load revision content meta-data, and a BlobLookup to load the serialized content. A ContentLookup layer may be added between RevisionLookup and RevisionSlotLookup to support structured storage and virtual slots. Access to the revision table may be implemented directly in RevisionLookup, or abstracted into a DAO.
 * PageUpdateController uses a RevisionSlotStore to write revision content meta-data, and a BlobStore to store the serialized content. A ContentStore layer may be added between the PageUpdateController and the BlobStore, to provide structured storage. Access to the revision table may be implemented directly in PageUpdateController, or abstracted into a DAO.
 * BlobStore and BlobLookup are initially implemented based on the text table and the External Storage logic. Much of the logic in Revision that currently implements access to revision content will move here.
 * Note that BlobLoockup and BlobStore will typically be implemented by a single class, as will RevisionSlotLookup and RevisionSlotStore.
 * Content Meta-Data Storage defines a DAO layer for content meta-data. It is the preferred way to access the content table.
 * Blob Storage defines a generic mechanism for storing and retrieving arbitrary data blobs, similar to ExternalStore.
 * Transaction Management specifies a generic transaction management interface.
 * Revision Retrieval Interface defines a (lazy loading) RevisionRecord object that provides access to revision meta-data as well as the Content objects of each slot, based on the Content Meta-Data and Blob Storage interfaces.
 * Page Update Interface uses the Content Meta-Data Storage and Blob Storage components to provide a builder object for new revisions which is responsible for managing the transactional context of the update.
 * The legacy Revision and WikiPage classes uses the Revision Lookup and Page Updater components to retain the legacy interface for the main slot.
 * Rendering and Parser Cache needs to be aware of slots. The web cache will be for a rendering of all slots of the current revision.

External Interface Compatibility
Support for multi-content revisions will have to be integrated into the external interfaces (both UI and API).
 * User Interface
 * Views (view, edit, diff, etc)
 * Web API
 * Dumps (Import/Export)

Implementation Roadmap
The actual scheduling of implementation steps will depend on available resources (that is, programmer time and mind share). For now, this section documents the dependencies of the relevant components.
 * Content Meta-Data Storage is the structural heart of MCR. It can be implemented without any dependencies, though having the new BlobStore service in place would be useful. Introducing the new meta-data storage requires a substantial schema change. All actual MCR functionality is blocked on this being implemented, but preliminary implementations that support only the main slot can be created before content meta-data storage is in place.
 * Blob Storage is a refactoring of existing functionality. It depends on none of the other components. Optional features that can be added at any later time: transaction support, ExternalStore integration, and structured content storage.
 * Transaction Management is highly generic and depends on none of the other components. Other components do not need to implement support for transactions from the start, though introducing such support will mean a breaking change to some method signatures (in particular for BlobStore and RevisionSlotStore).
 * Revision Retrieval Interface depends on content meta-data storage and BlobStore for full MCR functionality. A baseline implementation could however use existing code to provide access the the main slot only.
 * Page Update Interface depends on content meta-data storage and BlobStore for full MCR functionality. A baseline implementation could however use existing code to provide access the the main slot only.
 * Views and web API with MCR support need the high level revision retrieval interface, editing needs the page update interface. Read support for MCR should be enabled before write support. UI support should probably be available before API support.
 * Dumps (Import/Export) with MCR support needs to be available before MCR support is enabled for UI and API.

Use Cases
The ability to manage different independent content objects (documents) on the same page, is useful for several things (given in no particular order):
 * Manage categories as structured data. Compare T87686.
 * Structured "media info", especially license data, associated with a file description page, in addition to the free form wikitext. This would replace the template on commons.
 * Managing file upload history. Managing meta-data about uploaded files along with the wikitext would remove the need for an oldimage table (see Requests for comment/image and oldimage tables). This would however require quite a bit of refactoring.
 * Article quality assessments ("needs love", "featured", etc)
 * Blame maps (as derived content) that record which part of an article was added by which user.
 * Rendered HTML (as derived, possibly virtual "on-thy-fly" content). This would require the ability to update derived content without creating a new revision, on "events" like a template update. It would also require "sub-slots", since different HTML may be produced for different devices or target languages.
 * Template schema information (aka template data), instead of embedding XML in wikitext.
 * Template styles: the CSS used by a template would live in a separate slot, but could be edited along with the template.
 * Template documentation (instead of using a subpage)
 * Gadget styles (instead of having a separate .css page)
 * Lua module documentation
 * Infobox data. Removing the infobox parameters from the wikitext allows the infobox to easily be stripped, or shown separately, or formatted according to the target device.
 * OCR text from DeJaVu and PDF files (as derived content)
 * Workflow state

Rationale
The above use cases indicate that there is a need for bundling different kinds of information on a single page, to allow atomic editing of the different kinds of content together, provide a shared version history, and allow the different kinds of information to be watched, protected, moved, and deleted together. That is, a revision can have multiple "slots" containing different kinds of data, each having a unique role. The requirements for each slot correspond to what is already defined in the Content interface: we need to be able to store and load, import and export, render, edit, and diff them.

To address this need, MCR introduces an indirection between revisions and content objects, changing the relationship from 1:0 to a 1:n (or m:n, if content meta-data can be re-used by multiple revisions). This requires a substantial refactoring in the storage layer of MediaWiki, and some changes to the database schema. By requiring all revisions to have a "main" slot which will be used as the default in many situations, we can achieve interoperability with code that does not know about MCR.

An alternative approach discussed earlier was Multipart Content: Multiple Content objects would be coded into a single meta-objects, a multipart content object, using a mechanism similar to the MIME-Multipart mechanism used for email attachments. This however has two major disadvantages: performance and interoperability. The performance is not optimal, since the entire content blob must always be loaded to access any part of the content. Interoperability is not optimal, since it is unclear how external interfaces like the web API should behave to be backwards compatible: should they return the entire multipart object as the revision content, or always the main part of the content? The former breaks code that manipulates the content, while the latter causes data loss with code that treats content as opaque blobs. MCR faces a similar problem, but allows the new structures to be made explicit in the API response data and request parameters, instead of requiring clients to decode nested blobs.

Another advantage of MCR over the Multipart approach is flexibility. Derived content can be updated or added to revisions after the time of creation. Also, different kinds of content can use different optimized blob store mechanisms.

For these reasons, MCR was identified as the preferred approach.

Streams

 * Streams are not modeled directly, they exist purely as the history of a given slot for a given page and can be referenced by page title(+namespace) and slot name.
 * TBD: we may need a "closed" representation in a single string, for use in wiki links. For streams, we could use "namespace:title##slot" or "namespace:title||slot".
 * Which slots (resp. streams) are available for a given page may be configured per namespace, or per content model of the main slot (which in turn would typically be based on the namespace). The details of this are TBD.

Virtual Slots

 * Virtual slots will need to be implemented at a structured storage layer placed between RevisionLookup and the BlobLookup.
 * Virtual slots typically need access to the slot name and to the slots of the revision (per definition, virtual content is derived from other content). Perhaps it should have access to a RevisionRecord.
 * Some virtual slots may need access to data in the parent revision (e.g. for blame maps and diffs).
 * Virtual slots should have a way to signal whether the generated content should be stored (as persistent derived content) once it has been generated, or not. Virtual slots that do not allow generated content to be stored are considered "volatile". There could also be an option to persist virtual content only for a limited time ("cacheable").
 * Straw man interface: . Caution: accessing slot content via $revRec may cause other virtual slots to be triggered. Circular dependencies must be avoided.

Sub-slots

 * We may want to model sub-slots and/or sub-revisions.


 * Sub-slots: Slot names may be suffixed to allow multiple "sub-slots", all with the same storage backend and content model. E.g. there could be an "html" slot with one sub-slot per language, e.g. "html.de", "html.nl", etc.
 * Sub-revisions would model "events" such as a template bein updated.

Architecure Overview (old)
Note: primary (user generated) content slots must be enumerable. Which revision has which primary slots is recorded in the database. Secondary (derived) content slots may also be persistent in the database, but can just as well be purely virtual. As a point in case, we'd want a) a ParserCache implementation based on persistent derived slots as well as b) a virtual slot implementation based on the existing ParserCache.
 * PageStore -> create/update/delete pages. Uses RevisionStore. Does all the secondary data update stuff.
 * RevisionStore -> returns RevisionBuilder; Caller adds RevisionSlots and meta-data to RevisionBuilder
 * RevisionBuilder maintains transactional context. Needs to be aware of base rev id for "late" conflict detection!
 * late add support for RevisionUpdater, for updating persistent derived revision data
 * RevisionLookup returns RevisionRecord objects; LazyRevisionRecord for lazy loading?
 * RevisionRecord can enum RevisionSlots for primary content. LazyRevisionSlot for lazy loading of content.
 * RevisionSlots has Content and meta-data (size, hash, content model, change date, etc); Do we need a RevisionSlotLookup/RevisionSlotStore?
 * Primary content implements Content. Derived content implements Data(?!); Content extends Data.
 * RevisionStore/RevisionLookup is based on BlobStoreMultiplexer. Read/write is routed based on a prefix in the blob id.
 * BlobStoreMultiplexer manages multiple BlobStores
 * RevisionStore turns blobs into ContentObjects and creates RevisionSlot and RevisionRecord objects from them (or creates a LazyRevisionRecord that loads data on demand)