@happyvertical/sql 0.82.0 → 0.84.0

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/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as validateColumnName$1, c as validateTableName$1, d as formatDbError, f as parseConditionKey, h as unwrapRawKey, i as generateCreateIndexStatement, l as DatabaseSchemaManager, m as resolveInsertColumns, n as escapeStringLiteral, o as validateColumnNames, p as raw, r as generateAddColumnStatement, s as validateIndexName, t as escapeQuotedIdentifier, u as buildWhere } from "./chunks/alter-utils-F8JQn1LL.js";
1
+ import { a as validateColumnName$1, c as validateTableName$1, d as formatDbError, f as parseConditionKey, h as unwrapRawKey, i as generateCreateIndexStatement, l as DatabaseSchemaManager, m as resolveInsertColumns, n as escapeStringLiteral, o as validateColumnNames, p as raw, r as generateAddColumnStatement, s as validateIndexName, t as escapeQuotedIdentifier, u as buildWhere } from "./chunks/alter-utils-DeDDAATm.js";
2
2
  import { t as convertUniqueIndexesToInlineConstraints } from "./chunks/duckdb-schema-utils-B-XBP8QF.js";
3
3
  import { a as dropPostgresDatabase, c as postgresEnvFromUrl, i as databaseNameFromUrl, l as redactDatabaseUrl, n as assertCanImportDatabase, o as dumpPostgresDatabase, r as createPostgresDatabase, s as isLocalDatabaseUrl, t as assertCanExportDatabase, u as restorePostgresDatabase } from "./chunks/postgres-cli-DzZcXSIG.js";
4
4
  import { n as resolveSchemas, r as createTransactionLock, t as NestedTransactionError } from "./chunks/types-uVFHgaSU.js";
@@ -30,7 +30,7 @@ var VALID_BUCKET_UNITS = /* @__PURE__ */ new Set([
30
30
  "year"
31
31
  ]);
32
32
  function validateSqlIdentifier(identifier) {
33
- if (!/^[a-zA-Z0-9_.]+$/.test(identifier)) throw new Error(`Invalid SQL identifier: ${identifier}`);
33
+ if (typeof identifier !== "string" || !/^[a-zA-Z0-9_.]+$/.test(identifier)) throw new Error(`Invalid SQL identifier: ${identifier}`);
34
34
  return identifier;
35
35
  }
