csv-to-pg 3.3.4 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/esm/parse.js CHANGED
@@ -29,6 +29,37 @@ const parseJson = (value) => {
29
29
  return value;
30
30
  return value ? JSON.stringify(value) : undefined;
31
31
  };
32
+ /**
33
+ * Convert a PostgreSQL interval object to a PostgreSQL interval string.
34
+ * node-postgres returns intervals as objects like { hours: 12, minutes: 6, seconds: 41 }
35
+ */
36
+ const formatInterval = (value) => {
37
+ if (typeof value === 'string') {
38
+ return value;
39
+ }
40
+ if (value && typeof value === 'object') {
41
+ const interval = value;
42
+ const parts = [];
43
+ if (interval.years)
44
+ parts.push(`${interval.years} year${interval.years !== 1 ? 's' : ''}`);
45
+ if (interval.months)
46
+ parts.push(`${interval.months} mon${interval.months !== 1 ? 's' : ''}`);
47
+ if (interval.days)
48
+ parts.push(`${interval.days} day${interval.days !== 1 ? 's' : ''}`);
49
+ // Build time component
50
+ const hours = interval.hours || 0;
51
+ const minutes = interval.minutes || 0;
52
+ const seconds = interval.seconds || 0;
53
+ const milliseconds = interval.milliseconds || 0;
54
+ if (hours || minutes || seconds || milliseconds) {
55
+ const totalSeconds = seconds + milliseconds / 1000;
56
+ const timeStr = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${totalSeconds.toFixed(milliseconds ? 6 : 0).padStart(milliseconds ? 9 : 2, '0')}`;
57
+ parts.push(timeStr);
58
+ }
59
+ return parts.length > 0 ? parts.join(' ') : '00:00:00';
60
+ }
61
+ return undefined;
62
+ };
32
63
  /**
33
64
  * Escape a single array element for PostgreSQL array literal format.
34
65
  * Handles: NULL, quotes, backslashes, commas, braces, and whitespace.
@@ -255,6 +286,50 @@ const getCoercionFunc = (type, from, opts, fieldName) => {
255
286
  });
256
287
  return wrapValue(val, opts);
257
288
  };
289
+ case 'uuid[]':
290
+ return (record) => {
291
+ const rawValue = record[from[0]];
292
+ if (isNullToken(rawValue)) {
293
+ return makeNullOrThrow(fieldName, rawValue, type, required, 'value is empty or null');
294
+ }
295
+ // Handle array values - validate each UUID
296
+ if (Array.isArray(rawValue)) {
297
+ if (rawValue.length === 0) {
298
+ return makeNullOrThrow(fieldName, rawValue, type, required, 'array is empty');
299
+ }
300
+ const uuidRegex = /^([0-9a-fA-F]{8})-(([0-9a-fA-F]{4}-){3})([0-9a-fA-F]{12})$/i;
301
+ for (const item of rawValue) {
302
+ if (!uuidRegex.test(String(item))) {
303
+ return makeNullOrThrow(fieldName, rawValue, type, required, `array contains invalid UUID: ${item}`);
304
+ }
305
+ }
306
+ const arrayLiteral = psqlArray(rawValue);
307
+ if (isEmpty(arrayLiteral)) {
308
+ return makeNullOrThrow(fieldName, rawValue, type, required, 'failed to format array');
309
+ }
310
+ const val = nodes.aConst({
311
+ sval: ast.string({ sval: String(arrayLiteral) })
312
+ });
313
+ return wrapValue(val, opts);
314
+ }
315
+ // If not an array, treat as empty/null
316
+ return makeNullOrThrow(fieldName, rawValue, type, required, 'value is not an array');
317
+ };
318
+ case 'interval':
319
+ return (record) => {
320
+ const rawValue = record[from[0]];
321
+ if (isNullToken(rawValue)) {
322
+ return makeNullOrThrow(fieldName, rawValue, type, required, 'value is empty or null');
323
+ }
324
+ const value = formatInterval(rawValue);
325
+ if (isEmpty(value)) {
326
+ return makeNullOrThrow(fieldName, rawValue, type, required, 'value is empty or invalid interval');
327
+ }
328
+ const val = nodes.aConst({
329
+ sval: ast.string({ sval: String(value) })
330
+ });
331
+ return wrapValue(val, opts);
332
+ };
258
333
  case 'timestamp':
259
334
  case 'timestamptz':
260
335
  case 'date':
package/esm/parser.js CHANGED
@@ -9,7 +9,7 @@ export class Parser {
9
9
  }
10
10
  async parse(data) {
11
11
  const config = this.config;
12
- const { schema, table, singleStmts, conflict, headers, delimeter } = config;
12
+ const { schema, table, singleStmts, conflict, conflictDoNothing, headers, delimeter } = config;
13
13
  const opts = {};
14
14
  if (headers)
15
15
  opts.headers = headers;
@@ -44,7 +44,8 @@ export class Parser {
44
44
  table,
45
45
  types,
46
46
  record,
47
- conflict
47
+ conflict,
48
+ conflictDoNothing
48
49
  }));
49
50
  return deparse(stmts);
50
51
  }
@@ -54,7 +55,8 @@ export class Parser {
54
55
  table,
55
56
  types,
56
57
  records,
57
- conflict
58
+ conflict,
59
+ conflictDoNothing
58
60
  });
59
61
  return deparse([stmt]);
60
62
  }
package/esm/utils.js CHANGED
@@ -106,7 +106,14 @@ const indexElem = (name) => ({
106
106
  nulls_ordering: 'SORTBY_NULLS_DEFAULT'
107
107
  }
108
108
  });
109
- const makeConflictClause = (conflictElems, fields) => {
109
+ const makeConflictClause = (conflictElems, fields, conflictDoNothing) => {
110
+ // If conflictDoNothing is true, generate ON CONFLICT DO NOTHING without specifying columns
111
+ // This catches any unique constraint violation
112
+ if (conflictDoNothing) {
113
+ return {
114
+ action: 'ONCONFLICT_NOTHING'
115
+ };
116
+ }
110
117
  if (!conflictElems || !conflictElems.length)
111
118
  return undefined;
112
119
  const setElems = fields.filter((el) => !conflictElems.includes(el));
@@ -128,7 +135,7 @@ const makeConflictClause = (conflictElems, fields) => {
128
135
  };
129
136
  }
130
137
  };
131
- export const InsertOne = ({ schema = 'public', table, types, record, conflict }) => ({
138
+ export const InsertOne = ({ schema = 'public', table, types, record, conflict, conflictDoNothing }) => ({
132
139
  RawStmt: {
133
140
  stmt: {
134
141
  InsertStmt: {
@@ -152,14 +159,14 @@ export const InsertOne = ({ schema = 'public', table, types, record, conflict })
152
159
  limitOption: 'LIMIT_OPTION_DEFAULT'
153
160
  }
154
161
  },
155
- onConflictClause: makeConflictClause(conflict, Object.keys(types)),
162
+ onConflictClause: makeConflictClause(conflict, Object.keys(types), conflictDoNothing),
156
163
  override: 'OVERRIDING_NOT_SET'
157
164
  }
158
165
  },
159
166
  stmt_len: 1
160
167
  }
161
168
  });
162
- export const InsertMany = ({ schema = 'public', table, types, records, conflict }) => ({
169
+ export const InsertMany = ({ schema = 'public', table, types, records, conflict, conflictDoNothing }) => ({
163
170
  RawStmt: {
164
171
  stmt: {
165
172
  InsertStmt: {
@@ -181,7 +188,7 @@ export const InsertMany = ({ schema = 'public', table, types, records, conflict
181
188
  limitOption: 'LIMIT_OPTION_DEFAULT'
182
189
  }
183
190
  },
184
- onConflictClause: makeConflictClause(conflict, Object.keys(types)),
191
+ onConflictClause: makeConflictClause(conflict, Object.keys(types), conflictDoNothing),
185
192
  override: 'OVERRIDING_NOT_SET'
186
193
  }
187
194
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "csv-to-pg",
3
- "version": "3.3.4",
3
+ "version": "3.4.1",
4
4
  "author": "Dan Lynch <pyramation@gmail.com>",
5
5
  "description": "csv to pg statements",
6
6
  "main": "index.js",
@@ -50,5 +50,5 @@
50
50
  "js-yaml": "^3.14.0",
51
51
  "pgsql-deparser": "^17.17.2"
52
52
  },
53
- "gitHead": "22c89cfc6f67879e77ca47f652c31c80828665f5"
53
+ "gitHead": "cf0d7de6f89b52e1e3dcd2cda068e7be27296615"
54
54
  }
package/parse.js CHANGED
@@ -35,6 +35,37 @@ const parseJson = (value) => {
35
35
  return value;
36
36
  return value ? JSON.stringify(value) : undefined;
37
37
  };
38
+ /**
39
+ * Convert a PostgreSQL interval object to a PostgreSQL interval string.
40
+ * node-postgres returns intervals as objects like { hours: 12, minutes: 6, seconds: 41 }
41
+ */
42
+ const formatInterval = (value) => {
43
+ if (typeof value === 'string') {
44
+ return value;
45
+ }
46
+ if (value && typeof value === 'object') {
47
+ const interval = value;
48
+ const parts = [];
49
+ if (interval.years)
50
+ parts.push(`${interval.years} year${interval.years !== 1 ? 's' : ''}`);
51
+ if (interval.months)
52
+ parts.push(`${interval.months} mon${interval.months !== 1 ? 's' : ''}`);
53
+ if (interval.days)
54
+ parts.push(`${interval.days} day${interval.days !== 1 ? 's' : ''}`);
55
+ // Build time component
56
+ const hours = interval.hours || 0;
57
+ const minutes = interval.minutes || 0;
58
+ const seconds = interval.seconds || 0;
59
+ const milliseconds = interval.milliseconds || 0;
60
+ if (hours || minutes || seconds || milliseconds) {
61
+ const totalSeconds = seconds + milliseconds / 1000;
62
+ const timeStr = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${totalSeconds.toFixed(milliseconds ? 6 : 0).padStart(milliseconds ? 9 : 2, '0')}`;
63
+ parts.push(timeStr);
64
+ }
65
+ return parts.length > 0 ? parts.join(' ') : '00:00:00';
66
+ }
67
+ return undefined;
68
+ };
38
69
  /**
39
70
  * Escape a single array element for PostgreSQL array literal format.
40
71
  * Handles: NULL, quotes, backslashes, commas, braces, and whitespace.
@@ -264,6 +295,50 @@ const getCoercionFunc = (type, from, opts, fieldName) => {
264
295
  });
265
296
  return (0, utils_2.wrapValue)(val, opts);
266
297
  };
298
+ case 'uuid[]':
299
+ return (record) => {
300
+ const rawValue = record[from[0]];
301
+ if (isNullToken(rawValue)) {
302
+ return makeNullOrThrow(fieldName, rawValue, type, required, 'value is empty or null');
303
+ }
304
+ // Handle array values - validate each UUID
305
+ if (Array.isArray(rawValue)) {
306
+ if (rawValue.length === 0) {
307
+ return makeNullOrThrow(fieldName, rawValue, type, required, 'array is empty');
308
+ }
309
+ const uuidRegex = /^([0-9a-fA-F]{8})-(([0-9a-fA-F]{4}-){3})([0-9a-fA-F]{12})$/i;
310
+ for (const item of rawValue) {
311
+ if (!uuidRegex.test(String(item))) {
312
+ return makeNullOrThrow(fieldName, rawValue, type, required, `array contains invalid UUID: ${item}`);
313
+ }
314
+ }
315
+ const arrayLiteral = psqlArray(rawValue);
316
+ if (isEmpty(arrayLiteral)) {
317
+ return makeNullOrThrow(fieldName, rawValue, type, required, 'failed to format array');
318
+ }
319
+ const val = utils_1.nodes.aConst({
320
+ sval: utils_1.ast.string({ sval: String(arrayLiteral) })
321
+ });
322
+ return (0, utils_2.wrapValue)(val, opts);
323
+ }
324
+ // If not an array, treat as empty/null
325
+ return makeNullOrThrow(fieldName, rawValue, type, required, 'value is not an array');
326
+ };
327
+ case 'interval':
328
+ return (record) => {
329
+ const rawValue = record[from[0]];
330
+ if (isNullToken(rawValue)) {
331
+ return makeNullOrThrow(fieldName, rawValue, type, required, 'value is empty or null');
332
+ }
333
+ const value = formatInterval(rawValue);
334
+ if (isEmpty(value)) {
335
+ return makeNullOrThrow(fieldName, rawValue, type, required, 'value is empty or invalid interval');
336
+ }
337
+ const val = utils_1.nodes.aConst({
338
+ sval: utils_1.ast.string({ sval: String(value) })
339
+ });
340
+ return (0, utils_2.wrapValue)(val, opts);
341
+ };
267
342
  case 'timestamp':
268
343
  case 'timestamptz':
269
344
  case 'date':
package/parser.d.ts CHANGED
@@ -3,6 +3,7 @@ interface ParserConfig {
3
3
  table: string;
4
4
  singleStmts?: boolean;
5
5
  conflict?: string[];
6
+ conflictDoNothing?: boolean;
6
7
  headers?: string[];
7
8
  delimeter?: string;
8
9
  json?: boolean;
package/parser.js CHANGED
@@ -12,7 +12,7 @@ class Parser {
12
12
  }
13
13
  async parse(data) {
14
14
  const config = this.config;
15
- const { schema, table, singleStmts, conflict, headers, delimeter } = config;
15
+ const { schema, table, singleStmts, conflict, conflictDoNothing, headers, delimeter } = config;
16
16
  const opts = {};
17
17
  if (headers)
18
18
  opts.headers = headers;
@@ -47,7 +47,8 @@ class Parser {
47
47
  table,
48
48
  types,
49
49
  record,
50
- conflict
50
+ conflict,
51
+ conflictDoNothing
51
52
  }));
52
53
  return (0, pgsql_deparser_1.deparse)(stmts);
53
54
  }
@@ -57,7 +58,8 @@ class Parser {
57
58
  table,
58
59
  types,
59
60
  records,
60
- conflict
61
+ conflict,
62
+ conflictDoNothing
61
63
  });
62
64
  return (0, pgsql_deparser_1.deparse)([stmt]);
63
65
  }
package/utils.d.ts CHANGED
@@ -11,16 +11,18 @@ interface InsertOneParams {
11
11
  types: TypesMap;
12
12
  record: Record<string, unknown>;
13
13
  conflict?: string[];
14
+ conflictDoNothing?: boolean;
14
15
  }
15
- export declare const InsertOne: ({ schema, table, types, record, conflict }: InsertOneParams) => Node;
16
+ export declare const InsertOne: ({ schema, table, types, record, conflict, conflictDoNothing }: InsertOneParams) => Node;
16
17
  interface InsertManyParams {
17
18
  schema?: string;
18
19
  table: string;
19
20
  types: TypesMap;
20
21
  records: Record<string, unknown>[];
21
22
  conflict?: string[];
23
+ conflictDoNothing?: boolean;
22
24
  }
23
- export declare const InsertMany: ({ schema, table, types, records, conflict }: InsertManyParams) => Node;
25
+ export declare const InsertMany: ({ schema, table, types, records, conflict, conflictDoNothing }: InsertManyParams) => Node;
24
26
  interface WrapOptions {
25
27
  wrap?: string[];
26
28
  wrapAst?: (val: Node) => Node;
package/utils.js CHANGED
@@ -112,7 +112,14 @@ const indexElem = (name) => ({
112
112
  nulls_ordering: 'SORTBY_NULLS_DEFAULT'
113
113
  }
114
114
  });
115
- const makeConflictClause = (conflictElems, fields) => {
115
+ const makeConflictClause = (conflictElems, fields, conflictDoNothing) => {
116
+ // If conflictDoNothing is true, generate ON CONFLICT DO NOTHING without specifying columns
117
+ // This catches any unique constraint violation
118
+ if (conflictDoNothing) {
119
+ return {
120
+ action: 'ONCONFLICT_NOTHING'
121
+ };
122
+ }
116
123
  if (!conflictElems || !conflictElems.length)
117
124
  return undefined;
118
125
  const setElems = fields.filter((el) => !conflictElems.includes(el));
@@ -134,7 +141,7 @@ const makeConflictClause = (conflictElems, fields) => {
134
141
  };
135
142
  }
136
143
  };
137
- const InsertOne = ({ schema = 'public', table, types, record, conflict }) => ({
144
+ const InsertOne = ({ schema = 'public', table, types, record, conflict, conflictDoNothing }) => ({
138
145
  RawStmt: {
139
146
  stmt: {
140
147
  InsertStmt: {
@@ -158,7 +165,7 @@ const InsertOne = ({ schema = 'public', table, types, record, conflict }) => ({
158
165
  limitOption: 'LIMIT_OPTION_DEFAULT'
159
166
  }
160
167
  },
161
- onConflictClause: makeConflictClause(conflict, Object.keys(types)),
168
+ onConflictClause: makeConflictClause(conflict, Object.keys(types), conflictDoNothing),
162
169
  override: 'OVERRIDING_NOT_SET'
163
170
  }
164
171
  },
@@ -166,7 +173,7 @@ const InsertOne = ({ schema = 'public', table, types, record, conflict }) => ({
166
173
  }
167
174
  });
168
175
  exports.InsertOne = InsertOne;
169
- const InsertMany = ({ schema = 'public', table, types, records, conflict }) => ({
176
+ const InsertMany = ({ schema = 'public', table, types, records, conflict, conflictDoNothing }) => ({
170
177
  RawStmt: {
171
178
  stmt: {
172
179
  InsertStmt: {
@@ -188,7 +195,7 @@ const InsertMany = ({ schema = 'public', table, types, records, conflict }) => (
188
195
  limitOption: 'LIMIT_OPTION_DEFAULT'
189
196
  }
190
197
  },
191
- onConflictClause: makeConflictClause(conflict, Object.keys(types)),
198
+ onConflictClause: makeConflictClause(conflict, Object.keys(types), conflictDoNothing),
192
199
  override: 'OVERRIDING_NOT_SET'
193
200
  }
194
201
  },