@prisma-next/adapter-sqlite 0.5.0-dev.9 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +3 -3
  2. package/dist/{adapter-BG7_o_3h.d.mts → adapter-DswQk5bT.d.mts} +8 -5
  3. package/dist/adapter-DswQk5bT.d.mts.map +1 -0
  4. package/dist/{adapter-CcfiEUus.mjs → adapter-NxKQDxOd.mjs} +60 -64
  5. package/dist/adapter-NxKQDxOd.mjs.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 -42
  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 +107 -74
  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-CcfiEUus.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-ANhEQz9X.mjs +0 -102
  43. package/dist/codecs-ANhEQz9X.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 -129
  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.9",
3
+ "version": "0.5.0",
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/contract": "0.5.0-dev.9",
13
- "@prisma-next/cli": "0.5.0-dev.9",
14
- "@prisma-next/framework-components": "0.5.0-dev.9",
15
- "@prisma-next/contract-authoring": "0.5.0-dev.9",
16
- "@prisma-next/ids": "0.5.0-dev.9",
17
- "@prisma-next/family-sql": "0.5.0-dev.9",
18
- "@prisma-next/sql-contract": "0.5.0-dev.9",
19
- "@prisma-next/sql-contract-psl": "0.5.0-dev.9",
20
- "@prisma-next/sql-contract-ts": "0.5.0-dev.9",
21
- "@prisma-next/sql-operations": "0.5.0-dev.9",
22
- "@prisma-next/sql-runtime": "0.5.0-dev.9",
23
- "@prisma-next/sql-relational-core": "0.5.0-dev.9",
24
- "@prisma-next/sql-schema-ir": "0.5.0-dev.9",
25
- "@prisma-next/utils": "0.5.0-dev.9"
12
+ "arktype": "^2.1.29",
13
+ "@prisma-next/cli": "0.5.0",
14
+ "@prisma-next/contract-authoring": "0.5.0",
15
+ "@prisma-next/framework-components": "0.5.0",
16
+ "@prisma-next/ids": "0.5.0",
17
+ "@prisma-next/contract": "0.5.0",
18
+ "@prisma-next/sql-contract": "0.5.0",
19
+ "@prisma-next/family-sql": "0.5.0",
20
+ "@prisma-next/sql-operations": "0.5.0",
21
+ "@prisma-next/sql-contract-psl": "0.5.0",
22
+ "@prisma-next/sql-relational-core": "0.5.0",
23
+ "@prisma-next/sql-runtime": "0.5.0",
24
+ "@prisma-next/sql-schema-ir": "0.5.0",
25
+ "@prisma-next/target-sqlite": "0.5.0",
26
+ "@prisma-next/utils": "0.5.0",
27
+ "@prisma-next/sql-contract-ts": "0.5.0"
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/tsconfig": "0.0.0",
33
+ "vitest": "4.1.5",
34
+ "@prisma-next/driver-sqlite": "0.5.0",
32
35
  "@prisma-next/test-utils": "0.0.1",
33
- "@prisma-next/tsdown": "0.0.0"
36
+ "@prisma-next/tsdown": "0.0.0",
37
+ "@prisma-next/tsconfig": "0.0.0",
38
+ "@prisma-next/migration-tools": "0.5.0"
34
39
  },
35
40
  "exports": {
36
41
  "./adapter": "./dist/adapter.mjs",
@@ -1,34 +1,35 @@
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
+ MarkerReadResult,
22
+ NullCheckExpr,
23
+ OperationExpr,
24
+ OrderByItem,
25
+ ProjectionItem,
26
+ SelectAst,
27
+ SqlQueryable,
28
+ SubqueryExpr,
29
+ UpdateAst,
29
30
  } from '@prisma-next/sql-relational-core/ast';
30
- import { codecDefinitions } from './codecs';
31
- import { escapeLiteral, quoteIdentifier } from './sql-utils';
31
+ import { parseContractMarkerRow } from '@prisma-next/sql-runtime';
32
+ import { escapeLiteral, quoteIdentifier } from '@prisma-next/target-sqlite/sql-utils';
32
33
  import type { SqliteAdapterOptions, SqliteContract, SqliteLoweredStatement } from './types';
33
34
 
34
35
  const defaultCapabilities = Object.freeze({
@@ -47,60 +48,57 @@ class SqliteAdapterImpl implements Adapter<AnyQueryAst, SqliteContract, SqliteLo
47
48
  readonly targetId = 'sqlite' as const;
48
49
 
49
50
  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
51
 
58
52
  constructor(options?: SqliteAdapterOptions) {
59
53
  this.profile = Object.freeze({
60
54
  id: options?.profileId ?? 'sqlite/default@1',
61
55
  target: 'sqlite',
62
56
  capabilities: defaultCapabilities,
63
- codecs: () => this.codecRegistry,
64
- 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],
67
- }),
57
+ readMarker: (queryable: SqlQueryable) => readSqliteMarker(queryable),
68
58
  });
