@prisma-next/sql-runtime 0.3.0-dev.2 → 0.3.0-dev.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +1 -2
  2. package/dist/{accelerate-EEKAFGN3-SHR4XFVV.js → accelerate-EEKAFGN3-P6A6XJWJ.js} +28 -28
  3. package/dist/{accelerate-EEKAFGN3-SHR4XFVV.js.map → accelerate-EEKAFGN3-P6A6XJWJ.js.map} +1 -1
  4. package/dist/{dist-LCVVJCGI.js → dist-AQ3LWXOX.js} +13 -13
  5. package/dist/{dist-LCVVJCGI.js.map → dist-AQ3LWXOX.js.map} +1 -1
  6. package/dist/src/codecs/decoding.d.ts +4 -0
  7. package/dist/src/codecs/decoding.d.ts.map +1 -0
  8. package/dist/src/codecs/encoding.d.ts +5 -0
  9. package/dist/src/codecs/encoding.d.ts.map +1 -0
  10. package/dist/src/codecs/validation.d.ts +6 -0
  11. package/dist/src/codecs/validation.d.ts.map +1 -0
  12. package/dist/src/exports/index.d.ts +11 -0
  13. package/dist/src/exports/index.d.ts.map +1 -0
  14. package/dist/src/index.d.ts +2 -0
  15. package/dist/src/index.d.ts.map +1 -0
  16. package/dist/src/lower-sql-plan.d.ts +15 -0
  17. package/dist/src/lower-sql-plan.d.ts.map +1 -0
  18. package/dist/src/sql-context.d.ts +65 -0
  19. package/dist/src/sql-context.d.ts.map +1 -0
  20. package/dist/src/sql-family-adapter.d.ts +10 -0
  21. package/dist/src/sql-family-adapter.d.ts.map +1 -0
  22. package/dist/src/sql-marker.d.ts +22 -0
  23. package/dist/src/sql-marker.d.ts.map +1 -0
  24. package/dist/src/sql-runtime.d.ts +25 -0
  25. package/dist/src/sql-runtime.d.ts.map +1 -0
  26. package/dist/test/utils.d.ts +20 -24
  27. package/dist/test/utils.d.ts.map +1 -0
  28. package/dist/test/utils.js +25 -25
  29. package/dist/test/utils.js.map +1 -1
  30. package/package.json +22 -21
  31. package/src/codecs/decoding.ts +140 -0
  32. package/src/codecs/encoding.ts +76 -0
  33. package/src/codecs/validation.ts +67 -0
  34. package/src/exports/index.ts +38 -0
  35. package/src/index.ts +1 -0
  36. package/src/lower-sql-plan.ts +32 -0
  37. package/src/sql-context.ts +156 -0
  38. package/src/sql-family-adapter.ts +43 -0
  39. package/src/sql-marker.ts +105 -0
  40. package/src/sql-runtime.ts +166 -0
  41. package/test/async-iterable-result.test.ts +136 -0
  42. package/test/sql-context.test.ts +217 -0
  43. package/test/sql-family-adapter.test.ts +86 -0
  44. package/test/sql-runtime.test.ts +155 -0
  45. package/test/utils.ts +266 -0
  46. package/dist/index.d.ts +0 -29
  47. package/dist/sql-runtime-DgEbg2OP.d.ts +0 -109
