@smithers-orchestrator/db 0.22.0 → 0.24.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/package.json +12 -5
- package/src/adapter.js +341 -30
- package/src/dialect.js +229 -0
- package/src/ensure.js +7 -0
- package/src/index.d.ts +20 -0
- package/src/internal-schema/index.js +2 -0
- package/src/internal-schema/smithersWorkspaceCheckpoints.js +17 -0
- package/src/internal-schema/smithersWorkspaceStates.js +11 -0
- package/src/internal-schema.js +2 -0
- package/src/output.js +20 -3
- package/src/schema-migrations.js +48 -0
- package/src/snapshot.js +121 -1
- package/src/sql-message-storage.js +188 -10
- package/src/zodToCreateTableSQL.js +36 -4
- /package/src/{runState-types.ts → runState.d.ts} +0 -0
|
@@ -4,7 +4,16 @@ import { SqlError } from "@effect/sql/SqlError";
|
|
|
4
4
|
import * as Statement from "@effect/sql/Statement";
|
|
5
5
|
import { Database } from "bun:sqlite";
|
|
6
6
|
import { Context, Effect, Layer, ManagedRuntime, Scope, Stream } from "effect";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
POSTGRES,
|
|
9
|
+
SQLITE,
|
|
10
|
+
jsonExtractText,
|
|
11
|
+
translatePlaceholders,
|
|
12
|
+
} from "./dialect.js";
|
|
13
|
+
import {
|
|
14
|
+
runSmithersSchemaMigrations,
|
|
15
|
+
runSmithersSchemaInitPostgres,
|
|
16
|
+
} from "./schema-migrations.js";
|
|
8
17
|
import { camelToSnake } from "./utils/camelToSnake.js";
|
|
9
18
|
/** @typedef {import("drizzle-orm/bun-sqlite").BunSQLiteDatabase} BunSQLiteDatabase */
|
|
10
19
|
/** @typedef {import("./SqlMessageStorageEventHistoryQuery.ts").SqlMessageStorageEventHistoryQuery} SqlMessageStorageEventHistoryQuery */
|
|
@@ -205,6 +214,30 @@ const CREATE_TABLE_STATEMENTS = [
|
|
|
205
214
|
status TEXT NOT NULL,
|
|
206
215
|
error_json TEXT,
|
|
207
216
|
PRIMARY KEY (run_id, node_id, iteration, attempt, seq)
|
|
217
|
+
)`,
|
|
218
|
+
`CREATE TABLE IF NOT EXISTS _smithers_workspace_states (
|
|
219
|
+
run_id TEXT NOT NULL,
|
|
220
|
+
jj_cwd TEXT NOT NULL,
|
|
221
|
+
jj_commit_id TEXT NOT NULL,
|
|
222
|
+
jj_operation_id TEXT NOT NULL,
|
|
223
|
+
jj_change_id TEXT,
|
|
224
|
+
created_at_ms INTEGER NOT NULL,
|
|
225
|
+
PRIMARY KEY (run_id, jj_cwd, jj_commit_id)
|
|
226
|
+
)`,
|
|
227
|
+
`CREATE TABLE IF NOT EXISTS _smithers_workspace_checkpoints (
|
|
228
|
+
run_id TEXT NOT NULL,
|
|
229
|
+
node_id TEXT NOT NULL,
|
|
230
|
+
iteration INTEGER NOT NULL DEFAULT 0,
|
|
231
|
+
attempt INTEGER NOT NULL,
|
|
232
|
+
seq INTEGER NOT NULL,
|
|
233
|
+
jj_cwd TEXT NOT NULL,
|
|
234
|
+
jj_commit_id TEXT NOT NULL,
|
|
235
|
+
source TEXT NOT NULL,
|
|
236
|
+
tier INTEGER NOT NULL,
|
|
237
|
+
label TEXT,
|
|
238
|
+
tool_use_id TEXT,
|
|
239
|
+
created_at_ms INTEGER NOT NULL,
|
|
240
|
+
PRIMARY KEY (run_id, node_id, iteration, attempt, seq)
|
|
208
241
|
)`,
|
|
209
242
|
`CREATE TABLE IF NOT EXISTS _smithers_events (
|
|
210
243
|
run_id TEXT NOT NULL,
|
|
@@ -393,14 +426,17 @@ function applyBooleanColumns(row, booleanColumns) {
|
|
|
393
426
|
* @param {Record<string, unknown>} row
|
|
394
427
|
* @param {{ orIgnore?: boolean; conflictColumns?: readonly string[]; updateColumns?: readonly string[]; }} [options]
|
|
395
428
|
*/
|
|
396
|
-
function buildInsertSql(table, row, options) {
|
|
429
|
+
function buildInsertSql(table, row, options, dialect = SQLITE) {
|
|
397
430
|
const entries = Object.entries(row).filter(([, value]) => value !== undefined);
|
|
398
431
|
const columns = entries.map(([key]) => camelToSnake(key));
|
|
399
432
|
const params = entries.map(([, value]) => encodeParam(value));
|
|
400
433
|
const tableSql = quoteIdentifier(table);
|
|
401
434
|
const columnSql = columns.map(quoteIdentifier).join(", ");
|
|
402
435
|
const placeholderSql = columns.map(() => "?").join(", ");
|
|
403
|
-
|
|
436
|
+
// SQLite spells the ignore-on-conflict shorthand `INSERT OR IGNORE`;
|
|
437
|
+
// PostgreSQL has no such prefix and instead appends `ON CONFLICT DO NOTHING`.
|
|
438
|
+
const orIgnorePrefix = options?.orIgnore && dialect !== POSTGRES ? " OR IGNORE" : "";
|
|
439
|
+
let statement = `INSERT${orIgnorePrefix} INTO ${tableSql} (${columnSql}) ` +
|
|
404
440
|
`VALUES (${placeholderSql})`;
|
|
405
441
|
if (options?.conflictColumns && options.conflictColumns.length > 0) {
|
|
406
442
|
const conflictSql = options.conflictColumns.map(camelToSnake).map(quoteIdentifier).join(", ");
|
|
@@ -417,6 +453,9 @@ function buildInsertSql(table, row, options) {
|
|
|
417
453
|
statement += ` ON CONFLICT (${conflictSql}) DO UPDATE SET ${updateSql}`;
|
|
418
454
|
}
|
|
419
455
|
}
|
|
456
|
+
else if (options?.orIgnore && dialect === POSTGRES) {
|
|
457
|
+
statement += ` ON CONFLICT DO NOTHING`;
|
|
458
|
+
}
|
|
420
459
|
return { statement, params };
|
|
421
460
|
}
|
|
422
461
|
/**
|
|
@@ -524,18 +563,118 @@ function makeSqlClientEffect(sqlite) {
|
|
|
524
563
|
function makeSqlClientLayer(sqlite) {
|
|
525
564
|
return Layer.scoped(SqlClient.SqlClient, makeSqlClientEffect(sqlite));
|
|
526
565
|
}
|
|
566
|
+
/**
|
|
567
|
+
* @param {SqliteParam} value
|
|
568
|
+
* @returns {unknown}
|
|
569
|
+
*/
|
|
570
|
+
function toPostgresParam(value) {
|
|
571
|
+
// node-postgres maps Buffer → bytea; a bare Uint8Array does not round-trip.
|
|
572
|
+
if (value instanceof Uint8Array || Buffer.isBuffer(value)) {
|
|
573
|
+
return Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
574
|
+
}
|
|
575
|
+
// Smithers mirrors SQLite's storage model on Postgres: JSON lives in TEXT
|
|
576
|
+
// columns. SQLite/Drizzle stringify objects automatically; node-postgres does
|
|
577
|
+
// not, so do it here to keep a single encoding contract across dialects.
|
|
578
|
+
if (value !== null && typeof value === "object" && !(value instanceof Date)) {
|
|
579
|
+
return JSON.stringify(value);
|
|
580
|
+
}
|
|
581
|
+
return value;
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* A `@effect/sql` connection backed by a single node-postgres connection (any
|
|
585
|
+
* object exposing `query({ text, values, rowMode })` — a `pg.Client`, or a
|
|
586
|
+
* PGlite socket connection). Smithers writes SQL with `?` placeholders; this
|
|
587
|
+
* connection rewrites them to PostgreSQL's `$n` on the way out, mirroring the
|
|
588
|
+
* SQLite connection so the rest of the adapter is dialect-agnostic.
|
|
589
|
+
* @param {{ query: (config: { text: string; values?: ReadonlyArray<unknown>; rowMode?: "array" }) => Promise<{ rows?: ReadonlyArray<any> }> }} pgConn
|
|
590
|
+
* @returns {Connection}
|
|
591
|
+
*/
|
|
592
|
+
function createPostgresConnection(pgConn) {
|
|
593
|
+
const run = (statement, params, transformRows) => Effect.tryPromise({
|
|
594
|
+
try: async () => {
|
|
595
|
+
const text = translatePlaceholders(POSTGRES, statement);
|
|
596
|
+
const result = await pgConn.query({ text, values: params.map(toPostgresParam) });
|
|
597
|
+
const rows = result.rows ?? [];
|
|
598
|
+
return transformRows ? transformRows(rows) : rows;
|
|
599
|
+
},
|
|
600
|
+
catch: (cause) => new SqlError({ cause, message: "Failed to execute Postgres statement" }),
|
|
601
|
+
});
|
|
602
|
+
return {
|
|
603
|
+
execute: (statement, params, transformRows) => run(statement, params, transformRows),
|
|
604
|
+
executeRaw: (statement, params) => run(statement, params, undefined),
|
|
605
|
+
executeValues: (statement, params) => Effect.tryPromise({
|
|
606
|
+
try: async () => {
|
|
607
|
+
const text = translatePlaceholders(POSTGRES, statement);
|
|
608
|
+
const result = await pgConn.query({
|
|
609
|
+
text,
|
|
610
|
+
values: params.map(toPostgresParam),
|
|
611
|
+
rowMode: "array",
|
|
612
|
+
});
|
|
613
|
+
return result.rows ?? [];
|
|
614
|
+
},
|
|
615
|
+
catch: (cause) => new SqlError({ cause, message: "Failed to execute Postgres values statement" }),
|
|
616
|
+
}),
|
|
617
|
+
executeUnprepared: (statement, params, transformRows) => run(statement, params, transformRows),
|
|
618
|
+
executeStream: (statement, params, transformRows) => Stream.fromIterableEffect(run(statement, params, transformRows)),
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* @param {object} pgConn
|
|
623
|
+
* @returns {Effect.Effect<SqlClient.SqlClient, never>}
|
|
624
|
+
*/
|
|
625
|
+
function makePostgresSqlClientEffect(pgConn) {
|
|
626
|
+
// The compiler is only exercised by the `sql``` tagged template, which this
|
|
627
|
+
// storage never uses — every query is a pre-built string run through the raw
|
|
628
|
+
// connection, where the `?`→`$n` rewrite happens. So the SQLite compiler is
|
|
629
|
+
// an inert placeholder here.
|
|
630
|
+
const compiler = Statement.makeCompilerSqlite(camelToSnake);
|
|
631
|
+
const connection = createPostgresConnection(pgConn);
|
|
632
|
+
return Effect.gen(function* () {
|
|
633
|
+
const semaphore = yield* Effect.makeSemaphore(1);
|
|
634
|
+
const acquirer = semaphore.withPermits(1)(Effect.succeed(connection));
|
|
635
|
+
const transactionAcquirer = Effect.uninterruptibleMask((restore) => Effect.as(Effect.zipRight(restore(semaphore.take(1)), Effect.tap(Effect.scope, (scope) => Scope.addFinalizer(scope, semaphore.release(1)))), connection));
|
|
636
|
+
const reactivity = yield* Reactivity.make;
|
|
637
|
+
return yield* SqlClient.make({
|
|
638
|
+
acquirer,
|
|
639
|
+
compiler,
|
|
640
|
+
transactionAcquirer,
|
|
641
|
+
spanAttributes: [[ATTR_DB_SYSTEM_NAME, "postgresql"]],
|
|
642
|
+
transformRows: transformRowKeys,
|
|
643
|
+
}).pipe(Effect.provideService(Reactivity.Reactivity, reactivity));
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* @param {object} pgConn
|
|
648
|
+
*/
|
|
649
|
+
function makePostgresSqlClientLayer(pgConn) {
|
|
650
|
+
return Layer.scoped(SqlClient.SqlClient, makePostgresSqlClientEffect(pgConn));
|
|
651
|
+
}
|
|
527
652
|
export class SqlMessageStorage {
|
|
528
653
|
sqlite;
|
|
654
|
+
/** @type {import("./dialect.js").Dialect} */
|
|
655
|
+
dialect;
|
|
656
|
+
/** @type {object | null} */
|
|
657
|
+
pgConn;
|
|
529
658
|
// TODO(Phase 8): Keep this per-DB runtime until the unified runtime can
|
|
530
659
|
// inject a scoped SqlClient without rebuilding the per-connection semaphore.
|
|
531
660
|
runtime;
|
|
532
661
|
tableColumnsCache = new Map();
|
|
533
662
|
/**
|
|
534
|
-
* @param {BunSQLiteDatabase<any> | Database} db
|
|
663
|
+
* @param {BunSQLiteDatabase<any> | Database | { dialect: "postgres"; connection: object }} db
|
|
535
664
|
*/
|
|
536
665
|
constructor(db) {
|
|
537
|
-
|
|
538
|
-
|
|
666
|
+
if (db && typeof db === "object" && /** @type {any} */ (db).dialect === POSTGRES) {
|
|
667
|
+
this.dialect = POSTGRES;
|
|
668
|
+
this.pgConn = /** @type {any} */ (db).connection;
|
|
669
|
+
this.sqlite = null;
|
|
670
|
+
this.runtime = ManagedRuntime.make(makePostgresSqlClientLayer(this.pgConn));
|
|
671
|
+
}
|
|
672
|
+
else {
|
|
673
|
+
this.dialect = SQLITE;
|
|
674
|
+
this.sqlite = resolveSqliteDatabase(db);
|
|
675
|
+
this.pgConn = null;
|
|
676
|
+
this.runtime = ManagedRuntime.make(makeSqlClientLayer(this.sqlite));
|
|
677
|
+
}
|
|
539
678
|
}
|
|
540
679
|
/**
|
|
541
680
|
* @param {string} table
|
|
@@ -546,6 +685,13 @@ export class SqlMessageStorage {
|
|
|
546
685
|
if (cached) {
|
|
547
686
|
return cached;
|
|
548
687
|
}
|
|
688
|
+
if (this.dialect === POSTGRES) {
|
|
689
|
+
// A fresh PostgreSQL schema has no historical column drift to defend
|
|
690
|
+
// against, and PRAGMA is unavailable. Returning null tells
|
|
691
|
+
// filterKnownColumns to skip filtering, so a genuinely missing column
|
|
692
|
+
// surfaces as a loud insert error rather than a silently dropped field.
|
|
693
|
+
return null;
|
|
694
|
+
}
|
|
549
695
|
const rows = this.sqlite
|
|
550
696
|
.query(`PRAGMA table_info(${quoteIdentifier(table)})`)
|
|
551
697
|
.all();
|
|
@@ -562,7 +708,7 @@ export class SqlMessageStorage {
|
|
|
562
708
|
*/
|
|
563
709
|
filterKnownColumns(table, row) {
|
|
564
710
|
const knownColumns = this.getTableColumns(table);
|
|
565
|
-
return Object.fromEntries(Object.entries(row).filter(([key, value]) => value !== undefined && knownColumns.has(key)));
|
|
711
|
+
return Object.fromEntries(Object.entries(row).filter(([key, value]) => value !== undefined && (knownColumns === null || knownColumns.has(key))));
|
|
566
712
|
}
|
|
567
713
|
/**
|
|
568
714
|
* @template A, E
|
|
@@ -584,6 +730,16 @@ export class SqlMessageStorage {
|
|
|
584
730
|
* @returns {Effect.Effect<void, never>}
|
|
585
731
|
*/
|
|
586
732
|
ensureSchemaEffect() {
|
|
733
|
+
if (this.dialect === POSTGRES) {
|
|
734
|
+
const pgConn = this.pgConn;
|
|
735
|
+
return Effect.tryPromise({
|
|
736
|
+
try: () => runSmithersSchemaInitPostgres(pgConn, {
|
|
737
|
+
createTableStatements: CREATE_TABLE_STATEMENTS,
|
|
738
|
+
createIndexStatements: CREATE_INDEX_STATEMENTS,
|
|
739
|
+
}),
|
|
740
|
+
catch: (cause) => new SqlError({ cause, message: "Failed to initialize Postgres schema" }),
|
|
741
|
+
});
|
|
742
|
+
}
|
|
587
743
|
const sqlite = this.sqlite;
|
|
588
744
|
return Effect.sync(() => {
|
|
589
745
|
runSmithersSchemaMigrations(sqlite, {
|
|
@@ -622,6 +778,28 @@ export class SqlMessageStorage {
|
|
|
622
778
|
return rows[0];
|
|
623
779
|
}
|
|
624
780
|
/**
|
|
781
|
+
* Like {@link queryAll} but returns rows with their on-disk column names (no
|
|
782
|
+
* snake→camel transform). Used for "raw" output-table reads where callers
|
|
783
|
+
* expect the storage column names verbatim.
|
|
784
|
+
* @template T
|
|
785
|
+
* @param {string} statement
|
|
786
|
+
* @param {ReadonlyArray<SqliteParam>} [params]
|
|
787
|
+
* @returns {Promise<Array<T>>}
|
|
788
|
+
*/
|
|
789
|
+
queryAllRaw(statement, params = []) {
|
|
790
|
+
return this.withConnection((connection) => connection.execute(statement, params.map(encodeParam), undefined));
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* @template T
|
|
794
|
+
* @param {string} statement
|
|
795
|
+
* @param {ReadonlyArray<SqliteParam>} [params]
|
|
796
|
+
* @returns {Promise<T | undefined>}
|
|
797
|
+
*/
|
|
798
|
+
async queryOneRaw(statement, params = []) {
|
|
799
|
+
const rows = await this.queryAllRaw(statement, params);
|
|
800
|
+
return rows[0];
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
625
803
|
* @param {string} statement
|
|
626
804
|
* @param {ReadonlyArray<SqliteParam>} [params]
|
|
627
805
|
* @returns {Promise<void>}
|
|
@@ -636,7 +814,7 @@ export class SqlMessageStorage {
|
|
|
636
814
|
*/
|
|
637
815
|
insertIgnore(table, row) {
|
|
638
816
|
const filteredRow = this.filterKnownColumns(table, row);
|
|
639
|
-
const { statement, params } = buildInsertSql(table, filteredRow, { orIgnore: true });
|
|
817
|
+
const { statement, params } = buildInsertSql(table, filteredRow, { orIgnore: true }, this.dialect);
|
|
640
818
|
return this.execute(statement, params);
|
|
641
819
|
}
|
|
642
820
|
/**
|
|
@@ -651,7 +829,7 @@ export class SqlMessageStorage {
|
|
|
651
829
|
const { statement, params } = buildInsertSql(table, filteredRow, {
|
|
652
830
|
conflictColumns,
|
|
653
831
|
updateColumns,
|
|
654
|
-
});
|
|
832
|
+
}, this.dialect);
|
|
655
833
|
return this.execute(statement, params);
|
|
656
834
|
}
|
|
657
835
|
/**
|
|
@@ -694,7 +872,7 @@ export class SqlMessageStorage {
|
|
|
694
872
|
params.push(...query.types);
|
|
695
873
|
}
|
|
696
874
|
if (query.nodeId) {
|
|
697
|
-
clauses.push("
|
|
875
|
+
clauses.push(`${jsonExtractText(this.dialect, "payload_json", "$.nodeId")} = ?`);
|
|
698
876
|
params.push(query.nodeId);
|
|
699
877
|
}
|
|
700
878
|
return {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { unwrapZodType } from "./unwrapZodType.js";
|
|
2
|
+
import { columnType, SQLITE } from "./dialect.js";
|
|
2
3
|
import { camelToSnake } from "./utils/camelToSnake.js";
|
|
3
4
|
/**
|
|
4
5
|
* Determines the Zod base type name from a (possibly unwrapped) Zod type.
|
|
@@ -17,6 +18,21 @@ function sqliteTypeFor(zodFieldSchema) {
|
|
|
17
18
|
}
|
|
18
19
|
return "TEXT";
|
|
19
20
|
}
|
|
21
|
+
function sqliteKindFor(zodFieldSchema) {
|
|
22
|
+
const baseType = unwrapZodType(zodFieldSchema);
|
|
23
|
+
const baseTypeName = getZodBaseTypeName(baseType);
|
|
24
|
+
if (baseTypeName === "boolean")
|
|
25
|
+
return "boolean";
|
|
26
|
+
if (baseTypeName === "number" ||
|
|
27
|
+
baseTypeName === "int" ||
|
|
28
|
+
baseTypeName === "float")
|
|
29
|
+
return "number";
|
|
30
|
+
if (baseTypeName === "string" ||
|
|
31
|
+
baseTypeName === "enum" ||
|
|
32
|
+
baseTypeName === "literal")
|
|
33
|
+
return "string";
|
|
34
|
+
return "json";
|
|
35
|
+
}
|
|
20
36
|
function quoteIdentifier(identifier) {
|
|
21
37
|
return `"${String(identifier).replaceAll(`"`, `""`)}"`;
|
|
22
38
|
}
|
|
@@ -28,24 +44,28 @@ export function zodSchemaColumns(schema) {
|
|
|
28
44
|
const out = [];
|
|
29
45
|
const shape = schema.shape;
|
|
30
46
|
for (const [key] of Object.entries(shape)) {
|
|
31
|
-
out.push({ name: camelToSnake(key), sqliteType: sqliteTypeFor(shape[key]) });
|
|
47
|
+
out.push({ name: camelToSnake(key), sqliteType: sqliteTypeFor(shape[key]), kind: sqliteKindFor(shape[key]) });
|
|
32
48
|
}
|
|
33
49
|
return out;
|
|
34
50
|
}
|
|
35
51
|
/**
|
|
36
52
|
* Generates a CREATE TABLE IF NOT EXISTS SQL statement from a Zod schema.
|
|
37
|
-
* Used for runtime table creation without Drizzle migrations.
|
|
53
|
+
* Used for runtime table creation without Drizzle migrations. Pass
|
|
54
|
+
* `opts.dialect` to emit PostgreSQL-compatible column types; the default
|
|
55
|
+
* (`sqlite`) is byte-identical to the historical output.
|
|
38
56
|
*/
|
|
39
57
|
export function zodToCreateTableSQL(tableName, schema, opts) {
|
|
58
|
+
const dialect = opts?.dialect ?? SQLITE;
|
|
59
|
+
const integer = columnType(dialect, "INTEGER");
|
|
40
60
|
const colDefs = opts?.isInput
|
|
41
61
|
? [`run_id TEXT NOT NULL PRIMARY KEY`]
|
|
42
62
|
: [
|
|
43
63
|
`run_id TEXT NOT NULL`,
|
|
44
64
|
`node_id TEXT NOT NULL`,
|
|
45
|
-
`iteration
|
|
65
|
+
`iteration ${integer} NOT NULL DEFAULT 0`,
|
|
46
66
|
];
|
|
47
67
|
for (const { name, sqliteType } of zodSchemaColumns(schema)) {
|
|
48
|
-
colDefs.push(`"${name}" ${sqliteType}`);
|
|
68
|
+
colDefs.push(`"${name}" ${columnType(dialect, sqliteType)}`);
|
|
49
69
|
}
|
|
50
70
|
if (!opts?.isInput) {
|
|
51
71
|
colDefs.push(`PRIMARY KEY (run_id, node_id, iteration)`);
|
|
@@ -63,6 +83,18 @@ export function zodToCreateTableSQL(tableName, schema, opts) {
|
|
|
63
83
|
*/
|
|
64
84
|
export function syncZodTableSchema(sqlite, tableName, schema, opts) {
|
|
65
85
|
sqlite.run(zodToCreateTableSQL(tableName, schema, opts));
|
|
86
|
+
if (!opts?.isInput) {
|
|
87
|
+
try {
|
|
88
|
+
sqlite.run(`CREATE TABLE IF NOT EXISTS _smithers_output_schema_columns (table_name TEXT NOT NULL, column_name TEXT NOT NULL, kind TEXT NOT NULL, PRIMARY KEY (table_name, column_name))`);
|
|
89
|
+
const stmt = sqlite.query(`INSERT INTO _smithers_output_schema_columns (table_name, column_name, kind) VALUES (?, ?, ?) ON CONFLICT(table_name, column_name) DO UPDATE SET kind = excluded.kind`);
|
|
90
|
+
for (const { name, kind } of zodSchemaColumns(schema)) {
|
|
91
|
+
stmt.run(tableName, name, kind);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// Schema metadata is best-effort; table creation remains the source of truth.
|
|
96
|
+
}
|
|
97
|
+
}
|
|
66
98
|
let existing;
|
|
67
99
|
const quotedTable = quoteIdentifier(tableName);
|
|
68
100
|
try {
|
|
File without changes
|