@prisma-next/adapter-sqlite 0.12.0-dev.31 → 0.12.0-dev.32

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.
@@ -19,7 +19,6 @@ import type {
19
19
  LiteralExpr,
20
20
  LoweredParam,
21
21
  LowererContext,
22
- MarkerReadResult,
23
22
  NullCheckExpr,
24
23
  OperationExpr,
25
24
  OrderByItem,
@@ -35,9 +34,9 @@ import type {
35
34
  } from '@prisma-next/sql-relational-core/ast';
36
35
  import { isDdlNode } from '@prisma-next/sql-relational-core/ast';
37
36
  import type { RawCodecInferer } from '@prisma-next/sql-relational-core/expression';
38
- import { parseContractMarkerRow } from '@prisma-next/sql-runtime';
39
37
  import type { SqliteDdlNode } from '@prisma-next/target-sqlite/ddl';
40
38
  import { escapeLiteral, quoteIdentifier } from '@prisma-next/target-sqlite/sql-utils';
39
+ import { SqliteControlAdapter } from './control-adapter';
41
40
  import { renderLoweredDdl } from './ddl-renderer';
42
41
  import type { SqliteAdapterOptions, SqliteContract, SqliteLoweredStatement } from './types';
43
42
 
@@ -59,11 +58,27 @@ class SqliteAdapterImpl implements Adapter<AnyQueryAst, SqliteContract, SqliteLo
59
58
  readonly profile: AdapterProfile<'sqlite'>;
60
59
 
61
60
  constructor(options?: SqliteAdapterOptions) {
61
+ const controlAdapter = new SqliteControlAdapter();
62
62
  this.profile = Object.freeze({
63
63
  id: options?.profileId ?? 'sqlite/default@1',
64
64
  target: 'sqlite',
65
65
  capabilities: defaultCapabilities,
66
- readMarker: (queryable: SqlQueryable) => readSqliteMarker(queryable),
66
+ readMarker: (queryable: SqlQueryable) =>
67
+ controlAdapter.readMarkerDiscriminated(
68
+ {
69
+ familyId: 'sql',
70
+ targetId: 'sqlite',
71
+ query: async <Row = Record<string, unknown>>(
72
+ sql: string,
73
+ params?: readonly unknown[],
74
+ ) => {
75
+ const result = await queryable.query<Row>(sql, params);
76
+ return { rows: [...result.rows] };
77
+ },
78
+ close: async () => {},
79
+ },
80
+ APP_SPACE_ID,
81
+ ),
67
82
  });
68
83
  }
69
84
 
@@ -515,6 +530,8 @@ function renderInsertValue(value: InsertValue): string {
515
530
  return '?';
516
531
  case 'column-ref':
517
532
  return renderColumn(value);
533
+ case 'raw-expr':
534
+ return renderExpr(value);
518
535
  case 'default-value':
519
536
  throw new Error('SQLite does not support DEFAULT as a value in INSERT ... VALUES');
520
537
  default:
@@ -618,32 +635,6 @@ function renderReturning(returning: ReadonlyArray<ProjectionItem> | undefined):
618
635
  .join(', ')}`;
619
636
  }
620
637
 
621
- async function readSqliteMarker(queryable: SqlQueryable): Promise<MarkerReadResult> {
622
- const exists = await queryable.query(
623
- "select 1 from sqlite_master where type = 'table' and name = ?",
624
- ['_prisma_marker'],
625
- );
626
- if (exists.rows.length === 0) {
627
- return { kind: 'no-table' };
628
- }
629
-
630
- const result = await queryable.query(
631
- 'select core_hash, profile_hash, contract_json, canonical_version, updated_at, app_tag, meta, invariants from _prisma_marker where space = ?',
632
- [APP_SPACE_ID],
633
- );
634
- const row = result.rows[0];
635
- if (!row) {
636
- return { kind: 'absent' };
637
- }
638
- // SQLite stores arrays as JSON-encoded TEXT (no native array type), so the driver returns `invariants` as a string. Decode before delegating to the shared row schema, which expects `string[]`.
639
- const raw = row as Record<string, unknown>;
640
- const invariants =
641
- typeof raw['invariants'] === 'string'
642
- ? (JSON.parse(raw['invariants']) as unknown)
643
- : raw['invariants'];
644
- return { kind: 'present', record: parseContractMarkerRow({ ...raw, invariants }) };
645
- }
646
-
647
638
  export function createSqliteAdapter(options?: SqliteAdapterOptions) {
648
639
  return Object.freeze(new SqliteAdapterImpl(options));
649
640
  }
@@ -12,6 +12,7 @@ import type {
12
12
  DdlNode,
13
13
  LoweredStatement,
14
14
  LowererContext,
15
+ MarkerReadResult,
15
16
  } from '@prisma-next/sql-relational-core/ast';
16
17
  import { isDdlNode } from '@prisma-next/sql-relational-core/ast';
17
18
  import type {
@@ -31,37 +32,35 @@ import {
31
32
  import type { SqliteDdlNode } from '@prisma-next/target-sqlite/ddl';
32
33
  import { parseSqliteDefault } from '@prisma-next/target-sqlite/default-normalizer';
33
34
  import { normalizeSqliteNativeType } from '@prisma-next/target-sqlite/native-type-normalizer';
35
+ import { blindCast } from '@prisma-next/utils/casts';
34
36
  import { ifDefined } from '@prisma-next/utils/defined';
35
37
  import { renderLoweredSql } from './adapter';
36
38
  import { renderLoweredDdl } from './ddl-renderer';
37
39
  import { coerceLedgerAppliedAt, operationCountFromStored } from './ledger-decode';
40
+ import {
41
+ decodeSqliteMarkerRow,
42
+ execute,
43
+ ledger,
44
+ ledgerReadShape,
45
+ marker,
46
+ mergeInvariants,
47
+ NOW,
48
+ sqliteCatalog,
49
+ } from './marker-ledger';
38
50
  import type { SqliteContract } from './types';
39
51
 
40
52
  const SQLITE_MARKER_TABLE = '_prisma_marker';
41
53
  const SQLITE_LEDGER_TABLE = '_prisma_ledger';
42
54
 
43
- /**
44
- * SQLite stores arrays as JSON-encoded TEXT (no native array type), so the
45
- * driver returns `invariants` as a string. Decode before delegating to the
46
- * shared row schema, which expects `string[]`. A non-JSON value here is a
47
- * corrupt row and surfaces as `Invalid contract marker row: …` via the
48
- * typed-envelope wrapper.
49
- */
50
- function decodeSqliteMarkerRow(row: unknown): unknown {
51
- if (typeof row !== 'object' || row === null || !('invariants' in row)) {
52
- return row;
53
- }
54
- const record = row as { invariants: unknown };
55
- if (typeof record.invariants !== 'string') return row;
56
- let parsed: unknown;
57
- try {
58
- parsed = JSON.parse(record.invariants);
59
- } catch (err) {
60
- const detail = err instanceof Error ? err.message : String(err);
61
- throw new Error(`Invalid contract marker row: invariants is not valid JSON: ${detail}`);
62
- }
63
- return { ...record, invariants: parsed };
64
- }
55
+ type SqliteLedgerRow = {
56
+ readonly space: string;
57
+ readonly migration_name: string;
58
+ readonly migration_hash: string;
59
+ readonly origin_core_hash: string | null;
60
+ readonly destination_core_hash: string;
61
+ readonly operations: unknown;
62
+ readonly created_at: Date | string;
63
+ };
65
64
 
66
65
  // PRAGMA result row types
67
66
  type PragmaTableInfoRow = {
@@ -144,53 +143,16 @@ export class SqliteControlAdapter implements SqlControlAdapter<'sqlite'> {
144
143
  driver: ControlDriverInstance<'sql', 'sqlite'>,
145
144
  space: string,
146
145
  ): Promise<ContractMarkerRecord | null> {
147
- const markerContext = { space, markerLocation: SQLITE_MARKER_TABLE };
148
- const exists = await withMarkerReadErrorHandling(
149
- () =>
150
- driver.query(`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?`, [
151
- '_prisma_marker',
152
- ]),
153
- markerContext,
154
- );
155
- if (exists.rows.length === 0) {
156
- return null;
157
- }
158
-
159
- const result = await withMarkerReadErrorHandling(
160
- () =>
161
- driver.query<{
162
- core_hash: string;
163
- profile_hash: string;
164
- contract_json: unknown | null;
165
- canonical_version: number | null;
166
- updated_at: Date | string;
167
- app_tag: string | null;
168
- meta: unknown | null;
169
- invariants: unknown;
170
- }>(
171
- `SELECT
172
- core_hash,
173
- profile_hash,
174
- contract_json,
175
- canonical_version,
176
- updated_at,
177
- app_tag,
178
- meta,
179
- invariants
180
- FROM _prisma_marker
181
- WHERE space = ?`,
182
- [space],
183
- ),
184
- markerContext,
185
- );
146
+ const result = await this.readMarkerDiscriminated(driver, space);
147
+ return result.kind === 'present' ? result.record : null;
148
+ }
186
149
 
187
- const row = result.rows[0];
188
- if (!row) return null;
189
- return parseMarkerRowSafely(
190
- row,
191
- (raw) => parseContractMarkerRow(decodeSqliteMarkerRow(raw)),
192
- markerContext,
193
- );
150
+ async readMarkerDiscriminated(
151
+ driver: ControlDriverInstance<'sql', 'sqlite'>,
152
+ space: string,
153
+ ): Promise<MarkerReadResult> {
154
+ const markerContext = { space, markerLocation: SQLITE_MARKER_TABLE };
155
+ return withMarkerReadErrorHandling(() => this.readMarkerResult(driver, space), markerContext);
194
156
  }
195
157
 
196
158
  /**
@@ -202,48 +164,44 @@ export class SqliteControlAdapter implements SqlControlAdapter<'sqlite'> {
202
164
  driver: ControlDriverInstance<'sql', 'sqlite'>,
203
165
  ): Promise<ReadonlyMap<string, ContractMarkerRecord>> {
204
166
  const markerContext = { space: APP_SPACE_ID, markerLocation: SQLITE_MARKER_TABLE };
205
- const exists = await withMarkerReadErrorHandling(
206
- () =>
207
- driver.query(`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?`, [
208
- '_prisma_marker',
209
- ]),
210
- markerContext,
211
- );
212
- if (exists.rows.length === 0) {
167
+ return withMarkerReadErrorHandling(() => this.readAllMarkersResult(driver), markerContext);
168
+ }
169
+
170
+ private async readAllMarkersResult(
171
+ driver: ControlDriverInstance<'sql', 'sqlite'>,
172
+ ): Promise<ReadonlyMap<string, ContractMarkerRecord>> {
173
+ const lower = (query: AnyQueryAst) => this.lower(query, { contract: undefined });
174
+ const probe = sqliteCatalog
175
+ .select(sqliteCatalog.name)
176
+ .where(sqliteCatalog.type.eq('table').and(sqliteCatalog.name.eq('_prisma_marker')))
177
+ .build();
178
+ const exists = await execute(lower, driver, probe);
179
+ if (exists.length === 0) {
213
180
  return new Map();
214
181
  }
215
182
 
216
- const result = await withMarkerReadErrorHandling(
217
- () =>
218
- driver.query<{
219
- space: string;
220
- core_hash: string;
221
- profile_hash: string;
222
- contract_json: unknown | null;
223
- canonical_version: number | null;
224
- updated_at: Date | string;
225
- app_tag: string | null;
226
- meta: unknown | null;
227
- invariants: unknown;
228
- }>(
229
- `SELECT
230
- space,
231
- core_hash,
232
- profile_hash,
233
- contract_json,
234
- canonical_version,
235
- updated_at,
236
- app_tag,
237
- meta,
238
- invariants
239
- FROM _prisma_marker`,
240
- ),
241
- markerContext,
242
- );
243
-
244
- const rows = new Map<string, ContractMarkerRecord>();
245
- for (const row of result.rows) {
246
- rows.set(
183
+ const fetch = marker
184
+ .select(
185
+ marker.space,
186
+ marker.core_hash,
187
+ marker.profile_hash,
188
+ marker.contract_json,
189
+ marker.canonical_version,
190
+ marker.updated_at,
191
+ marker.app_tag,
192
+ marker.meta,
193
+ marker.invariants,
194
+ )
195
+ .build();
196
+ const rawRows = await execute(lower, driver, fetch);
197
+ const rows = blindCast<
198
+ ReadonlyArray<{ space: string } & Record<string, unknown>>,
199
+ 'Driver returns rows shaped by SELECT'
200
+ >(rawRows);
201
+
202
+ const out = new Map<string, ContractMarkerRecord>();
203
+ for (const row of rows) {
204
+ out.set(
247
205
  row.space,
248
206
  parseMarkerRowSafely(row, (raw) => parseContractMarkerRow(decodeSqliteMarkerRow(raw)), {
249
207
  space: row.space,
@@ -251,7 +209,7 @@ export class SqliteControlAdapter implements SqlControlAdapter<'sqlite'> {
251
209
  }),
252
210
  );
253
211
  }
254
- return rows;
212
+ return out;
255
213
  }
256
214
 
257
215
  /**
@@ -264,48 +222,39 @@ export class SqliteControlAdapter implements SqlControlAdapter<'sqlite'> {
264
222
  space?: string,
265
223
  ): Promise<readonly LedgerEntryRecord[]> {
266
224
  const ledgerContext = { space: space ?? '*', markerLocation: SQLITE_LEDGER_TABLE };
267
- const exists = await withMarkerReadErrorHandling(
268
- () =>
269
- driver.query(`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?`, [
270
- '_prisma_ledger',
271
- ]),
272
- ledgerContext,
273
- );
274
- if (exists.rows.length === 0) {
275
- return [];
276
- }
225
+ return withMarkerReadErrorHandling(() => this.readLedgerResult(driver, space), ledgerContext);
226
+ }
277
227
 
278
- type LedgerQueryRow = {
279
- space: string;
280
- migration_name: string;
281
- migration_hash: string;
282
- origin_core_hash: string | null;
283
- destination_core_hash: string;
284
- operations: unknown;
285
- created_at: Date | string;
286
- };
287
- let sql = `SELECT
288
- space,
289
- migration_name,
290
- migration_hash,
291
- origin_core_hash,
292
- destination_core_hash,
293
- operations,
294
- created_at
295
- FROM _prisma_ledger`;
296
- if (space !== undefined) {
297
- sql += `
298
- WHERE space = ?`;
228
+ private async readLedgerResult(
229
+ driver: ControlDriverInstance<'sql', 'sqlite'>,
230
+ space: string | undefined,
231
+ ): Promise<readonly LedgerEntryRecord[]> {
232
+ const lower = (query: AnyQueryAst) => this.lower(query, { contract: undefined });
233
+ const probe = sqliteCatalog
234
+ .select(sqliteCatalog.name)
235
+ .where(sqliteCatalog.type.eq('table').and(sqliteCatalog.name.eq('_prisma_ledger')))
236
+ .build();
237
+ const exists = await execute(lower, driver, probe);
238
+ if (exists.length === 0) {
239
+ return [];
299
240
  }
300
- sql += `
301
- ORDER BY id`;
302
241
 
303
- const result = await withMarkerReadErrorHandling(
304
- () => driver.query<LedgerQueryRow>(sql, space === undefined ? undefined : [space]),
305
- ledgerContext,
242
+ const base = ledgerReadShape.select(
243
+ ledgerReadShape.space,
244
+ ledgerReadShape.migration_name,
245
+ ledgerReadShape.migration_hash,
246
+ ledgerReadShape.origin_core_hash,
247
+ ledgerReadShape.destination_core_hash,
248
+ ledgerReadShape.operations,
249
+ ledgerReadShape.created_at,
250
+ );
251
+ const filtered = space !== undefined ? base.where(ledgerReadShape.space.eq(space)) : base;
252
+ const rawRows = await execute(lower, driver, filtered.orderBy(ledgerReadShape.id).build());
253
+ const rows = blindCast<readonly SqliteLedgerRow[], 'Driver returns rows shaped by SELECT'>(
254
+ rawRows,
306
255
  );
307
256
 
308
- return result.rows.map((row) => ({
257
+ return rows.map((row) => ({
309
258
  space: row.space,
310
259
  migrationName: row.migration_name,
311
260
  migrationHash: row.migration_hash,
@@ -316,6 +265,180 @@ export class SqliteControlAdapter implements SqlControlAdapter<'sqlite'> {
316
265
  }));
317
266
  }
318
267
 
268
+ /**
269
+ * Stamps the initial marker row for `space` via the shared contract-free DML
270
+ * builder, lowered through {@link lower} and executed on the driver. See the
271
+ * `SqlControlAdapter.initMarker` contract.
272
+ */
273
+ async insertMarker(
274
+ driver: ControlDriverInstance<'sql', 'sqlite'>,
275
+ space: string,
276
+ destination: {
277
+ readonly storageHash: string;
278
+ readonly profileHash: string;
279
+ readonly invariants?: readonly string[];
280
+ },
281
+ ): Promise<void> {
282
+ await execute(
283
+ (query) => this.lower(query, { contract: undefined }),
284
+ driver,
285
+ marker
286
+ .insert({
287
+ space,
288
+ core_hash: destination.storageHash,
289
+ profile_hash: destination.profileHash,
290
+ contract_json: null,
291
+ canonical_version: null,
292
+ updated_at: NOW,
293
+ app_tag: null,
294
+ meta: {},
295
+ invariants: destination.invariants ?? [],
296
+ })
297
+ .build(),
298
+ );
299
+ }
300
+
301
+ async initMarker(
302
+ driver: ControlDriverInstance<'sql', 'sqlite'>,
303
+ space: string,
304
+ destination: {
305
+ readonly storageHash: string;
306
+ readonly profileHash: string;
307
+ readonly invariants?: readonly string[];
308
+ },
309
+ ): Promise<void> {
310
+ await execute(
311
+ (query) => this.lower(query, { contract: undefined }),
312
+ driver,
313
+ marker
314
+ .upsert({
315
+ space,
316
+ core_hash: destination.storageHash,
317
+ profile_hash: destination.profileHash,
318
+ contract_json: null,
319
+ canonical_version: null,
320
+ updated_at: NOW,
321
+ app_tag: null,
322
+ meta: {},
323
+ invariants: destination.invariants ?? [],
324
+ })
325
+ .onConflict(marker.space)
326
+ .doUpdate((excluded) => ({
327
+ core_hash: excluded.core_hash,
328
+ profile_hash: excluded.profile_hash,
329
+ contract_json: excluded.contract_json,
330
+ canonical_version: excluded.canonical_version,
331
+ updated_at: NOW,
332
+ app_tag: excluded.app_tag,
333
+ meta: excluded.meta,
334
+ invariants: excluded.invariants,
335
+ }))
336
+ .build(),
337
+ );
338
+ }
339
+
340
+ /**
341
+ * Compare-and-swap advance of the marker row for `space`. See the
342
+ * `SqlControlAdapter.updateMarker` contract.
343
+ */
344
+ async updateMarker(
345
+ driver: ControlDriverInstance<'sql', 'sqlite'>,
346
+ space: string,
347
+ expectedFrom: string,
348
+ destination: {
349
+ readonly storageHash: string;
350
+ readonly profileHash: string;
351
+ readonly invariants?: readonly string[];
352
+ },
353
+ ): Promise<boolean> {
354
+ const currentInvariants =
355
+ destination.invariants === undefined
356
+ ? []
357
+ : ((await this.readMarker(driver, space))?.invariants ?? []);
358
+ const mergedInvariants =
359
+ destination.invariants === undefined
360
+ ? undefined
361
+ : mergeInvariants(currentInvariants, destination.invariants);
362
+
363
+ const query = marker
364
+ .update()
365
+ .set({
366
+ core_hash: destination.storageHash,
367
+ profile_hash: destination.profileHash,
368
+ updated_at: NOW,
369
+ ...(mergedInvariants !== undefined ? { invariants: mergedInvariants } : {}),
370
+ })
371
+ .where(marker.space.eq(space).and(marker.core_hash.eq(expectedFrom)))
372
+ .returning(marker.space)
373
+ .build();
374
+
375
+ const rows = await execute((q) => this.lower(q, { contract: undefined }), driver, query);
376
+ return rows.length > 0;
377
+ }
378
+
379
+ /**
380
+ * Appends a ledger entry for `space`. See the
381
+ * `SqlControlAdapter.writeLedgerEntry` contract.
382
+ */
383
+ async writeLedgerEntry(
384
+ driver: ControlDriverInstance<'sql', 'sqlite'>,
385
+ space: string,
386
+ entry: {
387
+ readonly edgeId: string;
388
+ readonly from: string;
389
+ readonly to: string;
390
+ readonly migrationName: string;
391
+ readonly migrationHash: string;
392
+ readonly operations: readonly unknown[];
393
+ },
394
+ ): Promise<void> {
395
+ await execute(
396
+ (query) => this.lower(query, { contract: undefined }),
397
+ driver,
398
+ ledger
399
+ .insert({
400
+ space,
401
+ migration_name: entry.migrationName,
402
+ migration_hash: entry.migrationHash,
403
+ origin_core_hash: entry.from,
404
+ destination_core_hash: entry.to,
405
+ operations: entry.operations,
406
+ })
407
+ .build(),
408
+ );
409
+ }
410
+
411
+ private async readMarkerResult(driver: ControlDriverInstance<'sql', 'sqlite'>, space: string) {
412
+ const lower = (query: AnyQueryAst) => this.lower(query, { contract: undefined });
413
+ const probe = sqliteCatalog
414
+ .select(sqliteCatalog.name)
415
+ .where(sqliteCatalog.type.eq('table').and(sqliteCatalog.name.eq('_prisma_marker')))
416
+ .build();
417
+ const exists = await execute(lower, driver, probe);
418
+ if (exists.length === 0) return { kind: 'no-table' as const };
419
+
420
+ const fetch = marker
421
+ .select(
422
+ marker.core_hash,
423
+ marker.profile_hash,
424
+ marker.contract_json,
425
+ marker.canonical_version,
426
+ marker.updated_at,
427
+ marker.app_tag,
428
+ marker.meta,
429
+ marker.invariants,
430
+ )
431
+ .where(marker.space.eq(space))
432
+ .build();
433
+ const result = await execute(lower, driver, fetch);
434
+ const row = result[0];
435
+ if (!row) return { kind: 'absent' as const };
436
+ return {
437
+ kind: 'present' as const,
438
+ record: parseContractMarkerRow(decodeSqliteMarkerRow(row)),
439
+ };
440
+ }
441
+
319
442
  async introspect(
320
443
  driver: ControlDriverInstance<'sql', 'sqlite'>,
321
444
  _contract?: unknown,
@@ -0,0 +1,124 @@
1
+ import type { ControlDriverInstance } from '@prisma-next/framework-components/control';
2
+ import {
3
+ type AnyQueryAst,
4
+ type LoweredStatement,
5
+ RawExpr,
6
+ } from '@prisma-next/sql-relational-core/ast';
7
+ import {
8
+ createAstCodecRegistry,
9
+ deriveParamMetadata,
10
+ encodeParamsWithMetadata,
11
+ } from '@prisma-next/sql-runtime';
12
+ import { SQLITE_DATETIME_CODEC_ID } from '@prisma-next/target-sqlite/codec-ids';
13
+ import { sqliteCodecRegistry } from '@prisma-next/target-sqlite/codecs';
14
+ import {
15
+ datetime,
16
+ integer,
17
+ jsonText,
18
+ sqliteTable,
19
+ text,
20
+ } from '@prisma-next/target-sqlite/contract-free';
21
+
22
+ const CONTROL_CODECS = createAstCodecRegistry(sqliteCodecRegistry);
23
+
24
+ export const marker = sqliteTable('_prisma_marker', {
25
+ space: text(),
26
+ core_hash: text(),
27
+ profile_hash: text(),
28
+ contract_json: jsonText({ nullable: true }),
29
+ canonical_version: integer({ nullable: true }),
30
+ updated_at: datetime(),
31
+ app_tag: text({ nullable: true }),
32
+ meta: jsonText({ nullable: true }),
33
+ invariants: jsonText(),
34
+ });
35
+
36
+ /**
37
+ * Writeable subset of `_prisma_ledger`. Omits the DB-generated `id`
38
+ * (`INTEGER PRIMARY KEY AUTOINCREMENT`) and `created_at` (default
39
+ * `strftime(...)`).
40
+ */
41
+ export const ledger = sqliteTable('_prisma_ledger', {
42
+ space: text(),
43
+ migration_name: text(),
44
+ migration_hash: text(),
45
+ origin_core_hash: text({ nullable: true }),
46
+ destination_core_hash: text(),
47
+ operations: jsonText(),
48
+ });
49
+
50
+ /**
51
+ * Read-side handle covering every column of `_prisma_ledger`, including
52
+ * the DB-generated `id` (for ORDER BY) and `created_at`.
53
+ */
54
+ export const ledgerReadShape = sqliteTable('_prisma_ledger', {
55
+ id: integer(),
56
+ space: text(),
57
+ migration_name: text(),
58
+ migration_hash: text(),
59
+ origin_core_hash: text({ nullable: true }),
60
+ destination_core_hash: text(),
61
+ operations: jsonText(),
62
+ created_at: text(),
63
+ });
64
+
65
+ export const sqliteCatalog = sqliteTable('sqlite_master', { type: text(), name: text() });
66
+
67
+ export const NOW = new RawExpr({
68
+ parts: ["datetime('now')"],
69
+ returns: { codecId: SQLITE_DATETIME_CODEC_ID, nullable: false },
70
+ });
71
+
72
+ type Lower = (query: AnyQueryAst) => LoweredStatement;
73
+
74
+ type MarkerDriver = {
75
+ query<Row = Record<string, unknown>>(
76
+ sql: string,
77
+ params?: readonly unknown[],
78
+ ): Promise<{ readonly rows: ReadonlyArray<Row> }>;
79
+ };
80
+
81
+ export function mergeInvariants(
82
+ current: readonly string[],
83
+ incoming: readonly string[],
84
+ ): readonly string[] {
85
+ return [...new Set([...current, ...incoming])].sort();
86
+ }
87
+
88
+ export async function execute(
89
+ lower: Lower,
90
+ driver: MarkerDriver,
91
+ query: AnyQueryAst,
92
+ ): Promise<readonly Record<string, unknown>[]> {
93
+ const lowered = lower(query);
94
+ const values = lowered.params.map((slot) => {
95
+ if (slot.kind === 'literal') return slot.value;
96
+ throw new Error('SQLite control DML lowered to a bind parameter, which is unsupported');
97
+ });
98
+ const encoded = await encodeParamsWithMetadata(
99
+ values,
100
+ deriveParamMetadata(query),
101
+ {},
102
+ CONTROL_CODECS,
103
+ );
104
+ const result = await driver.query(lowered.sql, encoded);
105
+ return result.rows;
106
+ }
107
+
108
+ export function decodeSqliteMarkerRow(row: unknown): unknown {
109
+ if (typeof row !== 'object' || row === null || !('invariants' in row)) {
110
+ return row;
111
+ }
112
+ const record = row as { invariants: unknown };
113
+ if (typeof record.invariants !== 'string') return row;
114
+ let parsed: unknown;
115
+ try {
116
+ parsed = JSON.parse(record.invariants);
117
+ } catch (err) {
118
+ const detail = err instanceof Error ? err.message : String(err);
119
+ throw new Error(`Invalid contract marker row: invariants is not valid JSON: ${detail}`);
120
+ }
121
+ return { ...record, invariants: parsed };
122
+ }
123
+
124
+ export type SqliteMarkerWriteDriver = ControlDriverInstance<'sql', 'sqlite'>;