Skip to content

Commit 4a20617

Browse files
authored
Merge pull request #887 from constructive-io/devin/1774334480-csv-to-pg-empty-array-defaults
fix(csv-to-pg): emit empty array '{}' instead of NULL for array types
2 parents f42b4df + 25ab51b commit 4a20617

3 files changed

Lines changed: 96 additions & 2 deletions

File tree

packages/csv-to-pg/__tests__/__snapshots__/export.test.ts.snap

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ exports[`test case arrays 1`] = `
88
('450e3b3b-b68d-4abc-990c-65cb8a1dcdb4', '{a,b}');"
99
`;
1010

11+
exports[`test case empty array fields emit empty array literal 1`] = `
12+
"INSERT INTO metaschema_modules_public.secure_table_provision (
13+
id,
14+
node_type,
15+
fields,
16+
grant_roles
17+
) VALUES
18+
('450e3b3b-b68d-4abc-990c-65cb8a1dcdb4', 'DataTimestamps', '{}', '{}');"
19+
`;
20+
1121
exports[`test case image/attachment 1`] = `
1222
"INSERT INTO metaschema_public.field (
1323
id,
@@ -61,6 +71,17 @@ exports[`test case jsonb/json 1`] = `
6171
('450e3b3b-b68d-4abc-990c-65cb8a1dcdb4', 'name here', '{"a":1}');"
6272
`;
6373

74+
exports[`test case null array fields emit empty array literal instead of NULL 1`] = `
75+
"INSERT INTO metaschema_modules_public.secure_table_provision (
76+
id,
77+
node_type,
78+
fields,
79+
grant_privileges,
80+
out_fields
81+
) VALUES
82+
('450e3b3b-b68d-4abc-990c-65cb8a1dcdb4', 'DataTimestamps', '{}', '{}', '{}');"
83+
`;
84+
6485
exports[`test case test case 1`] = `Promise {}`;
6586

6687
exports[`test case test case parser 1`] = `

packages/csv-to-pg/__tests__/export.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,64 @@ it('interval type', async () => {
220220
expect(sql).toMatchSnapshot();
221221
});
222222

223+
it('null array fields emit empty array literal instead of NULL', async () => {
224+
const parser = new Parser({
225+
schema: 'metaschema_modules_public',
226+
singleStmts: true,
227+
table: 'secure_table_provision',
228+
fields: {
229+
id: 'uuid',
230+
node_type: 'text',
231+
fields: 'jsonb[]',
232+
grant_privileges: 'jsonb[]',
233+
out_fields: 'uuid[]'
234+
}
235+
});
236+
237+
const sql = await parser.parse([
238+
{
239+
id: '450e3b3b-b68d-4abc-990c-65cb8a1dcdb4',
240+
node_type: 'DataTimestamps',
241+
fields: null,
242+
grant_privileges: null,
243+
out_fields: null
244+
}
245+
]);
246+
247+
// Should emit '{}' for array columns instead of NULL
248+
expect(sql).toContain("'{}'");
249+
expect(sql).not.toContain('NULL');
250+
expect(sql).toMatchSnapshot();
251+
});
252+
253+
it('empty array fields emit empty array literal', async () => {
254+
const parser = new Parser({
255+
schema: 'metaschema_modules_public',
256+
singleStmts: true,
257+
table: 'secure_table_provision',
258+
fields: {
259+
id: 'uuid',
260+
node_type: 'text',
261+
fields: 'jsonb[]',
262+
grant_roles: 'text[]'
263+
}
264+
});
265+
266+
const sql = await parser.parse([
267+
{
268+
id: '450e3b3b-b68d-4abc-990c-65cb8a1dcdb4',
269+
node_type: 'DataTimestamps',
270+
fields: [],
271+
grant_roles: []
272+
}
273+
]);
274+
275+
// Empty arrays should also emit '{}' not NULL
276+
expect(sql).toContain("'{}'");
277+
expect(sql).not.toContain('NULL');
278+
expect(sql).toMatchSnapshot();
279+
});
280+
223281
it('interval type with string value', async () => {
224282
const parser = new Parser({
225283
schema: 'metaschema_modules_public',

packages/csv-to-pg/src/parse.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,13 @@ const escapeArrayElement = (value: unknown): string => {
104104

105105
/**
106106
* Convert an array to PostgreSQL array literal format with proper escaping.
107+
* Returns '{}' for empty arrays instead of undefined.
107108
*/
108109
const psqlArray = (value: unknown): string | undefined => {
109-
if (Array.isArray(value) && value.length) {
110+
if (Array.isArray(value)) {
111+
if (value.length === 0) {
112+
return '{}';
113+
}
110114
return `{${value.map(escapeArrayElement).join(',')}}`;
111115
}
112116
return undefined;
@@ -213,12 +217,23 @@ export class ValidationError extends Error {
213217
type CoercionFunc = (record: Record<string, unknown>) => Node;
214218

215219
/**
216-
* Helper to create a NULL node or throw if field is required
220+
* Check if a type is an array type (e.g. 'text[]', 'uuid[]', 'jsonb[]')
221+
*/
222+
const isArrayType = (type: string): boolean => type.endsWith('[]');
223+
224+
/**
225+
* Helper to create a NULL node or throw if field is required.
226+
* For array types, emits an empty array literal '{}' instead of NULL.
217227
*/
218228
const makeNullOrThrow = (fieldName: string, rawValue: unknown, type: string, required: boolean, reason: string): Node => {
219229
if (required) {
220230
throw new ValidationError(fieldName, rawValue, type, reason);
221231
}
232+
if (isArrayType(type)) {
233+
return nodes.aConst({
234+
sval: ast.string({ sval: '{}' })
235+
});
236+
}
222237
return nodes.aConst({ isnull: true });
223238
};
224239

0 commit comments

Comments
 (0)