Skip to content

feat(objects): read an object's definition text on fifteen type-ids (#789) - #820

Merged
cevheri merged 94 commits into
mainfrom
feat/object-source-phase-2
Sep 13, 2026
Merged

feat(objects): read an object's definition text on fifteen type-ids (#789)#820
cevheri merged 94 commits into
mainfrom
feat/object-source-phase-2

Conversation

@cevheri

@cevheri cevheri commented Sep 13, 2026

Copy link
Copy Markdown
Member

Phase 2 of #789: every object that HAS a definition text can now be read, and no surface can show an empty editor over a definition it failed to read.

What a user can do that they could not

Open a view, a routine, a trigger, a package, a dictionary, a search pipeline or a Redis function library from the object tree, by the row menu or by activating the row, and read its definition in a read-only Monaco tab with a caption that says what the text IS: stored as submitted, rebuilt by the engine from its catalog, or a structured definition rendered as JSON, and whether it is a complete statement or only a body. Where the engine refuses, the tab shows the engine's own sentence instead of an editor. Tabs carry the object's ADDRESS, so a reload re-reads rather than restoring stale text, and a Source tab offers no Run anywhere.

Fifteen type-ids read source. druid and libredb deliberately read none and their provider docs say which absence that is. The census (tests/isolated/object-source-declarations.test.ts) builds every provider through the factory and pins 58 declared triples, 60 on a MariaDB connection, so the number cannot drift from the code.

Measured facts a reviewer would not expect

  • plsql, tsql and cql are not Monaco language ids. The installed monaco-editor 0.56.0 registers 89 ids and none of those three is among them; an unregistered id degrades to plain text with nothing thrown. Every declared sourceLanguage is pinned against the real editor in tests/isolated/monaco-language-ids.test.ts, and the compromises are named in each provider doc.
  • JSON.stringify silently loses a regular expression. A MongoDB pipeline holding one renders as {"$regex":{}}. That is a structural loss shown under a caption saying "complete", so the MongoDB read uses EJSON.stringify relaxed and renders the whole options document, not the two fields the design named.
  • PostgreSQL's pg_get_* family applies no privilege check at all (probe 1, PostgreSQL 18.4): a role holding no USAGE, no EXECUTE and no SELECT reads every character. So PostgreSQL has no unreadable case, measured rather than assumed.

The five other probes: MariaDB's SHOW CREATE PACKAGE needs no sql_mode=ORACLE and an EXECUTE-only caller gets a row with a NULL body; Oracle's EXECUTE ON DBMS_DDL is already granted to PUBLIC; SQL Server answers a row with a NULL definition rather than no row, and all three causes are distinguishable in one query; ClickHouse's create_query carries the text, so its function kind keeps hasSource; and a backslash IS processed as an escape inside a ClickHouse quoted identifier, in both the double-quote and the backtick form.

Verified at the route and in the provider suites rather than on screen

MariaDB's package and sequence (their folders are never drawn today, D57), Couchbase's external-function refusal, Trino's Hive-view translation failure, and the Elasticsearch and OpenSearch privilege refusals.

Deferrals filed, with their measurements

D56 Druid lookup, D57 MariaDB's two undrawn kinds, D58 ClickHouse non-SQL function origin, D59 Trino materialized-view fixture (the cheap route is measured shut: the Iceberg JDBC catalog refuses createMaterializedView), D60 MongoDB transport failure reported as a refusal, D61 search re-serialisation, D62 CockroachDB and Materialize, D63 a positional bind in postgres.ts, D64 the unpinned trigger listing join, D65 a provider double that dispatches on its own statement, D66 the SQLite kind-vocabulary guard, D67 assertObjectPathShape written eight times, D68 bun run test red on a shared process. Plus X14 to X16 from the Studio UI.

What is deliberately NOT here

Editing. #778 asked to edit a procedure; this phase is the read half that unblocks it, and Phase 3 is where an edit is applied. A part that came back truncated is already specified as not editable, because submitting a bounded body back is a data-loss shape.

package.json does not move: every addition is optional and additive on the published surface, so neither chart:bump nor the operator bundle is required.

How it was verified

Every provider's read was measured against a live engine with the fixture that reproduces it committed beside it. The standalone shell was driven in a real browser across the eighteen engines those seventeen type-ids cover, one container at a time; the embedded package surface was driven separately, mounted as a host does, with and without the host's optional readObjectSource, and no /api/db/objects request is made in either case. Agent mode was re-checked in .workflow-data: the grounding still carries kinds, derived groupings are still stated as not addressable, every statement of an auto run still arrives inside BEGIN READ ONLY, and no definition text and no objects/source reaches the model.

Local gates green at HEAD: format, lint, typecheck, knip, chart:check, channels:showcase:check, readme:check, security:check, build, build:lib, attw, and coverage at 53999/53999 lines. bun run test is 12320 pass 31 fail, and those 31 are D68: three files this branch never touched, which pass alone and fail only in a shared process.

…789)

Phase 2 reads an object's definition text. This commit is the half sixteen
provider tasks consume and nothing renders yet.

`ObjectSourceForm`, `ObjectSourceOrigin`, `ObjectSourcePart` and
`ObjectSourceDocument` join `src/lib/db/types.ts`, and
`DatabaseProvider.readObjectSource` is added as the one OPTIONAL object method:
two engines in the fleet have no kind with a definition text anywhere, so a
required method would put an unreachable throw in each.

`parts` is a non-empty tuple so a zero-part document is a compile error at every
provider, and the refused arm carries no `text` key at all, so there is no path
from a refusal to an editor buffer.

Five derivations in `src/lib/db/object-kinds.ts`: `kindHasSource`,
`isSourcePartUnavailable`, `SOURCE_CHARACTER_LIMIT`, `SOURCE_PART_LIMIT`,
`sourceBoundTruncationReason` and `applySourceBound`. The last is hoisted here
rather than written sixteen times, on the evidence that `comparePaths` was
written four times before anyone owned it.

