@prisma-next/sql-runtime 0.5.0-dev.3 → 0.5.0-dev.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +29 -21
  2. package/dist/{exports-BQZSVXXt.mjs → exports-Cq_9ZrU4.mjs} +649 -275
  3. package/dist/exports-Cq_9ZrU4.mjs.map +1 -0
  4. package/dist/{index-yb51L_1h.d.mts → index-Df2GsLSH.d.mts} +65 -16
  5. package/dist/index-Df2GsLSH.d.mts.map +1 -0
  6. package/dist/index.d.mts +2 -2
  7. package/dist/index.mjs +2 -2
  8. package/dist/test/utils.d.mts +6 -5
  9. package/dist/test/utils.d.mts.map +1 -1
  10. package/dist/test/utils.mjs +11 -5
  11. package/dist/test/utils.mjs.map +1 -1
  12. package/package.json +10 -12
  13. package/src/codecs/decoding.ts +256 -173
  14. package/src/codecs/encoding.ts +123 -39
  15. package/src/exports/index.ts +11 -7
  16. package/src/fingerprint.ts +22 -0
  17. package/src/guardrails/raw.ts +165 -0
  18. package/src/lower-sql-plan.ts +3 -3
  19. package/src/marker.ts +75 -0
  20. package/src/middleware/before-compile-chain.ts +1 -0
  21. package/src/middleware/budgets.ts +26 -96
  22. package/src/middleware/lints.ts +3 -3
  23. package/src/middleware/sql-middleware.ts +6 -5
  24. package/src/runtime-spi.ts +44 -0
  25. package/src/sql-family-adapter.ts +3 -2
  26. package/src/sql-marker.ts +62 -47
  27. package/src/sql-runtime.ts +321 -111
  28. package/dist/exports-BQZSVXXt.mjs.map +0 -1
  29. package/dist/index-yb51L_1h.d.mts.map +0 -1
  30. package/test/async-iterable-result.test.ts +0 -141
  31. package/test/before-compile-chain.test.ts +0 -223
  32. package/test/budgets.test.ts +0 -431
  33. package/test/context.types.test-d.ts +0 -68
  34. package/test/execution-stack.test.ts +0 -161
  35. package/test/json-schema-validation.test.ts +0 -571
  36. package/test/lints.test.ts +0 -160
  37. package/test/mutation-default-generators.test.ts +0 -254
  38. package/test/parameterized-types.test.ts +0 -529
  39. package/test/sql-context.test.ts +0 -384
  40. package/test/sql-family-adapter.test.ts +0 -103
  41. package/test/sql-runtime.test.ts +0 -792
  42. package/test/utils.ts +0 -297
