@toiroakr/lines-db 0.5.0 → 0.6.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 +18 -0
- package/bin/cli.js +378 -415
- package/dist/index.cjs +195 -327
- 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 +197 -328
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/cli.ts +226 -126
- package/src/database.ts +296 -52
- 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;
|
|
@@ -233,29 +313,48 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
233
313
|
}
|
|
234
314
|
}
|
|
235
315
|
|
|
316
|
+
// Convert validation errors to ValidationErrorDetail format
|
|
317
|
+
const validationErrorDetails: ValidationErrorDetail[] = validationErrors.map((ve) => ({
|
|
318
|
+
file: config.jsonlPath,
|
|
319
|
+
tableName,
|
|
320
|
+
rowIndex: ve.rowIndex,
|
|
321
|
+
issues: ve.error.issues,
|
|
322
|
+
type: 'schema' as const,
|
|
323
|
+
}));
|
|
324
|
+
|
|
236
325
|
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;
|
|
326
|
+
// Return errors instead of throwing
|
|
327
|
+
return { loaded: false, errors: validationErrorDetails };
|
|
246
328
|
}
|
|
247
329
|
|
|
248
330
|
// Determine schema - infer from validated data if auto-inference is enabled
|
|
249
331
|
let schema: TableSchema;
|
|
332
|
+
let inferredSchema: TableSchema | undefined;
|
|
333
|
+
|
|
334
|
+
// Always infer schema from validated data to capture valueType information (e.g., boolean)
|
|
335
|
+
if (validatedData.length > 0) {
|
|
336
|
+
inferredSchema = JsonlReader.inferSchema(tableName, validatedData);
|
|
337
|
+
}
|
|
338
|
+
|
|
250
339
|
if (config.schema) {
|
|
251
340
|
schema = config.schema;
|
|
341
|
+
// Merge valueType information from inferred schema
|
|
342
|
+
if (inferredSchema) {
|
|
343
|
+
for (const inferredCol of inferredSchema.columns) {
|
|
344
|
+
const schemaCol = schema.columns.find((c) => c.name === inferredCol.name);
|
|
345
|
+
if (schemaCol && inferredCol.valueType && !schemaCol.valueType) {
|
|
346
|
+
schemaCol.valueType = inferredCol.valueType;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
252
350
|
} else if (config.autoInferSchema !== false) {
|
|
253
351
|
if (validatedData.length === 0) {
|
|
254
|
-
return false;
|
|
352
|
+
return { loaded: false, errors: [] };
|
|
255
353
|
}
|
|
256
|
-
//
|
|
257
|
-
schema =
|
|
354
|
+
// Use inferred schema
|
|
355
|
+
schema = inferredSchema!;
|
|
258
356
|
} else {
|
|
357
|
+
// Critical error - throw exception
|
|
259
358
|
throw new Error(`No schema provided for table ${tableName} and autoInferSchema is disabled`);
|
|
260
359
|
}
|
|
261
360
|
|
|
@@ -292,10 +391,22 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
292
391
|
// Create table
|
|
293
392
|
this.createTable(schema);
|
|
294
393
|
|
|
295
|
-
// Insert validated data
|
|
296
|
-
|
|
394
|
+
// Insert validated data (with detailed validation if requested)
|
|
395
|
+
if (detailedValidate) {
|
|
396
|
+
const insertErrors = this.insertDataWithDetailedValidation(
|
|
397
|
+
tableName,
|
|
398
|
+
schema,
|
|
399
|
+
validatedData,
|
|
400
|
+
config.jsonlPath,
|
|
401
|
+
);
|
|
402
|
+
if (insertErrors.length > 0) {
|
|
403
|
+
return { loaded: false, errors: insertErrors };
|
|
404
|
+
}
|
|
405
|
+
} else {
|
|
406
|
+
this.insertData(tableName, schema, validatedData);
|
|
407
|
+
}
|
|
297
408
|
|
|
298
|
-
return true;
|
|
409
|
+
return { loaded: true, errors: [] };
|
|
299
410
|
}
|
|
300
411
|
|
|
301
412
|
/**
|
|
@@ -372,9 +483,52 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
372
483
|
}
|
|
373
484
|
|
|
374
485
|
/**
|
|
375
|
-
* Insert data into table
|
|
486
|
+
* Insert data into table using batch insert (multiple rows per SQL)
|
|
487
|
+
* SQLite has a parameter limit (default 999), so we batch rows accordingly
|
|
488
|
+
* Throws exception if any constraint violation occurs
|
|
376
489
|
*/
|
|
377
490
|
private insertData(tableName: string, schema: TableSchema, data: JsonObject[]): void {
|
|
491
|
+
if (data.length === 0) return;
|
|
492
|
+
|
|
493
|
+
const columnNames = schema.columns.map((col) => col.name);
|
|
494
|
+
const quotedColumns = columnNames.map((name) => this.quoteIdentifier(name));
|
|
495
|
+
const columnCount = columnNames.length;
|
|
496
|
+
|
|
497
|
+
// Calculate batch size to stay under SQLite's parameter limit (999)
|
|
498
|
+
// Leave some margin for safety
|
|
499
|
+
const maxBatchSize = Math.floor(900 / columnCount);
|
|
500
|
+
const batchSize = Math.max(1, Math.min(maxBatchSize, 100));
|
|
501
|
+
|
|
502
|
+
// Process data in batches
|
|
503
|
+
for (let i = 0; i < data.length; i += batchSize) {
|
|
504
|
+
const batch = data.slice(i, i + batchSize);
|
|
505
|
+
const rowPlaceholders = columnNames.map(() => '?').join(', ');
|
|
506
|
+
const valuesPlaceholders = batch.map(() => `(${rowPlaceholders})`).join(', ');
|
|
507
|
+
const sql = `INSERT INTO ${this.quoteTableName(tableName)} (${quotedColumns.join(', ')}) VALUES ${valuesPlaceholders}`;
|
|
508
|
+
|
|
509
|
+
const values: (string | number | bigint | null | Uint8Array)[] = [];
|
|
510
|
+
for (const row of batch) {
|
|
511
|
+
for (const col of columnNames) {
|
|
512
|
+
values.push(this.normalizeValue(row[col]));
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const stmt = this.db.prepare(sql);
|
|
517
|
+
stmt.run(...values);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Insert data into table one row at a time with detailed error reporting
|
|
523
|
+
* This is used for validation to catch constraint violations
|
|
524
|
+
*/
|
|
525
|
+
private insertDataWithDetailedValidation(
|
|
526
|
+
tableName: string,
|
|
527
|
+
schema: TableSchema,
|
|
528
|
+
data: JsonObject[],
|
|
529
|
+
filePath: string,
|
|
530
|
+
): ValidationErrorDetail[] {
|
|
531
|
+
const errors: ValidationErrorDetail[] = [];
|
|
378
532
|
const columnNames = schema.columns.map((col) => col.name);
|
|
379
533
|
const quotedColumns = columnNames.map((name) => this.quoteIdentifier(name));
|
|
380
534
|
const placeholders = columnNames.map(() => '?').join(', ');
|
|
@@ -382,10 +536,90 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
382
536
|
|
|
383
537
|
const stmt = this.db.prepare(sql);
|
|
384
538
|
|
|
385
|
-
for (
|
|
386
|
-
const
|
|
387
|
-
|
|
539
|
+
for (let rowIndex = 0; rowIndex < data.length; rowIndex++) {
|
|
540
|
+
const row = data[rowIndex];
|
|
541
|
+
try {
|
|
542
|
+
const values = columnNames.map((col) => this.normalizeValue(row[col]));
|
|
543
|
+
stmt.run(...values);
|
|
544
|
+
} catch (error) {
|
|
545
|
+
// Constraint violation occurred - analyze and record details
|
|
546
|
+
const constraintError = this.analyzeConstraintError(
|
|
547
|
+
error,
|
|
548
|
+
filePath,
|
|
549
|
+
tableName,
|
|
550
|
+
rowIndex,
|
|
551
|
+
row,
|
|
552
|
+
schema.foreignKeys || [],
|
|
553
|
+
);
|
|
554
|
+
if (constraintError) {
|
|
555
|
+
errors.push(constraintError);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
388
558
|
}
|
|
559
|
+
|
|
560
|
+
return errors;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Analyze constraint error and extract detailed information
|
|
565
|
+
*/
|
|
566
|
+
private analyzeConstraintError(
|
|
567
|
+
error: unknown,
|
|
568
|
+
file: string,
|
|
569
|
+
tableName: string,
|
|
570
|
+
rowIndex: number,
|
|
571
|
+
row: JsonObject,
|
|
572
|
+
foreignKeys: ForeignKeyDefinition[],
|
|
573
|
+
): ValidationErrorDetail | null {
|
|
574
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
575
|
+
|
|
576
|
+
// Foreign key constraint
|
|
577
|
+
if (errorMessage.includes('FOREIGN KEY constraint failed')) {
|
|
578
|
+
// Find which foreign key failed
|
|
579
|
+
for (const fk of foreignKeys) {
|
|
580
|
+
const fkValue = row[fk.column];
|
|
581
|
+
if (fkValue === null || fkValue === undefined) continue;
|
|
582
|
+
|
|
583
|
+
// Check if referenced value exists
|
|
584
|
+
try {
|
|
585
|
+
const result = this.query(
|
|
586
|
+
`SELECT COUNT(*) as count FROM ${this.quoteIdentifier(fk.references.table)} WHERE ${this.quoteIdentifier(fk.references.column)} = ?`,
|
|
587
|
+
[this.normalizeValue(fkValue)],
|
|
588
|
+
);
|
|
589
|
+
if (result.length > 0 && (result[0] as { count: number }).count === 0) {
|
|
590
|
+
return {
|
|
591
|
+
file,
|
|
592
|
+
tableName,
|
|
593
|
+
rowIndex,
|
|
594
|
+
issues: [],
|
|
595
|
+
type: 'foreignKey',
|
|
596
|
+
foreignKeyError: {
|
|
597
|
+
column: fk.column,
|
|
598
|
+
value: fkValue,
|
|
599
|
+
referencedTable: fk.references.table,
|
|
600
|
+
referencedColumn: fk.references.column,
|
|
601
|
+
},
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
} catch (_) {
|
|
605
|
+
// Referenced table doesn't exist yet
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Other constraint errors (primary key, unique, etc.)
|
|
611
|
+
return {
|
|
612
|
+
file,
|
|
613
|
+
tableName,
|
|
614
|
+
rowIndex,
|
|
615
|
+
issues: [
|
|
616
|
+
{
|
|
617
|
+
message: errorMessage,
|
|
618
|
+
path: [],
|
|
619
|
+
},
|
|
620
|
+
],
|
|
621
|
+
type: 'schema',
|
|
622
|
+
};
|
|
389
623
|
}
|
|
390
624
|
|
|
391
625
|
/**
|
|
@@ -1112,10 +1346,20 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
1112
1346
|
/**
|
|
1113
1347
|
* Sync database changes back to JSONL files
|
|
1114
1348
|
* Uses backward transformation when available
|
|
1349
|
+
* @param tableName Optional table name to sync. If not provided, syncs all loaded tables
|
|
1115
1350
|
*/
|
|
1116
|
-
async sync(): Promise<void> {
|
|
1117
|
-
|
|
1351
|
+
async sync(tableName?: string): Promise<void> {
|
|
1352
|
+
if (tableName) {
|
|
1353
|
+
// Sync only the specified table
|
|
1354
|
+
if (!this.schemas.has(tableName)) {
|
|
1355
|
+
throw new Error(`Table '${tableName}' is not loaded`);
|
|
1356
|
+
}
|
|
1118
1357
|
await this.syncTable(tableName);
|
|
1358
|
+
} else {
|
|
1359
|
+
// Sync all tables that are loaded (present in schemas map)
|
|
1360
|
+
for (const [name] of this.schemas) {
|
|
1361
|
+
await this.syncTable(name);
|
|
1362
|
+
}
|
|
1119
1363
|
}
|
|
1120
1364
|
}
|
|
1121
1365
|
|
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
|
}
|