ADR-274: The replacing writers replace a reference in the workspace
- Status
-
Accepted
- Date
-
2026-11-08
- Amends
-
ADR-264 (its refusal of a draft workspace before the card for
set_andpage_ social_ image replace_, the consequence that both write in live only, and the consequence thatfile_ reference attach_asks the inline owners live) and ADR-262 (its consequence thatfile_ to_ record set_asks the inline owners live)file_ reference_ alternative_ text - Authors
-
Netresearch DTT GmbH
Context
ADR-264 let the writers that add a file reference write it
in the draft workspace a run was started in, and kept the two writers that
REPLACE one refusing a draft workspace before the approval card:
set_ sets pages. or pages.,
deleting the image there with replace; replace_ puts a
new reference in the place of one on a content element, or removes it. Each
deletes a reference, and in a workspace the delete has to name the right uid,
or it discards a draft instead of deleting a record. An editor in the
workspace "Entwurf" met that refusal on 2026-11-06.
Neither tool writes a file. Both read sys_ and write
sys_ and the parent's field, and core versions both tables
and pages and tt_. So nothing of these two calls is beyond
TYPO3's workspaces.
What core does with the delete, read in Data of
TYPO3 14.3.10 and 13.4.37 (the dispatch is the same in both):
- a uid of a row whose
t3ver_is not 0 — a version, or a row new in the workspace — is discarded (wsid discard): the row is removed. Since 14.3.11 this holds only for a row new in the workspace, a move version and a delete placeholder; a version in the default state of the user's own workspace stands for its live record there, and core deletes the live record, which turns the version into a delete placeholder;() - a live uid whose record has a version in the user's workspace in the
default state turns that version into a delete placeholder
(
t3ver_2) and deletes the record's translations the same way (state delete);L10n Overlay Records () - a live uid without a version gets a new delete placeholder
(
versionizewithRecord () $delete), its translations too.
Measured on 2026-11-08 against TYPO3 14.3.10 (SQLite), with the tools as below, as an administrator — with the global backend user in live as well — and, for the social image, as an editor who holds PAGE_EDIT and not CONTENT_EDIT on the page:
- making the parent's version first versions its references; the field named
by the versions' uids and a delete of the replaced reference by its LIVE uid
leave the workspace showing exactly the new image at the old one's place,
the replaced one marked for deletion there, and live unchanged but for the
tstampandsorting_core writes onto the live references when it versions their parent (ADR-264);foreign - a delete by the uid of the reference's version discards that version: the old image shows again in the workspace beside the new one;
- a translated page that follows its parent's image gets a copy of the new image and its copy of the old one marked for deletion, in its own version; the translated overlay of a removed content-element reference is marked for deletion with it, and the translated element's version counts what it still shows;
- a reference new in the workspace, one the assistant set there earlier, is discarded by its own uid: it never was live.
Decision
Both tools write in the run's draft workspace, as the attaching writers
do (ADR-264): they refuse a draft workspace only where the
run did not record it (ADR-260), or where an installation
took the versioning off a table they write — pages or tt_, and
sys_ (refuse,
ADR-262) — before the card. execute, the card and the
refusal run in the acting user's workspace (Asks,
ADR-266).
The parent's version first. The page or the element gets its version
with its own version command before the write
(version). The datamap then names the parent's field with
the rows that hold its references in the workspace — the versions — and the
new reference is new in the workspace.
A delete names the uid a reference is addressed under: its live uid, so core marks it for deletion, or the uid of the row new in the workspace, so core discards a draft that never was live. Never the uid of a version.
Everything around the write reads the workspace: the page as the
workspace shows it with PAGE_EDIT asked of the stored row
(page), the element and its page, the reference, the
references of the field and their translations, the page's translations, the
read-back of the new row and of the parent version's counter. Another
workspace's rows are not there, and a version addressed by its own uid stays
refused as not found. Before replace_ updates an element,
core asks the element's STORED row — without an edit lock set on its draft
alone, and on the page it sits on live — and the tool asks the same row, as
the attaching writers do
(Attaches); the page the element shows on in the
workspace, which takes the new reference, is asked as well. The translated
elements whose overlays core deletes along are asked by their stored rows
too, and the records that hold them as inline children
(ADR-230), as for the element itself. Those owners and their
pages are read in the workspace as well — the live row, or the row new there,
stored and without the overlay, while the next owner up the chain is found
through the owner's row as the workspace shows it. An owner other than a page
moved in the workspace is asked on both pages: the one it left, where it stays
live until the workspace is published, and the one its move version holds —
either one the editor may not edit content on refuses. A page that holds the
reference is asked as the page itself, PAGE_EDIT and CONTENT_EDIT on its
stored row, not on the pages above it. This holds for
replace_,
attach_ and set_, which
share the check (Asks). Read live, an owner new in the
workspace was not found: the call was refused as an orphan's, and where a
second inline relation claimed the same pointer column without
foreign_, a live record of that other table with the same uid
counted as the holder, so the owner the editor may not change was never asked
and the card was shown (#1325). In the same way
set_ asks PAGE_EDIT of each translation's stored row,
which is what core asks when it saves the translations with the page.
A write that fails is taken back: the version this call made is
discarded, and with it everything the call wrote below it. This holds for
every failure: a read-back that does not match, an old reference the delete
did not remove (replace_ as set_), and
an exception thrown half-way, which leaves the tool after the take-back —
also one thrown by a hook while core makes the version, before the call
learns its uid. Where the parent had a version already, or is new in the workspace, a
read-back that does not match puts the parent back instead — the new
reference discarded by its uid and the field named by the rows it held
before — and an old reference the delete did not remove is handled as in
live: set_ puts the page back, while
replace_ answers that the element carries both and one
has to be removed by hand. An exception there takes
back the rows the write added by the same reading of before, described next,
and keeps the draft that was there.
Where the call made the version, the take-back works from a reading of the
workspace taken just before that version was made
(Creates): the rows
there of the record and of its translations — versions, delete placeholders,
translations new there — and the rows there of their inline children, file
references among them, and of the children's own inline children, four levels
down: an image of a content element that a news record holds is a draft two
levels below the record. The same code serves attach_
and attach_. Each step was measured on TYPO3 14.3.10 and
13.4.37:
- Every row there afterwards that was not there before is this call's, and is
discarded with core's
discardcommand on 14.3 —deleteturns a version of the default state into a delete placeholder there since 14.3.11 instead of removing it — and withdeleteon 13.4, which has nodiscardcommand and whosedeletestill discards: the children first, then the record's version and the drafts core made of its translations in the same run — a translated element's version and the delete placeholder of its overlay reference (delete), a translated page's version and its copies of the images. Core's discard of the record's version does not reach the translations' drafts: it looks for translations by the version's uid (L10n Overlay Records () discard), and a translation's version points at the live record.Localization Overlay Records () - Every row that was there before is kept. Core's discard of the record's
version takes the versions of its inline children along
(
discard), whoever made them; an alternative text an editor saved on one reference of the element is a draft of that reference without a draft of the element, and the discard of the version the call made for its write deleted it. The take-back's DataHandler is told the rows of before, by their own uid and by the live uid they are the version of, and skips them (Record Relations () Tool).Data Handler:: keep In The Workspace () - The discard runs with the parent's table in its datamap, which is never
processed. Core discards a file reference only with the right to update
one (
Data): CONTENT_EDIT, unless aHandler:: has Permission To Update () pagesrow is in the run, when it is PAGE_EDIT — the right the write was made under. Without it, for an editor who may edit a page's properties and not its content, core discarded the page version and left the new reference and the version of the page's live image in the workspace. - What is left is read from the rows and compared with the reading of before, column by column for the state, the pointers, the file and the texts of each row. Only where they match does the answer say the workspace was left as it was. Otherwise it names each row that differs — made by the call and still there, there before and gone, there before and changed, and which columns changed, without their values, which are an editor's content — and sends the editor to the Workspaces module.
A row of before that the failed write CHANGED stays changed. The case met is a translated page whose translation already had a draft of its image copy, made by an editor's save of the translation's title: core's synchronisation turns that draft into a delete placeholder, and the row keeps its uid. Putting it back would take discarding the editor's draft, and the take-back never discards what was there before; the answer names the row and says that publishing the workspace would delete the copy live.
The card and the answer name the workspace. Both cards carry the line of
ADR-260. Where set_ replaces an image
in a draft workspace, its warning says the image is removed from the page in
the workspace, live keeps it until the workspace is published, and an image
that exists there only as a draft is discarded — instead of the recycler,
which does not hold workspace rows. Where replace_ replaces
or removes a reference that exists only as a draft there, its card says the
reference is discarded with its texts and crop and the recycler does not
keep it. Where the page has translations, the card of
set_ counts those that follow the page's image and get
the new file too, and in a draft workspace those that get a draft of their
own: core saves every translation with the page there, one that keeps its own
image as well (measured on 14.3.10 and 13.4.37); the technical details name
them. A translation follows when its l10n_ for the field is
"parent", or "source" with a source translation that follows itself: core
synchronises a "source" field from the translation it was translated from,
not from the page (Data), so one whose
source keeps its own image, or that names the page as its source, keeps what
it has. In a draft workspace core reads that state from the translation's
stored row, the live one where the translation has a draft, so the card
does too (measured on 14.3.10). Where an installation sets l10n_
exclude on the field, core copies the page's image to every translation
whatever its state (Data),
and the card counts every translation (measured on 14.3.10, live and in a
draft workspace). A card names a content element without a header by its content type
and UID (Approval, on every card
that names a content element): "(empty)" there read as an element without
content. The UID sits on the editor's line only for such an element, which
has no header to be named by; the technical details keep it as before
(ADR-213). The answer ends with the draft note of
ADR-262.
An element moved in the workspace is refused. Core's move command
gives the element a move version there (t3ver_ 4) and moves each of
its references along with their own move versions. A delete of such a
reference by its live uid makes core discard the reference's move
(Data, 14.3.10 and 13.4.37: a version that is not
in the default state is discarded), and core has no command that deletes a
moved reference and keeps the move. Measured with a real move: a
replacement left the new reference beside the old one and the old
reference's move discarded, and a removal left the element counting one
reference while two showed. replace_ therefore refuses an
element that has a move version in the workspace — and a translated element
moved on its own, whose overlay of the reference core deletes along, and a
reference moved on its own with core's move command, which leaves its
element as it is (measured) — before
the approval is asked (refusal, as ADR-266
refuses what core does differently in a workspace), for any user, and says to
publish or discard the move first. A page moved in the
workspace is not affected: its references keep the page as their pid, so
only the page gets a move version, and set_ writes onto
it as onto any draft (measured).
Removing a draft reference is not refused. ADR-266
refuses delete_ of a record new in the workspace, because core
discards it beyond the recycler. A reference new in the workspace is the
image the workspace itself set — typically by an earlier call of these tools
or of the attaching writers — and replacing it again is the editor's
ordinary next step; the card says it is discarded.
Consequences
✓ An editor in a draft workspace can let the chat set or replace a page's social image and replace or remove an image on a content element; each change waits in the Workspaces module, and live stays as it was.
✓ Every writer that writes pages and content works in the run's draft
workspace; only the redirect writers refuse it (sys_ is not
versioned).
✗ Live is not untouched to the byte: versioning the parent writes tstamp
and renumbers sorting_ on its live references, as an editor's own
save in the workspace does (ADR-264). On TYPO3 13.4, where
the page's translation follows its image, core's synchronisation of the
translation also moves the translation's LIVE copy of the image from the
default-language page's pid to the translation's own uid when it versions the
translation; the tools name no pid of that row. 14.3 leaves it (measured in
the functional tests of both majors). Which file the copy references, its
translation pointer and its order stay. Taking a failed write back does not move it back.
✗ A reference new in the workspace that a call replaces or removes is discarded, not kept in a recycler, with its texts and crop; both cards say so. The removal's write target names that reference, and since its row is gone, the observed outcome of the run (ADR-185) reads as discarded although nobody reversed it.
✗ replace_ cannot change the images of an element moved
in the workspace until the move is published or discarded.
✓ The records that hold an element or a record as an inline child
(ADR-230) are asked as the workspace shows them, by
replace_, attach_ and
set_ alike: an owner that exists in the
workspace only is asked like a live one, and a live record of another table
with the same uid no longer stands in for it. set_
still reads the page of the reference's own record live.
✗ A failed write does not always leave the workspace as it was. A draft that was there before and that the write changed is named in the answer and left for the editor, never discarded (see above). The reading of before covers the record, its translations and four levels of their inline children, found by the pointer each child has in the workspace: a child whose draft points at another parent, or an overlay of a reference that sits on an element that is not one of the record's translations, is not in it. Where the write throws, the take-back runs and the exception leaves the tool; what the take-back could not put back is not in the answer then, since there is none. A row another session makes in the same workspace around the same record while the call runs is not in the reading of before, so a take-back in that window counts it as the call's and discards it; TYPO3 itself does not lock a record against two editors either.
✗ The translations were measured on TYPO3 14.3.10 locally and on the 13.4 line in the functional tests CI runs; the dispatch of the delete reads the same in both.