@prisma-next/adapter-sqlite 0.5.0-dev.7 → 0.5.0-dev.70

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.
Files changed (47) hide show
  1. package/README.md +3 -3
  2. package/dist/{adapter-lwcslPF8.mjs → adapter-C4IjVF9z.mjs} +56 -63
  3. package/dist/adapter-C4IjVF9z.mjs.map +1 -0
  4. package/dist/{adapter-BG7_o_3h.d.mts → adapter-DswQk5bT.d.mts} +8 -5
  5. package/dist/adapter-DswQk5bT.d.mts.map +1 -0
  6. package/dist/adapter.d.mts +1 -2
  7. package/dist/adapter.mjs +2 -4
  8. package/dist/codec-types.d.mts +2 -40
  9. package/dist/codec-types.mjs +1 -3
  10. package/dist/column-types.d.mts +1 -5
  11. package/dist/column-types.d.mts.map +1 -1
  12. package/dist/column-types.mjs +3 -8
  13. package/dist/column-types.mjs.map +1 -1
  14. package/dist/control.d.mts +41 -3
  15. package/dist/control.d.mts.map +1 -1
  16. package/dist/control.mjs +441 -10
  17. package/dist/control.mjs.map +1 -1
  18. package/dist/{descriptor-meta-Bg-c1LmL.mjs → descriptor-meta-DA4lgWT_.mjs} +2 -2
  19. package/dist/{descriptor-meta-Bg-c1LmL.mjs.map → descriptor-meta-DA4lgWT_.mjs.map} +1 -1
  20. package/dist/runtime.d.mts +2 -3
  21. package/dist/runtime.d.mts.map +1 -1
  22. package/dist/runtime.mjs +21 -15
  23. package/dist/runtime.mjs.map +1 -1
  24. package/dist/{types-gAqc4ucF.d.mts → types-bTlW__XL.d.mts} +1 -1
  25. package/dist/types-bTlW__XL.d.mts.map +1 -0
  26. package/dist/types.d.mts +1 -1
  27. package/dist/types.mjs +1 -1
  28. package/package.json +25 -20
  29. package/src/core/adapter.ts +89 -72
  30. package/src/core/column-types.ts +1 -7
  31. package/src/core/control-adapter.ts +331 -15
  32. package/src/core/control-mutation-defaults.ts +358 -0
  33. package/src/core/runtime-adapter.ts +19 -12
  34. package/src/exports/codec-types.ts +2 -6
  35. package/src/exports/column-types.ts +0 -1
  36. package/src/exports/control.ts +45 -1
  37. package/dist/adapter-BG7_o_3h.d.mts.map +0 -1
  38. package/dist/adapter-lwcslPF8.mjs.map +0 -1
  39. package/dist/codec-ids-o_Z8i4nt.mjs +0 -15
  40. package/dist/codec-ids-o_Z8i4nt.mjs.map +0 -1
  41. package/dist/codec-types.d.mts.map +0 -1
  42. package/dist/codecs-Bhz0W5Xm.mjs +0 -87
  43. package/dist/codecs-Bhz0W5Xm.mjs.map +0 -1
  44. package/dist/types-gAqc4ucF.d.mts.map +0 -1
  45. package/src/core/codec-ids.ts +0 -14
  46. package/src/core/codecs.ts +0 -108
  47. package/src/core/sql-utils.ts +0 -35
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@prisma-next/adapter-sqlite",
3
- "version": "0.5.0-dev.7",
3
+ "version": "0.5.0-dev.70",
4
+ "license": "Apache-2.0",
4
5
  "type": "module",
5
6
  "sideEffects": false,
6
7
  "files": [
@@ -8,29 +9,33 @@
8
9
  "src"
9
10
  ],
