@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/README.md +5 -3
- package/dist/aggregate.d.ts.map +1 -1
- package/dist/chunks/{alter-utils-F8JQn1LL.js → alter-utils-DeDDAATm.js} +25 -10
- package/dist/chunks/{alter-utils-F8JQn1LL.js.map → alter-utils-DeDDAATm.js.map} +1 -1
- package/dist/chunks/{duckdb-SVvTBGT0.js → duckdb-jSSa2MbC.js} +13 -11
- package/dist/chunks/{duckdb-SVvTBGT0.js.map → duckdb-jSSa2MbC.js.map} +1 -1
- package/dist/chunks/{sqlite-BoP0WIlT.js → sqlite-Crx5PQ3G.js} +19 -11
- package/dist/chunks/sqlite-Crx5PQ3G.js.map +1 -0
- package/dist/chunks/{sqlite-native-Ctfv7fW_.js → sqlite-native-DCAaADpP.js} +18 -10
- package/dist/chunks/sqlite-native-DCAaADpP.js.map +1 -0
- package/dist/chunks/types-uVFHgaSU.js.map +1 -1
- package/dist/duckdb.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +81 -30
- package/dist/index.js.map +1 -1
- package/dist/json.d.ts.map +1 -1
- package/dist/shared/alter-utils.d.ts +15 -2
- package/dist/shared/alter-utils.d.ts.map +1 -1
- package/dist/shared/types.d.ts +49 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/sqlite.d.ts.map +1 -1
- package/package.json +2 -2
- package/dist/chunks/sqlite-BoP0WIlT.js.map +0 -1
- package/dist/chunks/sqlite-native-Ctfv7fW_.js.map +0 -1
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-
|
|
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 =
|
|
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
|
|
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(
|
|
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 &&
|
|
967
|
-
const keys = Object.keys(
|
|
968
|
-
const dataValues = Object.values(
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
2557
|
+
const serializedData = serializeRecord(data);
|
|
2558
|
+
const conflictCols = [...conflictColumns];
|
|
2559
|
+
validateColumnNames([...conflictCols, ...Object.keys(serializedData)]);
|
|
2509
2560
|
try {
|
|
2510
|
-
return await executePostgresUpsert(executor, table,
|
|
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-
|
|
3285
|
-
if (options.type === "duckdb") return (await import("./chunks/duckdb-
|
|
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 = {
|