@toiroakr/lines-db 0.2.1 → 0.3.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/CHANGELOG.md +8 -0
- package/bin/cli.js +84 -43
- package/dist/index.cjs +83 -42
- package/dist/index.d.cts +12 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +83 -42
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +5 -2
- package/src/database.ts +200 -73
- package/src/jsonl-reader.ts +1 -1
- package/src/sqlite-adapter.ts +4 -0
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";
|
|
@@ -308,57 +309,73 @@ 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
|
-
|
|
315
|
-
|
|
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
|
+
foreignKeys = (await import(`${pathToFileURL$1(tableConfig.jsonlPath.replace(".jsonl", ".schema.ts")).href}?t=${Date.now()}`)).foreignKeys;
|
|
341
|
+
} catch {}
|
|
342
|
+
if (foreignKeys && foreignKeys.length > 0) for (const fk of foreignKeys) {
|
|
343
|
+
const referencedTable = fk.references.table;
|
|
344
|
+
if (!loadedTables.has(referencedTable)) if (this.tables.has(referencedTable)) await this.loadTableWithDependencies(referencedTable, loadedTables, loadingTables);
|
|
345
|
+
else throw new Error(`Foreign key reference to non-existent table '${referencedTable}' in table '${tableName}'`);
|
|
346
|
+
}
|
|
347
|
+
if (await this.loadTable(tableName, tableConfig)) loadedTables.add(tableName);
|
|
348
|
+
else this.tables.delete(tableName);
|
|
349
|
+
} finally {
|
|
350
|
+
loadingTables.delete(tableName);
|
|
319
351
|
}
|
|
320
352
|
}
|
|
321
353
|
/**
|
|
322
354
|
* Load a single table from JSONL file
|
|
355
|
+
* @returns true if table was loaded, false if skipped
|
|
323
356
|
*/
|
|
324
357
|
async loadTable(tableName, config) {
|
|
325
358
|
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
359
|
let validationSchema = config.validationSchema;
|
|
360
|
+
const schemaMetadata = {};
|
|
331
361
|
if (!validationSchema) try {
|
|
332
362
|
validationSchema = await SchemaLoader.loadSchema(config.jsonlPath);
|
|
333
|
-
} catch (
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
}
|
|
363
|
+
} catch (_error) {}
|
|
364
|
+
if (!config.validationSchema) try {
|
|
365
|
+
const { pathToFileURL: pathToFileURL$1 } = await import("node:url");
|
|
366
|
+
const schemaModule = await import(`${pathToFileURL$1(config.jsonlPath.replace(".jsonl", ".schema.ts")).href}?t=${Date.now()}`);
|
|
367
|
+
if (schemaModule.primaryKey) schemaMetadata.primaryKey = schemaModule.primaryKey;
|
|
368
|
+
if (schemaModule.foreignKeys) schemaMetadata.foreignKeys = schemaModule.foreignKeys;
|
|
369
|
+
if (schemaModule.indexes) schemaMetadata.indexes = schemaModule.indexes;
|
|
370
|
+
} catch (_error) {}
|
|
341
371
|
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
372
|
const validationErrors = [];
|
|
373
|
+
const validatedData = [];
|
|
358
374
|
for (let rowIndex = 0; rowIndex < data.length; rowIndex++) {
|
|
359
375
|
const row = data[rowIndex];
|
|
360
376
|
try {
|
|
361
|
-
this.
|
|
377
|
+
const validatedRow = this.validateAndTransform(tableName, row);
|
|
378
|
+
validatedData.push(validatedRow);
|
|
362
379
|
} catch (error) {
|
|
363
380
|
if (error instanceof Error && error.name === "ValidationError") validationErrors.push({
|
|
364
381
|
rowIndex,
|
|
@@ -375,13 +392,31 @@ var LinesDB = class LinesDB {
|
|
|
375
392
|
enhancedError.issues = validationErrors[0].error.issues;
|
|
376
393
|
throw enhancedError;
|
|
377
394
|
}
|
|
378
|
-
|
|
395
|
+
let schema;
|
|
396
|
+
if (config.schema) schema = config.schema;
|
|
397
|
+
else if (config.autoInferSchema !== false) {
|
|
398
|
+
if (validatedData.length === 0) return false;
|
|
399
|
+
schema = JsonlReader.inferSchema(tableName, validatedData);
|
|
400
|
+
} else throw new Error(`No schema provided for table ${tableName} and autoInferSchema is disabled`);
|
|
401
|
+
const biSchema = validationSchema;
|
|
402
|
+
const primaryKey = biSchema?.primaryKey || schemaMetadata.primaryKey;
|
|
403
|
+
const foreignKeys = biSchema?.foreignKeys || schemaMetadata.foreignKeys;
|
|
404
|
+
const indexes = biSchema?.indexes || schemaMetadata.indexes;
|
|
405
|
+
if (primaryKey && !schema.columns.some((col) => col.primaryKey)) for (const pkColumn of primaryKey) {
|
|
406
|
+
const col = schema.columns.find((c) => c.name === pkColumn);
|
|
407
|
+
if (col) col.primaryKey = true;
|
|
408
|
+
}
|
|
409
|
+
if (foreignKeys) schema.foreignKeys = foreignKeys;
|
|
410
|
+
if (indexes) schema.indexes = indexes;
|
|
411
|
+
this.schemas.set(tableName, schema);
|
|
412
|
+
this.createTable(schema);
|
|
413
|
+
this.insertData(tableName, schema, validatedData);
|
|
414
|
+
return true;
|
|
379
415
|
}
|
|
380
416
|
/**
|
|
381
417
|
* Create table in SQLite with constraints and indexes
|
|
382
418
|
*/
|
|
383
419
|
createTable(schema) {
|
|
384
|
-
this.db.exec("PRAGMA foreign_keys = ON");
|
|
385
420
|
const quotedTableName = this.quoteTableName(schema.name);
|
|
386
421
|
const columnDefs = schema.columns.map((col) => {
|
|
387
422
|
const sqlType = col.type === "JSON" ? "TEXT" : col.type;
|
|
@@ -508,17 +543,12 @@ var LinesDB = class LinesDB {
|
|
|
508
543
|
return deserializedRow;
|
|
509
544
|
}
|
|
510
545
|
/**
|
|
511
|
-
* Validate data using StandardSchema
|
|
546
|
+
* Validate data using StandardSchema and return the transformed value
|
|
512
547
|
* Note: Only synchronous validation is supported
|
|
513
548
|
*/
|
|
514
|
-
|
|
549
|
+
validateAndTransform(tableName, data) {
|
|
515
550
|
const schema = this.validationSchemas.get(tableName);
|
|
516
|
-
|
|
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));
|
|
551
|
+
if (!schema) return data;
|
|
522
552
|
const result = schema["~standard"].validate(data);
|
|
523
553
|
if (result instanceof Promise) throw new Error("Asynchronous validation is not supported. Please use synchronous validation schemas.");
|
|
524
554
|
if (result.issues && result.issues.length > 0) {
|
|
@@ -535,6 +565,17 @@ var LinesDB = class LinesDB {
|
|
|
535
565
|
error.issues = result.issues;
|
|
536
566
|
throw error;
|
|
537
567
|
}
|
|
568
|
+
const transformedValue = "value" in result ? result.value : data;
|
|
569
|
+
const normalizedValue = {};
|
|
570
|
+
for (const [key, value] of Object.entries(transformedValue)) normalizedValue[key] = value === void 0 ? null : value;
|
|
571
|
+
return normalizedValue;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Validate data using StandardSchema (without returning transformed value)
|
|
575
|
+
* Note: Only synchronous validation is supported
|
|
576
|
+
*/
|
|
577
|
+
validateData(tableName, data) {
|
|
578
|
+
this.validateAndTransform(tableName, data);
|
|
538
579
|
}
|
|
539
580
|
/**
|
|
540
581
|
* Insert a row into a table with validation
|
|
@@ -857,7 +898,7 @@ var LinesDB = class LinesDB {
|
|
|
857
898
|
await this.sync();
|
|
858
899
|
return result;
|
|
859
900
|
} catch (error) {
|
|
860
|
-
this.db.exec("ROLLBACK");
|
|
901
|
+
if (this.inTransaction) this.db.exec("ROLLBACK");
|
|
861
902
|
this.inTransaction = false;
|
|
862
903
|
throw error;
|
|
863
904
|
}
|