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-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
68 changes: 68 additions & 0 deletions data/postgres-core/dml.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1232,6 +1232,74 @@ 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("supports PostgreSQL text array inserts when parameters are inlined", async () => {
const table = createSearchTypesTable();
const query = getInsertQuery(
{
rows: [
{
arr_col: ["tag1", "tag2", "tag3"],
id: "row_insert_inline_arr",
},
],
table,
},
{ noParameters: true },
);

expect(query).toMatchInlineSnapshot(`
{
"parameters": [],
"sql": "insert into "public"."search_types" ("id", "arr_col") values ('row_insert_inline_arr', cast(array['tag1', 'tag2', 'tag3'] as text[])) 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_inserted_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_insert_inline_arr'
`);

expect(persisted.rows).toEqual([{ arr_col: "{tag1,tag2,tag3}" }]);
});

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