@toiroakr/lines-db 0.8.0 → 0.9.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/dist/index.js CHANGED
@@ -2,6 +2,7 @@ import { createRequire } from "node:module";
2
2
  import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
3
3
  import { basename, dirname, extname, isAbsolute, join, normalize, relative } from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
+ import { styleText } from "node:util";
5
6
 
6
7
  //#region rolldown:runtime
7
8
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
@@ -385,13 +386,20 @@ var LinesDB = class LinesDB {
385
386
  const loadedTables = /* @__PURE__ */ new Set();
386
387
  const loadingTables = /* @__PURE__ */ new Set();
387
388
  const attemptedTables = /* @__PURE__ */ new Set();
389
+ const allDeferredForeignKeys = [];
388
390
  for (const tableNameToLoad of tablesToLoad) if (!attemptedTables.has(tableNameToLoad)) {
389
391
  const tableTransform = tableNameToLoad === tableName ? transform : void 0;
390
- const { errors, warnings, rowCounts: tableRowCounts } = await this.loadTableWithDependencies(tableNameToLoad, loadedTables, loadingTables, attemptedTables, detailedValidate, tableTransform);
392
+ const { errors, warnings, rowCounts: tableRowCounts, deferredForeignKeys } = await this.loadTableWithDependencies(tableNameToLoad, loadedTables, loadingTables, attemptedTables, detailedValidate, tableTransform);
391
393
  allErrors.push(...errors);
392
394
  allWarnings.push(...warnings);
395
+ allDeferredForeignKeys.push(...deferredForeignKeys);
393
396
  for (const [k, v] of tableRowCounts) allRowCounts.set(k, v);
394
397
  }
398
+ if (detailedValidate && allDeferredForeignKeys.length > 0) for (const { tableName: tName, foreignKey: fk, filePath } of allDeferredForeignKeys) {
399
+ if (!loadedTables.has(fk.references.table)) continue;
400
+ const deferredErrors = this.validateDeferredForeignKey(tName, fk, filePath);
401
+ allErrors.push(...deferredErrors);
402
+ }
395
403
  const tableResults = tablesToLoad.map((name) => {
396
404
  const tableErrors = allErrors.filter((e) => e.tableName === name);
397
405
  const tableWarnings = allWarnings.filter((w) => w.includes(`'${name}'`));
@@ -417,10 +425,12 @@ var LinesDB = class LinesDB {
417
425
  const errors = [];
418
426
  const warnings = [];
419
427
  const rowCounts = /* @__PURE__ */ new Map();
428
+ const deferredForeignKeys = [];
420
429
  if (attemptedTables.has(tableName)) return {
421
430
  errors,
422
431
  warnings,
423
- rowCounts
432
+ rowCounts,
433
+ deferredForeignKeys
424
434
  };
425
435
  attemptedTables.add(tableName);
426
436
  if (loadingTables.has(tableName)) throw new Error(`Circular dependency detected for table '${tableName}'`);
@@ -444,23 +454,35 @@ var LinesDB = class LinesDB {
444
454
  const depResult = await this.loadTableWithDependencies(referencedTable, loadedTables, loadingTables, attemptedTables, detailedValidate, void 0);
445
455
  errors.push(...depResult.errors);
446
456
  warnings.push(...depResult.warnings);
457
+ deferredForeignKeys.push(...depResult.deferredForeignKeys);
447
458
  for (const [k, v] of depResult.rowCounts) rowCounts.set(k, v);
448
459
  } else throw new Error(`Foreign key reference to non-existent table '${referencedTable}' in table '${tableName}'`);
449
460
  }
450
461
  const failedDependencies = /* @__PURE__ */ new Set();
462
+ const circularDependencies = /* @__PURE__ */ new Set();
451
463
  if (foreignKeys && foreignKeys.length > 0) {
452
464
  for (const fk of foreignKeys) {
453
465
  const referencedTable = fk.references.table;
454
466
  if (referencedTable === tableName) continue;
455
- if (attemptedTables.has(referencedTable) && !loadedTables.has(referencedTable)) failedDependencies.add(referencedTable);
467
+ if (attemptedTables.has(referencedTable) && !loadedTables.has(referencedTable)) if (loadingTables.has(referencedTable)) circularDependencies.add(referencedTable);
468
+ else failedDependencies.add(referencedTable);
456
469
  }
457
470
  if (failedDependencies.size > 0) for (const dep of failedDependencies) warnings.push(`Skipping foreign key validation for table '${tableName}': referenced table '${dep}' has validation errors`);
458
471
  }
459
- const { loaded, rowCount, errors: loadErrors } = await this.loadTable(tableName, tableConfig, detailedValidate, transform, failedDependencies);
472
+ const allSkippedDependencies = new Set([...failedDependencies, ...circularDependencies]);
473
+ const { loaded, rowCount, errors: loadErrors } = await this.loadTable(tableName, tableConfig, detailedValidate, transform, allSkippedDependencies);
460
474
  errors.push(...loadErrors);
461
475
  rowCounts.set(tableName, rowCount);
462
- if (loaded) loadedTables.add(tableName);
463
- else {
476
+ if (loaded) {
477
+ loadedTables.add(tableName);
478
+ if (foreignKeys && circularDependencies.size > 0) {
479
+ for (const fk of foreignKeys) if (circularDependencies.has(fk.references.table)) deferredForeignKeys.push({
480
+ tableName,
481
+ foreignKey: fk,
482
+ filePath: tableConfig.jsonlPath
483
+ });
484
+ }
485
+ } else {
464
486
  warnings.push(`Table '${tableName}' was not loaded (no data or skipped)`);
465
487
  this.tables.delete(tableName);
466
488
  }
@@ -470,7 +492,8 @@ var LinesDB = class LinesDB {
470
492
  return {
471
493
  errors,
472
494
  warnings,
473
- rowCounts
495
+ rowCounts,
496
+ deferredForeignKeys
474
497
  };
475
498
  }
476
499
  /**
@@ -716,6 +739,34 @@ var LinesDB = class LinesDB {
716
739
  };
717
740
  }
718
741
  /**
742
+ * Validate a deferred foreign key constraint after all tables have been loaded.
743
+ * Used for circular dependency FK validation.
744
+ */
745
+ validateDeferredForeignKey(tableName, fk, filePath) {
746
+ const errors = [];
747
+ const quotedTable = this.quoteTableName(tableName);
748
+ const quotedColumn = this.quoteIdentifier(fk.column);
749
+ const quotedRefTable = this.quoteTableName(fk.references.table);
750
+ const sql = `SELECT rowid - 1 as idx, ${quotedColumn} as val FROM ${quotedTable} WHERE ${quotedColumn} IS NOT NULL AND ${quotedColumn} NOT IN (SELECT ${this.quoteIdentifier(fk.references.column)} FROM ${quotedRefTable})`;
751
+ try {
752
+ const rows = this.query(sql);
753
+ for (const row of rows) errors.push({
754
+ file: filePath,
755
+ tableName,
756
+ rowIndex: row.idx,
757
+ issues: [],
758
+ type: "foreignKey",
759
+ foreignKeyError: {
760
+ column: fk.column,
761
+ value: row.val,
762
+ referencedTable: fk.references.table,
763
+ referencedColumn: fk.references.column
764
+ }
765
+ });
766
+ } catch (_) {}
767
+ return errors;
768
+ }
769
+ /**
719
770
  * Execute a raw SQL query
720
771
  */
721
772
  query(sql, params = []) {
@@ -1304,5 +1355,108 @@ async function ensureTableRowsValid(options) {
1304
1355
  }
1305
1356
 
1306
1357
  //#endregion
1307
- export { DirectoryScanner, JsonlReader, JsonlWriter, LinesDB, RUNTIME, SCHEMA_EXTENSIONS, SchemaLoader, TypeGenerator, defineSchema, detectRuntime, ensureTableRowsValid, extractTableNameFromSchemaFile, findSchemaFile, hasBackward, isSchemaFile, rewriteExtensionForImport };
1358
+ //#region src/error-formatter.ts
1359
+ var ErrorFormatter = class {
1360
+ verbose;
1361
+ constructor(options = {}) {
1362
+ this.verbose = options.verbose ?? false;
1363
+ }
1364
+ /**
1365
+ * Format validation errors
1366
+ */
1367
+ formatValidationErrors(errors) {
1368
+ if (this.verbose) return this.formatValidationErrorsVerbose(errors);
1369
+ return this.formatValidationErrorsCompact(errors);
1370
+ }
1371
+ /**
1372
+ * Format foreign key error
1373
+ */
1374
+ formatForeignKeyError(error) {
1375
+ if (this.verbose) return this.formatForeignKeyErrorVerbose(error);
1376
+ return this.formatForeignKeyErrorCompact(error);
1377
+ }
1378
+ /**
1379
+ * Format compact (default) validation errors
1380
+ */
1381
+ formatValidationErrorsCompact(errors) {
1382
+ const lines = [];
1383
+ for (const error of errors) for (const issue of error.issues) {
1384
+ const fieldPath = this.getFieldPath(issue);
1385
+ const line = `${error.file}:${error.rowIndex + 1} • ${fieldPath}: ${issue.message}`;
1386
+ lines.push(line);
1387
+ }
1388
+ return lines.join("\n");
1389
+ }
1390
+ /**
1391
+ * Format verbose validation errors
1392
+ */
1393
+ formatValidationErrorsVerbose(errors) {
1394
+ const blocks = [];
1395
+ for (let i = 0; i < errors.length; i++) {
1396
+ const error = errors[i];
1397
+ const isLast = i === errors.length - 1;
1398
+ const prefix = isLast ? "└─" : "├─";
1399
+ const linePrefix = isLast ? " " : "│ ";
1400
+ const lines = [`${prefix} ${error.file}:${error.rowIndex + 1}`];
1401
+ for (const issue of error.issues) {
1402
+ const fieldPath = this.getFieldPath(issue);
1403
+ lines.push(`${linePrefix}Field: ${fieldPath}`);
1404
+ lines.push(`${linePrefix}Error: ${issue.message}`);
1405
+ }
1406
+ if (error.originalData !== void 0) lines.push(`${linePrefix}Original data: ${JSON.stringify(error.originalData)}`);
1407
+ if (error.data !== void 0) {
1408
+ const label = error.originalData !== void 0 ? "Transformed data" : "Data";
1409
+ lines.push(`${linePrefix}${label}: ${JSON.stringify(error.data)}`);
1410
+ }
1411
+ blocks.push(lines.join("\n"));
1412
+ }
1413
+ return blocks.join("\n│\n");
1414
+ }
1415
+ /**
1416
+ * Format compact foreign key error
1417
+ */
1418
+ formatForeignKeyErrorCompact(error) {
1419
+ return `${error.file}:${error.rowIndex + 1} • ${error.column}: Foreign key constraint failed - Referenced value ${JSON.stringify(error.value)} does not exist in ${error.referencedTable}(${error.referencedColumn})`;
1420
+ }
1421
+ /**
1422
+ * Format verbose foreign key error
1423
+ */
1424
+ formatForeignKeyErrorVerbose(error) {
1425
+ const lines = [
1426
+ `└─ ${error.file}:${error.rowIndex + 1}`,
1427
+ ` Type: Foreign Key Violation`,
1428
+ ` Field: ${error.column}`,
1429
+ ` Value: ${JSON.stringify(error.value)}`,
1430
+ ` References: ${error.referencedTable}(${error.referencedColumn})`,
1431
+ ` Error: Referenced value does not exist in target table`
1432
+ ];
1433
+ if (error.data !== void 0) lines.push(` Data: ${JSON.stringify(error.data)}`);
1434
+ return lines.join("\n");
1435
+ }
1436
+ /**
1437
+ * Get field path from issue
1438
+ */
1439
+ getFieldPath(issue) {
1440
+ if (!issue.path || issue.path.length === 0) return "root";
1441
+ return issue.path.map((segment) => {
1442
+ if (typeof segment === "object" && segment !== null && "key" in segment) return String(segment.key);
1443
+ return String(segment);
1444
+ }).join(".");
1445
+ }
1446
+ /**
1447
+ * Format error header with count
1448
+ */
1449
+ formatErrorHeader(count, file) {
1450
+ return styleText("red", `✗ Found ${count} error(s)${file ? ` in ${file}` : ""}`);
1451
+ }
1452
+ /**
1453
+ * Format migration failure header
1454
+ */
1455
+ formatMigrationFailureHeader() {
1456
+ return styleText("red", "\n✗ Migration failed and was rolled back");
1457
+ }
1458
+ };
1459
+
1460
+ //#endregion
1461
+ export { DirectoryScanner, ErrorFormatter, JsonlReader, JsonlWriter, LinesDB, RUNTIME, SCHEMA_EXTENSIONS, SchemaLoader, TypeGenerator, defineSchema, detectRuntime, ensureTableRowsValid, extractTableNameFromSchemaFile, findSchemaFile, hasBackward, isSchemaFile, rewriteExtensionForImport };
1308
1462
  //# sourceMappingURL=index.js.map