diff --git a/.changeset/fix-postgres-enum-array-updates.md b/.changeset/fix-postgres-enum-array-updates.md new file mode 100644 index 00000000..98479f89 --- /dev/null +++ b/.changeset/fix-postgres-enum-array-updates.md @@ -0,0 +1,5 @@ +--- +"@prisma/studio-core": patch +--- + +Fix PostgreSQL enum array cell updates failing with "Update Operation failed" by always writing array values as explicit `array[...]` expressions with an array-type cast instead of relying on driver-specific array parameter serialization. Also preserve the original error name when errors are deserialized from the Studio BFF transport. diff --git a/FEATURES.md b/FEATURES.md index 2acaa70e..a35506c9 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -275,7 +275,7 @@ Exports can copy directly to the clipboard or save to disk, include column heade Editable cells open popover editors with datatype-specific controls for raw text, numeric, boolean, enum, JSON/array, date, and time values. Save/cancel keyboard behavior is standardized, and null/default/empty semantics are handled explicitly per input type. -Native PostgreSQL arrays can be edited from JSON-style array values and are written back with explicit array casts when inline SQL literals are required. +Native PostgreSQL arrays can be edited from JSON-style array values and are always written back as explicit `array[...]` constructor expressions with an array-type cast, so writes never depend on driver-specific array parameter serialization. PostgreSQL user-defined enum arrays also persist through that same staged-edit flow, with schema-qualified casts emitted in a form PostgreSQL accepts for `enum[]` writes. ## Staged Multi-Cell Editing diff --git a/data/bff/bff-client.ts b/data/bff/bff-client.ts index aa2149c2..0a07722a 100644 --- a/data/bff/bff-client.ts +++ b/data/bff/bff-client.ts @@ -653,6 +653,6 @@ export function deserializeError(error: SerializedError): Error { } const regularError = new Error(error.message); - error.name = name; + regularError.name = name; return regularError; } diff --git a/data/postgres-core/dml.test.ts b/data/postgres-core/dml.test.ts index eae5c6a9..88f56466 100644 --- a/data/postgres-core/dml.test.ts +++ b/data/postgres-core/dml.test.ts @@ -1300,6 +1300,72 @@ describe("postgres-core/dml", () => { expect(persisted.rows).toEqual([{ arr_col: "{tag1,tag2,tag3}" }]); }); + it("supports PostgreSQL enum array updates with array values", async () => { + const table = createEnumArrayUsersTable(); + const query = getUpdateQuery({ + changes: { roles: ["ADMIN", "MANAGE"] }, + row: { id: 1 }, + table, + }); + + expect(query).toMatchInlineSnapshot(` + { + "parameters": [ + "ADMIN", + "MANAGE", + 1, + 1000, + ], + "sql": "update "public"."enum_array_users" set "roles" = cast(array[$1, $2] as "public"."studio_role"[]) where "id" = $3 returning "id", "roles", cast(floor(extract(epoch from now()) * $4) as text) as "__ps_updated_at__"", + "transformations": undefined, + } + `); + + const [error] = await executor.execute(query); + + expect(error).toBeNull(); + + const persisted = await pglite.query<{ roles: string }>(` + select "roles"::text as "roles" + from "public"."enum_array_users" + where "id" = 1 + `); + + expect(persisted.rows).toEqual([{ roles: "{ADMIN,MANAGE}" }]); + }); + + it("supports PostgreSQL enum array updates with array values when parameters are inlined", async () => { + const table = createEnumArrayUsersTable(); + const query = getUpdateQuery( + { + changes: { roles: ["ADMIN", "VISIT"] }, + row: { id: 1 }, + table, + }, + { noParameters: true }, + ); + + expect(query).toMatchInlineSnapshot(` + { + "parameters": [], + "sql": "update "public"."enum_array_users" set "roles" = cast(array['ADMIN', 'VISIT'] as "public"."studio_role"[]) where "id" = 1 returning "id", "roles", cast(floor(extract(epoch from now()) * 1000) as text) as "__ps_updated_at__"", + "transformations": undefined, + } + `); + + const [error] = await executor.execute(query); + + expect(error).toBeNull(); + + const persisted = await pglite.query<{ roles: string }>(` + select "roles"::text as "roles" + from "public"."enum_array_users" + where "id" = 1 + `); + + expect(persisted.rows).toEqual([{ roles: "{ADMIN,VISIT}" }]); + }); + it("casts PostgreSQL enum arrays with the array suffix outside the quoted user-defined type name", async () => { const table = createEnumArrayUsersTable(); const query = getUpdateQuery({ diff --git a/data/postgres-core/dml.ts b/data/postgres-core/dml.ts index 7d1bd238..c617f76a 100644 --- a/data/postgres-core/dml.ts +++ b/data/postgres-core/dml.ts @@ -52,7 +52,6 @@ export function getInsertQuery( applyTransformations({ columns, context: "insert", - noParameters: requirements?.noParameters, supportsDefaultKeyword: true, values: rows, }), @@ -286,7 +285,6 @@ export function getUpdateQuery( applyTransformations({ columns, context: "update", - noParameters: requirements?.noParameters, supportsDefaultKeyword: true, values: changes, }), diff --git a/data/query.ts b/data/query.ts index b80800dc..ce40fd54 100644 --- a/data/query.ts +++ b/data/query.ts @@ -278,7 +278,6 @@ function tupleFrom(items: unknown[]): Expression { export interface ApplyWriteTransformationsProps { columns: Table["columns"]; context: C; - noParameters?: boolean; values: C extends "update" ? Record : Record | Record[]; @@ -301,7 +300,6 @@ export function applyTransformations( interface TransformValuesProps { columns: Table["columns"]; context: "insert" | "update"; - noParameters?: boolean; supportsDefaultKeyword: boolean; values: Record; } @@ -333,10 +331,7 @@ function transformValues( return valueEntries.reduce( (obj, [key, value]) => ({ ...obj, - [key]: transformValue(value, columns[key]!, { - inlineArrayValues: props.noParameters === true, - supportsDefaultKeyword, - }), + [key]: transformValue(value, columns[key]!, supportsDefaultKeyword), }), requiredColumns.reduce((defaults, column) => { const { datatype, fkColumn, name } = column; @@ -376,13 +371,9 @@ function transformValues( function transformValue( value: unknown, column: Column, - options: { - inlineArrayValues?: boolean; - supportsDefaultKeyword?: boolean; - } = {}, + supportsDefaultKeyword = true, ): Expression { const { datatype, defaultValue, nullable } = column; - const { inlineArrayValues = false, supportsDefaultKeyword = true } = options; const eb = expressionBuilder(); @@ -394,7 +385,7 @@ function transformValue( return supportsDefaultKeyword ? sql`default` : eb.lit(null); } - if (inlineArrayValues && datatype.isArray && Array.isArray(value)) { + if (datatype.isArray && Array.isArray(value)) { return eb.cast( getArrayValueExpression(value), getArrayTypeCastTarget(datatype),