@smithers-orchestrator/db 0.23.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 +9 -5
- package/src/adapter.js +159 -5
- package/src/index.d.ts +20 -0
- package/src/zodToCreateTableSQL.js +28 -1
- /package/src/{runState-types.ts → runState.d.ts} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smithers-orchestrator/db",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "SQLite and Drizzle persistence adapter for Smithers workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
"import": "./src/index.js",
|
|
11
11
|
"default": "./src/index.js"
|
|
12
12
|
},
|
|
13
|
+
"./runState": {
|
|
14
|
+
"import": "./src/runState.js",
|
|
15
|
+
"default": "./src/runState.js"
|
|
16
|
+
},
|
|
13
17
|
"./*": {
|
|
14
18
|
"types": "./src/index.d.ts",
|
|
15
19
|
"import": "./src/*.js",
|
|
@@ -26,10 +30,10 @@
|
|
|
26
30
|
"drizzle-zod": "^0.8.3",
|
|
27
31
|
"effect": "^3.21.1",
|
|
28
32
|
"zod": "^4.3.6",
|
|
29
|
-
"@smithers-orchestrator/errors": "0.
|
|
30
|
-
"@smithers-orchestrator/
|
|
31
|
-
"@smithers-orchestrator/
|
|
32
|
-
"@smithers-orchestrator/
|
|
33
|
+
"@smithers-orchestrator/errors": "0.24.0",
|
|
34
|
+
"@smithers-orchestrator/graph": "0.24.0",
|
|
35
|
+
"@smithers-orchestrator/scheduler": "0.24.0",
|
|
36
|
+
"@smithers-orchestrator/observability": "0.24.0"
|
|
33
37
|
},
|
|
34
38
|
"devDependencies": {
|
|
35
39
|
"@electric-sql/pglite": "0.5.1",
|
package/src/adapter.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// @smithers-type-exports-end
|
|
12
12
|
|
|
13
13
|
import { getTableName } from "drizzle-orm";
|
|
14
|
+
import { getTableColumns } from "drizzle-orm/utils";
|
|
14
15
|
import { Effect, Exit, FiberId, Metric } from "effect";
|
|
15
16
|
import { toSmithersError } from "@smithers-orchestrator/errors/toSmithersError";
|
|
16
17
|
import { getSqlMessageStorage } from "./sql-message-storage.js";
|
|
@@ -25,6 +26,7 @@ import { camelToSnake } from "./utils/camelToSnake.js";
|
|
|
25
26
|
/** @typedef {import("./adapter/AlertStatus.ts").AlertStatus} AlertStatus */
|
|
26
27
|
/** @typedef {import("./adapter/AttemptRow.ts").AttemptRow} AttemptRow */
|
|
27
28
|
/** @typedef {import("drizzle-orm/bun-sqlite").BunSQLiteDatabase} BunSQLiteDatabase */
|
|
29
|
+
/** @typedef {import("drizzle-orm").Table} Table */
|
|
28
30
|
/** @typedef {import("./adapter/EventHistoryQuery.ts").EventHistoryQuery} EventHistoryQuery */
|
|
29
31
|
/** @typedef {import("./adapter/HumanRequestRow.ts").HumanRequestRow} HumanRequestRow */
|
|
30
32
|
/** @typedef {import("./output/OutputKey.ts").OutputKey} OutputKey */
|
|
@@ -427,6 +429,85 @@ function resolveOutputTableName(table) {
|
|
|
427
429
|
return "output";
|
|
428
430
|
}
|
|
429
431
|
}
|
|
432
|
+
/**
|
|
433
|
+
* @param {unknown} db
|
|
434
|
+
* @param {string} tableName
|
|
435
|
+
* @returns {unknown | null}
|
|
436
|
+
*/
|
|
437
|
+
function findDrizzleTableByName(db, tableName) {
|
|
438
|
+
const schema = /** @type {{ _?: { fullSchema?: Record<string, unknown> } }} */ (db)?._?.fullSchema;
|
|
439
|
+
if (!schema) return null;
|
|
440
|
+
for (const table of Object.values(schema)) {
|
|
441
|
+
try {
|
|
442
|
+
if (getTableName(/** @type {Table} */ (table)) === tableName) return table;
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
// Ignore non-table schema entries.
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* @param {unknown} table
|
|
452
|
+
* @returns {string[]}
|
|
453
|
+
*/
|
|
454
|
+
function getPhysicalBooleanColumnNames(table) {
|
|
455
|
+
try {
|
|
456
|
+
const cols = getTableColumns(/** @type {Table} */ (table));
|
|
457
|
+
const names = [];
|
|
458
|
+
for (const col of Object.values(cols)) {
|
|
459
|
+
const c = /** @type {Record<string, unknown> & { config?: { mode?: string }; mapFromDriverValue?: unknown; name?: unknown }} */ (/** @type {unknown} */ (col));
|
|
460
|
+
const mapFn = /** @type {{ toString?: () => string } | undefined} */ (c?.mapFromDriverValue);
|
|
461
|
+
if (c?.columnType === "SQLiteBoolean" ||
|
|
462
|
+
c?.config?.mode === "boolean" ||
|
|
463
|
+
c?.mode === "boolean" ||
|
|
464
|
+
c?.dataType === "boolean" ||
|
|
465
|
+
mapFn?.toString?.().includes("Boolean")) {
|
|
466
|
+
const name = typeof c.name === "string" ? c.name : null;
|
|
467
|
+
if (name) names.push(name);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return names;
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
return [];
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* @param {Record<string, unknown> | null | undefined} row
|
|
478
|
+
* @param {readonly string[]} columnNames
|
|
479
|
+
* @returns {Record<string, unknown> | null | undefined}
|
|
480
|
+
*/
|
|
481
|
+
function coerceRawBooleanColumns(row, columnNames) {
|
|
482
|
+
if (!row || columnNames.length === 0) return row;
|
|
483
|
+
const next = { ...row };
|
|
484
|
+
for (const columnName of columnNames) {
|
|
485
|
+
const value = next[columnName];
|
|
486
|
+
if (columnName in next && typeof value !== "boolean") {
|
|
487
|
+
if (value === 0 || value === 0n || value === "0") next[columnName] = false;
|
|
488
|
+
else if (value === 1 || value === 1n || value === "1") next[columnName] = true;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return next;
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* @param {unknown} client
|
|
495
|
+
* @param {string} tableName
|
|
496
|
+
* @returns {string[]}
|
|
497
|
+
*/
|
|
498
|
+
function getPersistedBooleanColumnNames(client, tableName) {
|
|
499
|
+
try {
|
|
500
|
+
const rows = /** @type {{ query: (sql: string) => { all: (...args: unknown[]) => Array<Record<string, unknown>> } }} */ (client)
|
|
501
|
+
.query(`SELECT column_name FROM _smithers_output_schema_columns WHERE table_name = ? AND kind = 'boolean'`)
|
|
502
|
+
.all(tableName);
|
|
503
|
+
return rows
|
|
504
|
+
.map((row) => row.column_name)
|
|
505
|
+
.filter((name) => typeof name === "string");
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
return [];
|
|
509
|
+
}
|
|
510
|
+
}
|
|
430
511
|
/**
|
|
431
512
|
* @param {unknown} db
|
|
432
513
|
* @returns {{ run: (sql: string) => unknown; query: (sql: string) => { run: (...args: unknown[]) => unknown; get: (...args: unknown[]) => Record<string, unknown> | null | undefined; all: () => Array<Record<string, unknown>> }; exec: (sql: string) => unknown; $client?: unknown }}
|
|
@@ -1191,14 +1272,19 @@ export class SmithersDb {
|
|
|
1191
1272
|
return runnableEffect(this.read(`get raw node output ${tableName}`, () => {
|
|
1192
1273
|
const escaped = tableName.replaceAll(`"`, `""`);
|
|
1193
1274
|
if (this.internalStorage.dialect === POSTGRES) {
|
|
1275
|
+
const boolColumns = getPhysicalBooleanColumnNames(findDrizzleTableByName(this.db, tableName));
|
|
1194
1276
|
return this.internalStorage
|
|
1195
1277
|
.queryOneRaw(`SELECT * FROM "${escaped}" WHERE run_id = ? AND node_id = ? ORDER BY iteration DESC LIMIT 1`, [runId, nodeId])
|
|
1196
|
-
.then((row) => row ?? null);
|
|
1278
|
+
.then((row) => coerceRawBooleanColumns(row ?? null, boolColumns) ?? null);
|
|
1197
1279
|
}
|
|
1198
1280
|
const client = this.db.session.client;
|
|
1281
|
+
const boolColumns = [
|
|
1282
|
+
...getPersistedBooleanColumnNames(client, tableName),
|
|
1283
|
+
...getPhysicalBooleanColumnNames(findDrizzleTableByName(this.db, tableName)),
|
|
1284
|
+
];
|
|
1199
1285
|
const stmt = client.query(`SELECT * FROM "${escaped}" WHERE run_id = ? AND node_id = ? ORDER BY iteration DESC LIMIT 1`);
|
|
1200
1286
|
const row = stmt.get(runId, nodeId);
|
|
1201
|
-
return Promise.resolve(row ?? null);
|
|
1287
|
+
return Promise.resolve(coerceRawBooleanColumns(row ?? null, boolColumns) ?? null);
|
|
1202
1288
|
}).pipe(Effect.catchAll(() => Effect.succeed(null))));
|
|
1203
1289
|
}
|
|
1204
1290
|
/**
|
|
@@ -1208,18 +1294,40 @@ export class SmithersDb {
|
|
|
1208
1294
|
* @param {number} iteration
|
|
1209
1295
|
* @returns {RunnableEffect<Record<string, unknown> | null, SmithersError>}
|
|
1210
1296
|
*/
|
|
1297
|
+
/**
|
|
1298
|
+
* Whether a physical table with this exact name exists in the database.
|
|
1299
|
+
* @param {string} tableName
|
|
1300
|
+
* @returns {RunnableEffect<boolean, SmithersError>}
|
|
1301
|
+
*/
|
|
1302
|
+
hasPhysicalTable(tableName) {
|
|
1303
|
+
return runnableEffect(this.read(`has physical table ${tableName}`, () => {
|
|
1304
|
+
if (this.internalStorage.dialect === POSTGRES) {
|
|
1305
|
+
return this.internalStorage
|
|
1306
|
+
.queryOneRaw(`SELECT 1 AS one FROM information_schema.tables WHERE table_name = ? LIMIT 1`, [tableName])
|
|
1307
|
+
.then((row) => row != null);
|
|
1308
|
+
}
|
|
1309
|
+
const client = this.db.session.client;
|
|
1310
|
+
const stmt = client.query(`SELECT 1 AS one FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1`);
|
|
1311
|
+
return Promise.resolve(stmt.get(tableName) != null);
|
|
1312
|
+
}).pipe(Effect.catchAll(() => Effect.succeed(false))));
|
|
1313
|
+
}
|
|
1211
1314
|
getRawNodeOutputForIteration(tableName, runId, nodeId, iteration) {
|
|
1212
1315
|
return runnableEffect(this.read(`get raw node output ${tableName} iteration ${iteration}`, () => {
|
|
1213
1316
|
const escaped = tableName.replaceAll(`"`, `""`);
|
|
1214
1317
|
if (this.internalStorage.dialect === POSTGRES) {
|
|
1318
|
+
const boolColumns = getPhysicalBooleanColumnNames(findDrizzleTableByName(this.db, tableName));
|
|
1215
1319
|
return this.internalStorage
|
|
1216
1320
|
.queryOneRaw(`SELECT * FROM "${escaped}" WHERE run_id = ? AND node_id = ? AND iteration = ? LIMIT 1`, [runId, nodeId, iteration])
|
|
1217
|
-
.then((row) => row ?? null);
|
|
1321
|
+
.then((row) => coerceRawBooleanColumns(row ?? null, boolColumns) ?? null);
|
|
1218
1322
|
}
|
|
1219
1323
|
const client = this.db.session.client;
|
|
1324
|
+
const boolColumns = [
|
|
1325
|
+
...getPersistedBooleanColumnNames(client, tableName),
|
|
1326
|
+
...getPhysicalBooleanColumnNames(findDrizzleTableByName(this.db, tableName)),
|
|
1327
|
+
];
|
|
1220
1328
|
const stmt = client.query(`SELECT * FROM "${escaped}" WHERE run_id = ? AND node_id = ? AND iteration = ? LIMIT 1`);
|
|
1221
1329
|
const row = stmt.get(runId, nodeId, iteration);
|
|
1222
|
-
return Promise.resolve(row ?? null);
|
|
1330
|
+
return Promise.resolve(coerceRawBooleanColumns(row ?? null, boolColumns) ?? null);
|
|
1223
1331
|
}).pipe(Effect.catchAll(() => Effect.succeed(null))));
|
|
1224
1332
|
}
|
|
1225
1333
|
/**
|
|
@@ -2125,6 +2233,34 @@ export class SmithersDb {
|
|
|
2125
2233
|
WHERE run_id = ? AND status = ?`, [runId, "requested"], { booleanColumns: ["autoApproved"] }));
|
|
2126
2234
|
}
|
|
2127
2235
|
/**
|
|
2236
|
+
* @param {string} runId
|
|
2237
|
+
* @returns {RunnableEffect<ApprovalRow[], SmithersError>}
|
|
2238
|
+
*/
|
|
2239
|
+
listDecidedApprovals(runId) {
|
|
2240
|
+
return this.read(`list decided approvals ${runId}`, () => this.internalStorage.queryAll(`SELECT a.*
|
|
2241
|
+
FROM _smithers_approvals a
|
|
2242
|
+
JOIN _smithers_nodes n
|
|
2243
|
+
ON a.run_id = n.run_id
|
|
2244
|
+
AND a.node_id = n.node_id
|
|
2245
|
+
AND a.iteration = n.iteration
|
|
2246
|
+
WHERE a.run_id = ?
|
|
2247
|
+
AND a.status IN ('approved', 'denied')
|
|
2248
|
+
AND n.state = 'pending'`, [runId], { booleanColumns: ["autoApproved"] }));
|
|
2249
|
+
}
|
|
2250
|
+
/**
|
|
2251
|
+
* Returns all decided approvals for a run (approved or denied), regardless of
|
|
2252
|
+
* node state. Used by why-diagnosis so denied gates (node state = 'failed')
|
|
2253
|
+
* are included in the diagnosis output.
|
|
2254
|
+
* @param {string} runId
|
|
2255
|
+
* @returns {RunnableEffect<ApprovalRow[], SmithersError>}
|
|
2256
|
+
*/
|
|
2257
|
+
listAllDecidedApprovals(runId) {
|
|
2258
|
+
return this.read(`list all decided approvals ${runId}`, () => this.internalStorage.queryAll(`SELECT *
|
|
2259
|
+
FROM _smithers_approvals
|
|
2260
|
+
WHERE run_id = ?
|
|
2261
|
+
AND status IN ('approved', 'denied')`, [runId], { booleanColumns: ["autoApproved"] }));
|
|
2262
|
+
}
|
|
2263
|
+
/**
|
|
2128
2264
|
* @returns {RunnableEffect<Array<Record<string, unknown>>, SmithersError>}
|
|
2129
2265
|
*/
|
|
2130
2266
|
listAllPendingApprovals() {
|
|
@@ -2134,8 +2270,12 @@ export class SmithersDb {
|
|
|
2134
2270
|
a.iteration,
|
|
2135
2271
|
a.status,
|
|
2136
2272
|
a.requested_at_ms,
|
|
2273
|
+
a.decided_at_ms,
|
|
2137
2274
|
a.note,
|
|
2138
2275
|
a.decided_by,
|
|
2276
|
+
a.request_json,
|
|
2277
|
+
a.decision_json,
|
|
2278
|
+
a.auto_approved,
|
|
2139
2279
|
r.workflow_name,
|
|
2140
2280
|
r.status AS run_status,
|
|
2141
2281
|
n.label AS node_label
|
|
@@ -2146,7 +2286,7 @@ export class SmithersDb {
|
|
|
2146
2286
|
AND a.node_id = n.node_id
|
|
2147
2287
|
AND a.iteration = n.iteration
|
|
2148
2288
|
WHERE a.status = ?
|
|
2149
|
-
ORDER BY COALESCE(a.requested_at_ms, 0) ASC, a.run_id, a.node_id, a.iteration`, ["requested"]));
|
|
2289
|
+
ORDER BY COALESCE(a.requested_at_ms, 0) ASC, a.run_id, a.node_id, a.iteration`, ["requested"], { booleanColumns: ["autoApproved"] }));
|
|
2150
2290
|
}
|
|
2151
2291
|
/**
|
|
2152
2292
|
* @param {string} workflowName
|
|
@@ -2558,6 +2698,20 @@ export class SmithersDb {
|
|
|
2558
2698
|
}
|
|
2559
2699
|
/**
|
|
2560
2700
|
* @param {string} runId
|
|
2701
|
+
* @returns {RunnableEffect<ApprovalRow[], SmithersError>}
|
|
2702
|
+
*/
|
|
2703
|
+
listDecidedApprovalsEffect(runId) {
|
|
2704
|
+
return this.listDecidedApprovals(runId);
|
|
2705
|
+
}
|
|
2706
|
+
/**
|
|
2707
|
+
* @param {string} runId
|
|
2708
|
+
* @returns {RunnableEffect<ApprovalRow[], SmithersError>}
|
|
2709
|
+
*/
|
|
2710
|
+
listAllDecidedApprovalsEffect(runId) {
|
|
2711
|
+
return this.listAllDecidedApprovals(runId);
|
|
2712
|
+
}
|
|
2713
|
+
/**
|
|
2714
|
+
* @param {string} runId
|
|
2561
2715
|
* @returns {RunnableEffect<FrameRow | undefined, SmithersError>}
|
|
2562
2716
|
*/
|
|
2563
2717
|
getLastFrameEffect(runId) {
|
package/src/index.d.ts
CHANGED
|
@@ -958,6 +958,16 @@ declare class SmithersDb {
|
|
|
958
958
|
*/
|
|
959
959
|
listPendingApprovals(runId: string): RunnableEffect<ApprovalRow[], SmithersError$1>;
|
|
960
960
|
/**
|
|
961
|
+
* @param {string} runId
|
|
962
|
+
* @returns {RunnableEffect<ApprovalRow[], SmithersError>}
|
|
963
|
+
*/
|
|
964
|
+
listDecidedApprovals(runId: string): RunnableEffect<ApprovalRow[], SmithersError$1>;
|
|
965
|
+
/**
|
|
966
|
+
* @param {string} runId
|
|
967
|
+
* @returns {RunnableEffect<ApprovalRow[], SmithersError>}
|
|
968
|
+
*/
|
|
969
|
+
listAllDecidedApprovals(runId: string): RunnableEffect<ApprovalRow[], SmithersError$1>;
|
|
970
|
+
/**
|
|
961
971
|
* @returns {RunnableEffect<Array<Record<string, unknown>>, SmithersError>}
|
|
962
972
|
*/
|
|
963
973
|
listAllPendingApprovals(): RunnableEffect<Array<Record<string, unknown>>, SmithersError$1>;
|
|
@@ -1179,6 +1189,16 @@ declare class SmithersDb {
|
|
|
1179
1189
|
listPendingApprovalsEffect(runId: string): RunnableEffect<ApprovalRow[], SmithersError$1>;
|
|
1180
1190
|
/**
|
|
1181
1191
|
* @param {string} runId
|
|
1192
|
+
* @returns {RunnableEffect<ApprovalRow[], SmithersError>}
|
|
1193
|
+
*/
|
|
1194
|
+
listDecidedApprovalsEffect(runId: string): RunnableEffect<ApprovalRow[], SmithersError$1>;
|
|
1195
|
+
/**
|
|
1196
|
+
* @param {string} runId
|
|
1197
|
+
* @returns {RunnableEffect<ApprovalRow[], SmithersError>}
|
|
1198
|
+
*/
|
|
1199
|
+
listAllDecidedApprovalsEffect(runId: string): RunnableEffect<ApprovalRow[], SmithersError$1>;
|
|
1200
|
+
/**
|
|
1201
|
+
* @param {string} runId
|
|
1182
1202
|
* @returns {RunnableEffect<FrameRow | undefined, SmithersError>}
|
|
1183
1203
|
*/
|
|
1184
1204
|
getLastFrameEffect(runId: string): RunnableEffect<FrameRow | undefined, SmithersError$1>;
|
|
@@ -18,6 +18,21 @@ function sqliteTypeFor(zodFieldSchema) {
|
|
|
18
18
|
}
|
|
19
19
|
return "TEXT";
|
|
20
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
|
+
}
|
|
21
36
|
function quoteIdentifier(identifier) {
|
|
22
37
|
return `"${String(identifier).replaceAll(`"`, `""`)}"`;
|
|
23
38
|
}
|
|
@@ -29,7 +44,7 @@ export function zodSchemaColumns(schema) {
|
|
|
29
44
|
const out = [];
|
|
30
45
|
const shape = schema.shape;
|
|
31
46
|
for (const [key] of Object.entries(shape)) {
|
|
32
|
-
out.push({ name: camelToSnake(key), sqliteType: sqliteTypeFor(shape[key]) });
|
|
47
|
+
out.push({ name: camelToSnake(key), sqliteType: sqliteTypeFor(shape[key]), kind: sqliteKindFor(shape[key]) });
|
|
33
48
|
}
|
|
34
49
|
return out;
|
|
35
50
|
}
|
|
@@ -68,6 +83,18 @@ export function zodToCreateTableSQL(tableName, schema, opts) {
|
|
|
68
83
|
*/
|
|
69
84
|
export function syncZodTableSchema(sqlite, tableName, schema, opts) {
|
|
70
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
|
+
}
|
|
71
98
|
let existing;
|
|
72
99
|
const quotedTable = quoteIdentifier(tableName);
|
|
73
100
|
try {
|
|
File without changes
|