@toiroakr/lines-db 0.2.1 → 0.4.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
@@ -29,6 +29,7 @@ function createDatabase(path = ":memory:") {
29
29
  function createNodeDatabase(path) {
30
30
  const { DatabaseSync } = __require("node:sqlite");
31
31
  const db = new DatabaseSync(path);
32
+ db.exec("PRAGMA foreign_keys = ON");
32
33
  return {
33
34
  prepare(sql) {
34
35
  const stmt = db.prepare(sql);
@@ -124,7 +125,7 @@ var JsonlReader = class {
124
125
  };
125
126
  }
126
127
  static inferType(value) {
127
- if (value === null) return "NULL";
128
+ if (value === null || value === void 0) return "NULL";
128
129
  if (typeof value === "number") return Number.isInteger(value) ? "INTEGER" : "REAL";
129
130
  if (typeof value === "string") return "TEXT";
130
131
  if (typeof value === "boolean") return "INTEGER";
@@ -260,9 +261,9 @@ var DirectoryScanner = class {
260
261
  * const schema = defineSchema(
261
262
  * v.object({ id: v.number(), customerId: v.number() }),
262
263
  * {
263
- * primaryKey: ['id'],
264
+ * primaryKey: 'id',
264
265
  * foreignKeys: [
265
- * { columns: ['customerId'], references: { table: 'users', columns: ['id'] } }
266
+ * { column: 'customerId', references: { table: 'users', column: 'id' } }
266
267
  * ]
267
268
  * }
268
269
  * );
@@ -308,57 +309,78 @@ var LinesDB = class LinesDB {
308
309
  }
309
310
  /**
310
311
  * Initialize database by loading all JSONL files
312
+ * Uses dependency resolution to ensure foreign key references are loaded in correct order
311
313
  */
312
314
  async initialize() {
313
315
  this.tables = await DirectoryScanner.scanDirectory(this.config.dataDir);
314
- for (const [tableName, tableConfig] of this.tables) try {
315
- await this.loadTable(tableName, tableConfig);
316
+ const loadedTables = /* @__PURE__ */ new Set();
317
+ const loadingTables = /* @__PURE__ */ new Set();
318
+ for (const [tableName] of this.tables) if (!loadedTables.has(tableName)) try {
319
+ await this.loadTableWithDependencies(tableName, loadedTables, loadingTables);
316
320
  } catch (error) {
317
321
  console.warn(`Warning: Failed to load table '${tableName}':`, error instanceof Error ? error.message : String(error));
318
322
  this.tables.delete(tableName);
323
+ this.schemas.delete(tableName);
324
+ this.validationSchemas.delete(tableName);
325
+ }
326
+ }
327
+ /**
328
+ * Load a table and its dependencies recursively
329
+ */
330
+ async loadTableWithDependencies(tableName, loadedTables, loadingTables) {
331
+ if (loadedTables.has(tableName)) return;
332
+ if (loadingTables.has(tableName)) throw new Error(`Circular dependency detected for table '${tableName}'`);
333
+ const tableConfig = this.tables.get(tableName);
334
+ if (!tableConfig) throw new Error(`Table configuration not found for '${tableName}'`);
335
+ loadingTables.add(tableName);
336
+ try {
337
+ let foreignKeys;
338
+ try {
339
+ const { pathToFileURL: pathToFileURL$1 } = await import("node:url");
340
+ const schemaModule = await import(`${pathToFileURL$1(tableConfig.jsonlPath.replace(".jsonl", ".schema.ts")).href}?t=${Date.now()}`);
341
+ foreignKeys = (schemaModule.schema || schemaModule.default)?.foreignKeys || schemaModule.foreignKeys;
342
+ } catch {}
343
+ if (foreignKeys && foreignKeys.length > 0) for (const fk of foreignKeys) {
344
+ const referencedTable = fk.references.table;
345
+ if (!loadedTables.has(referencedTable)) if (this.tables.has(referencedTable)) await this.loadTableWithDependencies(referencedTable, loadedTables, loadingTables);
346
+ else throw new Error(`Foreign key reference to non-existent table '${referencedTable}' in table '${tableName}'`);
347
+ }
348
+ if (await this.loadTable(tableName, tableConfig)) loadedTables.add(tableName);
349
+ else this.tables.delete(tableName);
350
+ } finally {
351
+ loadingTables.delete(tableName);
319
352
  }
320
353
  }
321
354
  /**
322
355
  * Load a single table from JSONL file
356
+ * @returns true if table was loaded, false if skipped
323
357
  */
324
358
  async loadTable(tableName, config) {
325
359
  const data = await JsonlReader.read(config.jsonlPath);
326
- if (data.length === 0) {
327
- console.warn(`Warning: Table ${tableName} has no data`);
328
- return;
329
- }
330
360
  let validationSchema = config.validationSchema;
361
+ const schemaMetadata = {};
331
362
  if (!validationSchema) try {
332
363
  validationSchema = await SchemaLoader.loadSchema(config.jsonlPath);
333
- } catch (error) {
334
- console.log(`[LinesDB] No validation schema for table '${tableName}':`, error instanceof Error ? error.message : String(error));
335
- }
336
- console.log(`[LinesDB] Loaded validation schema for table '${tableName}':`, validationSchema ? "FOUND" : "NOT FOUND");
337
- if (validationSchema) {
338
- console.log(`[LinesDB] Schema type:`, typeof validationSchema);
339
- console.log(`[LinesDB] Schema has '~standard':`, "~standard" in validationSchema);
340
- }
364
+ } catch (_error) {}
365
+ if (!config.validationSchema) try {
366
+ const { pathToFileURL: pathToFileURL$1 } = await import("node:url");
367
+ const schemaModule = await import(`${pathToFileURL$1(config.jsonlPath.replace(".jsonl", ".schema.ts")).href}?t=${Date.now()}`);
368
+ const schemaExport = schemaModule.schema || schemaModule.default;
369
+ if (schemaExport?.primaryKey) schemaMetadata.primaryKey = schemaExport.primaryKey;
370
+ else if (schemaModule.primaryKey) schemaMetadata.primaryKey = schemaModule.primaryKey;
371
+ if (schemaExport?.foreignKeys) schemaMetadata.foreignKeys = schemaExport.foreignKeys;
372
+ else if (schemaModule.foreignKeys) schemaMetadata.foreignKeys = schemaModule.foreignKeys;
373
+ if (schemaExport?.indexes) schemaMetadata.indexes = schemaExport.indexes;
374
+ else if (schemaModule.indexes) schemaMetadata.indexes = schemaModule.indexes;
375
+ } catch (_error) {}
341
376
  this.validationSchemas.set(tableName, validationSchema);
342
- let schema;
343
- if (config.schema) schema = config.schema;
344
- else if (config.autoInferSchema !== false) schema = JsonlReader.inferSchema(tableName, data);
345
- else throw new Error(`No schema provided for table ${tableName} and autoInferSchema is disabled`);
346
- if (validationSchema) {
347
- const biSchema = validationSchema;
348
- if (biSchema.primaryKey && !schema.columns.some((col) => col.primaryKey)) for (const pkColumn of biSchema.primaryKey) {
349
- const col = schema.columns.find((c) => c.name === pkColumn);
350
- if (col) col.primaryKey = true;
351
- }
352
- if (biSchema.foreignKeys) schema.foreignKeys = biSchema.foreignKeys;
353
- if (biSchema.indexes) schema.indexes = biSchema.indexes;
354
- }
355
- this.schemas.set(tableName, schema);
356
- this.createTable(schema);
357
377
  const validationErrors = [];
378
+ const validatedData = [];
358
379
  for (let rowIndex = 0; rowIndex < data.length; rowIndex++) {
359
380
  const row = data[rowIndex];
360
381
  try {
361
- this.validateData(tableName, row);
382
+ const validatedRow = this.validateAndTransform(tableName, row);
383
+ validatedData.push(validatedRow);
362
384
  } catch (error) {
363
385
  if (error instanceof Error && error.name === "ValidationError") validationErrors.push({
364
386
  rowIndex,
@@ -375,13 +397,34 @@ var LinesDB = class LinesDB {
375
397
  enhancedError.issues = validationErrors[0].error.issues;
376
398
  throw enhancedError;
377
399
  }
378
- this.insertData(tableName, schema, data);
400
+ let schema;
401
+ if (config.schema) schema = config.schema;
402
+ else if (config.autoInferSchema !== false) {
403
+ if (validatedData.length === 0) return false;
404
+ schema = JsonlReader.inferSchema(tableName, validatedData);
405
+ } else throw new Error(`No schema provided for table ${tableName} and autoInferSchema is disabled`);
406
+ const biSchema = validationSchema;
407
+ const primaryKey = biSchema?.primaryKey || schemaMetadata.primaryKey;
408
+ const foreignKeys = biSchema?.foreignKeys || schemaMetadata.foreignKeys;
409
+ const indexes = biSchema?.indexes || schemaMetadata.indexes;
410
+ if (primaryKey && !schema.columns.some((col) => col.primaryKey)) {
411
+ const col = schema.columns.find((c) => c.name === primaryKey);
412
+ if (col) col.primaryKey = true;
413
+ } else if (!primaryKey && !schema.columns.some((col) => col.primaryKey)) {
414
+ const idColumn = schema.columns.find((c) => c.name === "id");
415
+ if (idColumn) idColumn.primaryKey = true;
416
+ }
417
+ if (foreignKeys) schema.foreignKeys = foreignKeys;
418
+ if (indexes) schema.indexes = indexes;
419
+ this.schemas.set(tableName, schema);
420
+ this.createTable(schema);
421
+ this.insertData(tableName, schema, validatedData);
422
+ return true;
379
423
  }
380
424
  /**
381
425
  * Create table in SQLite with constraints and indexes
382
426
  */
383
427
  createTable(schema) {
384
- this.db.exec("PRAGMA foreign_keys = ON");
385
428
  const quotedTableName = this.quoteTableName(schema.name);
386
429
  const columnDefs = schema.columns.map((col) => {
387
430
  const sqlType = col.type === "JSON" ? "TEXT" : col.type;
@@ -393,7 +436,7 @@ var LinesDB = class LinesDB {
393
436
  });
394
437
  const foreignKeyDefs = [];
395
438
  if (schema.foreignKeys && schema.foreignKeys.length > 0) for (const fk of schema.foreignKeys) {
396
- const fkParts = [`FOREIGN KEY (${fk.columns.map((col) => this.quoteIdentifier(col)).join(", ")})`, `REFERENCES ${this.quoteTableName(fk.references.table)}(${fk.references.columns.map((col) => this.quoteIdentifier(col)).join(", ")})`];
439
+ const fkParts = [`FOREIGN KEY (${this.quoteIdentifier(fk.column)})`, `REFERENCES ${this.quoteTableName(fk.references.table)}(${this.quoteIdentifier(fk.references.column)})`];
397
440
  if (fk.onDelete) fkParts.push(`ON DELETE ${fk.onDelete}`);
398
441
  if (fk.onUpdate) fkParts.push(`ON UPDATE ${fk.onUpdate}`);
399
442
  foreignKeyDefs.push(fkParts.join(" "));
@@ -508,17 +551,12 @@ var LinesDB = class LinesDB {
508
551
  return deserializedRow;
509
552
  }
510
553
  /**
511
- * Validate data using StandardSchema
554
+ * Validate data using StandardSchema and return the transformed value
512
555
  * Note: Only synchronous validation is supported
513
556
  */
514
- validateData(tableName, data) {
557
+ validateAndTransform(tableName, data) {
515
558
  const schema = this.validationSchemas.get(tableName);
516
- console.log(`[LinesDB] validateData called for table '${tableName}', schema exists:`, !!schema);
517
- if (!schema) {
518
- console.log(`[LinesDB] No validation schema found for table '${tableName}', skipping validation`);
519
- return;
520
- }
521
- console.log(`[LinesDB] Validating data:`, JSON.stringify(data));
559
+ if (!schema) return data;
522
560
  const result = schema["~standard"].validate(data);
523
561
  if (result instanceof Promise) throw new Error("Asynchronous validation is not supported. Please use synchronous validation schemas.");
524
562
  if (result.issues && result.issues.length > 0) {
@@ -535,6 +573,17 @@ var LinesDB = class LinesDB {
535
573
  error.issues = result.issues;
536
574
  throw error;
537
575
  }
576
+ const transformedValue = "value" in result ? result.value : data;
577
+ const normalizedValue = {};
578
+ for (const [key, value] of Object.entries(transformedValue)) normalizedValue[key] = value === void 0 ? null : value;
579
+ return normalizedValue;
580
+ }
581
+ /**
582
+ * Validate data using StandardSchema (without returning transformed value)
583
+ * Note: Only synchronous validation is supported
584
+ */
585
+ validateData(tableName, data) {
586
+ this.validateAndTransform(tableName, data);
538
587
  }
539
588
  /**
540
589
  * Insert a row into a table with validation
@@ -857,7 +906,7 @@ var LinesDB = class LinesDB {
857
906
  await this.sync();
858
907
  return result;
859
908
  } catch (error) {
860
- this.db.exec("ROLLBACK");
909
+ if (this.inTransaction) this.db.exec("ROLLBACK");
861
910
  this.inTransaction = false;
862
911
  throw error;
863
912
  }
@@ -1025,9 +1074,9 @@ var Validator = class {
1025
1074
  allErrors.push(...result.errors);
1026
1075
  allWarnings.push(...result.warnings);
1027
1076
  }
1028
- if (filesWithSchema.length > 0) {
1029
- const fkErrors = await this.validateForeignKeys(dirPath, filesWithSchema);
1030
- allErrors.push(...fkErrors);
1077
+ if (filesWithSchema.length > 0 && allErrors.length === 0) {
1078
+ const dbErrors = await this.validateWithDatabase(dirPath, filesWithSchema);
1079
+ allErrors.push(...dbErrors);
1031
1080
  }
1032
1081
  return {
1033
1082
  valid: allErrors.length === 0,
@@ -1036,53 +1085,50 @@ var Validator = class {
1036
1085
  };
1037
1086
  }
1038
1087
  /**
1039
- * Validate foreign key constraints across all tables
1088
+ * Validate by loading data into an actual database
1089
+ * This catches constraint violations (unique, primary key, foreign key, etc.)
1040
1090
  */
1041
- async validateForeignKeys(dirPath, jsonlFiles) {
1091
+ async validateWithDatabase(dirPath, jsonlFiles) {
1042
1092
  const errors = [];
1043
- const tableData = /* @__PURE__ */ new Map();
1044
- const tableSchemas = /* @__PURE__ */ new Map();
1045
- for (const file of jsonlFiles) {
1046
- const tableName = basename(file, ".jsonl");
1047
- const data = await JsonlReader.read(file);
1048
- const schema = await SchemaLoader.loadSchema(file);
1049
- tableData.set(tableName, data);
1050
- tableSchemas.set(tableName, schema);
1051
- }
1052
- for (const file of jsonlFiles) {
1053
- const tableName = basename(file, ".jsonl");
1054
- const schema = tableSchemas.get(tableName);
1055
- const data = tableData.get(tableName);
1056
- if (!schema || !data || !schema.foreignKeys) continue;
1057
- for (const fk of schema.foreignKeys) {
1058
- const referencedTable = fk.references.table;
1059
- const referencedData = tableData.get(referencedTable);
1060
- if (!referencedData) continue;
1061
- const referencedValues = /* @__PURE__ */ new Set();
1062
- for (const refRow of referencedData) {
1063
- const keyValues = fk.references.columns.map((col) => refRow[col]);
1064
- const compositeKey = JSON.stringify(keyValues);
1065
- referencedValues.add(compositeKey);
1066
- }
1067
- for (let i = 0; i < data.length; i++) {
1068
- const row = data[i];
1069
- const foreignKeyValues = fk.columns.map((col) => row[col]);
1070
- const compositeKey = JSON.stringify(foreignKeyValues);
1071
- if (!referencedValues.has(compositeKey)) errors.push({
1072
- file,
1073
- tableName,
1074
- rowIndex: i,
1075
- issues: [],
1076
- type: "foreignKey",
1077
- foreignKeyError: {
1078
- column: fk.columns.join(", "),
1079
- value: foreignKeyValues.length === 1 ? foreignKeyValues[0] : foreignKeyValues,
1080
- referencedTable,
1081
- referencedColumn: fk.references.columns.join(", ")
1082
- }
1083
- });
1084
- }
1093
+ const warnMessages = [];
1094
+ const originalWarn = console.warn;
1095
+ console.warn = (...args) => {
1096
+ const message = args.map((arg) => String(arg)).join(" ");
1097
+ warnMessages.push(message);
1098
+ originalWarn(...args);
1099
+ };
1100
+ try {
1101
+ const db = LinesDB.create({ dataDir: dirPath });
1102
+ await db.initialize();
1103
+ await db.close();
1104
+ for (const message of warnMessages) if (message.includes("Failed to load table")) {
1105
+ const tableNameMatch = message.match(/Failed to load table '([^']+)'/);
1106
+ const tableName = tableNameMatch ? tableNameMatch[1] : "unknown";
1107
+ const file = jsonlFiles.find((f) => basename(f, ".jsonl") === tableName);
1108
+ errors.push({
1109
+ file: file || `${dirPath}/${tableName}.jsonl`,
1110
+ tableName,
1111
+ rowIndex: 0,
1112
+ issues: [{
1113
+ message: message.replace(/^Warning:\s*/, ""),
1114
+ path: []
1115
+ }],
1116
+ type: "schema"
1117
+ });
1085
1118
  }
1119
+ } catch (error) {
1120
+ errors.push({
1121
+ file: dirPath,
1122
+ tableName: "database",
1123
+ rowIndex: 0,
1124
+ issues: [{
1125
+ message: `Database initialization failed: ${error instanceof Error ? error.message : String(error)}`,
1126
+ path: []
1127
+ }],
1128
+ type: "schema"
1129
+ });
1130
+ } finally {
1131
+ console.warn = originalWarn;
1086
1132
  }
1087
1133
  return errors;
1088
1134
  }
@@ -1106,6 +1152,11 @@ var Validator = class {
1106
1152
  type: "schema"
1107
1153
  });
1108
1154
  }
1155
+ if (errors.length === 0) {
1156
+ const dirPath = dirname(filePath);
1157
+ const dbErrors = await this.validateWithDatabase(dirPath, [filePath]);
1158
+ errors.push(...dbErrors);
1159
+ }
1109
1160
  return {
1110
1161
  valid: errors.length === 0,
1111
1162
  errors,