@prisma-next/family-sql 0.3.0-dev.4 → 0.3.0-dev.40

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 (74) hide show
  1. package/README.md +11 -6
  2. package/dist/assembly-BVS641kd.mjs +106 -0
  3. package/dist/assembly-BVS641kd.mjs.map +1 -0
  4. package/dist/control-adapter.d.mts +60 -0
  5. package/dist/control-adapter.d.mts.map +1 -0
  6. package/dist/control-adapter.mjs +1 -0
  7. package/dist/control-instance-Cvmn5zpn.d.mts +292 -0
  8. package/dist/control-instance-Cvmn5zpn.d.mts.map +1 -0
  9. package/dist/control.d.mts +64 -0
  10. package/dist/control.d.mts.map +1 -0
  11. package/dist/control.mjs +534 -0
  12. package/dist/control.mjs.map +1 -0
  13. package/dist/runtime.d.mts +27 -0
  14. package/dist/runtime.d.mts.map +1 -0
  15. package/dist/runtime.mjs +38 -0
  16. package/dist/runtime.mjs.map +1 -0
  17. package/dist/schema-verify.d.mts +48 -0
  18. package/dist/schema-verify.d.mts.map +1 -0
  19. package/dist/schema-verify.mjs +4 -0
  20. package/dist/test-utils.d.mts +2 -0
  21. package/dist/test-utils.mjs +3 -0
  22. package/dist/verify-BfMETJcM.mjs +108 -0
  23. package/dist/verify-BfMETJcM.mjs.map +1 -0
  24. package/dist/verify-sql-schema-B4T5MEuz.mjs +934 -0
  25. package/dist/verify-sql-schema-B4T5MEuz.mjs.map +1 -0
  26. package/dist/verify-sql-schema-CvQoGm2Q.d.mts +67 -0
  27. package/dist/verify-sql-schema-CvQoGm2Q.d.mts.map +1 -0
  28. package/dist/{exports/verify.d.ts → verify.d.mts} +8 -5
  29. package/dist/verify.d.mts.map +1 -0
  30. package/dist/verify.mjs +3 -0
  31. package/package.json +38 -48
  32. package/src/core/assembly.ts +216 -0
  33. package/src/core/control-adapter.ts +67 -0
  34. package/src/core/control-descriptor.ts +37 -0
  35. package/src/core/control-instance.ts +750 -0
  36. package/src/core/migrations/plan-helpers.ts +164 -0
  37. package/src/core/migrations/policies.ts +8 -0
  38. package/src/core/migrations/types.ts +279 -0
  39. package/src/core/runtime-descriptor.ts +23 -0
  40. package/src/core/runtime-instance.ts +22 -0
  41. package/src/core/schema-verify/verify-helpers.ts +532 -0
  42. package/src/core/schema-verify/verify-sql-schema.ts +1011 -0
  43. package/src/core/verify.ts +168 -0
  44. package/src/exports/control-adapter.ts +1 -0
  45. package/src/exports/control.ts +59 -0
  46. package/src/exports/runtime.ts +3 -0
  47. package/src/exports/schema-verify.ts +19 -0
  48. package/src/exports/test-utils.ts +10 -0
  49. package/src/exports/verify.ts +1 -0
  50. package/dist/exports/chunk-6P44BVZ4.js +0 -580
  51. package/dist/exports/chunk-6P44BVZ4.js.map +0 -1
  52. package/dist/exports/chunk-C3GKWCKA.js +0 -96
  53. package/dist/exports/chunk-C3GKWCKA.js.map +0 -1
  54. package/dist/exports/chunk-F252JMEU.js +0 -772
  55. package/dist/exports/chunk-F252JMEU.js.map +0 -1
  56. package/dist/exports/control-adapter.d.ts +0 -44
  57. package/dist/exports/control-adapter.js +0 -1
  58. package/dist/exports/control-adapter.js.map +0 -1
  59. package/dist/exports/control.d.ts +0 -75
  60. package/dist/exports/control.js +0 -149
  61. package/dist/exports/control.js.map +0 -1
  62. package/dist/exports/instance-DiZi2k_2.d.ts +0 -127
  63. package/dist/exports/runtime.d.ts +0 -66
  64. package/dist/exports/runtime.js +0 -64
  65. package/dist/exports/runtime.js.map +0 -1
  66. package/dist/exports/schema-verify.d.ts +0 -75
  67. package/dist/exports/schema-verify.js +0 -11
  68. package/dist/exports/schema-verify.js.map +0 -1
  69. package/dist/exports/test-utils.d.ts +0 -33
  70. package/dist/exports/test-utils.js +0 -17
  71. package/dist/exports/test-utils.js.map +0 -1
  72. package/dist/exports/types-Bh7ftf0Q.d.ts +0 -275
  73. package/dist/exports/verify.js +0 -11
  74. package/dist/exports/verify.js.map +0 -1
