Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Pg] FIX: correct string escaping for empty PgArrays #1640

Merged
merged 4 commits into from
Dec 20, 2023
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
4 changes: 2 additions & 2 deletions drizzle-orm/src/pg-core/utils/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ export function makePgArray(array: any[]): string {
return makePgArray(item);
}

if (typeof item === 'string' && item.includes(',')) {
return `"${item.replace(/"/g, '\\"')}"`;
if (typeof item === 'string') {
return `"${item.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
}

return `${item}`;
Expand Down
52 changes: 40 additions & 12 deletions drizzle-orm/tests/makePgArray.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe.concurrent('makePgArray', () => {
it('parses simple 1D array', ({ expect }) => {
const input = ['1', '2', '3'];
const output = table.a.mapToDriverValue(input);
expect(output).toEqual('{1,2,3}');
expect(output).toEqual('{"1","2","3"}');
});

it('parses simple 2D array', ({ expect }) => {
Expand All @@ -26,13 +26,13 @@ describe.concurrent('makePgArray', () => {
['7', '8', '9'],
];
const output = table.b.mapToDriverValue(input);
expect(output).toEqual('{{1,2,3},{4,5,6},{7,8,9}}');
expect(output).toEqual('{{"1","2","3"},{"4","5","6"},{"7","8","9"}}');
});

it('parses array with quoted values', ({ expect }) => {
const input = ['1', '2,3', '4'];
const output = table.a.mapToDriverValue(input);
expect(output).toEqual('{1,"2,3",4}');
expect(output).toEqual('{"1","2,3","4"}');
});

it('parses array with nested quoted values', ({ expect }) => {
Expand All @@ -41,13 +41,13 @@ describe.concurrent('makePgArray', () => {
['5', '6,7', '8'],
];
const output = table.b.mapToDriverValue(input);
expect(output).toEqual('{{1,"2,3",4},{5,"6,7",8}}');
expect(output).toEqual('{{"1","2,3","4"},{"5","6,7","8"}}');
});

it('parses array with empty values', ({ expect }) => {
const input = ['1', '', '3'];
const output = table.a.mapToDriverValue(input);
expect(output).toEqual('{1,,3}');
expect(output).toEqual('{"1","","3"}');
});

it('parses array with empty nested values', ({ expect }) => {
Expand All @@ -57,7 +57,7 @@ describe.concurrent('makePgArray', () => {
['7', '8', '9'],
];
const output = table.b.mapToDriverValue(input);
expect(output).toEqual('{{1,2,3},{,5,6},{7,8,9}}');
expect(output).toEqual('{{"1","2","3"},{"","5","6"},{"7","8","9"}}');
});

it('parses empty array', ({ expect }) => {
Expand All @@ -75,25 +75,25 @@ describe.concurrent('makePgArray', () => {
it('parses single-level array with strings', ({ expect }) => {
const input = ['one', 'two', 'three'];
const output = table.a.mapToDriverValue(input);
expect(output).toEqual('{one,two,three}');
expect(output).toEqual('{"one","two","three"}');
});

it('parses single-level array with mixed values', ({ expect }) => {
const input = ['1', 'two', '3'];
const output = table.a.mapToDriverValue(input);
expect(output).toEqual('{1,two,3}');
expect(output).toEqual('{"1","two","3"}');
});

it('parses single-level array with commas inside quotes', ({ expect }) => {
const input = ['1', 'two, three', '4'];
const output = table.a.mapToDriverValue(input);
expect(output).toEqual('{1,"two, three",4}');
expect(output).toEqual('{"1","two, three","4"}');
});

it('parses single-level array with escaped quotes inside quotes', ({ expect }) => {
const input = ['1', 'two "three", four', '5'];
const output = table.a.mapToDriverValue(input);
expect(output).toEqual('{1,"two \\"three\\", four",5}');
expect(output).toEqual('{"1","two \\"three\\", four","5"}');
});

it('parses two-dimensional array with strings', ({ expect }) => {
Expand All @@ -103,7 +103,7 @@ describe.concurrent('makePgArray', () => {
['seven', 'eight', 'nine'],
];
const output = table.b.mapToDriverValue(input);
expect(output).toEqual('{{one,two,three},{four,five,six},{seven,eight,nine}}');
expect(output).toEqual('{{"one","two","three"},{"four","five","six"},{"seven","eight","nine"}}');
});

it('parses two-dimensional array with mixed values and escaped quotes', ({ expect }) => {
Expand All @@ -114,7 +114,35 @@ describe.concurrent('makePgArray', () => {
];
const output = table.b.mapToDriverValue(input);
expect(output).toEqual(
'{{1,"two \\"and a half\\", three",3},{four,"five \\"and a half\\", six",6},{seven,eight,nine}}',
'{{"1","two \\"and a half\\", three","3"},{"four","five \\"and a half\\", six","6"},{"seven","eight","nine"}}',
);
});

it('parses an array with null values', ({ expect }) => {
const input = ['1', null, '3'];
const output = table.a.mapToDriverValue(input);
expect(output).toEqual('{"1",null,"3"}');
});

it('parses an array with null values in nested arrays', ({ expect }) => {
const input = [
['1', '2', '3'],
[null, '5', '6'],
['7', '8', '9'],
];
const output = table.b.mapToDriverValue(input);
expect(output).toEqual('{{"1","2","3"},{null,"5","6"},{"7","8","9"}}');
});

it('parses string array with empty strings', ({ expect }) => {
const input = ['1', '', '3'];
const output = table.a.mapToDriverValue(input);
expect(output).toEqual('{"1","","3"}');
});

it('parses string array with backlash strings', ({ expect }) => {
const input = ['1', '\n', '3\\'];
const output = table.a.mapToDriverValue(input);
expect(output).toEqual('{"1","\n","3\\\\"}');
});
});
2 changes: 1 addition & 1 deletion drizzle-orm/tests/parsePgArray.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe.concurrent('parsePgArray', () => {
});

it('parses array with empty values', ({ expect }) => {
const input = '{1,,3}';
const input = '{1,"",3}';
const output = table.a.mapFromDriverValue(input);
expect(output).toEqual(['1', '', '3']);
});
Expand Down
38 changes: 38 additions & 0 deletions integration-tests/tests/pg.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3290,3 +3290,41 @@ test.serial('aggregate function: min', async (t) => {
t.deepEqual(result1[0]?.value, 10);
t.deepEqual(result2[0]?.value, null);
});

test.serial('array mapping and parsing', async (t) => {
const { db } = t.context;

const arrays = pgTable('arrays_tests', {
id: serial('id').primaryKey(),
tags: text('tags').array(),
nested: text('nested').array().array(),
numbers: integer('numbers').notNull().array(),
});

db.execute(sql`drop table if exists ${arrays}`);
db.execute(sql`
create table ${arrays} (
id serial primary key,
tags text[],
nested text[][],
numbers integer[]
)
`);

await db.insert(arrays).values({
tags: ['', 'b', 'c'],
nested: [['1', ''], ['3', '\\a']],
numbers: [1, 2, 3],
});

const result = await db.select().from(arrays);

t.deepEqual(result, [{
id: 1,
tags: ['', 'b', 'c'],
nested: [['1', ''], ['3', '\\a']],
numbers: [1, 2, 3],
}]);

await db.execute(sql`drop table ${arrays}`);
});
38 changes: 38 additions & 0 deletions integration-tests/tests/postgres.js.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2176,3 +2176,41 @@ test.serial('array operators', async (t) => {
t.deepEqual(overlaps, [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }]);
t.deepEqual(withSubQuery, [{ id: 1 }, { id: 3 }, { id: 5 }]);
});

test.serial('array mapping and parsing', async (t) => {
const { db } = t.context;

const arrays = pgTable('arrays_tests', {
id: serial('id').primaryKey(),
tags: text('tags').array(),
nested: text('nested').array().array(),
numbers: integer('numbers').notNull().array(),
});

db.execute(sql`drop table if exists ${arrays}`);
db.execute(sql`
create table ${arrays} (
id serial primary key,
tags text[],
nested text[][],
numbers integer[]
)
`);

await db.insert(arrays).values({
tags: ['', 'b', 'c'],
nested: [['1', ''], ['3', '\\a']],
numbers: [1, 2, 3],
});

const result = await db.select().from(arrays);

t.deepEqual(result, [{
id: 1,
tags: ['', 'b', 'c'],
nested: [['1', ''], ['3', '\\a']],
numbers: [1, 2, 3],
}]);

await db.execute(sql`drop table ${arrays}`);
});