@@ -0,0 +1,105 @@
1
+ import type { MarkerStatement } from '@prisma-next/runtime-executor';
2
+
3
+ export interface SqlStatement {
4
+ readonly sql: string;
5
+ readonly params: readonly unknown[];
6
+ }
7
+
8
+ export interface WriteMarkerInput {
9
+ readonly coreHash: string;
10
+ readonly profileHash: string;
11
+ readonly contractJson?: unknown;
12
+ readonly canonicalVersion?: number;
13
+ readonly appTag?: string;
14
+ readonly meta?: Record<string, unknown>;
15
+ }
16
+
17
+ export const ensureSchemaStatement: SqlStatement = {
18
+ sql: 'create schema if not exists prisma_contract',
19
+ params: [],
20
+ };
21
+
22
+ export const ensureTableStatement: SqlStatement = {
23
+ sql: `create table if not exists prisma_contract.marker (
24
+ id smallint primary key default 1,
25
+ core_hash text not null,
26
+ profile_hash text not null,
27
+ contract_json jsonb,
28
+ canonical_version int,
29
+ updated_at timestamptz not null default now(),
30
+ app_tag text,
31
+ meta jsonb not null default '{}'
32
+ )`,
33
+ params: [],
34
+ };
35
+
36
+ export function readContractMarker(): MarkerStatement {
37
+ return {
38
+ sql: `select
39
+ core_hash,
40
+ profile_hash,
41
+ contract_json,
42
+ canonical_version,
43
+ updated_at,
44
+ app_tag,
45
+ meta
46
+ from prisma_contract.marker
47
+ where id = $1`,
48
+ params: [1],
49
+ };
50
+ }
51
+
52
+ export interface WriteContractMarkerStatements {
53
+ readonly insert: SqlStatement;
54
+ readonly update: SqlStatement;
55
+ }
56
+
57
+ export function writeContractMarker(input: WriteMarkerInput): WriteContractMarkerStatements {
58
+ const baseParams: readonly unknown[] = [
59
+ 1,
60
+ input.coreHash,
61
+ input.profileHash,
62
+ input.contractJson ?? null,
63
+ input.canonicalVersion ?? null,
64
+ input.appTag ?? null,
65
+ JSON.stringify(input.meta ?? {}),
66
+ ];
67
+
68
+ const insert: SqlStatement = {
69
+ sql: `insert into prisma_contract.marker (
70
+ id,
71
+ core_hash,
72
+ profile_hash,
73
+ contract_json,
74
+ canonical_version,
75
+ updated_at,
76
+ app_tag,
77
+ meta
78
+ ) values (
79
+ $1,
80
+ $2,
81
+ $3,
82
+ $4::jsonb,
83
+ $5,
84
+ now(),
85
+ $6,
86
+ $7::jsonb
87
+ )`,
88
+ params: baseParams,
89
+ };
90
+
91
+ const update: SqlStatement = {
92
+ sql: `update prisma_contract.marker set
93
+ core_hash = $2,
94
+ profile_hash = $3,
95
+ contract_json = $4::jsonb,
96
+ canonical_version = $5,
97
+ updated_at = now(),
98
+ app_tag = $6,
99
+ meta = $7::jsonb
100
+ where id = $1`,
101
+ params: baseParams,
102
+ };
103
+
104
+ return { insert, update };
105
+ }
@@ -0,0 +1,166 @@
1
+ import type { ExecutionPlan } from '@prisma-next/contract/types';
2
+ import type { OperationRegistry } from '@prisma-next/operations';
3
+ import type {
4
+ Log,
5
+ Plugin,
6
+ RuntimeCore,
7
+ RuntimeCoreOptions,
8
+ RuntimeTelemetryEvent,
9
+ RuntimeVerifyOptions,
10
+ TelemetryOutcome,
11
+ } from '@prisma-next/runtime-executor';
12
+ import { AsyncIterableResult, createRuntimeCore } from '@prisma-next/runtime-executor';
13
+ import type { SqlContract, SqlStorage } from '@prisma-next/sql-contract/types';
14
+ import type {
15
+ Adapter,
16
+ CodecRegistry,
17
+ LoweredStatement,
18
+ SelectAst,
19
+ SqlDriver,
20
+ } from '@prisma-next/sql-relational-core/ast';
21
+ import type { SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';
22
+ import { decodeRow } from './codecs/decoding';
23
+ import { encodeParams } from './codecs/encoding';
24
+ import { validateCodecRegistryCompleteness } from './codecs/validation';
25
+ import { lowerSqlPlan } from './lower-sql-plan';
26
+ import type { RuntimeContext } from './sql-context';
27
+ import { SqlFamilyAdapter } from './sql-family-adapter';
28
+
29
+ export interface RuntimeOptions<
30
+ TContract extends SqlContract<SqlStorage> = SqlContract<SqlStorage>,
31
+ > {
32
+ readonly driver: SqlDriver;
33
+ readonly verify: RuntimeVerifyOptions;
34
+ readonly context: RuntimeContext<TContract>;
35
+ readonly plugins?: readonly Plugin<
36
+ TContract,
37
+ Adapter<SelectAst, SqlContract<SqlStorage>, LoweredStatement>,
38
+ SqlDriver
39
+ >[];
40
+ readonly mode?: 'strict' | 'permissive';
41
+ readonly log?: Log;
42
+ }
43
+
44
+ export interface Runtime {
45
+ execute<Row = Record<string, unknown>>(
46
+ plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
47
+ ): AsyncIterableResult<Row>;
48
+ telemetry(): RuntimeTelemetryEvent | null;
49
+ close(): Promise<void>;
50
+ operations(): OperationRegistry;
51
+ }
52
+
53
+ export type { RuntimeTelemetryEvent, RuntimeVerifyOptions, TelemetryOutcome };
54
+
55
+ class SqlRuntimeImpl<TContract extends SqlContract<SqlStorage> = SqlContract<SqlStorage>>
56
+ implements Runtime
57
+ {
58
+ private readonly core: RuntimeCore<
59
+ TContract,
60
+ Adapter<SelectAst, SqlContract<SqlStorage>, LoweredStatement>,
61
+ SqlDriver
62
+ >;
63
+ private readonly contract: TContract;
64
+ private readonly context: RuntimeContext<TContract>;
65
+ private readonly codecRegistry: CodecRegistry;
66
+ private codecRegistryValidated: boolean;
67
+
68
+ constructor(options: RuntimeOptions<TContract>) {
69
+ const { context, driver, verify, plugins, mode, log } = options;
70
+ this.contract = context.contract;
71
+ this.context = context;
72
+ this.codecRegistry = context.codecs;
73
+ this.codecRegistryValidated = false;
74
+
75
+ const familyAdapter = new SqlFamilyAdapter(context.contract);
76
+
77
+ const coreOptions: RuntimeCoreOptions<
78
+ TContract,
79
+ Adapter<SelectAst, SqlContract<SqlStorage>, LoweredStatement>,
80
+ SqlDriver
81
+ > = {
82
+ familyAdapter,
83
+ driver,
84
+ verify,
85
+ plugins: plugins as readonly Plugin<
86
+ TContract,
87
+ Adapter<SelectAst, SqlContract<SqlStorage>, LoweredStatement>,
88
+ SqlDriver
89
+ >[],
90
+ ...(mode !== undefined ? { mode } : {}),
91
+ ...(log !== undefined ? { log } : {}),
92
+ operationRegistry: context.operations,
93
+ };
94
+
95
+ this.core = createRuntimeCore(coreOptions);
96
+
97
+ if (verify.mode === 'startup') {
98
+ validateCodecRegistryCompleteness(this.codecRegistry, context.contract);
99
+ this.codecRegistryValidated = true;
100
+ }
101
+ }
102
+
103
+ private ensureCodecRegistryValidated(contract: SqlContract<SqlStorage>): void {
104
+ if (!this.codecRegistryValidated) {
105
+ validateCodecRegistryCompleteness(this.codecRegistry, contract);
106
+ this.codecRegistryValidated = true;
107
+ }
108
+ }
109
+
110
+ execute<Row = Record<string, unknown>>(
111
+ plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
112
+ ): AsyncIterableResult<Row> {
113
+ this.ensureCodecRegistryValidated(this.contract);
114
+
115
+ // Check if plan is SqlQueryPlan (has ast but no sql)
116
+ const isSqlQueryPlan = (p: ExecutionPlan<Row> | SqlQueryPlan<Row>): p is SqlQueryPlan<Row> => {
117
+ return 'ast' in p && !('sql' in p);
118
+ };
119
+
120
+ // Lower SqlQueryPlan to Plan if needed
121
+ const executablePlan: ExecutionPlan<Row> = isSqlQueryPlan(plan)
122
+ ? lowerSqlPlan(this.context, plan)
123
+ : plan;
124
+
125
+ const iterator = async function* (
126
+ self: SqlRuntimeImpl<TContract>,
127
+ ): AsyncGenerator<Row, void, unknown> {
128
+ const encodedParams = encodeParams(executablePlan, self.codecRegistry);
129
+ const planWithEncodedParams: ExecutionPlan<Row> = {
130
+ ...executablePlan,
131
+ params: encodedParams,
132
+ };
133
+
134
+ const coreIterator = self.core.execute(planWithEncodedParams);
135
+
136
+ for await (const rawRow of coreIterator) {
137
+ const decodedRow = decodeRow(
138
+ rawRow as Record<string, unknown>,
139
+ executablePlan,
140
+ self.codecRegistry,
141
+ );
142
+ yield decodedRow as Row;
143
+ }
144
+ };
145
+
146
+ return new AsyncIterableResult(iterator(this));
147
+ }
148
+
149
+ telemetry(): RuntimeTelemetryEvent | null {
150
+ return this.core.telemetry();
151
+ }
152
+
153
+ operations(): OperationRegistry {
154
+ return this.core.operations();
155
+ }
156
+
157
+ close(): Promise<void> {
158
+ return this.core.close();
159
+ }
160
+ }
161
+
162
+ export function createRuntime<TContract extends SqlContract<SqlStorage>>(
163
+ options: RuntimeOptions<TContract>,
164
+ ): Runtime {
165
+ return new SqlRuntimeImpl(options);
166
+ }
@@ -0,0 +1,136 @@
1
+ import type { ExecutionPlan } from '@prisma-next/contract/types';
2
+ import type { AsyncIterableResult } from '@prisma-next/runtime-executor';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { createRuntime } from '../src/exports';
5
+ import { createStubAdapter, createTestContext, createTestContract } from './utils';
6
+
7
+ // Mock driver that implements SqlDriver interface
8
+ class MockDriver {
9
+ private rows: ReadonlyArray<Record<string, unknown>> = [];
10
+
11
+ setRows(rows: ReadonlyArray<Record<string, unknown>>): void {
12
+ this.rows = rows;
13
+ }
14
+
15
+ async query(
16
+ _sql: string,
17
+ _params: readonly unknown[],
18
+ ): Promise<{ rows: ReadonlyArray<unknown> }> {
19
+ // Return empty marker result for contract verification
20
+ return { rows: [] };
21
+ }
22
+
23
+ async *execute<Row = Record<string, unknown>>(_options: {
24
+ sql: string;
25
+ params: readonly unknown[];
26
+ }): AsyncIterable<Row> {
27
+ for (const row of this.rows) {
28
+ yield row as Row;
29
+ }
30
+ }
31
+
32
+ async connect(): Promise<void> {
33
+ // No-op
34
+ }
35
+
36
+ async close(): Promise<void> {
37
+ // No-op
38
+ }
39
+ }
40
+
41
+ const fixtureContract = createTestContract({
42
+ schemaVersion: '1',
43
+ targetFamily: 'sql',
44
+ target: 'postgres',
45
+ coreHash: 'test-hash',
46
+ profileHash: 'test-profile-hash',
47
+ storage: {
48
+ tables: {
49
+ user: {
50
+ columns: {
51
+ id: { nativeType: 'int4', codecId: 'pg/int4@1', nullable: false },
52
+ email: { nativeType: 'text', codecId: 'pg/text@1', nullable: false },
53
+ },
54
+ uniques: [],
55
+ indexes: [],
56
+ foreignKeys: [],
57
+ },
58
+ },
59
+ },
60
+ models: {},
61
+ relations: {},
62
+ mappings: { codecTypes: {}, operationTypes: {} },
63
+ });
64
+
65
+ describe('SqlRuntime AsyncIterableResult integration', () => {
66
+ it('returns AsyncIterableResult from execute', async () => {
67
+ const adapter = createStubAdapter();
68
+ const driver = new MockDriver();
69
+ driver.setRows([
70
+ { id: 1, email: 'test1@example.com' },
71
+ { id: 2, email: 'test2@example.com' },
72
+ ]);
73
+ const context = createTestContext(fixtureContract, adapter);
74
+ const runtime = createRuntime({
75
+ driver: driver as unknown as Parameters<typeof createRuntime>[0]['driver'],
76
+ context,
77
+ verify: { mode: 'onFirstUse', requireMarker: false },
78
+ });
79
+
80
+ const plan: ExecutionPlan<{ id: number; email: string }> = {
81
+ sql: 'SELECT id, email FROM "user" ORDER BY id',
82
+ params: [],
83
+ meta: {
84
+ target: 'postgres',
85
+ targetFamily: 'sql',
86
+ coreHash: 'test-hash',
87
+ lane: 'sql',
88
+ paramDescriptors: [],
89
+ },
90
+ };
91
+
92
+ const result = runtime.execute(plan);
93
+
94
+ // Verify it's an AsyncIterableResult
95
+ expect(result).toBeInstanceOf(Object);
96
+ expect(typeof result.toArray).toBe('function');
97
+ expect(typeof result[Symbol.asyncIterator]).toBe('function');
98
+
99
+ await runtime.close();
100
+ });
101
+
102
+ it('preserves type information', async () => {
103
+ const adapter = createStubAdapter();
104
+ const driver = new MockDriver();
105
+ driver.setRows([{ id: 1, email: 'test@example.com' }]);
106
+ const context = createTestContext(fixtureContract, adapter);
107
+ const runtime = createRuntime({
108
+ driver: driver as unknown as Parameters<typeof createRuntime>[0]['driver'],
109
+ context,
110
+ verify: { mode: 'onFirstUse', requireMarker: false },
111
+ });
112
+
113
+ const plan: ExecutionPlan<{ id: number; email: string }> = {
114
+ sql: 'SELECT id, email FROM "user" LIMIT 1',
115
+ params: [],
116
+ meta: {
117
+ target: 'postgres',
118
+ targetFamily: 'sql',
119
+ coreHash: 'test-hash',
120
+ lane: 'sql',
121
+ paramDescriptors: [],
122
+ },
123
+ };
124
+
125
+ const result: AsyncIterableResult<{ id: number; email: string }> = runtime.execute(plan);
126
+ const rows = await result.toArray();
127
+
128
+ expect(rows.length).toBe(1);
129
+ if (rows[0]) {
130
+ expect(typeof rows[0].id).toBe('number');
131
+ expect(typeof rows[0].email).toBe('string');
132
+ }
133
+
134
+ await runtime.close();
135
+ });
136
+ });
@@ -0,0 +1,217 @@
1
+ import type { SqlContract, SqlStorage } from '@prisma-next/sql-contract/types';
2
+ import type { SqlOperationSignature } from '@prisma-next/sql-operations';
3
+ import type { CodecRegistry, SelectAst } from '@prisma-next/sql-relational-core/ast';
4
+ import { codec, createCodecRegistry } from '@prisma-next/sql-relational-core/ast';
5
+ import { describe, expect, it } from 'vitest';
6
+ import {
7
+ createRuntimeContext,
8
+ type SqlRuntimeExtensionDescriptor,
9
+ type SqlRuntimeExtensionInstance,
10
+ } from '../src/sql-context';
11
+
12
+ // Minimal test contract
13
+ const testContract: SqlContract<SqlStorage> = {
14
+ schemaVersion: '1',
15
+ targetFamily: 'sql',
16
+ target: 'postgres',
17
+ coreHash: 'sha256:test',
18
+ models: {},
19
+ relations: {},
20
+ storage: { tables: {} },
21
+ extensionPacks: {},
22
+ capabilities: {},
23
+ meta: {},
24
+ sources: {},
25
+ mappings: {
26
+ codecTypes: {},
27
+ operationTypes: {},
28
+ },
29
+ };
30
+
31
+ // Stub adapter codecs
32
+ function createStubCodecs(): CodecRegistry {
33
+ const registry = createCodecRegistry();
34
+ registry.register(
35
+ codec({
36
+ typeId: 'pg/int4@1',
37
+ targetTypes: ['int4'],
38
+ encode: (v: number) => v,
39
+ decode: (w: number) => w,
40
+ }),
41
+ );
42
+ return registry;
43
+ }
44
+
45
+ // Create a test adapter descriptor
46
+ function createTestAdapterDescriptor() {
47
+ const codecs = createStubCodecs();
48
+ return {
49
+ kind: 'adapter' as const,
50
+ id: 'test-adapter',
51
+ version: '0.0.1',
52
+ familyId: 'sql' as const,
53
+ targetId: 'postgres' as const,
54
+ create() {
55
+ return {
56
+ familyId: 'sql' as const,
57
+ targetId: 'postgres' as const,
58
+ profile: {
59
+ id: 'test-profile',
60
+ target: 'postgres',
61
+ capabilities: {},
62
+ codecs() {
63
+ return codecs;
64
+ },
65
+ },
66
+ lower(ast: SelectAst) {
67
+ return {
68
+ profileId: 'test-profile',
69
+ body: Object.freeze({ sql: JSON.stringify(ast), params: [] }),
70
+ };
71
+ },
72
+ };
73
+ },
74
+ };
75
+ }
76
+
77
+ // Create a test target descriptor
78
+ function createTestTargetDescriptor() {
79
+ return {
80
+ kind: 'target' as const,
81
+ id: 'postgres',
82
+ version: '0.0.1',
83
+ familyId: 'sql' as const,
84
+ targetId: 'postgres' as const,
85
+ create() {
86
+ return { familyId: 'sql' as const, targetId: 'postgres' as const };
87
+ },
88
+ };
89
+ }
90
+
91
+ // Create a test extension descriptor
92
+ function createTestExtensionDescriptor(options?: {
93
+ hasCodecs?: boolean;
94
+ hasOperations?: boolean;
95
+ }): SqlRuntimeExtensionDescriptor<'postgres'> {
96
+ const { hasCodecs = false, hasOperations = false } = options ?? {};
97
+
98
+ // Build the codecs function if needed
99
+ const codecsFn = hasCodecs
100
+ ? () => {
101
+ const registry = createCodecRegistry();
102
+ registry.register(
103
+ codec({
104
+ typeId: 'test/ext@1',
105
+ targetTypes: ['ext'],
106
+ encode: (v: string) => v,
107
+ decode: (w: string) => w,
108
+ }),
109
+ );
110
+ return registry;
111
+ }
112
+ : undefined;
113
+
114
+ // Build the operations function if needed
115
+ const operationsFn = hasOperations
116
+ ? (): ReadonlyArray<SqlOperationSignature> => [
117
+ {
118
+ forTypeId: 'test/ext@1',
119
+ method: 'testOp',
120
+ args: [],
121
+ returns: { kind: 'builtin', type: 'number' },
122
+ lowering: { targetFamily: 'sql', strategy: 'function', template: 'test()' },
123
+ },
124
+ ]
125
+ : undefined;
126
+
127
+ return {
128
+ kind: 'extension' as const,
129
+ id: 'test-extension',
130
+ version: '0.0.1',
131
+ familyId: 'sql' as const,
132
+ targetId: 'postgres' as const,
133
+ create(): SqlRuntimeExtensionInstance<'postgres'> {
134
+ // Return object with optional methods only if they exist
135
+ const instance: SqlRuntimeExtensionInstance<'postgres'> = {
136
+ familyId: 'sql' as const,
137
+ targetId: 'postgres' as const,
138
+ };
139
+ if (codecsFn) {
140
+ (instance as { codecs?: () => CodecRegistry }).codecs = codecsFn;
141
+ }
142
+ if (operationsFn) {
143
+ (instance as { operations?: () => ReadonlyArray<SqlOperationSignature> }).operations =
144
+ operationsFn;
145
+ }
146
+ return instance;
147
+ },
148
+ };
149
+ }
150
+
151
+ describe('createRuntimeContext', () => {
152
+ it('creates context with adapter codecs', () => {
153
+ const context = createRuntimeContext({
154
+ contract: testContract,
155
+ target: createTestTargetDescriptor(),
156
+ adapter: createTestAdapterDescriptor(),
157
+ });
158
+
159
+ expect(context.contract).toBe(testContract);
160
+ expect(context.adapter).toBeDefined();
161
+ expect(context.codecs.has('pg/int4@1')).toBe(true);
162
+ expect(context.operations).toBeDefined();
163
+ });
164
+
165
+ it('creates context with empty extension packs', () => {
166
+ const context = createRuntimeContext({
167
+ contract: testContract,
168
+ target: createTestTargetDescriptor(),
169
+ adapter: createTestAdapterDescriptor(),
170
+ extensionPacks: [],
171
+ });
172
+
173
+ expect(context.codecs.has('pg/int4@1')).toBe(true);
174
+ // No extension codecs registered
175
+ expect(context.codecs.has('test/ext@1')).toBe(false);
176
+ });
177
+
178
+ it('registers extension codecs', () => {
179
+ const context = createRuntimeContext({
180
+ contract: testContract,
181
+ target: createTestTargetDescriptor(),
182
+ adapter: createTestAdapterDescriptor(),
183
+ extensionPacks: [createTestExtensionDescriptor({ hasCodecs: true })],
184
+ });
185
+
186
+ // Adapter codec
187
+ expect(context.codecs.has('pg/int4@1')).toBe(true);
188
+ // Extension codec
189
+ expect(context.codecs.has('test/ext@1')).toBe(true);
190
+ });
191
+
192
+ it('registers extension operations', () => {
193
+ const context = createRuntimeContext({
194
+ contract: testContract,
195
+ target: createTestTargetDescriptor(),
196
+ adapter: createTestAdapterDescriptor(),
197
+ extensionPacks: [createTestExtensionDescriptor({ hasOperations: true })],
198
+ });
199
+
200
+ const ops = context.operations.byType('test/ext@1');
201
+ expect(ops.length).toBe(1);
202
+ expect(ops[0]?.method).toBe('testOp');
203
+ });
204
+
205
+ it('handles extension without codecs or operations', () => {
206
+ const context = createRuntimeContext({
207
+ contract: testContract,
208
+ target: createTestTargetDescriptor(),
209
+ adapter: createTestAdapterDescriptor(),
210
+ extensionPacks: [createTestExtensionDescriptor({ hasCodecs: false, hasOperations: false })],
211
+ });
212
+
213
+ // Only adapter codec
214
+ expect(context.codecs.has('pg/int4@1')).toBe(true);
215
+ expect(context.codecs.has('test/ext@1')).toBe(false);
216
+ });
217
+ });
@@ -0,0 +1,86 @@
1
+ import type { ExecutionPlan } from '@prisma-next/contract/types';
2
+ import type { SqlContract, SqlStorage } from '@prisma-next/sql-contract/types';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { SqlFamilyAdapter } from '../src/sql-family-adapter';
5
+
6
+ // Minimal test contract
7
+ const testContract: SqlContract<SqlStorage> = {
8
+ schemaVersion: '1',
9
+ targetFamily: 'sql',
10
+ target: 'postgres',
11
+ coreHash: 'sha256:test-hash',
12
+ models: {},
13
+ relations: {},
14
+ storage: { tables: {} },
15
+ extensionPacks: {},
16
+ capabilities: {},
17
+ meta: {},
18
+ sources: {},
19
+ mappings: {
20
+ codecTypes: {},
21
+ operationTypes: {},
22
+ },
23
+ };
24
+
25
+ describe('SqlFamilyAdapter', () => {
26
+ it('creates adapter with contract and marker reader', () => {
27
+ const adapter = new SqlFamilyAdapter(testContract);
28
+
29
+ expect(adapter.contract).toBe(testContract);
30
+ expect(adapter.markerReader).toBeDefined();
31
+ expect(adapter.markerReader.readMarkerStatement).toBeDefined();
32
+ });
33
+
34
+ it('validates plan with matching target and hash', () => {
35
+ const adapter = new SqlFamilyAdapter(testContract);
36
+ const plan: ExecutionPlan = {
37
+ meta: {
38
+ target: 'postgres',
39
+ coreHash: 'sha256:test-hash',
40
+ lane: 'sql',
41
+ paramDescriptors: [],
42
+ },
43
+ sql: 'SELECT 1',
44
+ params: [],
45
+ };
46
+
47
+ // Should not throw
48
+ expect(() => adapter.validatePlan(plan, testContract)).not.toThrow();
49
+ });
50
+
51
+ it('throws on plan target mismatch', () => {
52
+ const adapter = new SqlFamilyAdapter(testContract);
53
+ const plan: ExecutionPlan = {
54
+ meta: {
55
+ target: 'mysql', // Wrong target
56
+ coreHash: 'sha256:test-hash',
57
+ lane: 'sql',
58
+ paramDescriptors: [],
59
+ },
60
+ sql: 'SELECT 1',
61
+ params: [],
62
+ };
63
+
64
+ expect(() => adapter.validatePlan(plan, testContract)).toThrow(
65
+ 'Plan target does not match runtime target',
66
+ );
67
+ });
68
+
69
+ it('throws on plan coreHash mismatch', () => {
70
+ const adapter = new SqlFamilyAdapter(testContract);
71
+ const plan: ExecutionPlan = {
72
+ meta: {
73
+ target: 'postgres',
74
+ coreHash: 'sha256:different-hash', // Wrong hash
75
+ lane: 'sql',
76
+ paramDescriptors: [],
77
+ },
78
+ sql: 'SELECT 1',
79
+ params: [],
80
+ };
81
+
82
+ expect(() => adapter.validatePlan(plan, testContract)).toThrow(
83
+ 'Plan core hash does not match runtime contract',
84
+ );
85
+ });
86
+ });