@prisma-next/sql-runtime 0.5.0-dev.7 → 0.5.0-dev.8
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-BQZSVXXt.mjs → exports-Cssiepsb.mjs} +163 -76
- package/dist/exports-Cssiepsb.mjs.map +1 -0
- package/dist/index-yb51L_1h.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/test/utils.d.mts.map +1 -1
- package/dist/test/utils.mjs +7 -2
- package/dist/test/utils.mjs.map +1 -1
- package/package.json +13 -14
- package/src/codecs/decoding.ts +167 -115
- package/src/codecs/encoding.ts +57 -20
- package/src/middleware/before-compile-chain.ts +32 -1
- package/src/sql-runtime.ts +5 -3
- package/dist/exports-BQZSVXXt.mjs.map +0 -1
- package/test/async-iterable-result.test.ts +0 -141
- package/test/before-compile-chain.test.ts +0 -223
- package/test/budgets.test.ts +0 -431
- package/test/context.types.test-d.ts +0 -68
- package/test/execution-stack.test.ts +0 -161
- package/test/json-schema-validation.test.ts +0 -571
- package/test/lints.test.ts +0 -160
- package/test/mutation-default-generators.test.ts +0 -254
- package/test/parameterized-types.test.ts +0 -529
- package/test/sql-context.test.ts +0 -384
- package/test/sql-family-adapter.test.ts +0 -103
- package/test/sql-runtime.test.ts +0 -792
- package/test/utils.ts +0 -297
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(_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, 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 // string | Date includes Date which is not assignable to JsonValue, so\n // the JSON round-trip pair must be supplied explicitly.\n encodeJson: (value: string | Date) => (value instanceof Date ? value.toISOString() : value),\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 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":";;;;;;;;;AAgCA,SAAS,sCAAsC;AAC7C,QAAO,oBAAoB,KAAK,QAAQ;EACtC;EACA,WAAW,WAAqC,WAAW,SAAS;GAAE;GAAI;GAAQ,GAAG,EAAE,IAAI,CAAC;EAC7F,EAAE;;;;;;;AAQL,eAAsB,sBAEpB,SAA2C,MAAmC;AAE9E,QAAOA,eAAkB,QAAQ,QAAa,KAAK,CAAC;;;;;;AAOtD,eAAsB,mBACpB,SACA,MACe;AACf,QAAO,mBAAmB,QAAQ,QAAQ,KAAK,CAAC;;;;;AAMlD,eAAsB,iBAAiB,QAAgB,WAAwC;AAC7F,KAAI,UAAU,OAAO,SAAS,GAAG;AAC/B,QAAM,OAAO,MAAM,UAAU,KAAK,CAAC,GAAG,UAAU,OAAO,CAAC;AACxD;;AAGF,OAAM,OAAO,MAAM,UAAU,IAAI;;;;;;AAOnC,eAAsB,kBACpB,QACA,UACA,SACe;AACf,OAAM,OAAO,MAAM,gDAAgD;AACnE,OAAM,OAAO,MAAM,qCAAqC;AAExD,OAAM,QAAQ,OAAO;AAErB,OAAM,iBAAiB,QAAQ,sBAAsB;AACrD,OAAM,iBAAiB,QAAQ,qBAAqB;AAOpD,OAAM,iBAAiB,QANT,oBAAoB;EAChC,aAAa,SAAS,QAAQ;EAC9B,aAAa,SAAS;EACtB,cAAc;EACd,kBAAkB;EACnB,CAAC,CACmC,OAAO;;;;;;AAO9C,eAAsB,wBACpB,QACA,UACe;AAOf,OAAM,iBAAiB,QANT,oBAAoB;EAChC,aAAa,SAAS,QAAQ;EAC9B,aAAa,SAAS;EACtB,cAAc;EACd,kBAAkB;EACnB,CAAC,CACmC,OAAO;;;;;;;AAQ9C,SAAgB,4BACd,SACyC;CACzC,MAAM,gBAAgB,QAAQ,QAAQ,QAAQ;AAC9C,QAAO;EACL,MAAM;EACN,IAAI;EACJ,SAAS;EACT,UAAU;EACV,UAAU;EACV,cAAc;EACd,2BAA2B,EAAE;EAC7B,2BAA2B;EAC3B,OAAO,QAA+C;AACpD,UAAO,OAAO,OAAO;IAAE,UAAU;IAAgB,UAAU;IAAqB,EAAE,QAAQ;;EAE7F;;;;;AAMH,SAAgB,6BAAqE;AACnF,QAAO;EACL,MAAM;EACN,IAAI;EACJ,SAAS;EACT,UAAU;EACV,UAAU;EACV,cAAc,qBAAqB;EACnC,2BAA2B,EAAE;EAC7B,SAAS;AACP,UAAO;IAAE,UAAU;IAAgB,UAAU;IAAqB;;EAErE;;;;;;;;;AAUH,SAAgB,kBACd,UACA,SACA,SAG6B;AAC7B,QAAO,uBAAuB;EAC5B;EACA,OAAO;GACL,QAAQ,4BAA4B;GACpC,SAAS,4BAA4B,QAAQ;GAC7C,gBAAgB,SAAS,kBAAkB,EAAE;GAC9C;EACF,CAAC;;AAGJ,SAAgB,wBAAwB,SAQrC;AAQD,QAAO,0BAPO,wBAAwB;EACpC,QAAQ,4BAA4B;EACpC,SAAS,4BAA4B,mBAAmB,CAAC;EACzD,QAAQ,SAAS;EACjB,gBAAgB,SAAS,kBAAkB,EAAE;EAC9C,CAAC,CAEqC;;;;;;;;;AAUzC,SAAgB,oBAAgF;CAC9F,MAAM,gBAAgB,qBAAqB;AAK3C,eAAc,SACZ,MAAM;EACJ,QAAQ;EACR,aAAa,CAAC,OAAO;EACrB,SAAS,UAAkB;EAC3B,SAAS,SAAiB;EAC3B,CAAC,CACH;AAED,eAAc,SACZ,MAAM;EACJ,QAAQ;EACR,aAAa,CAAC,OAAO;EACrB,SAAS,UAAkB;EAC3B,SAAS,SAAiB;EAC3B,CAAC,CACH;AAED,eAAc,SACZ,MAAM;EACJ,QAAQ;EACR,aAAa,CAAC,cAAc;EAC5B,SAAS,UAA0B,iBAAiB,OAAO,MAAM,aAAa,GAAG;EACjF,SAAS,SAAyB,gBAAgB,OAAO,OAAO,IAAI,KAAK,KAAK;EAG9E,aAAa,UAA0B,iBAAiB,OAAO,MAAM,aAAa,GAAG;EACrF,aAAa,SAAS;AACpB,OAAI,OAAO,SAAS,SAAU,OAAM,IAAI,MAAM,2BAA2B;AACzE,UAAO,IAAI,KAAK,KAAK;;EAExB,CAAC,CACH;AAED,QAAO;EACL,SAAS;GACP,IAAI;GACJ,QAAQ;GACR,cAAc,EAAE;GAChB,SAAS;AACP,WAAO;;GAET,sBAAsB;AACpB,WAAO;KACL,KAAK;KACL,QAAQ,CAAC,EAAE;KACZ;;GAEJ;EACD,MAAM,KAAgB,KAAsE;GAC1F,MAAM,UAAU,KAAK,UAAU,IAAI;AACnC,UAAO,OAAO,OAAO;IAAE,KAAK;IAAS,QAAQ,IAAI,SAAS,CAAC,GAAG,IAAI,OAAO,GAAG,EAAE;IAAE,CAAC;;EAEpF;;AAGH,SAAgB,mBACd,UAKsB;CACtB,MAAM,EAAE,WAAW,GAAG,SAAS;CAC/B,MAAM,mBAAmB,SAAS,KAAK,kBAAkB,kBAAkB;AAE3E,QAAO;EACL,QAAQ,KAAK,aAAa;EAC1B,cAAc,KAAK,mBAAmB;EACtC,SAAS,KAAK,aACV;GAAE,GAAG,KAAK;GAAY,aAAa;GAAkB,GACrD;GAAE,aAAa;GAAkB,QAAQ,EAAE;GAAE;EACjD,QAAQ,KAAK,aAAa,EAAE;EAC5B,OAAO,KAAK,YAAY,EAAE;EAC1B,cAAc,KAAK,mBAAmB,EAAE;EACxC,gBAAgB,KAAK,qBAAqB,EAAE;EAC5C,MAAM,KAAK,WAAW,EAAE;EACxB,GAAI,YAAY,EAAE,WAAW,GAAG,EAAE;EAClC,aAAa,YAAY,KAAK,kBAAkB,qBAAqB;EACtE"}
|
package/package.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/sql-runtime",
|
|
3
|
-
"version": "0.5.0-dev.
|
|
3
|
+
"version": "0.5.0-dev.8",
|
|
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.
|
|
10
|
-
"@prisma-next/utils": "0.5.0-dev.
|
|
11
|
-
"@prisma-next/
|
|
12
|
-
"@prisma-next/
|
|
13
|
-
"@prisma-next/
|
|
14
|
-
"@prisma-next/
|
|
15
|
-
"@prisma-next/sql-
|
|
16
|
-
"@prisma-next/
|
|
17
|
-
"@prisma-next/sql-relational-core": "0.5.0-dev.
|
|
9
|
+
"@prisma-next/contract": "0.5.0-dev.8",
|
|
10
|
+
"@prisma-next/utils": "0.5.0-dev.8",
|
|
11
|
+
"@prisma-next/ids": "0.5.0-dev.8",
|
|
12
|
+
"@prisma-next/framework-components": "0.5.0-dev.8",
|
|
13
|
+
"@prisma-next/operations": "0.5.0-dev.8",
|
|
14
|
+
"@prisma-next/runtime-executor": "0.5.0-dev.8",
|
|
15
|
+
"@prisma-next/sql-contract": "0.5.0-dev.8",
|
|
16
|
+
"@prisma-next/sql-operations": "0.5.0-dev.8",
|
|
17
|
+
"@prisma-next/sql-relational-core": "0.5.0-dev.8"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/pg": "8.16.0",
|
|
@@ -22,14 +22,13 @@
|
|
|
22
22
|
"tsdown": "0.18.4",
|
|
23
23
|
"typescript": "5.9.3",
|
|
24
24
|
"vitest": "4.0.17",
|
|
25
|
-
"@prisma-next/test-utils": "0.0.1",
|
|
26
25
|
"@prisma-next/tsconfig": "0.0.0",
|
|
27
|
-
"@prisma-next/tsdown": "0.0.0"
|
|
26
|
+
"@prisma-next/tsdown": "0.0.0",
|
|
27
|
+
"@prisma-next/test-utils": "0.0.1"
|
|
28
28
|
},
|
|
29
29
|
"files": [
|
|
30
30
|
"dist",
|
|
31
|
-
"src"
|
|
32
|
-
"test"
|
|
31
|
+
"src"
|
|
33
32
|
],
|
|
34
33
|
"exports": {
|
|
35
34
|
".": "./dist/index.mjs",
|
package/src/codecs/decoding.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import type { ExecutionPlan } from '@prisma-next/contract/types';
|
|
2
|
+
import { isRuntimeError, runtimeError } from '@prisma-next/framework-components/runtime';
|
|
2
3
|
import type { Codec, CodecRegistry } from '@prisma-next/sql-relational-core/ast';
|
|
3
4
|
import type { JsonSchemaValidatorRegistry } from '@prisma-next/sql-relational-core/query-lane-context';
|
|
4
5
|
import { validateJsonValue } from './json-schema-validation';
|
|
5
6
|
|
|
7
|
+
type ColumnRef = { table: string; column: string };
|
|
8
|
+
type ColumnRefIndex = Map<string, ColumnRef>;
|
|
9
|
+
|
|
10
|
+
const WIRE_PREVIEW_LIMIT = 100;
|
|
11
|
+
|
|
6
12
|
function resolveRowCodec(
|
|
7
13
|
alias: string,
|
|
8
14
|
plan: ExecutionPlan,
|
|
@@ -29,12 +35,6 @@ function resolveRowCodec(
|
|
|
29
35
|
return null;
|
|
30
36
|
}
|
|
31
37
|
|
|
32
|
-
type ColumnRefIndex = Map<string, { table: string; column: string }>;
|
|
33
|
-
|
|
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
38
|
function buildColumnRefIndex(plan: ExecutionPlan): ColumnRefIndex | null {
|
|
39
39
|
const columns = plan.meta.refs?.columns;
|
|
40
40
|
if (!columns) return null;
|
|
@@ -46,7 +46,7 @@ function buildColumnRefIndex(plan: ExecutionPlan): ColumnRefIndex | null {
|
|
|
46
46
|
return index;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
function parseProjectionRef(value: string):
|
|
49
|
+
function parseProjectionRef(value: string): ColumnRef | null {
|
|
50
50
|
if (value.startsWith('include:') || value.startsWith('operation:')) {
|
|
51
51
|
return null;
|
|
52
52
|
}
|
|
@@ -66,7 +66,7 @@ function resolveColumnRefForAlias(
|
|
|
66
66
|
alias: string,
|
|
67
67
|
projection: ExecutionPlan['meta']['projection'],
|
|
68
68
|
fallbackColumnRefIndex: ColumnRefIndex | null,
|
|
69
|
-
):
|
|
69
|
+
): ColumnRef | undefined {
|
|
70
70
|
if (projection && !Array.isArray(projection)) {
|
|
71
71
|
const mappedRef = (projection as Record<string, string>)[alias];
|
|
72
72
|
if (typeof mappedRef !== 'string') {
|
|
@@ -78,18 +78,144 @@ function resolveColumnRefForAlias(
|
|
|
78
78
|
return fallbackColumnRefIndex?.get(alias);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
function previewWireValue(wireValue: unknown): string {
|
|
82
|
+
if (typeof wireValue === 'string') {
|
|
83
|
+
return wireValue.length > WIRE_PREVIEW_LIMIT
|
|
84
|
+
? `${wireValue.substring(0, WIRE_PREVIEW_LIMIT)}...`
|
|
85
|
+
: wireValue;
|
|
86
|
+
}
|
|
87
|
+
return String(wireValue).substring(0, WIRE_PREVIEW_LIMIT);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isJsonSchemaValidationError(error: unknown): boolean {
|
|
91
|
+
return isRuntimeError(error) && error.code === 'RUNTIME.JSON_SCHEMA_VALIDATION_FAILED';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function wrapDecodeFailure(
|
|
95
|
+
error: unknown,
|
|
96
|
+
alias: string,
|
|
97
|
+
ref: ColumnRef | undefined,
|
|
98
|
+
codec: Codec,
|
|
99
|
+
wireValue: unknown,
|
|
100
|
+
): never {
|
|
101
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
102
|
+
const target = ref ? `${ref.table}.${ref.column}` : alias;
|
|
103
|
+
const wrapped = runtimeError(
|
|
104
|
+
'RUNTIME.DECODE_FAILED',
|
|
105
|
+
`Failed to decode column ${target} with codec '${codec.id}': ${message}`,
|
|
106
|
+
{
|
|
107
|
+
...(ref ? { table: ref.table, column: ref.column } : { alias }),
|
|
108
|
+
codec: codec.id,
|
|
109
|
+
wirePreview: previewWireValue(wireValue),
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
wrapped.cause = error;
|
|
113
|
+
throw wrapped;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function wrapIncludeAggregateFailure(error: unknown, alias: string, wireValue: unknown): never {
|
|
117
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
118
|
+
const wrapped = runtimeError(
|
|
119
|
+
'RUNTIME.DECODE_FAILED',
|
|
120
|
+
`Failed to parse JSON array for include alias '${alias}': ${message}`,
|
|
121
|
+
{
|
|
122
|
+
alias,
|
|
123
|
+
wirePreview: previewWireValue(wireValue),
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
wrapped.cause = error;
|
|
127
|
+
throw wrapped;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function decodeIncludeAggregate(alias: string, wireValue: unknown): unknown {
|
|
131
|
+
if (wireValue === null || wireValue === undefined) {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
let parsed: unknown;
|
|
137
|
+
if (typeof wireValue === 'string') {
|
|
138
|
+
parsed = JSON.parse(wireValue);
|
|
139
|
+
} else if (Array.isArray(wireValue)) {
|
|
140
|
+
parsed = wireValue;
|
|
141
|
+
} else {
|
|
142
|
+
parsed = JSON.parse(String(wireValue));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!Array.isArray(parsed)) {
|
|
146
|
+
throw new Error(`Expected array for include alias '${alias}', got ${typeof parsed}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return parsed;
|
|
150
|
+
} catch (error) {
|
|
151
|
+
wrapIncludeAggregateFailure(error, alias, wireValue);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Decodes a single field. Single-armed: every cell takes the same path —
|
|
157
|
+
* `codec.decode → await → JSON-Schema validate → return plain value` — so
|
|
158
|
+
* sync- and async-authored codecs are indistinguishable to callers.
|
|
159
|
+
*/
|
|
160
|
+
async function decodeField(
|
|
161
|
+
alias: string,
|
|
162
|
+
wireValue: unknown,
|
|
163
|
+
plan: ExecutionPlan,
|
|
164
|
+
registry: CodecRegistry,
|
|
165
|
+
jsonValidators: JsonSchemaValidatorRegistry | undefined,
|
|
166
|
+
projection: ExecutionPlan['meta']['projection'],
|
|
167
|
+
fallbackColumnRefIndex: ColumnRefIndex | null,
|
|
168
|
+
): Promise<unknown> {
|
|
169
|
+
if (wireValue === null || wireValue === undefined) {
|
|
170
|
+
return wireValue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const codec = resolveRowCodec(alias, plan, registry);
|
|
174
|
+
if (!codec) {
|
|
175
|
+
return wireValue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const ref = resolveColumnRefForAlias(alias, projection, fallbackColumnRefIndex);
|
|
179
|
+
|
|
180
|
+
let decoded: unknown;
|
|
181
|
+
try {
|
|
182
|
+
decoded = await codec.decode(wireValue);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
wrapDecodeFailure(error, alias, ref, codec, wireValue);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (jsonValidators && ref) {
|
|
188
|
+
try {
|
|
189
|
+
validateJsonValue(jsonValidators, ref.table, ref.column, decoded, 'decode', codec.id);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
if (isJsonSchemaValidationError(error)) throw error;
|
|
192
|
+
wrapDecodeFailure(error, alias, ref, codec, wireValue);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return decoded;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Decodes a row by dispatching all per-cell codec calls concurrently via
|
|
201
|
+
* `Promise.all`. Each cell follows the single-armed `decodeField` path.
|
|
202
|
+
* Failures are wrapped in `RUNTIME.DECODE_FAILED` with `{ table, column,
|
|
203
|
+
* codec }` (or `{ alias, codec }` when no column ref is resolvable) and the
|
|
204
|
+
* original error attached on `cause`.
|
|
205
|
+
*/
|
|
206
|
+
export async function decodeRow(
|
|
82
207
|
row: Record<string, unknown>,
|
|
83
208
|
plan: ExecutionPlan,
|
|
84
209
|
registry: CodecRegistry,
|
|
85
210
|
jsonValidators?: JsonSchemaValidatorRegistry,
|
|
86
|
-
): Record<string, unknown
|
|
87
|
-
const decoded: Record<string, unknown> = {};
|
|
211
|
+
): Promise<Record<string, unknown>> {
|
|
88
212
|
const projection = plan.meta.projection;
|
|
89
213
|
|
|
90
|
-
//
|
|
214
|
+
// Build a column-ref index when the projection alias-to-ref mapping is
|
|
215
|
+
// unavailable so that decode failures and JSON-Schema validation can both
|
|
216
|
+
// surface { table, column } from `meta.refs.columns` when present.
|
|
91
217
|
const fallbackColumnRefIndex =
|
|
92
|
-
|
|
218
|
+
!projection || Array.isArray(projection) ? buildColumnRefIndex(plan) : null;
|
|
93
219
|
|
|
94
220
|
let aliases: readonly string[];
|
|
95
221
|
if (projection && !Array.isArray(projection)) {
|
|
@@ -100,7 +226,11 @@ export function decodeRow(
|
|
|
100
226
|
aliases = Object.keys(row);
|
|
101
227
|
}
|
|
102
228
|
|
|
103
|
-
|
|
229
|
+
const tasks: Promise<unknown>[] = [];
|
|
230
|
+
const includeIndices: { index: number; alias: string; value: unknown }[] = [];
|
|
231
|
+
|
|
232
|
+
for (let i = 0; i < aliases.length; i++) {
|
|
233
|
+
const alias = aliases[i] as string;
|
|
104
234
|
const wireValue = row[alias];
|
|
105
235
|
|
|
106
236
|
const projectionValue =
|
|
@@ -109,113 +239,35 @@ export function decodeRow(
|
|
|
109
239
|
: undefined;
|
|
110
240
|
|
|
111
241
|
if (typeof projectionValue === 'string' && projectionValue.startsWith('include:')) {
|
|
112
|
-
|
|
113
|
-
|
|
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 = {
|
|
145
|
-
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;
|
|
152
|
-
}
|
|
242
|
+
includeIndices.push({ index: i, alias, value: wireValue });
|
|
243
|
+
tasks.push(Promise.resolve(undefined));
|
|
153
244
|
continue;
|
|
154
245
|
}
|
|
155
246
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
}
|
|
247
|
+
tasks.push(
|
|
248
|
+
decodeField(
|
|
249
|
+
alias,
|
|
250
|
+
wireValue,
|
|
251
|
+
plan,
|
|
252
|
+
registry,
|
|
253
|
+
jsonValidators,
|
|
254
|
+
projection,
|
|
255
|
+
fallbackColumnRefIndex,
|
|
256
|
+
),
|
|
257
|
+
);
|
|
258
|
+
}
|
|
185
259
|
|
|
186
|
-
|
|
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
|
-
}
|
|
260
|
+
const settled = await Promise.all(tasks);
|
|
196
261
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
}
|
|
262
|
+
// Include aggregates are decoded synchronously after concurrent codec
|
|
263
|
+
// dispatch settles, so any decode failures upstream propagate first.
|
|
264
|
+
for (const entry of includeIndices) {
|
|
265
|
+
settled[entry.index] = decodeIncludeAggregate(entry.alias, entry.value);
|
|
218
266
|
}
|
|
219
267
|
|
|
268
|
+
const decoded: Record<string, unknown> = {};
|
|
269
|
+
for (let i = 0; i < aliases.length; i++) {
|
|
270
|
+
decoded[aliases[i] as string] = settled[i];
|
|
271
|
+
}
|
|
220
272
|
return decoded;
|
|
221
273
|
}
|
package/src/codecs/encoding.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExecutionPlan, ParamDescriptor } from '@prisma-next/contract/types';
|
|
2
|
+
import { runtimeError } from '@prisma-next/framework-components/runtime';
|
|
2
3
|
import type { Codec, CodecRegistry } from '@prisma-next/sql-relational-core/ast';
|
|
3
4
|
|
|
4
5
|
function resolveParamCodec(
|
|
@@ -15,12 +16,40 @@ function resolveParamCodec(
|
|
|
15
16
|
return null;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
function paramLabel(paramDescriptor: ParamDescriptor, paramIndex: number): string {
|
|
20
|
+
return paramDescriptor.name ?? `param[${paramIndex}]`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function wrapEncodeFailure(
|
|
24
|
+
error: unknown,
|
|
25
|
+
paramDescriptor: ParamDescriptor,
|
|
26
|
+
paramIndex: number,
|
|
27
|
+
codecId: string,
|
|
28
|
+
): never {
|
|
29
|
+
const label = paramLabel(paramDescriptor, paramIndex);
|
|
30
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
31
|
+
const wrapped = runtimeError(
|
|
32
|
+
'RUNTIME.ENCODE_FAILED',
|
|
33
|
+
`Failed to encode parameter ${label} with codec '${codecId}': ${message}`,
|
|
34
|
+
{ label, codec: codecId, paramIndex },
|
|
35
|
+
);
|
|
36
|
+
wrapped.cause = error;
|
|
37
|
+
throw wrapped;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Encodes a single parameter through its codec. Always awaits codec.encode so
|
|
42
|
+
* a Promise can never leak into the driver, even if a sync-authored codec is
|
|
43
|
+
* lifted to async by the codec() factory. Failures are wrapped in
|
|
44
|
+
* `RUNTIME.ENCODE_FAILED` with `{ label, codec, paramIndex }` and the original
|
|
45
|
+
* error attached on `cause`.
|
|
46
|
+
*/
|
|
47
|
+
export async function encodeParam(
|
|
19
48
|
value: unknown,
|
|
20
49
|
paramDescriptor: ParamDescriptor,
|
|
21
50
|
paramIndex: number,
|
|
22
51
|
registry: CodecRegistry,
|
|
23
|
-
): unknown {
|
|
52
|
+
): Promise<unknown> {
|
|
24
53
|
if (value === null || value === undefined) {
|
|
25
54
|
return null;
|
|
26
55
|
}
|
|
@@ -30,37 +59,45 @@ export function encodeParam(
|
|
|
30
59
|
return value;
|
|
31
60
|
}
|
|
32
61
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const label = paramDescriptor.name ?? `param[${paramIndex}]`;
|
|
38
|
-
throw new Error(
|
|
39
|
-
`Failed to encode parameter ${label}: ${error instanceof Error ? error.message : String(error)}`,
|
|
40
|
-
);
|
|
41
|
-
}
|
|
62
|
+
try {
|
|
63
|
+
return await codec.encode(value);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
wrapEncodeFailure(error, paramDescriptor, paramIndex, codec.id);
|
|
42
66
|
}
|
|
43
|
-
|
|
44
|
-
return value;
|
|
45
67
|
}
|
|
46
68
|
|
|
47
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Encodes all parameters concurrently via `Promise.all`. Per parameter, sync-
|
|
71
|
+
* and async-authored codecs share the same path: `codec.encode → await →
|
|
72
|
+
* return`. Param-level failures are wrapped in `RUNTIME.ENCODE_FAILED`.
|
|
73
|
+
*/
|
|
74
|
+
export async function encodeParams(
|
|
75
|
+
plan: ExecutionPlan,
|
|
76
|
+
registry: CodecRegistry,
|
|
77
|
+
): Promise<readonly unknown[]> {
|
|
48
78
|
if (plan.params.length === 0) {
|
|
49
79
|
return plan.params;
|
|
50
80
|
}
|
|
51
81
|
|
|
52
|
-
const
|
|
82
|
+
const descriptorCount = plan.meta.paramDescriptors.length;
|
|
83
|
+
const paramCount = plan.params.length;
|
|
53
84
|
|
|
54
|
-
|
|
85
|
+
const tasks: Promise<unknown>[] = new Array(paramCount);
|
|
86
|
+
for (let i = 0; i < paramCount; i++) {
|
|
55
87
|
const paramValue = plan.params[i];
|
|
56
88
|
const paramDescriptor = plan.meta.paramDescriptors[i];
|
|
57
89
|
|
|
58
|
-
if (paramDescriptor) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
90
|
+
if (!paramDescriptor) {
|
|
91
|
+
throw runtimeError(
|
|
92
|
+
'RUNTIME.MISSING_PARAM_DESCRIPTOR',
|
|
93
|
+
`Missing paramDescriptor for parameter at index ${i} (plan has ${paramCount} params, ${descriptorCount} descriptors). The planner must emit one descriptor per param; this is a contract violation.`,
|
|
94
|
+
{ paramIndex: i, paramCount, descriptorCount },
|
|
95
|
+
);
|
|
62
96
|
}
|
|
97
|
+
|
|
98
|
+
tasks[i] = encodeParam(paramValue, paramDescriptor, i, registry);
|
|
63
99
|
}
|
|
64
100
|
|
|
101
|
+
const encoded = await Promise.all(tasks);
|
|
65
102
|
return Object.freeze(encoded);
|
|
66
103
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ParamDescriptor, PlanMeta } from '@prisma-next/contract/types';
|
|
2
|
+
import type { AnyQueryAst } from '@prisma-next/sql-relational-core/ast';
|
|
1
3
|
import type { DraftPlan, SqlMiddleware, SqlMiddlewareContext } from './sql-middleware';
|
|
2
4
|
|
|
3
5
|
export async function runBeforeCompileChain(
|
|
@@ -24,5 +26,34 @@ export async function runBeforeCompileChain(
|
|
|
24
26
|
});
|
|
25
27
|
current = result;
|
|
26
28
|
}
|
|
27
|
-
|
|
29
|
+
|
|
30
|
+
if (current.ast === initial.ast) {
|
|
31
|
+
return current;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// The rewritten AST may have introduced, removed, or replaced ParamRefs, so
|
|
35
|
+
// the descriptors collected at lane build time no longer line up with what
|
|
36
|
+
// the adapter will emit when it walks the new AST. Re-derive descriptors
|
|
37
|
+
// from the rewritten AST so `params` and `paramDescriptors` stay in lockstep
|
|
38
|
+
// by the time `encodeParams` runs.
|
|
39
|
+
const paramDescriptors = deriveParamDescriptorsFromAst(current.ast);
|
|
40
|
+
const meta: PlanMeta = { ...current.meta, paramDescriptors };
|
|
41
|
+
return { ast: current.ast, meta };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function deriveParamDescriptorsFromAst(ast: AnyQueryAst): ReadonlyArray<ParamDescriptor> {
|
|
45
|
+
const refs = ast.collectParamRefs();
|
|
46
|
+
const seen = new Set<unknown>();
|
|
47
|
+
const descriptors: ParamDescriptor[] = [];
|
|
48
|
+
for (const ref of refs) {
|
|
49
|
+
if (seen.has(ref)) continue;
|
|
50
|
+
seen.add(ref);
|
|
51
|
+
descriptors.push({
|
|
52
|
+
index: descriptors.length + 1,
|
|
53
|
+
...(ref.name !== undefined ? { name: ref.name } : {}),
|
|
54
|
+
source: 'dsl',
|
|
55
|
+
...(ref.codecId !== undefined ? { codecId: ref.codecId } : {}),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return descriptors;
|
|
28
59
|
}
|
package/src/sql-runtime.ts
CHANGED
|
@@ -191,7 +191,9 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
|
|
|
191
191
|
);
|
|
192
192
|
|
|
193
193
|
const planToLower: SqlQueryPlan<Row> =
|
|
194
|
-
rewrittenDraft.ast === plan.ast
|
|
194
|
+
rewrittenDraft.ast === plan.ast
|
|
195
|
+
? plan
|
|
196
|
+
: { ...plan, ast: rewrittenDraft.ast, meta: rewrittenDraft.meta };
|
|
195
197
|
|
|
196
198
|
return lowerSqlPlan(this.adapter, this.contract, planToLower);
|
|
197
199
|
}
|
|
@@ -206,7 +208,7 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
|
|
|
206
208
|
self: SqlRuntimeImpl<TContract>,
|
|
207
209
|
): AsyncGenerator<Row, void, unknown> {
|
|
208
210
|
const executablePlan = await self.toExecutionPlan(plan);
|
|
209
|
-
const encodedParams = encodeParams(executablePlan, self.codecRegistry);
|
|
211
|
+
const encodedParams = await encodeParams(executablePlan, self.codecRegistry);
|
|
210
212
|
const planWithEncodedParams: ExecutionPlan<Row> = {
|
|
211
213
|
...executablePlan,
|
|
212
214
|
params: encodedParams,
|
|
@@ -215,7 +217,7 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
|
|
|
215
217
|
const coreIterator = queryable.execute(planWithEncodedParams);
|
|
216
218
|
|
|
217
219
|
for await (const rawRow of coreIterator) {
|
|
218
|
-
const decodedRow = decodeRow(
|
|
220
|
+
const decodedRow = await decodeRow(
|
|
219
221
|
rawRow as Record<string, unknown>,
|
|
220
222
|
executablePlan,
|
|
221
223
|
self.codecRegistry,
|