@prisma-next/adapter-sqlite 0.5.0-dev.19 → 0.5.0-dev.20
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.
- package/README.md +2 -2
- package/dist/{adapter-BG7_o_3h.d.mts → adapter-CjuvmCVF.d.mts} +10 -2
- package/dist/{adapter-BG7_o_3h.d.mts.map → adapter-CjuvmCVF.d.mts.map} +1 -1
- package/dist/{adapter-DEZK9PjU.mjs → adapter-DyWrpIfH.mjs} +39 -48
- package/dist/adapter-DyWrpIfH.mjs.map +1 -0
- package/dist/adapter.d.mts +2 -2
- package/dist/adapter.mjs +1 -2
- package/dist/codec-types.d.mts +2 -42
- package/dist/codec-types.mjs +1 -1
- package/dist/column-types.d.mts +1 -5
- package/dist/column-types.d.mts.map +1 -1
- package/dist/column-types.mjs +2 -6
- package/dist/column-types.mjs.map +1 -1
- package/dist/control.d.mts +35 -3
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +414 -8
- package/dist/control.mjs.map +1 -1
- package/dist/{descriptor-meta-Bg-c1LmL.mjs → descriptor-meta-DYT9Gt_F.mjs} +1 -1
- package/dist/{descriptor-meta-Bg-c1LmL.mjs.map → descriptor-meta-DYT9Gt_F.mjs.map} +1 -1
- package/dist/runtime.d.mts +2 -2
- package/dist/runtime.d.mts.map +1 -1
- package/dist/runtime.mjs +17 -4
- package/dist/runtime.mjs.map +1 -1
- package/dist/{types-gAqc4ucF.d.mts → types-BFRXGAgd.d.mts} +1 -1
- package/dist/{types-gAqc4ucF.d.mts.map → types-BFRXGAgd.d.mts.map} +1 -1
- package/dist/types.d.mts +1 -1
- package/package.json +20 -17
- package/src/core/adapter.ts +41 -27
- package/src/core/column-types.ts +1 -7
- package/src/core/control-adapter.ts +279 -15
- package/src/core/control-mutation-defaults.ts +352 -0
- package/src/core/runtime-adapter.ts +15 -2
- package/src/exports/codec-types.ts +8 -6
- package/src/exports/column-types.ts +0 -1
- package/src/exports/control.ts +45 -1
- package/dist/adapter-DEZK9PjU.mjs.map +0 -1
- package/dist/codec-ids-o_Z8i4nt.mjs +0 -15
- package/dist/codec-ids-o_Z8i4nt.mjs.map +0 -1
- package/dist/codec-types.d.mts.map +0 -1
- package/dist/codecs-ANhEQz9X.mjs +0 -102
- package/dist/codecs-ANhEQz9X.mjs.map +0 -1
- package/src/core/codec-ids.ts +0 -14
- package/src/core/codecs.ts +0 -129
- package/src/core/sql-utils.ts +0 -35
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/adapter-sqlite",
|
|
3
|
-
"version": "0.5.0-dev.
|
|
3
|
+
"version": "0.5.0-dev.20",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"files": [
|
|
@@ -9,28 +9,31 @@
|
|
|
9
9
|
],
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"arktype": "^2.0.0",
|
|
12
|
-
"@prisma-next/cli": "0.5.0-dev.
|
|
13
|
-
"@prisma-next/contract": "0.5.0-dev.
|
|
14
|
-
"@prisma-next/
|
|
15
|
-
"@prisma-next/
|
|
16
|
-
"@prisma-next/sql-contract": "0.5.0-dev.
|
|
17
|
-
"@prisma-next/ids": "0.5.0-dev.
|
|
18
|
-
"@prisma-next/
|
|
19
|
-
"@prisma-next/sql-contract-
|
|
20
|
-
"@prisma-next/sql-operations": "0.5.0-dev.
|
|
21
|
-
"@prisma-next/sql-
|
|
22
|
-
"@prisma-next/sql-
|
|
23
|
-
"@prisma-next/sql-
|
|
24
|
-
"@prisma-next/
|
|
25
|
-
"@prisma-next/sql-
|
|
12
|
+
"@prisma-next/cli": "0.5.0-dev.20",
|
|
13
|
+
"@prisma-next/contract-authoring": "0.5.0-dev.20",
|
|
14
|
+
"@prisma-next/framework-components": "0.5.0-dev.20",
|
|
15
|
+
"@prisma-next/family-sql": "0.5.0-dev.20",
|
|
16
|
+
"@prisma-next/sql-contract": "0.5.0-dev.20",
|
|
17
|
+
"@prisma-next/ids": "0.5.0-dev.20",
|
|
18
|
+
"@prisma-next/contract": "0.5.0-dev.20",
|
|
19
|
+
"@prisma-next/sql-contract-psl": "0.5.0-dev.20",
|
|
20
|
+
"@prisma-next/sql-operations": "0.5.0-dev.20",
|
|
21
|
+
"@prisma-next/sql-relational-core": "0.5.0-dev.20",
|
|
22
|
+
"@prisma-next/sql-runtime": "0.5.0-dev.20",
|
|
23
|
+
"@prisma-next/sql-schema-ir": "0.5.0-dev.20",
|
|
24
|
+
"@prisma-next/target-sqlite": "0.5.0-dev.20",
|
|
25
|
+
"@prisma-next/sql-contract-ts": "0.5.0-dev.20",
|
|
26
|
+
"@prisma-next/utils": "0.5.0-dev.20"
|
|
26
27
|
},
|
|
27
28
|
"devDependencies": {
|
|
29
|
+
"pathe": "^2.0.3",
|
|
28
30
|
"tsdown": "0.18.4",
|
|
29
31
|
"typescript": "5.9.3",
|
|
30
32
|
"vitest": "4.0.17",
|
|
31
|
-
"@prisma-next/
|
|
33
|
+
"@prisma-next/driver-sqlite": "0.5.0-dev.20",
|
|
32
34
|
"@prisma-next/tsconfig": "0.0.0",
|
|
33
|
-
"@prisma-next/tsdown": "0.0.0"
|
|
35
|
+
"@prisma-next/tsdown": "0.0.0",
|
|
36
|
+
"@prisma-next/test-utils": "0.0.1"
|
|
34
37
|
},
|
|
35
38
|
"exports": {
|
|
36
39
|
"./adapter": "./dist/adapter.mjs",
|
package/src/core/adapter.ts
CHANGED
|
@@ -28,8 +28,8 @@ import {
|
|
|
28
28
|
type UpdateAst,
|
|
29
29
|
} from '@prisma-next/sql-relational-core/ast';
|
|
30
30
|
import { parseContractMarkerRow } from '@prisma-next/sql-runtime';
|
|
31
|
-
import { codecDefinitions } from '
|
|
32
|
-
import { escapeLiteral, quoteIdentifier } from '
|
|
31
|
+
import { codecDefinitions } from '@prisma-next/target-sqlite/codecs';
|
|
32
|
+
import { escapeLiteral, quoteIdentifier } from '@prisma-next/target-sqlite/sql-utils';
|
|
33
33
|
import type { SqliteAdapterOptions, SqliteContract, SqliteLoweredStatement } from './types';
|
|
34
34
|
|
|
35
35
|
const defaultCapabilities = Object.freeze({
|
|
@@ -63,7 +63,7 @@ class SqliteAdapterImpl implements Adapter<AnyQueryAst, SqliteContract, SqliteLo
|
|
|
63
63
|
capabilities: defaultCapabilities,
|
|
64
64
|
codecs: () => this.codecRegistry,
|
|
65
65
|
readMarkerStatement: () => ({
|
|
66
|
-
sql: 'select core_hash, profile_hash, contract_json, canonical_version, updated_at, app_tag, meta, invariants from
|
|
66
|
+
sql: 'select core_hash, profile_hash, contract_json, canonical_version, updated_at, app_tag, meta, invariants from _prisma_marker where id = ?',
|
|
67
67
|
params: [1],
|
|
68
68
|
}),
|
|
69
69
|
// SQLite stores arrays as JSON-encoded TEXT (no native array type),
|
|
@@ -85,34 +85,48 @@ class SqliteAdapterImpl implements Adapter<AnyQueryAst, SqliteContract, SqliteLo
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
lower(ast: AnyQueryAst, context: LowererContext<SqliteContract>): SqliteLoweredStatement {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
params.push(ref.value);
|
|
92
|
-
}
|
|
88
|
+
return renderLoweredSql(ast, context.contract);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
93
91
|
|
|
94
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Lower a SQL query AST into a SQLite-flavored `{ sql, params }` payload.
|
|
94
|
+
*
|
|
95
|
+
* Shared between the runtime adapter (`SqliteAdapterImpl.lower`) and the
|
|
96
|
+
* control adapter (`SqliteControlAdapter.lower`) so both produce
|
|
97
|
+
* byte-identical SQL for the same AST and contract.
|
|
98
|
+
*/
|
|
99
|
+
export function renderLoweredSql(
|
|
100
|
+
ast: AnyQueryAst,
|
|
101
|
+
contract: SqliteContract,
|
|
102
|
+
): SqliteLoweredStatement {
|
|
103
|
+
const collectedParamRefs = ast.collectParamRefs();
|
|
104
|
+
const params: unknown[] = [];
|
|
105
|
+
for (const ref of collectedParamRefs) {
|
|
106
|
+
params.push(ref.value);
|
|
107
|
+
}
|
|
95
108
|
|
|
96
|
-
|
|
97
|
-
switch (node.kind) {
|
|
98
|
-
case 'select':
|
|
99
|
-
sql = renderSelect(node, context.contract);
|
|
100
|
-
break;
|
|
101
|
-
case 'insert':
|
|
102
|
-
sql = renderInsert(node);
|
|
103
|
-
break;
|
|
104
|
-
case 'update':
|
|
105
|
-
sql = renderUpdate(node, context.contract);
|
|
106
|
-
break;
|
|
107
|
-
case 'delete':
|
|
108
|
-
sql = renderDelete(node);
|
|
109
|
-
break;
|
|
110
|
-
default:
|
|
111
|
-
throw new Error(`Unsupported AST node kind: ${(node as { kind: string }).kind}`);
|
|
112
|
-
}
|
|
109
|
+
let sql: string;
|
|
113
110
|
|
|
114
|
-
|
|
111
|
+
const node = ast;
|
|
112
|
+
switch (node.kind) {
|
|
113
|
+
case 'select':
|
|
114
|
+
sql = renderSelect(node, contract);
|
|
115
|
+
break;
|
|
116
|
+
case 'insert':
|
|
117
|
+
sql = renderInsert(node);
|
|
118
|
+
break;
|
|
119
|
+
case 'update':
|
|
120
|
+
sql = renderUpdate(node, contract);
|
|
121
|
+
break;
|
|
122
|
+
case 'delete':
|
|
123
|
+
sql = renderDelete(node);
|
|
124
|
+
break;
|
|
125
|
+
default:
|
|
126
|
+
throw new Error(`Unsupported AST node kind: ${(node as { kind: string }).kind}`);
|
|
115
127
|
}
|
|
128
|
+
|
|
129
|
+
return Object.freeze({ sql, params });
|
|
116
130
|
}
|
|
117
131
|
|
|
118
132
|
function renderSelect(ast: SelectAst, contract?: SqliteContract): string {
|
package/src/core/column-types.ts
CHANGED
|
@@ -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 '
|
|
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,282 @@
|
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
+
): Promise<ContractMarkerRecord | null> {
|
|
95
|
+
const exists = await driver.query(
|
|
96
|
+
`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?`,
|
|
97
|
+
['_prisma_marker'],
|
|
98
|
+
);
|
|
99
|
+
if (exists.rows.length === 0) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const result = await driver.query<{
|
|
104
|
+
core_hash: string;
|
|
105
|
+
profile_hash: string;
|
|
106
|
+
contract_json: unknown | null;
|
|
107
|
+
canonical_version: number | null;
|
|
108
|
+
updated_at: Date | string;
|
|
109
|
+
app_tag: string | null;
|
|
110
|
+
meta: unknown | null;
|
|
111
|
+
invariants: unknown;
|
|
112
|
+
}>(
|
|
113
|
+
`SELECT
|
|
114
|
+
core_hash,
|
|
115
|
+
profile_hash,
|
|
116
|
+
contract_json,
|
|
117
|
+
canonical_version,
|
|
118
|
+
updated_at,
|
|
119
|
+
app_tag,
|
|
120
|
+
meta,
|
|
121
|
+
invariants
|
|
122
|
+
FROM _prisma_marker
|
|
123
|
+
WHERE id = ?`,
|
|
124
|
+
[1],
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const row = result.rows[0];
|
|
128
|
+
if (!row) return null;
|
|
129
|
+
// SQLite stores arrays as JSON-encoded TEXT (no native array type), so
|
|
130
|
+
// the driver returns `invariants` as a string. Decode before delegating
|
|
131
|
+
// to the shared row schema, which expects `string[]`.
|
|
132
|
+
const invariants =
|
|
133
|
+
typeof row.invariants === 'string' ? (JSON.parse(row.invariants) as unknown) : row.invariants;
|
|
134
|
+
return parseContractMarkerRow({ ...row, invariants });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async introspect(
|
|
138
|
+
driver: ControlDriverInstance<'sql', 'sqlite'>,
|
|
139
|
+
_contract?: unknown,
|
|
140
|
+
): Promise<SqlSchemaIR> {
|
|
141
|
+
// Filter out runner-managed control tables (`_prisma_marker`,
|
|
142
|
+
// `_prisma_ledger`) — they're an implementation detail of the migration
|
|
143
|
+
// runner, not part of the user-authored contract, so they must not
|
|
144
|
+
// appear in introspection output (otherwise strict schema verification
|
|
145
|
+
// flags them as `extra_table`).
|
|
146
|
+
const tablesResult = await driver.query<{ name: string }>(
|
|
147
|
+
`SELECT name FROM sqlite_master
|
|
148
|
+
WHERE type = 'table'
|
|
149
|
+
AND name NOT LIKE 'sqlite_%'
|
|
150
|
+
AND name NOT IN ('_prisma_marker', '_prisma_ledger')
|
|
151
|
+
ORDER BY name`,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const tables: Record<string, SqlTableIR> = {};
|
|
155
|
+
|
|
156
|
+
for (const tableRow of tablesResult.rows) {
|
|
157
|
+
const tableName = tableRow.name;
|
|
158
|
+
|
|
159
|
+
// SQLite's synchronous driver serializes reads — no benefit from Promise.all
|
|
160
|
+
const columnsResult = await driver.query<PragmaTableInfoRow>(
|
|
161
|
+
`PRAGMA table_info("${escapePragmaArg(tableName)}")`,
|
|
162
|
+
);
|
|
163
|
+
const fkResult = await driver.query<PragmaForeignKeyRow>(
|
|
164
|
+
`PRAGMA foreign_key_list("${escapePragmaArg(tableName)}")`,
|
|
165
|
+
);
|
|
166
|
+
const indexListResult = await driver.query<PragmaIndexListRow>(
|
|
167
|
+
`PRAGMA index_list("${escapePragmaArg(tableName)}")`,
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const columns: Record<string, SqlColumnIR> = {};
|
|
171
|
+
const pkColumns: Array<{ name: string; pk: number }> = [];
|
|
172
|
+
|
|
173
|
+
for (const col of columnsResult.rows) {
|
|
174
|
+
columns[col.name] = {
|
|
175
|
+
name: col.name,
|
|
176
|
+
nativeType: col.type.toLowerCase(),
|
|
177
|
+
nullable: col.notnull === 0 && col.pk === 0,
|
|
178
|
+
...ifDefined('default', col.dflt_value ?? undefined),
|
|
179
|
+
};
|
|
180
|
+
if (col.pk > 0) {
|
|
181
|
+
pkColumns.push({ name: col.name, pk: col.pk });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
pkColumns.sort((a, b) => a.pk - b.pk);
|
|
186
|
+
const primaryKey: PrimaryKey | undefined =
|
|
187
|
+
pkColumns.length > 0 ? { columns: pkColumns.map((c) => c.name) } : undefined;
|
|
188
|
+
|
|
189
|
+
const fkMap = new Map<number, FkAccumulator>();
|
|
190
|
+
for (const fk of fkResult.rows) {
|
|
191
|
+
const existing = fkMap.get(fk.id);
|
|
192
|
+
if (existing) {
|
|
193
|
+
existing.columns.push(fk.from);
|
|
194
|
+
existing.referencedColumns.push(fk.to);
|
|
195
|
+
} else {
|
|
196
|
+
fkMap.set(fk.id, {
|
|
197
|
+
columns: [fk.from],
|
|
198
|
+
referencedTable: fk.table,
|
|
199
|
+
referencedColumns: [fk.to],
|
|
200
|
+
onDelete: fk.on_delete,
|
|
201
|
+
onUpdate: fk.on_update,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const foreignKeys: readonly SqlForeignKeyIR[] = Array.from(fkMap.values()).map((fk) => ({
|
|
206
|
+
columns: Object.freeze([...fk.columns]) as readonly string[],
|
|
207
|
+
referencedTable: fk.referencedTable,
|
|
208
|
+
referencedColumns: Object.freeze([...fk.referencedColumns]) as readonly string[],
|
|
209
|
+
...ifDefined('onDelete', mapSqliteReferentialAction(fk.onDelete)),
|
|
210
|
+
...ifDefined('onUpdate', mapSqliteReferentialAction(fk.onUpdate)),
|
|
211
|
+
}));
|
|
212
|
+
|
|
213
|
+
const uniques: SqlUniqueIR[] = [];
|
|
214
|
+
const indexes: SqlIndexIR[] = [];
|
|
215
|
+
|
|
216
|
+
for (const idx of indexListResult.rows) {
|
|
217
|
+
// origin: 'c' = CREATE INDEX, 'u' = UNIQUE constraint, 'pk' = PRIMARY KEY
|
|
218
|
+
const idxInfoResult = await driver.query<PragmaIndexInfoRow>(
|
|
219
|
+
`PRAGMA index_info("${escapePragmaArg(idx.name)}")`,
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const idxColumns = idxInfoResult.rows.sort((a, b) => a.seqno - b.seqno).map((r) => r.name);
|
|
223
|
+
|
|
224
|
+
if (idx.origin === 'u') {
|
|
225
|
+
uniques.push({
|
|
226
|
+
columns: Object.freeze([...idxColumns]) as readonly string[],
|
|
227
|
+
name: idx.name,
|
|
228
|
+
});
|
|
229
|
+
} else if (idx.origin === 'c') {
|
|
230
|
+
indexes.push({
|
|
231
|
+
columns: Object.freeze([...idxColumns]) as readonly string[],
|
|
232
|
+
name: idx.name,
|
|
233
|
+
unique: idx.unique === 1,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
// Skip 'pk' origin — already captured in primaryKey
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
tables[tableName] = {
|
|
240
|
+
name: tableName,
|
|
241
|
+
columns,
|
|
242
|
+
...ifDefined('primaryKey', primaryKey),
|
|
243
|
+
foreignKeys,
|
|
244
|
+
uniques,
|
|
245
|
+
indexes,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
tables,
|
|
251
|
+
dependencies: [],
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// PRAGMA queries use the function-argument form (`PRAGMA table_info("name")`)
|
|
257
|
+
// which doesn't support `?` placeholders — the argument is part of the
|
|
258
|
+
// statement name, not a bound parameter. We quote-escape the table name instead.
|
|
259
|
+
function escapePragmaArg(name: string): string {
|
|
260
|
+
return name.replace(/"/g, '""');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const SQLITE_REFERENTIAL_ACTION_MAP: Record<string, SqlReferentialAction> = {
|
|
264
|
+
'NO ACTION': 'noAction',
|
|
265
|
+
RESTRICT: 'restrict',
|
|
266
|
+
CASCADE: 'cascade',
|
|
267
|
+
'SET NULL': 'setNull',
|
|
268
|
+
'SET DEFAULT': 'setDefault',
|
|
16
269
|
};
|
|
17
270
|
|
|
18
|
-
|
|
271
|
+
function mapSqliteReferentialAction(rule: string): SqlReferentialAction | undefined {
|
|
272
|
+
const normalized = rule.toUpperCase();
|
|
273
|
+
const mapped = SQLITE_REFERENTIAL_ACTION_MAP[normalized];
|
|
274
|
+
if (mapped === undefined) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
`Unknown SQLite referential action rule: "${rule}". ` +
|
|
277
|
+
'Expected one of: NO ACTION, RESTRICT, CASCADE, SET NULL, SET DEFAULT.',
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
if (mapped === 'noAction') return undefined;
|
|
281
|
+
return mapped;
|
|
282
|
+
}
|