10
11
  "dependencies": {
11
- "arktype": "^2.0.0",
12
- "@prisma-next/cli": "0.5.0-dev.7",
13
- "@prisma-next/contract": "0.5.0-dev.7",
14
- "@prisma-next/framework-components": "0.5.0-dev.7",
15
- "@prisma-next/family-sql": "0.5.0-dev.7",
16
- "@prisma-next/ids": "0.5.0-dev.7",
17
- "@prisma-next/sql-contract-psl": "0.5.0-dev.7",
18
- "@prisma-next/sql-contract-ts": "0.5.0-dev.7",
19
- "@prisma-next/sql-contract": "0.5.0-dev.7",
20
- "@prisma-next/sql-operations": "0.5.0-dev.7",
21
- "@prisma-next/sql-runtime": "0.5.0-dev.7",
22
- "@prisma-next/sql-relational-core": "0.5.0-dev.7",
23
- "@prisma-next/contract-authoring": "0.5.0-dev.7",
24
- "@prisma-next/utils": "0.5.0-dev.7",
25
- "@prisma-next/sql-schema-ir": "0.5.0-dev.7"
12
+ "arktype": "^2.1.29",
13
+ "@prisma-next/cli": "0.5.0-dev.70",
14
+ "@prisma-next/contract": "0.5.0-dev.70",
15
+ "@prisma-next/contract-authoring": "0.5.0-dev.70",
16
+ "@prisma-next/family-sql": "0.5.0-dev.70",
17
+ "@prisma-next/framework-components": "0.5.0-dev.70",
18
+ "@prisma-next/ids": "0.5.0-dev.70",
19
+ "@prisma-next/sql-contract-psl": "0.5.0-dev.70",
20
+ "@prisma-next/sql-contract-ts": "0.5.0-dev.70",
21
+ "@prisma-next/sql-operations": "0.5.0-dev.70",
22
+ "@prisma-next/sql-relational-core": "0.5.0-dev.70",
23
+ "@prisma-next/sql-contract": "0.5.0-dev.70",
24
+ "@prisma-next/sql-runtime": "0.5.0-dev.70",
25
+ "@prisma-next/target-sqlite": "0.5.0-dev.70",
26
+ "@prisma-next/utils": "0.5.0-dev.70",
27
+ "@prisma-next/sql-schema-ir": "0.5.0-dev.70"
26
28
  },
27
29
  "devDependencies": {
28
- "tsdown": "0.18.4",
30
+ "pathe": "^2.0.3",
31
+ "tsdown": "0.22.0",
29
32
  "typescript": "5.9.3",
30
- "vitest": "4.0.17",
31
- "@prisma-next/test-utils": "0.0.1",
33
+ "vitest": "4.1.5",
34
+ "@prisma-next/migration-tools": "0.5.0-dev.70",
35
+ "@prisma-next/driver-sqlite": "0.5.0-dev.70",
32
36
  "@prisma-next/tsconfig": "0.0.0",
33
- "@prisma-next/tsdown": "0.0.0"
37
+ "@prisma-next/tsdown": "0.0.0",
38
+ "@prisma-next/test-utils": "0.0.1"
34
39
  },
