@prisma-next/adapter-sqlite 0.12.0 → 0.13.0-dev.10

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.
@@ -1,16 +1,18 @@
1
- import type { ContractMarkerRecord } from '@prisma-next/contract/types';
1
+ import type { ContractMarkerRecord, LedgerEntryRecord } from '@prisma-next/contract/types';
2
2
  import { parseMarkerRowSafely, withMarkerReadErrorHandling } from '@prisma-next/errors/execution';
3
3
  import type { SqlControlAdapter } from '@prisma-next/family-sql/control-adapter';
4
4
  import { parseContractMarkerRow } from '@prisma-next/family-sql/verify';
5
- import {
6
- APP_SPACE_ID,
7
- type ControlDriverInstance,
8
- } from '@prisma-next/framework-components/control';
5
+ import { APP_SPACE_ID } from '@prisma-next/framework-components/control';
6
+ import { ledgerOriginFromStored } from '@prisma-next/migration-tools/ledger-origin';
7
+ import type { SqlControlDriverInstance } from '@prisma-next/sql-contract/types';
9
8
  import type {
10
9
  AnyQueryAst,
10
+ DdlNode,
11
11
  LoweredStatement,
12
12
  LowererContext,
13
+ MarkerReadResult,
13
14
  } from '@prisma-next/sql-relational-core/ast';