36
36
  function normalizeAdapter(adapterType) {
@@ -347,6 +347,15 @@ async function createJSONConnection(options) {
347
347
  if (!url) throw new DatabaseError("url is required for JSON adapter", { options });
348
348
  const pendingJSONFiles = /* @__PURE__ */ new Map();
349
349
  const inferredSchemaTables = /* @__PURE__ */ new Set();
350
+ const tableLoadErrors = [];
351
+ const reportLoadError = (info) => {
352
+ tableLoadErrors.push(info);
353
+ try {
354
+ options.onTableLoadError?.(info);
355
+ } catch (callbackError) {
356
+ console.error("[json-adapter] onTableLoadError callback threw:", callbackError);
357
+ }
358
+ };
350
359
  try {
351
360
  const { DuckDBInstance } = await import(
352
361
  /* @vite-ignore */
@@ -356,11 +365,13 @@ async function createJSONConnection(options) {
356
365
  try {
357
366
  await mkdir(url, { recursive: true });
358
367
  } catch (_error) {}
359
- if (autoRegister) await loadJSONTables(connection, url, schemas, pendingJSONFiles, eagerLoadTables, inferredSchemaTables);
368
+ if (autoRegister) await loadJSONTables(connection, url, schemas, pendingJSONFiles, eagerLoadTables, inferredSchemaTables, reportLoadError);
360
369
  return {
361
370
  connection,
362
371
  pendingJSONFiles,
363
- inferredSchemaTables
372
+ inferredSchemaTables,
373
+ tableLoadErrors,
374
+ reportLoadError
364
375
  };
365
376
  } catch (error) {
366
377
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -389,8 +400,9 @@ async function createJSONConnection(options) {
389
400
  * @param connection - DuckDB connection
390
401
  * @param tableName - Name of the table to load data into
391
402
  * @param filePath - Path to the JSON file
403
+ * @param reportLoadError - Sink invoked when parsed records fail to insert (issue #1139)
392
404
  */
393
- async function loadJSONData(connection, tableName, filePath) {
405
+ async function loadJSONData(connection, tableName, filePath, reportLoadError) {
394
406
  let records;
395
407
  try {
396
408
  const { readFile } = await import("node:fs/promises");
@@ -406,6 +418,11 @@ async function loadJSONData(connection, tableName, filePath) {
406
418
  if (inserted > 0) console.log(`[json-adapter] Loaded ${inserted} records into ${tableName} from JSON`);
407
419
  } catch (error) {
408
420
  console.error(`[json-adapter] Failed to load data for ${tableName}; the table will be empty:`, error);
421
+ reportLoadError?.({
422
+ table: tableName,
423
+ filePath,
424
+ error
425
+ });
409
426
  }
410
427
  }
411
428
  /**
@@ -432,8 +449,9 @@ async function loadJSONData(connection, tableName, filePath) {
432
449
  * @param pendingJSONFiles - Map to store JSON files pending deferred loading
433
450
  * @param eagerLoadTables - Whether to eagerly create tables with inferred schemas
434
451
  * @param inferredSchemaTables - Set to track tables created with inferred schemas
452
+ * @param reportLoadError - Sink for per-table data-load failures (issue #1139)
435
453
  */
436
- async function loadJSONTables(connection, dataDir, providedSchemas = {}, pendingJSONFiles = /* @__PURE__ */ new Map(), eagerLoadTables = true, inferredSchemaTables = /* @__PURE__ */ new Set()) {
454
+ async function loadJSONTables(connection, dataDir, providedSchemas = {}, pendingJSONFiles = /* @__PURE__ */ new Map(), eagerLoadTables = true, inferredSchemaTables = /* @__PURE__ */ new Set(), reportLoadError) {
437
455
  try {
438
456
  const jsonFiles = (await readdir(dataDir)).filter((file) => extname(file).toLowerCase() === ".json");
439
457
  for (const file of jsonFiles) {
@@ -443,7 +461,7 @@ async function loadJSONTables(connection, dataDir, providedSchemas = {}, pending
443
461
  if (schema) {
444
462
  console.log(`[json-adapter] Creating ${tableName} with provided schema definition`);
445
463
  await createTableFromSchema(connection, tableName, schema);
446
- await loadJSONData(connection, tableName, filePath);
464
+ await loadJSONData(connection, tableName, filePath, reportLoadError);
447
465
  } else {
448
466
  const schemaPath = join(dataDir, `${tableName}.schema.sql`);
449
467
  const { readFile, access } = await import("node:fs/promises");
@@ -458,7 +476,7 @@ async function loadJSONTables(connection, dataDir, providedSchemas = {}, pending
458
476
  if (schemaExists) try {
459
477
  const schemaDDL = await readFile(schemaPath, "utf-8");
460
478
  await connection.run(schemaDDL);
461
- await loadJSONData(connection, tableName, filePath);
479
+ await loadJSONData(connection, tableName, filePath, reportLoadError);
462
480
  } catch (error) {
463
481
  console.warn(`[json-adapter] Could not load ${tableName} with schema file: ${error instanceof Error ? error.message : String(error)}`);
464
482
  }
@@ -715,7 +733,7 @@ async function insertRecordsWithCast(connection, tableName, records) {
715
733
  * @throws DatabaseError if table name is invalid
716
734
  */
717
735
  function validateTableName(table) {
718
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table)) throw new DatabaseError(`Invalid table name: ${table}`, { table });
736
+ if (typeof table !== "string" || !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table)) throw new DatabaseError(`Invalid table name: ${table}`, { table });
719
737
  }
720
738
  /**
721
739
  * Creates a JSON database adapter using DuckDB in-memory engine
@@ -738,7 +756,7 @@ async function getDatabase$2(options) {
738
756
  const pending = pendingConnections$1.get(cacheKey);
739
757
  if (pending) return pending;
740
758
  const connectionPromise = (async () => {
741
- const { connection, pendingJSONFiles, inferredSchemaTables } = await createJSONConnection(options);
759
+ const { connection, pendingJSONFiles, inferredSchemaTables, tableLoadErrors, reportLoadError } = await createJSONConnection(options);
742
760
  const writeStrategy = options.writeStrategy || "immediate";
743
761
  const { url } = options;
744
762
  const connectionLock = createTransactionLock("json", options.transactionQueueTimeout);
@@ -871,7 +889,10 @@ async function getDatabase$2(options) {
871
889
  validateColumnNames(keys);
872
890
  const setClause = keys.map((key, idx) => `${key} = $${idx + 1}`).join(", ");
873
891
  const { sql: whereClause, values: whereValues } = buildWhere(where, keys.length + 1, "json");
874
- const dataValues = Object.values(data).map((value) => value instanceof Date ? value.toISOString() : value);
892
+ const dataValues = keys.map((key) => {
893
+ const value = data[key];
894
+ return value instanceof Date ? value.toISOString() : value;
895
+ });
875
896
  const sql = `UPDATE "${table}" SET ${setClause} ${whereClause}`;
876
897
  const values = [...dataValues, ...whereValues];
877
898
  try {
@@ -955,17 +976,19 @@ async function getDatabase$2(options) {
955
976
  table,
956
977
  writeStrategy
957
978
  });
958
- const missingColumns = conflictColumns.filter((col) => !(col in data));
979
+ const record = { ...data };
980
+ const conflictCols = conflictColumns.map((col) => String(col));
981
+ const missingColumns = conflictCols.filter((col) => !(col in record));
959
982
  if (missingColumns.length > 0) throw new DatabaseError("Conflict columns missing from data", {
960
983
  table,
961
- conflictColumns,
984
+ conflictColumns: conflictCols,
962
985
  missingColumns,
963
- availableColumns: Object.keys(data),
986
+ availableColumns: Object.keys(record),
964
987
  hint: "All columns specified in ON CONFLICT must be present in the data being inserted. Undefined values should be replaced with null or an appropriate default."
965
988
  });
966
- if (!options?.nullsDistinct && conflictColumns.some((col) => data[col] === null)) return executeNullAwareJSONUpsert(table, conflictColumns, data);
967
- const keys = Object.keys(data);
968
- const dataValues = Object.values(data);
989
+ if (!options?.nullsDistinct && conflictCols.some((col) => record[col] === null)) return executeNullAwareJSONUpsert(table, conflictCols, record);
990
+ const keys = Object.keys(record);
991
+ const dataValues = Object.values(record);
969
992
  const placeholders = [];
970
993
  const values = [];
971
994
  let paramIdx = 1;
@@ -1018,7 +1041,7 @@ async function getDatabase$2(options) {
1018
1041
  paramIdx++;
1019
1042
  }
1020
1043
  }
1021
- const conflict = conflictColumns.map((col) => `"${escapeQuotedIdentifier(col)}"`).join(", ");
1044
+ const conflict = conflictCols.map((col) => `"${escapeQuotedIdentifier(col)}"`).join(", ");
1022
1045
  const quotedKeys = keys.map((key) => `"${escapeQuotedIdentifier(key)}"`).join(", ");
1023
1046
  const quotedUpdateSetParts = keys.map((key, i) => {
1024
1047
  const part = updateSetParts[i];
@@ -1039,14 +1062,14 @@ async function getDatabase$2(options) {
1039
1062
  sql,
1040
1063
  values,
1041
1064
  valueTypes: values.map((v) => `${typeof v} (${v})`),
1042
- conflictColumns,
1065
+ conflictColumns: conflictCols,
1043
1066
  error: formatDbError(e)
1044
1067
  });
1045
1068
  throw new DatabaseError("Failed to upsert record into table", {
1046
1069
  table,
1047
1070
  sql,
1048
1071
  values,
1049
- conflictColumns,
1072
+ conflictColumns: conflictCols,
1050
1073
  originalError: formatDbError(e)
1051
1074
  });
1052
1075
  }
@@ -1220,6 +1243,32 @@ async function getDatabase$2(options) {
1220
1243
  await exportTableToJSON(connection, table, url);
1221
1244
  };
1222
1245
  /**
1246
+ * Returns the table-load failures collected for this connection
1247
+ *
1248
+ * A JSON data file that parses but whose records cannot be inserted into a
1249
+ * table created for it (a renamed/dropped column, a NOT NULL / PRIMARY KEY
1250
+ * violation, or a file whose fields match no column) leaves that table
1251
+ * present-but-empty and is logged to stderr; calling code otherwise cannot
1252
+ * tell it apart from a legitimately empty one. This exposes those failures
1253
+ * so a caller can detect and react to them (issue #1139).
1254
+ *
1255
+ * Scope: this covers loads into a table created from a provided schema, a
1256
+ * `.schema.sql` file, or deferred `syncSchema()` / `execute()` DDL. A file
1257
+ * with no schema at all is loaded by DuckDB's own inference; if that fails
1258
+ * the table is simply not created, which already surfaces as an error when
1259
+ * you query it, so it is not reported here.
1260
+ *
1261
+ * The list accumulates across the connection's lifetime — covering both
1262
+ * connection-time and later deferred loads — and is append-only (a later
1263
+ * successful reload of the same table does not remove an earlier failure).
1264
+ * A fresh array is returned each call, so mutating it does not affect the
1265
+ * adapter's state. Because connections are cached per URL, callers sharing a
1266
+ * URL share this list.
1267
+ *
1268
+ * @returns Table-load failures, oldest first
1269
+ */
1270
+ const getTableLoadErrors = () => [...tableLoadErrors];
1271
+ /**
1223
1272
  * Creates a table by inferring schema from a JSON file
1224
1273
  *
1225
1274
  * This utility is for users who want automatic schema inference from JSON.
@@ -1362,7 +1411,7 @@ async function getDatabase$2(options) {
1362
1411
  const tableName = createTableMatch[1];
1363
1412
  const pendingFile = pendingJSONFiles.get(tableName);
1364
1413
  if (pendingFile) {
1365
- await loadJSONData(connection, tableName, pendingFile);
1414
+ await loadJSONData(connection, tableName, pendingFile, reportLoadError);
1366
1415
  pendingJSONFiles.delete(tableName);
1367
1416
  } else if (writeStrategy !== "none") try {
1368
1417
  const { access } = await import("node:fs/promises");
@@ -1466,7 +1515,7 @@ async function getDatabase$2(options) {
1466
1515
  if (tableName) {
1467
1516
  const pendingFile = pendingJSONFiles.get(tableName);
1468
1517
  if (pendingFile) {
1469
- await loadJSONData(connection, tableName, pendingFile);
1518
+ await loadJSONData(connection, tableName, pendingFile, reportLoadError);
1470
1519
  pendingJSONFiles.delete(tableName);
1471
1520
  } else if (writeStrategy !== "none") try {
1472
1521
  const { access } = await import("node:fs/promises");
@@ -1658,7 +1707,8 @@ async function getDatabase$2(options) {
1658
1707
  transaction,
1659
1708
  beginTransaction,
1660
1709
  exportTable,
1661
- inferSchemaFromJSON
1710
+ inferSchemaFromJSON,
1711
+ getTableLoadErrors
1662
1712
  };
1663
1713
  })();
1664
1714
  pendingConnections$1.set(cacheKey, connectionPromise);
@@ -2287,8 +2337,7 @@ async function createDatabase(options, cacheKey) {
2287
2337
  throw error;
2288
2338
  }
2289
2339
  };
2290
- const executePostgresUpsert = async (executor, table, conflictColumns, data, options, acquireTransaction) => {
2291
- const serializedData = serializeRecord(data);
2340
+ const executePostgresUpsert = async (executor, table, conflictColumns, serializedData, options, acquireTransaction) => {
2292
2341
  validateUpsertConflictColumns(table, conflictColumns, serializedData);
2293
2342
  if (options?.nullsDistinct || !hasNullConflictValue(conflictColumns, serializedData)) return executeStandardPostgresUpsert(executor, table, conflictColumns, serializedData);
2294
2343
  if (await hasNativeNullsNotDistinctIndex(table, conflictColumns)) return executeStandardPostgresUpsert(executor, table, conflictColumns, serializedData);
@@ -2505,9 +2554,11 @@ async function createDatabase(options, cacheKey) {
2505
2554
  */
2506
2555
  const upsert = async (table, conflictColumns, data, options) => {
2507
2556
  validateTableName$1(table);
2508
- validateColumnNames([...conflictColumns, ...Object.keys(data)]);
2557
+ const serializedData = serializeRecord(data);
2558
+ const conflictCols = [...conflictColumns];
2559
+ validateColumnNames([...conflictCols, ...Object.keys(serializedData)]);
2509
2560
  try {
2510
- return await executePostgresUpsert(executor, table, conflictColumns, data, options, !inTransaction);
2561
+ return await executePostgresUpsert(executor, table, conflictCols, serializedData, options, !inTransaction);
2511
2562
  } catch (e) {
2512
2563
  if (e instanceof DatabaseError && e.message === "Conflict columns missing from data") throw e;
2513
2564
  throw new DatabaseError("Failed to upsert record into table", {
@@ -3281,8 +3332,8 @@ async function getDatabase(options = {}) {
3281
3332
  }));
3282
3333
  if (!options.type && (options.url?.startsWith("file:") || options.url === ":memory:")) options.type = "sqlite";
3283
3334
  if (options.type === "postgres") return (await Promise.resolve().then(() => postgres_exports)).getDatabase(options);
3284
- if (options.type === "sqlite") return (await import("./chunks/sqlite-BoP0WIlT.js")).getDatabase(options);
3285
- if (options.type === "duckdb") return (await import("./chunks/duckdb-SVvTBGT0.js")).getDatabase(options);
3335
+ if (options.type === "sqlite") return (await import("./chunks/sqlite-Crx5PQ3G.js")).getDatabase(options);
3336
+ if (options.type === "duckdb") return (await import("./chunks/duckdb-jSSa2MbC.js")).getDatabase(options);
3286
3337
  if (options.type === "json") return (await Promise.resolve().then(() => json_exports)).getDatabase(options);
3287
3338
  throw new Error("Invalid database type");
3288
3339
  }
@@ -3332,7 +3383,7 @@ function escapeSqlValue(value) {
3332
3383
  * @throws Error if the column name contains invalid characters
3333
3384
  */
3334
3385
  function validateColumnName(column) {
3335
- if (!/^[a-zA-Z0-9_.]+$/.test(column)) throw new Error(`Invalid column name: ${column}`);
3386
+ if (typeof column !== "string" || !/^[a-zA-Z0-9_.]+$/.test(column)) throw new Error(`Invalid column name: ${column}`);
3336
3387
  return column;
3337
3388
  }
3338
3389
  var src_default = {