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
30 changes: 30 additions & 0 deletions packages/csv-to-pg/__tests__/__snapshots__/export.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,36 @@ exports[`test case test case parser 1`] = `
('450e3b3b-b68d-4abc-990c-65cb8a1dcdb4', '450e3b3b-b68d-4abc-990c-65cb8a1dcdb4', '450e3b3b-b68d-4abc-990c-65cb8a1dcdb4', 'name here', 'description');"
`;

exports[`test case text fields convert empty strings to NULL when preserveEmptyStrings is false 1`] = `
"INSERT INTO services_public.webauthn_settings (
id,
database_id,
rp_id,
rp_name
) VALUES
('450e3b3b-b68d-4abc-990c-65cb8a1dcdb4', '550e8400-e29b-41d4-a716-446655440000', NULL, NULL);"
`;

exports[`test case text fields preserve empty strings by default 1`] = `
"INSERT INTO services_public.webauthn_settings (
id,
database_id,
rp_id,
rp_name
) VALUES
('450e3b3b-b68d-4abc-990c-65cb8a1dcdb4', '550e8400-e29b-41d4-a716-446655440000', '', '');"
`;

exports[`test case text fields with null values produce NULL 1`] = `
"INSERT INTO services_public.webauthn_settings (
id,
database_id,
rp_id,
rp_name
) VALUES
('450e3b3b-b68d-4abc-990c-65cb8a1dcdb4', '550e8400-e29b-41d4-a716-446655440000', NULL, NULL);"
`;

exports[`test case uuid[] arrays 1`] = `
"INSERT INTO metaschema_public.primary_key_constraint (
id,
Expand Down
82 changes: 82 additions & 0 deletions packages/csv-to-pg/__tests__/export.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,88 @@ it('empty array fields emit empty array literal', async () => {
expect(sql).toMatchSnapshot();
});

it('text fields preserve empty strings by default', async () => {
const parser = new Parser({
schema: 'services_public',
singleStmts: true,
table: 'webauthn_settings',
fields: {
id: 'uuid',
database_id: 'uuid',
rp_id: 'text',
rp_name: 'text'
}
});

const sql = await parser.parse([
{
id: '450e3b3b-b68d-4abc-990c-65cb8a1dcdb4',
database_id: '550e8400-e29b-41d4-a716-446655440000',
rp_id: '',
rp_name: ''
}
]);

// Default: empty strings are preserved as ''
expect(sql).not.toContain('NULL');
expect(sql).toMatchSnapshot();
});

it('text fields convert empty strings to NULL when preserveEmptyStrings is false', async () => {
const parser = new Parser({
schema: 'services_public',
singleStmts: true,
table: 'webauthn_settings',
preserveEmptyStrings: false,
fields: {
id: 'uuid',
database_id: 'uuid',
rp_id: 'text',
rp_name: 'text'
}
});

const sql = await parser.parse([
{
id: '450e3b3b-b68d-4abc-990c-65cb8a1dcdb4',
database_id: '550e8400-e29b-41d4-a716-446655440000',
rp_id: '',
rp_name: ''
}
]);

// Opt-in: empty strings treated as NULL (CSV convention)
expect(sql).toContain('NULL');
expect(sql).toMatchSnapshot();
});

it('text fields with null values produce NULL', async () => {
const parser = new Parser({
schema: 'services_public',
singleStmts: true,
table: 'webauthn_settings',
fields: {
id: 'uuid',
database_id: 'uuid',
rp_id: 'text',
rp_name: 'text'
}
});

const sql = await parser.parse([
{
id: '450e3b3b-b68d-4abc-990c-65cb8a1dcdb4',
database_id: '550e8400-e29b-41d4-a716-446655440000',
rp_id: null,
rp_name: null
}
]);

// Actual null values should still produce NULL
expect(sql).toContain('NULL');
expect(sql).toMatchSnapshot();
});

it('interval type with string value', async () => {
const parser = new Parser({
schema: 'metaschema_modules_public',
Expand Down
16 changes: 13 additions & 3 deletions packages/csv-to-pg/src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ const makeNullOrThrow = (fieldName: string, rawValue: unknown, type: string, req

// type (int, text, etc)
// from Array of keys that map to records found (e.g., ['lon', 'lat'])
const getCoercionFunc = (type: string, from: string[], opts: FieldOptions, fieldName: string): CoercionFunc => {
const getCoercionFunc = (type: string, from: string[], opts: FieldOptions, fieldName: string, globalOpts?: GlobalParseOptions): CoercionFunc => {
const parseFn = opts.parse || identity;
const required = opts.required || false;

Expand Down Expand Up @@ -436,7 +436,9 @@ const getCoercionFunc = (type: string, from: string[], opts: FieldOptions, field
case 'text':
return (record: Record<string, unknown>): Node => {
const rawValue = record[from[0]];
const value = parseFn(cleanseEmptyStrings(rawValue));
const preserve = globalOpts?.preserveEmptyStrings !== false;
const cleansed = preserve ? rawValue : cleanseEmptyStrings(rawValue);
const value = parseFn(cleansed);
if (isEmpty(value)) {
return makeNullOrThrow(fieldName, rawValue, type, required, 'value is empty or null');
}
Expand Down Expand Up @@ -525,15 +527,23 @@ interface ConfigFields {
[key: string]: string | FieldOptions;
}

export interface GlobalParseOptions {
preserveEmptyStrings?: boolean;
}

interface Config {
fields: ConfigFields;
preserveEmptyStrings?: boolean;
}

interface TypesMap {
[key: string]: CoercionFunc;
}

export const parseTypes = (config: Config): TypesMap => {
const globalOpts: GlobalParseOptions = {
preserveEmptyStrings: config.preserveEmptyStrings
};
return Object.entries(config.fields).reduce<TypesMap>((m, v) => {
let [key, value] = v;
let type: string;
Expand All @@ -552,7 +562,7 @@ export const parseTypes = (config: Config): TypesMap => {
type = value.type!;
from = getFromValue(value.from || key);
}
m[key] = getCoercionFunc(type, from, value, key);
m[key] = getCoercionFunc(type, from, value, key, globalOpts);
return m;
}, {});
};
1 change: 1 addition & 0 deletions packages/csv-to-pg/src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface ParserConfig {
input?: string;
debug?: boolean;
fields: Record<string, unknown>;
preserveEmptyStrings?: boolean;
}

interface CsvOptions {
Expand Down
Loading