15
+ import { isDdlNode } from '@prisma-next/sql-relational-core/ast';
14
16
  import type {
15
17
  PrimaryKey,
16
18
  SqlColumnIR,
@@ -21,36 +23,42 @@ import type {
21
23
  SqlTableIR,
22
24
  SqlUniqueIR,
23
25
  } from '@prisma-next/sql-schema-ir/types';
26
+ import {
27
+ buildControlTableBootstrapQueries,
28
+ buildSignMarkerBootstrapQueries,
29
+ } from '@prisma-next/target-sqlite/contract-free';
30
+ import type { SqliteDdlNode } from '@prisma-next/target-sqlite/ddl';
24
31
  import { parseSqliteDefault } from '@prisma-next/target-sqlite/default-normalizer';
25
32
  import { normalizeSqliteNativeType } from '@prisma-next/target-sqlite/native-type-normalizer';
33
+ import { blindCast } from '@prisma-next/utils/casts';
26
34
  import { ifDefined } from '@prisma-next/utils/defined';
27
35
  import { renderLoweredSql } from './adapter';
36
+ import { renderLoweredDdl } from './ddl-renderer';
37
+ import { coerceLedgerAppliedAt, operationCountFromStored } from './ledger-decode';
38
+ import {
39
+ decodeSqliteMarkerRow,
40
+ execute,
41
+ ledger,
42
+ ledgerReadShape,
43
+ marker,
44
+ mergeInvariants,
45
+ NOW,
46
+ sqliteCatalog,
47
+ } from './marker-ledger';
28
48
  import type { SqliteContract } from './types';
29
49
 
30
50
  const SQLITE_MARKER_TABLE = '_prisma_marker';
31
-
32
- /**
33
- * SQLite stores arrays as JSON-encoded TEXT (no native array type), so the
34
- * driver returns `invariants` as a string. Decode before delegating to the
35
- * shared row schema, which expects `string[]`. A non-JSON value here is a
36
- * corrupt row and surfaces as `Invalid contract marker row: …` via the
37
- * typed-envelope wrapper.
38
- */
39
- function decodeSqliteMarkerRow(row: unknown): unknown {
40
- if (typeof row !== 'object' || row === null || !('invariants' in row)) {
41
- return row;
42
- }
43
- const record = row as { invariants: unknown };
44
- if (typeof record.invariants !== 'string') return row;
45
- let parsed: unknown;
46
- try {
47
- parsed = JSON.parse(record.invariants);
48
- } catch (err) {
49
- const detail = err instanceof Error ? err.message : String(err);
50
- throw new Error(`Invalid contract marker row: invariants is not valid JSON: ${detail}`);
51
- }
52
- return { ...record, invariants: parsed };
53
- }
51
+ const SQLITE_LEDGER_TABLE = '_prisma_ledger';
52
+
53
+ type SqliteLedgerRow = {
54
+ readonly space: string;
55
+ readonly migration_name: string;
56
+ readonly migration_hash: string;
57
+ readonly origin_core_hash: string | null;
58
+ readonly destination_core_hash: string;
59
+ readonly operations: unknown;
60
+ readonly created_at: Date | string;
61
+ };
54
62
 
55
63
  // PRAGMA result row types
56
64
  type PragmaTableInfoRow = {
@@ -101,6 +109,14 @@ export class SqliteControlAdapter implements SqlControlAdapter<'sqlite'> {
101
109
  readonly normalizeDefault = parseSqliteDefault;
102
110
  readonly normalizeNativeType = normalizeSqliteNativeType;
103
111
 
112
+ bootstrapControlTableQueries(): readonly DdlNode[] {
113
+ return buildControlTableBootstrapQueries();
114
+ }
115
+
116
+ bootstrapSignMarkerQueries(): readonly DdlNode[] {
117
+ return buildSignMarkerBootstrapQueries();
118
+ }
119
+
104
120
  /**
105
121
  * Lower a SQL query AST into a SQLite-flavored `{ sql, params }` payload.
106
122
  *
@@ -109,7 +125,10 @@ export class SqliteControlAdapter implements SqlControlAdapter<'sqlite'> {
109
125
  * and contract. Used at migration plan/emit time (e.g. by `dataTransform`)
110
126
  * without instantiating the runtime adapter.
111
127
  */
112
- lower(ast: AnyQueryAst, context: LowererContext<unknown>): LoweredStatement {
128
+ lower(ast: AnyQueryAst | SqliteDdlNode, context: LowererContext<unknown>): LoweredStatement {
129
+ if (isDdlNode(ast)) {
130
+ return renderLoweredDdl(ast);
131
+ }
113
132
  return renderLoweredSql(ast, context.contract as SqliteContract);
114
133
  }
115
134
 
@@ -119,56 +138,19 @@ export class SqliteControlAdapter implements SqlControlAdapter<'sqlite'> {
119
138
  * "no such table" error.
120
139
  */
121
140
  async readMarker(
122
- driver: ControlDriverInstance<'sql', 'sqlite'>,
141
+ driver: SqlControlDriverInstance<'sqlite'>,
123
142
  space: string,
124
143
  ): Promise<ContractMarkerRecord | null> {
125
- const markerContext = { space, markerLocation: SQLITE_MARKER_TABLE };
126
- const exists = await withMarkerReadErrorHandling(
127
- () =>
128
- driver.query(`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?`, [
129
- '_prisma_marker',
130
- ]),
131
- markerContext,
132
- );
133
- if (exists.rows.length === 0) {
134
- return null;
135
- }
136
-
137
- const result = await withMarkerReadErrorHandling(
138
- () =>
139
- driver.query<{
140
- core_hash: string;
141
- profile_hash: string;
142
- contract_json: unknown | null;
143
- canonical_version: number | null;
144
- updated_at: Date | string;
145
- app_tag: string | null;
146
- meta: unknown | null;
147
- invariants: unknown;
148
- }>(
149
- `SELECT
150
- core_hash,
151
- profile_hash,
152
- contract_json,
153
- canonical_version,
154
- updated_at,
155
- app_tag,
156
- meta,
157
- invariants
158
- FROM _prisma_marker
159
- WHERE space = ?`,
160
- [space],
161
- ),
162
- markerContext,
163
- );
144
+ const result = await this.readMarkerDiscriminated(driver, space);
145
+ return result.kind === 'present' ? result.record : null;
146
+ }
164
147
 
165
- const row = result.rows[0];
166
- if (!row) return null;
167
- return parseMarkerRowSafely(
168
- row,
169
- (raw) => parseContractMarkerRow(decodeSqliteMarkerRow(raw)),
170
- markerContext,
171
- );
148
+ async readMarkerDiscriminated(
149
+ driver: SqlControlDriverInstance<'sqlite'>,
150
+ space: string,
151
+ ): Promise<MarkerReadResult> {
152
+ const markerContext = { space, markerLocation: SQLITE_MARKER_TABLE };
153
+ return withMarkerReadErrorHandling(() => this.readMarkerResult(driver, space), markerContext);
172
154
  }
173
155
 
174
156
  /**
@@ -177,51 +159,47 @@ export class SqliteControlAdapter implements SqlControlAdapter<'sqlite'> {
177
159
  * fresh database without the marker table returns an empty map.
178
160
  */
179
161
  async readAllMarkers(
180
- driver: ControlDriverInstance<'sql', 'sqlite'>,
162
+ driver: SqlControlDriverInstance<'sqlite'>,
181
163
  ): Promise<ReadonlyMap<string, ContractMarkerRecord>> {
182
164
  const markerContext = { space: APP_SPACE_ID, markerLocation: SQLITE_MARKER_TABLE };
183
- const exists = await withMarkerReadErrorHandling(
184
- () =>
185
- driver.query(`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?`, [
186
- '_prisma_marker',
187
- ]),
188
- markerContext,
189
- );
190
- if (exists.rows.length === 0) {
165
+ return withMarkerReadErrorHandling(() => this.readAllMarkersResult(driver), markerContext);
166
+ }
167
+
168
+ private async readAllMarkersResult(
169
+ driver: SqlControlDriverInstance<'sqlite'>,
170
+ ): Promise<ReadonlyMap<string, ContractMarkerRecord>> {
171
+ const lower = (query: AnyQueryAst) => this.lower(query, { contract: undefined });
172
+ const probe = sqliteCatalog
173
+ .select(sqliteCatalog.name)
174
+ .where(sqliteCatalog.type.eq('table').and(sqliteCatalog.name.eq('_prisma_marker')))
175
+ .build();
176
+ const exists = await execute(lower, driver, probe);
177
+ if (exists.length === 0) {
191
178
  return new Map();
192
179
  }
193
180
 
194
- const result = await withMarkerReadErrorHandling(
195
- () =>
196
- driver.query<{
197
- space: string;
198
- core_hash: string;
199
- profile_hash: string;
200
- contract_json: unknown | null;
201
- canonical_version: number | null;
202
- updated_at: Date | string;
203
- app_tag: string | null;
204
- meta: unknown | null;
205
- invariants: unknown;
206
- }>(
207
- `SELECT
208
- space,
209
- core_hash,
210
- profile_hash,
211
- contract_json,
212
- canonical_version,
213
- updated_at,
214
- app_tag,
215
- meta,
216
- invariants
217
- FROM _prisma_marker`,
218
- ),
219
- markerContext,
220
- );
221
-
222
- const rows = new Map<string, ContractMarkerRecord>();
223
- for (const row of result.rows) {
224
- rows.set(
181
+ const fetch = marker
182
+ .select(
183
+ marker.space,
184
+ marker.core_hash,
185
+ marker.profile_hash,
186
+ marker.contract_json,
187
+ marker.canonical_version,
188
+ marker.updated_at,
189
+ marker.app_tag,
190
+ marker.meta,
191
+ marker.invariants,
192
+ )
193
+ .build();
194
+ const rawRows = await execute(lower, driver, fetch);
195
+ const rows = blindCast<
196
+ ReadonlyArray<{ space: string } & Record<string, unknown>>,
197
+ 'Driver returns rows shaped by SELECT'
198
+ >(rawRows);
199
+
200
+ const out = new Map<string, ContractMarkerRecord>();
201
+ for (const row of rows) {
202
+ out.set(
225
203
  row.space,
226
204
  parseMarkerRowSafely(row, (raw) => parseContractMarkerRow(decodeSqliteMarkerRow(raw)), {
227
205
  space: row.space,
@@ -229,11 +207,238 @@ export class SqliteControlAdapter implements SqlControlAdapter<'sqlite'> {
229
207
  }),
230
208
  );
231
209
  }
232
- return rows;
210
+ return out;
211
+ }
212
+
213
+ /**
214
+ * Reads per-migration ledger rows from `_prisma_ledger` in apply order.
215
+ * Probes `sqlite_master` first so a fresh database without the ledger
216
+ * table returns `[]` instead of raising "no such table".
217
+ */
218
+ async readLedger(
219
+ driver: SqlControlDriverInstance<'sqlite'>,
220
+ space?: string,
221
+ ): Promise<readonly LedgerEntryRecord[]> {
222
+ const ledgerContext = { space: space ?? '*', markerLocation: SQLITE_LEDGER_TABLE };
223
+ return withMarkerReadErrorHandling(() => this.readLedgerResult(driver, space), ledgerContext);
224
+ }
225
+
226
+ private async readLedgerResult(
227
+ driver: SqlControlDriverInstance<'sqlite'>,
228
+ space: string | undefined,
229
+ ): Promise<readonly LedgerEntryRecord[]> {
230
+ const lower = (query: AnyQueryAst) => this.lower(query, { contract: undefined });
231
+ const probe = sqliteCatalog
232
+ .select(sqliteCatalog.name)
233
+ .where(sqliteCatalog.type.eq('table').and(sqliteCatalog.name.eq('_prisma_ledger')))
234
+ .build();
235
+ const exists = await execute(lower, driver, probe);
236
+ if (exists.length === 0) {
237
+ return [];
238
+ }
239
+
240
+ const base = ledgerReadShape.select(
241
+ ledgerReadShape.space,
242
+ ledgerReadShape.migration_name,
243
+ ledgerReadShape.migration_hash,
244
+ ledgerReadShape.origin_core_hash,
245
+ ledgerReadShape.destination_core_hash,
246
+ ledgerReadShape.operations,
247
+ ledgerReadShape.created_at,
248
+ );
249
+ const filtered = space !== undefined ? base.where(ledgerReadShape.space.eq(space)) : base;
250
+ const rawRows = await execute(lower, driver, filtered.orderBy(ledgerReadShape.id).build());
251
+ const rows = blindCast<readonly SqliteLedgerRow[], 'Driver returns rows shaped by SELECT'>(
252
+ rawRows,
253
+ );
254
+
255
+ return rows.map((row) => ({
256
+ space: row.space,
257
+ migrationName: row.migration_name,
258
+ migrationHash: row.migration_hash,
259
+ from: ledgerOriginFromStored(row.origin_core_hash),
260
+ to: row.destination_core_hash,
261
+ appliedAt: coerceLedgerAppliedAt(row.created_at),
262
+ operationCount: operationCountFromStored(row.operations),
263
+ }));
264
+ }
265
+
266
+ /**
267
+ * Stamps the initial marker row for `space` via the shared contract-free DML
268
+ * builder, lowered through {@link lower} and executed on the driver. See the
269
+ * `SqlControlAdapter.initMarker` contract.
270
+ */
271
+ async insertMarker(
272
+ driver: SqlControlDriverInstance<'sqlite'>,
273
+ space: string,
274
+ destination: {
275
+ readonly storageHash: string;
276
+ readonly profileHash: string;
277
+ readonly invariants?: readonly string[];
278
+ },
279
+ ): Promise<void> {
280
+ await execute(
281
+ (query) => this.lower(query, { contract: undefined }),
282
+ driver,
283
+ marker
284
+ .insert({
285
+ space,
286
+ core_hash: destination.storageHash,
287
+ profile_hash: destination.profileHash,
288
+ contract_json: null,
289
+ canonical_version: null,
290
+ updated_at: NOW,
291
+ app_tag: null,
292
+ meta: {},
293
+ invariants: destination.invariants ?? [],
294
+ })
295
+ .build(),
296
+ );
297
+ }
298
+
299
+ async initMarker(
300
+ driver: SqlControlDriverInstance<'sqlite'>,
301
+ space: string,
302
+ destination: {
303
+ readonly storageHash: string;
304
+ readonly profileHash: string;
305
+ readonly invariants?: readonly string[];
306
+ },
307
+ ): Promise<void> {
308
+ await execute(
309
+ (query) => this.lower(query, { contract: undefined }),
310
+ driver,
311
+ marker
312
+ .upsert({
313
+ space,
314
+ core_hash: destination.storageHash,
315
+ profile_hash: destination.profileHash,
316
+ contract_json: null,
317
+ canonical_version: null,
318
+ updated_at: NOW,
319
+ app_tag: null,
320
+ meta: {},
321
+ invariants: destination.invariants ?? [],
322
+ })
323
+ .onConflict(marker.space)
324
+ .doUpdate((excluded) => ({
325
+ core_hash: excluded.core_hash,
326
+ profile_hash: excluded.profile_hash,
327
+ contract_json: excluded.contract_json,
328
+ canonical_version: excluded.canonical_version,
329
+ updated_at: NOW,
330
+ app_tag: excluded.app_tag,
331
+ meta: excluded.meta,
332
+ invariants: excluded.invariants,
333
+ }))
334
+ .build(),
335
+ );
336
+ }
337
+
338
+ /**
339
+ * Compare-and-swap advance of the marker row for `space`. See the
340
+ * `SqlControlAdapter.updateMarker` contract.
341
+ */
342
+ async updateMarker(
343
+ driver: SqlControlDriverInstance<'sqlite'>,
344
+ space: string,
345
+ expectedFrom: string,
346
+ destination: {
347
+ readonly storageHash: string;
348
+ readonly profileHash: string;
349
+ readonly invariants?: readonly string[];
350
+ },
351
+ ): Promise<boolean> {
352
+ const currentInvariants =
353
+ destination.invariants === undefined
354
+ ? []
355
+ : ((await this.readMarker(driver, space))?.invariants ?? []);
356
+ const mergedInvariants =
357
+ destination.invariants === undefined
358
+ ? undefined
359
+ : mergeInvariants(currentInvariants, destination.invariants);
360
+
361
+ const query = marker
362
+ .update()
363
+ .set({
364
+ core_hash: destination.storageHash,
365
+ profile_hash: destination.profileHash,
366
+ updated_at: NOW,
367
+ ...(mergedInvariants !== undefined ? { invariants: mergedInvariants } : {}),
368
+ })
369
+ .where(marker.space.eq(space).and(marker.core_hash.eq(expectedFrom)))
370
+ .returning(marker.space)
371
+ .build();
372
+
373
+ const rows = await execute((q) => this.lower(q, { contract: undefined }), driver, query);
374
+ return rows.length > 0;
375
+ }
376
+
377
+ /**
378
+ * Appends a ledger entry for `space`. See the
379
+ * `SqlControlAdapter.writeLedgerEntry` contract.
380
+ */
381
+ async writeLedgerEntry(
382
+ driver: SqlControlDriverInstance<'sqlite'>,
383
+ space: string,
384
+ entry: {
385
+ readonly edgeId: string;
386
+ readonly from: string;
387
+ readonly to: string;
388
+ readonly migrationName: string;
389
+ readonly migrationHash: string;
390
+ readonly operations: readonly unknown[];
391
+ },
392
+ ): Promise<void> {
393
+ await execute(
394
+ (query) => this.lower(query, { contract: undefined }),
395
+ driver,
396
+ ledger
397
+ .insert({
398
+ space,
399
+ migration_name: entry.migrationName,
400
+ migration_hash: entry.migrationHash,
401
+ origin_core_hash: entry.from,
402
+ destination_core_hash: entry.to,
403
+ operations: entry.operations,
404
+ })
405
+ .build(),
406
+ );
407
+ }
408
+
409
+ private async readMarkerResult(driver: SqlControlDriverInstance<'sqlite'>, space: string) {
410
+ const lower = (query: AnyQueryAst) => this.lower(query, { contract: undefined });
411
+ const probe = sqliteCatalog
412
+ .select(sqliteCatalog.name)
413
+ .where(sqliteCatalog.type.eq('table').and(sqliteCatalog.name.eq('_prisma_marker')))
414
+ .build();
415
+ const exists = await execute(lower, driver, probe);
416
+ if (exists.length === 0) return { kind: 'no-table' as const };
417
+
418
+ const fetch = marker
419
+ .select(
420
+ marker.core_hash,
421
+ marker.profile_hash,
422
+ marker.contract_json,
423
+ marker.canonical_version,
424
+ marker.updated_at,
425
+ marker.app_tag,
426
+ marker.meta,
427
+ marker.invariants,
428
+ )
429
+ .where(marker.space.eq(space))
430
+ .build();
431
+ const result = await execute(lower, driver, fetch);
432
+ const row = result[0];
433
+ if (!row) return { kind: 'absent' as const };
434
+ return {
435
+ kind: 'present' as const,
436
+ record: parseContractMarkerRow(decodeSqliteMarkerRow(row)),
437
+ };
233
438
  }
234
439
 
235
440
  async introspect(
236
- driver: ControlDriverInstance<'sql', 'sqlite'>,
441
+ driver: SqlControlDriverInstance<'sqlite'>,
237
442
  _contract?: unknown,
238
443
  ): Promise<SqlSchemaIR> {
239
444
  // Filter out runner-managed control tables (`_prisma_marker`,
@@ -0,0 +1,123 @@
1
+ import { REFERENTIAL_ACTION_SQL } from '@prisma-next/sql-contract/referential-action-sql';
2
+ import type {
3
+ DdlColumn,
4
+ DdlColumnDefaultVisitor,
5
+ DdlTableConstraint,
6
+ ForeignKeyConstraint,
7
+ FunctionColumnDefault,
8
+ LiteralColumnDefault,
9
+ PrimaryKeyConstraint,
10
+ UniqueConstraint,
11
+ } from '@prisma-next/sql-relational-core/ast';
12
+ import type {
13
+ SqliteCreateTable,
14
+ SqliteDdlNode,
15
+ SqliteDdlVisitor,
16
+ } from '@prisma-next/target-sqlite/ddl';
17
+ import { escapeLiteral } from '@prisma-next/target-sqlite/sql-utils';
18
+ import type { SqliteLoweredStatement } from './types';
19
+
20
+ function renderPrimaryKeyConstraint(constraint: PrimaryKeyConstraint): string {
21
+ const cols = constraint.columns.join(', ');
22
+ if (constraint.name !== undefined) {
23
+ return `CONSTRAINT ${constraint.name} PRIMARY KEY (${cols})`;
24
+ }
25
+ return `PRIMARY KEY (${cols})`;
26
+ }
27
+
28
+ function renderForeignKeyConstraint(constraint: ForeignKeyConstraint): string {
29
+ const cols = constraint.columns.join(', ');
30
+ const refCols = constraint.refColumns.join(', ');
31
+ let sql = `FOREIGN KEY (${cols}) REFERENCES ${constraint.refTable} (${refCols})`;
32
+ if (constraint.onDelete !== undefined) {
33
+ sql += ` ON DELETE ${REFERENTIAL_ACTION_SQL[constraint.onDelete]}`;
34
+ }
35
+ if (constraint.onUpdate !== undefined) {
36
+ sql += ` ON UPDATE ${REFERENTIAL_ACTION_SQL[constraint.onUpdate]}`;
37
+ }
38
+ if (constraint.name !== undefined) {
39
+ sql = `CONSTRAINT ${constraint.name} ${sql}`;
40
+ }
41
+ return sql;
42
+ }
43
+
44
+ function renderUniqueConstraint(constraint: UniqueConstraint): string {
45
+ const cols = constraint.columns.join(', ');
46
+ if (constraint.name !== undefined) {
47
+ return `CONSTRAINT ${constraint.name} UNIQUE (${cols})`;
48
+ }
49
+ return `UNIQUE (${cols})`;
50
+ }
51
+
52
+ function renderTableConstraint(constraint: DdlTableConstraint): string {
53
+ switch (constraint.kind) {
54
+ case 'primary-key':
55
+ return renderPrimaryKeyConstraint(constraint);
56
+ case 'foreign-key':
57
+ return renderForeignKeyConstraint(constraint);
58
+ case 'unique':
59
+ return renderUniqueConstraint(constraint);
60
+ }
61
+ }
62
+
63
+ class SqliteDdlVisitorImpl implements SqliteDdlVisitor<string> {
64
+ createTable(node: SqliteCreateTable): string {
65
+ const ifNotExists = node.ifNotExists ? 'IF NOT EXISTS ' : '';
66
+ const tableRef = node.table;
67
+ const columnDefs = node.columns.map((column) => renderColumn(column));
68
+ const constraintDefs =
69
+ node.constraints !== undefined ? node.constraints.map(renderTableConstraint) : [];
70
+ const allDefs = [...columnDefs, ...constraintDefs].join(',\n ');
71
+ return `CREATE TABLE ${ifNotExists}${tableRef} (\n ${allDefs}\n )`;
72
+ }
73
+ }
74
+
75
+ const defaultVisitor: DdlColumnDefaultVisitor<string> = {
76
+ // SQLite has no `jsonb` / `json` column type, so the ctx is unused — but
77
+ // the visitor interface requires it for cross-dialect symmetry with the
78
+ // Postgres renderer, which uses ctx.nativeType to emit JSON casts.
79
+ literal(node: LiteralColumnDefault, _ctx): string {
80
+ const { value } = node;
81
+ if (typeof value === 'string') {
82
+ return `DEFAULT '${escapeLiteral(value)}'`;
83
+ }
84
+ if (typeof value === 'number' || typeof value === 'boolean') {
85
+ return `DEFAULT ${String(value)}`;
86
+ }
87
+ if (value === null) {
88
+ return 'DEFAULT NULL';
89
+ }
90
+ return `DEFAULT '${JSON.stringify(value)}'`;
91
+ },
92
+ function(node: FunctionColumnDefault, _ctx): string {
93
+ if (node.expression === 'autoincrement()') {
94
+ return '';
95
+ }
96
+ return `DEFAULT (${node.expression})`;
97
+ },
98
+ };
99
+
100
+ function renderColumn(column: DdlColumn): string {
101
+ if (column.type.includes('AUTOINCREMENT')) {
102
+ return `${column.name} ${column.type}`;
103
+ }
104
+ const parts = [column.name, column.type];
105
+ if (column.notNull) {
106
+ parts.push('NOT NULL');
107
+ }
108
+ if (column.primaryKey) {
109
+ parts.push('PRIMARY KEY');
110
+ }
111
+ const defaultClause = column.default
112
+ ? column.default.accept(defaultVisitor, { nativeType: column.type })
113
+ : '';
114
+ if (defaultClause.length > 0) {
115
+ parts.push(defaultClause);
116
+ }
117
+ return parts.join(' ');
118
+ }
119
+
120
+ export function renderLoweredDdl(ast: SqliteDdlNode): SqliteLoweredStatement {
121
+ const sql = ast.accept(new SqliteDdlVisitorImpl());
122
+ return Object.freeze({ sql, params: Object.freeze([]) });
123
+ }
@@ -0,0 +1,26 @@
1
+ const DESIGNATOR_LESS_UTC_DATETIME = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.\d+)?$/;
2
+
3
+ export function coerceLedgerAppliedAt(value: Date | string): Date {
4
+ if (value instanceof Date) {
5
+ return value;
6
+ }
7
+ if (DESIGNATOR_LESS_UTC_DATETIME.test(value)) {
8
+ return new Date(`${value.replace(' ', 'T')}Z`);
9
+ }
10
+ return new Date(value);
11
+ }
12
+
13
+ export function operationCountFromStored(operations: unknown): number {
14
+ if (Array.isArray(operations)) {
15
+ return operations.length;
16
+ }
17
+ if (typeof operations === 'string') {
18
+ try {
19
+ const parsed: unknown = JSON.parse(operations);
20
+ return Array.isArray(parsed) ? parsed.length : 0;
21
+ } catch {
22
+ return 0;
23
+ }
24
+ }
25
+ return 0;
26
+ }