@prisma-next/adapter-sqlite 0.12.0-dev.4 → 0.12.0-dev.40

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,4 +1,4 @@
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';
@@ -6,11 +6,15 @@ import {
6
6
  APP_SPACE_ID,
7
7
  type ControlDriverInstance,
8
8
  } from '@prisma-next/framework-components/control';
9
+ import { ledgerOriginFromStored } from '@prisma-next/migration-tools/ledger-origin';
9
10
  import type {
10
11
  AnyQueryAst,
12
+ DdlNode,
11
13
  LoweredStatement,
12
14
  LowererContext,
15
+ MarkerReadResult,
13
16
  } from '@prisma-next/sql-relational-core/ast';
17
+ import { isDdlNode } from '@prisma-next/sql-relational-core/ast';
14
18
  import type {
15
19
  PrimaryKey,
16
20
  SqlColumnIR,
@@ -21,36 +25,42 @@ import type {
21
25
  SqlTableIR,
22
26
  SqlUniqueIR,
23
27
  } from '@prisma-next/sql-schema-ir/types';
28
+ import {
29
+ buildControlTableBootstrapQueries,
30
+ buildSignMarkerBootstrapQueries,
31
+ } from '@prisma-next/target-sqlite/contract-free';
32
+ import type { SqliteDdlNode } from '@prisma-next/target-sqlite/ddl';
24
33
  import { parseSqliteDefault } from '@prisma-next/target-sqlite/default-normalizer';
25
34
  import { normalizeSqliteNativeType } from '@prisma-next/target-sqlite/native-type-normalizer';
35
+ import { blindCast } from '@prisma-next/utils/casts';
26
36
  import { ifDefined } from '@prisma-next/utils/defined';
27
37
  import { renderLoweredSql } from './adapter';
38
+ import { renderLoweredDdl } from './ddl-renderer';
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';
28
50
  import type { SqliteContract } from './types';
29
51
 
30
52
  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
- }
53
+ const SQLITE_LEDGER_TABLE = '_prisma_ledger';
54
+
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
+ };
54
64
 
55
65
  // PRAGMA result row types
56
66
  type PragmaTableInfoRow = {
@@ -101,6 +111,14 @@ export class SqliteControlAdapter implements SqlControlAdapter<'sqlite'> {
101
111
  readonly normalizeDefault = parseSqliteDefault;
102
112
  readonly normalizeNativeType = normalizeSqliteNativeType;
103
113
 
114
+ bootstrapControlTableQueries(): readonly DdlNode[] {
115
+ return buildControlTableBootstrapQueries();
116
+ }
117
+
118
+ bootstrapSignMarkerQueries(): readonly DdlNode[] {
119
+ return buildSignMarkerBootstrapQueries();
120
+ }
121
+
104
122
  /**
105
123
  * Lower a SQL query AST into a SQLite-flavored `{ sql, params }` payload.
106
124
  *
@@ -109,7 +127,10 @@ export class SqliteControlAdapter implements SqlControlAdapter<'sqlite'> {
109
127
  * and contract. Used at migration plan/emit time (e.g. by `dataTransform`)
110
128
  * without instantiating the runtime adapter.
111
129
  */
112
- lower(ast: AnyQueryAst, context: LowererContext<unknown>): LoweredStatement {
130
+ lower(ast: AnyQueryAst | SqliteDdlNode, context: LowererContext<unknown>): LoweredStatement {
131
+ if (isDdlNode(ast)) {
132
+ return renderLoweredDdl(ast);
133
+ }
113
134
  return renderLoweredSql(ast, context.contract as SqliteContract);
114
135
  }
115
136
 
@@ -122,53 +143,16 @@ export class SqliteControlAdapter implements SqlControlAdapter<'sqlite'> {
122
143
  driver: ControlDriverInstance<'sql', 'sqlite'>,
123
144
  space: string,
124
145
  ): 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
- );
146
+ const result = await this.readMarkerDiscriminated(driver, space);
147
+ return result.kind === 'present' ? result.record : null;
148
+ }
164
149
 
165
- const row = result.rows[0];
166
- if (!row) return null;
167
- return parseMarkerRowSafely(
168
- row,
169
- (raw) => parseContractMarkerRow(decodeSqliteMarkerRow(raw)),
170
- markerContext,
171
- );
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);
172
156
  }
173
157
 
