@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.
- package/README.md +1 -2
- package/dist/{accelerate-EEKAFGN3-SHR4XFVV.js → accelerate-EEKAFGN3-P6A6XJWJ.js} +28 -28
- package/dist/{accelerate-EEKAFGN3-SHR4XFVV.js.map → accelerate-EEKAFGN3-P6A6XJWJ.js.map} +1 -1
- package/dist/{dist-LCVVJCGI.js → dist-AQ3LWXOX.js} +13 -13
- package/dist/{dist-LCVVJCGI.js.map → dist-AQ3LWXOX.js.map} +1 -1
- package/dist/src/codecs/decoding.d.ts +4 -0
- package/dist/src/codecs/decoding.d.ts.map +1 -0
- package/dist/src/codecs/encoding.d.ts +5 -0
- package/dist/src/codecs/encoding.d.ts.map +1 -0
- package/dist/src/codecs/validation.d.ts +6 -0
- package/dist/src/codecs/validation.d.ts.map +1 -0
- package/dist/src/exports/index.d.ts +11 -0
- package/dist/src/exports/index.d.ts.map +1 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/lower-sql-plan.d.ts +15 -0
- package/dist/src/lower-sql-plan.d.ts.map +1 -0
- package/dist/src/sql-context.d.ts +65 -0
- package/dist/src/sql-context.d.ts.map +1 -0
- package/dist/src/sql-family-adapter.d.ts +10 -0
- package/dist/src/sql-family-adapter.d.ts.map +1 -0
- package/dist/src/sql-marker.d.ts +22 -0
- package/dist/src/sql-marker.d.ts.map +1 -0
- package/dist/src/sql-runtime.d.ts +25 -0
- package/dist/src/sql-runtime.d.ts.map +1 -0
- package/dist/test/utils.d.ts +20 -24
- package/dist/test/utils.d.ts.map +1 -0
- package/dist/test/utils.js +25 -25
- package/dist/test/utils.js.map +1 -1
- package/package.json +22 -21
- package/src/codecs/decoding.ts +140 -0
- package/src/codecs/encoding.ts +76 -0
- package/src/codecs/validation.ts +67 -0
- package/src/exports/index.ts +38 -0
- package/src/index.ts +1 -0
- package/src/lower-sql-plan.ts +32 -0
- package/src/sql-context.ts +156 -0
- package/src/sql-family-adapter.ts +43 -0
- package/src/sql-marker.ts +105 -0
- package/src/sql-runtime.ts +166 -0
- 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 +266 -0
- package/dist/index.d.ts +0 -29
- 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
|
+
});
|