@prisma-next/adapter-sqlite 0.5.0-dev.8 → 0.5.0-dev.81
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 +3 -3
- package/dist/{adapter-CcfiEUus.mjs → adapter-C4IjVF9z.mjs} +56 -63
- package/dist/adapter-C4IjVF9z.mjs.map +1 -0
- package/dist/{adapter-BG7_o_3h.d.mts → adapter-DswQk5bT.d.mts} +8 -5
- package/dist/adapter-DswQk5bT.d.mts.map +1 -0
- package/dist/adapter.d.mts +1 -2
- package/dist/adapter.mjs +2 -4
- package/dist/codec-types.d.mts +2 -42
- package/dist/codec-types.mjs +1 -3
- package/dist/column-types.d.mts +1 -5
- package/dist/column-types.d.mts.map +1 -1
- package/dist/column-types.mjs +3 -8
- package/dist/column-types.mjs.map +1 -1
- package/dist/control.d.mts +41 -3
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +441 -10
- package/dist/control.mjs.map +1 -1
- package/dist/{descriptor-meta-Bg-c1LmL.mjs → descriptor-meta-DA4lgWT_.mjs} +2 -2
- package/dist/{descriptor-meta-Bg-c1LmL.mjs.map → descriptor-meta-DA4lgWT_.mjs.map} +1 -1
- package/dist/runtime.d.mts +2 -3
- package/dist/runtime.d.mts.map +1 -1
- package/dist/runtime.mjs +21 -15
- package/dist/runtime.mjs.map +1 -1
- package/dist/{types-gAqc4ucF.d.mts → types-bTlW__XL.d.mts} +1 -1
- package/dist/types-bTlW__XL.d.mts.map +1 -0
- package/dist/types.d.mts +1 -1
- package/dist/types.mjs +1 -1
- package/package.json +25 -20
- package/src/core/adapter.ts +89 -72
- package/src/core/column-types.ts +1 -7
- package/src/core/control-adapter.ts +331 -15
- package/src/core/control-mutation-defaults.ts +358 -0
- package/src/core/runtime-adapter.ts +19 -12
- package/src/exports/codec-types.ts +2 -6
- package/src/exports/column-types.ts +0 -1
- package/src/exports/control.ts +45 -1
- package/dist/adapter-BG7_o_3h.d.mts.map +0 -1
- package/dist/adapter-CcfiEUus.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/dist/types-gAqc4ucF.d.mts.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,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/adapter-sqlite",
|
|
3
|
-
"version": "0.5.0-dev.
|
|
3
|
+
"version": "0.5.0-dev.81",
|
|
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.
|
|
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/
|
|
17
|
-
"@prisma-next/
|
|
18
|
-
"@prisma-next/sql-contract": "0.5.0-dev.
|
|
19
|
-
"@prisma-next/sql-
|
|
20
|
-
"@prisma-next/sql-
|
|
21
|
-
"@prisma-next/sql-
|
|
22
|
-
"@prisma-next/sql-
|
|
23
|
-
"@prisma-next/sql-
|
|
24
|
-
"@prisma-next/
|
|
25
|
-
"@prisma-next/
|
|
12
|
+
"arktype": "^2.1.29",
|
|
13
|
+
"@prisma-next/cli": "0.5.0-dev.81",
|
|
14
|
+
"@prisma-next/contract-authoring": "0.5.0-dev.81",
|
|
15
|
+
"@prisma-next/contract": "0.5.0-dev.81",
|
|
16
|
+
"@prisma-next/framework-components": "0.5.0-dev.81",
|
|
17
|
+
"@prisma-next/family-sql": "0.5.0-dev.81",
|
|
18
|
+
"@prisma-next/ids": "0.5.0-dev.81",
|
|
19
|
+
"@prisma-next/sql-contract": "0.5.0-dev.81",
|
|
20
|
+
"@prisma-next/sql-operations": "0.5.0-dev.81",
|
|
21
|
+
"@prisma-next/sql-contract-ts": "0.5.0-dev.81",
|
|
22
|
+
"@prisma-next/sql-contract-psl": "0.5.0-dev.81",
|
|
23
|
+
"@prisma-next/sql-relational-core": "0.5.0-dev.81",
|
|
24
|
+
"@prisma-next/sql-schema-ir": "0.5.0-dev.81",
|
|
25
|
+
"@prisma-next/target-sqlite": "0.5.0-dev.81",
|
|
26
|
+
"@prisma-next/sql-runtime": "0.5.0-dev.81",
|
|
27
|
+
"@prisma-next/utils": "0.5.0-dev.81"
|
|
26
28
|
},
|
|
27
29
|
"devDependencies": {
|
|
28
|
-
"
|
|
30
|
+
"pathe": "^2.0.3",
|
|
31
|
+
"tsdown": "0.22.0",
|
|
29
32
|
"typescript": "5.9.3",
|
|
30
|
-
"vitest": "4.
|
|
33
|
+
"vitest": "4.1.5",
|
|
34
|
+
"@prisma-next/driver-sqlite": "0.5.0-dev.81",
|
|
35
|
+
"@prisma-next/migration-tools": "0.5.0-dev.81",
|
|
36
|
+
"@prisma-next/test-utils": "0.0.1",
|
|
31
37
|
"@prisma-next/tsdown": "0.0.0",
|
|
32
|
-
"@prisma-next/tsconfig": "0.0.0"
|
|
33
|
-
"@prisma-next/test-utils": "0.0.1"
|
|
38
|
+
"@prisma-next/tsconfig": "0.0.0"
|
|
34
39
|
},
|
|
35
40
|
"exports": {
|
|
36
41
|
"./adapter": "./dist/adapter.mjs",
|
package/src/core/adapter.ts
CHANGED
|
@@ -1,34 +1,33 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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 {
|
|
31
|
-
import { escapeLiteral, quoteIdentifier } from '
|
|
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
|
|
66
|
-
params: [
|
|
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
|
-
|
|
72
|
-
return
|
|
71
|
+
lower(ast: AnyQueryAst, context: LowererContext<SqliteContract>): SqliteLoweredStatement {
|
|
72
|
+
return renderLoweredSql(ast, context.contract);
|
|
73
73
|
}
|
|
74
|
+
}
|
|
74
75
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
91
|
+
let sql: string;
|
|
83
92
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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<
|
|
516
|
+
function renderReturning(returning: ReadonlyArray<ProjectionItem> | undefined): string {
|
|
510
517
|
if (!returning?.length) {
|
|
511
518
|
return '';
|
|
512
519
|
}
|
|
513
|
-
return ` RETURNING ${returning
|
|
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) {
|
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,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
|
-
|
|
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
|
+
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
|
-
|
|
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
|
+
}
|