@prisma-next/sql-runtime 0.4.0-dev.9 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{exports-BO6Fl7yn.mjs → exports-BQZSVXXt.mjs} +102 -10
- package/dist/exports-BQZSVXXt.mjs.map +1 -0
- package/dist/{index-n6z6trta.d.mts → index-yb51L_1h.d.mts} +70 -18
- package/dist/index-yb51L_1h.d.mts.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/test/utils.d.mts +1 -1
- package/dist/test/utils.d.mts.map +1 -1
- package/dist/test/utils.mjs +6 -9
- package/dist/test/utils.mjs.map +1 -1
- package/package.json +11 -11
- package/src/exports/index.ts +5 -1
- package/src/lower-sql-plan.ts +2 -4
- package/src/middleware/before-compile-chain.ts +28 -0
- package/src/middleware/budgets.ts +16 -27
- package/src/middleware/lints.ts +3 -3
- package/src/middleware/sql-middleware.ts +31 -2
- package/src/sql-runtime.ts +163 -9
- package/test/before-compile-chain.test.ts +223 -0
- package/test/budgets.test.ts +6 -6
- package/test/execution-stack.test.ts +1 -4
- package/test/lints.test.ts +5 -4
- package/test/sql-runtime.test.ts +436 -11
- package/test/utils.ts +2 -5
- package/dist/exports-BO6Fl7yn.mjs.map +0 -1
- package/dist/index-n6z6trta.d.mts.map +0 -1
package/dist/test/utils.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.mjs","names":["collectAsync"],"sources":["../../test/utils.ts"],"sourcesContent":["import type { Contract, ExecutionPlan, ResultType } from '@prisma-next/contract/types';\nimport { coreHash, profileHash } from '@prisma-next/contract/types';\nimport {\n instantiateExecutionStack,\n type RuntimeDriverDescriptor,\n} from '@prisma-next/framework-components/execution';\nimport { builtinGeneratorIds } from '@prisma-next/ids';\nimport { generateId } from '@prisma-next/ids/runtime';\nimport type { SqlStorage } from '@prisma-next/sql-contract/types';\nimport type { Adapter, LoweredStatement, SelectAst } from '@prisma-next/sql-relational-core/ast';\nimport { codec, createCodecRegistry } from '@prisma-next/sql-relational-core/ast';\nimport type { SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';\nimport { collectAsync, drainAsyncIterable } from '@prisma-next/test-utils';\nimport type { Client } from 'pg';\nimport type { SqlStatement } from '../src/exports';\nimport {\n createExecutionContext,\n type createRuntime,\n createSqlExecutionStack,\n ensureSchemaStatement,\n ensureTableStatement,\n writeContractMarker,\n} from '../src/exports';\nimport type {\n ExecutionContext,\n SqlRuntimeAdapterDescriptor,\n SqlRuntimeAdapterInstance,\n SqlRuntimeDriverInstance,\n SqlRuntimeExtensionDescriptor,\n SqlRuntimeTargetDescriptor,\n} from '../src/sql-context';\n\nfunction createTestMutationDefaultGenerators() {\n return builtinGeneratorIds.map((id) => ({\n id,\n generate: (params?: Record<string, unknown>) => generateId(params ? { id, params } : { id }),\n }));\n}\n\n/**\n * Executes a plan and collects all results into an array.\n * This helper DRYs up the common pattern of executing plans in tests.\n * The return type is inferred from the plan's type parameter.\n */\nexport async function executePlanAndCollect<\n P extends ExecutionPlan<ResultType<P>> | SqlQueryPlan<ResultType<P>>,\n>(runtime: ReturnType<typeof createRuntime>, plan: P): Promise<ResultType<P>[]> {\n type Row = ResultType<P>;\n return collectAsync<Row>(runtime.execute<Row>(plan));\n}\n\n/**\n * Drains a plan execution, consuming all results without collecting them.\n * Useful for testing side effects without memory overhead.\n */\nexport async function drainPlanExecution(\n runtime: ReturnType<typeof createRuntime>,\n plan: ExecutionPlan | SqlQueryPlan<unknown>,\n): Promise<void> {\n return drainAsyncIterable(runtime.execute(plan));\n}\n\n/**\n * Executes a SQL statement on a database client.\n */\nexport async function executeStatement(client: Client, statement: SqlStatement): Promise<void> {\n if (statement.params.length > 0) {\n await client.query(statement.sql, [...statement.params]);\n return;\n }\n\n await client.query(statement.sql);\n}\n\n/**\n * Sets up database schema and data, then writes the contract marker.\n * This helper DRYs up the common pattern of database setup in tests.\n */\nexport async function setupTestDatabase(\n client: Client,\n contract: Contract<SqlStorage>,\n setupFn: (client: Client) => Promise<void>,\n): Promise<void> {\n await client.query('drop schema if exists prisma_contract cascade');\n await client.query('create schema if not exists public');\n\n await setupFn(client);\n\n await executeStatement(client, ensureSchemaStatement);\n await executeStatement(client, ensureTableStatement);\n const write = writeContractMarker({\n storageHash: contract.storage.storageHash,\n profileHash: contract.profileHash,\n contractJson: contract,\n canonicalVersion: 1,\n });\n await executeStatement(client, write.insert);\n}\n\n/**\n * Writes a contract marker to the database.\n * This helper DRYs up the common pattern of writing contract markers in tests.\n */\nexport async function writeTestContractMarker(\n client: Client,\n contract: Contract<SqlStorage>,\n): Promise<void> {\n const write = writeContractMarker({\n storageHash: contract.storage.storageHash,\n profileHash: contract.profileHash,\n contractJson: contract,\n canonicalVersion: 1,\n });\n await executeStatement(client, write.insert);\n}\n\n/**\n * Creates a test adapter descriptor from a raw adapter.\n * Wraps the adapter in an SqlRuntimeAdapterDescriptor with static contributions\n * derived from the adapter's codec registry.\n */\nexport function createTestAdapterDescriptor(\n adapter: Adapter<SelectAst, Contract<SqlStorage>, LoweredStatement>,\n): SqlRuntimeAdapterDescriptor<'postgres'> {\n const codecRegistry = adapter.profile.codecs();\n return {\n kind: 'adapter' as const,\n id: 'test-adapter',\n version: '0.0.1',\n familyId: 'sql' as const,\n targetId: 'postgres' as const,\n codecs: () => codecRegistry,\n parameterizedCodecs: () => [],\n mutationDefaultGenerators: createTestMutationDefaultGenerators,\n create(): SqlRuntimeAdapterInstance<'postgres'> {\n return Object.assign({ familyId: 'sql' as const, targetId: 'postgres' as const }, adapter);\n },\n };\n}\n\n/**\n * Creates a test target descriptor with empty static contributions.\n */\nexport function createTestTargetDescriptor(): SqlRuntimeTargetDescriptor<'postgres'> {\n return {\n kind: 'target' as const,\n id: 'postgres',\n version: '0.0.1',\n familyId: 'sql' as const,\n targetId: 'postgres' as const,\n codecs: () => createCodecRegistry(),\n parameterizedCodecs: () => [],\n create() {\n return { familyId: 'sql' as const, targetId: 'postgres' as const };\n },\n };\n}\n\n/**\n * Creates an ExecutionContext for testing.\n * This helper DRYs up the common pattern of context creation in tests.\n *\n * Accepts a raw adapter and optional extension descriptors, wrapping the\n * adapter in a descriptor internally for descriptor-first context creation.\n */\nexport function createTestContext<TContract extends Contract<SqlStorage>>(\n contract: TContract,\n adapter: Adapter<SelectAst, Contract<SqlStorage>, LoweredStatement>,\n options?: {\n extensionPacks?: ReadonlyArray<SqlRuntimeExtensionDescriptor<'postgres'>>;\n },\n): ExecutionContext<TContract> {\n return createExecutionContext({\n contract,\n stack: {\n target: createTestTargetDescriptor(),\n adapter: createTestAdapterDescriptor(adapter),\n extensionPacks: options?.extensionPacks ?? [],\n },\n });\n}\n\nexport function createTestStackInstance(options?: {\n extensionPacks?: ReadonlyArray<SqlRuntimeExtensionDescriptor<'postgres'>>;\n driver?: RuntimeDriverDescriptor<\n 'sql',\n 'postgres',\n unknown,\n SqlRuntimeDriverInstance<'postgres'>\n >;\n}) {\n const stack = createSqlExecutionStack({\n target: createTestTargetDescriptor(),\n adapter: createTestAdapterDescriptor(createStubAdapter()),\n driver: options?.driver,\n extensionPacks: options?.extensionPacks ?? [],\n });\n\n return instantiateExecutionStack(stack);\n}\n\n/**\n * Creates a stub adapter for testing.\n * This helper DRYs up the common pattern of adapter creation in tests.\n *\n * The stub adapter includes simple codecs for common test types (pg/int4@1, pg/text@1, pg/timestamptz@1)\n * to enable type inference in tests without requiring the postgres adapter package.\n */\nexport function createStubAdapter(): Adapter<SelectAst, Contract<SqlStorage>, LoweredStatement> {\n const codecRegistry = createCodecRegistry();\n\n // Register stub codecs for common test types\n // These match the codec IDs used in test contracts (pg/int4@1, pg/text@1, pg/timestamptz@1)\n // but don't require importing from the postgres adapter package\n codecRegistry.register(\n codec({\n typeId: 'pg/int4@1',\n targetTypes: ['int4'],\n encode: (value: number) => value,\n decode: (wire: number) => wire,\n }),\n );\n\n codecRegistry.register(\n codec({\n typeId: 'pg/text@1',\n targetTypes: ['text'],\n encode: (value: string) => value,\n decode: (wire: string) => wire,\n }),\n );\n\n codecRegistry.register(\n codec({\n typeId: 'pg/timestamptz@1',\n targetTypes: ['timestamptz'],\n encode: (value: string | Date) => (value instanceof Date ? value.toISOString() : value),\n decode: (wire: string | Date) => (wire instanceof Date ? wire : new Date(wire)),\n }),\n );\n\n return {\n profile: {\n id: 'stub-profile',\n target: 'postgres',\n capabilities: {},\n codecs() {\n return codecRegistry;\n },\n readMarkerStatement() {\n return {\n sql: 'select core_hash, profile_hash, contract_json, canonical_version, updated_at, app_tag, meta from prisma_contract.marker where id = $1',\n params: [1],\n };\n },\n },\n lower(ast: SelectAst, ctx: { contract: Contract<SqlStorage>; params?: readonly unknown[] }) {\n const sqlText = JSON.stringify(ast);\n return {\n profileId: this.profile.id,\n body: Object.freeze({ sql: sqlText, params: ctx.params ? [...ctx.params] : [] }),\n };\n },\n };\n}\n\nexport function createTestContract(\n contract: Partial<Omit<Contract<SqlStorage>, 'profileHash' | 'storage'>> & {\n storageHash?: string;\n profileHash?: string;\n storage?: Omit<SqlStorage, 'storageHash'>;\n },\n): Contract<SqlStorage> {\n const { execution, ...rest } = contract;\n const storageHashValue = coreHash(rest['storageHash'] ?? 'sha256:testcore');\n\n return {\n target: rest['target'] ?? 'postgres',\n targetFamily: rest['targetFamily'] ?? 'sql',\n storage: rest['storage']\n ? { ...rest['storage'], storageHash: storageHashValue }\n : { storageHash: storageHashValue, tables: {} },\n models: rest['models'] ?? {},\n roots: rest['roots'] ?? {},\n capabilities: rest['capabilities'] ?? {},\n extensionPacks: rest['extensionPacks'] ?? {},\n meta: rest['meta'] ?? {},\n ...(execution ? { execution } : {}),\n profileHash: profileHash(rest['profileHash'] ?? 'sha256:testprofile'),\n };\n}\n\n// Re-export generic utilities from test-utils\nexport {\n collectAsync,\n createDevDatabase,\n type DevDatabase,\n teardownTestDatabase,\n withClient,\n} from '@prisma-next/test-utils';\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"utils.mjs","names":["collectAsync"],"sources":["../../test/utils.ts"],"sourcesContent":["import type { Contract, ExecutionPlan, ResultType } from '@prisma-next/contract/types';\nimport { coreHash, profileHash } from '@prisma-next/contract/types';\nimport {\n instantiateExecutionStack,\n type RuntimeDriverDescriptor,\n} from '@prisma-next/framework-components/execution';\nimport { builtinGeneratorIds } from '@prisma-next/ids';\nimport { generateId } from '@prisma-next/ids/runtime';\nimport type { SqlStorage } from '@prisma-next/sql-contract/types';\nimport type { Adapter, LoweredStatement, SelectAst } from '@prisma-next/sql-relational-core/ast';\nimport { codec, createCodecRegistry } from '@prisma-next/sql-relational-core/ast';\nimport type { SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';\nimport { collectAsync, drainAsyncIterable } from '@prisma-next/test-utils';\nimport type { Client } from 'pg';\nimport type { SqlStatement } from '../src/exports';\nimport {\n createExecutionContext,\n type createRuntime,\n createSqlExecutionStack,\n ensureSchemaStatement,\n ensureTableStatement,\n writeContractMarker,\n} from '../src/exports';\nimport type {\n ExecutionContext,\n SqlRuntimeAdapterDescriptor,\n SqlRuntimeAdapterInstance,\n SqlRuntimeDriverInstance,\n SqlRuntimeExtensionDescriptor,\n SqlRuntimeTargetDescriptor,\n} from '../src/sql-context';\n\nfunction createTestMutationDefaultGenerators() {\n return builtinGeneratorIds.map((id) => ({\n id,\n generate: (params?: Record<string, unknown>) => generateId(params ? { id, params } : { id }),\n }));\n}\n\n/**\n * Executes a plan and collects all results into an array.\n * This helper DRYs up the common pattern of executing plans in tests.\n * The return type is inferred from the plan's type parameter.\n */\nexport async function executePlanAndCollect<\n P extends ExecutionPlan<ResultType<P>> | SqlQueryPlan<ResultType<P>>,\n>(runtime: ReturnType<typeof createRuntime>, plan: P): Promise<ResultType<P>[]> {\n type Row = ResultType<P>;\n return collectAsync<Row>(runtime.execute<Row>(plan));\n}\n\n/**\n * Drains a plan execution, consuming all results without collecting them.\n * Useful for testing side effects without memory overhead.\n */\nexport async function drainPlanExecution(\n runtime: ReturnType<typeof createRuntime>,\n plan: ExecutionPlan | SqlQueryPlan<unknown>,\n): Promise<void> {\n return drainAsyncIterable(runtime.execute(plan));\n}\n\n/**\n * Executes a SQL statement on a database client.\n */\nexport async function executeStatement(client: Client, statement: SqlStatement): Promise<void> {\n if (statement.params.length > 0) {\n await client.query(statement.sql, [...statement.params]);\n return;\n }\n\n await client.query(statement.sql);\n}\n\n/**\n * Sets up database schema and data, then writes the contract marker.\n * This helper DRYs up the common pattern of database setup in tests.\n */\nexport async function setupTestDatabase(\n client: Client,\n contract: Contract<SqlStorage>,\n setupFn: (client: Client) => Promise<void>,\n): Promise<void> {\n await client.query('drop schema if exists prisma_contract cascade');\n await client.query('create schema if not exists public');\n\n await setupFn(client);\n\n await executeStatement(client, ensureSchemaStatement);\n await executeStatement(client, ensureTableStatement);\n const write = writeContractMarker({\n storageHash: contract.storage.storageHash,\n profileHash: contract.profileHash,\n contractJson: contract,\n canonicalVersion: 1,\n });\n await executeStatement(client, write.insert);\n}\n\n/**\n * Writes a contract marker to the database.\n * This helper DRYs up the common pattern of writing contract markers in tests.\n */\nexport async function writeTestContractMarker(\n client: Client,\n contract: Contract<SqlStorage>,\n): Promise<void> {\n const write = writeContractMarker({\n storageHash: contract.storage.storageHash,\n profileHash: contract.profileHash,\n contractJson: contract,\n canonicalVersion: 1,\n });\n await executeStatement(client, write.insert);\n}\n\n/**\n * Creates a test adapter descriptor from a raw adapter.\n * Wraps the adapter in an SqlRuntimeAdapterDescriptor with static contributions\n * derived from the adapter's codec registry.\n */\nexport function createTestAdapterDescriptor(\n adapter: Adapter<SelectAst, Contract<SqlStorage>, LoweredStatement>,\n): SqlRuntimeAdapterDescriptor<'postgres'> {\n const codecRegistry = adapter.profile.codecs();\n return {\n kind: 'adapter' as const,\n id: 'test-adapter',\n version: '0.0.1',\n familyId: 'sql' as const,\n targetId: 'postgres' as const,\n codecs: () => codecRegistry,\n parameterizedCodecs: () => [],\n mutationDefaultGenerators: createTestMutationDefaultGenerators,\n create(_stack): SqlRuntimeAdapterInstance<'postgres'> {\n return Object.assign({ familyId: 'sql' as const, targetId: 'postgres' as const }, adapter);\n },\n };\n}\n\n/**\n * Creates a test target descriptor with empty static contributions.\n */\nexport function createTestTargetDescriptor(): SqlRuntimeTargetDescriptor<'postgres'> {\n return {\n kind: 'target' as const,\n id: 'postgres',\n version: '0.0.1',\n familyId: 'sql' as const,\n targetId: 'postgres' as const,\n codecs: () => createCodecRegistry(),\n parameterizedCodecs: () => [],\n create() {\n return { familyId: 'sql' as const, targetId: 'postgres' as const };\n },\n };\n}\n\n/**\n * Creates an ExecutionContext for testing.\n * This helper DRYs up the common pattern of context creation in tests.\n *\n * Accepts a raw adapter and optional extension descriptors, wrapping the\n * adapter in a descriptor internally for descriptor-first context creation.\n */\nexport function createTestContext<TContract extends Contract<SqlStorage>>(\n contract: TContract,\n adapter: Adapter<SelectAst, Contract<SqlStorage>, LoweredStatement>,\n options?: {\n extensionPacks?: ReadonlyArray<SqlRuntimeExtensionDescriptor<'postgres'>>;\n },\n): ExecutionContext<TContract> {\n return createExecutionContext({\n contract,\n stack: {\n target: createTestTargetDescriptor(),\n adapter: createTestAdapterDescriptor(adapter),\n extensionPacks: options?.extensionPacks ?? [],\n },\n });\n}\n\nexport function createTestStackInstance(options?: {\n extensionPacks?: ReadonlyArray<SqlRuntimeExtensionDescriptor<'postgres'>>;\n driver?: RuntimeDriverDescriptor<\n 'sql',\n 'postgres',\n unknown,\n SqlRuntimeDriverInstance<'postgres'>\n >;\n}) {\n const stack = createSqlExecutionStack({\n target: createTestTargetDescriptor(),\n adapter: createTestAdapterDescriptor(createStubAdapter()),\n driver: options?.driver,\n extensionPacks: options?.extensionPacks ?? [],\n });\n\n return instantiateExecutionStack(stack);\n}\n\n/**\n * Creates a stub adapter for testing.\n * This helper DRYs up the common pattern of adapter creation in tests.\n *\n * The stub adapter includes simple codecs for common test types (pg/int4@1, pg/text@1, pg/timestamptz@1)\n * to enable type inference in tests without requiring the postgres adapter package.\n */\nexport function createStubAdapter(): Adapter<SelectAst, Contract<SqlStorage>, LoweredStatement> {\n const codecRegistry = createCodecRegistry();\n\n // Register stub codecs for common test types\n // These match the codec IDs used in test contracts (pg/int4@1, pg/text@1, pg/timestamptz@1)\n // but don't require importing from the postgres adapter package\n codecRegistry.register(\n codec({\n typeId: 'pg/int4@1',\n targetTypes: ['int4'],\n encode: (value: number) => value,\n decode: (wire: number) => wire,\n }),\n );\n\n codecRegistry.register(\n codec({\n typeId: 'pg/text@1',\n targetTypes: ['text'],\n encode: (value: string) => value,\n decode: (wire: string) => wire,\n }),\n );\n\n codecRegistry.register(\n codec({\n typeId: 'pg/timestamptz@1',\n targetTypes: ['timestamptz'],\n encode: (value: string | Date) => (value instanceof Date ? value.toISOString() : value),\n decode: (wire: string | Date) => (wire instanceof Date ? wire : new Date(wire)),\n }),\n );\n\n return {\n profile: {\n id: 'stub-profile',\n target: 'postgres',\n capabilities: {},\n codecs() {\n return codecRegistry;\n },\n readMarkerStatement() {\n return {\n sql: 'select core_hash, profile_hash, contract_json, canonical_version, updated_at, app_tag, meta from prisma_contract.marker where id = $1',\n params: [1],\n };\n },\n },\n lower(ast: SelectAst, ctx: { contract: Contract<SqlStorage>; params?: readonly unknown[] }) {\n const sqlText = JSON.stringify(ast);\n return Object.freeze({ sql: sqlText, params: ctx.params ? [...ctx.params] : [] });\n },\n };\n}\n\nexport function createTestContract(\n contract: Partial<Omit<Contract<SqlStorage>, 'profileHash' | 'storage'>> & {\n storageHash?: string;\n profileHash?: string;\n storage?: Omit<SqlStorage, 'storageHash'>;\n },\n): Contract<SqlStorage> {\n const { execution, ...rest } = contract;\n const storageHashValue = coreHash(rest['storageHash'] ?? 'sha256:testcore');\n\n return {\n target: rest['target'] ?? 'postgres',\n targetFamily: rest['targetFamily'] ?? 'sql',\n storage: rest['storage']\n ? { ...rest['storage'], storageHash: storageHashValue }\n : { storageHash: storageHashValue, tables: {} },\n models: rest['models'] ?? {},\n roots: rest['roots'] ?? {},\n capabilities: rest['capabilities'] ?? {},\n extensionPacks: rest['extensionPacks'] ?? {},\n meta: rest['meta'] ?? {},\n ...(execution ? { execution } : {}),\n profileHash: profileHash(rest['profileHash'] ?? 'sha256:testprofile'),\n };\n}\n\n// Re-export generic utilities from test-utils\nexport {\n collectAsync,\n createDevDatabase,\n type DevDatabase,\n teardownTestDatabase,\n withClient,\n} from '@prisma-next/test-utils';\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/sql-runtime",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "SQL runtime implementation for Prisma Next",
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"arktype": "^2.1.26",
|
|
9
|
-
"@prisma-next/contract": "0.4.
|
|
10
|
-
"@prisma-next/utils": "0.4.
|
|
11
|
-
"@prisma-next/
|
|
12
|
-
"@prisma-next/
|
|
13
|
-
"@prisma-next/operations": "0.4.
|
|
14
|
-
"@prisma-next/
|
|
15
|
-
"@prisma-next/
|
|
16
|
-
"@prisma-next/sql-relational-core": "0.4.
|
|
17
|
-
"@prisma-next/sql-contract": "0.4.
|
|
9
|
+
"@prisma-next/contract": "0.4.2",
|
|
10
|
+
"@prisma-next/utils": "0.4.2",
|
|
11
|
+
"@prisma-next/ids": "0.4.2",
|
|
12
|
+
"@prisma-next/runtime-executor": "0.4.2",
|
|
13
|
+
"@prisma-next/operations": "0.4.2",
|
|
14
|
+
"@prisma-next/framework-components": "0.4.2",
|
|
15
|
+
"@prisma-next/sql-operations": "0.4.2",
|
|
16
|
+
"@prisma-next/sql-relational-core": "0.4.2",
|
|
17
|
+
"@prisma-next/sql-contract": "0.4.2"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/pg": "8.16.0",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"tsdown": "0.18.4",
|
|
23
23
|
"typescript": "5.9.3",
|
|
24
24
|
"vitest": "4.0.17",
|
|
25
|
-
"@prisma-next/tsconfig": "0.0.0",
|
|
26
25
|
"@prisma-next/test-utils": "0.0.1",
|
|
26
|
+
"@prisma-next/tsconfig": "0.0.0",
|
|
27
27
|
"@prisma-next/tsdown": "0.0.0"
|
|
28
28
|
},
|
|
29
29
|
"files": [
|
package/src/exports/index.ts
CHANGED
|
@@ -44,8 +44,12 @@ export {
|
|
|
44
44
|
export type {
|
|
45
45
|
CreateRuntimeOptions,
|
|
46
46
|
Runtime,
|
|
47
|
+
RuntimeConnection,
|
|
48
|
+
RuntimeQueryable,
|
|
47
49
|
RuntimeTelemetryEvent,
|
|
50
|
+
RuntimeTransaction,
|
|
48
51
|
RuntimeVerifyOptions,
|
|
49
52
|
TelemetryOutcome,
|
|
53
|
+
TransactionContext,
|
|
50
54
|
} from '../sql-runtime';
|
|
51
|
-
export { createRuntime } from '../sql-runtime';
|
|
55
|
+
export { createRuntime, withTransaction } from '../sql-runtime';
|
package/src/lower-sql-plan.ts
CHANGED
|
@@ -21,11 +21,9 @@ export function lowerSqlPlan<Row>(
|
|
|
21
21
|
params: queryPlan.params,
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
-
const body = lowered.body;
|
|
25
|
-
|
|
26
24
|
return Object.freeze({
|
|
27
|
-
sql:
|
|
28
|
-
params:
|
|
25
|
+
sql: lowered.sql,
|
|
26
|
+
params: lowered.params ?? queryPlan.params,
|
|
29
27
|
ast: queryPlan.ast,
|
|
30
28
|
meta: queryPlan.meta,
|
|
31
29
|
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { DraftPlan, SqlMiddleware, SqlMiddlewareContext } from './sql-middleware';
|
|
2
|
+
|
|
3
|
+
export async function runBeforeCompileChain(
|
|
4
|
+
middleware: readonly SqlMiddleware[],
|
|
5
|
+
initial: DraftPlan,
|
|
6
|
+
ctx: SqlMiddlewareContext,
|
|
7
|
+
): Promise<DraftPlan> {
|
|
8
|
+
let current = initial;
|
|
9
|
+
for (const mw of middleware) {
|
|
10
|
+
if (!mw.beforeCompile) {
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
const result = await mw.beforeCompile(current, ctx);
|
|
14
|
+
if (result === undefined) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (result.ast === current.ast) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
ctx.log.debug?.({
|
|
21
|
+
event: 'middleware.rewrite',
|
|
22
|
+
middleware: mw.name,
|
|
23
|
+
lane: current.meta.lane,
|
|
24
|
+
});
|
|
25
|
+
current = result;
|
|
26
|
+
}
|
|
27
|
+
return current;
|
|
28
|
+
}
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import type { ExecutionPlan } from '@prisma-next/contract/types';
|
|
2
2
|
import { type RuntimeErrorEnvelope, runtimeError } from '@prisma-next/framework-components/runtime';
|
|
3
|
-
import type {
|
|
4
|
-
AfterExecuteResult,
|
|
5
|
-
Middleware,
|
|
6
|
-
MiddlewareContext,
|
|
7
|
-
} from '@prisma-next/runtime-executor';
|
|
3
|
+
import type { AfterExecuteResult } from '@prisma-next/runtime-executor';
|
|
8
4
|
import { isQueryAst, type SelectAst } from '@prisma-next/sql-relational-core/ast';
|
|
5
|
+
import type { SqlMiddleware, SqlMiddlewareContext } from './sql-middleware';
|
|
9
6
|
|
|
10
7
|
export interface BudgetsOptions {
|
|
11
8
|
readonly maxRows?: number;
|
|
@@ -77,7 +74,7 @@ function hasDetectableLimitFromHeuristics(plan: ExecutionPlan): boolean {
|
|
|
77
74
|
function emitBudgetViolation(
|
|
78
75
|
error: RuntimeErrorEnvelope,
|
|
79
76
|
shouldBlock: boolean,
|
|
80
|
-
ctx:
|
|
77
|
+
ctx: SqlMiddlewareContext,
|
|
81
78
|
): void {
|
|
82
79
|
if (shouldBlock) {
|
|
83
80
|
throw error;
|
|
@@ -89,7 +86,7 @@ function emitBudgetViolation(
|
|
|
89
86
|
});
|
|
90
87
|
}
|
|
91
88
|
|
|
92
|
-
export function budgets
|
|
89
|
+
export function budgets(options?: BudgetsOptions): SqlMiddleware {
|
|
93
90
|
const maxRows = options?.maxRows ?? 10_000;
|
|
94
91
|
const defaultTableRows = options?.defaultTableRows ?? 10_000;
|
|
95
92
|
const tableRows = options?.tableRows ?? {};
|
|
@@ -102,7 +99,7 @@ export function budgets<TContract = unknown>(options?: BudgetsOptions): Middlewa
|
|
|
102
99
|
name: 'budgets',
|
|
103
100
|
familyId: 'sql' as const,
|
|
104
101
|
|
|
105
|
-
async beforeExecute(plan: ExecutionPlan, ctx:
|
|
102
|
+
async beforeExecute(plan: ExecutionPlan, ctx: SqlMiddlewareContext) {
|
|
106
103
|
observedRowsByPlan.set(plan, { count: 0 });
|
|
107
104
|
|
|
108
105
|
if (isQueryAst(plan.ast)) {
|
|
@@ -115,11 +112,7 @@ export function budgets<TContract = unknown>(options?: BudgetsOptions): Middlewa
|
|
|
115
112
|
return evaluateWithHeuristics(plan, ctx);
|
|
116
113
|
},
|
|
117
114
|
|
|
118
|
-
async onRow(
|
|
119
|
-
_row: Record<string, unknown>,
|
|
120
|
-
plan: ExecutionPlan,
|
|
121
|
-
_ctx: MiddlewareContext<TContract>,
|
|
122
|
-
) {
|
|
115
|
+
async onRow(_row: Record<string, unknown>, plan: ExecutionPlan, _ctx: SqlMiddlewareContext) {
|
|
123
116
|
const state = observedRowsByPlan.get(plan);
|
|
124
117
|
if (!state) return;
|
|
125
118
|
state.count += 1;
|
|
@@ -135,7 +128,7 @@ export function budgets<TContract = unknown>(options?: BudgetsOptions): Middlewa
|
|
|
135
128
|
async afterExecute(
|
|
136
129
|
_plan: ExecutionPlan,
|
|
137
130
|
result: AfterExecuteResult,
|
|
138
|
-
ctx:
|
|
131
|
+
ctx: SqlMiddlewareContext,
|
|
139
132
|
) {
|
|
140
133
|
const latencyMs = result.latencyMs;
|
|
141
134
|
if (latencyMs > maxLatencyMs) {
|
|
@@ -146,17 +139,13 @@ export function budgets<TContract = unknown>(options?: BudgetsOptions): Middlewa
|
|
|
146
139
|
maxLatencyMs,
|
|
147
140
|
}),
|
|
148
141
|
shouldBlock,
|
|
149
|
-
ctx
|
|
142
|
+
ctx,
|
|
150
143
|
);
|
|
151
144
|
}
|
|
152
145
|
},
|
|
153
146
|
});
|
|
154
147
|
|
|
155
|
-
function evaluateSelectAst(
|
|
156
|
-
plan: ExecutionPlan,
|
|
157
|
-
ast: SelectAst,
|
|
158
|
-
ctx: MiddlewareContext<TContract>,
|
|
159
|
-
) {
|
|
148
|
+
function evaluateSelectAst(plan: ExecutionPlan, ast: SelectAst, ctx: SqlMiddlewareContext) {
|
|
160
149
|
const hasAggNoGroup = hasAggregateWithoutGroupBy(ast);
|
|
161
150
|
const estimated = estimateRowsFromAst(
|
|
162
151
|
ast,
|
|
@@ -177,7 +166,7 @@ export function budgets<TContract = unknown>(options?: BudgetsOptions): Middlewa
|
|
|
177
166
|
maxRows,
|
|
178
167
|
}),
|
|
179
168
|
shouldBlock,
|
|
180
|
-
ctx
|
|
169
|
+
ctx,
|
|
181
170
|
);
|
|
182
171
|
return;
|
|
183
172
|
}
|
|
@@ -188,7 +177,7 @@ export function budgets<TContract = unknown>(options?: BudgetsOptions): Middlewa
|
|
|
188
177
|
maxRows,
|
|
189
178
|
}),
|
|
190
179
|
shouldBlock,
|
|
191
|
-
ctx
|
|
180
|
+
ctx,
|
|
192
181
|
);
|
|
193
182
|
return;
|
|
194
183
|
}
|
|
@@ -201,12 +190,12 @@ export function budgets<TContract = unknown>(options?: BudgetsOptions): Middlewa
|
|
|
201
190
|
maxRows,
|
|
202
191
|
}),
|
|
203
192
|
shouldBlock,
|
|
204
|
-
ctx
|
|
193
|
+
ctx,
|
|
205
194
|
);
|
|
206
195
|
}
|
|
207
196
|
}
|
|
208
197
|
|
|
209
|
-
async function evaluateWithHeuristics(plan: ExecutionPlan, ctx:
|
|
198
|
+
async function evaluateWithHeuristics(plan: ExecutionPlan, ctx: SqlMiddlewareContext) {
|
|
210
199
|
const estimated = estimateRowsFromHeuristics(plan, tableRows, defaultTableRows);
|
|
211
200
|
const isUnbounded = !hasDetectableLimitFromHeuristics(plan);
|
|
212
201
|
const sqlUpper = plan.sql.trimStart().toUpperCase();
|
|
@@ -222,7 +211,7 @@ export function budgets<TContract = unknown>(options?: BudgetsOptions): Middlewa
|
|
|
222
211
|
maxRows,
|
|
223
212
|
}),
|
|
224
213
|
shouldBlock,
|
|
225
|
-
ctx
|
|
214
|
+
ctx,
|
|
226
215
|
);
|
|
227
216
|
return;
|
|
228
217
|
}
|
|
@@ -233,7 +222,7 @@ export function budgets<TContract = unknown>(options?: BudgetsOptions): Middlewa
|
|
|
233
222
|
maxRows,
|
|
234
223
|
}),
|
|
235
224
|
shouldBlock,
|
|
236
|
-
ctx
|
|
225
|
+
ctx,
|
|
237
226
|
);
|
|
238
227
|
return;
|
|
239
228
|
}
|
|
@@ -247,7 +236,7 @@ export function budgets<TContract = unknown>(options?: BudgetsOptions): Middlewa
|
|
|
247
236
|
maxRows,
|
|
248
237
|
}),
|
|
249
238
|
shouldBlock,
|
|
250
|
-
ctx
|
|
239
|
+
ctx,
|
|
251
240
|
);
|
|
252
241
|
}
|
|
253
242
|
return;
|
package/src/middleware/lints.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { ExecutionPlan } from '@prisma-next/contract/types';
|
|
2
2
|
import { runtimeError } from '@prisma-next/framework-components/runtime';
|
|
3
|
-
import type { Middleware, MiddlewareContext } from '@prisma-next/runtime-executor';
|
|
4
3
|
import { evaluateRawGuardrails } from '@prisma-next/runtime-executor';
|
|
5
4
|
import {
|
|
6
5
|
type AnyFromSource,
|
|
@@ -8,6 +7,7 @@ import {
|
|
|
8
7
|
isQueryAst,
|
|
9
8
|
} from '@prisma-next/sql-relational-core/ast';
|
|
10
9
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
10
|
+
import type { SqlMiddleware, SqlMiddlewareContext } from './sql-middleware';
|
|
11
11
|
|
|
12
12
|
export interface LintsOptions {
|
|
13
13
|
readonly severities?: {
|
|
@@ -138,14 +138,14 @@ function getConfiguredSeverity(code: string, options?: LintsOptions): 'warn' | '
|
|
|
138
138
|
* Fallback: When ast is missing, `fallbackWhenAstMissing: 'raw'` uses heuristic
|
|
139
139
|
* SQL parsing; `'skip'` skips all lints. Default is `'raw'`.
|
|
140
140
|
*/
|
|
141
|
-
export function lints
|
|
141
|
+
export function lints(options?: LintsOptions): SqlMiddleware {
|
|
142
142
|
const fallback = options?.fallbackWhenAstMissing ?? 'raw';
|
|
143
143
|
|
|
144
144
|
return Object.freeze({
|
|
145
145
|
name: 'lints',
|
|
146
146
|
familyId: 'sql' as const,
|
|
147
147
|
|
|
148
|
-
async beforeExecute(plan: ExecutionPlan, ctx:
|
|
148
|
+
async beforeExecute(plan: ExecutionPlan, ctx: SqlMiddlewareContext) {
|
|
149
149
|
if (isQueryAst(plan.ast)) {
|
|
150
150
|
const findings = evaluateAstLints(plan.ast);
|
|
151
151
|
|
|
@@ -1,17 +1,46 @@
|
|
|
1
|
-
import type { Contract, ExecutionPlan } from '@prisma-next/contract/types';
|
|
1
|
+
import type { Contract, ExecutionPlan, PlanMeta } from '@prisma-next/contract/types';
|
|
2
2
|
import type {
|
|
3
3
|
AfterExecuteResult,
|
|
4
4
|
RuntimeMiddleware,
|
|
5
5
|
RuntimeMiddlewareContext,
|
|
6
6
|
} from '@prisma-next/framework-components/runtime';
|
|
7
7
|
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
8
|
+
import type { AnyQueryAst } from '@prisma-next/sql-relational-core/ast';
|
|
8
9
|
|
|
9
10
|
export interface SqlMiddlewareContext extends RuntimeMiddlewareContext {
|
|
10
11
|
readonly contract: Contract<SqlStorage>;
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Pre-lowering query view passed to `beforeCompile`. Carries the typed SQL
|
|
16
|
+
* AST and plan metadata; `sql`/`params` are produced later by the adapter.
|
|
17
|
+
*/
|
|
18
|
+
export interface DraftPlan {
|
|
19
|
+
readonly ast: AnyQueryAst;
|
|
20
|
+
readonly meta: PlanMeta;
|
|
21
|
+
}
|
|
22
|
+
|
|
13
23
|
export interface SqlMiddleware extends RuntimeMiddleware {
|
|
14
|
-
readonly familyId
|
|
24
|
+
readonly familyId?: 'sql';
|
|
25
|
+
/**
|
|
26
|
+
* Rewrite the query AST before it is lowered to SQL. Middlewares run in
|
|
27
|
+
* registration order; each sees the predecessor's output, so rewrites
|
|
28
|
+
* compose (e.g. soft-delete + tenant isolation).
|
|
29
|
+
*
|
|
30
|
+
* Return `undefined` (or a draft whose `ast` reference equals the input's)
|
|
31
|
+
* to pass through. Return a draft with a new `ast` reference to replace it;
|
|
32
|
+
* the runtime emits a `middleware.rewrite` debug log event and continues
|
|
33
|
+
* with the new draft. `adapter.lower()` runs once after the chain.
|
|
34
|
+
*
|
|
35
|
+
* Use `AstRewriter` / `SelectAst.withWhere` / `AndExpr.of` etc. to build
|
|
36
|
+
* the rewritten AST. Predicates and literals go through parameterized
|
|
37
|
+
* constructors by default — no SQL-injection surface is added. **Warning:**
|
|
38
|
+
* constructing `LiteralExpr.of(userInput)` from untrusted input bypasses
|
|
39
|
+
* that guarantee; use `ParamRef.of(userInput, ...)` instead.
|
|
40
|
+
*
|
|
41
|
+
* See `docs/architecture docs/subsystems/4. Runtime & Middleware Framework.md`.
|
|
42
|
+
*/
|
|
43
|
+
beforeCompile?(draft: DraftPlan, ctx: SqlMiddlewareContext): Promise<DraftPlan | undefined>;
|
|
15
44
|
beforeExecute?(plan: ExecutionPlan, ctx: SqlMiddlewareContext): Promise<void>;
|
|
16
45
|
onRow?(
|
|
17
46
|
row: Record<string, unknown>,
|
package/src/sql-runtime.ts
CHANGED
|
@@ -6,14 +6,17 @@ import type {
|
|
|
6
6
|
import { checkMiddlewareCompatibility } from '@prisma-next/framework-components/runtime';
|
|
7
7
|
import type {
|
|
8
8
|
Log,
|
|
9
|
-
Middleware,
|
|
10
9
|
RuntimeCore,
|
|
11
10
|
RuntimeCoreOptions,
|
|
12
11
|
RuntimeTelemetryEvent,
|
|
13
12
|
RuntimeVerifyOptions,
|
|
14
13
|
TelemetryOutcome,
|
|
15
14
|
} from '@prisma-next/runtime-executor';
|
|
16
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
AsyncIterableResult,
|
|
17
|
+
createRuntimeCore,
|
|
18
|
+
runtimeError,
|
|
19
|
+
} from '@prisma-next/runtime-executor';
|
|
17
20
|
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
18
21
|
import type {
|
|
19
22
|
Adapter,
|
|
@@ -29,6 +32,8 @@ import { decodeRow } from './codecs/decoding';
|
|
|
29
32
|
import { encodeParams } from './codecs/encoding';
|
|
30
33
|
import { validateCodecRegistryCompleteness } from './codecs/validation';
|
|
31
34
|
import { lowerSqlPlan } from './lower-sql-plan';
|
|
35
|
+
import { runBeforeCompileChain } from './middleware/before-compile-chain';
|
|
36
|
+
import type { SqlMiddleware } from './middleware/sql-middleware';
|
|
32
37
|
import type {
|
|
33
38
|
ExecutionContext,
|
|
34
39
|
SqlRuntimeAdapterInstance,
|
|
@@ -41,7 +46,7 @@ export interface RuntimeOptions<TContract extends Contract<SqlStorage> = Contrac
|
|
|
41
46
|
readonly adapter: Adapter<AnyQueryAst, Contract<SqlStorage>, LoweredStatement>;
|
|
42
47
|
readonly driver: SqlDriver<unknown>;
|
|
43
48
|
readonly verify: RuntimeVerifyOptions;
|
|
44
|
-
readonly middleware?: readonly
|
|
49
|
+
readonly middleware?: readonly SqlMiddleware[];
|
|
45
50
|
readonly mode?: 'strict' | 'permissive';
|
|
46
51
|
readonly log?: Log;
|
|
47
52
|
}
|
|
@@ -60,7 +65,7 @@ export interface CreateRuntimeOptions<
|
|
|
60
65
|
readonly context: ExecutionContext<TContract>;
|
|
61
66
|
readonly driver: SqlDriver<unknown>;
|
|
62
67
|
readonly verify: RuntimeVerifyOptions;
|
|
63
|
-
readonly middleware?: readonly
|
|
68
|
+
readonly middleware?: readonly SqlMiddleware[];
|
|
64
69
|
readonly mode?: 'strict' | 'permissive';
|
|
65
70
|
readonly log?: Log;
|
|
66
71
|
}
|
|
@@ -73,7 +78,28 @@ export interface Runtime extends RuntimeQueryable {
|
|
|
73
78
|
|
|
74
79
|
export interface RuntimeConnection extends RuntimeQueryable {
|
|
75
80
|
transaction(): Promise<RuntimeTransaction>;
|
|
81
|
+
/**
|
|
82
|
+
* Returns the connection to the pool for reuse. Only call this when the
|
|
83
|
+
* connection is known to be in a clean state. If a transaction
|
|
84
|
+
* commit/rollback failed or the connection is otherwise suspect, call
|
|
85
|
+
* `destroy(reason)` instead.
|
|
86
|
+
*/
|
|
76
87
|
release(): Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* Evicts the connection so it is never reused. Call this when the
|
|
90
|
+
* connection may be in an indeterminate state (e.g. a failed rollback
|
|
91
|
+
* leaving an open transaction, or a broken socket).
|
|
92
|
+
*
|
|
93
|
+
* If teardown fails the error is propagated and the connection remains
|
|
94
|
+
* retryable, so the caller can decide whether to swallow the failure or
|
|
95
|
+
* retry cleanup. Calling destroy() or release() more than once after a
|
|
96
|
+
* successful teardown is caller error.
|
|
97
|
+
*
|
|
98
|
+
* `reason` is advisory context only. It may be surfaced to driver-level
|
|
99
|
+
* observability hooks (e.g. pg-pool's `'release'` event) but does not
|
|
100
|
+
* influence eviction behavior and is not rethrown.
|
|
101
|
+
*/
|
|
102
|
+
destroy(reason?: unknown): Promise<void>;
|
|
77
103
|
}
|
|
78
104
|
|
|
79
105
|
export interface RuntimeTransaction extends RuntimeQueryable {
|
|
@@ -87,6 +113,10 @@ export interface RuntimeQueryable {
|
|
|
87
113
|
): AsyncIterableResult<Row>;
|
|
88
114
|
}
|
|
89
115
|
|
|
116
|
+
export interface TransactionContext extends RuntimeQueryable {
|
|
117
|
+
readonly invalidated: boolean;
|
|
118
|
+
}
|
|
119
|
+
|
|
90
120
|
interface CoreQueryable {
|
|
91
121
|
execute<Row = Record<string, unknown>>(plan: ExecutionPlan<Row>): AsyncIterableResult<Row>;
|
|
92
122
|
}
|
|
@@ -96,7 +126,7 @@ export type { RuntimeTelemetryEvent, RuntimeVerifyOptions, TelemetryOutcome };
|
|
|
96
126
|
class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorage>>
|
|
97
127
|
implements Runtime
|
|
98
128
|
{
|
|
99
|
-
private readonly core: RuntimeCore<TContract, SqlDriver<unknown
|
|
129
|
+
private readonly core: RuntimeCore<TContract, SqlDriver<unknown>, SqlMiddleware>;
|
|
100
130
|
private readonly contract: TContract;
|
|
101
131
|
private readonly adapter: Adapter<AnyQueryAst, Contract<SqlStorage>, LoweredStatement>;
|
|
102
132
|
private readonly codecRegistry: CodecRegistry;
|
|
@@ -119,7 +149,7 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
|
|
|
119
149
|
|
|
120
150
|
const familyAdapter = new SqlFamilyAdapter(context.contract, adapter.profile);
|
|
121
151
|
|
|
122
|
-
const coreOptions: RuntimeCoreOptions<TContract, SqlDriver<unknown
|
|
152
|
+
const coreOptions: RuntimeCoreOptions<TContract, SqlDriver<unknown>, SqlMiddleware> = {
|
|
123
153
|
familyAdapter,
|
|
124
154
|
driver,
|
|
125
155
|
verify,
|
|
@@ -143,12 +173,27 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
|
|
|
143
173
|
}
|
|
144
174
|
}
|
|
145
175
|
|
|
146
|
-
private toExecutionPlan<Row>(
|
|
176
|
+
private async toExecutionPlan<Row>(
|
|
177
|
+
plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
|
|
178
|
+
): Promise<ExecutionPlan<Row>> {
|
|
147
179
|
const isSqlQueryPlan = (p: ExecutionPlan<Row> | SqlQueryPlan<Row>): p is SqlQueryPlan<Row> => {
|
|
148
180
|
return 'ast' in p && !('sql' in p);
|
|
149
181
|
};
|
|
150
182
|
|
|
151
|
-
|
|
183
|
+
if (!isSqlQueryPlan(plan)) {
|
|
184
|
+
return plan;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const rewrittenDraft = await runBeforeCompileChain(
|
|
188
|
+
this.core.middleware,
|
|
189
|
+
{ ast: plan.ast, meta: plan.meta },
|
|
190
|
+
this.core.middlewareContext,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const planToLower: SqlQueryPlan<Row> =
|
|
194
|
+
rewrittenDraft.ast === plan.ast ? plan : { ...plan, ast: rewrittenDraft.ast };
|
|
195
|
+
|
|
196
|
+
return lowerSqlPlan(this.adapter, this.contract, planToLower);
|
|
152
197
|
}
|
|
153
198
|
|
|
154
199
|
private executeAgainstQueryable<Row = Record<string, unknown>>(
|
|
@@ -156,11 +201,11 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
|
|
|
156
201
|
queryable: CoreQueryable,
|
|
157
202
|
): AsyncIterableResult<Row> {
|
|
158
203
|
this.ensureCodecRegistryValidated(this.contract);
|
|
159
|
-
const executablePlan = this.toExecutionPlan(plan);
|
|
160
204
|
|
|
161
205
|
const iterator = async function* (
|
|
162
206
|
self: SqlRuntimeImpl<TContract>,
|
|
163
207
|
): AsyncGenerator<Row, void, unknown> {
|
|
208
|
+
const executablePlan = await self.toExecutionPlan(plan);
|
|
164
209
|
const encodedParams = encodeParams(executablePlan, self.codecRegistry);
|
|
165
210
|
const planWithEncodedParams: ExecutionPlan<Row> = {
|
|
166
211
|
...executablePlan,
|
|
@@ -206,6 +251,7 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
|
|
|
206
251
|
};
|
|
207
252
|
},
|
|
208
253
|
release: coreConn.release.bind(coreConn),
|
|
254
|
+
destroy: coreConn.destroy.bind(coreConn),
|
|
209
255
|
execute<Row = Record<string, unknown>>(
|
|
210
256
|
plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
|
|
211
257
|
): AsyncIterableResult<Row> {
|
|
@@ -224,6 +270,114 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
|
|
|
224
270
|
}
|
|
225
271
|
}
|
|
226
272
|
|
|
273
|
+
function transactionClosedError(): Error {
|
|
274
|
+
return runtimeError(
|
|
275
|
+
'RUNTIME.TRANSACTION_CLOSED',
|
|
276
|
+
'Cannot read from a query result after the transaction has ended. Await the result or call .toArray() inside the transaction callback.',
|
|
277
|
+
{},
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export async function withTransaction<R>(
|
|
282
|
+
runtime: Runtime,
|
|
283
|
+
fn: (tx: TransactionContext) => PromiseLike<R>,
|
|
284
|
+
): Promise<R> {
|
|
285
|
+
const connection = await runtime.connection();
|
|
286
|
+
const transaction = await connection.transaction();
|
|
287
|
+
|
|
288
|
+
let invalidated = false;
|
|
289
|
+
const txContext: TransactionContext = {
|
|
290
|
+
get invalidated() {
|
|
291
|
+
return invalidated;
|
|
292
|
+
},
|
|
293
|
+
execute<Row = Record<string, unknown>>(
|
|
294
|
+
plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
|
|
295
|
+
): AsyncIterableResult<Row> {
|
|
296
|
+
if (invalidated) {
|
|
297
|
+
throw transactionClosedError();
|
|
298
|
+
}
|
|
299
|
+
const inner = transaction.execute(plan);
|
|
300
|
+
const guarded = async function* (): AsyncGenerator<Row, void, unknown> {
|
|
301
|
+
for await (const row of inner) {
|
|
302
|
+
if (invalidated) {
|
|
303
|
+
throw transactionClosedError();
|
|
304
|
+
}
|
|
305
|
+
yield row;
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
return new AsyncIterableResult(guarded());
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
let connectionDisposed = false;
|
|
313
|
+
const destroyConnection = async (reason: unknown): Promise<void> => {
|
|
314
|
+
if (connectionDisposed) return;
|
|
315
|
+
connectionDisposed = true;
|
|
316
|
+
// SqlConnection.destroy() propagates teardown errors so callers can
|
|
317
|
+
// decide what to do with them. Here, we're already about to throw a
|
|
318
|
+
// more informative error describing why we're evicting the connection
|
|
319
|
+
// (rollback/commit failure), so swallowing the teardown error is the
|
|
320
|
+
// right call — surfacing it would mask the original cause.
|
|
321
|
+
await connection.destroy(reason).catch(() => undefined);
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
let result: R;
|
|
326
|
+
try {
|
|
327
|
+
result = await fn(txContext);
|
|
328
|
+
} catch (error) {
|
|
329
|
+
try {
|
|
330
|
+
await transaction.rollback();
|
|
331
|
+
} catch (rollbackError) {
|
|
332
|
+
await destroyConnection(rollbackError);
|
|
333
|
+
const wrapped = runtimeError(
|
|
334
|
+
'RUNTIME.TRANSACTION_ROLLBACK_FAILED',
|
|
335
|
+
'Transaction rollback failed after callback error',
|
|
336
|
+
{ rollbackError },
|
|
337
|
+
);
|
|
338
|
+
wrapped.cause = error;
|
|
339
|
+
throw wrapped;
|
|
340
|
+
}
|
|
341
|
+
throw error;
|
|
342
|
+
} finally {
|
|
343
|
+
invalidated = true;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
await transaction.commit();
|
|
348
|
+
} catch (commitError) {
|
|
349
|
+
// After a failed COMMIT the server-side transaction may be: (a) already
|
|
350
|
+
// committed (error on response path), (b) already rolled back (deferred
|
|
351
|
+
// constraint / serialization failure), or (c) still open (COMMIT never
|
|
352
|
+
// reached the server). Attempt a best-effort rollback to cover (c) and
|
|
353
|
+
// confirm the protocol is healthy.
|
|
354
|
+
//
|
|
355
|
+
// If rollback succeeds, the server is definitely no longer in a
|
|
356
|
+
// transaction (no-op in (a)/(b), real cleanup in (c)) and we've just
|
|
357
|
+
// proved the connection round-trips correctly — it's safe to return
|
|
358
|
+
// to the pool. If rollback fails, the connection state is ambiguous
|
|
359
|
+
// (broken socket, protocol desync, etc.) and we must destroy it.
|
|
360
|
+
try {
|
|
361
|
+
await transaction.rollback();
|
|
362
|
+
} catch {
|
|
363
|
+
await destroyConnection(commitError);
|
|
364
|
+
}
|
|
365
|
+
const wrapped = runtimeError(
|
|
366
|
+
'RUNTIME.TRANSACTION_COMMIT_FAILED',
|
|
367
|
+
'Transaction commit failed',
|
|
368
|
+
{ commitError },
|
|
369
|
+
);
|
|
370
|
+
wrapped.cause = commitError;
|
|
371
|
+
throw wrapped;
|
|
372
|
+
}
|
|
373
|
+
return result;
|
|
374
|
+
} finally {
|
|
375
|
+
if (!connectionDisposed) {
|
|
376
|
+
await connection.release();
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
227
381
|
export function createRuntime<TContract extends Contract<SqlStorage>, TTargetId extends string>(
|
|
228
382
|
options: CreateRuntimeOptions<TContract, TTargetId>,
|
|
229
383
|
): Runtime {
|