@prisma-next/sql-runtime 0.3.0-pr.75.6 → 0.3.0-pr.75.7
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/package.json +10 -9
- package/test/async-iterable-result.test.ts +136 -0
- package/test/sql-context.test.ts +217 -0
- package/test/sql-family-adapter.test.ts +86 -0
- package/test/sql-runtime.test.ts +155 -0
- package/test/utils.ts +262 -0
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/sql-runtime",
|
|
3
|
-
"version": "0.3.0-pr.75.
|
|
3
|
+
"version": "0.3.0-pr.75.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "SQL runtime implementation for Prisma Next",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@prisma-next/contract": "0.3.0-pr.75.
|
|
9
|
-
"@prisma-next/
|
|
10
|
-
"@prisma-next/
|
|
11
|
-
"@prisma-next/runtime-executor": "0.3.0-pr.75.
|
|
12
|
-
"@prisma-next/sql-contract": "0.3.0-pr.75.
|
|
13
|
-
"@prisma-next/sql-operations": "0.3.0-pr.75.
|
|
14
|
-
"@prisma-next/sql-relational-core": "0.3.0-pr.75.
|
|
8
|
+
"@prisma-next/contract": "0.3.0-pr.75.7",
|
|
9
|
+
"@prisma-next/core-execution-plane": "0.3.0-pr.75.7",
|
|
10
|
+
"@prisma-next/operations": "0.3.0-pr.75.7",
|
|
11
|
+
"@prisma-next/runtime-executor": "0.3.0-pr.75.7",
|
|
12
|
+
"@prisma-next/sql-contract": "0.3.0-pr.75.7",
|
|
13
|
+
"@prisma-next/sql-operations": "0.3.0-pr.75.7",
|
|
14
|
+
"@prisma-next/sql-relational-core": "0.3.0-pr.75.7"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
17
|
"@types/pg": "8.16.0",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
26
|
"dist",
|
|
27
|
-
"src"
|
|
27
|
+
"src",
|
|
28
|
+
"test"
|
|
28
29
|
],
|
|
29
30
|
"exports": {
|
|
30
31
|
".": {
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { createOperationRegistry } from '@prisma-next/operations';
|
|
2
|
+
import type { SqlContract, SqlStorage } from '@prisma-next/sql-contract/types';
|
|
3
|
+
import type {
|
|
4
|
+
CodecRegistry,
|
|
5
|
+
SelectAst,
|
|
6
|
+
SqlDriver,
|
|
7
|
+
SqlExecuteRequest,
|
|
8
|
+
} from '@prisma-next/sql-relational-core/ast';
|
|
9
|
+
import { codec, createCodecRegistry } from '@prisma-next/sql-relational-core/ast';
|
|
10
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
11
|
+
import type { RuntimeContext } from '../src/sql-context';
|
|
12
|
+
import { createRuntime } from '../src/sql-runtime';
|
|
13
|
+
|
|
14
|
+
// Minimal test contract
|
|
15
|
+
const testContract: SqlContract<SqlStorage> = {
|
|
16
|
+
schemaVersion: '1',
|
|
17
|
+
targetFamily: 'sql',
|
|
18
|
+
target: 'postgres',
|
|
19
|
+
coreHash: 'sha256:test',
|
|
20
|
+
models: {},
|
|
21
|
+
relations: {},
|
|
22
|
+
storage: { tables: {} },
|
|
23
|
+
extensionPacks: {},
|
|
24
|
+
capabilities: {},
|
|
25
|
+
meta: {},
|
|
26
|
+
sources: {},
|
|
27
|
+
mappings: {
|
|
28
|
+
codecTypes: {},
|
|
29
|
+
operationTypes: {},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Create a stub codec registry
|
|
34
|
+
function createStubCodecs(): CodecRegistry {
|
|
35
|
+
const registry = createCodecRegistry();
|
|
36
|
+
registry.register(
|
|
37
|
+
codec({
|
|
38
|
+
typeId: 'pg/int4@1',
|
|
39
|
+
targetTypes: ['int4'],
|
|
40
|
+
encode: (v: number) => v,
|
|
41
|
+
decode: (w: number) => w,
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
return registry;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Create a stub adapter
|
|
48
|
+
function createStubAdapter() {
|
|
49
|
+
const codecs = createStubCodecs();
|
|
50
|
+
return {
|
|
51
|
+
familyId: 'sql' as const,
|
|
52
|
+
targetId: 'postgres' as const,
|
|
53
|
+
profile: {
|
|
54
|
+
id: 'test-profile',
|
|
55
|
+
target: 'postgres',
|
|
56
|
+
capabilities: {},
|
|
57
|
+
codecs() {
|
|
58
|
+
return codecs;
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
lower(ast: SelectAst) {
|
|
62
|
+
return {
|
|
63
|
+
profileId: 'test-profile',
|
|
64
|
+
body: Object.freeze({ sql: JSON.stringify(ast), params: [] }),
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Create a mock driver that implements SqlDriver interface
|
|
71
|
+
function createMockDriver(): SqlDriver {
|
|
72
|
+
const execute = vi.fn().mockImplementation(async function* (_request: SqlExecuteRequest) {
|
|
73
|
+
yield { id: 1 };
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
connect: vi.fn().mockResolvedValue(undefined),
|
|
78
|
+
execute,
|
|
79
|
+
query: vi.fn().mockResolvedValue({ rows: [], rowCount: 0 }),
|
|
80
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Create a test runtime context
|
|
85
|
+
function createTestContext(contract: SqlContract<SqlStorage>): RuntimeContext<typeof contract> {
|
|
86
|
+
const adapter = createStubAdapter();
|
|
87
|
+
return {
|
|
88
|
+
contract,
|
|
89
|
+
adapter,
|
|
90
|
+
codecs: adapter.profile.codecs(),
|
|
91
|
+
operations: createOperationRegistry(),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
describe('createRuntime', () => {
|
|
96
|
+
it('creates runtime with valid options', () => {
|
|
97
|
+
const context = createTestContext(testContract);
|
|
98
|
+
const driver = createMockDriver();
|
|
99
|
+
|
|
100
|
+
const runtime = createRuntime({
|
|
101
|
+
context,
|
|
102
|
+
driver,
|
|
103
|
+
verify: { mode: 'onFirstUse', requireMarker: false },
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(runtime).toBeDefined();
|
|
107
|
+
expect(runtime.execute).toBeDefined();
|
|
108
|
+
expect(runtime.telemetry).toBeDefined();
|
|
109
|
+
expect(runtime.operations).toBeDefined();
|
|
110
|
+
expect(runtime.close).toBeDefined();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('returns operations registry', () => {
|
|
114
|
+
const context = createTestContext(testContract);
|
|
115
|
+
const driver = createMockDriver();
|
|
116
|
+
|
|
117
|
+
const runtime = createRuntime({
|
|
118
|
+
context,
|
|
119
|
+
driver,
|
|
120
|
+
verify: { mode: 'onFirstUse', requireMarker: false },
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const ops = runtime.operations();
|
|
124
|
+
expect(ops).toBeDefined();
|
|
125
|
+
expect(ops.byType).toBeDefined();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('returns null telemetry when no events', () => {
|
|
129
|
+
const context = createTestContext(testContract);
|
|
130
|
+
const driver = createMockDriver();
|
|
131
|
+
|
|
132
|
+
const runtime = createRuntime({
|
|
133
|
+
context,
|
|
134
|
+
driver,
|
|
135
|
+
verify: { mode: 'onFirstUse', requireMarker: false },
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Before any execution, telemetry should be null
|
|
139
|
+
expect(runtime.telemetry()).toBeNull();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('closes runtime', async () => {
|
|
143
|
+
const context = createTestContext(testContract);
|
|
144
|
+
const driver = createMockDriver();
|
|
145
|
+
|
|
146
|
+
const runtime = createRuntime({
|
|
147
|
+
context,
|
|
148
|
+
driver,
|
|
149
|
+
verify: { mode: 'onFirstUse', requireMarker: false },
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
await runtime.close();
|
|
153
|
+
expect(driver.close).toHaveBeenCalled();
|
|
154
|
+
});
|
|
155
|
+
});
|
package/test/utils.ts
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import type { ExecutionPlan, ResultType } from '@prisma-next/contract/types';
|
|
2
|
+
import type { SqlContract, SqlStorage } from '@prisma-next/sql-contract/types';
|
|
3
|
+
import type { Adapter, LoweredStatement, SelectAst } from '@prisma-next/sql-relational-core/ast';
|
|
4
|
+
import { codec, createCodecRegistry } from '@prisma-next/sql-relational-core/ast';
|
|
5
|
+
import type { SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';
|
|
6
|
+
import { collectAsync, drainAsyncIterable } from '@prisma-next/test-utils';
|
|
7
|
+
import type { Client } from 'pg';
|
|
8
|
+
import type { SqlStatement } from '../src/exports';
|
|
9
|
+
import {
|
|
10
|
+
type createRuntime,
|
|
11
|
+
createRuntimeContext,
|
|
12
|
+
ensureSchemaStatement,
|
|
13
|
+
ensureTableStatement,
|
|
14
|
+
writeContractMarker,
|
|
15
|
+
} from '../src/exports';
|
|
16
|
+
import type {
|
|
17
|
+
RuntimeContext,
|
|
18
|
+
SqlRuntimeAdapterInstance,
|
|
19
|
+
SqlRuntimeExtensionDescriptor,
|
|
20
|
+
} from '../src/sql-context';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Executes a plan and collects all results into an array.
|
|
24
|
+
* This helper DRYs up the common pattern of executing plans in tests.
|
|
25
|
+
* The return type is inferred from the plan's type parameter.
|
|
26
|
+
*/
|
|
27
|
+
export async function executePlanAndCollect<
|
|
28
|
+
P extends ExecutionPlan<ResultType<P>> | SqlQueryPlan<ResultType<P>>,
|
|
29
|
+
>(runtime: ReturnType<typeof createRuntime>, plan: P): Promise<ResultType<P>[]> {
|
|
30
|
+
type Row = ResultType<P>;
|
|
31
|
+
return collectAsync<Row>(runtime.execute<Row>(plan));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Drains a plan execution, consuming all results without collecting them.
|
|
36
|
+
* Useful for testing side effects without memory overhead.
|
|
37
|
+
*/
|
|
38
|
+
export async function drainPlanExecution(
|
|
39
|
+
runtime: ReturnType<typeof createRuntime>,
|
|
40
|
+
plan: ExecutionPlan | SqlQueryPlan<unknown>,
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
return drainAsyncIterable(runtime.execute(plan));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Executes a SQL statement on a database client.
|
|
47
|
+
*/
|
|
48
|
+
export async function executeStatement(client: Client, statement: SqlStatement): Promise<void> {
|
|
49
|
+
if (statement.params.length > 0) {
|
|
50
|
+
await client.query(statement.sql, [...statement.params]);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
await client.query(statement.sql);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Sets up database schema and data, then writes the contract marker.
|
|
59
|
+
* This helper DRYs up the common pattern of database setup in tests.
|
|
60
|
+
*/
|
|
61
|
+
export async function setupTestDatabase(
|
|
62
|
+
client: Client,
|
|
63
|
+
contract: SqlContract<SqlStorage>,
|
|
64
|
+
setupFn: (client: Client) => Promise<void>,
|
|
65
|
+
): Promise<void> {
|
|
66
|
+
await client.query('drop schema if exists prisma_contract cascade');
|
|
67
|
+
await client.query('create schema if not exists public');
|
|
68
|
+
|
|
69
|
+
await setupFn(client);
|
|
70
|
+
|
|
71
|
+
await executeStatement(client, ensureSchemaStatement);
|
|
72
|
+
await executeStatement(client, ensureTableStatement);
|
|
73
|
+
const write = writeContractMarker({
|
|
74
|
+
coreHash: contract.coreHash,
|
|
75
|
+
profileHash: contract.profileHash ?? contract.coreHash,
|
|
76
|
+
contractJson: contract,
|
|
77
|
+
canonicalVersion: 1,
|
|
78
|
+
});
|
|
79
|
+
await executeStatement(client, write.insert);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Writes a contract marker to the database.
|
|
84
|
+
* This helper DRYs up the common pattern of writing contract markers in tests.
|
|
85
|
+
*/
|
|
86
|
+
export async function writeTestContractMarker(
|
|
87
|
+
client: Client,
|
|
88
|
+
contract: SqlContract<SqlStorage>,
|
|
89
|
+
): Promise<void> {
|
|
90
|
+
const write = writeContractMarker({
|
|
91
|
+
coreHash: contract.coreHash,
|
|
92
|
+
profileHash: contract.profileHash ?? contract.coreHash,
|
|
93
|
+
contractJson: contract,
|
|
94
|
+
canonicalVersion: 1,
|
|
95
|
+
});
|
|
96
|
+
await executeStatement(client, write.insert);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Creates a test adapter descriptor from a raw adapter.
|
|
101
|
+
* This wraps the adapter in a descriptor for descriptor-first context creation in tests.
|
|
102
|
+
* The adapter instance IS an Adapter (via intersection), with identity properties added.
|
|
103
|
+
*/
|
|
104
|
+
function createTestAdapterDescriptor(
|
|
105
|
+
adapter: Adapter<SelectAst, SqlContract<SqlStorage>, LoweredStatement>,
|
|
106
|
+
): {
|
|
107
|
+
readonly kind: 'adapter';
|
|
108
|
+
readonly id: string;
|
|
109
|
+
readonly version: string;
|
|
110
|
+
readonly familyId: 'sql';
|
|
111
|
+
readonly targetId: 'postgres';
|
|
112
|
+
create(): SqlRuntimeAdapterInstance<'postgres'>;
|
|
113
|
+
} {
|
|
114
|
+
return {
|
|
115
|
+
kind: 'adapter' as const,
|
|
116
|
+
id: 'test-adapter',
|
|
117
|
+
version: '0.0.1',
|
|
118
|
+
familyId: 'sql' as const,
|
|
119
|
+
targetId: 'postgres' as const,
|
|
120
|
+
create(): SqlRuntimeAdapterInstance<'postgres'> {
|
|
121
|
+
// Return an object that combines identity properties with the adapter's methods
|
|
122
|
+
return Object.assign(
|
|
123
|
+
{
|
|
124
|
+
familyId: 'sql' as const,
|
|
125
|
+
targetId: 'postgres' as const,
|
|
126
|
+
},
|
|
127
|
+
adapter,
|
|
128
|
+
);
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Creates a test target descriptor.
|
|
135
|
+
* This is a minimal descriptor for descriptor-first context creation in tests.
|
|
136
|
+
*/
|
|
137
|
+
function createTestTargetDescriptor(): {
|
|
138
|
+
readonly kind: 'target';
|
|
139
|
+
readonly id: string;
|
|
140
|
+
readonly version: string;
|
|
141
|
+
readonly familyId: 'sql';
|
|
142
|
+
readonly targetId: 'postgres';
|
|
143
|
+
create(): { readonly familyId: 'sql'; readonly targetId: 'postgres' };
|
|
144
|
+
} {
|
|
145
|
+
return {
|
|
146
|
+
kind: 'target' as const,
|
|
147
|
+
id: 'postgres',
|
|
148
|
+
version: '0.0.1',
|
|
149
|
+
familyId: 'sql' as const,
|
|
150
|
+
targetId: 'postgres' as const,
|
|
151
|
+
create() {
|
|
152
|
+
return { familyId: 'sql' as const, targetId: 'postgres' as const };
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Creates a runtime context with standard test configuration.
|
|
159
|
+
* This helper DRYs up the common pattern of context creation in tests.
|
|
160
|
+
*
|
|
161
|
+
* Accepts a raw adapter and optional extension descriptors, wrapping the
|
|
162
|
+
* adapter in a descriptor internally for descriptor-first context creation.
|
|
163
|
+
*/
|
|
164
|
+
export function createTestContext<TContract extends SqlContract<SqlStorage>>(
|
|
165
|
+
contract: TContract,
|
|
166
|
+
adapter: Adapter<SelectAst, SqlContract<SqlStorage>, LoweredStatement>,
|
|
167
|
+
options?: {
|
|
168
|
+
extensionPacks?: ReadonlyArray<SqlRuntimeExtensionDescriptor<'postgres'>>;
|
|
169
|
+
},
|
|
170
|
+
): RuntimeContext<TContract> {
|
|
171
|
+
return createRuntimeContext<TContract, 'postgres'>({
|
|
172
|
+
contract,
|
|
173
|
+
target: createTestTargetDescriptor(),
|
|
174
|
+
adapter: createTestAdapterDescriptor(adapter),
|
|
175
|
+
extensionPacks: options?.extensionPacks ?? [],
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Creates a stub adapter for testing.
|
|
181
|
+
* This helper DRYs up the common pattern of adapter creation in tests.
|
|
182
|
+
*
|
|
183
|
+
* The stub adapter includes simple codecs for common test types (pg/int4@1, pg/text@1, pg/timestamptz@1)
|
|
184
|
+
* to enable type inference in tests without requiring the postgres adapter package.
|
|
185
|
+
*/
|
|
186
|
+
export function createStubAdapter(): Adapter<SelectAst, SqlContract<SqlStorage>, LoweredStatement> {
|
|
187
|
+
const codecRegistry = createCodecRegistry();
|
|
188
|
+
|
|
189
|
+
// Register stub codecs for common test types
|
|
190
|
+
// These match the codec IDs used in test contracts (pg/int4@1, pg/text@1, pg/timestamptz@1)
|
|
191
|
+
// but don't require importing from the postgres adapter package
|
|
192
|
+
codecRegistry.register(
|
|
193
|
+
codec({
|
|
194
|
+
typeId: 'pg/int4@1',
|
|
195
|
+
targetTypes: ['int4'],
|
|
196
|
+
encode: (value: number) => value,
|
|
197
|
+
decode: (wire: number) => wire,
|
|
198
|
+
}),
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
codecRegistry.register(
|
|
202
|
+
codec({
|
|
203
|
+
typeId: 'pg/text@1',
|
|
204
|
+
targetTypes: ['text'],
|
|
205
|
+
encode: (value: string) => value,
|
|
206
|
+
decode: (wire: string) => wire,
|
|
207
|
+
}),
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
codecRegistry.register(
|
|
211
|
+
codec({
|
|
212
|
+
typeId: 'pg/timestamptz@1',
|
|
213
|
+
targetTypes: ['timestamptz'],
|
|
214
|
+
encode: (value: string | Date) => (value instanceof Date ? value.toISOString() : value),
|
|
215
|
+
decode: (wire: string | Date) =>
|
|
216
|
+
typeof wire === 'string' ? wire : wire instanceof Date ? wire.toISOString() : String(wire),
|
|
217
|
+
}),
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
profile: {
|
|
222
|
+
id: 'stub-profile',
|
|
223
|
+
target: 'postgres',
|
|
224
|
+
capabilities: {},
|
|
225
|
+
codecs() {
|
|
226
|
+
return codecRegistry;
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
lower(ast: SelectAst, ctx: { contract: SqlContract<SqlStorage>; params?: readonly unknown[] }) {
|
|
230
|
+
const sqlText = JSON.stringify(ast);
|
|
231
|
+
return {
|
|
232
|
+
profileId: this.profile.id,
|
|
233
|
+
body: Object.freeze({ sql: sqlText, params: ctx.params ? [...ctx.params] : [] }),
|
|
234
|
+
};
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Creates a valid test contract without using validateContract.
|
|
241
|
+
* Ensures mappings are present and returns the contract with proper typing.
|
|
242
|
+
* This helper allows tests to create contracts without depending on sql-query.
|
|
243
|
+
*/
|
|
244
|
+
export function createTestContract<T extends SqlContract<SqlStorage>>(contract: T): T {
|
|
245
|
+
// Ensure mappings are present
|
|
246
|
+
if (!contract.mappings) {
|
|
247
|
+
return {
|
|
248
|
+
...contract,
|
|
249
|
+
mappings: { codecTypes: {}, operationTypes: {} },
|
|
250
|
+
} as T;
|
|
251
|
+
}
|
|
252
|
+
return contract;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Re-export generic utilities from test-utils
|
|
256
|
+
export {
|
|
257
|
+
collectAsync,
|
|
258
|
+
createDevDatabase,
|
|
259
|
+
type DevDatabase,
|
|
260
|
+
teardownTestDatabase,
|
|
261
|
+
withClient,
|
|
262
|
+
} from '@prisma-next/test-utils';
|