@@ -0,0 +1,750 @@
1
+ import type {
2
+ TargetBoundComponentDescriptor,
3
+ TargetDescriptor,
4
+ } from '@prisma-next/contract/framework-components';
5
+ import type { ContractIR } from '@prisma-next/contract/ir';
6
+ import type { ContractMarkerRecord, TypesImportSpec } from '@prisma-next/contract/types';
7
+ import { emit } from '@prisma-next/core-control-plane/emission';
8
+ import type { CoreSchemaView, SchemaTreeNode } from '@prisma-next/core-control-plane/schema-view';
9
+ import type {
10
+ ControlDriverInstance,
11
+ ControlFamilyInstance,
12
+ EmitContractResult,
13
+ OperationContext,
14
+ SignDatabaseResult,
15
+ VerifyDatabaseResult,
16
+ VerifyDatabaseSchemaResult,
17
+ } from '@prisma-next/core-control-plane/types';
18
+ import type { OperationRegistry } from '@prisma-next/operations';
19
+ import type { SqlContract, SqlStorage } from '@prisma-next/sql-contract/types';
20
+ import { validateContract } from '@prisma-next/sql-contract/validate';
21
+ import { sqlTargetFamilyHook } from '@prisma-next/sql-contract-emitter';
22
+ import {
23
+ ensureSchemaStatement,
24
+ ensureTableStatement,
25
+ writeContractMarker,
26
+ } from '@prisma-next/sql-runtime';
27
+ import type { SqlSchemaIR, SqlTableIR } from '@prisma-next/sql-schema-ir/types';
28
+ import { ifDefined } from '@prisma-next/utils/defined';
29
+ import {
30
+ assembleOperationRegistry,
31
+ extractCodecTypeImports,
32
+ extractExtensionIds,
33
+ extractOperationTypeImports,
34
+ extractParameterizedRenderers,
35
+ extractParameterizedTypeImports,
36
+ type SqlControlDescriptorWithContributions,
37
+ } from './assembly';
38
+ import type { SqlControlAdapter } from './control-adapter';
39
+ import type {
40
+ SqlControlAdapterDescriptor,
41
+ SqlControlExtensionDescriptor,
42
+ } from './migrations/types';
43
+ import { verifySqlSchema } from './schema-verify/verify-sql-schema';
44
+ import { collectSupportedCodecTypeIds, readMarker } from './verify';
45
+
46
+ function extractCodecTypeIdsFromContract(contract: unknown): readonly string[] {
47
+ const typeIds = new Set<string>();
48
+
49
+ // Type guard for SQL contract structure
50
+ if (
51
+ typeof contract === 'object' &&
52
+ contract !== null &&
53
+ 'storage' in contract &&
54
+ typeof contract.storage === 'object' &&
55
+ contract.storage !== null &&
56
+ 'tables' in contract.storage
57
+ ) {
58
+ const storage = contract.storage as { tables?: Record<string, unknown> };
59
+ if (storage.tables && typeof storage.tables === 'object') {
60
+ for (const table of Object.values(storage.tables)) {
61
+ if (
62
+ typeof table === 'object' &&
63
+ table !== null &&
64
+ 'columns' in table &&
65
+ typeof table.columns === 'object' &&
66
+ table.columns !== null
67
+ ) {
68
+ const columns = table.columns as Record<string, { codecId: string } | undefined>;
69
+ for (const column of Object.values(columns)) {
70
+ if (
71
+ column &&
72
+ typeof column === 'object' &&
73
+ 'codecId' in column &&
74
+ typeof column.codecId === 'string'
75
+ ) {
76
+ typeIds.add(column.codecId);
77
+ }
78
+ }
79
+ }
80
+ }
81
+ }
82
+ }
83
+
84
+ return Array.from(typeIds).sort();
85
+ }
86
+
87
+ function createVerifyResult(options: {
88
+ ok: boolean;
89
+ code?: string;
90
+ summary: string;
91
+ contractStorageHash: string;
92
+ contractProfileHash?: string;
93
+ marker?: ContractMarkerRecord;
94
+ expectedTargetId: string;
95
+ actualTargetId?: string;
96
+ missingCodecs?: readonly string[];
97
+ codecCoverageSkipped?: boolean;
98
+ configPath?: string;
99
+ contractPath: string;
100
+ totalTime: number;
101
+ }): VerifyDatabaseResult {
102
+ const contract: { storageHash: string; profileHash?: string } = {
103
+ storageHash: options.contractStorageHash,
104
+ };
105
+ if (options.contractProfileHash) {
106
+ contract.profileHash = options.contractProfileHash;
107
+ }
108
+
109
+ const target: { expected: string; actual?: string } = {
110
+ expected: options.expectedTargetId,
111
+ };
112
+ if (options.actualTargetId) {
113
+ target.actual = options.actualTargetId;
114
+ }
115
+
116
+ const meta: { contractPath: string; configPath?: string } = {
117
+ contractPath: options.contractPath,
118
+ };
119
+ if (options.configPath) {
120
+ meta.configPath = options.configPath;
121
+ }
122
+
123
+ const result: VerifyDatabaseResult = {
124
+ ok: options.ok,
125
+ summary: options.summary,
126
+ contract,
127
+ target,
128
+ meta,
129
+ timings: {
130
+ total: options.totalTime,
131
+ },
132
+ };
133
+
134
+ if (options.code) {
135
+ (result as { code?: string }).code = options.code;
136
+ }
137
+
138
+ if (options.marker) {
139
+ (result as { marker?: { storageHash: string; profileHash: string } }).marker = {
140
+ storageHash: options.marker.storageHash,
141
+ profileHash: options.marker.profileHash,
142
+ };
143
+ }
144
+
145
+ if (options.missingCodecs) {
146
+ (result as { missingCodecs?: readonly string[] }).missingCodecs = options.missingCodecs;
147
+ }
148
+
149
+ if (options.codecCoverageSkipped) {
150
+ (result as { codecCoverageSkipped?: boolean }).codecCoverageSkipped =
151
+ options.codecCoverageSkipped;
152
+ }
153
+
154
+ return result;
155
+ }
156
+
157
+ interface SqlTypeMetadata {
158
+ readonly typeId: string;
159
+ readonly familyId: 'sql';
160
+ readonly targetId: string;
161
+ readonly nativeType?: string;
162
+ }
163
+
164
+ type SqlTypeMetadataRegistry = Map<string, SqlTypeMetadata>;
165
+
166
+ interface SqlFamilyInstanceState {
167
+ readonly operationRegistry: OperationRegistry;
168
+ readonly codecTypeImports: ReadonlyArray<TypesImportSpec>;
169
+ readonly operationTypeImports: ReadonlyArray<TypesImportSpec>;
170
+ readonly extensionIds: ReadonlyArray<string>;
171
+ readonly typeMetadataRegistry: SqlTypeMetadataRegistry;
172
+ }
173
+
174
+ export interface SchemaVerifyOptions {
175
+ readonly driver: ControlDriverInstance<'sql', string>;
176
+ readonly contractIR: unknown;
177
+ readonly strict: boolean;
178
+ readonly context?: OperationContext;
179
+ /**
180
+ * Active framework components participating in this composition.
181
+ * All components must have matching familyId ('sql') and targetId.
182
+ */
183
+ readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
184
+ }
185
+
186
+ export interface SqlControlFamilyInstance
187
+ extends ControlFamilyInstance<'sql'>,
188
+ SqlFamilyInstanceState {
189
+ validateContractIR(contractJson: unknown): ContractIR;
190
+
191
+ verify(options: {
192
+ readonly driver: ControlDriverInstance<'sql', string>;
193
+ readonly contractIR: unknown;
194
+ readonly expectedTargetId: string;
195
+ readonly contractPath: string;
196
+ readonly configPath?: string;
197
+ }): Promise<VerifyDatabaseResult>;
198
+
199
+ schemaVerify(options: SchemaVerifyOptions): Promise<VerifyDatabaseSchemaResult>;
200
+
201
+ sign(options: {
202
+ readonly driver: ControlDriverInstance<'sql', string>;
203
+ readonly contractIR: unknown;
204
+ readonly contractPath: string;
205
+ readonly configPath?: string;
206
+ }): Promise<SignDatabaseResult>;
207
+
208
+ introspect(options: {
209
+ readonly driver: ControlDriverInstance<'sql', string>;
210
+ readonly contractIR?: unknown;
211
+ }): Promise<SqlSchemaIR>;
212
+
213
+ toSchemaView(schema: SqlSchemaIR): CoreSchemaView;
214
+
215
+ emitContract(options: { readonly contractIR: ContractIR | unknown }): Promise<EmitContractResult>;
216
+ }
217
+
218
+ export type SqlFamilyInstance = SqlControlFamilyInstance;
219
+
220
+ interface CreateSqlFamilyInstanceOptions<TTargetId extends string> {
221
+ readonly target: TargetDescriptor<'sql', TTargetId> &
222
+ SqlControlDescriptorWithContributions &
223
+ DescriptorWithStorageTypes;
224
+ readonly adapter: SqlControlAdapterDescriptor<TTargetId> & DescriptorWithStorageTypes;
225
+ readonly extensionPacks: readonly (SqlControlExtensionDescriptor<TTargetId> &
226
+ DescriptorWithStorageTypes)[];
227
+ }
228
+
229
+ function isSqlControlAdapter<TTargetId extends string>(
230
+ value: unknown,
231
+ ): value is SqlControlAdapter<TTargetId> {
232
+ return (
233
+ typeof value === 'object' &&
234
+ value !== null &&
235
+ 'introspect' in value &&
236
+ typeof (value as { introspect: unknown }).introspect === 'function'
237
+ );
238
+ }
239
+
240
+ interface DescriptorWithStorageTypes {
241
+ readonly targetId?: string | undefined;
242
+ readonly types?:
243
+ | {
244
+ readonly storage?:
245
+ | ReadonlyArray<{
246
+ readonly typeId: string;
247
+ readonly familyId: string;
248
+ readonly targetId: string;
249
+ readonly nativeType?: string | undefined;
250
+ }>
251
+ | undefined;
252
+ }
253
+ | undefined;
254
+ }
255
+
256
+ function buildSqlTypeMetadataRegistry(options: {
257
+ readonly target: DescriptorWithStorageTypes;
258
+ readonly adapter: DescriptorWithStorageTypes & { readonly targetId: string };
259
+ readonly extensionPacks: readonly DescriptorWithStorageTypes[];
260
+ }): SqlTypeMetadataRegistry {
261
+ const { target, adapter, extensionPacks: extensions } = options;
262
+ const registry = new Map<string, SqlTypeMetadata>();
263
+ const targetId = adapter.targetId;
264
+ const descriptors = [target, adapter, ...extensions];
265
+
266
+ for (const descriptor of descriptors) {
267
+ const types = descriptor.types;
268
+ const storageTypes = types?.storage;
269
+
270
+ if (!storageTypes) {
271
+ continue;
272
+ }
273
+
274
+ for (const storageType of storageTypes) {
275
+ if (storageType.familyId === 'sql' && storageType.targetId === targetId) {
276
+ registry.set(storageType.typeId, {
277
+ typeId: storageType.typeId,
278
+ familyId: 'sql',
279
+ targetId: storageType.targetId,
280
+ ...(storageType.nativeType !== undefined ? { nativeType: storageType.nativeType } : {}),
281
+ });
282
+ }
283
+ }
284
+ }
285
+
286
+ return registry;
287
+ }
288
+
289
+ export function createSqlFamilyInstance<TTargetId extends string>(
290
+ options: CreateSqlFamilyInstanceOptions<TTargetId>,
291
+ ): SqlFamilyInstance {
292
+ const { target, adapter, extensionPacks: extensions = [] } = options;
293
+
294
+ const descriptors: SqlControlDescriptorWithContributions[] = [target, adapter, ...extensions];
295
+
296
+ const operationRegistry = assembleOperationRegistry(descriptors);
297
+ const codecTypeImports = extractCodecTypeImports(descriptors);
298
+ const operationTypeImports = extractOperationTypeImports(descriptors);
299
+ const extensionIds = extractExtensionIds(adapter, target, extensions);
300
+ const parameterizedRenderers = extractParameterizedRenderers(descriptors);
301
+ const parameterizedTypeImports = extractParameterizedTypeImports(descriptors);
302
+
303
+ const typeMetadataRegistry = buildSqlTypeMetadataRegistry({
304
+ target,
305
+ adapter,
306
+ extensionPacks: extensions,
307
+ });
308
+
309
+ function stripMappings(contract: unknown): unknown {
310
+ if (typeof contract === 'object' && contract !== null && 'mappings' in contract) {
311
+ const { mappings: _mappings, ...contractIR } = contract as {
312
+ mappings?: unknown;
313
+ [key: string]: unknown;
314
+ };
315
+ return contractIR;
316
+ }
317
+ return contract;
318
+ }
319
+
320
+ return {
321
+ familyId: 'sql',
322
+ operationRegistry,
323
+ codecTypeImports,
324
+ operationTypeImports,
325
+ extensionIds,
326
+ typeMetadataRegistry,
327
+
328
+ validateContractIR(contractJson: unknown): ContractIR {
329
+ const validated = validateContract<SqlContract<SqlStorage>>(contractJson);
330
+ const { mappings: _mappings, ...contractIR } = validated;
331
+ return contractIR as ContractIR;
332
+ },
333
+
334
+ async verify(verifyOptions: {
335
+ readonly driver: ControlDriverInstance<'sql', string>;
336
+ readonly contractIR: unknown;
337
+ readonly expectedTargetId: string;
338
+ readonly contractPath: string;
339
+ readonly configPath?: string;
340
+ }): Promise<VerifyDatabaseResult> {
341
+ const { driver, contractIR, expectedTargetId, contractPath, configPath } = verifyOptions;
342
+ const startTime = Date.now();
343
+
344
+ if (
345
+ typeof contractIR !== 'object' ||
346
+ contractIR === null ||
347
+ !('storageHash' in contractIR) ||
348
+ !('target' in contractIR) ||
349
+ typeof contractIR.storageHash !== 'string' ||
350
+ typeof contractIR.target !== 'string'
351
+ ) {
352
+ throw new Error('Contract is missing required fields: storageHash or target');
353
+ }
354
+
355
+ const contractStorageHash = contractIR.storageHash;
356
+ const contractProfileHash =
357
+ 'profileHash' in contractIR && typeof contractIR.profileHash === 'string'
358
+ ? contractIR.profileHash
359
+ : undefined;
360
+ const contractTarget = contractIR.target;
361
+
362
+ const marker = await readMarker(driver);
363
+
364
+ let missingCodecs: readonly string[] | undefined;
365
+ let codecCoverageSkipped = false;
366
+ const supportedTypeIds = collectSupportedCodecTypeIds([adapter, target, ...extensions]);
367
+ if (supportedTypeIds.length === 0) {
368
+ codecCoverageSkipped = true;
369
+ } else {
370
+ const supportedSet = new Set(supportedTypeIds);
371
+ const usedTypeIds = extractCodecTypeIdsFromContract(contractIR);
372
+ const missing = usedTypeIds.filter((id) => !supportedSet.has(id));
373
+ if (missing.length > 0) {
374
+ missingCodecs = missing;
375
+ }
376
+ }
377
+
378
+ if (!marker) {
379
+ const totalTime = Date.now() - startTime;
380
+ return createVerifyResult({
381
+ ok: false,
382
+ code: 'PN-RTM-3001',
383
+ summary: 'Marker missing',
384
+ contractStorageHash,
385
+ expectedTargetId,
386
+ contractPath,
387
+ totalTime,
388
+ ...(contractProfileHash ? { contractProfileHash } : {}),
389
+ ...(missingCodecs ? { missingCodecs } : {}),
390
+ ...(codecCoverageSkipped ? { codecCoverageSkipped } : {}),
391
+ ...(configPath ? { configPath } : {}),
392
+ });
393
+ }
394
+
395
+ if (contractTarget !== expectedTargetId) {
396
+ const totalTime = Date.now() - startTime;
397
+ return createVerifyResult({
398
+ ok: false,
399
+ code: 'PN-RTM-3003',
400
+ summary: 'Target mismatch',
401
+ contractStorageHash,
402
+ marker,
403
+ expectedTargetId,
404
+ actualTargetId: contractTarget,
405
+ contractPath,
406
+ totalTime,
407
+ ...(contractProfileHash ? { contractProfileHash } : {}),
408
+ ...(missingCodecs ? { missingCodecs } : {}),
409
+ ...(codecCoverageSkipped ? { codecCoverageSkipped } : {}),
410
+ ...(configPath ? { configPath } : {}),
411
+ });
412
+ }
413
+
414
+ if (marker.storageHash !== contractStorageHash) {
415
+ const totalTime = Date.now() - startTime;
416
+ return createVerifyResult({
417
+ ok: false,
418
+ code: 'PN-RTM-3002',
419
+ summary: 'Hash mismatch',
420
+ contractStorageHash,
421
+ marker,
422
+ expectedTargetId,
423
+ contractPath,
424
+ totalTime,
425
+ ...(contractProfileHash ? { contractProfileHash } : {}),
426
+ ...(missingCodecs ? { missingCodecs } : {}),
427
+ ...(codecCoverageSkipped ? { codecCoverageSkipped } : {}),
428
+ ...(configPath ? { configPath } : {}),
429
+ });
430
+ }
431
+
432
+ if (contractProfileHash && marker.profileHash !== contractProfileHash) {
433
+ const totalTime = Date.now() - startTime;
434
+ return createVerifyResult({
435
+ ok: false,
436
+ code: 'PN-RTM-3002',
437
+ summary: 'Hash mismatch',
438
+ contractStorageHash,
439
+ contractProfileHash,
440
+ marker,
441
+ expectedTargetId,
442
+ contractPath,
443
+ totalTime,
444
+ ...(missingCodecs ? { missingCodecs } : {}),
445
+ ...(codecCoverageSkipped ? { codecCoverageSkipped } : {}),
446
+ ...(configPath ? { configPath } : {}),
447
+ });
448
+ }
449
+
450
+ const totalTime = Date.now() - startTime;
451
+ return createVerifyResult({
452
+ ok: true,
453
+ summary: 'Database matches contract',
454
+ contractStorageHash,
455
+ marker,
456
+ expectedTargetId,
457
+ contractPath,
458
+ totalTime,
459
+ ...(contractProfileHash ? { contractProfileHash } : {}),
460
+ ...(missingCodecs ? { missingCodecs } : {}),
461
+ ...(codecCoverageSkipped ? { codecCoverageSkipped } : {}),
462
+ ...(configPath ? { configPath } : {}),
463
+ });
464
+ },
465
+
466
+ async schemaVerify(options: SchemaVerifyOptions): Promise<VerifyDatabaseSchemaResult> {
467
+ const { driver, contractIR, strict, context, frameworkComponents } = options;
468
+
469
+ const contract = validateContract<SqlContract<SqlStorage>>(contractIR);
470
+
471
+ const controlAdapter = adapter.create();
472
+ if (!isSqlControlAdapter(controlAdapter)) {
473
+ throw new Error('Adapter does not implement SqlControlAdapter.introspect()');
474
+ }
475
+ const schemaIR = await controlAdapter.introspect(driver, contractIR);
476
+
477
+ return verifySqlSchema({
478
+ contract,
479
+ schema: schemaIR,
480
+ strict,
481
+ ...ifDefined('context', context),
482
+ typeMetadataRegistry,
483
+ frameworkComponents,
484
+ // Wire up target-specific normalizers if available
485
+ ...ifDefined('normalizeDefault', controlAdapter.normalizeDefault),
486
+ ...ifDefined('normalizeNativeType', controlAdapter.normalizeNativeType),
487
+ });
488
+ },
489
+ async sign(options: {
490
+ readonly driver: ControlDriverInstance<'sql', string>;
491
+ readonly contractIR: unknown;
492
+ readonly contractPath: string;
493
+ readonly configPath?: string;
494
+ }): Promise<SignDatabaseResult> {
495
+ const { driver, contractIR, contractPath, configPath } = options;
496
+ const startTime = Date.now();
497
+
498
+ const contract = validateContract<SqlContract<SqlStorage>>(contractIR);
499
+
500
+ const contractStorageHash = contract.storageHash;
501
+ const contractProfileHash =
502
+ 'profileHash' in contract && typeof contract.profileHash === 'string'
503
+ ? contract.profileHash
504
+ : contractStorageHash;
505
+ const contractTarget = contract.target;
506
+
507
+ await driver.query(ensureSchemaStatement.sql, ensureSchemaStatement.params);
508
+ await driver.query(ensureTableStatement.sql, ensureTableStatement.params);
509
+
510
+ const existingMarker = await readMarker(driver);
511
+
512
+ let markerCreated = false;
513
+ let markerUpdated = false;
514
+ let previousHashes: { storageHash?: string; profileHash?: string } | undefined;
515
+
516
+ if (!existingMarker) {
517
+ const write = writeContractMarker({
518
+ storageHash: contractStorageHash,
519
+ profileHash: contractProfileHash,
520
+ contractJson: contractIR,
521
+ canonicalVersion: 1,
522
+ });
523
+ await driver.query(write.insert.sql, write.insert.params);
524
+ markerCreated = true;
525
+ } else {
526
+ const existingStorageHash = existingMarker.storageHash;
527
+ const existingProfileHash = existingMarker.profileHash;
528
+
529
+ const storageHashMatches = existingStorageHash === contractStorageHash;
530
+ const profileHashMatches = existingProfileHash === contractProfileHash;
531
+
532
+ if (!storageHashMatches || !profileHashMatches) {
533
+ previousHashes = {
534
+ storageHash: existingStorageHash,
535
+ profileHash: existingProfileHash,
536
+ };
537
+ const write = writeContractMarker({
538
+ storageHash: contractStorageHash,
539
+ profileHash: contractProfileHash,
540
+ contractJson: contractIR,
541
+ canonicalVersion: existingMarker.canonicalVersion ?? 1,
542
+ });
543
+ await driver.query(write.update.sql, write.update.params);
544
+ markerUpdated = true;
545
+ }
546
+ }
547
+
548
+ let summary: string;
549
+ if (markerCreated) {
550
+ summary = 'Database signed (marker created)';
551
+ } else if (markerUpdated) {
552
+ summary = `Database signed (marker updated from ${previousHashes?.storageHash ?? 'unknown'})`;
553
+ } else {
554
+ summary = 'Database already signed with this contract';
555
+ }
556
+
557
+ const totalTime = Date.now() - startTime;
558
+
559
+ return {
560
+ ok: true,
561
+ summary,
562
+ contract: {
563
+ storageHash: contractStorageHash,
564
+ profileHash: contractProfileHash,
565
+ },
566
+ target: {
567
+ expected: contractTarget,
568
+ actual: contractTarget,
569
+ },
570
+ marker: {
571
+ created: markerCreated,
572
+ updated: markerUpdated,
573
+ ...(previousHashes ? { previous: previousHashes } : {}),
574
+ },
575
+ meta: {
576
+ contractPath,
577
+ ...(configPath ? { configPath } : {}),
578
+ },
579
+ timings: {
580
+ total: totalTime,
581
+ },
582
+ };
583
+ },
584
+ async readMarker(options: {
585
+ readonly driver: ControlDriverInstance<'sql', string>;
586
+ }): Promise<ContractMarkerRecord | null> {
587
+ return readMarker(options.driver);
588
+ },
589
+ async introspect(options: {
590
+ readonly driver: ControlDriverInstance<'sql', string>;
591
+ readonly contractIR?: unknown;
592
+ }): Promise<SqlSchemaIR> {
593
+ const { driver, contractIR } = options;
594
+
595
+ const controlAdapter = adapter.create();
596
+ if (!isSqlControlAdapter(controlAdapter)) {
597
+ throw new Error('Adapter does not implement SqlControlAdapter.introspect()');
598
+ }
599
+ return controlAdapter.introspect(driver, contractIR);
600
+ },
601
+
602
+ toSchemaView(schema: SqlSchemaIR): CoreSchemaView {
603
+ const rootLabel = 'contract';
604
+
605
+ const tableNodes: readonly SchemaTreeNode[] = Object.entries(schema.tables).map(
606
+ ([tableName, table]: [string, SqlTableIR]) => {
607
+ const children: SchemaTreeNode[] = [];
608
+
609
+ const columnNodes: SchemaTreeNode[] = [];
610
+ for (const [columnName, column] of Object.entries(table.columns)) {
611
+ const typeDisplay = column.nativeType;
612
+ const nullability = column.nullable ? 'nullable' : 'not nullable';
613
+ const label = `${columnName}: ${typeDisplay} (${nullability})`;
614
+ columnNodes.push({
615
+ kind: 'field',
616
+ id: `column-${tableName}-${columnName}`,
617
+ label,
618
+ meta: {
619
+ nativeType: column.nativeType,
620
+ nullable: column.nullable,
621
+ ...ifDefined('default', column.default),
622
+ },
623
+ });
624
+ }
625
+
626
+ if (columnNodes.length > 0) {
627
+ children.push({
628
+ kind: 'collection',
629
+ id: `columns-${tableName}`,
630
+ label: 'columns',
631
+ children: columnNodes,
632
+ });
633
+ }
634
+
635
+ if (table.primaryKey) {
636
+ const pkColumns = table.primaryKey.columns.join(', ');
637
+ children.push({
638
+ kind: 'index',
639
+ id: `primary-key-${tableName}`,
640
+ label: `primary key: ${pkColumns}`,
641
+ meta: {
642
+ columns: table.primaryKey.columns,
643
+ ...(table.primaryKey.name ? { name: table.primaryKey.name } : {}),
644
+ },
645
+ });
646
+ }
647
+
648
+ for (const unique of table.uniques) {
649
+ const name = unique.name ?? `${tableName}_${unique.columns.join('_')}_unique`;
650
+ const label = `unique ${name}`;
651
+ children.push({
652
+ kind: 'index',
653
+ id: `unique-${tableName}-${name}`,
654
+ label,
655
+ meta: {
656
+ columns: unique.columns,
657
+ unique: true,
658
+ },
659
+ });
660
+ }
661
+
662
+ for (const index of table.indexes) {
663
+ const name = index.name ?? `${tableName}_${index.columns.join('_')}_idx`;
664
+ const label = index.unique ? `unique index ${name}` : `index ${name}`;
665
+ children.push({
666
+ kind: 'index',
667
+ id: `index-${tableName}-${name}`,
668
+ label,
669
+ meta: {
670
+ columns: index.columns,
671
+ unique: index.unique,
672
+ },
673
+ });
674
+ }
675
+
676
+ const tableMeta: Record<string, unknown> = {};
677
+ if (table.primaryKey) {
678
+ tableMeta['primaryKey'] = table.primaryKey.columns;
679
+ if (table.primaryKey.name) {
680
+ tableMeta['primaryKeyName'] = table.primaryKey.name;
681
+ }
682
+ }
683
+ if (table.foreignKeys.length > 0) {
684
+ tableMeta['foreignKeys'] = table.foreignKeys.map((fk) => ({
685
+ columns: fk.columns,
686
+ referencedTable: fk.referencedTable,
687
+ referencedColumns: fk.referencedColumns,
688
+ ...(fk.name ? { name: fk.name } : {}),
689
+ }));
690
+ }
691
+
692
+ const node: SchemaTreeNode = {
693
+ kind: 'entity',
694
+ id: `table-${tableName}`,
695
+ label: `table ${tableName}`,
696
+ ...(Object.keys(tableMeta).length > 0 ? { meta: tableMeta } : {}),
697
+ ...(children.length > 0 ? { children: children as readonly SchemaTreeNode[] } : {}),
698
+ };
699
+ return node;
700
+ },
701
+ );
702
+
703
+ const extensionNodes: readonly SchemaTreeNode[] = schema.extensions.map((extName) => ({
704
+ kind: 'extension',
705
+ id: `extension-${extName}`,
706
+ label: `${extName} extension is enabled`,
707
+ }));
708
+
709
+ const rootChildren = [...tableNodes, ...extensionNodes];
710
+
711
+ const rootNode: SchemaTreeNode = {
712
+ kind: 'root',
713
+ id: 'sql-schema',
714
+ label: rootLabel,
715
+ ...(rootChildren.length > 0 ? { children: rootChildren } : {}),
716
+ };
717
+
718
+ return {
719
+ root: rootNode,
720
+ };
721
+ },
722
+
723
+ async emitContract({ contractIR }): Promise<EmitContractResult> {
724
+ const contractWithoutMappings = stripMappings(contractIR);
725
+ const validatedIR = this.validateContractIR(contractWithoutMappings);
726
+
727
+ const result = await emit(
728
+ validatedIR,
729
+ {
730
+ outputDir: '',
731
+ operationRegistry,
732
+ codecTypeImports,
733
+ operationTypeImports,
734
+ extensionIds,
735
+ parameterizedRenderers,
736
+ parameterizedTypeImports,
737
+ },
738
+ sqlTargetFamilyHook,
739
+ );
740
+
741
+ return {
742
+ contractJson: result.contractJson,
743
+ contractDts: result.contractDts,
744
+ storageHash: result.storageHash,
745
+ ...(result.executionHash ? { executionHash: result.executionHash } : {}),
746
+ profileHash: result.profileHash,
747
+ };
748
+ },
749
+ };
750
+ }