diff --git a/packages/1-framework/1-core/framework-components/src/shared/resolve-codec.ts b/packages/1-framework/1-core/framework-components/src/shared/resolve-codec.ts index 200f26a65bd5..68fb58d3f8c2 100644 --- a/packages/1-framework/1-core/framework-components/src/shared/resolve-codec.ts +++ b/packages/1-framework/1-core/framework-components/src/shared/resolve-codec.ts @@ -66,12 +66,9 @@ export function validateCodecTypeParams(descriptor: AnyCodecDescriptor, ref: Cod /** * Resolves a `Codec` instance: validates `ref.typeParams` via - * {@link validateCodecTypeParams} then calls `descriptor.factory(validated)(ctx)`. - * - * The descriptor's `factory` is typed against its own `P`; the registry erases - * `P` to `any`, so the factory is narrowed to `(params: unknown) => (ctx) => Codec` - * at the call boundary. The `paramsSchema` validates the input above before we - * forward it, so the narrowing is safe by construction. + * {@link validateCodecTypeParams} then calls `descriptor.factory(validated)(ctx)` + * as a method on `descriptor`, preserving `this` for factories that build + * their returned codec from the descriptor instance (e.g. `new XCodec(this)`). */ export function materializeCodec( descriptor: AnyCodecDescriptor, @@ -79,8 +76,5 @@ export function materializeCodec( ctx: CodecInstanceContext, ): Codec { const validated = validateCodecTypeParams(descriptor, ref); - return blindCast< - (params: unknown) => (ctx: CodecInstanceContext) => Codec, - 'registry erases P to any; paramsSchema validates input before forwarding' - >(descriptor.factory)(validated)(ctx); + return descriptor.factory(validated)(ctx); } diff --git a/packages/1-framework/1-core/framework-components/test/materialize-codec.test.ts b/packages/1-framework/1-core/framework-components/test/materialize-codec.test.ts new file mode 100644 index 000000000000..f57fefef9e06 --- /dev/null +++ b/packages/1-framework/1-core/framework-components/test/materialize-codec.test.ts @@ -0,0 +1,125 @@ +import type { JsonValue } from '@internal/contract/types'; +import type { StandardSchemaV1 } from '@standard-schema/spec'; +import { test } from 'vitest'; +import { + type AnyCodecDescriptor, + type CodecCallContext, + type CodecDescriptor, + CodecDescriptorImpl, + CodecImpl, + type CodecInstanceContext, + type CodecRef, + type CodecTrait, + materializeCodec, + voidParamsSchema, +} from '../src/exports/codec'; + +class Int4FixtureCodec extends CodecImpl<'demo/int4@1', readonly ['equality'], number, number> { + async encode(value: number, _ctx: CodecCallContext): Promise { + return value; + } + async decode(wire: number, _ctx: CodecCallContext): Promise { + return wire; + } + encodeJson(value: number): JsonValue { + return value; + } + decodeJson(json: JsonValue): number { + return json as number; + } +} + +class Int4FixtureDescriptor extends CodecDescriptorImpl { + override readonly codecId = 'demo/int4@1' as const; + override readonly traits: readonly CodecTrait[] = ['equality']; + override readonly targetTypes: readonly string[] = ['int4']; + override readonly paramsSchema: StandardSchemaV1 = voidParamsSchema; + override factory(): (ctx: CodecInstanceContext) => Int4FixtureCodec { + return () => new Int4FixtureCodec(this); + } +} + +const int4FixtureDescriptor = new Int4FixtureDescriptor(); + +type VectorParams = { readonly length: number }; +const vectorFixtureParamsSchema: StandardSchemaV1 = { + '~standard': { + version: 1, + vendor: 'demo', + validate: (input) => ({ value: input as VectorParams }), + }, +}; + +class VectorFixtureCodec extends CodecImpl< + 'demo/vector@1', + readonly ['equality'], + string, + number[] +> { + constructor( + descriptor: CodecDescriptor, + public readonly dimension: N, + ) { + super(descriptor); + } + async encode(value: number[], _ctx: CodecCallContext): Promise { + return `[${value.join(',')}]`; + } + async decode(wire: string, _ctx: CodecCallContext): Promise { + return wire.slice(1, -1).split(',').map(Number); + } + encodeJson(value: number[]): JsonValue { + return value; + } + decodeJson(json: JsonValue): number[] { + return json as number[]; + } +} + +class VectorFixtureDescriptor extends CodecDescriptorImpl { + override readonly codecId = 'demo/vector@1' as const; + override readonly traits: readonly CodecTrait[] = ['equality']; + override readonly targetTypes: readonly string[] = ['vector']; + override readonly paramsSchema = vectorFixtureParamsSchema; + override factory(params: { + readonly length: N; + }): (ctx: CodecInstanceContext) => VectorFixtureCodec { + return () => new VectorFixtureCodec(this, params.length); + } +} + +const vectorFixtureDescriptor = new VectorFixtureDescriptor(); + +const stubCtx = {} as CodecInstanceContext; + +function descriptorFor(ref: CodecRef): AnyCodecDescriptor { + if (ref.codecId === int4FixtureDescriptor.codecId) return int4FixtureDescriptor; + if (ref.codecId === vectorFixtureDescriptor.codecId) return vectorFixtureDescriptor; + throw new Error(`no fixture descriptor for ${ref.codecId}`); +} + +test('materializeCodec resolves a non-parameterized codec whose id reads the descriptor codecId', ({ + expect, +}) => { + const ref: CodecRef = { codecId: 'demo/int4@1' }; + const codec = materializeCodec(descriptorFor(ref), ref, stubCtx); + expect(codec.id).toBe('demo/int4@1'); +}); + +test('materializeCodec resolves a parameterized codec whose id reads the descriptor codecId', ({ + expect, +}) => { + const ref: CodecRef = { codecId: 'demo/vector@1', typeParams: { length: 1536 } }; + const codec = materializeCodec(descriptorFor(ref), ref, stubCtx); + expect(codec.id).toBe('demo/vector@1'); +}); + +test('materializeCodec produces a codec whose encode/decode still run through the descriptor-bound factory', async ({ + expect, +}) => { + const ref: CodecRef = { codecId: 'demo/vector@1', typeParams: { length: 3 } }; + const codec = materializeCodec(descriptorFor(ref), ref, stubCtx); + const wire = await codec.encode([1, 2, 3], {}); + expect(wire).toBe('[1,2,3]'); + expect(await codec.decode(wire, {})).toEqual([1, 2, 3]); +}); diff --git a/packages/3-targets/6-adapters/postgres/src/core/sql-renderer.ts b/packages/3-targets/6-adapters/postgres/src/core/sql-renderer.ts index 4c9def468e75..a7f98ae17bf4 100644 --- a/packages/3-targets/6-adapters/postgres/src/core/sql-renderer.ts +++ b/packages/3-targets/6-adapters/postgres/src/core/sql-renderer.ts @@ -40,7 +40,7 @@ import { } from '@internal/sql-relational-core/ast'; import type { PostgresCodecDescriptorRegistry } from '@internal/target-postgres/codec-descriptor'; import { PG_ENUM_CODEC_ID } from '@internal/target-postgres/codec-ids'; -import { isPgEnumParams } from '@internal/target-postgres/codecs'; +import { isPgEnumParams, pgEnumDescriptor } from '@internal/target-postgres/codecs'; import { escapeLiteral, quoteIdentifier, @@ -379,6 +379,10 @@ function renderOrderByExpr( return renderExpr(expr, contract, pim); } +function projectsNativeEnumArray(codec: CodecRef | undefined): boolean { + return codec?.many === true && codec.codecId === pgEnumDescriptor.codecId; +} + function renderProjection( projection: ReadonlyArray, contract: PostgresContract, @@ -390,7 +394,9 @@ function renderProjection( if (item.expr.kind === 'literal') { return `${renderLiteral(item.expr)} AS ${alias}`; } - return `${renderExpr(item.expr, contract, pim)} AS ${alias}`; + const rendered = renderExpr(item.expr, contract, pim); + const cast = projectsNativeEnumArray(item.codec) ? '::text[]' : ''; + return `${rendered}${cast} AS ${alias}`; }) .join(', '); } @@ -403,8 +409,9 @@ function renderReturning( return items .map((item) => { if (item.expr.kind === 'column-ref') { - const rendered = renderColumn(item.expr); - return item.expr.column === item.alias + const cast = projectsNativeEnumArray(item.codec) ? '::text[]' : ''; + const rendered = `${renderColumn(item.expr)}${cast}`; + return item.expr.column === item.alias && cast === '' ? rendered : `${rendered} AS ${quoteIdentifier(item.alias)}`; } diff --git a/packages/3-targets/6-adapters/postgres/test/sql-renderer.enum-array-projection-cast.test.ts b/packages/3-targets/6-adapters/postgres/test/sql-renderer.enum-array-projection-cast.test.ts new file mode 100644 index 000000000000..48ff4acb1a13 --- /dev/null +++ b/packages/3-targets/6-adapters/postgres/test/sql-renderer.enum-array-projection-cast.test.ts @@ -0,0 +1,136 @@ +import { + ColumnRef, + InsertAst, + ParamRef, + ProjectionItem, + SelectAst, + TableSource, +} from '@internal/sql-relational-core/ast'; +import { postgresCodecDescriptorRegistry } from '@internal/target-postgres/codecs'; +import { applicationDomainOf } from '@repo/test-utils'; +import { describe, expect, it } from 'vitest'; +import { TestSqlContractSerializer as SqlContractSerializer } from '../../../../2-sql/9-family/test/test-sql-contract-serializer'; +import { renderLoweredSql } from '../src/core/sql-renderer'; +import type { PostgresContract } from '../src/core/types'; + +const baseContract = new SqlContractSerializer().deserializeContract({ + target: 'postgres', + targetFamily: 'sql', + profileHash: 'sha256:enum-array-projection-cast-test', + roots: {}, + capabilities: { returning: { enabled: true } }, + extensions: {}, + meta: {}, + storage: { + storageHash: 'sha256:enum-array-projection-cast', + namespaces: { + __unbound__: { + id: '__unbound__', + entries: { + table: { + probe: { + columns: { + id: { codecId: 'pg/text@1', nativeType: 'text', nullable: false }, + mood: { + codecId: 'pg/enum@1', + nativeType: 'mood', + nullable: false, + typeParams: { typeName: 'mood' }, + }, + moods: { + codecId: 'pg/enum@1', + nativeType: 'mood', + nullable: false, + many: true, + typeParams: { typeName: 'mood' }, + }, + labels: { codecId: 'pg/text@1', nativeType: 'text', nullable: false, many: true }, + }, + uniques: [], + indexes: [], + foreignKeys: [], + }, + }, + }, + }, + }, + }, + domain: applicationDomainOf({ models: {} }), +}) as PostgresContract; + +function selectProjection( + alias: string, + column: string, + codec?: Parameters[2], +) { + return SelectAst.from(TableSource.named('probe')).withProjection([ + ProjectionItem.of(alias, ColumnRef.of('probe', column), codec), + ]); +} + +describe('renderLoweredSql — native-enum array projection cast', () => { + it('casts a many + pg/enum@1 SELECT projection to ::text[]', () => { + const ast = selectProjection('moods', 'moods', { + codecId: 'pg/enum@1', + many: true, + typeParams: { typeName: 'mood' }, + }); + + const lowered = renderLoweredSql(ast, baseContract, postgresCodecDescriptorRegistry); + + expect(lowered.sql).toBe('SELECT "probe"."moods"::text[] AS "moods" FROM "probe"'); + }); + + it('casts a many + pg/enum@1 RETURNING projection to ::text[] with an explicit alias', () => { + const ast = InsertAst.into(TableSource.named('probe')) + .withRows([{ id: ParamRef.of('1', { name: 'id', codec: { codecId: 'pg/text@1' } }) }]) + .withReturning([ + ProjectionItem.of('moods', ColumnRef.of('probe', 'moods'), { + codecId: 'pg/enum@1', + many: true, + typeParams: { typeName: 'mood' }, + }), + ]); + + const lowered = renderLoweredSql(ast, baseContract, postgresCodecDescriptorRegistry); + + expect(lowered.sql).toBe( + 'INSERT INTO "probe" ("id") VALUES ($1) RETURNING "probe"."moods"::text[] AS "moods"', + ); + }); + + it('leaves a scalar (non-many) pg/enum@1 projection uncast', () => { + const ast = selectProjection('mood', 'mood', { + codecId: 'pg/enum@1', + typeParams: { typeName: 'mood' }, + }); + + const lowered = renderLoweredSql(ast, baseContract, postgresCodecDescriptorRegistry); + + expect(lowered.sql).toBe('SELECT "probe"."mood" AS "mood" FROM "probe"'); + }); + + it('leaves an ordinary many text[] projection uncast', () => { + const ast = selectProjection('labels', 'labels', { codecId: 'pg/text@1', many: true }); + + const lowered = renderLoweredSql(ast, baseContract, postgresCodecDescriptorRegistry); + + expect(lowered.sql).toBe('SELECT "probe"."labels" AS "labels" FROM "probe"'); + }); + + it('leaves an ordinary scalar text projection uncast', () => { + const ast = selectProjection('id', 'id', { codecId: 'pg/text@1' }); + + const lowered = renderLoweredSql(ast, baseContract, postgresCodecDescriptorRegistry); + + expect(lowered.sql).toBe('SELECT "probe"."id" AS "id" FROM "probe"'); + }); + + it('leaves a projection with no codec uncast', () => { + const ast = selectProjection('moods', 'moods'); + + const lowered = renderLoweredSql(ast, baseContract, postgresCodecDescriptorRegistry); + + expect(lowered.sql).toBe('SELECT "probe"."moods" AS "moods" FROM "probe"'); + }); +}); diff --git a/test/integration/test/enum-array-decode/_fixture/contract.prisma b/test/integration/test/enum-array-decode/_fixture/contract.prisma new file mode 100644 index 000000000000..218e47f225b9 --- /dev/null +++ b/test/integration/test/enum-array-decode/_fixture/contract.prisma @@ -0,0 +1,11 @@ +native_enum Mood { + URGENT = "URGENT" + NORMAL = "NORMAL" + LOW = "LOW" +} + +model Probe { + id String @id + moods pg.enum(Mood)[] + note String +} diff --git a/test/integration/test/enum-array-decode/_fixture/generated/contract.d.ts b/test/integration/test/enum-array-decode/_fixture/generated/contract.d.ts new file mode 100644 index 000000000000..e7942e012d57 --- /dev/null +++ b/test/integration/test/enum-array-decode/_fixture/generated/contract.d.ts @@ -0,0 +1,396 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma contract emit'. +// To regenerate, run: prisma contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@internal/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + TimeString, + Timestamp, + TimestampString, + Timestamptz, + TimestamptzString, + Timetz, + VarBit, + Varchar, +} from '@internal/target-postgres/codec-types'; + +import type { ContractWithTypeMaps, TypeMaps as TypeMapsType } from '@internal/sql-contract/types'; +import type { + Contract as ContractType, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@internal/contract/types'; + +export type StorageHash = + StorageHashBase<'0802020fe7c66e5060038b8fa6f608ae426c3ab22c1a3dd97bd84a0c28dfd2ea'>; +export type ExecutionHash = ExecutionHashBase; +export type ProfileHash = + ProfileHashBase<'3916f444a8a17ad749191acf9e08dad97d1a327b88c2f1d45d12f240296aa8b2'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +export type AggregateTypes = { + readonly avg: { + readonly byCodec: { + readonly 'pg/float@1': { readonly output: 'pg/float8@1'; readonly nullable: true }; + readonly 'pg/float4@1': { readonly output: 'pg/float8@1'; readonly nullable: true }; + readonly 'pg/float8@1': { readonly output: 'pg/float8@1'; readonly nullable: true }; + readonly 'pg/int@1': { readonly output: 'pg/float8@1'; readonly nullable: true }; + readonly 'pg/int2@1': { readonly output: 'pg/float8@1'; readonly nullable: true }; + readonly 'pg/int4@1': { readonly output: 'pg/float8@1'; readonly nullable: true }; + readonly 'pg/int8@1': { readonly output: 'pg/float8@1'; readonly nullable: true }; + readonly 'pg/int8number@1': { readonly output: 'pg/float8@1'; readonly nullable: true }; + readonly 'pg/interval@1': { readonly output: 'pg/interval@1'; readonly nullable: true }; + readonly 'pg/numeric@1': { readonly output: 'pg/numeric@1'; readonly nullable: true }; + readonly 'pg/time-string@1': { readonly output: 'pg/interval@1'; readonly nullable: true }; + readonly 'pg/time-temporal@1': { readonly output: 'pg/interval@1'; readonly nullable: true }; + readonly 'pg/unboundedint@1': { readonly output: 'pg/float8@1'; readonly nullable: true }; + readonly 'sql/float@1': { readonly output: 'pg/float8@1'; readonly nullable: true }; + readonly 'sql/int@1': { readonly output: 'pg/float8@1'; readonly nullable: true }; + }; + }; + readonly avgDecimal: { + readonly byCodec: { + readonly 'pg/int@1': { readonly output: 'pg/numeric@1'; readonly nullable: true }; + readonly 'pg/int2@1': { readonly output: 'pg/numeric@1'; readonly nullable: true }; + readonly 'pg/int4@1': { readonly output: 'pg/numeric@1'; readonly nullable: true }; + readonly 'pg/int8@1': { readonly output: 'pg/numeric@1'; readonly nullable: true }; + readonly 'pg/int8number@1': { readonly output: 'pg/numeric@1'; readonly nullable: true }; + readonly 'pg/numeric@1': { readonly output: 'pg/numeric@1'; readonly nullable: true }; + readonly 'pg/unboundedint@1': { readonly output: 'pg/numeric@1'; readonly nullable: true }; + readonly 'sql/int@1': { readonly output: 'pg/numeric@1'; readonly nullable: true }; + }; + }; + readonly count: { + readonly byCodec: {}; + readonly withoutInput: { readonly output: 'pg/int8number@1'; readonly nullable: false }; + readonly anyInput: { readonly output: 'pg/int8number@1'; readonly nullable: false }; + }; + readonly countBigInt: { + readonly byCodec: {}; + readonly withoutInput: { readonly output: 'pg/int8@1'; readonly nullable: false }; + readonly anyInput: { readonly output: 'pg/int8@1'; readonly nullable: false }; + }; + readonly max: { + readonly byCodec: { + readonly 'pg/char@1': { readonly output: 'pg/char@1'; readonly nullable: true }; + readonly 'pg/date-string@1': { readonly output: 'pg/date-string@1'; readonly nullable: true }; + readonly 'pg/date-temporal@1': { + readonly output: 'pg/date-temporal@1'; + readonly nullable: true; + }; + readonly 'pg/enum@1': { readonly output: 'pg/enum@1'; readonly nullable: true }; + readonly 'pg/float@1': { readonly output: 'pg/float@1'; readonly nullable: true }; + readonly 'pg/float4@1': { readonly output: 'pg/float4@1'; readonly nullable: true }; + readonly 'pg/float8@1': { readonly output: 'pg/float8@1'; readonly nullable: true }; + readonly 'pg/inet@1': { readonly output: 'pg/inet@1'; readonly nullable: true }; + readonly 'pg/int@1': { readonly output: 'pg/int@1'; readonly nullable: true }; + readonly 'pg/int2@1': { readonly output: 'pg/int2@1'; readonly nullable: true }; + readonly 'pg/int4@1': { readonly output: 'pg/int4@1'; readonly nullable: true }; + readonly 'pg/int8@1': { readonly output: 'pg/int8@1'; readonly nullable: true }; + readonly 'pg/int8number@1': { readonly output: 'pg/int8number@1'; readonly nullable: true }; + readonly 'pg/interval@1': { readonly output: 'pg/interval@1'; readonly nullable: true }; + readonly 'pg/numeric@1': { readonly output: 'pg/numeric@1'; readonly nullable: true }; + readonly 'pg/text-array@1': { readonly output: 'pg/text-array@1'; readonly nullable: true }; + readonly 'pg/text@1': { readonly output: 'pg/text@1'; readonly nullable: true }; + readonly 'pg/time-string@1': { readonly output: 'pg/time-string@1'; readonly nullable: true }; + readonly 'pg/time-temporal@1': { + readonly output: 'pg/time-temporal@1'; + readonly nullable: true; + }; + readonly 'pg/timestamp-string@1': { + readonly output: 'pg/timestamp-string@1'; + readonly nullable: true; + }; + readonly 'pg/timestamp-temporal@1': { + readonly output: 'pg/timestamp-temporal@1'; + readonly nullable: true; + }; + readonly 'pg/timestamptz-string@1': { + readonly output: 'pg/timestamptz-string@1'; + readonly nullable: true; + }; + readonly 'pg/timestamptz-temporal@1': { + readonly output: 'pg/timestamptz-temporal@1'; + readonly nullable: true; + }; + readonly 'pg/timetz@1': { readonly output: 'pg/timetz@1'; readonly nullable: true }; + readonly 'pg/unboundedint@1': { + readonly output: 'pg/unboundedint@1'; + readonly nullable: true; + }; + readonly 'pg/varchar@1': { readonly output: 'pg/text@1'; readonly nullable: true }; + readonly 'sql/char@1': { readonly output: 'sql/char@1'; readonly nullable: true }; + readonly 'sql/float@1': { readonly output: 'sql/float@1'; readonly nullable: true }; + readonly 'sql/int@1': { readonly output: 'sql/int@1'; readonly nullable: true }; + readonly 'sql/text@1': { readonly output: 'sql/text@1'; readonly nullable: true }; + readonly 'sql/varchar@1': { readonly output: 'pg/text@1'; readonly nullable: true }; + }; + }; + readonly min: { + readonly byCodec: { + readonly 'pg/char@1': { readonly output: 'pg/char@1'; readonly nullable: true }; + readonly 'pg/date-string@1': { readonly output: 'pg/date-string@1'; readonly nullable: true }; + readonly 'pg/date-temporal@1': { + readonly output: 'pg/date-temporal@1'; + readonly nullable: true; + }; + readonly 'pg/enum@1': { readonly output: 'pg/enum@1'; readonly nullable: true }; + readonly 'pg/float@1': { readonly output: 'pg/float@1'; readonly nullable: true }; + readonly 'pg/float4@1': { readonly output: 'pg/float4@1'; readonly nullable: true }; + readonly 'pg/float8@1': { readonly output: 'pg/float8@1'; readonly nullable: true }; + readonly 'pg/inet@1': { readonly output: 'pg/inet@1'; readonly nullable: true }; + readonly 'pg/int@1': { readonly output: 'pg/int@1'; readonly nullable: true }; + readonly 'pg/int2@1': { readonly output: 'pg/int2@1'; readonly nullable: true }; + readonly 'pg/int4@1': { readonly output: 'pg/int4@1'; readonly nullable: true }; + readonly 'pg/int8@1': { readonly output: 'pg/int8@1'; readonly nullable: true }; + readonly 'pg/int8number@1': { readonly output: 'pg/int8number@1'; readonly nullable: true }; + readonly 'pg/interval@1': { readonly output: 'pg/interval@1'; readonly nullable: true }; + readonly 'pg/numeric@1': { readonly output: 'pg/numeric@1'; readonly nullable: true }; + readonly 'pg/text-array@1': { readonly output: 'pg/text-array@1'; readonly nullable: true }; + readonly 'pg/text@1': { readonly output: 'pg/text@1'; readonly nullable: true }; + readonly 'pg/time-string@1': { readonly output: 'pg/time-string@1'; readonly nullable: true }; + readonly 'pg/time-temporal@1': { + readonly output: 'pg/time-temporal@1'; + readonly nullable: true; + }; + readonly 'pg/timestamp-string@1': { + readonly output: 'pg/timestamp-string@1'; + readonly nullable: true; + }; + readonly 'pg/timestamp-temporal@1': { + readonly output: 'pg/timestamp-temporal@1'; + readonly nullable: true; + }; + readonly 'pg/timestamptz-string@1': { + readonly output: 'pg/timestamptz-string@1'; + readonly nullable: true; + }; + readonly 'pg/timestamptz-temporal@1': { + readonly output: 'pg/timestamptz-temporal@1'; + readonly nullable: true; + }; + readonly 'pg/timetz@1': { readonly output: 'pg/timetz@1'; readonly nullable: true }; + readonly 'pg/unboundedint@1': { + readonly output: 'pg/unboundedint@1'; + readonly nullable: true; + }; + readonly 'pg/varchar@1': { readonly output: 'pg/text@1'; readonly nullable: true }; + readonly 'sql/char@1': { readonly output: 'sql/char@1'; readonly nullable: true }; + readonly 'sql/float@1': { readonly output: 'sql/float@1'; readonly nullable: true }; + readonly 'sql/int@1': { readonly output: 'sql/int@1'; readonly nullable: true }; + readonly 'sql/text@1': { readonly output: 'sql/text@1'; readonly nullable: true }; + readonly 'sql/varchar@1': { readonly output: 'pg/text@1'; readonly nullable: true }; + }; + }; + readonly sum: { + readonly byCodec: { + readonly 'pg/float@1': { readonly output: 'pg/float8@1'; readonly nullable: true }; + readonly 'pg/float4@1': { readonly output: 'pg/float4@1'; readonly nullable: true }; + readonly 'pg/float8@1': { readonly output: 'pg/float8@1'; readonly nullable: true }; + readonly 'pg/int@1': { readonly output: 'pg/int8number@1'; readonly nullable: true }; + readonly 'pg/int2@1': { readonly output: 'pg/int8number@1'; readonly nullable: true }; + readonly 'pg/int4@1': { readonly output: 'pg/int8number@1'; readonly nullable: true }; + readonly 'pg/int8@1': { readonly output: 'pg/int8number@1'; readonly nullable: true }; + readonly 'pg/int8number@1': { readonly output: 'pg/int8number@1'; readonly nullable: true }; + readonly 'pg/interval@1': { readonly output: 'pg/interval@1'; readonly nullable: true }; + readonly 'pg/numeric@1': { readonly output: 'pg/numeric@1'; readonly nullable: true }; + readonly 'pg/time-string@1': { readonly output: 'pg/interval@1'; readonly nullable: true }; + readonly 'pg/time-temporal@1': { readonly output: 'pg/interval@1'; readonly nullable: true }; + readonly 'pg/unboundedint@1': { + readonly output: 'pg/unboundedint@1'; + readonly nullable: true; + }; + readonly 'sql/float@1': { readonly output: 'pg/float8@1'; readonly nullable: true }; + readonly 'sql/int@1': { readonly output: 'pg/int8number@1'; readonly nullable: true }; + }; + }; + readonly sumBigInt: { + readonly byCodec: { + readonly 'pg/int@1': { readonly output: 'pg/int8@1'; readonly nullable: true }; + readonly 'pg/int2@1': { readonly output: 'pg/int8@1'; readonly nullable: true }; + readonly 'pg/int4@1': { readonly output: 'pg/int8@1'; readonly nullable: true }; + readonly 'pg/int8@1': { readonly output: 'pg/unboundedint@1'; readonly nullable: true }; + readonly 'pg/int8number@1': { readonly output: 'pg/unboundedint@1'; readonly nullable: true }; + readonly 'pg/unboundedint@1': { + readonly output: 'pg/unboundedint@1'; + readonly nullable: true; + }; + readonly 'sql/int@1': { readonly output: 'pg/int8@1'; readonly nullable: true }; + }; + }; +}; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? Encoded extends CodecTypes[CodecId]['json'] + ? Encoded + : CodecTypes[CodecId]['json'] + : Encoded; + +export type FieldOutputTypes = { + readonly public: { + readonly Probe: { + readonly id: CodecTypes['pg/text@1']['output']; + readonly moods: ReadonlyArray<'URGENT' | 'NORMAL' | 'LOW'>; + readonly note: CodecTypes['pg/text@1']['output']; + }; + }; +}; +export type FieldInputTypes = { + readonly public: { + readonly Probe: { + readonly id: CodecTypes['pg/text@1']['input']; + readonly moods: ReadonlyArray<'URGENT' | 'NORMAL' | 'LOW'>; + readonly note: CodecTypes['pg/text@1']['input']; + }; + }; +}; +export type StorageColumnTypes = { + readonly public: { + readonly probe: { + readonly id: CodecTypes['pg/text@1']['output']; + readonly moods: ReadonlyArray<'URGENT' | 'NORMAL' | 'LOW'>; + readonly note: CodecTypes['pg/text@1']['output']; + }; + }; +}; +export type StorageColumnInputTypes = { + readonly public: { + readonly probe: { + readonly id: CodecTypes['pg/text@1']['input']; + readonly moods: ReadonlyArray<'URGENT' | 'NORMAL' | 'LOW'>; + readonly note: CodecTypes['pg/text@1']['input']; + }; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes, + StorageColumnTypes, + StorageColumnInputTypes, + AggregateTypes +>; + +type ContractBase = Omit< + ContractType<{ + readonly namespaces: { + readonly public: { + readonly id: 'public'; + readonly kind: 'postgres-schema'; + readonly entries: { + readonly table: { + readonly probe: { + columns: { + readonly id: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly moods: { + readonly nativeType: 'Mood'; + readonly codecId: 'pg/enum@1'; + readonly nullable: false; + readonly typeParams: { readonly typeName: 'Mood' }; + }; + readonly note: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + readonly valueSet: { + readonly Mood: { + readonly kind: 'valueSet'; + readonly values: readonly ['URGENT', 'NORMAL', 'LOW']; + }; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }>, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly probe: { readonly namespace: 'public' & NamespaceId; readonly model: 'Probe' }; + }; + readonly domain: { + readonly namespaces: { + readonly public: { + readonly models: { + readonly Probe: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly moods: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/enum@1' }; + readonly many: true; + }; + readonly note: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'probe'; + readonly namespaceId: 'public'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly moods: { readonly column: 'moods' }; + readonly note: { readonly column: 'note' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly checkConstraint: true; + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + readonly scalarList: true; + }; + }; + readonly extensions: {}; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; diff --git a/test/integration/test/enum-array-decode/_fixture/generated/contract.json b/test/integration/test/enum-array-decode/_fixture/generated/contract.json new file mode 100644 index 000000000000..d37b61dd1218 --- /dev/null +++ b/test/integration/test/enum-array-decode/_fixture/generated/contract.json @@ -0,0 +1,165 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "3916f444a8a17ad749191acf9e08dad97d1a327b88c2f1d45d12f240296aa8b2", + "roots": { + "probe": { + "model": "Probe", + "namespace": "public" + } + }, + "domain": { + "namespaces": { + "public": { + "models": { + "Probe": { + "fields": { + "id": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "moods": { + "many": true, + "nullable": false, + "type": { + "codecId": "pg/enum@1", + "kind": "scalar" + } + }, + "note": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "id": { + "column": "id" + }, + "moods": { + "column": "moods" + }, + "note": { + "column": "note" + } + }, + "namespaceId": "public", + "table": "probe" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "public": { + "entries": { + "native_enum": { + "Mood": { + "kind": "postgres-enum", + "members": [ + "URGENT", + "NORMAL", + "LOW" + ], + "typeName": "Mood" + } + }, + "table": { + "probe": { + "checks": [ + { + "expression": "array_position(\"moods\", NULL) IS NULL", + "name": "probe_moods_elem_not_null_34959499", + "prefix": "probe_moods_elem_not_null" + } + ], + "columns": { + "id": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "moods": { + "codecId": "pg/enum@1", + "many": true, + "nativeType": "Mood", + "nullable": false, + "typeParams": { + "typeName": "Mood" + }, + "valueSet": { + "entityKind": "valueSet", + "entityName": "Mood", + "namespaceId": "public", + "plane": "storage" + } + }, + "note": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + }, + "valueSet": { + "Mood": { + "kind": "valueSet", + "values": [ + "URGENT", + "NORMAL", + "LOW" + ] + } + } + }, + "id": "public", + "kind": "postgres-schema" + } + }, + "storageHash": "0802020fe7c66e5060038b8fa6f608ae426c3ab22c1a3dd97bd84a0c28dfd2ea" + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "checkConstraint": true, + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true, + "scalarList": true + } + }, + "extensions": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma contract emit\".", + "regenerate": "To regenerate, run: prisma contract emit" + } +} \ No newline at end of file diff --git a/test/integration/test/enum-array-decode/_fixture/prisma.config.ts b/test/integration/test/enum-array-decode/_fixture/prisma.config.ts new file mode 100644 index 000000000000..f197173e5b84 --- /dev/null +++ b/test/integration/test/enum-array-decode/_fixture/prisma.config.ts @@ -0,0 +1,9 @@ +import { defineConfig as ormConfig } from '@internal/postgres/config'; +import { defineConfig } from '@prisma/cli-engine'; + +export default defineConfig({ + orm: ormConfig({ + contract: './contract.prisma', + output: 'generated', + }), +}); diff --git a/test/integration/test/enum-array-decode/enum-array-decode.test.ts b/test/integration/test/enum-array-decode/enum-array-decode.test.ts new file mode 100644 index 000000000000..25f1678bc843 --- /dev/null +++ b/test/integration/test/enum-array-decode/enum-array-decode.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest'; +import { timeouts, withPostgresPort } from '../_harness/postgres'; +import type { Contract } from './_fixture/generated/contract'; +import contractJson from './_fixture/generated/contract.json' with { type: 'json' }; + +function withEnumArrayDecode(fn: Parameters>[1]) { + return withPostgresPort({ contractJson }, fn); +} + +describe('decoding a native-enum array column', () => { + it( + 'reads a row with a native-enum array column', + () => + withEnumArrayDecode(async ({ db }) => { + const created = await db.public.Probe.create({ + id: '1', + moods: ['URGENT', 'LOW'], + note: '{"a": 1}', + }); + + expect(created).toEqual({ id: '1', moods: ['URGENT', 'LOW'], note: '{"a": 1}' }); + + const found = await db.public.Probe.first({ id: '1' }); + + expect(found).toEqual({ id: '1', moods: ['URGENT', 'LOW'], note: '{"a": 1}' }); + }), + timeouts.spinUpPpgDev, + ); +}); diff --git a/test/integration/test/ports/engines/queries/filters/field_reference/enum_filter/enum_filter.test.ts b/test/integration/test/ports/engines/queries/filters/field_reference/enum_filter/enum_filter.test.ts index fd0790ba4ebc..95d31e2ae92d 100644 --- a/test/integration/test/ports/engines/queries/filters/field_reference/enum_filter/enum_filter.test.ts +++ b/test/integration/test/ports/engines/queries/filters/field_reference/enum_filter/enum_filter.test.ts @@ -20,7 +20,7 @@ function withEnumFieldReference(fn: Parameters } describe('ports/engines/queries/filters/field-reference/enum-filter', () => { - it.fails( + it( 'inclusion_filter', () => withEnumFieldReference(async ({ db }) => { @@ -33,9 +33,6 @@ describe('ports/engines/queries/filters/field-reference/enum-filter', () => { .all(); expect(await ids(referencedScalarInList(scalar, list, true)), 'notIn').toEqual([{ id: 2 }]); - expect(await ids(referencedScalarInList(scalar, list, true)), 'not: { in }').toEqual([ - { id: 2 }, - ]); expect( await db.public.TestModel.where(referencedScalarInList(scalar, list)) .orderBy((row) => row.id.asc())