All four types are published from `src/exports/types.ts`; every addition is new
and additive, so nothing an external consumer compiles against moves.
…library (#789)

The conformance helper gains the source half, and Redis gains the method its
declaration has promised since Phase 1: `redis.ts` holds the only kind in the
fleet that declares `hasSource`, so the new pairing assertion is red against it
the moment it lands and the two have to arrive together.

What `assertObjectSurface` now asserts, each with a mutation that kills a test:

- the pairing, in BOTH directions and outside every loop, so the fifteen
  providers that implement nothing are certified as strongly as the two that do;
- a source-bearing kind the expectation never names is refused by name, one
  notch narrower than "names none": Oracle declares nine of them;
- a document's path and kind are the ones it was asked for;
- two parts of one document may not share an id;
- a part's text is never empty and a refusal's sentence never blank;
- a part's language agrees with the kind's declared `sourceLanguage`;
- a caller's bound is honoured, reported with the one shared sentence and the
  right number, and NEVER reported on an unbounded read;
- an absent object raises a `QueryError` naming the segment, with the same
  kind's successful read in the same run as its positive control.

Redis reads `FUNCTION LIST LIBRARYNAME <name> WITHCODE` and selects the entry
whose `library_name` is BYTE-EQUAL to the last path segment. Measured on Redis
8.10.0: the library dictionary is case-sensitive, so `libredb_probe` and
`LIBREDB_PROBE` coexist, while the `LIBRARYNAME` argument is a case-insensitive
glob, so one lookup answers both and `reply[0]` would return the wrong library's
Lua. The fixture now loads that pair. An absent library answers an empty array
rather than an error, so emptiness is raised as absence; a denied read carries
the server's own sentence unprefixed as the part's `unavailable`.

Also measured and recorded in the provider doc: `redis-cli` reading from stdin
does not skip a `#` line, it sends it as a command, so the fixture carries no
comments.
…ise a dropped Redis socket (#789)

Fix round 1 on the object source contract.

The union does NOT make a part carrying both `text` and `unavailable`
unrepresentable: measured against tsc 6.0.3 with no cast, such a literal
compiles, because TypeScript's excess-property check on a union admits any
property declared on any member. `isSourcePartUnavailable` then narrows it to
the refusal arm and the document walk continued past every remaining check, so
a Source pane would render a refusal sentence over a definition the engine
really returned. `assertSourceDocument` now throws for it before the narrowing,
and both docblocks that claimed the type closed this say what was measured.

The Redis refusal arm caught every rejection. Measured against ioredis 5.11.1
and redis 8.10.0: a server error reply is a `ReplyError`, a dropped socket is a
plain Error reading "Connection is closed.", and the old catch presented the
second as the server's own refusal of this object. Only a server error reply is
a refusal now; everything else raises a ConnectionError naming the library.

`applySourceBound` cut UTF-16 code units, so a bound landing between a surrogate
pair emitted a lone surrogate as the last character of a part. It drops the
unpaired half and still reports the caller's number as the bound.

Three test repairs: the `readonly` predicate test narrowed a const at its
declaration and so passed for any predicate spelling, the wrong-limit-number
test threw with no pattern, and the two unmarked `applySourceBound` cases used
`toEqual`, which ignores an explicitly-undefined property. Each is now killed by
the mutation it exists for.
…guard that is not written (#789)

The round-1 repair inserted isServerErrorReply between parseFunctionLibraryCode's
docblock and parseFunctionLibraryCode, so the function that selects a library by
byte-equal name was undocumented and fifteen lines of measured Redis behaviour were
attributed to the error classifier. Neither Biome nor oxlint nor ESLint reads comment
adjacency, so a guard replaces the review note: two tests parse the provider with the
TypeScript compiler API and assert that no module-private function is left undocumented
and that the byte-equal rule is attached to the function that implements it.

ObjectSourcePart's docblock said the client's shape check refuses a part carrying both
text and unavailable, and refuses an empty text, for a host's answer. Nothing in this
tree does either: the embedded seam's shape check is later work. Both sentences now say
what stands today, and the forward reference points at isRenderableShape, which exists,
rather than at a name no file defines.
`db-dark` and `db-light` were defined inside `QueryEditor`'s `beforeMount`, which is
per-mount state: a second Monaco mount that does not run that callback paints with
Monaco's stock theme and sits visibly beside a query editor it does not match. The
read-only object source viewer is that second mount.

Both definitions move verbatim into `src/lib/editor/monaco-theme.ts`, which exports
`defineStudioThemes` plus the two ids as constants so no mount can spell one wrong.
The payloads are byte-identical to the ones that shipped, and the unit test pins both
in full: a hoist that changes one hex digit is a visible regression nothing else here
would catch.

The module-scope `configureMonacoLoader()` call stays in `QueryEditor`, and
`tests/isolated/monaco-loader-wiring.test.ts` still observes it at module-evaluation
time.
…a reason (#789)

A source-bearing kind the expectation OMITS was already refused by name, but a
kind it NAMES AT ZERO was exercised by nothing and passed in silence. Oracle
declares nine source-bearing kinds, so an expectation naming eight truthfully
and the ninth at zero certified that ninth unread.

Requiring a non-zero everywhere would refuse correct fixtures: Cassandra ships
materialized views and user-defined functions disabled, and a Trino materialized
view needs a connector the compose cluster may not get. So the bar is the
reason, in the grammar KindCount already uses for an absence: a zero that says
which absence it is, in the engine's or the fixture's own words.

assertObjectSurface now throws by name for a source-bearing kind counted at zero
with no emptyKinds entry, for a blank or whitespace one, and for a reason naming
a kind that is not a source-bearing kind at zero, so a sentence cannot outlive
the absence it describes. Four mutations run and four killed.
…ettle (#789)

Fix round 1 on the expectation-reason ruling.

The emptyKinds docblock is the instruction sixteen provider tasks read, and both of its
motivating examples were false about the containers this repository starts: the compose
service re-enables Cassandra's materialized views and user-defined functions, and Trino's
zero is `view` rather than `materialized_view`. Replaced with the zeros actually committed
here, with Cassandra kept as the worked example of writing an engine default where a
fixture fact belongs.

The reason itself is now checked where it is decidable. A zeroed source-bearing kind is
listed, and a sentence claiming an absence the fixture does not have is refused by name
with the listing beside it. The four verdict strings the docblock called not-reasons are
refused rather than only described. The stale-reason loop runs before the early return for
a provider bearing no source, so a sentence cannot survive a dropped hasSource
declaration. And `zeroed` is the exact complement of `wanted`, so a count that is neither
zero nor positive is held to the same reason instead of falling between both guards.

A source-bearing kind whose count is the unavailable arm can be neither named nor omitted,
which two providers can already produce; the count refusal now says so instead of leaving
the cheap wrong exit of dropping the declaration.

Five new tests, nine mutations run and nine killed.
…a definition is (#789)

The caption is not decoration. Without it a PostgreSQL pg_get_viewdef answer, which is a
bare SELECT with no CREATE VIEW in front of it, reads as a complete statement a user could
copy and run, and a DuckDB macro body reads as the bytes somebody typed rather than the
engine's regeneration from its catalog.

A pure exported function rather than JSX, per standing ruling 5b after the happy-dom clamp
finding: happy-dom returns zeros for layout, so copy reached through a rendered element can
be asserted vacuously. All six compositions are pinned with no DOM at all.

Two frozen records rather than one six-cell table, because the two axes are independent and
no engine couples them: PostgreSQL produces regenerated in both forms, Couchbase produces
partial from a rendered origin.
…789)

The source read is its OWN seam rather than a fourth member of the tree's ObjectReadRequest.
Measured: that union is paired with a ReadSlot whose three kinds each land in a TreeCache
map, isRenderableShape dispatches on the slot and not on the route, and the surface that
wants a source document is a tab holding no handle on the tree's private source at all. A
fourth member would also oblige the embedded adapter's exhaustive switch to carry an arm for
a state the design says cannot occur, which is the deleted 501 in a new place under a
coverage gate.

isSourceDocumentShape is the live home of the invariants TypeScript cannot hold. Four of its
rules are about two facts collapsing into one rather than about malformed data: a part
carrying both text and unavailable narrows to the refusal and drops the definition in
silence, and our own compiler admits that literal because the excess-property check on a
union accepts a property declared on any member; a refusal with a blank sentence draws our
headline over an empty line; an empty text is not a definition; and a truncation mark with
no reason is a warning banner with nothing in it. Duplicate part ids are refused for the
switcher's sake, since activePartId addresses a part by id.

Nine mutations run and nine killed.
POST /api/db/objects/source is the seventh route under that prefix, built on
handleObjectRequest so it inherits auth-before-parse, rate limiting, connection
resolution and error mapping with no new line of any of them.

Two exported helpers carry the parts that are its own. requireSourceReader is
one branch with two conjuncts: a kind that declares no source, and a provider
that declares one and implements no method, which readObjectSource being
optional makes representable. Both are reachable and both are pinned by the
refusal SENTENCE rather than the status, because a provider raising its own
error also maps to 400. It is a helper rather than an inline throw because
ObjectRouteError is module-private, and keeping it private is what keeps the
status vocabulary in one file.

boundSourceDocument applies the character bound to the ANSWER instead of
trusting the number the route passed the provider, on the inventory route's
precedent: sixteen providers plus any host implementing the embedded seam sit
behind it, and the route serialises the whole document in one response. A
provider that bounded correctly is returned unchanged; one that over-answered
is sliced and its own sentence is kept and joined, because a second bound is a
second fact. parts.length is bounded too, since the tuple type has no upper
bound.

The rate-limit query-bucket census is re-measured rather than incremented: it
claimed twenty routes and named four schema routes that no longer exist, while
never mentioning the object routes that had joined. Twenty-three handlers
today, sixteen direct call sites and seven through object-route.ts.
The one rule this surface exists for: an unreadable source never opens an empty editor. An
empty editor reads as "there is no source", and a user who types over it deletes the object,
which is the composition DBeaver ships, measured in its source. It is closed twice here. The
type gives a refused part no text key, and the component renders a DIFFERENT element for a
refusal and for a failed read, so there is no editor on screen to type into even if a later
change made one writable. The renderer does not lean on the type for this, because the union
does not make a part carrying both keys a compile error: the excess-property check on a union
admits a property declared on any member, so isSourceDocumentShape refuses that part first.

Not QueryEditor, for four measured reasons: it hardcodes readOnly false with no prop to
change it, installs a Run action and a Cmd+Enter binding unconditionally on mount, renders an
execute toolbar, and takes a closed four-member language union reachable only through
resolveTabType, which CLAUDE.md forbids extending. A definition opened in it would offer to
execute itself. readOnly true is not a security boundary either and the docblock says so; the
boundary in Phase 2 is that no write path exists at all.

The whole read lives in one place so the two shells cannot drift, keyed on the address rather
than on the array and object props, which are a fresh identity every render. The token
recorded is the one at the moment the read was ISSUED: a DDL that runs while a read is in
flight cannot be attributed to either side of it, so the tab marks itself stale and offers a
re-read rather than claiming the text is current.

The barrel re-exports six names and not the caption, which has no consumer outside this
folder; object-tree/index.ts records what happens otherwise.

Also registers the suite as its own group in tests/run-components.sh. That file has no named
owner in the phase's ownership map, and a component test file absent from it never runs in CI
at all, which this repository has shipped once before. Group 27 is separate from Group 15
because that group holds QueryEditor.test.tsx, which installs a different double of
@monaco-editor/react, and mock.module is process-wide.

18 mutations run and 18 killed. Raw lcov for the viewer: LF 172, LH 172, no DA record at zero.
The route's character bound was defeated by the one malformed part shape
`ObjectSourcePart` already documents as compiling: a part carrying
`unavailable` beside `text` narrows to the refusal arm, so `boundPart`
returned it untouched. Measured through `boundSourceDocument` at a
1,000,000 bound, a hybrid part carrying 2,000,000 characters came back
whole, 2,000,141 characters of JSON on the wire, while a client narrowing
the same way renders a refusal over a definition the engine returned.
`assertObjectSurface` refuses that shape only in the provider suites.

An empty `parts` array raised `TypeError: part is not an Object` inside
the `in` check and reached `createErrorResponse` as an unhandled error;
it is now a named 400 like every other caller mistake here.

The unauthenticated-caller census is derived from a readdir of
`src/app/api/db/objects` instead of an array literal. Deleting a handler
from that literal left the file green with the test still naming a count,
so an eighth route could have landed uncensused; the zero-iteration case
now throws by name.

Both bound docblocks claimed sixteen providers "plus any host implementing
the embedded source seam" sit behind this route. Measured: the provider
comes from a closed `switch (connection.type)` in `src/lib/db/factory.ts`
with no registration point, and the embedded shell has no API routes, so
a host never reaches here. The claim is corrected to the sixteen providers.
…s pattern (#789)

The shape check ran on one of the two entry paths. isSourceDocumentShape was called inside
the read effect only, so a document arriving through the document PROP was never checked
while the component docblock stated the opposite as fact. That entry is real rather than
theoretical: use-tab-manager.ts restores the tab set from localStorage with a JSON.parse
guarded only by Array.isArray, so once a Source tab carries its document, an older shape, a
truncated write or a hand-edited entry reaches the viewer with needsRead false. Measured on
the previous component: a part with an empty text mounted the editor holding "", a part
carrying both text and unavailable rendered the refusal pane over a real definition, and an
origin outside its union captioned "undefined Complete as shown.". The render now refuses a
prop-supplied document in the same failure grammar the read path uses, and the docblock says
which entry does what instead of claiming a guard that was not written.

Every non-selected tab carried an aria-controls pointing at an element that does not exist,
because only the active panel is rendered. That is an axe aria-valid-attr-value violation,
and the old test could not see it: it resolved the IDREF only inside the selected branch, so
the mutation from panelId(index) to panelId(activeIndex) left 24 pass 0 fail. Only the active
tab names a panel now, and the test resolves every reference it finds, checks the panel points
back at that tab, and counts them.

The tabs pattern was also only half written, and the claim that it was complete was refuted by
the repository's own tab bar: StudioTabBar implements the KEYBOARD half and omits
aria-controls, while this switcher had the ARIA half and no roving tabindex and no arrow keys,
so both tab buttons sat in the tab order and no arrow key did anything. jsx-a11y has no rule
for either, measured: lint reported zero errors over that version. The switcher now carries
the roving tabindex, ArrowLeft, ArrowRight, Home and End with wrapping, and focus following
activation, mirroring StudioTabBar.

The barrel had no runtime importer anywhere in the tree, so its two export lines produced no
DA record and the coverage gate could not see them. The suite now imports through it.

9 mutations run and 9 killed, including the one that survived round 1. Raw lcov over the
three suites: ObjectSourceView.tsx LF 200 LH 200, index.ts LF 2 LH 2, source-caption.ts LF 11
LH 11, source-reader.ts LF 42 LH 42, no DA record at zero anywhere.
Five of the seven declared kinds gain `hasSource` with `sourceLanguage: "pgsql"`:
view, materialized view, function, procedure and trigger. `table` and `sequence`
declare nothing, because PostgreSQL publishes no `pg_get_tabledef` and no
`pg_get_sequencedef` and `pg_catalog.pg_sequences` publishes properties rather
than text; a kind an engine cannot answer for is absent from the declaration
rather than declared and refused.

`readObjectSource` passes the catalog's own OID and never a `regprocedure` or
`regclass` cast. Measured on PostgreSQL 18.4: the `pg_get_*` family applies no
privilege check at all, so a role holding no USAGE, no EXECUTE and no SELECT read
the complete text in the same session where calling the function was refused,
while the cast raised 42501 for that same role. A provider that casts therefore
manufactures a refusal the engine never made, on an object the tree has already
listed. The `src_probe` role that measurement used ships in the fixture so it can
be re-run.

Absence raises and is never a refusal part: no row, a NULL and a whitespace-only
definition are one fact and PostgreSQL utters no sentence for it. The two
refusals that do exist are a wire-compatible fork missing a catalog surface
(42883, 42703) and carry the server's own sentence unprefixed; everything else,
a transport failure included, raises.

The schema is derived through `containerDepth()` and the object name is the last
path segment, pinned by a two-level declaration driven all the way to the binds.
The routine identity expression is hoisted so the listing that writes a routine's
address and the read that consumes it have one writer, and the path-shape check
`describeObject` carried is hoisted beside it for the same reason.
…31603 is (#789)

All nine declared Oracle kinds gain hasSource, all under the Monaco id `sql`,
because `plsql` is not among the 89 ids monaco-editor 0.56.0 registers and an
unregistered id degrades to plain text silently. readObjectSource answers
DBMS_METADATA.GET_DDL with the metadata type taken from the translation table
this file already ships, so the two dictionary vocabularies cannot drift.

Three things this read does that no other object read here does.

It asks ORA-31603 a second question. Oracle words a privilege refusal as
"object not found in schema", and #765 made reading another owner's object the
ordinary path, so the unqualified sentence tells a user their objects are gone.
Measured on Oracle XE 21.3.0.0.0: GET_DDL over REPORTING.REPORT_DAILY, which APP
holds SELECT on, raises the same error word for word as GET_DDL over
REPORTING.NO_SUCH_TABLE. ALL_OBJECTS separates them, and ALL_OBJECTS by name:
it is privilege-filtered, while DBA_OBJECTS would turn the disambiguation into
a cross-schema existence oracle. A visible object is a refusal part carrying
Oracle's own sentence first and whole; an invisible one raises.

It detects wrapped PL/SQL, which raises nothing at all. The rule is a header
POSITION and not a text search: the token after the closing quote of the quoted
name is the bare keyword `wrapped` and the next line is the wrap format marker.
The fixture commits three plain, compiling functions built to defeat the naive
textual rule and a zero-argument control, so the predicate has adversarial cases
anybody can re-break.

It emits TWO parts for a package, through PACKAGE_SPEC and PACKAGE_BODY rather
than one through the bare PACKAGE type, because one CLOB cannot say a body is
absent, cannot report a wrapped body beside a readable spec, and cannot carry
the two halves' independent STATUS. A package with a spec and no body emits one
part and its missing body is an absence, not a refusal.

The read passes lobFetchTypeHandler, which runObjectQuery does not, and raises
rather than coercing when a CLOB still arrives as a stream. describeObject's
path-shape check moves to one shared writer so the two methods cannot come to
disagree about one engine.

22 mutations run, 22 killed.
…flag as text (#789)

Fix round 1 on Task 6. Five mutants of one class survived the whole suite,
all of them edits to statement TEXT that leave the binds untouched and are
therefore invisible to a double that routes by the pg_get_* function name:
the materialized view built from the view's relkind, two dropped
n.nspname = $1 predicates, a dropped c.relkind and the trigger's pretty
flag turned on. Each now dies to a statement-text pin, and the
materialized view gains the read test it never had.

MEASURED_VIEW_DEFINITION was a hand-abridged four-column text while its
docblock claimed it was what PostgreSQL 18.4 answered. Re-measured on a
postgres:18 brought up on docker/postgres-init: 606 characters and twelve
select-list columns. The constant now carries those bytes, daily_sales and
revenue_by_month get their own measured texts instead of borrowing the
view's, and a length assertion pins each one, because every substring
assertion passes against an abridgement.

assertObjectPathShape counted containerLevels.length while readObjectSource
sliced the container with containerDepth(), which saturates at two. It now
reads declaredLevels(), which is what its own docblock already required.
…d create the markerless unit (#789)

Five of the nine kinds bound their DBMS_METADATA.GET_DDL object_type through a
double that keys on the object NAME alone, so mutating the metadata spelling of
synonym, sequence, trigger, view or procedure left the whole suite green while
every live Source tab on them would have raised ORA-31600.

The vocabulary is now pinned as a LITERAL nine-row list captured from the live
server, not read back out of the provider's own translation table, and the
conformance double keys its GET_DDL arm on the metadata type and the name
together the way the source describe's double already did. Mutating any kind's
spelling to a nonsense value now fails two or more tests; package's spellings
live in ORACLE_SOURCE_PART_PLANS and fail three and four, and the table's
package.metadata value is read by nothing, which its docblock now says.

APP_MARKERLESS_HEADER stops being a hand-written string and becomes an object
the fixture creates. Measured on Oracle XE 21.3.0.0.0: `wrapped` after a function
name is accepted, because it is the wrap keyword, the unit fails to compile with
PLS-00753 and is left INVALID, and GET_DDL answers its source verbatim anyway,
byte-identical to the constant the test already carried. That is the only object
that asserts the marker half of the wrapped rule.

Also: the source read's docblock pointed at isWrappedDefinition, which has not
existed since the rename to wrapFormatMarker; and the reproduction recipe ran on
--name oracle-src -p 1521:1521, which collides with any machine already running
an Oracle, where every measurement in that section was taken on src-task07-oracle
at host port 15217.
…ody thought of

Round 1 pinned each statement's WHERE clause and its pretty flag, which killed the four
mutants living in a predicate and left the SELECT expression and the JOINs unpinned. Three
mutants then survived the whole suite at 205 pass 0 fail: rewriting pg_get_functiondef(p.oid)
to take a ::regprocedure cast, which manufactures a 42501 the engine never made on an object
the tree already listed; rewriting it to p.prorettype, which asks for a different catalog
entry; and rewriting the trigger join to tgconstrrelid, which is 0 for every trigger that is
not a constraint trigger.

The double does not execute SQL, so no assertion on params or on the returned document can
see any of the three. One equality per kind closes the class rather than the members of it
somebody has thought of. Whitespace is squashed because indentation is not behaviour; every
other byte is pinned.

Verified: 205 pass 0 fail, and rewriting p.oid to p.prorettype now gives 204 pass 1 fail.

Refs #789
…y thought of

Round 1 pinned toContain("DBMS_METADATA.GET_DDL") on the first read and
toContain("FROM ALL_OBJECTS") plus not.toContain("DBA_OBJECTS") on ruling C's second
question, and left every other byte of both statements unpinned. The double does not
execute SQL, so no assertion on params and none on the returned document can see a bind
marker move, a predicate leave, or a column alias change. Four mutants then survived the
whole suite at 202 pass 0 fail:

- GET_DDL(:1, :2, :3) rewritten to GET_DDL(:2, :1, :3), which sends the object name as
  the object_type argument and is ORA-31600 on every one of the nine kinds;
- OWNER = :1 AND OBJECT_NAME = :2 rewritten to OWNER = :2 AND OBJECT_NAME = :1, which
  answers NO ROW for everything and turns ruling C's refusal into a false absence;
- AND OBJECT_TYPE = :3 deleted, which answers a PACKAGE row to a PACKAGE BODY question,
  so a package with a spec and no body reports a refusal instead of an absence;
- AS DDL renamed to AS DDL_TEXT while readDefinition still reads rows[0].DDL, which is
  undefined live and invisible to a double that builds its row as { DDL: text }.

One equality per read, and the GET_DDL statement pinned PER KIND inside the
metadata-vocabulary test, because a kind is the unit a Source tab breaks in. Whitespace is
squashed because indentation is not behaviour; every other byte is pinned. All four mutants
now die: 198/4, 201/1, 201/1, 198/4.

Refs #789
…789)

Three prose numerals had drifted from the literals below them, all in the
wrapped-PL/SQL argument, and each one told a maintainer that a fixture object
the predicate depends on is surplus.

The test double's comment said "the ten keys" over a thirteen-entry object.
The numeral is dropped rather than corrected: the literal below it IS the
inventory, so a count beside it can only agree or be wrong, and the next name
added to the double would stale it again. The comment now says what the literal
cannot, which is that the keys are one per (metadata type, name) pair and each
of the two packages takes two.

docs/providers/oracle.md said "three plain, VALID, COMPILING functions built to
defeat the naive TEXTUAL rule" over a four-row table whose fourth row is
APP_ZERO_ARG, the control for the header position, with "All four fail the
predicate" four lines below. Writing "four" over it would have made the count
right and the description wrong, because the control defeats nothing. It now
names four and says which three are the defeaters and which one is the control.

The same numeral was live in docker/oracle-init/01-object-fixture.sql, and that
one mattered more: "The three plain units below are the point of this block and
must not be tidied" is the sentence a maintainer meets while holding the units,
and under that wording APP_ZERO_ARG was the one plain unit the protection did
not cover, while a later comment in the same file already said "the four units
above". Fixed in the same round.

No object the fixture creates moved: every changed line in the .sql begins with
"--", checked by diff. No executable line changed anywhere, in src/ or in the
suite. Two control mutations re-measured because this round edits the comment
directly above the driver double they exercise: dropping the marker conjunct at
oracle.ts:626 gives 201 pass 1 fail, and corrupting synonym.metadata gives 200
pass 2 fail, both matching the baselines. Suite unchanged at 202 pass, 0 fail,
624 expect() calls.
…ch refusal a NULL is (#789)

`readObjectSource` answers one object's definition as a document of named parts, on
every kind either server declares: MySQL's six and MariaDB's eight each have a
`SHOW CREATE` form, so this is the one provider in the fleet with no kind that
declares nothing. The declaration is resolved from the server's own `VERSION()`
string, as everything else on this provider is, so both branches are driven.

Measured 2026-09-13 on MySQL 26.7.0 and MariaDB 12.3.2 against the two committed
fixtures, and three of the measurements refute what was written down before:

- `SHOW CREATE SEQUENCE` answers `Table` and `Create Table`, NOT `Create Sequence`.
  A MariaDB sequence is a table underneath, which is the same fact that puts it in
  `information_schema.TABLES` with `TABLE_TYPE = 'SEQUENCE'`.
- `SHOW CREATE PACKAGE` needs NO `sql_mode=ORACLE`, so the session mode is never
  touched. The ORACLE spelling in the reply's own `sql_mode` column is the mode the
  package was CREATED under, which is where the opposite belief came from.
- A caller holding `GRANT EXECUTE ON app.*` gets a ROW whose body column is NULL on
  all four routine forms and a raised privilege error on the other four kinds, which
  refutes "MySQL, MariaDB: no unreadable case". The NULL is the one refusal here
  whose words are ours, because the server utters none; every raised sentence is
  carried verbatim and unprefixed.

Absence RAISES and names the object, and the sentence is ours rather than the
server's because ER_TRG_DOES_NOT_EXIST is the bare words "Trigger does not exist".
A package is two statements and one node, specification first, because a body cannot
exist without a spec and a spec can exist without a body; `app.spec_only_pkg` in the
MariaDB fixture is the one-part shape. Both fixtures gain the EXECUTE-only user.

`docs/providers/mysql.md` 1.1 said MariaDB's `package` and `sequence` "reach the
object browser", which is false for the standalone tree: provider-meta never
connects, so the client's copy of the declaration is the MySQL six. That sentence is
corrected. Both kinds still declare hasSource, because the declaration is true about
the engine and withholding it would be a second wrong declaration.

26 mutants run and 26 killed. Two survived the first round, both a reply COLUMN
name, and both were invisible because a wrong column reads as a refusal that every
count, statement and conformance assertion tolerates; the per-kind readable-text
assertion is what kills them now.
…close D53 (#789)

The SQLite object-surface DDL lived inside its integration test and the libSQL one
lived only as a fenced block in docs/providers/libsql.md. The second shape is what
D53 was filed for: a measurement a reader can see and cannot re-run. Both now live
in docker/sqlite-init/, the SQLite suite READS its file rather than carrying a
literal, and build-fixture.ts replays either into a database FILE that opens in
Studio. apply-to-libsql.ts sends the libSQL one to a running sqld, which ships no
client of its own.

The plan assumed the two fixtures were one DDL. Measured, they are not: 01 holds
TEMP and ATTACHed objects sqld refuses outright and six tables, 02 holds a STRICT
table, the second implicit-index shape and ten. Converging them would rewrite every
count in both suites and invalidate a live capture. One directory, one splitter, one
build script, one file per engine's own catalog.

Both fixtures gain a TRIGGER whose name is also a table's, which SQLite and sqld
both accept while CREATE INDEX and CREATE VIEW under an existing table's name are
refused. It is the only object either fixture holds for which
"SELECT sql FROM sqlite_schema WHERE name = ?" answers two rows, so it is what makes
the coming source read's type-from-the-KIND derivation testable rather than asserted
by statement shape (standing ruling 5a).

The libSQL sqlite_schema capture was re-taken against a self-hosted sqld 0.24.33
(40a151bd 2025-12-19) started from 02, adding the definition text the Source read
answers from. sql is NULL on exactly one row, sqlite_autoindex_badges_1, and the
listings exclude every sqlite_-prefixed name, which is why this engine has no
reachable source refusal.

The splitter ends a CREATE TRIGGER statement only at the ";" after its END: a
split(";") hands the engine a truncated body and a bare END and it refuses both.
ANALYZE is in neither file, measured refused by sqld as
"SQL string could not be parsed: unsupported statement: ANALYZE".
…s it is (#789)

SQL Server's `view`, `procedure`, `function` and `trigger` gain `hasSource` with
`sourceLanguage: "sql"`; `table`, `synonym` and `sequence` declare nothing, because
only the types P, RF, V, TR, FN, IF, TF and R have a SQL module at all and
sys.synonyms and sys.sequences hold properties rather than text.

`readObjectSource` reads sys.sql_modules, three-part named at the path's own catalog
with the schema and the name bound. A caller without VIEW DEFINITION gets a ROW with a
NULL definition rather than no row, so a NULL is never an absence, and the three causes
of one are separated and answered with three different sentences.

MEASURED, and it refutes the row this work was given: OBJECTPROPERTY(id,'IsEncrypted')
resolves its object id in the CONNECTED database whatever database a three-part name
addresses. Verified end to end through this provider on SQL Server 2022 RTM-CU26,
connected to libredb_objects_two and reading libredb_objects.app.order_summary_secret:
the shipped statement reports the module as encrypted and the same read with
OBJECTPROPERTY in its place reports it as "no VIEW DEFINITION". The flag therefore
comes from sys.syscomments.encrypted, which is three-part nameable and, measured with
VIEW DEFINITION granted on two of four objects to one login, is visible exactly where
that permission is, per object, for the denied principal itself.

The fixture gains an encrypted VIEW and an encrypted PROCEDURE, each beside a readable
sibling of its own kind, and the src_probe login with SELECT and EXECUTE on SCHEMA::app
and no VIEW DEFINITION. EXECUTE is load-bearing: without it metadata visibility hides
every procedure in the schema and the encrypted-procedure case cannot be reached.

The two-level path derivation is pinned here for the fleet (standing ruling 5g): one
spyOn drives a two-level declaration to a bound value, and a second swaps the two levels
over, which a positional implementation cannot survive.
…ext is the author's own (#789)

All four declared kinds gain hasSource with sourceLanguage "sql", because
sqlite_schema keeps a sql column on every row it has: there is no kind on this
engine that declares nothing. One statement answers all four,
"SELECT s.sql AS sql FROM sqlite_schema AS s WHERE s.type = ? AND s.name = ?",
with both binds as parameters, so this read needs no identifier escaper at all.

form is complete and origin is STORED, which here is a real distinction rather than
a formality: the text is what the author submitted, newlines and inner spacing
included. The caption exists to stop a regeneration reading as somebody's own text,
and if every engine said regenerated the distinction would be decoration. The one
caveat is measured on SQLite 3.53.2 and is in the provider doc: the engine
normalizes the CREATE TABLE <name> prefix, drops anything after the closing
parenthesis, and REWRITES the stored text on ALTER TABLE.

NO REFUSAL IS REACHABLE and the doc states it as a CANNOT. sqlite_schema.sql is
NULL only for an index the engine made for itself, and every listing here excludes
the sqlite_ prefix, so no path the tree produces addresses such a row. The blank arm
is still written and still tested through the statement seam, because an empty
definition must never reach an editor as a definition; the sentence is ours, since
SQLite supplies none.

The type value comes from the KIND through SOURCE_CATALOG_TYPES and never from what
the name matches. The fixture's audit_log is a table AND a trigger, which SQLite
accepts while refusing an index or a view under an existing table's name, so
WHERE name = ? alone answers two rows with the table's first: the rule is exercised
rather than asserted by statement shape (standing ruling 5a).

assertObjectPathShape is hoisted, one writer for describeObject and this method, so
the detail pane and the Source tab cannot disagree about a trigger's address.

Ten mutations run, nine killed against 142 pass 0 fail:
  drop "s.type = ?"                       135 pass 7 fail
  SELECT s.name instead of s.sql          137 pass 5 fail
  name bind becomes path[0]               140 pass 2 fail
  path shape stops deriving its levels    140 pass 2 fail
  blank text no longer refused            141 pass 1 fail
  NULL text no longer refused             140 pass 2 fail
  the caller's bound dropped              140 pass 2 fail
  origin claims a regeneration            141 pass 1 fail
  absence answers a refusal, not a raise  140 pass 2 fail
  bind the kind id in place of the map    142 pass 0 fail, SURVIVED
The survivor is behaviour-identical on this engine: all four kind ids and all four
sqlite_schema.type values are spelled the same, so no fixture can tell the map from
the id. It is the ruling 5g case of a derivation only a differently-spelled engine
could distinguish, and it is reported rather than papered over. The guard above it,
a kind declaring source with no catalog type behind it, IS driven and does raise.

Coverage of sqlite.ts from raw lcov: LF 745, LH 745, zero DA records at 0.
… key on (#789)

All four declared kinds gain hasSource with sourceLanguage "sql", answered by the
same statement the SQLite provider uses, because libSQL IS SQLite:
"SELECT s.sql AS sql FROM sqlite_schema AS s WHERE s.type = ? AND s.name = ?".
One round trip, one part, both binds parameters, no identifier escaper needed.

form is complete and origin is STORED. Verified against a self-hosted sqld 0.24.33
built from docker/sqlite-init/02-libsql-object-fixture.sql: the orders table comes
back carrying the twenty-one-space continuation indent it was sent with, which is
what separates this family from every engine that rebuilds a statement out of its
catalog. The ALTER TABLE caveat on "stored" is recorded in docs/providers/sqlite.md
and pointed at from here.

NO REFUSAL IS REACHABLE and the doc states it as a CANNOT with all three candidates
measured: there is no privilege system to refuse a read; sqld's statement allowlist
refuses VACUUM, ANALYZE, ATTACH and PRAGMA query_only and does not touch a plain
SELECT from sqlite_schema; and sqlite_schema.sql is NULL only for an index the engine
made for itself, which every listing here excludes by name. The blank arm is written
and tested anyway, because an empty definition must never reach an editor as one.

NOTHING KEYS ON REFUSAL WORDING, in the provider or in the suite. sqld and Turso
Cloud word the identical refusal differently, so a test pinning either sentence would
pass on one deployment and fail on the other. The one sentence asserted is ours, for
a NULL the server says nothing about. A statement failure and an expired credential
both RAISE through the provider's own mapping instead of becoming a refusal part.

The type value comes from the KIND and never from what the name matches. The fixture
holds a trigger named "badges" beside a table named "badges", measured live to answer
two rows with the table's first, so the rule is exercised rather than asserted by
statement shape (standing ruling 5a).

assertObjectPathShape is hoisted, one writer for describeLibSQLObject and this
function. The sqlite_schema capture the suite answers from was re-taken live and now
carries the definition text, which is what makes the per-object text pin real rather
than a comparison of the fake against itself.

Eleven mutations run, ten killed against 96 pass 0 fail:
  drop "s.type = ?"                         88 pass 8 fail
  SELECT s.name instead of s.sql            88 pass 8 fail
  name bind becomes path[0]                 94 pass 2 fail
  path shape stops deriving its levels      94 pass 2 fail
  blank text no longer refused              95 pass 1 fail
  NULL read as String(row.sql)              95 pass 1 fail
  the caller's bound dropped                94 pass 2 fail
  origin claims a regeneration              95 pass 1 fail
  absence answers a refusal, not a raise    94 pass 2 fail
  a transport failure becomes a refusal     95 pass 1 fail
  bind the kind id in place of the map      96 pass 0 fail, SURVIVED
The survivor is behaviour-identical on this engine: the four kind ids and the four
sqlite_schema.type values are spelled the same, so no fixture can tell the map from
the id. Reported rather than papered over, per ruling 5g.

The blank-text mutant survived the FIRST round at 95 pass 0 fail, because no row of
this catalog is whitespace-only and nothing drove that half of the guard. A test that
answers a whitespace-only definition through the fake kills it now.

Coverage from raw lcov: objects.ts LF 348 LH 348, index.ts LF 202 LH 202, zero DA
records at 0.
)

readObjectSource read the schema as an optional level for every kind while
only the trigger has a statement spelling that binds none. Under a
declaration with no schema level the object statement went out naming
@Schema with nothing bound to it, which is SQL Server Msg 137 reaching the
caller as a driver error instead of this provider's own named refusal. It
is latent, since it needs a declaration change to reach, and this file is
the fleet's derivation pin, so thirteen providers copy the shape.

sourceStatement becomes sourceRead, answering the statement and the binds
that statement declares as one value: the object arm takes the schema
through requiredSegment, the trigger arm keeps both spellings. The test
asserts no statement was issued at all, so the refusal has to come before
the round trip rather than after a miss the mocked double reports as a
clean absence.

Also: the fixture no longer credits OBJECTPROPERTY with separating a
denial from an encryption, which is the mechanism the provider rejects by
name; the cross-catalog OBJECTPROPERTY measurement now says NULL or 0 and
never the right answer, because the pair it named is object-creation-order
dependent; and the encrypted-module test walks [path, kind] tuples instead
of reading a kind out of a path position.
…nd withdraw a false measurement (#789)

The refusal for a definition that is not there said the same sentence for all
three shapes it serves: "The engine stores NULL there only for an index it
created for itself". That is true of a stored NULL and false of the other two,
and the worst of the three is the absent reply column, where it sends a
maintainer to the listing filter instead of to the statement that asked for a
column the reply does not carry.

Both providers now decide the shape first, through Object.hasOwn on the row
rather than through the value, and carry one sentence per shape. The absent
sentence says it is a fact about the read; the blank sentence claims no cause at
all, because none was measured; the NULL sentence keeps the cause it is true of,
and both suites now assert it there and assert its absence on the other two. The
libSQL suite gains the absent-column test it never had.

Also withdrawn: the SOURCE_SQL docblock claimed that a formatter exploding a kind
entry takes it out of the agent-side kind vocabulary guard "without failing
anything else". Re-measured, that is a loud red, two declared ids against four.
The silent case is a FIFTH kind added as a multi-line entry, which is missing
from the guard's population and from COMPOSED_KIND_WORDS at once, so both sides
shrink together and the guard passes over a kind nothing maps. The docblock now
records both measurements and names the real defect.

Two smaller repairs: the libSQL fixture header said curl is the applier, above a
command block that runs apply-to-libsql.ts and beside two files stating the image
carries neither sqlite3 nor curl; and three em dashes introduced in the sqlite
doc are gone.
…e anybody can apply (#789)

The DDL every DuckDB object-surface assertion reasons about lived inside
`seededObjectProvider()`, so the measurements were real and un-reproducible
outside a test run, which is the shape standing ruling 5i forbids. It moves to
`docker/duckdb-init/01-object-fixture.sql`, read by one reader with two callers:
the suite replays it into `:memory:`, and `bun docker/duckdb-init/build-fixture.ts`
replays it into a database FILE a person can point Studio at.

The second catalog is why the reader substitutes rather than just splitting. The
fixture's `ATTACH '{{warehouse}}' AS warehouse` needs `:memory:` for the suite and a
sibling file for a file build, or every `warehouse` object vanishes when the builder
exits and a person opening the result finds one catalog where the tests saw two.
DuckDB does not persist an attachment inside a database file, so the built file shows
the `memory` half alone and the builder prints the ATTACH that reaches the other; the
sibling is built anyway, because dropping it would give a person a fixture that cannot
show the two container levels this engine's object model is about.

Three throws, each over a zero that passes silently otherwise, and all three measured
live by deleting them (147 pass 0 fail becomes 146 pass 1 fail in each case, and the
same for dropping the comment-line rule): no statement at all builds an empty database
and every count then reads zero against zero; no substitution sends DuckDB the literal
string `{{warehouse}}` as a file PATH, which v1.5.5 accepts by creating a file of that
name while the suite still sees two catalogs; and a blank target is the same shape one
step further along.
…rather than a statement (#789)

All four declared kinds gain hasSource with sourceLanguage "sql", so DuckDB's
"declares nothing" half is empty. `sql` is honest here rather than a compromise:
the dialect is PostgreSQL-shaped and monaco-editor 0.56.0 registers no DuckDB id.

The macro is the one `partial` kind ON THIS ENGINE and it is deliberate. It is not
the fleet's only one: the design names PostgreSQL view and materialized_view and
Couchbase function as producers of the same arm, and postgres.ts already answers
`partial` for a relation. `duckdb_functions().macro_definition` is a BODY, measured
on v1.5.5: `CREATE MACRO add_one(x) AS x + 1` publishes `(x + 1)` and a table macro
publishes its bare SELECT. DuckDB publishes no CREATE MACRO statement anywhere:
there is no duckdb_macros() and information_schema has no .routines on this engine.
Assembling one out of `parameters` would show a user a statement the engine never
published, which they might copy and run; the caption that says the text is the body
only is cheaper and more honest. Phase 3 assembles.

`origin` is `regenerated` on all four and that is measured, not assumed: `note VARCHAR
DEFAULT 'none'` reads back as `DEFAULT('none')`, `name` comes back quoted, the schema
qualification is dropped, an inline REFERENCES becomes a table-level FOREIGN KEY clause
and every sequence option is spelled out. None of it is the author's bytes.

No refusal exists. DuckDB has no users, no roles and no passwords, and measured on a
fresh instance holding the fixture, zero of its tables, zero of 47 views, zero sequences
and zero of 136 macros carry a NULL or whitespace-only text; even `CREATE MACRO no_body()
AS NULL` publishes the four characters NULL. A missing name answers ZERO ROWS, which is
absence, so it raises naming the object and its container. The blank arm exists anyway and
says which of three shapes produced it, and those three sentences are ours rather than the
engine's, which docs/providers/duckdb.md records as this engine's one exception to the
"the engine's own sentence" guarantee.

The source statement is built from the SAME per-kind record and the SAME container filter
the count, the listing and the four detail reads use, so a change to either moves the
source read with it, and every bind is a parameter: each duckdb_* function publishes its
container as a COLUMN, so there is no identifier position and no escaper.

Recipe rule 9: the shipped declaration is already two-level, so a spyOn that swaps in the
same two levels would pass for an implementation hardcoding path[0] and path[1]. A SECOND
declaration swaps the levels OVER and feeds the path in the swapped order, driven to a
bound value against the crossed fixture where alpha.beta.pin and beta.alpha.pin are two
different real tables. Mutating the binds to [path[0], path[1], path[2]] kills exactly that
one test, 160 pass 0 fail becoming 159 pass 1 fail, and nothing else in the suite notices.

Seven mutations run and all seven killed: renaming the reply alias (the recipe rule 6
shape, 9 tests), dropping the schema from the source filter (8), origin to stored (1), form
hardcoded to complete (2), positional binds (1), dropping the whitespace-only check (1),
and folding away the absent-column shape (1).

Section 12 of the provider doc carries the same two limits the Object source section
states, because that table is what a reader scans before reaching the section.
…ile (#789)

The six curl lines carried the ClickHouse service's own password as a literal inside the
URL, which is the shape a credential scanner reports and the shape that goes stale the day
`database-compose.yml` changes it. The block now reads the value out of the file that
sets it. Measured: `awk '/CLICKHOUSE_PASSWORD/{print $2}' database-compose.yml` answers
`password123`, which is what the lines carried, so every command still runs as written.
…e dashes this branch added

X17 was measured in a real browser during this phase's final QA and was recorded only in a
gitignored work file, which under this repository's own rules is recorded nowhere. A Source
tab persisted by address is restored before the host's capabilities are consulted, so a host
that stops implementing the source reader between sessions gets a tab that still says
"Source: <name>" over an ordinary editable query editor with a live RUN button. The branch
that produces it is deliberate, documented at its site and pinned by a test; the defect is
only what the label tells a person the pane is.

Also drops the three em dashes this branch added under src/, which the repository's style
rules forbid and no gate catches. The en dashes in the backlog index predate this branch and
are left alone.

Verified: typecheck clean, backlog-structure 98 pass 0 fail (the index row was recomputed,
which the guard demanded), object-source components 33 pass 0 fail.

Refs #789
… a real one

A secret scanner reported this pull request, and this is the only credential on the branch
shaped like a genuine password: mixed case, a digit and punctuation. Every other fixture
credential in this repository is the trivially obvious value for its own context (postgres,
root, admin, druid), which is both honest about what it is and quiet to a scanner.

The login is local to docker/mssql-init and is referenced nowhere else, so the value is free
to change. CHECK_POLICY stays OFF, which is what lets a simple value be accepted.

Refs #789
…uage (#789)

`readObjectSource` never checked the path shape that `describeObject` has
checked since Phase 1. The HTTP route bounds an empty path, but the method is
published through `@libredb/studio` and reached by the embedded host seam and
by the conformance helper, none of which sees the route. Measured before the
fix: an empty path made the name `undefined` and ioredis threw
`undefined is not an object (evaluating 'arg.toUpperCase')` out of its command
encoder, this provider's defect arriving as the driver's. Both methods now go
through one `assertObjectPathShape`, so the rule and its sentence have one
writer and the accepted depth still comes from `declaredLevels`.

The `spec.sourceLanguage ?? "lua"` fallback is gone. Counted across the fleet:
nine of the eleven providers that read source raise on a missing language and
exactly two fell back, this one and Oracle. A fallback in two of eleven is not
a safety net, because the isolated census pins every declared language, so the
arm is only reachable by a declaration somebody deleted, and an unregistered
Monaco id degrades to plain text with nothing observable.

Found by the external review of PR #820, verified against this tree first.
… from its id (#789)

A part id is the engine's own word and the arrow-key focus move interpolated it into
'[role="tab"][data-part-id="..."]'. Measured on happy-dom 20: a part id of "char"(integer),
which standing ruling 2 records as a real PostgreSQL routine identity, raised a DOMException
out of the key handler and the arrow key did nothing, while the click path kept working.
Matched through dataset now, the way ObjectTree.tsx already matches a row id.
The route cut with a bare part.text.slice(0, limit) while all sixteen providers cut through
applySourceBound. The bound counts UTF-16 code units, so it can land between the halves of an
astral character: measured, a text holding an emoji at exactly the boundary came back ending in
a lone high surrogate, which JSON serialises as a lone escape and Monaco draws as a replacement
glyph. The mark is still composed at the route, which keeps a provider's own smaller bound's
sentence and joins to it.
#789)

Two findings from the external review of PR #820, both verified here first.

`isObjectNotFoundError` scanned the message for "ORA-31603" while node-oracledb
exposes the code as a numeric `errorNum`, which is the same defect class as
keying on a sentence: an error that merely quotes the code, a wrapped or a
logged one, made a read that failed for another reason look like a missing
object, and the second ALL_OBJECTS question was then asked about the wrong
fact. The number now decides. Measured on oracledb 6.10.0: thin assigns
`err.errorNum` in lib/thin/protocol/protocol.js and every prebuilt thick addon
in build/Release exports the same property name. An error carrying no numeric
`errorNum` at all still falls to the text scan, asked second, so a rejection
composed outside the driver is not lost.

`spec.sourceLanguage ?? "sql"` is gone. Nine of the eleven providers that read
source already raise here and only this one and redis fell back. The isolated
census pins all nine Oracle languages, so the arm is reachable only by a
deleted declaration, and an unregistered Monaco id degrades to plain text with
nothing observable, so the literal hid the deletion.
…at the route (#789)

The route applies SOURCE_CHARACTER_LIMIT and SOURCE_PART_LIMIT to every answer it serialises,
and the embedded shell calls no route at all: its document comes from a host function and went
straight into Monaco. The shape check now holds both bounds, and the refusal sentence is held to
the same number, because it is a text this viewer renders and the route carries it through
untouched. An overrun is a failed read rather than a silent truncation: this seam cannot say
whether the host already cut the text, so a truncation mark it composed would be a claim about a
cut it did not make.
… digit (#789)

The census greps zero times for `readObjectSource`. It pinned the DECLARATIONS
and not the pairing, so Phase 1's exact hole in its Phase 2 spelling, a kind
declaring `hasSource` with no method behind it, was visible only to
`assertObjectSurface`, which needs a live engine. On a machine with no
containers that guard does not exist.

Per type-id, and driven through the real factory: source-bearing kinds exist if
and only if the built provider implements `readObjectSource`. Both directions
were mutated. Stripping `hasSource` from redis's `function` leaves a method
nothing can reach and fails this test by name, 3 of 7 red. Adding
`hasSource: true` to druid's `datasource` leaves a Source tab with no reader
and fails it by name, 3 of 7 red. The population is asserted too, implementers
against the committed table and the two abstainers by name, because a
biconditional is satisfied by a population holding only one of its sides.

Also drops the stale digit from `tests/run-components.sh`: the comment said 33
where `TOTAL_GROUPS` is 44 and the grep it names prints 44. The comment now
names the command and no number, which is the only form that cannot go stale.

Found by the external review of PR #820.
…hod (#789)

The checklist said "the five object methods" and named them, so a provider
following it shipped without `readObjectSource`. It now names six and carries
the part of the contract a new provider cannot infer from the interface: the
`hasSource` and `sourceLanguage` declarations and why a missing language raises
instead of falling back to a literal, the path-shape check the method needs
because the route is not its only caller, the refusal-versus-absence rule as a
table (a refusal is a PART carrying the engine's own sentence, an object that
cannot be found RAISES, an empty text RAISES, a dropped transport raises a
ConnectionError), what `origin` and `form` state about the text, and the two
isolated tests that read the whole fleet at once and that no provider suite can
stand in for.

Found by the external review of PR #820.
…ne for another (#789)

One asked ref held the LAST address and dropped every answer whose address no longer matched,
so a pane moving from object A to object B threw A's in-flight answer away: A's tab went back to
nothing read and paid for a second round trip. Dropping on unmount is intended and stays; this
was the opposite. The ref is now a set of addresses this instance issued, and the answer is
written through the onChange captured when the read was issued, which in both shells names the
tab that asked.

What a dropped answer was standing in for is now checked directly: a document names the object it
is for, every provider writes path: [...path] beside the kind, and a document naming another
object is a failed read with its own sentence rather than a definition drawn under this object's
name. That also closes the reviewer's caption minor: the pane is still captioned by the object
asked for, and it no longer renders a stranger's definition under it.
…able" (#789)

D62 recorded the two source refusal arms as unverified on CockroachDB and
Materialize, and the doc still told a reader they were reachable. Both forks
were brought up, one at a time, and the shipped statements were run against
each.

CockroachDB v26.2.5 answers every one of them, `pg_get_viewdef`,
`pg_get_functiondef`, `p.prokind` and `pg_get_triggerdef`, so neither refusal
arm is needed there at all.

Materialize v26.40.0 answers SQLSTATE XX000 for each absent surface, not 42883
and not 42703, so `isMissingCatalogSurface()` does not match and the read
RAISES instead of answering a refusal part. The code is the server's and not
the harness's: in the same session 1/0 answers 22012 and a syntax error answers
42601. A third fact is sharper still: `pg_get_viewdef` returns NULL for a
MATERIALIZED VIEW while returning the definition for a plain view in the same
schema, and a NULL definition is read here as an absence, so a materialized
view that exists is reported as one the catalog does not hold.

The doc now carries the measurement as a table, the commands anybody can re-run
and the reason widening the arms to XX000 is not an obvious fix: XX000 is
Materialize's generic internal error. D62 said a third code makes that arm a
code change rather than a doc change, and `postgres.ts` is not this task's to
edit, so the code decision is reported rather than taken.
…finitions (#789)

A Source tab whose host no longer declares readObjectSource was read as an ordinary tab, and
this branch pinned that as correct. Half the reasoning holds and is kept: the viewer's default
reader posts to /api/db/objects/source and this package ships no routes, so the pane must never
fall through to it. What it missed is what a person sees. The source TEXT is never persisted,
only the address, so the tab came back named Source: app.order_total(integer) holding an empty,
editable editor with a live Run button, which is the empty-editor hazard this phase exists to
prevent, reached through a door nobody was watching.

The address now stands on its own, so there is no Run button, no toolbar and no statement loader
on a Source tab whatever the host currently declares, and the pane refuses in the viewer's own
failure grammar when there is nothing to show and nothing to read with. A definition already in
hand is left on screen. The no-route guard is this file's afterEach, which fails if any request
went out at all.
… tab (#789)

Fixed rather than deferred: StudioWorkspace keeps the pane when a host stops declaring
readObjectSource and refuses in the viewer's failure grammar, so no Source tab can present an
empty editable editor with a Run button. The filing also called the label the whole defect and
that was wrong: the text is never persisted, so what a person saw was an empty editor under a
Source name, not an editor holding the definition. The section index row is recomputed.
…route (#789)

A Source tab whose state is cleared while the host declares no reader would issue the read. The
only control that clears one is the viewer's stale banner, and this shell hands the viewer a
hardcoded refreshToken of 0, so it is never drawn. Written down at the conjunction rather than
left for a later author to rediscover.
…y omit (#789)

The checklist listed `readObjectSource` inside "the six object methods" and then said none can be
omitted and that a method whose data the engine does not expose returns a neutral value. Both halves
are false for this one method: `DatabaseProvider.readObjectSource?` is optional, a provider with no
source-bearing kind must not have it, and an empty text raises rather than answering a neutral value.
A contributor who followed the sentence would have written a neutral-valued method, declared no
`hasSource` kind, and failed the census pairing by name with nothing in the checklist to explain it.

The census subsection now also names the third thing a new type-id has to move, the committed
abstainer list, which it never mentioned. That list is hoisted to `CENSUS_ABSTAINERS` so the doc and
the assertion name one thing.

Two guards in the census file, because prose is not type-checked, and both refuse their own vacuity
by name. Mutations, baseline 9 pass 0 fail: `readObjectSource` put back in the omission sentence,
8/1 by name; the omission sentence reworded away, 8/1 on the vacuity throw; `druid` dropped from the
section, 8/1 by name; the section heading renamed, 8/1 on the vacuity throw; `cassandra` added to
`CENSUS_ABSTAINERS` without the doc, 7/2 by name.

The pairing test also gains the named zero-iteration guard its sibling already had: with the loop
head emptied it failed on an expect diff that named neither the loop nor the empty census, and it
now fails with "the pairing guard censused 0 type-ids", 8 pass 1 fail.

Two em dashes this branch added to this file are replaced with commas.
#789)

The Materialize section said the XX000 decision was "filed rather than taken". It was not: the two
defects it names, the refusal arms that raise on Materialize and `pg_get_viewdef` answering NULL for
a materialized view, are recorded in this section and nowhere else. A shipped doc pointing at a
filing nobody made is the dead-pointer class, so the sentence now says what is true, that this
section is the record. The backlog entries themselves belong to the file's owner, and the text for
them is in the task report.
Both shells branched the editor pane on `sourceTab === undefined ||
conn.activeConnection === null`, so a Source tab that was active when the
connection went away mounted `QueryToolbar` plus `QueryEditor`: a tab still
labelled `Source: <name>` over an EMPTY, EDITABLE buffer with a live Run
button. That is the composition this whole surface exists to prevent, through
a third door, and round 1 named it in a work file rather than fixing it or
filing it.

It is reached, not merely admitted by the type. MEASURED: with the old branch
restored, driving the embedded shell to an open Source tab and then handing it
`connections={[]}` renders `query-editor` with an empty `data-value` and a RUN
button beside it, under the tab name `Source: app.order_total(integer)`.
`use-connection-adapter.ts` auto-selects whenever the host's list is non-empty,
so a null active connection means exactly "the host handed an empty array",
which a host does when a person deletes the last connection in its own UI.

The conjunct existed only because `ObjectSourceView` could not take a null
connection. It now takes one and answers the one thing it can: no read is
issued, because there is nothing to read with, and the pane refuses in its own
failure grammar. A definition ALREADY IN HAND stays on screen, which is the
same decision the host-withdrawal arm makes one level up.

Three tests, RED before the change: the viewer's own refusal and in-hand arms,
the embedded shell driven with an empty connection list, and the standalone
shell driven by deleting the active connection under an open tab.
`isSourceDocumentShape` bounds a part's text and a refusal sentence at
`SOURCE_CHARACTER_LIMIT`, on the rule its own docblock states: a refusal is
bounded because it is a text this component renders. `truncated.reason` is
also a text this component renders, straight into the warning banner at
`ObjectSourceView`, from the same unbounded host path, and it carried no bound
at all.

MEASURED before the fix: a part carrying
`truncated: { limit: 1, reason: "r".repeat(SOURCE_CHARACTER_LIMIT * 5) }`
passed the predicate, so five million characters reached a `<div>` on the one
seam with no route in front of it, which is the whole premise of the bound.

An overrun is a failed read, never a silent cut, for the same reason it is for
a text. The new test carries a CONTROL exactly on the bound, so this is an
off-by-one assertion and not a refusal of everything large.
…789)

The route carries an over-long refusal SENTENCE through untouched, on purpose:
the sentence is the engine's own words, the refused arm carries no `truncated`
mark in the type, and slicing it would ship half a sentence. The client seam
now bounds that same sentence, so the route's test name, "carries a refused
part through untouched", promised a guarantee that no longer holds end to end:
on the standalone path a 1,000,010-character refusal reaches the viewer, fails
`isSourceDocumentShape` and draws "The source read answered with a body this
viewer cannot render." instead of the engine's sentence.

Neither side changes. The test says which end does what, and asserts BOTH:
the route's body still carries the whole sentence, and the client refuses that
body. Removing the client's refusal bound now fails this test as well as the
viewer's own, so the pair cannot drift back into a silent contradiction.
`namesThisObject`'s docblock shipped "every provider in the fleet writes it as
`path: [...path]` beside the `kind` it was asked for, 55 sites". The claim
holds; 55 counts something else. MEASURED on this tree: `path: [...path]`
occurs 55 times under `src/lib/db`, and 24 of those build a source DOCUMENT
(`grep -rn -A6 'path: \[\.\.\.path\]' src/lib/db | grep -cE '\bparts\b'`). The
other 31 are `describeObject` returns of the shape
`{ path: [...path], columns, indexes, foreignKeys }`, which carry no `kind`.

The number is corrected rather than deleted, with its basis command beside it,
and the docblock now says what actually holds a provider to the rule: the
conformance helper compares a document's `path` and `kind` against the request
it was built from. A count guards nothing.
…rivation (#789)

Nine providers open readObjectSource with the same preamble, verbatim: findKind,
then a throw for a kind the engine never declared, a throw for a declared kind that
publishes no definition text, and a throw for a source-bearing kind with no
sourceLanguage. The only thing that differed between the nine was the engine's
display name and its type id, so both become arguments and the preamble becomes
requireSourceKind, beside the other derivations in object-kinds.ts.

This is standing ruling 5h's shape: a helper written for the third time belongs in
one place, and this one was written nine times. SonarCloud's duplication report on
PR #820 named one of its copies as a block repeating across five providers at once,
which is how it was noticed, but the reason is the rule rather than the gate.

Every sentence is carried over unchanged. The provider suites assert on those
sentences and a reworded throw would be a behaviour change hiding inside a hoist.
…ders (#789)

sqlite, libsql, duckdb, clickhouse, cassandra, postgres, mssql, mysql and trino each
opened readObjectSource with the same seventeen lines. They now call
requireSourceKind and pass their own display name and type id, which is the only
thing that ever differed between the nine copies.

No sentence changed, and the provider suites are what proves it: rewording the
no-source sentence in the one shared guard fails nine tests across the nine suites,
disabling the hasSource check fails nine, and rewording the sourceLanguage sentence
fails nine. Every one of those was green before the hoist and is green after it.

The PostgreSQL suite was the one that did not pin the engine's name in either
sentence, so dropping the display name from the shared guard left it passing while
eight others failed. It pins both now: with the name reaching the guard as an
argument, a provider passing the wrong literal is a real failure mode and this is
the test that sees it.
…reach (#789)

requireSourceKind is the one readObjectSource entry guard and nine providers call
it. Six type-ids still open the method with their own copy: couchbase, mongodb,
redis, the shared search module serving elasticsearch and opensearch, and oracle.
All five modules collapse "the engine never declared this kind" and "the kind
publishes no definition text" into one sentence, which is less true than the two
it replaces because it tells the caller the kind exists.

Recorded rather than converted, for the reason the hoist gave: rewording those
throws is a behaviour change and five provider suites assert the exact wording.
The entry carries the route layer's fourth spelling of the same refusal too, and
the cost of leaving all of it, which is that the hoist's second-order gain holds
for nine of seventeen type-ids only.

The Drivers and connections index line moves with the entry: the range and the
count are derived from the bodies by tests/unit/backlog-structure.test.ts.
…#789)

The floor under the loop that checks all three requireSourceKind raises asserted
toHaveLength(3) on a SECOND freshly written literal, so it could not see the loop
at all. Measured: replacing the loop's array with [] as string[] left the file at
35 pass 0 fail, with expect() calls falling 60 to 48 as the only trace. That loop
is the only place in the repository that asserts raised.provider and instanceof
QueryError for the three arms, so in that state the helper could answer a bare
Error with no provider id and this suite would stay green. It now iterates a
named const, counts the iterations and throws by name: the same emptying is
34 pass 1 fail, "requireSourceKind arms: expected 3 iterations, ran 0".

The clickhouse and trino suites asserted the sourceLanguage refusal without the
engine's name, which is the gap already closed for PostgreSQL on the other two
arms. The name reaches the shared guard as an argument now, so a provider passing
the wrong literal would attribute one engine's refusal to another. Dropping the
display name from that raise left the two suites at 344 pass 0 fail before this
change and fails two tests by name after it.
@sonarqubecloud

Copy link
Copy Markdown

@cevheri cevheri self-assigned this Sep 13, 2026
@cevheri
cevheri merged commit aca5f5a into main Sep 13, 2026
27 checks passed
@cevheri
cevheri deleted the feat/object-source-phase-2 branch September 13, 2026 17:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants