ADR-263: The read tools read the run's workspace
- Status
-
Accepted
- Date
-
2026-11-05
- Amends
-
ADR-260 (its consequence "the other read tools still read live only")
- Authors
-
Netresearch DTT GmbH
Context
ADR-260 let a run started in a draft workspace write into
it, and made get_ read the page as that workspace shows it.
Every other tool that reads a workspace-aware table (ctrl.)
still read live rows only, so the model searched, listed and paged through
text the editor no longer saw: read_, search_,
get_, the file tools on sys_ and
sys_, and highlight_.
Two tools did not keep the rule of commit 61ab69eb3 (another workspace's drafts are never read) at all, and a third read live where the editor sees the draft:
get_readrecord_ history sys_by the uid the model named. A version row's uid, or the uid of a record new in another workspace, returned that workspace's changes, values included. Core's history module filters the entries by workspace (history Record, TYPO3 14.3.10 and 13.4.37) and resolves the live uid to the version in the user's workspace (History:: find Events For Record () Record).History:: resolve Element () get_accepted any page uid, so a version row's uid fed that version's TSconfig into the rootline. And the sharedtsconfig Pageoverlaid the rootline with the acting user's workspace even for a run that had not recorded one.Ts Config Reader read_read live only. Core'sfal_ asset_ meta Metaadds aData Repository:: find By File Uid () Workspacefor the context's workspace (14.3.10 and 13.4.37), which drops version rows, so in the backend it returns live values too; the workspace overlay of file metadata happens only for a frontend request (Restriction Filereturns early outside one, 14.3.10 and 13.4.37).Metadata Overlay Aspect:: language And Workspace Overlay () sys_is versioned all the same, and the backend edit form and the workspace preview show the draft.file_ metadata
The overlay get_ uses — fetch, then
Backend each row in PHP — works for one page with a
bounded number of elements. It does not work for a search or a paged list:
a SQL LIKE over live rows cannot find a term that only a version row
holds, and a LIMIT/OFFSET applied before the overlay counts rows the
workspace deleted. Core's Workspace with a workspace leaves
the version rows out (it keeps live rows, records new in the workspace and
move pointers) and expects the caller to overlay after the query.
Decision
One row per record, chosen in SQL. Since TYPO3 11 a workspace version
is a complete row: a change, a move (the version carries the new pid)
and a deletion (t3ver_ 2) are each one row whose t3ver_ names
the live record, and a record new in the workspace is one row with
t3ver_ 0. The rows a user in workspace W sees are therefore a row
predicate (Reads):
- a live row (
t3ver_0,wsid t3ver_0) that has no version in W, checked by aoid NOT EXISTSsubquery that carries only the deleted restriction — a version hidden in W still hides the live row — and - a row of W itself, version or new record, that is not a deletion.
Every filter, LIKE, enable-field restriction, count, limit and offset is
then applied to what the user sees, in the database. A term only the
version holds finds the record; a term the workspace replaced, hid or
deleted does not; rows of another workspace never match.
Page access is the live page's. For a non-admin, whether a page may be
shown is asked of its live row, as core's page tree asks it; a version keeps
the permissions it was created with. read_ and search_
ask it per row through the live uid, get_ too in a draft
workspace.
Live uids. A row is reported under t3ver_ where that is set, its
own uid otherwise — the uid the writers address. A uid filter matches the
record or its version, never a version row by its own uid. A paged query
orders by that live uid.
Whose workspace. The acting user's, and only where the run's actor
recorded the same one (as get_ and the writers of
ADR-260 decide it). A run without a recorded workspace, and
every run in live, keeps the previous query unchanged.
The tools. read_, search_, get_,
read_, search_, get_ and
highlight_ read the rows the predicate selects. get_
accepts a page only where it is a live page, or one the run's workspace
shows, and reads the rootline in the run's workspace
(Page takes it; the writers keep
passing none, which reads the user's). get_ refuses, for
every user, a uid that names a version row or a record of another
workspace; on a workspace-aware table it filters the entries by
sys_ as core's history module does (0, or 0 and the
run's draft workspace). In a draft workspace it reads the entries of the
live uid and of the record's version there together, where core's module
replaces the live uid with the version's (resolve).
Not in scope. get_ keeps its overlay (it reads one page
and has its own tests). site_ answers from the published site:
its indexed_search and ke_search backends index the live frontend, and its
database backend (Database) reads pages and
tt_ with Workspace. resolve_ resolves a URL through the site router, which
routes live pages. The tools on sys_, sys_ and the other
tables without versioning read no versions. get_ and
check_ read sys_, which has no versioning;
the rootline they build for a page is core's Rootline in the
request's workspace (see Consequences). Record and
Batch serve the field fill and the editor-action batches, not
the chat's read tools.
Consequences
✓ In a draft workspace the model lists, searches and pages through the records the editor sees there, under the uids the writers take.
✓ A version row's uid and another workspace's draft are refused by
get_ and get_, also for admins and also in
live.
✗ get_ in live now leaves out entries sys_
stores with a draft workspace, as core's history module does.
✗ get_ and check_ (admin only) build the
rootline with core's Rootline, which reads pages in the
request's workspace, not the run's, and accept a version row's uid as the
page. What they return is TypoScript from sys_; a page's draft
only changes which conditions match.
✗ A record deleted in the draft workspace is left out, as the workspace
preview leaves it out; the backend list modules show it marked as deleted.
The records on a page deleted there get no version of their own, so
read_ and search_ still return them, as core's backend
search does; get_ no longer shows the page.
✗ The NOT EXISTS subquery runs per candidate row; it uses the
t3ver_ index core defines on every workspace-aware table.