feat(objects): read an object's definition text on fifteen type-ids (#789) - #820
Merged
Conversation
…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.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



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.
druidandlibredbdeliberately 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,tsqlandcqlare 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 declaredsourceLanguageis pinned against the real editor intests/isolated/monaco-language-ids.test.ts, and the compromises are named in each provider doc.JSON.stringifysilently 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 usesEJSON.stringifyrelaxed and renders the wholeoptionsdocument, not the two fields the design named.pg_get_*family applies no privilege check at all (probe 1, PostgreSQL 18.4): a role holding noUSAGE, noEXECUTEand noSELECTreads every character. So PostgreSQL has no unreadable case, measured rather than assumed.The five other probes: MariaDB's
SHOW CREATE PACKAGEneeds nosql_mode=ORACLEand an EXECUTE-only caller gets a row with a NULL body; Oracle'sEXECUTE ON DBMS_DDLis already granted to PUBLIC; SQL Server answers a row with a NULLdefinitionrather than no row, and all three causes are distinguishable in one query; ClickHouse'screate_querycarries the text, so itsfunctionkind keepshasSource; 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
packageandsequence(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 inpostgres.ts, D64 the unpinned trigger listing join, D65 a provider double that dispatches on its own statement, D66 the SQLite kind-vocabulary guard, D67assertObjectPathShapewritten eight times, D68bun run testred 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.jsondoes not move: every addition is optional and additive on the published surface, so neitherchart:bumpnor 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/objectsrequest 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 insideBEGIN READ ONLY, and no definition text and noobjects/sourcereaches 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 testis 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.