dbdoctor for agents
This guide is for coding agents that help a developer judge what dbdoctor: found in a TYPO3 instance. It describes the JSON report of dbdoctor and a way through it that keeps the number of calls and tokens low. The report is experimental: While its schema is 0.x, its structure may change with any release. Check classes and their order are no API at all.
The goal is a verdict the developer can trust: Which findings are harmless, which need their eye, and why.
Rules
- Read only. Use
--, nothing else. Never runmode check -- format json --or the interactive mode, never change the database yourself. Fixing is the developer's decision.mode execute - Prefer
--, see below. On stdout, always addoutput <directory> --: The report is about half the size without whitespace.compact - The default
--has uids, pids, TCA control fields and timestamps, no content.tier structure --adds titles and other TCA label fields, cut to 80 characters. They help to identify records, but they are content of the instance: Ask the developer before using them.tier labels - Run dbdoctor with the TYPO3 CLI of the instance from its project root, for example
vendor/orbin/ typo3 bin/: TYPO3 finds its installation relative to the current directory. In a ddev project, usetypo3 ddev exec vendor/; a relativebin/ typo3 --directory is then relative to the project root in the container, which is the project directory on the host as well. A run takes seconds on small and minutes on large instances.output -fis the SQL log of execute mode, not a report file. - The exit code is the one of check mode: 0 all ok, 1 findings, 4 error. Errors in options come as JSON with an
errorobject.
How dbdoctor works
dbdoctor is a chain of checks, run in a fixed order. Each check finds records that violate one rule, and knows exactly one fix per finding: There are no alternatives to choose from. Later checks rely on earlier checks having fixed their findings: A record an early check deletes is no finding of a later check any more, a record an early check moves gets new findings later. The developer runs dbdoctor until check mode is green, a single run does not guarantee that.
This is why only the first check that is not ok has final results. Each check has a status:
ok: No findings.findings: Something to fix, the check has counts, facets and findings.skipped: The check ran into an unfixed finding of an earlier check it relies on, seeskip.Reason not-: With the defaultrun --, checks after the first one that is not ok do not run.scope until- first- finding
With --, all checks run, and every result after the first check that is not ok is preliminary: true. Use it for an overview, and keep in mind that preliminary findings may vanish or change once the earlier checks are fixed. A preliminary finding is likely to stay when its records are not among the records the earlier not ok checks change.
Workflow
Write the report into a directory once, then read its files in steps. Each question is answered from the same snapshot, and nothing has to be read at once:
vendor/bin/typo3 dbdoctor:health -m check --format json --scope all --output var/dbdoctor/report-1
The directory must not exist yet. stdout names the index file. The files contain record data: Keep them out of public folders, and remove the directory when done.
- Overview: Read
index.: run data, sites, and per checkjson position,class,status,preliminary. Checks with findings havetitle,counts,fileandfindings. Note the first check with statusFile findingsorskipped: Only its results are final. - One check: Read its
file: description, counts, facets and groups. Facets and groups cover all findings. - Samples: The
findingshas all findings of the check, one JSON object per line. Each group names a few finding ids asFile samples:grep '"id":"0123456789abcdef"' checks/. Search for other findings by table, uid or reason code, for example55-…findings. jsonl grep -orm 5 '"code":"parent- missing"' checks/ 55-…findings. jsonl grep '"table":"tt_. A findings file of a few findings is quickest read completely; do not read a large one completely.content","uid": 123,' checks/*. findings. jsonl pages.has the rootline of each page with findings, one page per line:jsonl grep '^.{"uid": 42,' pages. jsonl tables.has the TCA roles, row count and inline parents of each table, one table per line:jsonl grep '^.{"table":"tt_ content"' tables. jsonl - Judge groups of findings, see below, and repeat 2. and 3. for the next check.
Without the option to write files, the same works on stdout with smaller calls: -- is a summary of all checks without findings, -- (repeatable, short class name works) reports one check with up to -- findings (default 100), -- leaves out rootlines and related records. --, -- (with --), -- and -- narrow findings down, counts and facets are then of the matching findings, counts. has all findings of the check, and a check without matching findings is listed with filter. For a check after the first one that is not ok, add --, otherwise it is not-. -- is a summary, not "unlimited". The filters work with -- as well.
Reading a check
title,description: What the check looks for and what its fix does. Read the description before judging, it often says why a fix is safe.tags: Kinds of changes the check does:remove(delete rows),soft-,delete workspace-,remove update-,fields insert, andriskyfor checks whose fix may change what the frontend shows.key: What a finding is about:Types record(one row),record-(one field of a row, a record may have several findings),field mm(rows of an MM table of one local record).counts: Four numbers in different units.findings,records(distinct records),writes(single changes), and per table inbythe affectedTable rows, theirshareof the table andsuspicious. MM findings have no record: They count infindingsbut not inrecords, and one MM finding (all rows of one missing local record) can stand for manyrows.-
facets: Counts over all findings of the check, also when findings are cut by--.limit hidden,languageandtstampcount only findings with a record,Age rootonly findings with a page, so their sums can be lower thanPage findings.operation: Changes per operation:update,soft-,delete delete,insert,mm-.delete reason: Findings per reason code.state: The record islive,deleted(soft-deleted),workspace(a workspace version), ornone(MM findings have no record).state: Operations per state.By Operation hidden,language,tstamp(Age <30d,<1y,<5y,older,none).root: The site root page above the record's page,Page noneif the page is in no site.write: Distinct kinds of changes: table, field and the kind of the new value:Patterns 0,1,empty,role:<role>(the value of another field of the record, for examplerole:), orl10n Parent value.
-
findings: Each withid: Stable for the same check and key.key:type,tableanduid(andfield), or formm:mm,Table uid,Local local,Table fieldandmatch.Fields pid: The page of the record.reason:codeandparametersname the cause and the related records, for exampleparent-with the uid of the parent.missing messageis prose for humans, do not parse it. Some checks have no reason (null,nonein the facet): The check rule itself is the reason.evidence: Extra data of some checks, like the number of MM rows.record: uid, pid and TCA control fields under names that are the same for all tables:deleted,hidden,starttime,endtime,language,l10n,Parent l10n,Source type,tstamp,crdate,cruser, andId workspacewithid,stateandoid. Only fields the table has. Workspaceid0 is live.stateis the core's t3ver_state: 0 a changed version of the live recordoid(or a live record), 1 a record new in the workspace, 2 a delete placeholder, 4 a move pointer.hiddenis the field value: A workspace version markedvisibleis still not shown to visitors before publishing.writes: The changes dbdoctor would do:seq(their order within the check),operation,table,uid, for updatessetwitholdandnewper field,valuesof an insert,whereof an MM delete. A list inwherematches any of its values.context: The language parentl10nand translation sourceParent l10nof the record,Source missingif they do not exist. The translation source is left out when it is the language parent.
groups: Findings of the sametable,reason,state,hidden,operation(the operations of a finding joined with+), andin(whether the page is in a site), withSite countand up to three finding ids assamples. Largest groups first.hiddenisnullfor tables without hidden field and for MM findings,infor findings without page.Site pages(top level,pages.withjsonl --): Per page of the findings itsoutput rootlinefrom the tree root down with thedoktypeof each page (1 standard, 254 folder, 4 shortcut, …), pages flaggeddeleted,hidden,missingor withworkspace, itsroot, the root page of its site orPage Id null, and itstranslations: uid and language of each page translation, flaggeddeleted,hiddenor withworkspace.tables(top level,tables.withjsonl --): Per table of the findings and changesoutput row, theCount roles(role name to field name of the table, to map the record names back to columns), andinline: All tables and fields that can own records of an inline child table, with their foreign field and match fields.Parents sites(top level): Root page and languages of each site. Languages other than the default one havefallbackandType fallbacks: Withstrict, a page without translation in this language is not available in it; withfallback, it is shown in the first fallback language that has a translation, with the content of that language;freeshows records of the language only.
Judging
Judge groups, not single findings: A check with tens of thousands of findings has few groups. Give each group a verdict, name the group values and facets it is based on, and look at the samples of each group before concluding. Groups do not split by field or write pattern: When write of a check show different fields, look at samples of each.
- Frontend impact is what the developer cares about most. dbdoctor tries to keep the frontend unchanged, the README section "Impact on Frontend rendering" explains where it can not. Findings on records that are soft-deleted, workspace versions, or on pages outside of all sites (
rootPage none) do not change what visitors see when fixed. Hidden records and records with a pastendtimeusually do not either. - Operations on live records are the interesting part:
stateshows if deletes or updates hit live records. An update that fills a field from another field of the record (By Operation role:...patterns) is usually a consistency fix; a delete of a live, visible record on a page of a site needs a look at samples. - Few write patterns mean a uniform fix: One pattern over thousands of findings is one decision.
- Age: Old untouched records are often leftovers of old imports, migrations or extensions. Recent ones may point to a bug that still produces broken records: Worth telling the developer, a fix alone will not stop it.
suspiciousis a hint, not a verdict. It marks a table where a check flags more than half of the rows, and more than 100. That often means the check and the TCA disagree, for example an extension uses a table in a way dbdoctor does not expect, and the findings are no broken data. It can be valid as well, for example an MM table whose rows mostly point to records that were deleted. Look at samples and the reason parameters.- Inline children: Which records may own a child is in
tables.<table>.. A child whose parent is missing according to one parent table may belong to another one: Check the match fields.inline Parents - Content of a language on a page: Localized content is shown only on pages available in its language.
pages.<pid>.and the fallback of the site language tell whether it is: A content element in language 1 on a page without translation 1 is not shown in language 1, the page is not available or shows the fallback language instead.translations - Translations and workspaces:
context.,l10n Parent context.andl10n Source record.often explain a finding without further queries, for example a language parent pointing to a workspace version instead of a live record. Deleting workspace records does not change the live site, but loses unpublished work: Records new in a workspace (state 1) are gone for good.workspace - Which side does the fix trust? A check that updates fields keeps one field and derives the other from it, the
writesand the description tell which. When the trusted field looks wrong itself, the fix makes the record consistent but keeps the actual error: Tell the developer, with the field that looks wrong and why. - A check may be wrong. dbdoctor checks are rules derived from TCA; unusual setups break them. If samples of a group look like valid data, say so and describe why, with record keys. Do not suggest code changes, the maintainer of dbdoctor prefers a precise description.
Verdict
Hand the developer a short report per check with findings:
- Check, status, whether it is preliminary, counts.
- Groups, each with a verdict: harmless (fixing changes nothing visible), needs a look (fix may change the frontend or loses data someone may need), or check seems wrong (findings look like valid data).
- The basis of each verdict: facet values and the samples looked at.
- The single records that need the developer's eye, with table, uid, page and why.
- Causes that probably still produce broken records, when groups have recent records.
The same record can be a finding of several checks, for example a file reference of a missing record is dangling and an inline child without parent: Count it once when summing up. Numbers in the verdict come from the report, never estimate them.
Querying the report files
The workflow above needs no tool besides reading files and grep. For other questions, any JSON tool works. jq is very useful when installed, for example in a report directory:
# Checks that are not ok, with their number of findings
jq -c '.checks[] | select(.status != "ok") | {position, class, status, preliminary, findings: .counts.findings}' index.json
# Key, reason code and changes of each finding of a check
jq -c '{key, reason: .reason.code, writes: [.writes[] | {operation, table, uid, set}]}' checks/55-GroupFieldRelationMissing.findings.jsonl
# Findings on live records only
jq -c 'select(.record != null and (.record.deleted // 0) == 0 and (.record.workspace.id // 0) == 0) | .key' checks/55-GroupFieldRelationMissing.findings.jsonl
# Findings per page
jq -r '.pid' checks/55-GroupFieldRelationMissing.findings.jsonl | sort | uniq -c | sort -rn | head
PHP is always available on a TYPO3 host and works as fallback:
php -r 'foreach (file($argv[1]) as $line) { $f = json_decode($line, true); if (($f["record"]["deleted"] ?? 0) === 0) { echo json_encode($f["key"]), PHP_EOL; } }' checks/55-GroupFieldRelationMissing.findings.jsonl