@prisma-next/adapter-sqlite 0.12.0-dev.31 → 0.12.0-dev.32
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/dist/{adapter-oxTARI-j.mjs → adapter-CwTpH1kL.mjs} +384 -19
- package/dist/adapter-CwTpH1kL.mjs.map +1 -0
- package/dist/adapter-DJmACrLG.d.mts.map +1 -1
- package/dist/adapter.mjs +1 -1
- package/dist/control.d.mts +41 -1
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +3 -262
- package/dist/control.mjs.map +1 -1
- package/dist/runtime.mjs +2 -2
- package/package.json +22 -22
- package/src/core/adapter.ts +20 -29
- package/src/core/control-adapter.ts +268 -145
- package/src/core/marker-ledger.ts +124 -0
- package/dist/adapter-oxTARI-j.mjs.map +0 -1
package/src/core/adapter.ts
CHANGED
|
@@ -19,7 +19,6 @@ import type {
|
|
|
19
19
|
LiteralExpr,
|
|
20
20
|
LoweredParam,
|
|
21
21
|
LowererContext,
|
|
22
|
-
MarkerReadResult,
|
|
23
22
|
NullCheckExpr,
|
|
24
23
|
OperationExpr,
|
|
25
24
|
OrderByItem,
|
|
@@ -35,9 +34,9 @@ import type {
|
|
|
35
34
|
} from '@prisma-next/sql-relational-core/ast';
|
|
36
35
|
import { isDdlNode } from '@prisma-next/sql-relational-core/ast';
|
|
37
36
|
import type { RawCodecInferer } from '@prisma-next/sql-relational-core/expression';
|
|
38
|
-
import { parseContractMarkerRow } from '@prisma-next/sql-runtime';
|
|
39
37
|
import type { SqliteDdlNode } from '@prisma-next/target-sqlite/ddl';
|
|
40
38
|
import { escapeLiteral, quoteIdentifier } from '@prisma-next/target-sqlite/sql-utils';
|
|
39
|
+
import { SqliteControlAdapter } from './control-adapter';
|
|
41
40
|
import { renderLoweredDdl } from './ddl-renderer';
|
|
42
41
|
import type { SqliteAdapterOptions, SqliteContract, SqliteLoweredStatement } from './types';
|
|
43
42
|
|
|
@@ -59,11 +58,27 @@ class SqliteAdapterImpl implements Adapter<AnyQueryAst, SqliteContract, SqliteLo
|
|
|
59
58
|
readonly profile: AdapterProfile<'sqlite'>;
|
|
60
59
|
|
|
61
60
|
constructor(options?: SqliteAdapterOptions) {
|
|
61
|
+
const controlAdapter = new SqliteControlAdapter();
|
|
62
62
|
this.profile = Object.freeze({
|
|
63
63
|
id: options?.profileId ?? 'sqlite/default@1',
|
|
64
64
|
target: 'sqlite',
|
|
65
65
|
capabilities: defaultCapabilities,
|
|
66
|
-
readMarker: (queryable: SqlQueryable) =>
|
|
66
|
+
readMarker: (queryable: SqlQueryable) =>
|
|
67
|
+
controlAdapter.readMarkerDiscriminated(
|
|
68
|
+
{
|
|
69
|
+
familyId: 'sql',
|
|
70
|
+
targetId: 'sqlite',
|
|
71
|
+
query: async <Row = Record<string, unknown>>(
|
|
72
|
+
sql: string,
|
|
73
|
+
params?: readonly unknown[],
|
|
74
|
+
) => {
|
|
75
|
+
const result = await queryable.query<Row>(sql, params);
|
|
76
|
+
return { rows: [...result.rows] };
|
|
77
|
+
},
|
|
78
|
+
close: async () => {},
|
|
79
|
+
},
|
|
80
|
+
APP_SPACE_ID,
|
|
81
|
+
),
|
|
67
82
|
});
|
|
68
83
|
}
|
|
69
84
|
|
|
@@ -515,6 +530,8 @@ function renderInsertValue(value: InsertValue): string {
|
|
|
515
530
|
return '?';
|
|
516
531
|
case 'column-ref':
|
|
517
532
|
return renderColumn(value);
|
|
533
|
+
case 'raw-expr':
|
|
534
|
+
return renderExpr(value);
|
|
518
535
|
case 'default-value':
|
|
519
536
|
throw new Error('SQLite does not support DEFAULT as a value in INSERT ... VALUES');
|
|
520
537
|
default:
|
|
@@ -618,32 +635,6 @@ function renderReturning(returning: ReadonlyArray<ProjectionItem> | undefined):
|
|
|
618
635
|
.join(', ')}`;
|
|
619
636
|
}
|
|
620
637
|
|
|
621
|
-
async function readSqliteMarker(queryable: SqlQueryable): Promise<MarkerReadResult> {
|
|
622
|
-
const exists = await queryable.query(
|
|
623
|
-
"select 1 from sqlite_master where type = 'table' and name = ?",
|
|
624
|
-
['_prisma_marker'],
|
|
625
|
-
);
|
|
626
|
-
if (exists.rows.length === 0) {
|
|
627
|
-
return { kind: 'no-table' };
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
const result = await queryable.query(
|
|
631
|
-
'select core_hash, profile_hash, contract_json, canonical_version, updated_at, app_tag, meta, invariants from _prisma_marker where space = ?',
|
|
632
|
-
[APP_SPACE_ID],
|
|
633
|
-
);
|
|
634
|
-
const row = result.rows[0];
|
|
635
|
-
if (!row) {
|
|
636
|
-
return { kind: 'absent' };
|
|
637
|
-
}
|
|
638
|
-
// 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[]`.
|
|
639
|
-
const raw = row as Record<string, unknown>;
|
|
640
|
-
const invariants =
|
|
641
|
-
typeof raw['invariants'] === 'string'
|
|
642
|
-
? (JSON.parse(raw['invariants']) as unknown)
|
|
643
|
-
: raw['invariants'];
|
|
644
|
-
return { kind: 'present', record: parseContractMarkerRow({ ...raw, invariants }) };
|
|
645
|
-
}
|
|
646
|
-
|
|
647
638
|
export function createSqliteAdapter(options?: SqliteAdapterOptions) {
|
|
648
639
|
return Object.freeze(new SqliteAdapterImpl(options));
|
|
649
640
|
}
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
DdlNode,
|
|
13
13
|
LoweredStatement,
|
|
14
14
|
LowererContext,
|
|
15
|
+
MarkerReadResult,
|
|
15
16
|
} from '@prisma-next/sql-relational-core/ast';
|
|
16
17
|
import { isDdlNode } from '@prisma-next/sql-relational-core/ast';
|
|
17
18
|
import type {
|
|
@@ -31,37 +32,35 @@ import {
|
|
|
31
32
|
import type { SqliteDdlNode } from '@prisma-next/target-sqlite/ddl';
|
|
32
33
|
import { parseSqliteDefault } from '@prisma-next/target-sqlite/default-normalizer';
|
|
33
34
|
import { normalizeSqliteNativeType } from '@prisma-next/target-sqlite/native-type-normalizer';
|
|
35
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
34
36
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
35
37
|
import { renderLoweredSql } from './adapter';
|
|
36
38
|
import { renderLoweredDdl } from './ddl-renderer';
|
|
37
39
|
import { coerceLedgerAppliedAt, operationCountFromStored } from './ledger-decode';
|
|
40
|
+
import {
|
|
41
|
+
decodeSqliteMarkerRow,
|
|
42
|
+
execute,
|
|
43
|
+
ledger,
|
|
44
|
+
ledgerReadShape,
|
|
45
|
+
marker,
|
|
46
|
+
mergeInvariants,
|
|
47
|
+
NOW,
|
|
48
|
+
sqliteCatalog,
|
|
49
|
+
} from './marker-ledger';
|
|
38
50
|
import type { SqliteContract } from './types';
|
|
39
51
|
|
|
40
52
|
const SQLITE_MARKER_TABLE = '_prisma_marker';
|
|
41
53
|
const SQLITE_LEDGER_TABLE = '_prisma_ledger';
|
|
42
54
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return row;
|
|
53
|
-
}
|
|
54
|
-
const record = row as { invariants: unknown };
|
|
55
|
-
if (typeof record.invariants !== 'string') return row;
|
|
56
|
-
let parsed: unknown;
|
|
57
|
-
try {
|
|
58
|
-
parsed = JSON.parse(record.invariants);
|
|
59
|
-
} catch (err) {
|
|
60
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
61
|
-
throw new Error(`Invalid contract marker row: invariants is not valid JSON: ${detail}`);
|
|
62
|
-
}
|
|
63
|
-
return { ...record, invariants: parsed };
|
|
64
|
-
}
|
|
55
|
+
type SqliteLedgerRow = {
|
|
56
|
+
readonly space: string;
|
|
57
|
+
readonly migration_name: string;
|
|
58
|
+
readonly migration_hash: string;
|
|
59
|
+
readonly origin_core_hash: string | null;
|
|
60
|
+
readonly destination_core_hash: string;
|
|
61
|
+
readonly operations: unknown;
|
|
62
|
+
readonly created_at: Date | string;
|
|
63
|
+
};
|
|
65
64
|
|
|
66
65
|
// PRAGMA result row types
|
|
67
66
|
type PragmaTableInfoRow = {
|
|
@@ -144,53 +143,16 @@ export class SqliteControlAdapter implements SqlControlAdapter<'sqlite'> {
|
|
|
144
143
|
driver: ControlDriverInstance<'sql', 'sqlite'>,
|
|
145
144
|
space: string,
|
|
146
145
|
): Promise<ContractMarkerRecord | null> {
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
driver.query(`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?`, [
|
|
151
|
-
'_prisma_marker',
|
|
152
|
-
]),
|
|
153
|
-
markerContext,
|
|
154
|
-
);
|
|
155
|
-
if (exists.rows.length === 0) {
|
|
156
|
-
return null;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const result = await withMarkerReadErrorHandling(
|
|
160
|
-
() =>
|
|
161
|
-
driver.query<{
|
|
162
|
-
core_hash: string;
|
|
163
|
-
profile_hash: string;
|
|
164
|
-
contract_json: unknown | null;
|
|
165
|
-
canonical_version: number | null;
|
|
166
|
-
updated_at: Date | string;
|
|
167
|
-
app_tag: string | null;
|
|
168
|
-
meta: unknown | null;
|
|
169
|
-
invariants: unknown;
|
|
170
|
-
}>(
|
|
171
|
-
`SELECT
|
|
172
|
-
core_hash,
|
|
173
|
-
profile_hash,
|
|
174
|
-
contract_json,
|
|
175
|
-
canonical_version,
|
|
176
|
-
updated_at,
|
|
177
|
-
app_tag,
|
|
178
|
-
meta,
|
|
179
|
-
invariants
|
|
180
|
-
FROM _prisma_marker
|
|
181
|
-
WHERE space = ?`,
|
|
182
|
-
[space],
|
|
183
|
-
),
|
|
184
|
-
markerContext,
|
|
185
|
-
);
|
|
146
|
+
const result = await this.readMarkerDiscriminated(driver, space);
|
|
147
|
+
return result.kind === 'present' ? result.record : null;
|
|
148
|
+
}
|
|
186
149
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
);
|
|
150
|
+
async readMarkerDiscriminated(
|
|
151
|
+
driver: ControlDriverInstance<'sql', 'sqlite'>,
|
|
152
|
+
space: string,
|
|
153
|
+
): Promise<MarkerReadResult> {
|
|
154
|
+
const markerContext = { space, markerLocation: SQLITE_MARKER_TABLE };
|
|
155
|
+
return withMarkerReadErrorHandling(() => this.readMarkerResult(driver, space), markerContext);
|
|
194
156
|
}
|
|
195
157
|
|
|
196
158
|
/**
|
|
@@ -202,48 +164,44 @@ export class SqliteControlAdapter implements SqlControlAdapter<'sqlite'> {
|
|
|
202
164
|
driver: ControlDriverInstance<'sql', 'sqlite'>,
|
|
203
165
|
): Promise<ReadonlyMap<string, ContractMarkerRecord>> {
|
|
204
166
|
const markerContext = { space: APP_SPACE_ID, markerLocation: SQLITE_MARKER_TABLE };
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
);
|
|
212
|
-
|
|
167
|
+
return withMarkerReadErrorHandling(() => this.readAllMarkersResult(driver), markerContext);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private async readAllMarkersResult(
|
|
171
|
+
driver: ControlDriverInstance<'sql', 'sqlite'>,
|
|
172
|
+
): Promise<ReadonlyMap<string, ContractMarkerRecord>> {
|
|
173
|
+
const lower = (query: AnyQueryAst) => this.lower(query, { contract: undefined });
|
|
174
|
+
const probe = sqliteCatalog
|
|
175
|
+
.select(sqliteCatalog.name)
|
|
176
|
+
.where(sqliteCatalog.type.eq('table').and(sqliteCatalog.name.eq('_prisma_marker')))
|
|
177
|
+
.build();
|
|
178
|
+
const exists = await execute(lower, driver, probe);
|
|
179
|
+
if (exists.length === 0) {
|
|
213
180
|
return new Map();
|
|
214
181
|
}
|
|
215
182
|
|
|
216
|
-
const
|
|
217
|
-
(
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
invariants
|
|
239
|
-
FROM _prisma_marker`,
|
|
240
|
-
),
|
|
241
|
-
markerContext,
|
|
242
|
-
);
|
|
243
|
-
|
|
244
|
-
const rows = new Map<string, ContractMarkerRecord>();
|
|
245
|
-
for (const row of result.rows) {
|
|
246
|
-
rows.set(
|
|
183
|
+
const fetch = marker
|
|
184
|
+
.select(
|
|
185
|
+
marker.space,
|
|
186
|
+
marker.core_hash,
|
|
187
|
+
marker.profile_hash,
|
|
188
|
+
marker.contract_json,
|
|
189
|
+
marker.canonical_version,
|
|
190
|
+
marker.updated_at,
|
|
191
|
+
marker.app_tag,
|
|
192
|
+
marker.meta,
|
|
193
|
+
marker.invariants,
|
|
194
|
+
)
|
|
195
|
+
.build();
|
|
196
|
+
const rawRows = await execute(lower, driver, fetch);
|
|
197
|
+
const rows = blindCast<
|
|
198
|
+
ReadonlyArray<{ space: string } & Record<string, unknown>>,
|
|
199
|
+
'Driver returns rows shaped by SELECT'
|
|
200
|
+
>(rawRows);
|
|
201
|
+
|
|
202
|
+
const out = new Map<string, ContractMarkerRecord>();
|
|
203
|
+
for (const row of rows) {
|
|
204
|
+
out.set(
|
|
247
205
|
row.space,
|
|
248
206
|
parseMarkerRowSafely(row, (raw) => parseContractMarkerRow(decodeSqliteMarkerRow(raw)), {
|
|
249
207
|
space: row.space,
|
|
@@ -251,7 +209,7 @@ export class SqliteControlAdapter implements SqlControlAdapter<'sqlite'> {
|
|
|
251
209
|
}),
|
|
252
210
|
);
|
|
253
211
|
}
|
|
254
|
-
return
|
|
212
|
+
return out;
|
|
255
213
|
}
|
|
256
214
|
|
|
257
215
|
/**
|
|
@@ -264,48 +222,39 @@ export class SqliteControlAdapter implements SqlControlAdapter<'sqlite'> {
|
|
|
264
222
|
space?: string,
|
|
265
223
|
): Promise<readonly LedgerEntryRecord[]> {
|
|
266
224
|
const ledgerContext = { space: space ?? '*', markerLocation: SQLITE_LEDGER_TABLE };
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
driver.query(`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?`, [
|
|
270
|
-
'_prisma_ledger',
|
|
271
|
-
]),
|
|
272
|
-
ledgerContext,
|
|
273
|
-
);
|
|
274
|
-
if (exists.rows.length === 0) {
|
|
275
|
-
return [];
|
|
276
|
-
}
|
|
225
|
+
return withMarkerReadErrorHandling(() => this.readLedgerResult(driver, space), ledgerContext);
|
|
226
|
+
}
|
|
277
227
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
migration_hash,
|
|
291
|
-
origin_core_hash,
|
|
292
|
-
destination_core_hash,
|
|
293
|
-
operations,
|
|
294
|
-
created_at
|
|
295
|
-
FROM _prisma_ledger`;
|
|
296
|
-
if (space !== undefined) {
|
|
297
|
-
sql += `
|
|
298
|
-
WHERE space = ?`;
|
|
228
|
+
private async readLedgerResult(
|
|
229
|
+
driver: ControlDriverInstance<'sql', 'sqlite'>,
|
|
230
|
+
space: string | undefined,
|
|
231
|
+
): Promise<readonly LedgerEntryRecord[]> {
|
|
232
|
+
const lower = (query: AnyQueryAst) => this.lower(query, { contract: undefined });
|
|
233
|
+
const probe = sqliteCatalog
|
|
234
|
+
.select(sqliteCatalog.name)
|
|
235
|
+
.where(sqliteCatalog.type.eq('table').and(sqliteCatalog.name.eq('_prisma_ledger')))
|
|
236
|
+
.build();
|
|
237
|
+
const exists = await execute(lower, driver, probe);
|
|
238
|
+
if (exists.length === 0) {
|
|
239
|
+
return [];
|
|
299
240
|
}
|
|
300
|
-
sql += `
|
|
301
|
-
ORDER BY id`;
|
|
302
241
|
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
242
|
+
const base = ledgerReadShape.select(
|
|
243
|
+
ledgerReadShape.space,
|
|
244
|
+
ledgerReadShape.migration_name,
|
|
245
|
+
ledgerReadShape.migration_hash,
|
|
246
|
+
ledgerReadShape.origin_core_hash,
|
|
247
|
+
ledgerReadShape.destination_core_hash,
|
|
248
|
+
ledgerReadShape.operations,
|
|
249
|
+
ledgerReadShape.created_at,
|
|
250
|
+
);
|
|
251
|
+
const filtered = space !== undefined ? base.where(ledgerReadShape.space.eq(space)) : base;
|
|
252
|
+
const rawRows = await execute(lower, driver, filtered.orderBy(ledgerReadShape.id).build());
|
|
253
|
+
const rows = blindCast<readonly SqliteLedgerRow[], 'Driver returns rows shaped by SELECT'>(
|
|
254
|
+
rawRows,
|
|
306
255
|
);
|
|
307
256
|
|
|
308
|
-
return
|
|
257
|
+
return rows.map((row) => ({
|
|
309
258
|
space: row.space,
|
|
310
259
|
migrationName: row.migration_name,
|
|
311
260
|
migrationHash: row.migration_hash,
|
|
@@ -316,6 +265,180 @@ export class SqliteControlAdapter implements SqlControlAdapter<'sqlite'> {
|
|
|
316
265
|
}));
|
|
317
266
|
}
|
|
318
267
|
|
|
268
|
+
/**
|
|
269
|
+
* Stamps the initial marker row for `space` via the shared contract-free DML
|
|
270
|
+
* builder, lowered through {@link lower} and executed on the driver. See the
|
|
271
|
+
* `SqlControlAdapter.initMarker` contract.
|
|
272
|
+
*/
|
|
273
|
+
async insertMarker(
|
|
274
|
+
driver: ControlDriverInstance<'sql', 'sqlite'>,
|
|
275
|
+
space: string,
|
|
276
|
+
destination: {
|
|
277
|
+
readonly storageHash: string;
|
|
278
|
+
readonly profileHash: string;
|
|
279
|
+
readonly invariants?: readonly string[];
|
|
280
|
+
},
|
|
281
|
+
): Promise<void> {
|
|
282
|
+
await execute(
|
|
283
|
+
(query) => this.lower(query, { contract: undefined }),
|
|
284
|
+
driver,
|
|
285
|
+
marker
|
|
286
|
+
.insert({
|
|
287
|
+
space,
|
|
288
|
+
core_hash: destination.storageHash,
|
|
289
|
+
profile_hash: destination.profileHash,
|
|
290
|
+
contract_json: null,
|
|
291
|
+
canonical_version: null,
|
|
292
|
+
updated_at: NOW,
|
|
293
|
+
app_tag: null,
|
|
294
|
+
meta: {},
|
|
295
|
+
invariants: destination.invariants ?? [],
|
|
296
|
+
})
|
|
297
|
+
.build(),
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async initMarker(
|
|
302
|
+
driver: ControlDriverInstance<'sql', 'sqlite'>,
|
|
303
|
+
space: string,
|
|
304
|
+
destination: {
|
|
305
|
+
readonly storageHash: string;
|
|
306
|
+
readonly profileHash: string;
|
|
307
|
+
readonly invariants?: readonly string[];
|
|
308
|
+
},
|
|
309
|
+
): Promise<void> {
|
|
310
|
+
await execute(
|
|
311
|
+
(query) => this.lower(query, { contract: undefined }),
|
|
312
|
+
driver,
|
|
313
|
+
marker
|
|
314
|
+
.upsert({
|
|
315
|
+
space,
|
|
316
|
+
core_hash: destination.storageHash,
|
|
317
|
+
profile_hash: destination.profileHash,
|
|
318
|
+
contract_json: null,
|
|
319
|
+
canonical_version: null,
|
|
320
|
+
updated_at: NOW,
|
|
321
|
+
app_tag: null,
|
|
322
|
+
meta: {},
|
|
323
|
+
invariants: destination.invariants ?? [],
|
|
324
|
+
})
|
|
325
|
+
.onConflict(marker.space)
|
|
326
|
+
.doUpdate((excluded) => ({
|
|
327
|
+
core_hash: excluded.core_hash,
|
|
328
|
+
profile_hash: excluded.profile_hash,
|
|
329
|
+
contract_json: excluded.contract_json,
|
|
330
|
+
canonical_version: excluded.canonical_version,
|
|
331
|
+
updated_at: NOW,
|
|
332
|
+
app_tag: excluded.app_tag,
|
|
333
|
+
meta: excluded.meta,
|
|
334
|
+
invariants: excluded.invariants,
|
|
335
|
+
}))
|
|
336
|
+
.build(),
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Compare-and-swap advance of the marker row for `space`. See the
|
|
342
|
+
* `SqlControlAdapter.updateMarker` contract.
|
|
343
|
+
*/
|
|
344
|
+
async updateMarker(
|
|
345
|
+
driver: ControlDriverInstance<'sql', 'sqlite'>,
|
|
346
|
+
space: string,
|
|
347
|
+
expectedFrom: string,
|
|
348
|
+
destination: {
|
|
349
|
+
readonly storageHash: string;
|
|
350
|
+
readonly profileHash: string;
|
|
351
|
+
readonly invariants?: readonly string[];
|
|
352
|
+
},
|
|
353
|
+
): Promise<boolean> {
|
|
354
|
+
const currentInvariants =
|
|
355
|
+
destination.invariants === undefined
|
|
356
|
+
? []
|
|
357
|
+
: ((await this.readMarker(driver, space))?.invariants ?? []);
|
|
358
|
+
const mergedInvariants =
|
|
359
|
+
destination.invariants === undefined
|
|
360
|
+
? undefined
|
|
361
|
+
: mergeInvariants(currentInvariants, destination.invariants);
|
|
362
|
+
|
|
363
|
+
const query = marker
|
|
364
|
+
.update()
|
|
365
|
+
.set({
|
|
366
|
+
core_hash: destination.storageHash,
|
|
367
|
+
profile_hash: destination.profileHash,
|
|
368
|
+
updated_at: NOW,
|
|
369
|
+
...(mergedInvariants !== undefined ? { invariants: mergedInvariants } : {}),
|
|
370
|
+
})
|
|
371
|
+
.where(marker.space.eq(space).and(marker.core_hash.eq(expectedFrom)))
|
|
372
|
+
.returning(marker.space)
|
|
373
|
+
.build();
|
|
374
|
+
|
|
375
|
+
const rows = await execute((q) => this.lower(q, { contract: undefined }), driver, query);
|
|
376
|
+
return rows.length > 0;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Appends a ledger entry for `space`. See the
|
|
381
|
+
* `SqlControlAdapter.writeLedgerEntry` contract.
|
|
382
|
+
*/
|
|
383
|
+
async writeLedgerEntry(
|
|
384
|
+
driver: ControlDriverInstance<'sql', 'sqlite'>,
|
|
385
|
+
space: string,
|
|
386
|
+
entry: {
|
|
387
|
+
readonly edgeId: string;
|
|
388
|
+
readonly from: string;
|
|
389
|
+
readonly to: string;
|
|
390
|
+
readonly migrationName: string;
|
|
391
|
+
readonly migrationHash: string;
|
|
392
|
+
readonly operations: readonly unknown[];
|
|
393
|
+
},
|
|
394
|
+
): Promise<void> {
|
|
395
|
+
await execute(
|
|
396
|
+
(query) => this.lower(query, { contract: undefined }),
|
|
397
|
+
driver,
|
|
398
|
+
ledger
|
|
399
|
+
.insert({
|
|
400
|
+
space,
|
|
401
|
+
migration_name: entry.migrationName,
|
|
402
|
+
migration_hash: entry.migrationHash,
|
|
403
|
+
origin_core_hash: entry.from,
|
|
404
|
+
destination_core_hash: entry.to,
|
|
405
|
+
operations: entry.operations,
|
|
406
|
+
})
|
|
407
|
+
.build(),
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
private async readMarkerResult(driver: ControlDriverInstance<'sql', 'sqlite'>, space: string) {
|
|
412
|
+
const lower = (query: AnyQueryAst) => this.lower(query, { contract: undefined });
|
|
413
|
+
const probe = sqliteCatalog
|
|
414
|
+
.select(sqliteCatalog.name)
|
|
415
|
+
.where(sqliteCatalog.type.eq('table').and(sqliteCatalog.name.eq('_prisma_marker')))
|
|
416
|
+
.build();
|
|
417
|
+
const exists = await execute(lower, driver, probe);
|
|
418
|
+
if (exists.length === 0) return { kind: 'no-table' as const };
|
|
419
|
+
|
|
420
|
+
const fetch = marker
|
|
421
|
+
.select(
|
|
422
|
+
marker.core_hash,
|
|
423
|
+
marker.profile_hash,
|
|
424
|
+
marker.contract_json,
|
|
425
|
+
marker.canonical_version,
|
|
426
|
+
marker.updated_at,
|
|
427
|
+
marker.app_tag,
|
|
428
|
+
marker.meta,
|
|
429
|
+
marker.invariants,
|
|
430
|
+
)
|
|
431
|
+
.where(marker.space.eq(space))
|
|
432
|
+
.build();
|
|
433
|
+
const result = await execute(lower, driver, fetch);
|
|
434
|
+
const row = result[0];
|
|
435
|
+
if (!row) return { kind: 'absent' as const };
|
|
436
|
+
return {
|
|
437
|
+
kind: 'present' as const,
|
|
438
|
+
record: parseContractMarkerRow(decodeSqliteMarkerRow(row)),
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
319
442
|
async introspect(
|
|
320
443
|
driver: ControlDriverInstance<'sql', 'sqlite'>,
|
|
321
444
|
_contract?: unknown,
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { ControlDriverInstance } from '@prisma-next/framework-components/control';
|
|
2
|
+
import {
|
|
3
|
+
type AnyQueryAst,
|
|
4
|
+
type LoweredStatement,
|
|
5
|
+
RawExpr,
|
|
6
|
+
} from '@prisma-next/sql-relational-core/ast';
|
|
7
|
+
import {
|
|
8
|
+
createAstCodecRegistry,
|
|
9
|
+
deriveParamMetadata,
|
|
10
|
+
encodeParamsWithMetadata,
|
|
11
|
+
} from '@prisma-next/sql-runtime';
|
|
12
|
+
import { SQLITE_DATETIME_CODEC_ID } from '@prisma-next/target-sqlite/codec-ids';
|
|
13
|
+
import { sqliteCodecRegistry } from '@prisma-next/target-sqlite/codecs';
|
|
14
|
+
import {
|
|
15
|
+
datetime,
|
|
16
|
+
integer,
|
|
17
|
+
jsonText,
|
|
18
|
+
sqliteTable,
|
|
19
|
+
text,
|
|
20
|
+
} from '@prisma-next/target-sqlite/contract-free';
|
|
21
|
+
|
|
22
|
+
const CONTROL_CODECS = createAstCodecRegistry(sqliteCodecRegistry);
|
|
23
|
+
|
|
24
|
+
export const marker = sqliteTable('_prisma_marker', {
|
|
25
|
+
space: text(),
|
|
26
|
+
core_hash: text(),
|
|
27
|
+
profile_hash: text(),
|
|
28
|
+
contract_json: jsonText({ nullable: true }),
|
|
29
|
+
canonical_version: integer({ nullable: true }),
|
|
30
|
+
updated_at: datetime(),
|
|
31
|
+
app_tag: text({ nullable: true }),
|
|
32
|
+
meta: jsonText({ nullable: true }),
|
|
33
|
+
invariants: jsonText(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Writeable subset of `_prisma_ledger`. Omits the DB-generated `id`
|
|
38
|
+
* (`INTEGER PRIMARY KEY AUTOINCREMENT`) and `created_at` (default
|
|
39
|
+
* `strftime(...)`).
|
|
40
|
+
*/
|
|
41
|
+
export const ledger = sqliteTable('_prisma_ledger', {
|
|
42
|
+
space: text(),
|
|
43
|
+
migration_name: text(),
|
|
44
|
+
migration_hash: text(),
|
|
45
|
+
origin_core_hash: text({ nullable: true }),
|
|
46
|
+
destination_core_hash: text(),
|
|
47
|
+
operations: jsonText(),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Read-side handle covering every column of `_prisma_ledger`, including
|
|
52
|
+
* the DB-generated `id` (for ORDER BY) and `created_at`.
|
|
53
|
+
*/
|
|
54
|
+
export const ledgerReadShape = sqliteTable('_prisma_ledger', {
|
|
55
|
+
id: integer(),
|
|
56
|
+
space: text(),
|
|
57
|
+
migration_name: text(),
|
|
58
|
+
migration_hash: text(),
|
|
59
|
+
origin_core_hash: text({ nullable: true }),
|
|
60
|
+
destination_core_hash: text(),
|
|
61
|
+
operations: jsonText(),
|
|
62
|
+
created_at: text(),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export const sqliteCatalog = sqliteTable('sqlite_master', { type: text(), name: text() });
|
|
66
|
+
|
|
67
|
+
export const NOW = new RawExpr({
|
|
68
|
+
parts: ["datetime('now')"],
|
|
69
|
+
returns: { codecId: SQLITE_DATETIME_CODEC_ID, nullable: false },
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
type Lower = (query: AnyQueryAst) => LoweredStatement;
|
|
73
|
+
|
|
74
|
+
type MarkerDriver = {
|
|
75
|
+
query<Row = Record<string, unknown>>(
|
|
76
|
+
sql: string,
|
|
77
|
+
params?: readonly unknown[],
|
|
78
|
+
): Promise<{ readonly rows: ReadonlyArray<Row> }>;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export function mergeInvariants(
|
|
82
|
+
current: readonly string[],
|
|
83
|
+
incoming: readonly string[],
|
|
84
|
+
): readonly string[] {
|
|
85
|
+
return [...new Set([...current, ...incoming])].sort();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function execute(
|
|
89
|
+
lower: Lower,
|
|
90
|
+
driver: MarkerDriver,
|
|
91
|
+
query: AnyQueryAst,
|
|
92
|
+
): Promise<readonly Record<string, unknown>[]> {
|
|
93
|
+
const lowered = lower(query);
|
|
94
|
+
const values = lowered.params.map((slot) => {
|
|
95
|
+
if (slot.kind === 'literal') return slot.value;
|
|
96
|
+
throw new Error('SQLite control DML lowered to a bind parameter, which is unsupported');
|
|
97
|
+
});
|
|
98
|
+
const encoded = await encodeParamsWithMetadata(
|
|
99
|
+
values,
|
|
100
|
+
deriveParamMetadata(query),
|
|
101
|
+
{},
|
|
102
|
+
CONTROL_CODECS,
|
|
103
|
+
);
|
|
104
|
+
const result = await driver.query(lowered.sql, encoded);
|
|
105
|
+
return result.rows;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function decodeSqliteMarkerRow(row: unknown): unknown {
|
|
109
|
+
if (typeof row !== 'object' || row === null || !('invariants' in row)) {
|
|
110
|
+
return row;
|
|
111
|
+
}
|
|
112
|
+
const record = row as { invariants: unknown };
|
|
113
|
+
if (typeof record.invariants !== 'string') return row;
|
|
114
|
+
let parsed: unknown;
|
|
115
|
+
try {
|
|
116
|
+
parsed = JSON.parse(record.invariants);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
119
|
+
throw new Error(`Invalid contract marker row: invariants is not valid JSON: ${detail}`);
|
|
120
|
+
}
|
|
121
|
+
return { ...record, invariants: parsed };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export type SqliteMarkerWriteDriver = ControlDriverInstance<'sql', 'sqlite'>;
|