174
158
  /**
@@ -180,48 +164,44 @@ export class SqliteControlAdapter implements SqlControlAdapter<'sqlite'> {
180
164
  driver: ControlDriverInstance<'sql', 'sqlite'>,
181
165
  ): Promise<ReadonlyMap<string, ContractMarkerRecord>> {
182
166
  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) {
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) {
191
180
  return new Map();
192
181
  }
193
182
 
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(
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(
225
205
  row.space,
226
206
  parseMarkerRowSafely(row, (raw) => parseContractMarkerRow(decodeSqliteMarkerRow(raw)), {
227
207
  space: row.space,
@@ -229,7 +209,234 @@ export class SqliteControlAdapter implements SqlControlAdapter<'sqlite'> {
229
209
  }),
230
210
  );
231
211
  }
232
- return rows;
212
+ return out;
213
+ }
214
+
215
+ /**
216
+ * Reads per-migration ledger rows from `_prisma_ledger` in apply order.
217
+ * Probes `sqlite_master` first so a fresh database without the ledger
218
+ * table returns `[]` instead of raising "no such table".
219
+ */
220
+ async readLedger(
221
+ driver: ControlDriverInstance<'sql', 'sqlite'>,
222
+ space?: string,
223
+ ): Promise<readonly LedgerEntryRecord[]> {
224
+ const ledgerContext = { space: space ?? '*', markerLocation: SQLITE_LEDGER_TABLE };
225
+ return withMarkerReadErrorHandling(() => this.readLedgerResult(driver, space), ledgerContext);
226
+ }
227
+
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 [];
240
+ }
241
+
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,
255
+ );
256
+
257
+ return rows.map((row) => ({
258
+ space: row.space,
259
+ migrationName: row.migration_name,
260
+ migrationHash: row.migration_hash,
261
+ from: ledgerOriginFromStored(row.origin_core_hash),
262
+ to: row.destination_core_hash,
263
+ appliedAt: coerceLedgerAppliedAt(row.created_at),
264
+ operationCount: operationCountFromStored(row.operations),
265
+ }));
266
+ }
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
+ };
233
440
  }
234
441
 
235
442
  async introspect(
@@ -0,0 +1,67 @@
1
+ import type {
2
+ DdlColumn,
3
+ DdlColumnDefaultVisitor,
4
+ FunctionColumnDefault,
5
+ LiteralColumnDefault,
6
+ } from '@prisma-next/sql-relational-core/ast';
7
+ import type {
8
+ SqliteCreateTable,
9
+ SqliteDdlNode,
10
+ SqliteDdlVisitor,
11
+ } from '@prisma-next/target-sqlite/ddl';
12
+ import { escapeLiteral } from '@prisma-next/target-sqlite/sql-utils';
13
+ import type { SqliteLoweredStatement } from './types';
14
+
15
+ class SqliteDdlVisitorImpl implements SqliteDdlVisitor<string> {
16
+ createTable(node: SqliteCreateTable): string {
17
+ const ifNotExists = node.ifNotExists ? 'IF NOT EXISTS ' : '';
18
+ const tableRef = node.table;
19
+ const columnDefs = node.columns.map((column) => renderColumn(column)).join(',\n ');
20
+ return `CREATE TABLE ${ifNotExists}${tableRef} (\n ${columnDefs}\n )`;
21
+ }
22
+ }
23
+
24
+ const defaultVisitor: DdlColumnDefaultVisitor<string> = {
25
+ literal(node: LiteralColumnDefault): string {
26
+ const { value } = node;
27
+ if (typeof value === 'string') {
28
+ return `DEFAULT '${escapeLiteral(value)}'`;
29
+ }
30
+ if (typeof value === 'number' || typeof value === 'boolean') {
31
+ return `DEFAULT ${String(value)}`;
32
+ }
33
+ if (value === null) {
34
+ return 'DEFAULT NULL';
35
+ }
36
+ return `DEFAULT '${JSON.stringify(value)}'`;
37
+ },
38
+ function(node: FunctionColumnDefault): string {
39
+ if (node.expression === 'autoincrement()') {
40
+ return '';
41
+ }
42
+ return `DEFAULT (${node.expression})`;
43
+ },
44
+ };
45
+
46
+ function renderColumn(column: DdlColumn): string {
47
+ if (column.type.includes('AUTOINCREMENT')) {
48
+ return `${column.name} ${column.type}`;
49
+ }
50
+ const parts = [column.name, column.type];
51
+ if (column.notNull) {
52
+ parts.push('NOT NULL');
53
+ }
54
+ if (column.primaryKey) {
55
+ parts.push('PRIMARY KEY');
56
+ }
57
+ const defaultClause = column.default ? column.default.accept(defaultVisitor) : '';
58
+ if (defaultClause.length > 0) {
59
+ parts.push(defaultClause);
60
+ }
61
+ return parts.join(' ');
62
+ }
63
+
64
+ export function renderLoweredDdl(ast: SqliteDdlNode): SqliteLoweredStatement {
65
+ const sql = ast.accept(new SqliteDdlVisitorImpl());
66
+ return Object.freeze({ sql, params: Object.freeze([]) });
67
+ }
@@ -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
+ }