Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-postgres-enum-array-updates.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion data/bff/bff-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,6 @@ export function deserializeError(error: SerializedError): Error {
}

const regularError = new Error(error.message);
error.name = name;
regularError.name = name;
return regularError;
}
66 changes: 66 additions & 0 deletions data/postgres-core/dml.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
2 changes: 0 additions & 2 deletions data/postgres-core/dml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ export function getInsertQuery(
applyTransformations({
columns,
context: "insert",
noParameters: requirements?.noParameters,
supportsDefaultKeyword: true,
values: rows,
}),
Expand Down Expand Up @@ -286,7 +285,6 @@ export function getUpdateQuery(
applyTransformations({
columns,
context: "update",
noParameters: requirements?.noParameters,
supportsDefaultKeyword: true,
values: changes,
}),
Expand Down
15 changes: 3 additions & 12 deletions data/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,6 @@ function tupleFrom(items: unknown[]): Expression<any> {
export interface ApplyWriteTransformationsProps<C extends "insert" | "update"> {
columns: Table["columns"];
context: C;
noParameters?: boolean;
values: C extends "update"
? Record<string, unknown>
: Record<string, unknown> | Record<string, unknown>[];
Expand All @@ -301,7 +300,6 @@ export function applyTransformations<C extends "insert" | "update">(
interface TransformValuesProps {
columns: Table["columns"];
context: "insert" | "update";
noParameters?: boolean;
supportsDefaultKeyword: boolean;
values: Record<string, unknown>;
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -376,13 +371,9 @@ function transformValues(
function transformValue(
value: unknown,
column: Column,
options: {
inlineArrayValues?: boolean;
supportsDefaultKeyword?: boolean;
} = {},
supportsDefaultKeyword = true,
): Expression<any> {
const { datatype, defaultValue, nullable } = column;
const { inlineArrayValues = false, supportsDefaultKeyword = true } = options;

const eb = expressionBuilder();

Expand All @@ -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),
Expand Down