@toiroakr/lines-db 0.5.0 → 0.6.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/CHANGELOG.md +24 -0
- package/bin/cli.js +400 -418
- package/dist/index.cjs +217 -330
- package/dist/index.d.cts +64 -84
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +64 -84
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +219 -331
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/cli.ts +226 -126
- package/src/database.ts +342 -53
- package/src/index.ts +2 -2
- package/src/jsonl-migration.ts +24 -56
- package/src/schema.ts +37 -32
- package/src/types.ts +21 -0
- package/src/validator.test.ts +0 -507
- package/src/validator.ts +0 -441
package/src/database.ts
CHANGED
|
@@ -10,11 +10,13 @@ import type {
|
|
|
10
10
|
JsonObject,
|
|
11
11
|
TableConfig,
|
|
12
12
|
StandardSchema,
|
|
13
|
-
StandardSchemaIssue,
|
|
14
13
|
ValidationError,
|
|
15
14
|
Table,
|
|
16
15
|
TableDefs,
|
|
17
16
|
WhereCondition,
|
|
17
|
+
ValidationResult,
|
|
18
|
+
ValidationErrorDetail,
|
|
19
|
+
ForeignKeyDefinition,
|
|
18
20
|
} from './types.js';
|
|
19
21
|
import type { BiDirectionalSchema } from './schema.js';
|
|
20
22
|
|
|
@@ -39,35 +41,68 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
/**
|
|
42
|
-
* Initialize database by loading all JSONL files
|
|
44
|
+
* Initialize database by loading all JSONL files or a specific table
|
|
43
45
|
* Uses dependency resolution to ensure foreign key references are loaded in correct order
|
|
46
|
+
* @param options Optional configuration for initialization
|
|
47
|
+
* @param options.tableName Optional table name to initialize. If not provided, initializes all tables
|
|
48
|
+
* @param options.detailedValidate If true, performs detailed validation by inserting rows one by one to catch constraint violations
|
|
49
|
+
* @param options.transform Optional transform function to apply to rows before validation (only applied to the specified tableName)
|
|
50
|
+
* @returns ValidationResult containing validation status, errors, and warnings
|
|
44
51
|
*/
|
|
45
|
-
async initialize(
|
|
52
|
+
async initialize(options?: {
|
|
53
|
+
tableName?: string;
|
|
54
|
+
detailedValidate?: boolean;
|
|
55
|
+
transform?: (row: JsonObject) => JsonObject;
|
|
56
|
+
}): Promise<ValidationResult> {
|
|
57
|
+
const allErrors: ValidationErrorDetail[] = [];
|
|
58
|
+
const allWarnings: string[] = [];
|
|
59
|
+
const tableName = options?.tableName;
|
|
60
|
+
const detailedValidate = options?.detailedValidate ?? false;
|
|
61
|
+
const transform = options?.transform;
|
|
62
|
+
|
|
46
63
|
// Scan directory for JSONL files
|
|
47
64
|
this.tables = await DirectoryScanner.scanDirectory(this.config.dataDir);
|
|
48
65
|
|
|
66
|
+
// Determine which tables to load
|
|
67
|
+
const tablesToLoad = tableName ? [tableName] : Array.from(this.tables.keys());
|
|
68
|
+
|
|
69
|
+
// Validate that all requested tables exist BEFORE starting to load
|
|
70
|
+
for (const tableNameToLoad of tablesToLoad) {
|
|
71
|
+
if (!this.tables.has(tableNameToLoad)) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Table '${tableNameToLoad}' not found in directory '${this.config.dataDir}'`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
49
78
|
// Track loaded tables and tables currently being loaded (for circular dependency detection)
|
|
50
79
|
const loadedTables = new Set<string>();
|
|
51
80
|
const loadingTables = new Set<string>();
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
81
|
+
const attemptedTables = new Set<string>(); // Track all attempted tables (loaded or not)
|
|
82
|
+
|
|
83
|
+
// Load tables with dependency resolution
|
|
84
|
+
for (const tableNameToLoad of tablesToLoad) {
|
|
85
|
+
if (!attemptedTables.has(tableNameToLoad)) {
|
|
86
|
+
// Only apply transform to the specified table
|
|
87
|
+
const tableTransform = tableNameToLoad === tableName ? transform : undefined;
|
|
88
|
+
const { errors, warnings } = await this.loadTableWithDependencies(
|
|
89
|
+
tableNameToLoad,
|
|
90
|
+
loadedTables,
|
|
91
|
+
loadingTables,
|
|
92
|
+
attemptedTables,
|
|
93
|
+
detailedValidate,
|
|
94
|
+
tableTransform,
|
|
95
|
+
);
|
|
96
|
+
allErrors.push(...errors);
|
|
97
|
+
allWarnings.push(...warnings);
|
|
69
98
|
}
|
|
70
99
|
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
valid: allErrors.length === 0,
|
|
103
|
+
errors: allErrors,
|
|
104
|
+
warnings: allWarnings,
|
|
105
|
+
};
|
|
71
106
|
}
|
|
72
107
|
|
|
73
108
|
/**
|
|
@@ -77,12 +112,21 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
77
112
|
tableName: string,
|
|
78
113
|
loadedTables: Set<string>,
|
|
79
114
|
loadingTables: Set<string>,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
115
|
+
attemptedTables: Set<string>,
|
|
116
|
+
detailedValidate: boolean,
|
|
117
|
+
transform?: (row: JsonObject) => JsonObject,
|
|
118
|
+
): Promise<{ errors: ValidationErrorDetail[]; warnings: string[] }> {
|
|
119
|
+
const errors: ValidationErrorDetail[] = [];
|
|
120
|
+
const warnings: string[] = [];
|
|
121
|
+
|
|
122
|
+
// Skip if already attempted (loaded or not)
|
|
123
|
+
if (attemptedTables.has(tableName)) {
|
|
124
|
+
return { errors, warnings };
|
|
84
125
|
}
|
|
85
126
|
|
|
127
|
+
// Mark as attempted
|
|
128
|
+
attemptedTables.add(tableName);
|
|
129
|
+
|
|
86
130
|
// Check for circular dependencies
|
|
87
131
|
if (loadingTables.has(tableName)) {
|
|
88
132
|
throw new Error(`Circular dependency detected for table '${tableName}'`);
|
|
@@ -119,10 +163,26 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
119
163
|
if (foreignKeys && foreignKeys.length > 0) {
|
|
120
164
|
for (const fk of foreignKeys) {
|
|
121
165
|
const referencedTable = fk.references.table;
|
|
122
|
-
|
|
166
|
+
|
|
167
|
+
// Skip self-referencing foreign keys (e.g., nullable parent_id columns)
|
|
168
|
+
if (referencedTable === tableName) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!attemptedTables.has(referencedTable)) {
|
|
123
173
|
// Check if referenced table exists in our tables map
|
|
124
174
|
if (this.tables.has(referencedTable)) {
|
|
125
|
-
|
|
175
|
+
// Dependencies should not have transform applied
|
|
176
|
+
const depResult = await this.loadTableWithDependencies(
|
|
177
|
+
referencedTable,
|
|
178
|
+
loadedTables,
|
|
179
|
+
loadingTables,
|
|
180
|
+
attemptedTables,
|
|
181
|
+
detailedValidate,
|
|
182
|
+
undefined,
|
|
183
|
+
);
|
|
184
|
+
errors.push(...depResult.errors);
|
|
185
|
+
warnings.push(...depResult.warnings);
|
|
126
186
|
} else {
|
|
127
187
|
throw new Error(
|
|
128
188
|
`Foreign key reference to non-existent table '${referencedTable}' in table '${tableName}'`,
|
|
@@ -133,26 +193,46 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
133
193
|
}
|
|
134
194
|
|
|
135
195
|
// Now load this table
|
|
136
|
-
const
|
|
137
|
-
|
|
196
|
+
const { loaded, errors: loadErrors } = await this.loadTable(
|
|
197
|
+
tableName,
|
|
198
|
+
tableConfig,
|
|
199
|
+
detailedValidate,
|
|
200
|
+
transform,
|
|
201
|
+
);
|
|
202
|
+
errors.push(...loadErrors);
|
|
203
|
+
|
|
204
|
+
if (loaded) {
|
|
138
205
|
loadedTables.add(tableName);
|
|
139
206
|
} else {
|
|
140
207
|
// Table was not loaded (e.g., empty data)
|
|
208
|
+
warnings.push(`Table '${tableName}' was not loaded (no data or skipped)`);
|
|
141
209
|
this.tables.delete(tableName);
|
|
142
210
|
}
|
|
143
211
|
} finally {
|
|
144
212
|
// Remove from loading set
|
|
145
213
|
loadingTables.delete(tableName);
|
|
146
214
|
}
|
|
215
|
+
|
|
216
|
+
return { errors, warnings };
|
|
147
217
|
}
|
|
148
218
|
|
|
149
219
|
/**
|
|
150
220
|
* Load a single table from JSONL file
|
|
151
|
-
* @returns
|
|
221
|
+
* @returns Object with loaded status and validation errors
|
|
152
222
|
*/
|
|
153
|
-
private async loadTable(
|
|
223
|
+
private async loadTable(
|
|
224
|
+
tableName: string,
|
|
225
|
+
config: TableConfig,
|
|
226
|
+
detailedValidate: boolean,
|
|
227
|
+
transform?: (row: JsonObject) => JsonObject,
|
|
228
|
+
): Promise<{ loaded: boolean; errors: ValidationErrorDetail[] }> {
|
|
154
229
|
// Read JSONL file
|
|
155
|
-
|
|
230
|
+
let data = await JsonlReader.read(config.jsonlPath);
|
|
231
|
+
|
|
232
|
+
// Apply transform if provided (before validation)
|
|
233
|
+
if (transform) {
|
|
234
|
+
data = data.map((row) => transform(row));
|
|
235
|
+
}
|
|
156
236
|
|
|
157
237
|
// Load validation schema if provided or try to auto-load
|
|
158
238
|
let validationSchema = config.validationSchema;
|
|
@@ -200,8 +280,23 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
200
280
|
} else if (schemaModule.indexes) {
|
|
201
281
|
schemaMetadata.indexes = schemaModule.indexes;
|
|
202
282
|
}
|
|
283
|
+
|
|
284
|
+
// Debug: log loaded metadata
|
|
285
|
+
if (process.env.DEBUG_LINES_DB) {
|
|
286
|
+
console.log(`[lines-db] Schema metadata for ${tableName}:`);
|
|
287
|
+
console.log(` primaryKey: ${schemaMetadata.primaryKey}`);
|
|
288
|
+
console.log(` foreignKeys: ${JSON.stringify(schemaMetadata.foreignKeys)}`);
|
|
289
|
+
console.log(` indexes: ${JSON.stringify(schemaMetadata.indexes)}`);
|
|
290
|
+
}
|
|
203
291
|
} catch (_error) {
|
|
204
292
|
// Schema file not found - this is OK
|
|
293
|
+
// Debug: log error for investigation
|
|
294
|
+
if (process.env.DEBUG_LINES_DB) {
|
|
295
|
+
console.warn(
|
|
296
|
+
`[lines-db] Failed to load schema metadata for ${tableName}:`,
|
|
297
|
+
_error instanceof Error ? _error.message : String(_error),
|
|
298
|
+
);
|
|
299
|
+
}
|
|
205
300
|
}
|
|
206
301
|
}
|
|
207
302
|
|
|
@@ -233,29 +328,48 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
233
328
|
}
|
|
234
329
|
}
|
|
235
330
|
|
|
331
|
+
// Convert validation errors to ValidationErrorDetail format
|
|
332
|
+
const validationErrorDetails: ValidationErrorDetail[] = validationErrors.map((ve) => ({
|
|
333
|
+
file: config.jsonlPath,
|
|
334
|
+
tableName,
|
|
335
|
+
rowIndex: ve.rowIndex,
|
|
336
|
+
issues: ve.error.issues,
|
|
337
|
+
type: 'schema' as const,
|
|
338
|
+
}));
|
|
339
|
+
|
|
236
340
|
if (validationErrors.length > 0) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
);
|
|
240
|
-
enhancedError.name = 'ValidationError';
|
|
241
|
-
(enhancedError as unknown as { validationErrors: typeof validationErrors }).validationErrors =
|
|
242
|
-
validationErrors;
|
|
243
|
-
(enhancedError as unknown as { issues: ReadonlyArray<StandardSchemaIssue> }).issues =
|
|
244
|
-
validationErrors[0].error.issues;
|
|
245
|
-
throw enhancedError;
|
|
341
|
+
// Return errors instead of throwing
|
|
342
|
+
return { loaded: false, errors: validationErrorDetails };
|
|
246
343
|
}
|
|
247
344
|
|
|
248
345
|
// Determine schema - infer from validated data if auto-inference is enabled
|
|
249
346
|
let schema: TableSchema;
|
|
347
|
+
let inferredSchema: TableSchema | undefined;
|
|
348
|
+
|
|
349
|
+
// Always infer schema from validated data to capture valueType information (e.g., boolean)
|
|
350
|
+
if (validatedData.length > 0) {
|
|
351
|
+
inferredSchema = JsonlReader.inferSchema(tableName, validatedData);
|
|
352
|
+
}
|
|
353
|
+
|
|
250
354
|
if (config.schema) {
|
|
251
355
|
schema = config.schema;
|
|
356
|
+
// Merge valueType information from inferred schema
|
|
357
|
+
if (inferredSchema) {
|
|
358
|
+
for (const inferredCol of inferredSchema.columns) {
|
|
359
|
+
const schemaCol = schema.columns.find((c) => c.name === inferredCol.name);
|
|
360
|
+
if (schemaCol && inferredCol.valueType && !schemaCol.valueType) {
|
|
361
|
+
schemaCol.valueType = inferredCol.valueType;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
252
365
|
} else if (config.autoInferSchema !== false) {
|
|
253
366
|
if (validatedData.length === 0) {
|
|
254
|
-
return false;
|
|
367
|
+
return { loaded: false, errors: [] };
|
|
255
368
|
}
|
|
256
|
-
//
|
|
257
|
-
schema =
|
|
369
|
+
// Use inferred schema
|
|
370
|
+
schema = inferredSchema!;
|
|
258
371
|
} else {
|
|
372
|
+
// Critical error - throw exception
|
|
259
373
|
throw new Error(`No schema provided for table ${tableName} and autoInferSchema is disabled`);
|
|
260
374
|
}
|
|
261
375
|
|
|
@@ -285,6 +399,18 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
285
399
|
}
|
|
286
400
|
if (indexes) {
|
|
287
401
|
schema.indexes = indexes;
|
|
402
|
+
|
|
403
|
+
// Apply unique constraint from single-column unique indexes to column definitions
|
|
404
|
+
// This is required for foreign key references, as SQLite requires the referenced column
|
|
405
|
+
// to have a UNIQUE constraint in the table definition (not just an index)
|
|
406
|
+
for (const index of indexes) {
|
|
407
|
+
if (index.unique && index.columns.length === 1) {
|
|
408
|
+
const col = schema.columns.find((c) => c.name === index.columns[0]);
|
|
409
|
+
if (col && !col.unique && !col.primaryKey) {
|
|
410
|
+
col.unique = true;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
288
414
|
}
|
|
289
415
|
|
|
290
416
|
this.schemas.set(tableName, schema);
|
|
@@ -292,10 +418,22 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
292
418
|
// Create table
|
|
293
419
|
this.createTable(schema);
|
|
294
420
|
|
|
295
|
-
// Insert validated data
|
|
296
|
-
|
|
421
|
+
// Insert validated data (with detailed validation if requested)
|
|
422
|
+
if (detailedValidate) {
|
|
423
|
+
const insertErrors = this.insertDataWithDetailedValidation(
|
|
424
|
+
tableName,
|
|
425
|
+
schema,
|
|
426
|
+
validatedData,
|
|
427
|
+
config.jsonlPath,
|
|
428
|
+
);
|
|
429
|
+
if (insertErrors.length > 0) {
|
|
430
|
+
return { loaded: false, errors: insertErrors };
|
|
431
|
+
}
|
|
432
|
+
} else {
|
|
433
|
+
this.insertData(tableName, schema, validatedData);
|
|
434
|
+
}
|
|
297
435
|
|
|
298
|
-
return true;
|
|
436
|
+
return { loaded: true, errors: [] };
|
|
299
437
|
}
|
|
300
438
|
|
|
301
439
|
/**
|
|
@@ -308,13 +446,31 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
308
446
|
// Quote table name to handle special characters
|
|
309
447
|
const quotedTableName = this.quoteTableName(schema.name);
|
|
310
448
|
|
|
449
|
+
// Build a set of columns that should have UNIQUE constraint
|
|
450
|
+
// This includes columns marked as unique in schema AND single-column unique indexes
|
|
451
|
+
// The latter is required for foreign key references, as SQLite requires the referenced column
|
|
452
|
+
// to have a UNIQUE constraint in the table definition (not just a separately created index)
|
|
453
|
+
const uniqueColumns = new Set<string>();
|
|
454
|
+
for (const col of schema.columns) {
|
|
455
|
+
if (col.unique) {
|
|
456
|
+
uniqueColumns.add(col.name);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (schema.indexes) {
|
|
460
|
+
for (const index of schema.indexes) {
|
|
461
|
+
if (index.unique && index.columns.length === 1) {
|
|
462
|
+
uniqueColumns.add(index.columns[0]);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
311
467
|
const columnDefs = schema.columns.map((col) => {
|
|
312
468
|
// JSON type is stored as TEXT in SQLite
|
|
313
469
|
const sqlType = col.type === 'JSON' ? 'TEXT' : col.type;
|
|
314
470
|
const parts = [this.quoteIdentifier(col.name), sqlType];
|
|
315
471
|
if (col.primaryKey) parts.push('PRIMARY KEY');
|
|
316
472
|
if (col.notNull) parts.push('NOT NULL');
|
|
317
|
-
if (col.
|
|
473
|
+
if (uniqueColumns.has(col.name) && !col.primaryKey) parts.push('UNIQUE');
|
|
318
474
|
return parts.join(' ');
|
|
319
475
|
});
|
|
320
476
|
|
|
@@ -372,9 +528,52 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
372
528
|
}
|
|
373
529
|
|
|
374
530
|
/**
|
|
375
|
-
* Insert data into table
|
|
531
|
+
* Insert data into table using batch insert (multiple rows per SQL)
|
|
532
|
+
* SQLite has a parameter limit (default 999), so we batch rows accordingly
|
|
533
|
+
* Throws exception if any constraint violation occurs
|
|
376
534
|
*/
|
|
377
535
|
private insertData(tableName: string, schema: TableSchema, data: JsonObject[]): void {
|
|
536
|
+
if (data.length === 0) return;
|
|
537
|
+
|
|
538
|
+
const columnNames = schema.columns.map((col) => col.name);
|
|
539
|
+
const quotedColumns = columnNames.map((name) => this.quoteIdentifier(name));
|
|
540
|
+
const columnCount = columnNames.length;
|
|
541
|
+
|
|
542
|
+
// Calculate batch size to stay under SQLite's parameter limit (999)
|
|
543
|
+
// Leave some margin for safety
|
|
544
|
+
const maxBatchSize = Math.floor(900 / columnCount);
|
|
545
|
+
const batchSize = Math.max(1, Math.min(maxBatchSize, 100));
|
|
546
|
+
|
|
547
|
+
// Process data in batches
|
|
548
|
+
for (let i = 0; i < data.length; i += batchSize) {
|
|
549
|
+
const batch = data.slice(i, i + batchSize);
|
|
550
|
+
const rowPlaceholders = columnNames.map(() => '?').join(', ');
|
|
551
|
+
const valuesPlaceholders = batch.map(() => `(${rowPlaceholders})`).join(', ');
|
|
552
|
+
const sql = `INSERT INTO ${this.quoteTableName(tableName)} (${quotedColumns.join(', ')}) VALUES ${valuesPlaceholders}`;
|
|
553
|
+
|
|
554
|
+
const values: (string | number | bigint | null | Uint8Array)[] = [];
|
|
555
|
+
for (const row of batch) {
|
|
556
|
+
for (const col of columnNames) {
|
|
557
|
+
values.push(this.normalizeValue(row[col]));
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const stmt = this.db.prepare(sql);
|
|
562
|
+
stmt.run(...values);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Insert data into table one row at a time with detailed error reporting
|
|
568
|
+
* This is used for validation to catch constraint violations
|
|
569
|
+
*/
|
|
570
|
+
private insertDataWithDetailedValidation(
|
|
571
|
+
tableName: string,
|
|
572
|
+
schema: TableSchema,
|
|
573
|
+
data: JsonObject[],
|
|
574
|
+
filePath: string,
|
|
575
|
+
): ValidationErrorDetail[] {
|
|
576
|
+
const errors: ValidationErrorDetail[] = [];
|
|
378
577
|
const columnNames = schema.columns.map((col) => col.name);
|
|
379
578
|
const quotedColumns = columnNames.map((name) => this.quoteIdentifier(name));
|
|
380
579
|
const placeholders = columnNames.map(() => '?').join(', ');
|
|
@@ -382,10 +581,90 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
382
581
|
|
|
383
582
|
const stmt = this.db.prepare(sql);
|
|
384
583
|
|
|
385
|
-
for (
|
|
386
|
-
const
|
|
387
|
-
|
|
584
|
+
for (let rowIndex = 0; rowIndex < data.length; rowIndex++) {
|
|
585
|
+
const row = data[rowIndex];
|
|
586
|
+
try {
|
|
587
|
+
const values = columnNames.map((col) => this.normalizeValue(row[col]));
|
|
588
|
+
stmt.run(...values);
|
|
589
|
+
} catch (error) {
|
|
590
|
+
// Constraint violation occurred - analyze and record details
|
|
591
|
+
const constraintError = this.analyzeConstraintError(
|
|
592
|
+
error,
|
|
593
|
+
filePath,
|
|
594
|
+
tableName,
|
|
595
|
+
rowIndex,
|
|
596
|
+
row,
|
|
597
|
+
schema.foreignKeys || [],
|
|
598
|
+
);
|
|
599
|
+
if (constraintError) {
|
|
600
|
+
errors.push(constraintError);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
388
603
|
}
|
|
604
|
+
|
|
605
|
+
return errors;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Analyze constraint error and extract detailed information
|
|
610
|
+
*/
|
|
611
|
+
private analyzeConstraintError(
|
|
612
|
+
error: unknown,
|
|
613
|
+
file: string,
|
|
614
|
+
tableName: string,
|
|
615
|
+
rowIndex: number,
|
|
616
|
+
row: JsonObject,
|
|
617
|
+
foreignKeys: ForeignKeyDefinition[],
|
|
618
|
+
): ValidationErrorDetail | null {
|
|
619
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
620
|
+
|
|
621
|
+
// Foreign key constraint
|
|
622
|
+
if (errorMessage.includes('FOREIGN KEY constraint failed')) {
|
|
623
|
+
// Find which foreign key failed
|
|
624
|
+
for (const fk of foreignKeys) {
|
|
625
|
+
const fkValue = row[fk.column];
|
|
626
|
+
if (fkValue === null || fkValue === undefined) continue;
|
|
627
|
+
|
|
628
|
+
// Check if referenced value exists
|
|
629
|
+
try {
|
|
630
|
+
const result = this.query(
|
|
631
|
+
`SELECT COUNT(*) as count FROM ${this.quoteIdentifier(fk.references.table)} WHERE ${this.quoteIdentifier(fk.references.column)} = ?`,
|
|
632
|
+
[this.normalizeValue(fkValue)],
|
|
633
|
+
);
|
|
634
|
+
if (result.length > 0 && (result[0] as { count: number }).count === 0) {
|
|
635
|
+
return {
|
|
636
|
+
file,
|
|
637
|
+
tableName,
|
|
638
|
+
rowIndex,
|
|
639
|
+
issues: [],
|
|
640
|
+
type: 'foreignKey',
|
|
641
|
+
foreignKeyError: {
|
|
642
|
+
column: fk.column,
|
|
643
|
+
value: fkValue,
|
|
644
|
+
referencedTable: fk.references.table,
|
|
645
|
+
referencedColumn: fk.references.column,
|
|
646
|
+
},
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
} catch (_) {
|
|
650
|
+
// Referenced table doesn't exist yet
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Other constraint errors (primary key, unique, etc.)
|
|
656
|
+
return {
|
|
657
|
+
file,
|
|
658
|
+
tableName,
|
|
659
|
+
rowIndex,
|
|
660
|
+
issues: [
|
|
661
|
+
{
|
|
662
|
+
message: errorMessage,
|
|
663
|
+
path: [],
|
|
664
|
+
},
|
|
665
|
+
],
|
|
666
|
+
type: 'schema',
|
|
667
|
+
};
|
|
389
668
|
}
|
|
390
669
|
|
|
391
670
|
/**
|
|
@@ -1112,10 +1391,20 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
1112
1391
|
/**
|
|
1113
1392
|
* Sync database changes back to JSONL files
|
|
1114
1393
|
* Uses backward transformation when available
|
|
1394
|
+
* @param tableName Optional table name to sync. If not provided, syncs all loaded tables
|
|
1115
1395
|
*/
|
|
1116
|
-
async sync(): Promise<void> {
|
|
1117
|
-
|
|
1396
|
+
async sync(tableName?: string): Promise<void> {
|
|
1397
|
+
if (tableName) {
|
|
1398
|
+
// Sync only the specified table
|
|
1399
|
+
if (!this.schemas.has(tableName)) {
|
|
1400
|
+
throw new Error(`Table '${tableName}' is not loaded`);
|
|
1401
|
+
}
|
|
1118
1402
|
await this.syncTable(tableName);
|
|
1403
|
+
} else {
|
|
1404
|
+
// Sync all tables that are loaded (present in schemas map)
|
|
1405
|
+
for (const [name] of this.schemas) {
|
|
1406
|
+
await this.syncTable(name);
|
|
1407
|
+
}
|
|
1119
1408
|
}
|
|
1120
1409
|
}
|
|
1121
1410
|
|
package/src/index.ts
CHANGED
|
@@ -5,14 +5,12 @@ export { SchemaLoader } from './schema-loader.js';
|
|
|
5
5
|
export { DirectoryScanner } from './directory-scanner.js';
|
|
6
6
|
export { defineSchema, hasBackward } from './schema.js';
|
|
7
7
|
export { TypeGenerator } from './type-generator.js';
|
|
8
|
-
export { Validator } from './validator.js';
|
|
9
8
|
export { ensureTableRowsValid } from './jsonl-migration.js';
|
|
10
9
|
export type { TableValidationOptions } from './jsonl-migration.js';
|
|
11
10
|
export { detectRuntime, RUNTIME } from './runtime.js';
|
|
12
11
|
export type { RuntimeEnvironment } from './runtime.js';
|
|
13
12
|
export type { SQLiteDatabase, SQLiteStatement } from './sqlite-adapter.js';
|
|
14
13
|
export type { TypeGeneratorOptions } from './type-generator.js';
|
|
15
|
-
export type { ValidatorOptions, ValidationResult, ValidationErrorDetail } from './validator.js';
|
|
16
14
|
export type {
|
|
17
15
|
TableSchema,
|
|
18
16
|
ColumnDefinition,
|
|
@@ -25,6 +23,8 @@ export type {
|
|
|
25
23
|
StandardSchemaResult,
|
|
26
24
|
StandardSchemaIssue,
|
|
27
25
|
ValidationError,
|
|
26
|
+
ValidationResult,
|
|
27
|
+
ValidationErrorDetail,
|
|
28
28
|
InferInput,
|
|
29
29
|
InferOutput,
|
|
30
30
|
Table,
|
package/src/jsonl-migration.ts
CHANGED
|
@@ -11,66 +11,34 @@ export interface TableValidationOptions {
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Validate a table by temporarily supplying in-memory rows while reusing the existing LinesDB validation pipeline.
|
|
14
|
-
* If validation fails,
|
|
14
|
+
* If validation fails, throws an error with validation details.
|
|
15
15
|
*/
|
|
16
16
|
export async function ensureTableRowsValid(options: TableValidationOptions): Promise<void> {
|
|
17
|
-
console.log('[ensureTableRowsValid] START');
|
|
18
|
-
console.log('[ensureTableRowsValid] dataDir:', options.dataDir);
|
|
19
|
-
console.log('[ensureTableRowsValid] tableName:', options.tableName);
|
|
20
|
-
console.log('[ensureTableRowsValid] rows count:', options.rows.length);
|
|
21
|
-
|
|
22
17
|
const tablePath = join(options.dataDir, `${options.tableName}.jsonl`);
|
|
23
18
|
const overrides = new Map<string, JsonObject[]>([[tablePath, options.rows]]);
|
|
24
|
-
console.log('[ensureTableRowsValid] tablePath:', tablePath);
|
|
25
|
-
|
|
26
|
-
let capturedError: Error | null = null;
|
|
27
|
-
|
|
28
|
-
// Intercept console.warn to capture validation errors
|
|
29
|
-
const originalWarn = console.warn;
|
|
30
|
-
const warnMessages: string[] = [];
|
|
31
|
-
console.warn = (...args: any[]) => {
|
|
32
|
-
const message = args.join(' ');
|
|
33
|
-
console.log('[ensureTableRowsValid] Captured warn:', message);
|
|
34
|
-
warnMessages.push(message);
|
|
35
|
-
// Check if this is a validation error for our table
|
|
36
|
-
if (
|
|
37
|
-
message.includes(`Failed to load table '${options.tableName}'`) &&
|
|
38
|
-
message.includes('Validation failed')
|
|
39
|
-
) {
|
|
40
|
-
// Extract the original error from the warn message
|
|
41
|
-
capturedError = new Error(message);
|
|
42
|
-
console.log('[ensureTableRowsValid] Captured validation error!');
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
19
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
20
|
+
await JsonlReader.withOverrides(overrides, async () => {
|
|
21
|
+
const db = LinesDB.create({ dataDir: options.dataDir });
|
|
22
|
+
try {
|
|
23
|
+
// Initialize only the target table
|
|
24
|
+
const result = await db.initialize({ tableName: options.tableName });
|
|
25
|
+
|
|
26
|
+
// If validation failed, throw an error with details
|
|
27
|
+
if (!result.valid) {
|
|
28
|
+
const errorCount = result.errors.length;
|
|
29
|
+
const errorDetails = result.errors
|
|
30
|
+
.map((e) => {
|
|
31
|
+
const issueMessages = e.issues.map((issue) => issue.message).join(', ');
|
|
32
|
+
return ` Row ${e.rowIndex}: ${issueMessages}`;
|
|
33
|
+
})
|
|
34
|
+
.join('\n');
|
|
35
|
+
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Validation failed for table '${options.tableName}' (${errorCount} error(s)):\n${errorDetails}`,
|
|
38
|
+
);
|
|
59
39
|
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
console.warn = originalWarn;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
console.log('[ensureTableRowsValid] Warnings captured:', warnMessages.length);
|
|
68
|
-
console.log('[ensureTableRowsValid] capturedError:', capturedError ? 'YES' : 'NO');
|
|
69
|
-
|
|
70
|
-
if (capturedError) {
|
|
71
|
-
console.log('[ensureTableRowsValid] Throwing captured error');
|
|
72
|
-
throw capturedError;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
console.log('[ensureTableRowsValid] END (success)');
|
|
40
|
+
} finally {
|
|
41
|
+
await db.close();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
76
44
|
}
|