Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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-text-array-edits.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@prisma/studio-core": patch
---

Fix PostgreSQL text array cell edits when queries are compiled with inline values.
1 change: 1 addition & 0 deletions FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,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.
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
32 changes: 32 additions & 0 deletions data/postgres-core/dml.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1232,6 +1232,38 @@ describe("postgres-core/dml", () => {
]);
});

it("supports PostgreSQL text array updates when parameters are inlined", async () => {
const table = createSearchTypesTable();
const query = getUpdateQuery(
{
changes: { arr_col: ["tag1", "tag2", "tag3"] },
row: { id: "row_tr" },
table,
},
{ noParameters: true },
);

expect(query).toMatchInlineSnapshot(`
{
"parameters": [],
"sql": "update "public"."search_types" set "arr_col" = cast(array['tag1', 'tag2', 'tag3'] as text[]) where "id" = 'row_tr' returning "id", "str_col", "dt_col", "bool_col", "enum_col", "time_col", "raw_col", "num_col", "json_col", "arr_col", 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<{ arr_col: string }>(`
select "arr_col"::text as "arr_col"
from "public"."search_types"
where "id" = 'row_tr'
`);

expect(persisted.rows).toEqual([{ arr_col: "{tag1,tag2,tag3}" }]);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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: 2 additions & 0 deletions data/postgres-core/dml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export function getInsertQuery(
applyTransformations({
columns,
context: "insert",
noParameters: requirements?.noParameters,
supportsDefaultKeyword: true,
values: rows,
}),
Expand Down Expand Up @@ -285,6 +286,7 @@ export function getUpdateQuery(
applyTransformations({
columns,
context: "update",
noParameters: requirements?.noParameters,
supportsDefaultKeyword: true,
values: changes,
}),
Expand Down
37 changes: 35 additions & 2 deletions data/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ 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 @@ -300,6 +301,7 @@ 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 @@ -331,7 +333,10 @@ function transformValues(
return valueEntries.reduce(
(obj, [key, value]) => ({
...obj,
[key]: transformValue(value, columns[key]!, supportsDefaultKeyword),
[key]: transformValue(value, columns[key]!, {
inlineArrayValues: props.noParameters === true,
supportsDefaultKeyword,
}),
}),
requiredColumns.reduce((defaults, column) => {
const { datatype, fkColumn, name } = column;
Expand Down Expand Up @@ -371,9 +376,13 @@ function transformValues(
function transformValue(
value: unknown,
column: Column,
supportsDefaultKeyword = true,
options: {
inlineArrayValues?: boolean;
supportsDefaultKeyword?: boolean;
} = {},
): Expression<any> {
const { datatype, defaultValue, nullable } = column;
const { inlineArrayValues = false, supportsDefaultKeyword = true } = options;

const eb = expressionBuilder();

Expand All @@ -385,13 +394,37 @@ function transformValue(
return supportsDefaultKeyword ? sql`default` : eb.lit(null);
}

if (inlineArrayValues && datatype.isArray && Array.isArray(value)) {
return eb.cast(
getArrayValueExpression(value),
getArrayTypeCastTarget(datatype),
);
}

if (!datatype.isNative) {
return eb.cast(eb.val(value), getUserDefinedTypeCastTarget(datatype));
}

return eb.val(value);
}

function getArrayValueExpression(value: unknown[]): Expression<any> {
return sql`array[${sql.join(
value.map((item) =>
Array.isArray(item) ? getArrayValueExpression(item) : sql`${item}`,
),
sql`, `,
)}]`;
}

function getArrayTypeCastTarget(datatype: DataType): Expression<any> {
if (!datatype.isNative) {
return getUserDefinedTypeCastTarget(datatype);
}

return sql.raw(datatype.name);
}

function getUserDefinedTypeCastTarget(datatype: DataType): Expression<any> {
const { isArray, name, schema } = datatype;

Expand Down