@@ -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(_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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"}
1
+ {"version":3,"file":"utils.mjs","names":["collectAsync"],"sources":["../../test/utils.ts"],"sourcesContent":["import type { Contract } 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 type { ResultType } from '@prisma-next/framework-components/runtime';\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 { SqlExecutionPlan, 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 parseContractMarkerRow,\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 SqlExecutionPlan<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: SqlExecutionPlan | 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: Date) => value,\n decode: (wire: Date) => wire,\n // Date is not assignable to JsonValue, so the JSON round-trip pair\n // must be supplied explicitly.\n encodeJson: (value: Date) => value.toISOString(),\n decodeJson: (json) => {\n if (typeof json !== 'string') throw new Error('expected ISO date string');\n return new Date(json);\n },\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, invariants from prisma_contract.marker where id = $1',\n params: [1],\n };\n },\n parseMarkerRow: parseContractMarkerRow,\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,19 @@
1
1
  {
2
2
  "name": "@prisma-next/sql-runtime",
3
- "version": "0.5.0-dev.3",
3
+ "version": "0.5.0-dev.30",
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.5.0-dev.3",
10
- "@prisma-next/framework-components": "0.5.0-dev.3",
11
- "@prisma-next/ids": "0.5.0-dev.3",
12
- "@prisma-next/utils": "0.5.0-dev.3",
13
- "@prisma-next/runtime-executor": "0.5.0-dev.3",
14
- "@prisma-next/sql-contract": "0.5.0-dev.3",
15
- "@prisma-next/sql-operations": "0.5.0-dev.3",
16
- "@prisma-next/sql-relational-core": "0.5.0-dev.3",
17
- "@prisma-next/operations": "0.5.0-dev.3"
9
+ "@prisma-next/contract": "0.5.0-dev.30",
10
+ "@prisma-next/framework-components": "0.5.0-dev.30",
11
+ "@prisma-next/utils": "0.5.0-dev.30",
12
+ "@prisma-next/operations": "0.5.0-dev.30",
13
+ "@prisma-next/ids": "0.5.0-dev.30",
14
+ "@prisma-next/sql-contract": "0.5.0-dev.30",
15
+ "@prisma-next/sql-relational-core": "0.5.0-dev.30",
16
+ "@prisma-next/sql-operations": "0.5.0-dev.30"
18
17
  },
19
18
  "devDependencies": {
20
19
  "@types/pg": "8.16.0",
@@ -28,8 +27,7 @@
28
27
  },
29
28
  "files": [
30
29
  "dist",
31
- "src",
32
- "test"
30
+ "src"
33
31
  ],
34
32
  "exports": {
35
33
  ".": "./dist/index.mjs",
@@ -1,221 +1,304 @@
1
- import type { ExecutionPlan } from '@prisma-next/contract/types';
2
- import type { Codec, CodecRegistry } from '@prisma-next/sql-relational-core/ast';
1
+ import {
2
+ checkAborted,
3
+ isRuntimeError,
4
+ raceAgainstAbort,
5
+ runtimeError,
6
+ } from '@prisma-next/framework-components/runtime';
7
+ import type {
8
+ AnyQueryAst,
9
+ Codec,
10
+ CodecRegistry,
11
+ ProjectionItem,
12
+ SqlCodecCallContext,
13
+ } from '@prisma-next/sql-relational-core/ast';
14
+ import type { SqlExecutionPlan } from '@prisma-next/sql-relational-core/plan';
3
15
  import type { JsonSchemaValidatorRegistry } from '@prisma-next/sql-relational-core/query-lane-context';
4
16
  import { validateJsonValue } from './json-schema-validation';
5
17
 
6
- function resolveRowCodec(
7
- alias: string,
8
- plan: ExecutionPlan,
9
- registry: CodecRegistry,
10
- ): Codec | null {
11
- const planCodecId = plan.meta.annotations?.codecs?.[alias] as string | undefined;
12
- if (planCodecId) {
13
- const codec = registry.get(planCodecId);
14
- if (codec) {
15
- return codec;
16
- }
18
+ type ColumnRef = { table: string; column: string };
19
+
20
+ interface DecodeContext {
21
+ readonly aliases: ReadonlyArray<string> | undefined;
22
+ readonly codecs: ReadonlyMap<string, Codec>;
23
+ readonly columnRefs: ReadonlyMap<string, ColumnRef>;
24
+ readonly includeAliases: ReadonlySet<string>;
25
+ }
26
+
27
+ const WIRE_PREVIEW_LIMIT = 100;
28
+ const EMPTY_INCLUDE_ALIASES: ReadonlySet<string> = new Set<string>();
29
+
30
+ function isAstBackedPlan(
31
+ plan: SqlExecutionPlan,
32
+ ): plan is SqlExecutionPlan & { readonly ast: AnyQueryAst } {
33
+ return plan.ast !== undefined;
34
+ }
35
+
36
+ function projectionListFromAst(ast: AnyQueryAst): ReadonlyArray<ProjectionItem> | undefined {
37
+ if (ast.kind === 'select') {
38
+ return ast.projection;
39
+ }
40
+ return ast.returning;
41
+ }
42
+
43
+ function buildDecodeContext(plan: SqlExecutionPlan, registry: CodecRegistry): DecodeContext {
44
+ if (!isAstBackedPlan(plan)) {
45
+ return {
46
+ aliases: undefined,
47
+ codecs: new Map(),
48
+ columnRefs: new Map(),
49
+ includeAliases: EMPTY_INCLUDE_ALIASES,
50
+ };
51
+ }
52
+
53
+ const projection = projectionListFromAst(plan.ast);
54
+ if (!projection) {
55
+ return {
56
+ aliases: undefined,
57
+ codecs: new Map(),
58
+ columnRefs: new Map(),
59
+ includeAliases: EMPTY_INCLUDE_ALIASES,
60
+ };
17
61
  }
18
62
 
19
- if (plan.meta.projectionTypes) {
20
- const typeId = plan.meta.projectionTypes[alias];
21
- if (typeId) {
22
- const codec = registry.get(typeId);
63
+ const aliases: string[] = [];
64
+ const codecs = new Map<string, Codec>();
65
+ const columnRefs = new Map<string, ColumnRef>();
66
+ const includeAliases = new Set<string>();
67
+
68
+ for (const item of projection) {
69
+ aliases.push(item.alias);
70
+
71
+ if (item.codecId) {
72
+ const codec = registry.get(item.codecId);
23
73
  if (codec) {
24
- return codec;
74
+ codecs.set(item.alias, codec);
25
75
  }
26
76
  }
77
+
78
+ if (item.expr.kind === 'column-ref') {
79
+ columnRefs.set(item.alias, { table: item.expr.table, column: item.expr.column });
80
+ } else if (item.expr.kind === 'subquery' || item.expr.kind === 'json-array-agg') {
81
+ includeAliases.add(item.alias);
82
+ }
27
83
  }
28
84
 
29
- return null;
85
+ return { aliases, codecs, columnRefs, includeAliases };
30
86
  }
31
87
 
32
- type ColumnRefIndex = Map<string, { table: string; column: string }>;
88
+ function previewWireValue(wireValue: unknown): string {
89
+ if (typeof wireValue === 'string') {
90
+ return wireValue.length > WIRE_PREVIEW_LIMIT
91
+ ? `${wireValue.substring(0, WIRE_PREVIEW_LIMIT)}...`
92
+ : wireValue;
93
+ }
94
+ return String(wireValue).substring(0, WIRE_PREVIEW_LIMIT);
95
+ }
33
96
 
34
- /**
35
- * Builds a lookup index from column name → { table, column } ref.
36
- * Called once per decodeRow invocation to avoid O(aliases × refs) linear scans.
37
- */
38
- function buildColumnRefIndex(plan: ExecutionPlan): ColumnRefIndex | null {
39
- const columns = plan.meta.refs?.columns;
40
- if (!columns) return null;
97
+ function isJsonSchemaValidationError(error: unknown): boolean {
98
+ return isRuntimeError(error) && error.code === 'RUNTIME.JSON_SCHEMA_VALIDATION_FAILED';
99
+ }
100
+
101
+ function wrapDecodeFailure(
102
+ error: unknown,
103
+ alias: string,
104
+ ref: ColumnRef | undefined,
105
+ codec: Codec,
106
+ wireValue: unknown,
107
+ ): never {
108
+ const message = error instanceof Error ? error.message : String(error);
109
+ const target = ref ? `${ref.table}.${ref.column}` : alias;
110
+ const wrapped = runtimeError(
111
+ 'RUNTIME.DECODE_FAILED',
112
+ `Failed to decode column ${target} with codec '${codec.id}': ${message}`,
113
+ {
114
+ ...(ref ? { table: ref.table, column: ref.column } : { alias }),
115
+ codec: codec.id,
116
+ wirePreview: previewWireValue(wireValue),
117
+ },
118
+ );
119
+ wrapped.cause = error;
120
+ throw wrapped;
121
+ }
122
+
123
+ function wrapIncludeAggregateFailure(error: unknown, alias: string, wireValue: unknown): never {
124
+ const message = error instanceof Error ? error.message : String(error);
125
+ const wrapped = runtimeError(
126
+ 'RUNTIME.DECODE_FAILED',
127
+ `Failed to parse JSON array for include alias '${alias}': ${message}`,
128
+ {
129
+ alias,
130
+ wirePreview: previewWireValue(wireValue),
131
+ },
132
+ );
133
+ wrapped.cause = error;
134
+ throw wrapped;
135
+ }
41
136
 
42
- const index: ColumnRefIndex = new Map();
43
- for (const ref of columns) {
44
- index.set(ref.column, ref);
137
+ function decodeIncludeAggregate(alias: string, wireValue: unknown): unknown {
138
+ if (wireValue === null || wireValue === undefined) {
139
+ return [];
140
+ }
141
+
142
+ try {
143
+ let parsed: unknown;
144
+ if (typeof wireValue === 'string') {
145
+ parsed = JSON.parse(wireValue);
146
+ } else if (Array.isArray(wireValue)) {
147
+ parsed = wireValue;
148
+ } else {
149
+ parsed = JSON.parse(String(wireValue));
150
+ }
151
+
152
+ if (!Array.isArray(parsed)) {
153
+ throw new Error(`Expected array for include alias '${alias}', got ${typeof parsed}`);
154
+ }
155
+
156
+ return parsed;
157
+ } catch (error) {
158
+ wrapIncludeAggregateFailure(error, alias, wireValue);
45
159
  }
46
- return index;
47
160
  }
48
161
 
49
- function parseProjectionRef(value: string): { table: string; column: string } | null {
50
- if (value.startsWith('include:') || value.startsWith('operation:')) {
162
+ /**
163
+ * Decodes a single field. Single-armed: every cell takes the same path —
164
+ * `codec.decode → await → JSON-Schema validate → return plain value` — so
165
+ * sync- and async-authored codecs are indistinguishable to callers.
166
+ *
167
+ * The row-level `rowCtx` is repackaged into a per-cell
168
+ * `SqlCodecCallContext` whose `column = { table, name }` is a structural
169
+ * projection of the per-cell `ColumnRef = { table, column }` resolved from
170
+ * the AST-backed `DecodeContext` (the same resolution `wrapDecodeFailure`
171
+ * uses for envelope construction — one resolution per cell, two consumers).
172
+ * Cells the runtime cannot resolve to a single underlying column (aggregate
173
+ * aliases, computed projections without a simple ref) get
174
+ * `column: undefined`, matching the spec contract that the runtime never
175
+ * silently defaults this field.
176
+ */
177
+ async function decodeField(
178
+ alias: string,
179
+ wireValue: unknown,
180
+ decodeCtx: DecodeContext,
181
+ jsonValidators: JsonSchemaValidatorRegistry | undefined,
182
+ rowCtx: SqlCodecCallContext,
183
+ ): Promise<unknown> {
184
+ if (wireValue === null) {
51
185
  return null;
52
186
  }
53
187
 
54
- const separatorIndex = value.indexOf('.');
55
- if (separatorIndex <= 0 || separatorIndex === value.length - 1) {
56
- return null;
188
+ const codec = decodeCtx.codecs.get(alias);
189
+ if (!codec) {
190
+ return wireValue;
57
191
  }
58
192
 
59
- return {
60
- table: value.slice(0, separatorIndex),
61
- column: value.slice(separatorIndex + 1),
62
- };
63
- }
193
+ const ref = decodeCtx.columnRefs.get(alias);
64
194
 
65
- function resolveColumnRefForAlias(
66
- alias: string,
67
- projection: ExecutionPlan['meta']['projection'],
68
- fallbackColumnRefIndex: ColumnRefIndex | null,
69
- ): { table: string; column: string } | undefined {
70
- if (projection && !Array.isArray(projection)) {
71
- const mappedRef = (projection as Record<string, string>)[alias];
72
- if (typeof mappedRef !== 'string') {
73
- return undefined;
195
+ // Per-cell ctx: the cell-level `column` is a `SqlColumnRef = { table, name }`
196
+ // projection of the resolved `ColumnRef = { table, column }` (same
197
+ // resolution `wrapDecodeFailure` uses below — no double work). Cells the
198
+ // runtime cannot resolve (aggregate aliases, computed projections without
199
+ // a simple ref) drop the `column` field entirely explicitly cleared so
200
+ // a previously-populated `rowCtx.column` cannot leak through to unrelated
201
+ // cells. Destructuring (rather than `column: undefined`) is required
202
+ // because `SqlCodecCallContext.column` is declared `column?: SqlColumnRef`
203
+ // under `exactOptionalPropertyTypes`.
204
+ let cellCtx: SqlCodecCallContext;
205
+ if (ref) {
206
+ cellCtx = { ...rowCtx, column: { table: ref.table, name: ref.column } };
207
+ } else {
208
+ const { column: _drop, ...rowCtxWithoutColumn } = rowCtx;
209
+ cellCtx = rowCtxWithoutColumn;
210
+ }
211
+
212
+ let decoded: unknown;
213
+ try {
214
+ decoded = await codec.decode(wireValue, cellCtx);
215
+ } catch (error) {
216
+ wrapDecodeFailure(error, alias, ref, codec, wireValue);
217
+ }
218
+
219
+ if (jsonValidators && ref) {
220
+ try {
221
+ validateJsonValue(jsonValidators, ref.table, ref.column, decoded, 'decode', codec.id);
222
+ } catch (error) {
223
+ if (isJsonSchemaValidationError(error)) throw error;
224
+ wrapDecodeFailure(error, alias, ref, codec, wireValue);
74
225
  }
75
- return parseProjectionRef(mappedRef) ?? undefined;
76
226
  }
77
227
 
78
- return fallbackColumnRefIndex?.get(alias);
228
+ return decoded;
79
229
  }
80
230
 
81
- export function decodeRow(
231
+ /**
232
+ * Decodes a row by dispatching all per-cell codec calls concurrently via
233
+ * `Promise.all`. Each cell follows the single-armed `decodeField` path.
234
+ * Failures are wrapped in `RUNTIME.DECODE_FAILED` with `{ table, column,
235
+ * codec }` (or `{ alias, codec }` when no column ref is resolvable) and the
236
+ * original error attached on `cause`.
237
+ *
238
+ * When `rowCtx.signal` is provided:
239
+ *
240
+ * - **Already-aborted at entry** short-circuits with `RUNTIME.ABORTED`
241
+ * (`{ phase: 'decode' }`) before any `codec.decode` call is made.
242
+ * - **Mid-flight aborts** race the per-cell `Promise.all` against the
243
+ * signal so the runtime returns promptly even when codec bodies ignore
244
+ * it. In-flight bodies that ignore the signal complete in the
245
+ * background (cooperative cancellation).
246
+ * - Existing `RUNTIME.DECODE_FAILED` envelopes from codec bodies pass
247
+ * through unchanged (no double wrap).
248
+ */
249
+ export async function decodeRow(
82
250
  row: Record<string, unknown>,
83
- plan: ExecutionPlan,
251
+ plan: SqlExecutionPlan,
84
252
  registry: CodecRegistry,
85
- jsonValidators?: JsonSchemaValidatorRegistry,
86
- ): Record<string, unknown> {
87
- const decoded: Record<string, unknown> = {};
88
- const projection = plan.meta.projection;
253
+ jsonValidators: JsonSchemaValidatorRegistry | undefined,
254
+ rowCtx: SqlCodecCallContext,
255
+ ): Promise<Record<string, unknown>> {
256
+ checkAborted(rowCtx, 'decode');
257
+ const signal = rowCtx.signal;
89
258
 
90
- // Fallback for plans that do not provide projection alias -> table.column mapping.
91
- const fallbackColumnRefIndex =
92
- jsonValidators && (!projection || Array.isArray(projection)) ? buildColumnRefIndex(plan) : null;
259
+ const decodeCtx = buildDecodeContext(plan, registry);
93
260
 
94
- let aliases: readonly string[];
95
- if (projection && !Array.isArray(projection)) {
96
- aliases = Object.keys(projection);
97
- } else if (projection && Array.isArray(projection)) {
98
- aliases = projection;
99
- } else {
100
- aliases = Object.keys(row);
101
- }
261
+ const aliases = decodeCtx.aliases ?? Object.keys(row);
102
262
 
103
- for (const alias of aliases) {
104
- const wireValue = row[alias];
105
-
106
- const projectionValue =
107
- projection && typeof projection === 'object' && !Array.isArray(projection)
108
- ? (projection as Record<string, string>)[alias]
109
- : undefined;
110
-
111
- if (typeof projectionValue === 'string' && projectionValue.startsWith('include:')) {
112
- if (wireValue === null || wireValue === undefined) {
113
- decoded[alias] = [];
114
- continue;
115
- }
116
-
117
- try {
118
- let parsed: unknown;
119
- if (typeof wireValue === 'string') {
120
- parsed = JSON.parse(wireValue);
121
- } else if (Array.isArray(wireValue)) {
122
- parsed = wireValue;
123
- } else {
124
- parsed = JSON.parse(String(wireValue));
125
- }
126
-
127
- if (!Array.isArray(parsed)) {
128
- throw new Error(`Expected array for include alias '${alias}', got ${typeof parsed}`);
129
- }
130
-
131
- decoded[alias] = parsed;
132
- } catch (error) {
133
- const decodeError = new Error(
134
- `Failed to parse JSON array for include alias '${alias}': ${error instanceof Error ? error.message : String(error)}`,
135
- ) as Error & {
136
- code: string;
137
- category: string;
138
- severity: string;
139
- details?: Record<string, unknown>;
140
- };
141
- decodeError.code = 'RUNTIME.DECODE_FAILED';
142
- decodeError.category = 'RUNTIME';
143
- decodeError.severity = 'error';
144
- decodeError.details = {
263
+ if (decodeCtx.aliases !== undefined) {
264
+ for (const alias of decodeCtx.aliases) {
265
+ if (!Object.hasOwn(row, alias)) {
266
+ throw runtimeError('RUNTIME.DECODE_FAILED', `Row missing projection alias "${alias}"`, {
145
267
  alias,
146
- wirePreview:
147
- typeof wireValue === 'string' && wireValue.length > 100
148
- ? `${wireValue.substring(0, 100)}...`
149
- : String(wireValue).substring(0, 100),
150
- };
151
- throw decodeError;
268
+ expectedAliases: decodeCtx.aliases,
269
+ presentKeys: Object.keys(row),
270
+ });
152
271
  }
153
- continue;
154
272
  }
273
+ }
155
274
 
156
- if (wireValue === null || wireValue === undefined) {
157
- decoded[alias] = wireValue;
158
- continue;
159
- }
275
+ const tasks: Promise<unknown>[] = [];
276
+ const includeIndices: { index: number; alias: string; value: unknown }[] = [];
160
277
 
161
- const codec = resolveRowCodec(alias, plan, registry);
278
+ for (let i = 0; i < aliases.length; i++) {
279
+ const alias = aliases[i] as string;
280
+ const wireValue = row[alias];
162
281
 
163
- if (!codec) {
164
- decoded[alias] = wireValue;
282
+ if (decodeCtx.includeAliases.has(alias)) {
283
+ includeIndices.push({ index: i, alias, value: wireValue });
284
+ tasks.push(Promise.resolve(undefined));
165
285
  continue;
166
286
  }
167
287
 
168
- try {
169
- const decodedValue = codec.decode(wireValue);
170
-
171
- // Validate decoded JSON value against schema
172
- if (jsonValidators) {
173
- const ref = resolveColumnRefForAlias(alias, projection, fallbackColumnRefIndex);
174
- if (ref) {
175
- validateJsonValue(
176
- jsonValidators,
177
- ref.table,
178
- ref.column,
179
- decodedValue,
180
- 'decode',
181
- codec.id,
182
- );
183
- }
184
- }
288
+ tasks.push(decodeField(alias, wireValue, decodeCtx, jsonValidators, rowCtx));
289
+ }
185
290
 
186
- decoded[alias] = decodedValue;
187
- } catch (error) {
188
- // Re-throw JSON schema validation errors as-is
189
- if (
190
- error instanceof Error &&
191
- 'code' in error &&
192
- (error as Error & { code: string }).code === 'RUNTIME.JSON_SCHEMA_VALIDATION_FAILED'
193
- ) {
194
- throw error;
195
- }
291
+ const settled = await raceAgainstAbort(Promise.all(tasks), signal, 'decode');
196
292
 
197
- const decodeError = new Error(
198
- `Failed to decode row alias '${alias}' with codec '${codec.id}': ${error instanceof Error ? error.message : String(error)}`,
199
- ) as Error & {
200
- code: string;
201
- category: string;
202
- severity: string;
203
- details?: Record<string, unknown>;
204
- };
205
- decodeError.code = 'RUNTIME.DECODE_FAILED';
206
- decodeError.category = 'RUNTIME';
207
- decodeError.severity = 'error';
208
- decodeError.details = {
209
- alias,
210
- codec: codec.id,
211
- wirePreview:
212
- typeof wireValue === 'string' && wireValue.length > 100
213
- ? `${wireValue.substring(0, 100)}...`
214
- : String(wireValue).substring(0, 100),
215
- };
216
- throw decodeError;
217
- }
293
+ // Include aggregates are decoded synchronously after concurrent codec
294
+ // dispatch settles, so any decode failures upstream propagate first.
295
+ for (const entry of includeIndices) {
296
+ settled[entry.index] = decodeIncludeAggregate(entry.alias, entry.value);
218
297
  }
219
298
 
299
+ const decoded: Record<string, unknown> = {};
300
+ for (let i = 0; i < aliases.length; i++) {
301
+ decoded[aliases[i] as string] = settled[i];
302
+ }
220
303
  return decoded;
221
304
  }