@fabricorg/databricks-testkit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +82 -0
- package/dist/chunk-7JVDOSBO.js +94 -0
- package/dist/chunk-7JVDOSBO.js.map +1 -0
- package/dist/chunk-F3KU6VZS.js +56 -0
- package/dist/chunk-F3KU6VZS.js.map +1 -0
- package/dist/chunk-FBADA7LQ.js +195 -0
- package/dist/chunk-FBADA7LQ.js.map +1 -0
- package/dist/chunk-MLBZCHRM.js +37 -0
- package/dist/chunk-MLBZCHRM.js.map +1 -0
- package/dist/duckdb-context-FX23RUBV.js +4 -0
- package/dist/duckdb-context-FX23RUBV.js.map +1 -0
- package/dist/fixtures-CAWhzIHu.d.cts +27 -0
- package/dist/fixtures-CAWhzIHu.d.ts +27 -0
- package/dist/index.cjs +1152 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +301 -0
- package/dist/index.d.ts +301 -0
- package/dist/index.js +690 -0
- package/dist/index.js.map +1 -0
- package/dist/parity.cjs +411 -0
- package/dist/parity.cjs.map +1 -0
- package/dist/parity.d.cts +17 -0
- package/dist/parity.d.ts +17 -0
- package/dist/parity.js +45 -0
- package/dist/parity.js.map +1 -0
- package/dist/workspace-context-AIUYISUS.js +4 -0
- package/dist/workspace-context-AIUYISUS.js.map +1 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fabric
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# @fabricorg/databricks-testkit
|
|
2
|
+
|
|
3
|
+
Testing toolkit for Databricks artifacts (ADR-0006). One `ExecutionContext`,
|
|
4
|
+
two profiles:
|
|
5
|
+
|
|
6
|
+
- **local** — DuckDB in-process (`@duckdb/node-api`, optional peer). No
|
|
7
|
+
workspace, no credentials, sub-second. The default.
|
|
8
|
+
- **live** — a real Databricks SQL warehouse via the SQL Statement Execution
|
|
9
|
+
API, built on `@fabric-harness/databricks`. Always gated by
|
|
10
|
+
`DBX_TEST_LIVE=1`.
|
|
11
|
+
|
|
12
|
+
Positional `?` params work in both profiles — the live context transparently
|
|
13
|
+
rewrites them to the named markers the Statement Execution API requires.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { createExecutionContext, expectTable } from '@fabricorg/databricks-testkit';
|
|
17
|
+
|
|
18
|
+
const ctx = await createExecutionContext(); // DBX_TEST_PROFILE=local|live
|
|
19
|
+
await ctx.loadFixture({
|
|
20
|
+
table: 'conversions',
|
|
21
|
+
schema: 'subject_id STRING, value DOUBLE, "at" TIMESTAMP',
|
|
22
|
+
rows: [['s1', 49, '2026-07-01T12:00:00Z']],
|
|
23
|
+
});
|
|
24
|
+
await expectTable(ctx, 'conversions').toHaveRowCount(1);
|
|
25
|
+
await ctx.close();
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Cross-engine parity suites
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
// vitest file — import from the /parity subpath
|
|
32
|
+
import { defineParitySuite } from '@fabricorg/databricks-testkit/parity';
|
|
33
|
+
|
|
34
|
+
defineParitySuite({
|
|
35
|
+
name: 'aggregate-attribution',
|
|
36
|
+
sql: aggregateSql,
|
|
37
|
+
params,
|
|
38
|
+
fixtures: [...],
|
|
39
|
+
golden: 'golden/aggregate-attribution.json',
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
DuckDB must reproduce the committed golden rows on every run; with
|
|
44
|
+
`DBX_TEST_LIVE=1` the same statement also runs on Databricks and both engines
|
|
45
|
+
must agree. Create/refresh goldens with `DBX_TEST_UPDATE_GOLDEN=1` and commit
|
|
46
|
+
the diff. See `src/parity.dogfood.test.ts` for the canonical example (it
|
|
47
|
+
locks the product's own aggregate SQL).
|
|
48
|
+
|
|
49
|
+
## Unit-testing orchestration logic
|
|
50
|
+
|
|
51
|
+
`createMockDatabricksClient()` is a framework-free fake of the REST client
|
|
52
|
+
surface — queue responses per (method, path-prefix) to drive submit → poll →
|
|
53
|
+
terminal state machines without a workspace.
|
|
54
|
+
|
|
55
|
+
## Live environment
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
DBX_TEST_LIVE=1
|
|
59
|
+
DATABRICKS_HOST=https://<workspace>.cloud.databricks.com
|
|
60
|
+
DATABRICKS_HTTP_PATH=/sql/1.0/warehouses/<id> # or DATABRICKS_WAREHOUSE_ID
|
|
61
|
+
DBX_TEST_CATALOG=fx_test
|
|
62
|
+
DBX_TEST_SCHEMA=scenarios
|
|
63
|
+
# Seeded live tables are replaced on load and dropped on close. For debugging only:
|
|
64
|
+
# DBX_TEST_KEEP_FIXTURES=1
|
|
65
|
+
# auth precedence: DATABRICKS_BEARER → DATABRICKS_CLIENT_ID/SECRET (OAuth M2M) → DATABRICKS_TOKEN (PAT)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Databricks workload checks
|
|
69
|
+
|
|
70
|
+
The live suite includes SQL, Delta contracts, Unity Catalog access, Jobs,
|
|
71
|
+
Lakeflow pipelines, Lakebase credential exchange and provider refresh, Unity
|
|
72
|
+
Catalog Volume I/O, Databricks Apps/resource health, secret metadata, isolated
|
|
73
|
+
Auto Loader ingestion, notebook submit, and dbt build. Checks are independently
|
|
74
|
+
configured and report `pass | fail | not-configured`; only IDs listed in
|
|
75
|
+
`DBX_TEST_REQUIRED_CHECKS` gate a run.
|
|
76
|
+
|
|
77
|
+
Auto Loader full refreshes require a distinct `DBX_TEST_AUTOLOADER_PIPELINE_ID`.
|
|
78
|
+
The check refuses to use `DBX_TEST_PIPELINE_ID`, protecting production telemetry.
|
|
79
|
+
|
|
80
|
+
Docs: `apps/docs/content/docs/testing/` · Plan:
|
|
81
|
+
`.weave/plans/databricks-testing.md` · BDD layer: `@fabricorg/databricks-bdd`
|
|
82
|
+
(Phase 2).
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { parseFixtureColumns, resolveFixtureRows } from './chunk-F3KU6VZS.js';
|
|
2
|
+
|
|
3
|
+
// src/duckdb-context.ts
|
|
4
|
+
async function createDuckDbContext(options = {}) {
|
|
5
|
+
const factory = options.connectionFactory ?? defaultConnectionFactory;
|
|
6
|
+
const conn = await factory(options.path ?? ":memory:");
|
|
7
|
+
const schema = options.schema;
|
|
8
|
+
if (schema) await conn.run(`CREATE SCHEMA IF NOT EXISTS ${schema}`);
|
|
9
|
+
const qual = (table) => schema ? `${schema}.${table}` : table;
|
|
10
|
+
return {
|
|
11
|
+
profile: "local",
|
|
12
|
+
qual,
|
|
13
|
+
async exec(sql, params) {
|
|
14
|
+
await conn.run(sql, params ? [...params] : void 0);
|
|
15
|
+
},
|
|
16
|
+
async query(sql, params) {
|
|
17
|
+
const reader = await conn.runAndReadAll(sql, params ? [...params] : void 0);
|
|
18
|
+
return reader.getRowObjects().map(normalizeRow);
|
|
19
|
+
},
|
|
20
|
+
async loadFixture(fixture) {
|
|
21
|
+
if (fixture.schema) {
|
|
22
|
+
const columns2 = parseFixtureColumns(fixture.schema);
|
|
23
|
+
const ddl = columns2.map((c) => `"${c.name}" ${portableType(c.type)}`).join(", ");
|
|
24
|
+
await conn.run(`CREATE TABLE IF NOT EXISTS ${qual(fixture.table)} (${ddl})`);
|
|
25
|
+
const rows2 = await resolveFixtureRows(fixture, columns2);
|
|
26
|
+
for (const row of rows2) {
|
|
27
|
+
const casts = columns2.map((c) => isTimestamp(c.type) ? "CAST(? AS TIMESTAMP)" : "?").join(", ");
|
|
28
|
+
await conn.run(`INSERT INTO ${qual(fixture.table)} VALUES (${casts})`, row);
|
|
29
|
+
}
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const described = await conn.runAndReadAll(`DESCRIBE ${qual(fixture.table)}`);
|
|
33
|
+
const columns = described.getRowObjects().map((r) => ({ name: String(r.column_name), type: String(r.column_type) }));
|
|
34
|
+
const rows = await resolveFixtureRows(fixture, columns);
|
|
35
|
+
for (const row of rows) {
|
|
36
|
+
const casts = columns.map((c) => isTimestamp(c.type) ? "CAST(? AS TIMESTAMP)" : "?").join(", ");
|
|
37
|
+
await conn.run(`INSERT INTO ${qual(fixture.table)} VALUES (${casts})`, row);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
async close() {
|
|
41
|
+
conn.closeSync();
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function portableType(type) {
|
|
46
|
+
return type.replace(/\bSTRING\b/gi, "VARCHAR");
|
|
47
|
+
}
|
|
48
|
+
function isTimestamp(type) {
|
|
49
|
+
return /TIMESTAMP/i.test(type);
|
|
50
|
+
}
|
|
51
|
+
function normalizeRow(row) {
|
|
52
|
+
const out = {};
|
|
53
|
+
for (const [key, value] of Object.entries(row)) {
|
|
54
|
+
out[key] = normalizeValue(value);
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
function normalizeValue(value) {
|
|
59
|
+
if (typeof value === "bigint") {
|
|
60
|
+
if (value > BigInt(Number.MAX_SAFE_INTEGER) || value < BigInt(-Number.MAX_SAFE_INTEGER)) {
|
|
61
|
+
return value.toString();
|
|
62
|
+
}
|
|
63
|
+
return Number(value);
|
|
64
|
+
}
|
|
65
|
+
if (value !== null && typeof value === "object") {
|
|
66
|
+
const v = value;
|
|
67
|
+
if (typeof v.micros === "bigint") {
|
|
68
|
+
const millis = Number(v.micros / 1000n);
|
|
69
|
+
return new Date(millis).toISOString().replace("T", " ").replace(/\.\d+Z$/, "");
|
|
70
|
+
}
|
|
71
|
+
if (typeof v.scale === "number" && typeof v.value === "bigint") {
|
|
72
|
+
return Number(v.value) / 10 ** v.scale;
|
|
73
|
+
}
|
|
74
|
+
return String(value);
|
|
75
|
+
}
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
async function defaultConnectionFactory(path) {
|
|
79
|
+
const moduleName = "@duckdb/node-api";
|
|
80
|
+
let mod;
|
|
81
|
+
try {
|
|
82
|
+
mod = await import(moduleName);
|
|
83
|
+
} catch {
|
|
84
|
+
throw new Error(
|
|
85
|
+
"createDuckDbContext requires the optional peer @duckdb/node-api \u2014 run: pnpm add -D @duckdb/node-api"
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
const instance = await mod.DuckDBInstance.create(path);
|
|
89
|
+
return await instance.connect();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export { createDuckDbContext, normalizeRow };
|
|
93
|
+
//# sourceMappingURL=chunk-7JVDOSBO.js.map
|
|
94
|
+
//# sourceMappingURL=chunk-7JVDOSBO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/duckdb-context.ts"],"names":["columns","rows"],"mappings":"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","file":"chunk-7JVDOSBO.js","sourcesContent":["import type { ExecutionContext } from './context.js';\nimport { type TableFixture, parseFixtureColumns, resolveFixtureRows } from './fixtures.js';\n\nexport interface DuckDbContextOptions {\n /** Database file path; ':memory:' (default) for a throwaway in-process db. */\n path?: string;\n /** Optional schema to create + qualify tables with. */\n schema?: string;\n /** Injectable for tests of this module itself. */\n connectionFactory?: (path: string) => Promise<DuckDbConnectionLike>;\n}\n\n/** Structural slice of the lazily-imported @duckdb/node-api connection. */\nexport interface DuckDbConnectionLike {\n run(sql: string, values?: unknown[]): Promise<unknown>;\n runAndReadAll(\n sql: string,\n values?: unknown[],\n ): Promise<{ getRowObjects(): Record<string, unknown>[] }>;\n closeSync(): void;\n}\n\n/**\n * Hermetic local execution context on DuckDB — the `local` profile of the\n * testing pyramid. No workspace, no credentials, sub-second.\n */\nexport async function createDuckDbContext(\n options: DuckDbContextOptions = {},\n): Promise<ExecutionContext> {\n const factory = options.connectionFactory ?? defaultConnectionFactory;\n const conn = await factory(options.path ?? ':memory:');\n const schema = options.schema;\n if (schema) await conn.run(`CREATE SCHEMA IF NOT EXISTS ${schema}`);\n const qual = (table: string) => (schema ? `${schema}.${table}` : table);\n\n return {\n profile: 'local',\n qual,\n async exec(sql, params) {\n await conn.run(sql, params ? [...params] : undefined);\n },\n async query(sql, params) {\n const reader = await conn.runAndReadAll(sql, params ? [...params] : undefined);\n return reader.getRowObjects().map(normalizeRow);\n },\n async loadFixture(fixture: TableFixture) {\n if (fixture.schema) {\n const columns = parseFixtureColumns(fixture.schema);\n const ddl = columns.map((c) => `\"${c.name}\" ${portableType(c.type)}`).join(', ');\n await conn.run(`CREATE TABLE IF NOT EXISTS ${qual(fixture.table)} (${ddl})`);\n const rows = await resolveFixtureRows(fixture, columns);\n for (const row of rows) {\n const casts = columns\n .map((c) => (isTimestamp(c.type) ? 'CAST(? AS TIMESTAMP)' : '?'))\n .join(', ');\n await conn.run(`INSERT INTO ${qual(fixture.table)} VALUES (${casts})`, row);\n }\n return;\n }\n // Table pre-exists: infer column order from the catalog.\n const described = await conn.runAndReadAll(`DESCRIBE ${qual(fixture.table)}`);\n const columns = described\n .getRowObjects()\n .map((r) => ({ name: String(r.column_name), type: String(r.column_type) }));\n const rows = await resolveFixtureRows(fixture, columns);\n for (const row of rows) {\n const casts = columns\n .map((c) => (isTimestamp(c.type) ? 'CAST(? AS TIMESTAMP)' : '?'))\n .join(', ');\n await conn.run(`INSERT INTO ${qual(fixture.table)} VALUES (${casts})`, row);\n }\n },\n async close() {\n conn.closeSync();\n },\n };\n}\n\n/** Databricks types → DuckDB types for the portable fixture subset. */\nfunction portableType(type: string): string {\n return type.replace(/\\bSTRING\\b/gi, 'VARCHAR');\n}\n\nfunction isTimestamp(type: string): boolean {\n return /TIMESTAMP/i.test(type);\n}\n\n/**\n * Normalize DuckDB values to plain JSON-ish values so assertions and golden\n * files are engine-portable: bigint→number, TIMESTAMP→`YYYY-MM-DD HH:MM:SS`\n * (UTC, microseconds trimmed), DECIMAL→number.\n */\nexport function normalizeRow(row: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(row)) {\n out[key] = normalizeValue(value);\n }\n return out;\n}\n\nfunction normalizeValue(value: unknown): unknown {\n if (typeof value === 'bigint') {\n if (value > BigInt(Number.MAX_SAFE_INTEGER) || value < BigInt(-Number.MAX_SAFE_INTEGER)) {\n return value.toString();\n }\n return Number(value);\n }\n if (value !== null && typeof value === 'object') {\n const v = value as { micros?: bigint; width?: number; scale?: number; value?: bigint };\n if (typeof v.micros === 'bigint') {\n // DuckDBTimestampValue — micros since epoch, UTC.\n const millis = Number(v.micros / 1000n);\n return new Date(millis)\n .toISOString()\n .replace('T', ' ')\n .replace(/\\.\\d+Z$/, '');\n }\n if (typeof v.scale === 'number' && typeof v.value === 'bigint') {\n // DuckDBDecimalValue.\n return Number(v.value) / 10 ** v.scale;\n }\n return String(value);\n }\n return value;\n}\n\nasync function defaultConnectionFactory(path: string): Promise<DuckDbConnectionLike> {\n const moduleName = '@duckdb/node-api';\n let mod: { DuckDBInstance: { create(path: string): Promise<{ connect(): Promise<unknown> }> } };\n try {\n mod = (await import(moduleName)) as typeof mod;\n } catch {\n throw new Error(\n 'createDuckDbContext requires the optional peer @duckdb/node-api — run: pnpm add -D @duckdb/node-api',\n );\n }\n const instance = await mod.DuckDBInstance.create(path);\n return (await instance.connect()) as DuckDbConnectionLike;\n}\n"]}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// src/fixtures.ts
|
|
2
|
+
function parseFixtureColumns(schema) {
|
|
3
|
+
const columns = [];
|
|
4
|
+
let depth = 0;
|
|
5
|
+
let current = "";
|
|
6
|
+
const parts = [];
|
|
7
|
+
for (const ch of schema) {
|
|
8
|
+
if (ch === "(") depth++;
|
|
9
|
+
if (ch === ")") depth--;
|
|
10
|
+
if (ch === "," && depth === 0) {
|
|
11
|
+
parts.push(current);
|
|
12
|
+
current = "";
|
|
13
|
+
} else {
|
|
14
|
+
current += ch;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (current.trim()) parts.push(current);
|
|
18
|
+
for (const part of parts) {
|
|
19
|
+
const trimmed = part.trim();
|
|
20
|
+
const match = trimmed.match(/^("([^"]+)"|`([^`]+)`|(\w+))\s+(.+)$/s);
|
|
21
|
+
if (!match) throw new Error(`Unparseable fixture column: '${trimmed}'`);
|
|
22
|
+
columns.push({
|
|
23
|
+
name: match[2] ?? match[3] ?? match[4] ?? "",
|
|
24
|
+
type: (match[5] ?? "").trim()
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
if (columns.length === 0) throw new Error("Fixture schema declared no columns");
|
|
28
|
+
return columns;
|
|
29
|
+
}
|
|
30
|
+
async function resolveFixtureRows(fixture, columns) {
|
|
31
|
+
const raw = [...fixture.rows ?? []];
|
|
32
|
+
if (fixture.ndjsonFile) {
|
|
33
|
+
const fs = await import('fs/promises');
|
|
34
|
+
const text = await fs.readFile(fixture.ndjsonFile, "utf8");
|
|
35
|
+
for (const line of text.split("\n")) {
|
|
36
|
+
if (line.trim().length === 0) continue;
|
|
37
|
+
raw.push(JSON.parse(line));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return raw.map((row) => {
|
|
41
|
+
if (Array.isArray(row)) {
|
|
42
|
+
if (row.length !== columns.length) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Fixture row for ${fixture.table} has ${row.length} values, expected ${columns.length}`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return [...row];
|
|
48
|
+
}
|
|
49
|
+
const record = row;
|
|
50
|
+
return columns.map((c) => c.name in record ? record[c.name] : null);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { parseFixtureColumns, resolveFixtureRows };
|
|
55
|
+
//# sourceMappingURL=chunk-F3KU6VZS.js.map
|
|
56
|
+
//# sourceMappingURL=chunk-F3KU6VZS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/fixtures.ts"],"names":[],"mappings":"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","file":"chunk-F3KU6VZS.js","sourcesContent":["/**\n * Declarative, engine-agnostic table seeds. The same fixture applies to a\n * local DuckDB table or a scratch Unity Catalog schema — column DDL uses the\n * portable subset (VARCHAR/STRING, BIGINT, DOUBLE, TIMESTAMP, BOOLEAN).\n */\nexport interface TableFixture {\n table: string;\n /**\n * Column DDL, e.g. `subject_id STRING, value DOUBLE, \"at\" TIMESTAMP`.\n * Optional when the table already exists in the target context.\n */\n schema?: string;\n /** Row tuples in column order, or row objects keyed by column name. */\n rows?: readonly (readonly unknown[] | Record<string, unknown>)[];\n /** Path to an NDJSON file of row objects (one JSON object per line). */\n ndjsonFile?: string;\n}\n\nexport interface FixtureColumn {\n name: string;\n type: string;\n}\n\n/** Parse `name TYPE, name TYPE(args), \"quoted\" TYPE` column DDL into columns. */\nexport function parseFixtureColumns(schema: string): FixtureColumn[] {\n const columns: FixtureColumn[] = [];\n let depth = 0;\n let current = '';\n const parts: string[] = [];\n for (const ch of schema) {\n if (ch === '(') depth++;\n if (ch === ')') depth--;\n if (ch === ',' && depth === 0) {\n parts.push(current);\n current = '';\n } else {\n current += ch;\n }\n }\n if (current.trim()) parts.push(current);\n for (const part of parts) {\n const trimmed = part.trim();\n const match = trimmed.match(/^(\"([^\"]+)\"|`([^`]+)`|(\\w+))\\s+(.+)$/s);\n if (!match) throw new Error(`Unparseable fixture column: '${trimmed}'`);\n columns.push({\n name: match[2] ?? match[3] ?? match[4] ?? '',\n type: (match[5] ?? '').trim(),\n });\n }\n if (columns.length === 0) throw new Error('Fixture schema declared no columns');\n return columns;\n}\n\n/** Resolve fixture rows (inline tuples/objects or NDJSON file) into tuples in `columns` order. */\nexport async function resolveFixtureRows(\n fixture: TableFixture,\n columns: readonly FixtureColumn[],\n): Promise<unknown[][]> {\n const raw: (readonly unknown[] | Record<string, unknown>)[] = [...(fixture.rows ?? [])];\n if (fixture.ndjsonFile) {\n const fs = await import('node:fs/promises');\n const text = await fs.readFile(fixture.ndjsonFile, 'utf8');\n for (const line of text.split('\\n')) {\n if (line.trim().length === 0) continue;\n raw.push(JSON.parse(line) as Record<string, unknown>);\n }\n }\n return raw.map((row) => {\n if (Array.isArray(row)) {\n if (row.length !== columns.length) {\n throw new Error(\n `Fixture row for ${fixture.table} has ${row.length} values, expected ${columns.length}`,\n );\n }\n return [...row];\n }\n const record = row as Record<string, unknown>;\n return columns.map((c) => (c.name in record ? record[c.name] : null));\n });\n}\n"]}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { parseFixtureColumns, resolveFixtureRows } from './chunk-F3KU6VZS.js';
|
|
2
|
+
import { DatabricksRestClient, waitForDatabricksStatement } from '@fabric-harness/databricks';
|
|
3
|
+
|
|
4
|
+
async function createWorkspaceContext(options = {}) {
|
|
5
|
+
const env = options.env ?? process.env;
|
|
6
|
+
const warehouseId = resolveWarehouseId(env);
|
|
7
|
+
const catalog = env.DBX_TEST_CATALOG;
|
|
8
|
+
const schema = options.schema ?? env.DBX_TEST_SCHEMA;
|
|
9
|
+
const client = options.client ?? createClientFromEnv(env, options.fetchImpl);
|
|
10
|
+
const fixtureTables = /* @__PURE__ */ new Set();
|
|
11
|
+
const qual = (table) => [catalog, schema, table].filter((part) => Boolean(part)).join(".");
|
|
12
|
+
async function run(sql, params) {
|
|
13
|
+
const { statement, parameters } = toNamedParameters(sql, params ?? []);
|
|
14
|
+
const submitted = await client.post("/api/2.0/sql/statements", {
|
|
15
|
+
statement,
|
|
16
|
+
warehouse_id: warehouseId,
|
|
17
|
+
...catalog ? { catalog } : {},
|
|
18
|
+
...schema ? { schema } : {},
|
|
19
|
+
...parameters.length > 0 ? { parameters } : {},
|
|
20
|
+
wait_timeout: "30s",
|
|
21
|
+
on_wait_timeout: "CONTINUE"
|
|
22
|
+
});
|
|
23
|
+
options.onStatement?.({
|
|
24
|
+
sql,
|
|
25
|
+
...submitted.statement_id ? { statementId: submitted.statement_id } : {}
|
|
26
|
+
});
|
|
27
|
+
const state = submitted.status?.state;
|
|
28
|
+
let terminal = submitted;
|
|
29
|
+
if (state !== "SUCCEEDED" && state !== "FAILED" && state !== "CANCELED" && state !== "CLOSED") {
|
|
30
|
+
terminal = await waitForDatabricksStatement(
|
|
31
|
+
client,
|
|
32
|
+
submitted.statement_id ?? "",
|
|
33
|
+
{ pollIntervalMs: options.pollIntervalMs, timeoutMs: options.timeoutMs }
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
if (terminal.status?.state !== "SUCCEEDED") {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Databricks statement ${terminal.status?.state ?? "UNKNOWN"}: ${terminal.status?.error?.message ?? sql.slice(0, 200)}`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return terminal;
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
profile: "live",
|
|
45
|
+
qual,
|
|
46
|
+
async exec(sql, params) {
|
|
47
|
+
await run(sql, params);
|
|
48
|
+
},
|
|
49
|
+
async query(sql, params) {
|
|
50
|
+
const response = await run(sql, params);
|
|
51
|
+
return parseStatementRows(response);
|
|
52
|
+
},
|
|
53
|
+
async loadFixture(fixture) {
|
|
54
|
+
if (!schema) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
"Live fixtures require DBX_TEST_SCHEMA (or a schema option) so tests cannot replace unscoped tables"
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
if (!fixture.schema) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Live fixtures require explicit column DDL for ${fixture.table} (schema inference is local-only)`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
const columns = parseFixtureColumns(fixture.schema);
|
|
65
|
+
const ddl = columns.map((c) => `\`${c.name}\` ${c.type}`).join(", ");
|
|
66
|
+
const target = qual(fixture.table);
|
|
67
|
+
await run(`CREATE OR REPLACE TABLE ${target} (${ddl}) USING DELTA`);
|
|
68
|
+
fixtureTables.add(target);
|
|
69
|
+
const rows = await resolveFixtureRows(fixture, columns);
|
|
70
|
+
for (const row of rows) {
|
|
71
|
+
const placeholders = columns.map((c) => /TIMESTAMP/i.test(c.type) ? "CAST(? AS TIMESTAMP)" : "?").join(", ");
|
|
72
|
+
await run(`INSERT INTO ${qual(fixture.table)} VALUES (${placeholders})`, row);
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
async close() {
|
|
76
|
+
if (options.cleanupFixtures !== false && env.DBX_TEST_KEEP_FIXTURES !== "1") {
|
|
77
|
+
for (const table of [...fixtureTables].reverse()) {
|
|
78
|
+
await run(`DROP TABLE IF EXISTS ${table}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
fixtureTables.clear();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function toNamedParameters(sql, params) {
|
|
86
|
+
let out = "";
|
|
87
|
+
let index = 0;
|
|
88
|
+
let quote = null;
|
|
89
|
+
for (let i = 0; i < sql.length; i++) {
|
|
90
|
+
const ch = sql[i];
|
|
91
|
+
if (quote) {
|
|
92
|
+
out += ch;
|
|
93
|
+
if (ch === quote) quote = null;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (ch === "'" || ch === '"' || ch === "`") {
|
|
97
|
+
quote = ch;
|
|
98
|
+
out += ch;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (ch === "?") {
|
|
102
|
+
index += 1;
|
|
103
|
+
out += `:p${index}`;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
out += ch;
|
|
107
|
+
}
|
|
108
|
+
if (index !== params.length) {
|
|
109
|
+
throw new Error(`SQL has ${index} placeholders but ${params.length} params were provided`);
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
statement: out,
|
|
113
|
+
parameters: params.map((value, i) => ({
|
|
114
|
+
name: `p${i + 1}`,
|
|
115
|
+
value: value === null || value === void 0 ? null : String(value)
|
|
116
|
+
}))
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function parseStatementRows(response) {
|
|
120
|
+
const columns = [...response.manifest?.schema?.columns ?? []].sort(
|
|
121
|
+
(a, b) => (a.position ?? 0) - (b.position ?? 0)
|
|
122
|
+
);
|
|
123
|
+
const data = response.result?.data_array ?? [];
|
|
124
|
+
return data.map((tuple) => {
|
|
125
|
+
const row = {};
|
|
126
|
+
columns.forEach((column, i) => {
|
|
127
|
+
row[column.name] = convertValue(tuple[i] ?? null, column.type_name ?? "STRING");
|
|
128
|
+
});
|
|
129
|
+
return row;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
var NUMERIC_TYPES = /^(INT|LONG|SHORT|BYTE|FLOAT|DOUBLE|DECIMAL)/i;
|
|
133
|
+
function convertValue(value, typeName) {
|
|
134
|
+
if (value === null) return null;
|
|
135
|
+
if (NUMERIC_TYPES.test(typeName)) return Number(value);
|
|
136
|
+
if (/^BOOLEAN/i.test(typeName)) return value === "true";
|
|
137
|
+
return value;
|
|
138
|
+
}
|
|
139
|
+
function resolveWarehouseId(env) {
|
|
140
|
+
if (env.DATABRICKS_WAREHOUSE_ID) return env.DATABRICKS_WAREHOUSE_ID;
|
|
141
|
+
const httpPath = env.DATABRICKS_HTTP_PATH;
|
|
142
|
+
const match = httpPath?.match(/\/warehouses\/([\w-]+)/);
|
|
143
|
+
if (match?.[1]) return match[1];
|
|
144
|
+
throw new Error(
|
|
145
|
+
"Live profile requires DATABRICKS_WAREHOUSE_ID or DATABRICKS_HTTP_PATH (/sql/1.0/warehouses/<id>)"
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
function createClientFromEnv(env, fetchImpl) {
|
|
149
|
+
const host = env.DATABRICKS_HOST;
|
|
150
|
+
if (!host) throw new Error("Live profile requires DATABRICKS_HOST");
|
|
151
|
+
return new DatabricksRestClient({
|
|
152
|
+
host,
|
|
153
|
+
token: resolveTokenProvider(host, env, fetchImpl),
|
|
154
|
+
...fetchImpl ? { fetchImpl } : {}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function resolveTokenProvider(host, env, fetchImpl = fetch) {
|
|
158
|
+
if (env.DATABRICKS_BEARER) return env.DATABRICKS_BEARER;
|
|
159
|
+
const clientId = env.DATABRICKS_CLIENT_ID;
|
|
160
|
+
const clientSecret = env.DATABRICKS_CLIENT_SECRET;
|
|
161
|
+
if (clientId && clientSecret) {
|
|
162
|
+
let cached = null;
|
|
163
|
+
return async () => {
|
|
164
|
+
if (cached && Date.now() < cached.expiresAt) return cached.token;
|
|
165
|
+
const response = await fetchImpl(`${host.replace(/\/$/, "")}/oidc/v1/token`, {
|
|
166
|
+
method: "POST",
|
|
167
|
+
headers: {
|
|
168
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
169
|
+
authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`
|
|
170
|
+
},
|
|
171
|
+
body: new URLSearchParams({
|
|
172
|
+
grant_type: "client_credentials",
|
|
173
|
+
scope: env.DATABRICKS_SCOPE ?? "all-apis"
|
|
174
|
+
}).toString()
|
|
175
|
+
});
|
|
176
|
+
if (!response.ok) {
|
|
177
|
+
throw new Error(`OAuth M2M token exchange failed: ${response.status}`);
|
|
178
|
+
}
|
|
179
|
+
const body = await response.json();
|
|
180
|
+
cached = {
|
|
181
|
+
token: body.access_token,
|
|
182
|
+
expiresAt: Date.now() + ((body.expires_in ?? 3600) - 300) * 1e3
|
|
183
|
+
};
|
|
184
|
+
return body.access_token;
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
if (env.DATABRICKS_TOKEN) return env.DATABRICKS_TOKEN;
|
|
188
|
+
throw new Error(
|
|
189
|
+
"Live profile requires DATABRICKS_BEARER, DATABRICKS_CLIENT_ID/SECRET, or DATABRICKS_TOKEN"
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export { createClientFromEnv, createWorkspaceContext, parseStatementRows, resolveTokenProvider, toNamedParameters };
|
|
194
|
+
//# sourceMappingURL=chunk-FBADA7LQ.js.map
|
|
195
|
+
//# sourceMappingURL=chunk-FBADA7LQ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/workspace-context.ts"],"names":[],"mappings":"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","file":"chunk-FBADA7LQ.js","sourcesContent":["import { DatabricksRestClient, waitForDatabricksStatement } from '@fabric-harness/databricks';\nimport type { ExecutionContext } from './context.js';\nimport { type TableFixture, parseFixtureColumns, resolveFixtureRows } from './fixtures.js';\n\n/** Structural slice of DatabricksRestClient the workspace context uses (injectable in tests). */\nexport interface WorkspaceClientLike {\n get<T>(path: string, query?: Record<string, string | number | boolean | undefined>): Promise<T>;\n post<T>(\n path: string,\n body?: unknown,\n query?: Record<string, string | number | boolean | undefined>,\n options?: unknown,\n ): Promise<T>;\n}\n\nexport interface WorkspaceContextOptions {\n env?: Record<string, string | undefined>;\n /** Schema within the catalog used by qual(); defaults to DBX_TEST_SCHEMA. */\n schema?: string;\n /** Injectable client for hermetic tests; defaults to a DatabricksRestClient from env auth. */\n client?: WorkspaceClientLike;\n /** Injectable fetch for the OAuth M2M exchange. */\n fetchImpl?: typeof fetch;\n pollIntervalMs?: number;\n timeoutMs?: number;\n /** Drop tables seeded by this context on close. Defaults to true. */\n cleanupFixtures?: boolean;\n /** Observe submitted statements for redacted failure evidence. */\n onStatement?: (statement: { sql: string; statementId?: string }) => void;\n}\n\ninterface StatementResponse {\n statement_id?: string;\n status?: { state?: string; error?: { message?: string } };\n manifest?: { schema?: { columns?: { name: string; type_name?: string; position?: number }[] } };\n result?: { data_array?: (string | null)[][] };\n}\n\n/**\n * Live execution context on a Databricks SQL warehouse via the SQL Statement\n * Execution API (/api/2.0/sql/statements). The Statement Execution API takes\n * NAMED parameter markers, so positional `?` params are rewritten to\n * `:p1..:pn` — the ExecutionContext surface stays identical to the local\n * profile.\n */\nexport async function createWorkspaceContext(\n options: WorkspaceContextOptions = {},\n): Promise<ExecutionContext> {\n const env = options.env ?? process.env;\n const warehouseId = resolveWarehouseId(env);\n const catalog = env.DBX_TEST_CATALOG;\n const schema = options.schema ?? env.DBX_TEST_SCHEMA;\n const client = options.client ?? createClientFromEnv(env, options.fetchImpl);\n const fixtureTables = new Set<string>();\n\n const qual = (table: string) =>\n [catalog, schema, table].filter((part): part is string => Boolean(part)).join('.');\n\n async function run(sql: string, params?: readonly unknown[]): Promise<StatementResponse> {\n const { statement, parameters } = toNamedParameters(sql, params ?? []);\n const submitted = await client.post<StatementResponse>('/api/2.0/sql/statements', {\n statement,\n warehouse_id: warehouseId,\n ...(catalog ? { catalog } : {}),\n ...(schema ? { schema } : {}),\n ...(parameters.length > 0 ? { parameters } : {}),\n wait_timeout: '30s',\n on_wait_timeout: 'CONTINUE',\n });\n options.onStatement?.({\n sql,\n ...(submitted.statement_id ? { statementId: submitted.statement_id } : {}),\n });\n const state = submitted.status?.state;\n let terminal = submitted;\n if (state !== 'SUCCEEDED' && state !== 'FAILED' && state !== 'CANCELED' && state !== 'CLOSED') {\n terminal = (await waitForDatabricksStatement(\n client as DatabricksRestClient,\n submitted.statement_id ?? '',\n { pollIntervalMs: options.pollIntervalMs, timeoutMs: options.timeoutMs },\n )) as StatementResponse;\n }\n if (terminal.status?.state !== 'SUCCEEDED') {\n throw new Error(\n `Databricks statement ${terminal.status?.state ?? 'UNKNOWN'}: ${\n terminal.status?.error?.message ?? sql.slice(0, 200)\n }`,\n );\n }\n return terminal;\n }\n\n return {\n profile: 'live',\n qual,\n async exec(sql, params) {\n await run(sql, params);\n },\n async query(sql, params) {\n const response = await run(sql, params);\n return parseStatementRows(response);\n },\n async loadFixture(fixture: TableFixture) {\n if (!schema) {\n throw new Error(\n 'Live fixtures require DBX_TEST_SCHEMA (or a schema option) so tests cannot replace unscoped tables',\n );\n }\n if (!fixture.schema) {\n throw new Error(\n `Live fixtures require explicit column DDL for ${fixture.table} (schema inference is local-only)`,\n );\n }\n const columns = parseFixtureColumns(fixture.schema);\n const ddl = columns.map((c) => `\\`${c.name}\\` ${c.type}`).join(', ');\n const target = qual(fixture.table);\n await run(`CREATE OR REPLACE TABLE ${target} (${ddl}) USING DELTA`);\n fixtureTables.add(target);\n const rows = await resolveFixtureRows(fixture, columns);\n for (const row of rows) {\n const placeholders = columns\n .map((c) => (/TIMESTAMP/i.test(c.type) ? 'CAST(? AS TIMESTAMP)' : '?'))\n .join(', ');\n await run(`INSERT INTO ${qual(fixture.table)} VALUES (${placeholders})`, row);\n }\n },\n async close() {\n if (options.cleanupFixtures !== false && env.DBX_TEST_KEEP_FIXTURES !== '1') {\n for (const table of [...fixtureTables].reverse()) {\n await run(`DROP TABLE IF EXISTS ${table}`);\n }\n }\n fixtureTables.clear();\n },\n };\n}\n\n/** Rewrite positional `?` markers to the named `:pN` markers the Statement Execution API requires. */\nexport function toNamedParameters(\n sql: string,\n params: readonly unknown[],\n): { statement: string; parameters: { name: string; value: string | null }[] } {\n let out = '';\n let index = 0;\n let quote: string | null = null;\n for (let i = 0; i < sql.length; i++) {\n const ch = sql[i] as string;\n if (quote) {\n out += ch;\n if (ch === quote) quote = null;\n continue;\n }\n if (ch === \"'\" || ch === '\"' || ch === '`') {\n quote = ch;\n out += ch;\n continue;\n }\n if (ch === '?') {\n index += 1;\n out += `:p${index}`;\n continue;\n }\n out += ch;\n }\n if (index !== params.length) {\n throw new Error(`SQL has ${index} placeholders but ${params.length} params were provided`);\n }\n return {\n statement: out,\n parameters: params.map((value, i) => ({\n name: `p${i + 1}`,\n value: value === null || value === undefined ? null : String(value),\n })),\n };\n}\n\n/** Map a terminal statement response to normalized row objects. */\nexport function parseStatementRows(response: StatementResponse): Record<string, unknown>[] {\n const columns = [...(response.manifest?.schema?.columns ?? [])].sort(\n (a, b) => (a.position ?? 0) - (b.position ?? 0),\n );\n const data = response.result?.data_array ?? [];\n return data.map((tuple) => {\n const row: Record<string, unknown> = {};\n columns.forEach((column, i) => {\n row[column.name] = convertValue(tuple[i] ?? null, column.type_name ?? 'STRING');\n });\n return row;\n });\n}\n\nconst NUMERIC_TYPES = /^(INT|LONG|SHORT|BYTE|FLOAT|DOUBLE|DECIMAL)/i;\n\nfunction convertValue(value: string | null, typeName: string): unknown {\n if (value === null) return null;\n if (NUMERIC_TYPES.test(typeName)) return Number(value);\n if (/^BOOLEAN/i.test(typeName)) return value === 'true';\n return value;\n}\n\nfunction resolveWarehouseId(env: Record<string, string | undefined>): string {\n if (env.DATABRICKS_WAREHOUSE_ID) return env.DATABRICKS_WAREHOUSE_ID;\n const httpPath = env.DATABRICKS_HTTP_PATH;\n const match = httpPath?.match(/\\/warehouses\\/([\\w-]+)/);\n if (match?.[1]) return match[1];\n throw new Error(\n 'Live profile requires DATABRICKS_WAREHOUSE_ID or DATABRICKS_HTTP_PATH (/sql/1.0/warehouses/<id>)',\n );\n}\n\n/**\n * Auth precedence (ADR-0006): DATABRICKS_BEARER (pre-minted, CI) →\n * DATABRICKS_CLIENT_ID/SECRET (OAuth M2M) → DATABRICKS_TOKEN (PAT).\n */\nexport function createClientFromEnv(\n env: Record<string, string | undefined>,\n fetchImpl?: typeof fetch,\n): DatabricksRestClient {\n const host = env.DATABRICKS_HOST;\n if (!host) throw new Error('Live profile requires DATABRICKS_HOST');\n return new DatabricksRestClient({\n host,\n token: resolveTokenProvider(host, env, fetchImpl),\n ...(fetchImpl ? { fetchImpl } : {}),\n });\n}\n\nexport function resolveTokenProvider(\n host: string,\n env: Record<string, string | undefined>,\n fetchImpl: typeof fetch = fetch,\n): string | (() => Promise<string>) {\n if (env.DATABRICKS_BEARER) return env.DATABRICKS_BEARER;\n const clientId = env.DATABRICKS_CLIENT_ID;\n const clientSecret = env.DATABRICKS_CLIENT_SECRET;\n if (clientId && clientSecret) {\n let cached: { token: string; expiresAt: number } | null = null;\n return async () => {\n if (cached && Date.now() < cached.expiresAt) return cached.token;\n const response = await fetchImpl(`${host.replace(/\\/$/, '')}/oidc/v1/token`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/x-www-form-urlencoded',\n authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`,\n },\n body: new URLSearchParams({\n grant_type: 'client_credentials',\n scope: env.DATABRICKS_SCOPE ?? 'all-apis',\n }).toString(),\n });\n if (!response.ok) {\n throw new Error(`OAuth M2M token exchange failed: ${response.status}`);\n }\n const body = (await response.json()) as { access_token: string; expires_in?: number };\n cached = {\n token: body.access_token,\n expiresAt: Date.now() + ((body.expires_in ?? 3600) - 300) * 1000,\n };\n return body.access_token;\n };\n }\n if (env.DATABRICKS_TOKEN) return env.DATABRICKS_TOKEN;\n throw new Error(\n 'Live profile requires DATABRICKS_BEARER, DATABRICKS_CLIENT_ID/SECRET, or DATABRICKS_TOKEN',\n );\n}\n"]}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// src/golden.ts
|
|
2
|
+
async function compareToGolden(goldenPath, actual, label, env = process.env) {
|
|
3
|
+
const fs = await import('fs/promises');
|
|
4
|
+
const serialized = `${JSON.stringify(actual, null, 2)}
|
|
5
|
+
`;
|
|
6
|
+
if (env.DBX_TEST_UPDATE_GOLDEN === "1") {
|
|
7
|
+
const { dirname } = await import('path');
|
|
8
|
+
await fs.mkdir(dirname(goldenPath), { recursive: true });
|
|
9
|
+
await fs.writeFile(goldenPath, serialized, "utf8");
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
let expected;
|
|
13
|
+
try {
|
|
14
|
+
expected = await fs.readFile(goldenPath, "utf8");
|
|
15
|
+
} catch {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`Golden file ${goldenPath} does not exist for ${label}. Run once with DBX_TEST_UPDATE_GOLDEN=1 to create it, then commit it.`
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
if (normalize(expected) !== normalize(serialized)) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`${label} diverges from golden ${goldenPath}.
|
|
23
|
+
Expected:
|
|
24
|
+
${expected}
|
|
25
|
+
Actual:
|
|
26
|
+
${serialized}
|
|
27
|
+
If the change is intentional, re-run with DBX_TEST_UPDATE_GOLDEN=1 and commit the diff.`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function normalize(json) {
|
|
32
|
+
return JSON.stringify(JSON.parse(json));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export { compareToGolden };
|
|
36
|
+
//# sourceMappingURL=chunk-MLBZCHRM.js.map
|
|
37
|
+
//# sourceMappingURL=chunk-MLBZCHRM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/golden.ts"],"names":[],"mappings":";AAKA,eAAsB,gBACpB,UAAA,EACA,MAAA,EACA,KAAA,EACA,GAAA,GAA0C,QAAQ,GAAA,EACnC;AACf,EAAA,MAAM,EAAA,GAAK,MAAM,OAAO,aAAkB,CAAA;AAC1C,EAAA,MAAM,aAAa,CAAA,EAAG,IAAA,CAAK,UAAU,MAAA,EAAQ,IAAA,EAAM,CAAC,CAAC;AAAA,CAAA;AACrD,EAAA,IAAI,GAAA,CAAI,2BAA2B,GAAA,EAAK;AACtC,IAAA,MAAM,EAAE,OAAA,EAAQ,GAAI,MAAM,OAAO,MAAW,CAAA;AAC5C,IAAA,MAAM,EAAA,CAAG,MAAM,OAAA,CAAQ,UAAU,GAAG,EAAE,SAAA,EAAW,MAAM,CAAA;AACvD,IAAA,MAAM,EAAA,CAAG,SAAA,CAAU,UAAA,EAAY,UAAA,EAAY,MAAM,CAAA;AACjD,IAAA;AAAA,EACF;AACA,EAAA,IAAI,QAAA;AACJ,EAAA,IAAI;AACF,IAAA,QAAA,GAAW,MAAM,EAAA,CAAG,QAAA,CAAS,UAAA,EAAY,MAAM,CAAA;AAAA,EACjD,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,YAAA,EAAe,UAAU,CAAA,oBAAA,EAAuB,KAAK,CAAA,sEAAA;AAAA,KACvD;AAAA,EACF;AACA,EAAA,IAAI,SAAA,CAAU,QAAQ,CAAA,KAAM,SAAA,CAAU,UAAU,CAAA,EAAG;AACjD,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,EAAG,KAAK,CAAA,sBAAA,EAAyB,UAAU,CAAA;AAAA;AAAA,EAAiB,QAAQ;AAAA;AAAA,EAAc,UAAU;AAAA,uFAAA;AAAA,KAC9F;AAAA,EACF;AACF;AAEA,SAAS,UAAU,IAAA,EAAsB;AACvC,EAAA,OAAO,IAAA,CAAK,SAAA,CAAU,IAAA,CAAK,KAAA,CAAM,IAAI,CAAC,CAAA;AACxC","file":"chunk-MLBZCHRM.js","sourcesContent":["/**\n * Golden-result files: committed JSON snapshots of query results that every\n * engine and every refactor must reproduce exactly — the frozen-vector\n * pattern from @fabricorg/experiments-testkit generalized to warehouse rows.\n */\nexport async function compareToGolden(\n goldenPath: string,\n actual: readonly Record<string, unknown>[],\n label: string,\n env: Record<string, string | undefined> = process.env,\n): Promise<void> {\n const fs = await import('node:fs/promises');\n const serialized = `${JSON.stringify(actual, null, 2)}\\n`;\n if (env.DBX_TEST_UPDATE_GOLDEN === '1') {\n const { dirname } = await import('node:path');\n await fs.mkdir(dirname(goldenPath), { recursive: true });\n await fs.writeFile(goldenPath, serialized, 'utf8');\n return;\n }\n let expected: string;\n try {\n expected = await fs.readFile(goldenPath, 'utf8');\n } catch {\n throw new Error(\n `Golden file ${goldenPath} does not exist for ${label}. Run once with DBX_TEST_UPDATE_GOLDEN=1 to create it, then commit it.`,\n );\n }\n if (normalize(expected) !== normalize(serialized)) {\n throw new Error(\n `${label} diverges from golden ${goldenPath}.\\nExpected:\\n${expected}\\nActual:\\n${serialized}\\nIf the change is intentional, re-run with DBX_TEST_UPDATE_GOLDEN=1 and commit the diff.`,\n );\n }\n}\n\nfunction normalize(json: string): string {\n return JSON.stringify(JSON.parse(json));\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"duckdb-context-FX23RUBV.js"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative, engine-agnostic table seeds. The same fixture applies to a
|
|
3
|
+
* local DuckDB table or a scratch Unity Catalog schema — column DDL uses the
|
|
4
|
+
* portable subset (VARCHAR/STRING, BIGINT, DOUBLE, TIMESTAMP, BOOLEAN).
|
|
5
|
+
*/
|
|
6
|
+
interface TableFixture {
|
|
7
|
+
table: string;
|
|
8
|
+
/**
|
|
9
|
+
* Column DDL, e.g. `subject_id STRING, value DOUBLE, "at" TIMESTAMP`.
|
|
10
|
+
* Optional when the table already exists in the target context.
|
|
11
|
+
*/
|
|
12
|
+
schema?: string;
|
|
13
|
+
/** Row tuples in column order, or row objects keyed by column name. */
|
|
14
|
+
rows?: readonly (readonly unknown[] | Record<string, unknown>)[];
|
|
15
|
+
/** Path to an NDJSON file of row objects (one JSON object per line). */
|
|
16
|
+
ndjsonFile?: string;
|
|
17
|
+
}
|
|
18
|
+
interface FixtureColumn {
|
|
19
|
+
name: string;
|
|
20
|
+
type: string;
|
|
21
|
+
}
|
|
22
|
+
/** Parse `name TYPE, name TYPE(args), "quoted" TYPE` column DDL into columns. */
|
|
23
|
+
declare function parseFixtureColumns(schema: string): FixtureColumn[];
|
|
24
|
+
/** Resolve fixture rows (inline tuples/objects or NDJSON file) into tuples in `columns` order. */
|
|
25
|
+
declare function resolveFixtureRows(fixture: TableFixture, columns: readonly FixtureColumn[]): Promise<unknown[][]>;
|
|
26
|
+
|
|
27
|
+
export { type FixtureColumn as F, type TableFixture as T, parseFixtureColumns as p, resolveFixtureRows as r };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative, engine-agnostic table seeds. The same fixture applies to a
|
|
3
|
+
* local DuckDB table or a scratch Unity Catalog schema — column DDL uses the
|
|
4
|
+
* portable subset (VARCHAR/STRING, BIGINT, DOUBLE, TIMESTAMP, BOOLEAN).
|
|
5
|
+
*/
|
|
6
|
+
interface TableFixture {
|
|
7
|
+
table: string;
|
|
8
|
+
/**
|
|
9
|
+
* Column DDL, e.g. `subject_id STRING, value DOUBLE, "at" TIMESTAMP`.
|
|
10
|
+
* Optional when the table already exists in the target context.
|
|
11
|
+
*/
|
|
12
|
+
schema?: string;
|
|
13
|
+
/** Row tuples in column order, or row objects keyed by column name. */
|
|
14
|
+
rows?: readonly (readonly unknown[] | Record<string, unknown>)[];
|
|
15
|
+
/** Path to an NDJSON file of row objects (one JSON object per line). */
|
|
16
|
+
ndjsonFile?: string;
|
|
17
|
+
}
|
|
18
|
+
interface FixtureColumn {
|
|
19
|
+
name: string;
|
|
20
|
+
type: string;
|
|
21
|
+
}
|
|
22
|
+
/** Parse `name TYPE, name TYPE(args), "quoted" TYPE` column DDL into columns. */
|
|
23
|
+
declare function parseFixtureColumns(schema: string): FixtureColumn[];
|
|
24
|
+
/** Resolve fixture rows (inline tuples/objects or NDJSON file) into tuples in `columns` order. */
|
|
25
|
+
declare function resolveFixtureRows(fixture: TableFixture, columns: readonly FixtureColumn[]): Promise<unknown[][]>;
|
|
26
|
+
|
|
27
|
+
export { type FixtureColumn as F, type TableFixture as T, parseFixtureColumns as p, resolveFixtureRows as r };
|