@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/src/database.ts
CHANGED
|
@@ -40,108 +40,172 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* Initialize database by loading all JSONL files
|
|
43
|
+
* Uses dependency resolution to ensure foreign key references are loaded in correct order
|
|
43
44
|
*/
|
|
44
45
|
async initialize(): Promise<void> {
|
|
45
46
|
// Scan directory for JSONL files
|
|
46
47
|
this.tables = await DirectoryScanner.scanDirectory(this.config.dataDir);
|
|
47
48
|
|
|
48
|
-
//
|
|
49
|
-
|
|
49
|
+
// Track loaded tables and tables currently being loaded (for circular dependency detection)
|
|
50
|
+
const loadedTables = new Set<string>();
|
|
51
|
+
const loadingTables = new Set<string>();
|
|
52
|
+
|
|
53
|
+
// Load all tables with dependency resolution
|
|
54
|
+
for (const [tableName] of this.tables) {
|
|
55
|
+
if (!loadedTables.has(tableName)) {
|
|
56
|
+
try {
|
|
57
|
+
await this.loadTableWithDependencies(tableName, loadedTables, loadingTables);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
// Log error but continue loading other tables
|
|
60
|
+
console.warn(
|
|
61
|
+
`Warning: Failed to load table '${tableName}':`,
|
|
62
|
+
error instanceof Error ? error.message : String(error),
|
|
63
|
+
);
|
|
64
|
+
// Remove the failed table from the tables map
|
|
65
|
+
this.tables.delete(tableName);
|
|
66
|
+
this.schemas.delete(tableName);
|
|
67
|
+
this.validationSchemas.delete(tableName);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Load a table and its dependencies recursively
|
|
75
|
+
*/
|
|
76
|
+
private async loadTableWithDependencies(
|
|
77
|
+
tableName: string,
|
|
78
|
+
loadedTables: Set<string>,
|
|
79
|
+
loadingTables: Set<string>,
|
|
80
|
+
): Promise<void> {
|
|
81
|
+
// Skip if already loaded
|
|
82
|
+
if (loadedTables.has(tableName)) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check for circular dependencies
|
|
87
|
+
if (loadingTables.has(tableName)) {
|
|
88
|
+
throw new Error(`Circular dependency detected for table '${tableName}'`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Get table config
|
|
92
|
+
const tableConfig = this.tables.get(tableName);
|
|
93
|
+
if (!tableConfig) {
|
|
94
|
+
throw new Error(`Table configuration not found for '${tableName}'`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Mark as currently loading
|
|
98
|
+
loadingTables.add(tableName);
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
// Load schema module to check for foreign key dependencies
|
|
102
|
+
// We need to load the entire module to access foreignKeys export
|
|
103
|
+
let foreignKeys: BiDirectionalSchema['foreignKeys'];
|
|
104
|
+
|
|
50
105
|
try {
|
|
51
|
-
await
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
106
|
+
const { pathToFileURL } = await import('node:url');
|
|
107
|
+
const schemaPath = tableConfig.jsonlPath.replace('.jsonl', '.schema.ts');
|
|
108
|
+
const schemaUrl = pathToFileURL(schemaPath).href;
|
|
109
|
+
const schemaModule = await import(`${schemaUrl}?t=${Date.now()}`);
|
|
110
|
+
foreignKeys = schemaModule.foreignKeys;
|
|
111
|
+
} catch {
|
|
112
|
+
// Schema file not found - will continue without validation
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// If there are foreign key dependencies, load them first
|
|
116
|
+
if (foreignKeys && foreignKeys.length > 0) {
|
|
117
|
+
for (const fk of foreignKeys) {
|
|
118
|
+
const referencedTable = fk.references.table;
|
|
119
|
+
if (!loadedTables.has(referencedTable)) {
|
|
120
|
+
// Check if referenced table exists in our tables map
|
|
121
|
+
if (this.tables.has(referencedTable)) {
|
|
122
|
+
await this.loadTableWithDependencies(referencedTable, loadedTables, loadingTables);
|
|
123
|
+
} else {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`Foreign key reference to non-existent table '${referencedTable}' in table '${tableName}'`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Now load this table
|
|
133
|
+
const wasLoaded = await this.loadTable(tableName, tableConfig);
|
|
134
|
+
if (wasLoaded) {
|
|
135
|
+
loadedTables.add(tableName);
|
|
136
|
+
} else {
|
|
137
|
+
// Table was not loaded (e.g., empty data)
|
|
59
138
|
this.tables.delete(tableName);
|
|
60
139
|
}
|
|
140
|
+
} finally {
|
|
141
|
+
// Remove from loading set
|
|
142
|
+
loadingTables.delete(tableName);
|
|
61
143
|
}
|
|
62
144
|
}
|
|
63
145
|
|
|
64
146
|
/**
|
|
65
147
|
* Load a single table from JSONL file
|
|
148
|
+
* @returns true if table was loaded, false if skipped
|
|
66
149
|
*/
|
|
67
|
-
private async loadTable(tableName: string, config: TableConfig): Promise<
|
|
150
|
+
private async loadTable(tableName: string, config: TableConfig): Promise<boolean> {
|
|
68
151
|
// Read JSONL file
|
|
69
152
|
const data = await JsonlReader.read(config.jsonlPath);
|
|
70
153
|
|
|
71
|
-
if (data.length === 0) {
|
|
72
|
-
console.warn(`Warning: Table ${tableName} has no data`);
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
154
|
// Load validation schema if provided or try to auto-load
|
|
77
155
|
let validationSchema = config.validationSchema;
|
|
156
|
+
const schemaMetadata: {
|
|
157
|
+
primaryKey?: readonly string[];
|
|
158
|
+
foreignKeys?: BiDirectionalSchema['foreignKeys'];
|
|
159
|
+
indexes?: BiDirectionalSchema['indexes'];
|
|
160
|
+
} = {};
|
|
161
|
+
|
|
78
162
|
if (!validationSchema) {
|
|
79
163
|
try {
|
|
80
164
|
validationSchema = await SchemaLoader.loadSchema(config.jsonlPath);
|
|
81
|
-
} catch (
|
|
165
|
+
} catch (_error) {
|
|
82
166
|
// Schema file not found or failed to load - this is OK, table can still be used without validation
|
|
83
|
-
console.log(
|
|
84
|
-
`[LinesDB] No validation schema for table '${tableName}':`,
|
|
85
|
-
error instanceof Error ? error.message : String(error),
|
|
86
|
-
);
|
|
87
167
|
}
|
|
88
168
|
}
|
|
89
|
-
console.log(
|
|
90
|
-
`[LinesDB] Loaded validation schema for table '${tableName}':`,
|
|
91
|
-
validationSchema ? 'FOUND' : 'NOT FOUND',
|
|
92
|
-
);
|
|
93
|
-
if (validationSchema) {
|
|
94
|
-
console.log(`[LinesDB] Schema type:`, typeof validationSchema);
|
|
95
|
-
console.log(`[LinesDB] Schema has '~standard':`, '~standard' in validationSchema);
|
|
96
|
-
}
|
|
97
|
-
this.validationSchemas.set(tableName, validationSchema);
|
|
98
169
|
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
if (config.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
170
|
+
// Load schema metadata (foreignKeys, primaryKey, indexes) from schema module
|
|
171
|
+
// SchemaLoader.loadSchema() only returns the validation schema object, not metadata
|
|
172
|
+
if (!config.validationSchema) {
|
|
173
|
+
// Only load if not already provided via config
|
|
174
|
+
try {
|
|
175
|
+
const { pathToFileURL } = await import('node:url');
|
|
176
|
+
const schemaPath = config.jsonlPath.replace('.jsonl', '.schema.ts');
|
|
177
|
+
const schemaUrl = pathToFileURL(schemaPath).href;
|
|
178
|
+
const schemaModule = await import(`${schemaUrl}?t=${Date.now()}`);
|
|
108
179
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const biSchema = validationSchema as BiDirectionalSchema;
|
|
112
|
-
if (biSchema.primaryKey && !schema.columns.some((col) => col.primaryKey)) {
|
|
113
|
-
// Add primary key constraint to columns
|
|
114
|
-
for (const pkColumn of biSchema.primaryKey) {
|
|
115
|
-
const col = schema.columns.find((c) => c.name === pkColumn);
|
|
116
|
-
if (col) {
|
|
117
|
-
col.primaryKey = true;
|
|
118
|
-
}
|
|
180
|
+
if (schemaModule.primaryKey) {
|
|
181
|
+
schemaMetadata.primaryKey = schemaModule.primaryKey;
|
|
119
182
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
183
|
+
if (schemaModule.foreignKeys) {
|
|
184
|
+
schemaMetadata.foreignKeys = schemaModule.foreignKeys;
|
|
185
|
+
}
|
|
186
|
+
if (schemaModule.indexes) {
|
|
187
|
+
schemaMetadata.indexes = schemaModule.indexes;
|
|
188
|
+
}
|
|
189
|
+
} catch (_error) {
|
|
190
|
+
// Schema file not found - this is OK
|
|
126
191
|
}
|
|
127
192
|
}
|
|
128
193
|
|
|
129
|
-
this.
|
|
130
|
-
|
|
131
|
-
// Create table
|
|
132
|
-
this.createTable(schema);
|
|
194
|
+
this.validationSchemas.set(tableName, validationSchema);
|
|
133
195
|
|
|
134
|
-
// Validate data
|
|
196
|
+
// Validate data first and collect validated (transformed) data
|
|
135
197
|
const validationErrors: Array<{
|
|
136
198
|
rowIndex: number;
|
|
137
199
|
rowData: JsonObject;
|
|
138
200
|
error: ValidationError;
|
|
139
201
|
}> = [];
|
|
202
|
+
const validatedData: JsonObject[] = [];
|
|
140
203
|
|
|
141
204
|
for (let rowIndex = 0; rowIndex < data.length; rowIndex++) {
|
|
142
205
|
const row = data[rowIndex];
|
|
143
206
|
try {
|
|
144
|
-
this.
|
|
207
|
+
const validatedRow = this.validateAndTransform(tableName, row);
|
|
208
|
+
validatedData.push(validatedRow);
|
|
145
209
|
} catch (error) {
|
|
146
210
|
if (error instanceof Error && error.name === 'ValidationError') {
|
|
147
211
|
validationErrors.push({
|
|
@@ -167,15 +231,60 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
167
231
|
throw enhancedError;
|
|
168
232
|
}
|
|
169
233
|
|
|
170
|
-
|
|
234
|
+
// Determine schema - infer from validated data if auto-inference is enabled
|
|
235
|
+
let schema: TableSchema;
|
|
236
|
+
if (config.schema) {
|
|
237
|
+
schema = config.schema;
|
|
238
|
+
} else if (config.autoInferSchema !== false) {
|
|
239
|
+
if (validatedData.length === 0) {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
// Infer schema from validated data (which may have additional fields added by validation)
|
|
243
|
+
schema = JsonlReader.inferSchema(tableName, validatedData);
|
|
244
|
+
} else {
|
|
245
|
+
throw new Error(`No schema provided for table ${tableName} and autoInferSchema is disabled`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Enhance schema with constraints from validation schema and schema metadata
|
|
249
|
+
// Priority: config.validationSchema (as BiDirectionalSchema) > schemaMetadata
|
|
250
|
+
const biSchema = validationSchema as BiDirectionalSchema;
|
|
251
|
+
const primaryKey = biSchema?.primaryKey || schemaMetadata.primaryKey;
|
|
252
|
+
const foreignKeys = biSchema?.foreignKeys || schemaMetadata.foreignKeys;
|
|
253
|
+
const indexes = biSchema?.indexes || schemaMetadata.indexes;
|
|
254
|
+
|
|
255
|
+
if (primaryKey && !schema.columns.some((col) => col.primaryKey)) {
|
|
256
|
+
// Add primary key constraint to columns
|
|
257
|
+
for (const pkColumn of primaryKey) {
|
|
258
|
+
const col = schema.columns.find((c) => c.name === pkColumn);
|
|
259
|
+
if (col) {
|
|
260
|
+
col.primaryKey = true;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (foreignKeys) {
|
|
265
|
+
schema.foreignKeys = foreignKeys;
|
|
266
|
+
}
|
|
267
|
+
if (indexes) {
|
|
268
|
+
schema.indexes = indexes;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
this.schemas.set(tableName, schema);
|
|
272
|
+
|
|
273
|
+
// Create table
|
|
274
|
+
this.createTable(schema);
|
|
275
|
+
|
|
276
|
+
// Insert validated data
|
|
277
|
+
this.insertData(tableName, schema, validatedData);
|
|
278
|
+
|
|
279
|
+
return true;
|
|
171
280
|
}
|
|
172
281
|
|
|
173
282
|
/**
|
|
174
283
|
* Create table in SQLite with constraints and indexes
|
|
175
284
|
*/
|
|
176
285
|
private createTable(schema: TableSchema): void {
|
|
177
|
-
//
|
|
178
|
-
|
|
286
|
+
// Note: Foreign key constraints are enabled at database connection level (see sqlite-adapter.ts)
|
|
287
|
+
// No need to enable them here for each table
|
|
179
288
|
|
|
180
289
|
// Quote table name to handle special characters
|
|
181
290
|
const quotedTableName = this.quoteTableName(schema.name);
|
|
@@ -402,19 +511,14 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
402
511
|
}
|
|
403
512
|
|
|
404
513
|
/**
|
|
405
|
-
* Validate data using StandardSchema
|
|
514
|
+
* Validate data using StandardSchema and return the transformed value
|
|
406
515
|
* Note: Only synchronous validation is supported
|
|
407
516
|
*/
|
|
408
|
-
private
|
|
517
|
+
private validateAndTransform(tableName: string, data: unknown): JsonObject {
|
|
409
518
|
const schema = this.validationSchemas.get(tableName);
|
|
410
|
-
console.log(`[LinesDB] validateData called for table '${tableName}', schema exists:`, !!schema);
|
|
411
519
|
if (!schema) {
|
|
412
|
-
|
|
413
|
-
`[LinesDB] No validation schema found for table '${tableName}', skipping validation`,
|
|
414
|
-
);
|
|
415
|
-
return;
|
|
520
|
+
return data as JsonObject;
|
|
416
521
|
}
|
|
417
|
-
console.log(`[LinesDB] Validating data:`, JSON.stringify(data));
|
|
418
522
|
|
|
419
523
|
const result = schema['~standard'].validate(data);
|
|
420
524
|
|
|
@@ -452,6 +556,27 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
452
556
|
error.issues = result.issues;
|
|
453
557
|
throw error;
|
|
454
558
|
}
|
|
559
|
+
|
|
560
|
+
// Return the transformed value from validation
|
|
561
|
+
// When there are no issues, result.value should be present
|
|
562
|
+
const transformedValue = ('value' in result ? result.value : data) as JsonObject;
|
|
563
|
+
|
|
564
|
+
// Convert undefined values to null for JSON compatibility
|
|
565
|
+
const normalizedValue: JsonObject = {};
|
|
566
|
+
for (const [key, value] of Object.entries(transformedValue)) {
|
|
567
|
+
normalizedValue[key] = value === undefined ? null : value;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return normalizedValue;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Validate data using StandardSchema (without returning transformed value)
|
|
575
|
+
* Note: Only synchronous validation is supported
|
|
576
|
+
*/
|
|
577
|
+
private validateData(tableName: string, data: unknown): void {
|
|
578
|
+
// Use validateAndTransform but discard the result
|
|
579
|
+
this.validateAndTransform(tableName, data);
|
|
455
580
|
}
|
|
456
581
|
|
|
457
582
|
/**
|
|
@@ -999,7 +1124,9 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
999
1124
|
|
|
1000
1125
|
return result;
|
|
1001
1126
|
} catch (error) {
|
|
1002
|
-
this.
|
|
1127
|
+
if (this.inTransaction) {
|
|
1128
|
+
this.db.exec('ROLLBACK');
|
|
1129
|
+
}
|
|
1003
1130
|
this.inTransaction = false;
|
|
1004
1131
|
throw error;
|
|
1005
1132
|
}
|
package/src/jsonl-reader.ts
CHANGED
|
@@ -123,7 +123,7 @@ export class JsonlReader {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
private static inferType(value: unknown): string {
|
|
126
|
-
if (value === null) return 'NULL';
|
|
126
|
+
if (value === null || value === undefined) return 'NULL';
|
|
127
127
|
if (typeof value === 'number') {
|
|
128
128
|
return Number.isInteger(value) ? 'INTEGER' : 'REAL';
|
|
129
129
|
}
|
package/src/sqlite-adapter.ts
CHANGED
|
@@ -39,6 +39,10 @@ function createNodeDatabase(path: string): SQLiteDatabase {
|
|
|
39
39
|
const { DatabaseSync } = require('node:sqlite');
|
|
40
40
|
const db = new DatabaseSync(path);
|
|
41
41
|
|
|
42
|
+
// CRITICAL: Enable foreign key constraints
|
|
43
|
+
// SQLite disables foreign keys by default, which is a major data integrity issue
|
|
44
|
+
db.exec('PRAGMA foreign_keys = ON');
|
|
45
|
+
|
|
42
46
|
return {
|
|
43
47
|
prepare(sql: string): SQLiteStatement {
|
|
44
48
|
const stmt = db.prepare(sql);
|