Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
73a3294
feat(objects): add the object source contract types and derivations (…
cevheri Sep 12, 2026
e147e6d
feat(objects): assert the source contract, and read a Redis function …
cevheri Sep 12, 2026
aa8adf0
fix(objects): refuse a part that is both a refusal and a text, and ra…
cevheri Sep 12, 2026
bab3f87
fix(objects): reattach the Redis parser docblock and stop claiming a …
cevheri Sep 12, 2026
48991f0
refactor(editor): give the two Monaco themes one owner (#789)
cevheri Sep 12, 2026
7a2a83d
test(objects): refuse a source-bearing kind expected at zero without …
cevheri Sep 12, 2026
f4d03b9
test(objects): check the half of an empty-kind reason a machine can s…
cevheri Sep 12, 2026
7a6dba9
feat(ui): add the object source caption, the sentence that says what …
cevheri Sep 12, 2026
e69978e
feat(ui): add the object source read seam and its client shape check …
cevheri Sep 12, 2026
d5d9fb9
feat(api): add the object source route (#789)
cevheri Sep 12, 2026
b19ad65
feat(ui): add the read-only object source viewer (#789)
cevheri Sep 12, 2026
d9df9ec
fix(api): refuse a source part that is both a refusal and a text (#789)
cevheri Sep 12, 2026
528f2e0
fix(ui): check the source document on BOTH entries and finish the tab…
cevheri Sep 12, 2026
c823648
feat(objects): read a PostgreSQL object's definition text (#789)
cevheri Sep 12, 2026
fbd4f59
docs(postgres): record the live source read, nine objects as a role h…
cevheri Sep 12, 2026
8ec952b
feat(oracle): read an object's definition, and say which absence ORA-…
cevheri Sep 12, 2026
eefb43c
test(postgres): pin every source statement's WHERE clause and pretty …
cevheri Sep 12, 2026
1ba8174
test(oracle): pin the GET_DDL metadata spelling of all nine kinds, an…
cevheri Sep 12, 2026
4ae4dc2
test(postgres): pin each source statement whole, not the tokens someb…
cevheri Sep 12, 2026
5143744
test(oracle): pin each source statement whole, not the tokens somebod…
cevheri Sep 12, 2026
58e6d95
docs(oracle): make three stale counts carry their basis, not a digit …
cevheri Sep 12, 2026
3018919
feat(mysql): read an object's definition on both servers, and say whi…
cevheri Sep 12, 2026
8cf22f6
test(sqlite,libsql): give both engines an openable fixture FILE, and …
cevheri Sep 12, 2026
c516a51
feat(mssql): read an object's definition, and say which of three NULL…
cevheri Sep 12, 2026
a61c122
feat(sqlite): read an object's definition, the one engine where the t…
cevheri Sep 12, 2026
3d96d81
feat(libsql): read an object's definition, with no refusal anyone can…
cevheri Sep 12, 2026
b7f45d7
fix(mssql): require the schema where the source statement names it (#…
cevheri Sep 12, 2026
3f6f729
fix(sqlite,libsql): say which of the three blank definitions it is, a…
cevheri Sep 12, 2026
0115e4e
test(duckdb): move the object fixture out of the suite and into a fil…
cevheri Sep 12, 2026
98fc50f
feat(duckdb): read an object's definition, and say when it is a body …
cevheri Sep 12, 2026
1f54f54
feat(clickhouse): read an object's definition text (#789)
cevheri Sep 12, 2026
fdc12e9
fix(clickhouse): pin the trailing-backslash escape and drive both no-…
cevheri Sep 12, 2026
bcacf05
fix(duckdb): refuse a non-text definition as its own shape, and withd…
cevheri Sep 12, 2026
c65d505
docs(duckdb): make two stale shape counts carry their basis, not a di…
cevheri Sep 12, 2026
699798a
test(objects): let the shared contract run in a container below the r…
cevheri Sep 12, 2026
bc69936
feat(cassandra): read an object's definition through the server's own…
cevheri Sep 12, 2026
09388bb
test(trino): give the fixture the four objects that defeat a shortcut…
cevheri Sep 13, 2026
70e285d
feat(trino): read an object's definition, and never claim a Hive view…
cevheri Sep 13, 2026
8d695de
docs(cassandra): withdraw a claim this phase made false, in both file…
cevheri Sep 13, 2026
dd4085b
docs(objects): name the zero the trino suite actually commits (#789)
cevheri Sep 13, 2026
40dc602
fix(trino): key the Hive view refusal on the fault name, not on the w…
cevheri Sep 13, 2026
1e03d6e
feat(mongodb): read a view's definition, and never print a reconstruc…
cevheri Sep 13, 2026
3c33033
docs(trino): make four stale function counts carry their basis, and g…
cevheri Sep 13, 2026
a8dfb03
fix(mongodb): raise a dropped MongoDB socket instead of printing it a…
cevheri Sep 13, 2026
a453359
feat(couchbase): read a SQL++ function's definition text (#789)
cevheri Sep 13, 2026
818c574
feat(search): read an object's definition on both search products, an…
cevheri Sep 13, 2026
08be027
test(search): drive all eight refusal arms and every unreadable body,…
cevheri Sep 13, 2026
0d91723
test(couchbase): assert the refusal document's own identity, which no…
cevheri Sep 13, 2026
ff0c2d5
feat(ui): offer View Source on every row whose kind declares one (#789)
cevheri Sep 13, 2026
73fff34
feat(ui): carry a Source tab's ADDRESS, and re-read rather than resto…
cevheri Sep 13, 2026
349916e
docs(providers): record why druid and libredb publish no definition t…
cevheri Sep 13, 2026
49d8541
feat(ui): open an object's source in a tab from the tree (#789)
cevheri Sep 13, 2026
99137c3
fix(ui): keep every Run entry point off a Source tab, and refuse a st…
cevheri Sep 13, 2026
9e82edf
fix(ui): close the last two Run entry points a Source tab left live (…
cevheri Sep 13, 2026
9efcfaf
style(druid): restore a docblock continuation prefix a comment edit d…
cevheri Sep 13, 2026
78d4516
test(objects): census every provider's source declarations through th…
cevheri Sep 13, 2026
1ae6c2b
test(editor): pin every declared sourceLanguage against the installed…
cevheri Sep 13, 2026
e05cb9c
feat(workspace): let a host answer the object source read (#789)
cevheri Sep 13, 2026
83cb2de
test(objects): correct two false MEASURED claims the census docblocks…
cevheri Sep 13, 2026
850d1f4
fix(workspace): make a host's source read a failed read rather than a…
cevheri Sep 13, 2026
fbf2331
docs(backlog): file the Phase 2 deferrals, twelve entries and one add…
cevheri Sep 13, 2026
dc70180
test(factory): isolate the one file that must load @/lib/db/factory f…
cevheri Sep 13, 2026
982cad3
test(objects): isolate the census, trim two unused exports, and file …
cevheri Sep 13, 2026
f55fe20
docs(backlog): file the two pre-known Studio UI deferrals (#789)
cevheri Sep 13, 2026
9b3fa1a
docs(backlog): make X15's printed basis a command that answers what i…
cevheri Sep 13, 2026
6b5ba57
fix(ui): keep the part a reader is on across a re-read (#789)
cevheri Sep 13, 2026
39b3b6d
docs(backlog): file the dev server's dead page at 127.0.0.1 (#789)
cevheri Sep 13, 2026
97105c2
test(ui): give the landing-patch assertion a floor it can fail on (#789)
cevheri Sep 13, 2026
b44a43e
test(objects): pin the four rows that carry no menu trigger, with a c…
cevheri Sep 13, 2026
e28b247
docs(clickhouse): derive the reproduction password from the compose f…
cevheri Sep 13, 2026
0d05f7e
docs(backlog): file a Source tab that becomes editable, and drop thre…
cevheri Sep 13, 2026
b88773d
test(mssql): give the fixture login a password that cannot be read as…
cevheri Sep 13, 2026
3604ecd
fix(redis): check the source read's path shape, and raise a lost lang…
cevheri Sep 13, 2026
7e3bb87
fix(ui): find a source part's tab by dataset, not by a selector built…
cevheri Sep 13, 2026
d98f767
fix(api): bound a source part through the fleet's one slicer (#789)
cevheri Sep 13, 2026
ce0a8fe
fix(oracle): settle ORA-31603 by its number, and raise a lost languag…
cevheri Sep 13, 2026
e36bc0d
fix(ui): bound a host's source document at the client seam, not only …
cevheri Sep 13, 2026
5c6d93f
test(objects): pin the PAIRING the census never saw, and drop a stale…
cevheri Sep 13, 2026
39a4e0c
docs(providers): give the new-provider checklist the sixth object met…
cevheri Sep 13, 2026
b21a272
fix(ui): keep a source answer for the object that asked, and refuse o…
cevheri Sep 13, 2026
2f5ee84
docs(postgres): measure both forks, and withdraw "both arms are reach…
cevheri Sep 13, 2026
e705b4c
fix(workspace): keep the pane and refuse when a host stops reading de…
cevheri Sep 13, 2026
213e3da
docs(backlog): drop X17, the Source tab that became an editable query…
cevheri Sep 13, 2026
f0119b7
docs(workspace): name the one path that could still reach the source …
cevheri Sep 13, 2026
d15c953
docs(providers): say the sixth object method is the one a provider ma…
cevheri Sep 13, 2026
c123b23
docs(postgres): stop claiming a backlog filing that does not exist ye…
cevheri Sep 13, 2026
a142591
fix(ui): keep a Source tab a pane when its connection goes away (#789)
cevheri Sep 13, 2026
7d2c670
fix(ui): bound the third host string this viewer renders (#789)
cevheri Sep 13, 2026
781d48d
test(api): pin both ends of the one input two files disagreed about (…
cevheri Sep 13, 2026
48afd83
docs(ui): make a shipped count name the population it counts (#789)
cevheri Sep 13, 2026
0301337
refactor(objects): hoist the readObjectSource entry guard into one de…
cevheri Sep 13, 2026
567084e
refactor(objects): call the shared source entry guard from nine provi…
cevheri Sep 13, 2026
dd44dca
docs(backlog): file the six providers the source guard hoist did not …
cevheri Sep 13, 2026
79a14c5
test(objects): give the three-arm guard a floor that can see the loop…
cevheri Sep 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions database-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -648,9 +648,21 @@ services:
#
# THERE IS NO ICEBERG CATALOG HERE, and that is a deliberate limit rather than an
# oversight. A materialized view is the one object kind Trino cannot create without one,
# and on 476 only a HIVE-METASTORE-backed Iceberg catalog will create one at all: the
# JDBC and REST catalog types both answer `createMaterializedView is not supported`. So
# an Iceberg catalog here would mean a metastore service, a warehouse volume and a
# and on 476 only a HIVE-METASTORE-backed Iceberg catalog will create one at all.
#
# THE CHEAP ROUTE WAS PROBED FOR #789 AND REFUSED, so the next person does not have to
# spend the same afternoon finding out. The hope was that Trino's Iceberg connector would
# take `iceberg.catalog.type=jdbc` against the `postgres` service above, giving a
# materialized-view fixture for the price of one properties file and a warehouse volume
# with NO new image in this file. Measured on 2026-09-13, the JDBC catalog was brought
# all the way up and WORKS - `CREATE SCHEMA`, `CREATE TABLE` and a two-row `INSERT` all
# succeeded - and `CREATE MATERIALIZED VIEW` still answered
# `createMaterializedView is not supported for Iceberg JDBC catalogs`. Two traps found on
# the way, both recorded in docs/providers/trino.md: Trino 476 never creates the JDBC
# catalog's own `iceberg_tables` table, and a `file://` warehouse needs
# `fs.hadoop.enabled=true` rather than the native local filesystem.
#
# So an Iceberg catalog here would mean a metastore service, a warehouse volume and a
# second image in this file for one object kind. The object surface therefore answers
# `materialized_view: 0` everywhere on this cluster, which is the TRUE answer for it, and
# docs/providers/trino.md carries the catalog properties and the exact statements that
Expand Down
29 changes: 29 additions & 0 deletions docker/clickhouse-init/01-object-fixture.sql
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,32 @@ ENGINE = Dictionary(demo.dict_customers);
-- A user-defined function. CREATE FUNCTION takes no database qualifier, because a
-- ClickHouse UDF is SERVER-GLOBAL: system.functions has no database column at all.
CREATE FUNCTION order_total_with_tax AS (total) -> total * 1.2;

-- Two ADVERSARIAL NAMES for the source read's escaper (#789).
--
-- Measured on 26.7.1.1315 (#789 probe 11): a backslash inside a QUOTED IDENTIFIER is
-- processed as an ESCAPE on this engine, in the double-quote form and the backtick form
-- alike, so a name ending in one swallows the closing quote and the parser runs on into
-- whatever follows. The source read therefore takes NO identifier position at all: every
-- caller-supplied name reaches its statements as a STRING LITERAL, where the same hazard
-- exists and the provider's `literal()` answers it by escaping the backslash as well as
-- doubling the quote.
--
-- These two tables are what makes a test of that non-vacuous rather than a claim in a
-- docblock. `bs_one\` stores exactly ONE trailing backslash (hex(name) = 62735F6F6E655C,
-- length 7), and reading its definition back with the backslash left unescaped fails with
-- code 62, `Single quoted string is not closed`, at the position of the clause that
-- followed. Renaming or dropping either one makes that measurement unrepeatable.
CREATE TABLE demo.`bs_one\\`
(
x UInt8
)
ENGINE = MergeTree
ORDER BY x;

CREATE TABLE demo.`dq"two`
(
x UInt8
)
ENGINE = MergeTree
ORDER BY x;
20 changes: 19 additions & 1 deletion docker/couchbase-init/01-object-fixture.sh
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,20 @@
# 7. `_default`.`airline` carries the SAME collection name as `inventory`.`airline`, with
# an index of the same name over a DIFFERENT key, so a scope-blind filter on a
# collection's indexes reports the wrong keys rather than merely the wrong count.
# 8. The two `discount` functions are also what the SOURCE READ is measured on
# (#789 Phase 2). Measured on Server 8.0.2 Community, `system:functions`
# answers `inventory`.`discount` as
# {"#language":"inline","expression":"(`price` - ((`price` * `pct`) / 100))",
# "parameters":["price","pct"],"text":"price - (price * pct / 100)"}.
# `text` is the body AS AUTHORED and `expression` is the engine's
# normalisation of it, which is the measurement behind reading `text` and
# captioning the part `origin: "stored"`. The two bodies differ from each
# other, so a read that matched on the name alone answers the wrong one
# rather than the same one by luck. There is no EXTERNAL JavaScript function
# here and there cannot be: Community Edition refuses to create one
# ("Functions of type javascript are only supported in Enterprise Edition",
# measured verbatim), which is why the provider's no-body refusal branch is
# driven by its suite and not by this fixture.
#
# The `_system` scope and its collections (`_mobile`, `_query`) are the server's
# own and are created by it, not here. They are what the `_system` exclusion is
Expand Down Expand Up @@ -121,7 +135,11 @@ n1ql "CREATE INDEX \`ix_name\` IF NOT EXISTS ON \`$BUCKET\`.\`_default\`.\`airli
# Enterprise Edition", measured, which is why the provider never classifies on
# definition.`#language`.
n1ql "CREATE OR REPLACE FUNCTION \`$BUCKET\`.\`inventory\`.\`discount\`(price, pct) { price - (price * pct / 100) }"
n1ql "CREATE OR REPLACE FUNCTION \`$BUCKET\`.\`_default\`.\`discount\`(x) { x }"
# The body is `x / 2` and not a single token on purpose: the object-surface
# conformance helper bounds the LONGEST definition it read and refuses a
# definition under two characters, because a one-character text cannot be told
# bounded from unbounded (#789).
n1ql "CREATE OR REPLACE FUNCTION \`$BUCKET\`.\`_default\`.\`discount\`(x) { x / 2 }"

# 4. A GLOBAL function, which must never reach the tree.
n1ql "CREATE OR REPLACE FUNCTION \`celsius\`(f) { (f - 32) / 1.8 }"
Expand Down
69 changes: 69 additions & 0 deletions docker/duckdb-init/01-object-fixture.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
-- The DuckDB object-surface and object-source fixture (#789).
--
-- Two catalogs, four schemas, and at least one object of every declared kind, so every
-- assertion `tests/integration/db/duckdb-provider.test.ts` makes about counts, listings,
-- detail and definition text is made about objects THIS file created.
--
-- Applied two ways, by one reader: the integration 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. Before #789 this DDL lived only inside the suite, which is the shape
-- standing ruling 5i forbids: a measurement nobody outside the test run can re-run.
--
-- THE SECOND CATALOG'S TARGET IS A PLACEHOLDER, and it has to be. `ATTACH ':memory:' AS
-- warehouse` gives the suite a second real catalog inside one process; a database FILE
-- needs a sibling file there instead, or every `warehouse` object vanishes the moment the
-- builder exits and a person opening the file finds one catalog where the tests saw two.
-- `readFixtureStatements()` substitutes {{warehouse}} and throws by name when nothing
-- substituted it.
--
-- DuckDB has NO trigger and NO stored procedure (both are parser errors on v1.5.5), so
-- neither appears below and neither is declared.

CREATE SCHEMA analytics;

CREATE TABLE main.customers (id INTEGER PRIMARY KEY, name VARCHAR NOT NULL, note VARCHAR DEFAULT 'none');
CREATE TABLE main.orders (id INTEGER PRIMARY KEY, customer_id INTEGER REFERENCES main.customers(id), total DECIMAL(12,2));
CREATE INDEX ix_orders_customer ON main.orders(customer_id);

CREATE TABLE analytics.events (id BIGINT, payload VARCHAR);

-- Two SAME-NAMED tables in two schemas, with different columns and an index each. This is
-- what a detail read's schema filter is for: without it `main.customers` and
-- `analytics.customers` merge, and the merged answer is a table with columns it does not
-- have rather than an error anybody would notice.
CREATE TABLE analytics.customers (event_id BIGINT);
CREATE TABLE analytics.orders (id INTEGER);
CREATE INDEX ix_orders_customer ON analytics.orders(id);

CREATE VIEW main.customer_names AS SELECT name FROM main.customers;
CREATE VIEW analytics.event_days AS SELECT id FROM analytics.events;

-- One macro of each form. A scalar macro is `function_type` 'macro' and a table macro is
-- 'table_macro'; they are the whole vocabulary `FUNCTION_TYPE_RULES` accounts for, and
-- both are here so the two arms cannot rot into one.
CREATE MACRO main.add_one(x) AS x + 1;
CREATE MACRO analytics.recent_events(n) AS TABLE SELECT * FROM analytics.events LIMIT n;

CREATE SEQUENCE main.customer_seq START 1;
CREATE SEQUENCE analytics.event_seq START 100;

-- One name, three kinds, in one schema, and it is the reason `readObjectSource` takes the
-- KIND as well as the path. Measured on DuckDB v1.5.5: `CREATE SEQUENCE overlap` and
-- `CREATE MACRO overlap(x)` both succeed while the table `overlap` exists, and only
-- `CREATE VIEW overlap` is refused (`Catalog Error: Table with name "overlap" already
-- exists!`). A source read keyed on the name alone answers one of the three at random.
CREATE TABLE main.overlap (id INTEGER);
CREATE SEQUENCE main.overlap;
CREATE MACRO main.overlap(x) AS x;

-- A second real catalog, with a schema set of its own and no macro or sequence, so a
-- declared-and-empty folder has somewhere to be measured.
ATTACH '{{warehouse}}' AS warehouse;
CREATE SCHEMA warehouse.stock;
CREATE TABLE warehouse.main.ledger (id INTEGER);
-- Same SCHEMA name, same TABLE name, different CATALOG. This is the two-level engine's
-- characteristic case and the only thing that can show the catalog filter working: a
-- detail read that dropped `database_name = $1` would merge `memory.main.customers` with
-- this one and report a table with columns from both.
CREATE TABLE warehouse.main.customers (sku VARCHAR);
CREATE TABLE warehouse.stock.items (sku VARCHAR);
167 changes: 167 additions & 0 deletions docker/duckdb-init/build-fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/**
* The DuckDB object-surface and object-source fixture, read and applied (#789).
*
* Two callers and one reader. `tests/integration/db/duckdb-provider.test.ts` calls
* `readFixtureStatements(":memory:")` and replays the result into an in-memory database, so
* the objects its object-surface and Source assertions reason about are created BY
* `01-object-fixture.sql`; running this file as a script replays the same statements into a
* database FILE a person can open in Studio. Before #789 the DDL lived only inside the
* suite, which is the shape standing ruling 5i forbids: a live measurement nobody outside
* the test run can re-run.
*
* Run it:
*
* bun docker/duckdb-init/build-fixture.ts # ./.duckdb-fixture/object-fixture.duckdb
* bun docker/duckdb-init/build-fixture.ts /tmp/demo.duckdb # anywhere else
*
* It is re-runnable: an existing file at the target path is removed first, together with
* the `.wal` sidecar a crashed session can leave beside it and the warehouse sibling below,
* so a second run produces a database identical to the first rather than one holding both
* runs' objects.
*
* THE SECOND CATALOG IS WHY THIS FILE SUBSTITUTES A PLACEHOLDER RATHER THAN JUST SPLITTING.
* The fixture's `ATTACH '{{warehouse}}' AS warehouse` is a second REAL catalog, and the two
* callers need two different targets: the suite wants `:memory:`, which lives and dies with
* the process, and a file build wants a sibling file, or every `warehouse` object vanishes
* when the builder exits and a person opening the result finds one catalog where the tests
* saw two.
*
* ONE LIMIT, STATED HERE RATHER THAN DISCOVERED: DuckDB does NOT persist an attachment
* inside a database file. So opening the built file shows the `memory` half only, and the
* second catalog is reached with `ATTACH '<target>.warehouse.duckdb' AS warehouse` in a
* query tab. The sibling is still built, because the alternative - dropping the second
* catalog from the file build - would give a person a fixture that cannot show the two
* container levels this engine's whole object model is about.
*/
import { DuckDBInstance } from "@duckdb/node-api";
import * as fs from "fs";
import * as path from "path";

/** Where the fixture lives, so a caller names a file rather than a path. */
export const FIXTURE_DIRECTORY = import.meta.dir;

/** The fixture the DuckDB provider's own suite replays. */
export const DUCKDB_FIXTURE_FILE = "01-object-fixture.sql";

/** The default target, relative to the repository root. */
export const DEFAULT_FIXTURE_PATH = ".duckdb-fixture/object-fixture.duckdb";

/**
* The token `ATTACH` names its target with, and the one thing in this fixture that is not
* literal SQL.
*
* Spelled once and exported, so the substitution and the guard that it happened cannot
* disagree about what they are looking for.
*/
export const WAREHOUSE_PLACEHOLDER = "{{warehouse}}";

/** The second catalog's target for a file build, derived from the target's own path. */
export function warehouseSibling(file: string): string {
return `${file}.warehouse.duckdb`;
}

/**
* Every statement of the fixture, comments removed, in the file's own order, with the
* second catalog's target substituted in.
*
* A line whose first non-blank characters are `--` is dropped whole; there is no `--`
* inside a string literal in this fixture, and a splitter that pretended otherwise would be
* claiming a lexer it does not have. A statement ends at a line ending in `;`, which is
* enough here for a reason DuckDB gives rather than luck: this engine has no trigger and no
* stored procedure, so no statement in the fixture carries a body holding its own
* semicolon. That is the trap `docker/sqlite-init/build-fixture.ts` needs a trigger rule
* for, and it cannot arise on this engine.
*
* THREE THROWS, each over a zero that would otherwise pass silently:
*
* - no statement at all, which builds an EMPTY database and makes every count assertion
* downstream read zero against zero. The same throw covers a path resolving to the wrong
* file;
* - no substitution, which would send DuckDB the literal string `{{warehouse}}` as a file
* PATH. Measured on v1.5.5: that ATTACH succeeds and creates a file called
* `{{warehouse}}` in the working directory, so the failure is silent litter rather than
* an error, and the suite would still see two catalogs;
* - an empty warehouse target, because `ATTACH '' AS warehouse` is the same shape one step
* further along.
*/
export function readFixtureStatements(warehouse: string, file: string = DUCKDB_FIXTURE_FILE): string[] {
if (warehouse.trim() === "") {
throw new Error("the warehouse target is blank, so the fixture's second catalog would attach nothing");
}
const resolved = path.isAbsolute(file) ? file : path.join(FIXTURE_DIRECTORY, file);
const text = fs.readFileSync(resolved, "utf8");
const statements: string[] = [];
let buffer = "";
for (const line of text.split("\n")) {
if (line.trim().startsWith("--")) continue;
const body = line.trimEnd();
if (!body.endsWith(";")) {
buffer += `${line}\n`;
continue;
}
const statement = `${buffer}${body.slice(0, -1)}`.trim();
if (statement !== "") statements.push(statement);
buffer = "";
}
if (statements.length === 0) {
throw new Error(`${resolved} yielded no statement, so applying it would build an empty database`);
}
const substituted = statements.map((statement) => statement.split(WAREHOUSE_PLACEHOLDER).join(warehouse));
if (substituted.every((statement, index) => statement === statements[index])) {
throw new Error(
`${resolved} carries no ${WAREHOUSE_PLACEHOLDER} to substitute, so the second catalog would attach a file ` +
"named after the placeholder itself",
);
}
return substituted;
}

/**
* Delete a DuckDB database, the `.wal` sidecar, and the warehouse sibling beside it.
*
* ABSENCE IS THE ONLY FAILURE THIS SWALLOWS, and the narrowing is the point: the file not
* being there IS the state this function is asked for, while any other errno means the file
* is still on disk. A bare `catch {}` reported success there, and `buildObjectFixture` then
* opened the STALE database and replayed the fixture DDL on top of it, so the first failure
* a person saw was `CREATE TABLE main.customers` raising a catalog error that named nothing
* about an undeleted file. Raising here is also the house rule: no silent recovery and no
* symptom-masking guard.
*/
export function removeDatabaseFile(file: string): void {
for (const target of [file, `${file}.wal`, warehouseSibling(file), `${warehouseSibling(file)}.wal`]) {
try {
fs.unlinkSync(target);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
}
}

/** Build the fixture at `file`, replacing whatever is there, with its warehouse sibling. */
export async function buildObjectFixture(file: string): Promise<void> {
removeDatabaseFile(file);
fs.mkdirSync(path.dirname(file), { recursive: true });
const instance = await DuckDBInstance.create(file);
const connection = await instance.connect();
try {
for (const statement of readFixtureStatements(warehouseSibling(file))) await connection.run(statement);
// Both catalogs, so neither file is left holding an uncheckpointed write-ahead log a
// reader would have to replay. The first is spelled bare rather than by name: a DuckDB
// file's catalog is named after its own stem, so the connected catalog here is
// `object-fixture` and never the `memory` the in-process suite sees.
await connection.run("CHECKPOINT");
await connection.run("CHECKPOINT warehouse");
} finally {
connection.closeSync();
instance.closeSync();
}
}

if (import.meta.main) {
const target = path.resolve(process.argv[2] ?? DEFAULT_FIXTURE_PATH);
await buildObjectFixture(target);
process.stdout.write(
`DuckDB object fixture written to ${target}\n` +
`Second catalog: ${warehouseSibling(target)} (ATTACH '${warehouseSibling(target)}' AS warehouse)\n`,
);
}
Loading
Loading