35
40
  "exports": {
36
41
  "./adapter": "./dist/adapter.mjs",
@@ -1,34 +1,33 @@
1
- import {
2
- type Adapter,
3
- type AdapterProfile,
4
- type AggregateExpr,
5
- type AnyExpression,
6
- type AnyFromSource,
7
- type AnyQueryAst,
8
- type BinaryExpr,
9
- type CodecParamsDescriptor,
10
- type ColumnRef,
11
- createCodecRegistry,
12
- type DeleteAst,
13
- type InsertAst,
14
- type InsertValue,
15
- type JoinAst,
16
- type JoinOnExpr,
17
- type JsonArrayAggExpr,
18
- type JsonObjectExpr,
19
- type ListExpression,
20
- type LiteralExpr,
21
- type LowererContext,
22
- type NullCheckExpr,
23
- type OperationExpr,
24
- type OrderByItem,
25
- type ProjectionItem,
26
- type SelectAst,
27
- type SubqueryExpr,
28
- type UpdateAst,
1
+ import { APP_SPACE_ID } from '@prisma-next/framework-components/control';
2
+ import type {
3
+ Adapter,
4
+ AdapterProfile,
5
+ AggregateExpr,
6
+ AnyExpression,
7
+ AnyFromSource,
8
+ AnyQueryAst,
9
+ BinaryExpr,
10
+ ColumnRef,
11
+ DeleteAst,
12
+ InsertAst,
13
+ InsertValue,
14
+ JoinAst,
15
+ JoinOnExpr,
16
+ JsonArrayAggExpr,
17
+ JsonObjectExpr,
18
+ ListExpression,
19
+ LiteralExpr,
20
+ LowererContext,
21
+ NullCheckExpr,
22
+ OperationExpr,
23
+ OrderByItem,
24
+ ProjectionItem,
25
+ SelectAst,
26
+ SubqueryExpr,
27
+ UpdateAst,
29
28
  } from '@prisma-next/sql-relational-core/ast';
30
- import { codecDefinitions } from './codecs';
31
- import { escapeLiteral, quoteIdentifier } from './sql-utils';
29
+ import { parseContractMarkerRow } from '@prisma-next/sql-runtime';
30
+ import { escapeLiteral, quoteIdentifier } from '@prisma-next/target-sqlite/sql-utils';
32
31
  import type { SqliteAdapterOptions, SqliteContract, SqliteLoweredStatement } from './types';
33
32
 
34
33
  const defaultCapabilities = Object.freeze({
@@ -47,60 +46,69 @@ class SqliteAdapterImpl implements Adapter<AnyQueryAst, SqliteContract, SqliteLo
47
46
  readonly targetId = 'sqlite' as const;
48
47
 
49
48
  readonly profile: AdapterProfile<'sqlite'>;
50
- private readonly codecRegistry = (() => {
51
- const registry = createCodecRegistry();
52
- for (const definition of Object.values(codecDefinitions)) {
53
- registry.register(definition.codec);
54
- }
55
- return registry;
56
- })();
57
49
 
58
50
  constructor(options?: SqliteAdapterOptions) {
59
51
  this.profile = Object.freeze({
60
52
  id: options?.profileId ?? 'sqlite/default@1',
61
53
  target: 'sqlite',
62
54
  capabilities: defaultCapabilities,
63
- codecs: () => this.codecRegistry,
64
55
  readMarkerStatement: () => ({
65
- sql: 'select core_hash, profile_hash, contract_json, canonical_version, updated_at, app_tag, meta from prisma_contract_marker where id = ?',
66
- params: [1],
56
+ sql: 'select core_hash, profile_hash, contract_json, canonical_version, updated_at, app_tag, meta, invariants from _prisma_marker where space = ?',
57
+ params: [APP_SPACE_ID],
67
58
  }),
59
+ // 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[]`.
60
+ parseMarkerRow: (row: unknown) => {
61
+ const raw = row as Record<string, unknown>;
62
+ const invariants =
63
+ typeof raw['invariants'] === 'string'
64
+ ? (JSON.parse(raw['invariants']) as unknown)
65
+ : raw['invariants'];
66
+ return parseContractMarkerRow({ ...raw, invariants });
67
+ },
68
68
  });
69
69
  }
70
70
 
71
- parameterizedCodecs(): ReadonlyArray<CodecParamsDescriptor> {
72
- return [];
71
+ lower(ast: AnyQueryAst, context: LowererContext<SqliteContract>): SqliteLoweredStatement {
72
+ return renderLoweredSql(ast, context.contract);
73
73
  }
74
+ }
74
75
 
75
- lower(ast: AnyQueryAst, context: LowererContext<SqliteContract>): SqliteLoweredStatement {
76
- const collectedParamRefs = ast.collectParamRefs();
77
- const params: unknown[] = [];
78
- for (const ref of collectedParamRefs) {
79
- params.push(ref.value);
80
- }
76
+ /**
77
+ * Lower a SQL query AST into a SQLite-flavored `{ sql, params }` payload.
78
+ *
79
+ * Shared between the runtime adapter (`SqliteAdapterImpl.lower`) and the control adapter (`SqliteControlAdapter.lower`) so both produce byte-identical SQL for the same AST and contract.
80
+ */
81
+ export function renderLoweredSql(
82
+ ast: AnyQueryAst,
83
+ contract: SqliteContract,
84
+ ): SqliteLoweredStatement {
85
+ const collectedParamRefs = ast.collectParamRefs();
86
+ const params: unknown[] = [];
87
+ for (const ref of collectedParamRefs) {
88
+ params.push(ref.value);
89
+ }
81
90
 
82
- let sql: string;
91
+ let sql: string;
83
92
 
84
- const node = ast;
85
- switch (node.kind) {
86
- case 'select':
87
- sql = renderSelect(node, context.contract);
88
- break;
89
- case 'insert':
90
- sql = renderInsert(node);
91
- break;
92
- case 'update':
93
- sql = renderUpdate(node, context.contract);
94
- break;
95
- case 'delete':
96
- sql = renderDelete(node);
97
- break;
98
- default:
99
- throw new Error(`Unsupported AST node kind: ${(node as { kind: string }).kind}`);
100
- }
101
-
102
- return Object.freeze({ sql, params });
93
+ const node = ast;
94
+ switch (node.kind) {
95
+ case 'select':
96
+ sql = renderSelect(node, contract);
97
+ break;
98
+ case 'insert':
99
+ sql = renderInsert(node);
100
+ break;
101
+ case 'update':
102
+ sql = renderUpdate(node, contract);
103
+ break;
104
+ case 'delete':
105
+ sql = renderDelete(node);
106
+ break;
107
+ default:
108
+ throw new Error(`Unsupported AST node kind: ${(node as { kind: string }).kind}`);
103
109
  }
110
+
111
+ return Object.freeze({ sql, params });
104
112
  }
105
113
 
106
114
  function renderSelect(ast: SelectAst, contract?: SqliteContract): string {
@@ -222,8 +230,7 @@ function renderExpr(expr: AnyExpression, contract?: SqliteContract): string {
222
230
  }
223
231
  }
224
232
 
225
- // `excluded` is a pseudo-table in ON CONFLICT DO UPDATE that references the
226
- // row proposed for insertion. It is not quoted because it's a keyword.
233
+ // `excluded` is a pseudo-table in ON CONFLICT DO UPDATE that references the row proposed for insertion. It is not quoted because it's a keyword.
227
234
  function renderColumn(ref: ColumnRef): string {
228
235
  if (ref.table === 'excluded') {
229
236
  return `excluded.${quoteIdentifier(ref.column)}`;
@@ -506,11 +513,21 @@ function renderDelete(ast: DeleteAst): string {
506
513
  return `DELETE FROM ${table}${whereClause}${returningClause}`;
507
514
  }
508
515
 
509
- function renderReturning(returning: ReadonlyArray<ColumnRef> | undefined): string {
516
+ function renderReturning(returning: ReadonlyArray<ProjectionItem> | undefined): string {
510
517
  if (!returning?.length) {
511
518
  return '';
512
519
  }
513
- return ` RETURNING ${returning.map((col) => `${quoteIdentifier(col.table)}.${quoteIdentifier(col.column)}`).join(', ')}`;
520
+ return ` RETURNING ${returning
521
+ .map((item) => {
522
+ if (item.expr.kind === 'column-ref') {
523
+ const rendered = `${quoteIdentifier(item.expr.table)}.${quoteIdentifier(item.expr.column)}`;
524
+ return item.expr.column === item.alias
525
+ ? rendered
526
+ : `${rendered} AS ${quoteIdentifier(item.alias)}`;
527
+ }
528
+ return `${renderExpr(item.expr)} AS ${quoteIdentifier(item.alias)}`;
529
+ })
530
+ .join(', ')}`;
514
531
  }
515
532
 
516
533
  export function createSqliteAdapter(options?: SqliteAdapterOptions) {
@@ -1,13 +1,12 @@
1
1
  import {
2
2
  SQLITE_BIGINT_CODEC_ID,
3
3
  SQLITE_BLOB_CODEC_ID,
4
- SQLITE_BOOLEAN_CODEC_ID,
5
4
  SQLITE_DATETIME_CODEC_ID,
6
5
  SQLITE_INTEGER_CODEC_ID,
7
6
  SQLITE_JSON_CODEC_ID,
8
7
  SQLITE_REAL_CODEC_ID,
9
8
  SQLITE_TEXT_CODEC_ID,
10
- } from './codec-ids';
9
+ } from '@prisma-next/target-sqlite/codec-ids';
11
10
 
12
11
  export const textColumn = {
13
12
  codecId: SQLITE_TEXT_CODEC_ID,
@@ -29,11 +28,6 @@ export const blobColumn = {
29
28
  nativeType: 'blob',
30
29
  } as const;
31
30
 
32
- export const booleanColumn = {
33
- codecId: SQLITE_BOOLEAN_CODEC_ID,
34
- nativeType: 'integer',
35
- } as const;
36
-
37
31
  export const datetimeColumn = {
38
32
  codecId: SQLITE_DATETIME_CODEC_ID,
39
33
  nativeType: 'text',
@@ -1,18 +1,334 @@
1
+ import type { ContractMarkerRecord } from '@prisma-next/contract/types';
2
+ import type { SqlControlAdapter } from '@prisma-next/family-sql/control-adapter';
3
+ import { parseContractMarkerRow } from '@prisma-next/family-sql/verify';
4
+ import type { ControlDriverInstance } from '@prisma-next/framework-components/control';
1
5
  import type {
2
- ControlAdapterDescriptor,
3
- ControlAdapterInstance,
4
- } from '@prisma-next/framework-components/control';
5
- import { sqliteAdapterDescriptorMeta } from './descriptor-meta';
6
-
7
- const sqliteControlAdapterDescriptor: ControlAdapterDescriptor<
8
- 'sql',
9
- 'sqlite',
10
- ControlAdapterInstance<'sql', 'sqlite'>
11
- > = {
12
- ...sqliteAdapterDescriptorMeta,
13
- create(_stack): ControlAdapterInstance<'sql', 'sqlite'> {
14
- return { familyId: 'sql', targetId: 'sqlite' };
15
- },
6
+ AnyQueryAst,
7
+ LoweredStatement,
8
+ LowererContext,
9
+ } from '@prisma-next/sql-relational-core/ast';
10
+ import type {
11
+ PrimaryKey,
12
+ SqlColumnIR,
13
+ SqlForeignKeyIR,
14
+ SqlIndexIR,
15
+ SqlReferentialAction,
16
+ SqlSchemaIR,
17
+ SqlTableIR,
18
+ SqlUniqueIR,
19
+ } from '@prisma-next/sql-schema-ir/types';
20
+ import { parseSqliteDefault } from '@prisma-next/target-sqlite/default-normalizer';
21
+ import { normalizeSqliteNativeType } from '@prisma-next/target-sqlite/native-type-normalizer';
22
+ import { ifDefined } from '@prisma-next/utils/defined';
23
+ import { renderLoweredSql } from './adapter';
24
+ import type { SqliteContract } from './types';
25
+
26
+ // PRAGMA result row types
27
+ type PragmaTableInfoRow = {
28
+ cid: number;
29
+ name: string;
30
+ type: string;
31
+ notnull: number;
32
+ dflt_value: string | null;
33
+ pk: number;
34
+ };
35
+
36
+ type PragmaForeignKeyRow = {
37
+ id: number;
38
+ seq: number;
39
+ table: string;
40
+ from: string;
41
+ to: string;
42
+ on_update: string;
43
+ on_delete: string;
44
+ };
45
+
46
+ type PragmaIndexListRow = {
47
+ seq: number;
48
+ name: string;
49
+ unique: number;
50
+ origin: string;
51
+ partial: number;
52
+ };
53
+
54
+ type PragmaIndexInfoRow = {
55
+ seqno: number;
56
+ cid: number;
57
+ name: string;
58
+ };
59
+
60
+ type FkAccumulator = {
61
+ columns: string[];
62
+ referencedTable: string;
63
+ referencedColumns: string[];
64
+ onDelete: string;
65
+ onUpdate: string;
66
+ };
67
+
68
+ export class SqliteControlAdapter implements SqlControlAdapter<'sqlite'> {
69
+ readonly familyId = 'sql' as const;
70
+ readonly targetId = 'sqlite' as const;
71
+
72
+ readonly normalizeDefault = parseSqliteDefault;
73
+ readonly normalizeNativeType = normalizeSqliteNativeType;
74
+
75
+ /**
76
+ * Lower a SQL query AST into a SQLite-flavored `{ sql, params }` payload.
77
+ *
78
+ * Delegates to the shared `renderLoweredSql` renderer so the control adapter
79
+ * emits byte-identical SQL to `SqliteAdapterImpl.lower()` for the same AST
80
+ * and contract. Used at migration plan/emit time (e.g. by `dataTransform`)
81
+ * without instantiating the runtime adapter.
82
+ */
83
+ lower(ast: AnyQueryAst, context: LowererContext<unknown>): LoweredStatement {
84
+ return renderLoweredSql(ast, context.contract as SqliteContract);
85
+ }
86
+
87
+ /**
88
+ * Reads the contract marker from `_prisma_marker`. Probes `sqlite_master`
89
+ * first so a fresh database (no marker table) returns `null` instead of a
90
+ * "no such table" error.
91
+ */
92
+ async readMarker(
93
+ driver: ControlDriverInstance<'sql', 'sqlite'>,
94
+ space: string,
95
+ ): Promise<ContractMarkerRecord | null> {
96
+ const exists = await driver.query(
97
+ `SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?`,
98
+ ['_prisma_marker'],
99
+ );
100
+ if (exists.rows.length === 0) {
101
+ return null;
102
+ }
103
+
104
+ const result = await driver.query<{
105
+ core_hash: string;
106
+ profile_hash: string;
107
+ contract_json: unknown | null;
108
+ canonical_version: number | null;
109
+ updated_at: Date | string;
110
+ app_tag: string | null;
111
+ meta: unknown | null;
112
+ invariants: unknown;
113
+ }>(
114
+ `SELECT
115
+ core_hash,
116
+ profile_hash,
117
+ contract_json,
118
+ canonical_version,
119
+ updated_at,
120
+ app_tag,
121
+ meta,
122
+ invariants
123
+ FROM _prisma_marker
124
+ WHERE space = ?`,
125
+ [space],
126
+ );
127
+
128
+ const row = result.rows[0];
129
+ if (!row) return null;
130
+ // SQLite stores arrays as JSON-encoded TEXT (no native array type), so
131
+ // the driver returns `invariants` as a string. Decode before delegating
132
+ // to the shared row schema, which expects `string[]`.
133
+ const invariants =
134
+ typeof row.invariants === 'string' ? (JSON.parse(row.invariants) as unknown) : row.invariants;
135
+ return parseContractMarkerRow({ ...row, invariants });
136
+ }
137
+
138
+ /**
139
+ * Reads every row from `_prisma_marker` and returns them keyed by
140
+ * `space`. Mirrors the existence probe in {@link readMarker}: a
141
+ * fresh database without the marker table returns an empty map.
142
+ */
143
+ async readAllMarkers(
144
+ driver: ControlDriverInstance<'sql', 'sqlite'>,
145
+ ): Promise<ReadonlyMap<string, ContractMarkerRecord>> {
146
+ const exists = await driver.query(
147
+ `SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?`,
148
+ ['_prisma_marker'],
149
+ );
150
+ if (exists.rows.length === 0) {
151
+ return new Map();
152
+ }
153
+
154
+ const result = await driver.query<{
155
+ space: string;
156
+ core_hash: string;
157
+ profile_hash: string;
158
+ contract_json: unknown | null;
159
+ canonical_version: number | null;
160
+ updated_at: Date | string;
161
+ app_tag: string | null;
162
+ meta: unknown | null;
163
+ invariants: unknown;
164
+ }>(
165
+ `SELECT
166
+ space,
167
+ core_hash,
168
+ profile_hash,
169
+ contract_json,
170
+ canonical_version,
171
+ updated_at,
172
+ app_tag,
173
+ meta,
174
+ invariants
175
+ FROM _prisma_marker`,
176
+ );
177
+
178
+ const rows = new Map<string, ContractMarkerRecord>();
179
+ for (const row of result.rows) {
180
+ const invariants =
181
+ typeof row.invariants === 'string'
182
+ ? (JSON.parse(row.invariants) as unknown)
183
+ : row.invariants;
184
+ rows.set(row.space, parseContractMarkerRow({ ...row, invariants }));
185
+ }
186
+ return rows;
187
+ }
188
+
189
+ async introspect(
190
+ driver: ControlDriverInstance<'sql', 'sqlite'>,
191
+ _contract?: unknown,
192
+ ): Promise<SqlSchemaIR> {
193
+ // Filter out runner-managed control tables (`_prisma_marker`,
194
+ // `_prisma_ledger`) — they're an implementation detail of the migration
195
+ // runner, not part of the user-authored contract, so they must not
196
+ // appear in introspection output (otherwise strict schema verification
197
+ // flags them as `extra_table`).
198
+ const tablesResult = await driver.query<{ name: string }>(
199
+ `SELECT name FROM sqlite_master
200
+ WHERE type = 'table'
201
+ AND name NOT LIKE 'sqlite_%'
202
+ AND name NOT IN ('_prisma_marker', '_prisma_ledger')
203
+ ORDER BY name`,
204
+ );
205
+
206
+ const tables: Record<string, SqlTableIR> = {};
207
+
208
+ for (const tableRow of tablesResult.rows) {
209
+ const tableName = tableRow.name;
210
+
211
+ // SQLite's synchronous driver serializes reads — no benefit from Promise.all
212
+ const columnsResult = await driver.query<PragmaTableInfoRow>(
213
+ `PRAGMA table_info("${escapePragmaArg(tableName)}")`,
214
+ );
215
+ const fkResult = await driver.query<PragmaForeignKeyRow>(
216
+ `PRAGMA foreign_key_list("${escapePragmaArg(tableName)}")`,
217
+ );
218
+ const indexListResult = await driver.query<PragmaIndexListRow>(
219
+ `PRAGMA index_list("${escapePragmaArg(tableName)}")`,
220
+ );
221
+
222
+ const columns: Record<string, SqlColumnIR> = {};
223
+ const pkColumns: Array<{ name: string; pk: number }> = [];
224
+
225
+ for (const col of columnsResult.rows) {
226
+ columns[col.name] = {
227
+ name: col.name,
228
+ nativeType: col.type.toLowerCase(),
229
+ nullable: col.notnull === 0 && col.pk === 0,
230
+ ...ifDefined('default', col.dflt_value ?? undefined),
231
+ };
232
+ if (col.pk > 0) {
233
+ pkColumns.push({ name: col.name, pk: col.pk });
234
+ }
235
+ }
236
+
237
+ pkColumns.sort((a, b) => a.pk - b.pk);
238
+ const primaryKey: PrimaryKey | undefined =
239
+ pkColumns.length > 0 ? { columns: pkColumns.map((c) => c.name) } : undefined;
240
+
241
+ const fkMap = new Map<number, FkAccumulator>();
242
+ for (const fk of fkResult.rows) {
243
+ const existing = fkMap.get(fk.id);
244
+ if (existing) {
245
+ existing.columns.push(fk.from);
246
+ existing.referencedColumns.push(fk.to);
247
+ } else {
248
+ fkMap.set(fk.id, {
249
+ columns: [fk.from],
250
+ referencedTable: fk.table,
251
+ referencedColumns: [fk.to],
252
+ onDelete: fk.on_delete,
253
+ onUpdate: fk.on_update,
254
+ });
255
+ }
256
+ }
257
+ const foreignKeys: readonly SqlForeignKeyIR[] = Array.from(fkMap.values()).map((fk) => ({
258
+ columns: Object.freeze([...fk.columns]) as readonly string[],
259
+ referencedTable: fk.referencedTable,
260
+ referencedColumns: Object.freeze([...fk.referencedColumns]) as readonly string[],
261
+ ...ifDefined('onDelete', mapSqliteReferentialAction(fk.onDelete)),
262
+ ...ifDefined('onUpdate', mapSqliteReferentialAction(fk.onUpdate)),
263
+ }));
264
+
265
+ const uniques: SqlUniqueIR[] = [];
266
+ const indexes: SqlIndexIR[] = [];
267
+
268
+ for (const idx of indexListResult.rows) {
269
+ // origin: 'c' = CREATE INDEX, 'u' = UNIQUE constraint, 'pk' = PRIMARY KEY
270
+ const idxInfoResult = await driver.query<PragmaIndexInfoRow>(
271
+ `PRAGMA index_info("${escapePragmaArg(idx.name)}")`,
272
+ );
273
+
274
+ const idxColumns = idxInfoResult.rows.sort((a, b) => a.seqno - b.seqno).map((r) => r.name);
275
+
276
+ if (idx.origin === 'u') {
277
+ uniques.push({
278
+ columns: Object.freeze([...idxColumns]) as readonly string[],
279
+ name: idx.name,
280
+ });
281
+ } else if (idx.origin === 'c') {
282
+ indexes.push({
283
+ columns: Object.freeze([...idxColumns]) as readonly string[],
284
+ name: idx.name,
285
+ unique: idx.unique === 1,
286
+ });
287
+ }
288
+ // Skip 'pk' origin — already captured in primaryKey
289
+ }
290
+
291
+ tables[tableName] = {
292
+ name: tableName,
293
+ columns,
294
+ ...ifDefined('primaryKey', primaryKey),
295
+ foreignKeys,
296
+ uniques,
297
+ indexes,
298
+ };
299
+ }
300
+
301
+ return {
302
+ tables,
303
+ dependencies: [],
304
+ };
305
+ }
306
+ }
307
+
308
+ // PRAGMA queries use the function-argument form (`PRAGMA table_info("name")`)
309
+ // which doesn't support `?` placeholders — the argument is part of the
310
+ // statement name, not a bound parameter. We quote-escape the table name instead.
311
+ function escapePragmaArg(name: string): string {
312
+ return name.replace(/"/g, '""');
313
+ }
314
+
315
+ const SQLITE_REFERENTIAL_ACTION_MAP: Record<string, SqlReferentialAction> = {
316
+ 'NO ACTION': 'noAction',
317
+ RESTRICT: 'restrict',
318
+ CASCADE: 'cascade',
319
+ 'SET NULL': 'setNull',
320
+ 'SET DEFAULT': 'setDefault',
16
321
  };
17
322
 
18
- export default sqliteControlAdapterDescriptor;
323
+ function mapSqliteReferentialAction(rule: string): SqlReferentialAction | undefined {
324
+ const normalized = rule.toUpperCase();
325
+ const mapped = SQLITE_REFERENTIAL_ACTION_MAP[normalized];
326
+ if (mapped === undefined) {
327
+ throw new Error(
328
+ `Unknown SQLite referential action rule: "${rule}". ` +
329
+ 'Expected one of: NO ACTION, RESTRICT, CASCADE, SET NULL, SET DEFAULT.',
330
+ );
331
+ }
332
+ if (mapped === 'noAction') return undefined;
333
+ return mapped;
334
+ }