69
59
  }
70
60
 
71
- parameterizedCodecs(): ReadonlyArray<CodecParamsDescriptor> {
72
- return [];
73
- }
74
-
75
61
  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
- }
62
+ return renderLoweredSql(ast, context.contract);
63
+ }
64
+ }
81
65
 
82
- let sql: string;
66
+ /**
67
+ * Lower a SQL query AST into a SQLite-flavored `{ sql, params }` payload.
68
+ *
69
+ * 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.
70
+ */
71
+ export function renderLoweredSql(
72
+ ast: AnyQueryAst,
73
+ contract: SqliteContract,
74
+ ): SqliteLoweredStatement {
75
+ const collectedParamRefs = ast.collectParamRefs();
76
+ const params: unknown[] = [];
77
+ for (const ref of collectedParamRefs) {
78
+ params.push(ref.value);
79
+ }
83
80
 
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
- }
81
+ let sql: string;
101
82
 
102
- return Object.freeze({ sql, params });
83
+ const node = ast;
84
+ switch (node.kind) {
85
+ case 'select':
86
+ sql = renderSelect(node, contract);
87
+ break;
88
+ case 'insert':
89
+ sql = renderInsert(node);
90
+ break;
91
+ case 'update':
92
+ sql = renderUpdate(node, contract);
93
+ break;
94
+ case 'delete':
95
+ sql = renderDelete(node);
96
+ break;
97
+ default:
98
+ throw new Error(`Unsupported AST node kind: ${(node as { kind: string }).kind}`);
103
99
  }
100
+
101
+ return Object.freeze({ sql, params });
104
102
  }
105
103
 
106
104
  function renderSelect(ast: SelectAst, contract?: SqliteContract): string {
@@ -222,8 +220,7 @@ function renderExpr(expr: AnyExpression, contract?: SqliteContract): string {
222
220
  }
223
221
  }
224
222
 
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.
223
+ // `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
224
  function renderColumn(ref: ColumnRef): string {
228
225
  if (ref.table === 'excluded') {
229
226
  return `excluded.${quoteIdentifier(ref.column)}`;
@@ -506,11 +503,47 @@ function renderDelete(ast: DeleteAst): string {
506
503
  return `DELETE FROM ${table}${whereClause}${returningClause}`;
507
504
  }
508
505
 
509
- function renderReturning(returning: ReadonlyArray<ColumnRef> | undefined): string {
506
+ function renderReturning(returning: ReadonlyArray<ProjectionItem> | undefined): string {
510
507
  if (!returning?.length) {
511
508
  return '';
512
509
  }
513
- return ` RETURNING ${returning.map((col) => `${quoteIdentifier(col.table)}.${quoteIdentifier(col.column)}`).join(', ')}`;
510
+ return ` RETURNING ${returning
511
+ .map((item) => {
512
+ if (item.expr.kind === 'column-ref') {
513
+ const rendered = `${quoteIdentifier(item.expr.table)}.${quoteIdentifier(item.expr.column)}`;
514
+ return item.expr.column === item.alias
515
+ ? rendered
516
+ : `${rendered} AS ${quoteIdentifier(item.alias)}`;
517
+ }
518
+ return `${renderExpr(item.expr)} AS ${quoteIdentifier(item.alias)}`;
519
+ })
520
+ .join(', ')}`;
521
+ }
522
+
523
+ async function readSqliteMarker(queryable: SqlQueryable): Promise<MarkerReadResult> {
524
+ const exists = await queryable.query(
525
+ "select 1 from sqlite_master where type = 'table' and name = ?",
526
+ ['_prisma_marker'],
527
+ );
528
+ if (exists.rows.length === 0) {
529
+ return { kind: 'no-table' };
530
+ }
531
+
532
+ const result = await queryable.query(
533
+ 'select core_hash, profile_hash, contract_json, canonical_version, updated_at, app_tag, meta, invariants from _prisma_marker where space = ?',
534
+ [APP_SPACE_ID],
535
+ );
536
+ const row = result.rows[0];
537
+ if (!row) {
538
+ return { kind: 'absent' };
539
+ }
540
+ // 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[]`.
541
+ const raw = row as Record<string, unknown>;
542
+ const invariants =
543
+ typeof raw['invariants'] === 'string'
544
+ ? (JSON.parse(raw['invariants']) as unknown)
545
+ : raw['invariants'];
546
+ return { kind: 'present', record: parseContractMarkerRow({ ...raw, invariants }) };
514
547
  }
515
548
 
516
549
  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
+ }