diff --git a/packages/csv-to-pg/__tests__/__snapshots__/export.test.ts.snap b/packages/csv-to-pg/__tests__/__snapshots__/export.test.ts.snap index 811d944dc..125b4cdf7 100644 --- a/packages/csv-to-pg/__tests__/__snapshots__/export.test.ts.snap +++ b/packages/csv-to-pg/__tests__/__snapshots__/export.test.ts.snap @@ -8,6 +8,16 @@ exports[`test case arrays 1`] = ` ('450e3b3b-b68d-4abc-990c-65cb8a1dcdb4', '{a,b}');" `; +exports[`test case empty array fields emit empty array literal 1`] = ` +"INSERT INTO metaschema_modules_public.secure_table_provision ( + id, + node_type, + fields, + grant_roles +) VALUES + ('450e3b3b-b68d-4abc-990c-65cb8a1dcdb4', 'DataTimestamps', '{}', '{}');" +`; + exports[`test case image/attachment 1`] = ` "INSERT INTO metaschema_public.field ( id, @@ -61,6 +71,17 @@ exports[`test case jsonb/json 1`] = ` ('450e3b3b-b68d-4abc-990c-65cb8a1dcdb4', 'name here', '{"a":1}');" `; +exports[`test case null array fields emit empty array literal instead of NULL 1`] = ` +"INSERT INTO metaschema_modules_public.secure_table_provision ( + id, + node_type, + fields, + grant_privileges, + out_fields +) VALUES + ('450e3b3b-b68d-4abc-990c-65cb8a1dcdb4', 'DataTimestamps', '{}', '{}', '{}');" +`; + exports[`test case test case 1`] = `Promise {}`; exports[`test case test case parser 1`] = ` diff --git a/packages/csv-to-pg/__tests__/export.test.ts b/packages/csv-to-pg/__tests__/export.test.ts index 7646fd99d..4c36e6ffb 100644 --- a/packages/csv-to-pg/__tests__/export.test.ts +++ b/packages/csv-to-pg/__tests__/export.test.ts @@ -220,6 +220,64 @@ it('interval type', async () => { expect(sql).toMatchSnapshot(); }); +it('null array fields emit empty array literal instead of NULL', async () => { + const parser = new Parser({ + schema: 'metaschema_modules_public', + singleStmts: true, + table: 'secure_table_provision', + fields: { + id: 'uuid', + node_type: 'text', + fields: 'jsonb[]', + grant_privileges: 'jsonb[]', + out_fields: 'uuid[]' + } + }); + + const sql = await parser.parse([ + { + id: '450e3b3b-b68d-4abc-990c-65cb8a1dcdb4', + node_type: 'DataTimestamps', + fields: null, + grant_privileges: null, + out_fields: null + } + ]); + + // Should emit '{}' for array columns instead of NULL + expect(sql).toContain("'{}'"); + expect(sql).not.toContain('NULL'); + expect(sql).toMatchSnapshot(); +}); + +it('empty array fields emit empty array literal', async () => { + const parser = new Parser({ + schema: 'metaschema_modules_public', + singleStmts: true, + table: 'secure_table_provision', + fields: { + id: 'uuid', + node_type: 'text', + fields: 'jsonb[]', + grant_roles: 'text[]' + } + }); + + const sql = await parser.parse([ + { + id: '450e3b3b-b68d-4abc-990c-65cb8a1dcdb4', + node_type: 'DataTimestamps', + fields: [], + grant_roles: [] + } + ]); + + // Empty arrays should also emit '{}' not NULL + expect(sql).toContain("'{}'"); + expect(sql).not.toContain('NULL'); + expect(sql).toMatchSnapshot(); +}); + it('interval type with string value', async () => { const parser = new Parser({ schema: 'metaschema_modules_public', diff --git a/packages/csv-to-pg/src/parse.ts b/packages/csv-to-pg/src/parse.ts index 6421fb2fe..921ed04d0 100644 --- a/packages/csv-to-pg/src/parse.ts +++ b/packages/csv-to-pg/src/parse.ts @@ -104,9 +104,13 @@ const escapeArrayElement = (value: unknown): string => { /** * Convert an array to PostgreSQL array literal format with proper escaping. + * Returns '{}' for empty arrays instead of undefined. */ const psqlArray = (value: unknown): string | undefined => { - if (Array.isArray(value) && value.length) { + if (Array.isArray(value)) { + if (value.length === 0) { + return '{}'; + } return `{${value.map(escapeArrayElement).join(',')}}`; } return undefined; @@ -213,12 +217,23 @@ export class ValidationError extends Error { type CoercionFunc = (record: Record) => Node; /** - * Helper to create a NULL node or throw if field is required + * Check if a type is an array type (e.g. 'text[]', 'uuid[]', 'jsonb[]') + */ +const isArrayType = (type: string): boolean => type.endsWith('[]'); + +/** + * Helper to create a NULL node or throw if field is required. + * For array types, emits an empty array literal '{}' instead of NULL. */ const makeNullOrThrow = (fieldName: string, rawValue: unknown, type: string, required: boolean, reason: string): Node => { if (required) { throw new ValidationError(fieldName, rawValue, type, reason); } + if (isArrayType(type)) { + return nodes.aConst({ + sval: ast.string({ sval: '{}' }) + }); + } return nodes.aConst({ isnull: true }); };