@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/cli.ts
CHANGED
|
@@ -5,13 +5,13 @@ import { register } from 'node:module';
|
|
|
5
5
|
register('tsx', import.meta.url, { data: {} });
|
|
6
6
|
|
|
7
7
|
import { TypeGenerator } from './type-generator.js';
|
|
8
|
-
import { Validator } from './validator.js';
|
|
9
8
|
import { LinesDB } from './database.js';
|
|
10
9
|
import { ErrorFormatter } from './error-formatter.js';
|
|
11
|
-
import type { ValidationError } from './types.js';
|
|
10
|
+
import type { ValidationError, JsonObject } from './types.js';
|
|
12
11
|
import { Command } from 'commander';
|
|
13
12
|
import { styleText } from 'node:util';
|
|
14
13
|
import { writeFile, stat, readdir } from 'node:fs/promises';
|
|
14
|
+
import { basename, dirname } from 'node:path';
|
|
15
15
|
import { runInNewContext } from 'node:vm';
|
|
16
16
|
|
|
17
17
|
const originalEmitWarning = process.emitWarning;
|
|
@@ -81,8 +81,29 @@ program
|
|
|
81
81
|
.option('-v, --verbose', 'Show verbose error output', false)
|
|
82
82
|
.action(async (path: string, options: { verbose: boolean }) => {
|
|
83
83
|
try {
|
|
84
|
-
|
|
85
|
-
const
|
|
84
|
+
// Determine if path is a file or directory
|
|
85
|
+
const stats = await stat(path);
|
|
86
|
+
let dataDir: string;
|
|
87
|
+
let tableName: string | undefined;
|
|
88
|
+
|
|
89
|
+
if (stats.isDirectory()) {
|
|
90
|
+
dataDir = path;
|
|
91
|
+
// Validate all tables in directory
|
|
92
|
+
} else if (stats.isFile() && path.endsWith('.jsonl')) {
|
|
93
|
+
dataDir = dirname(path);
|
|
94
|
+
tableName = basename(path, '.jsonl');
|
|
95
|
+
} else {
|
|
96
|
+
throw new Error(`Invalid path: ${path}. Must be a directory or .jsonl file.`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Use LinesDB.initialize() with detailed validation
|
|
100
|
+
const db = LinesDB.create({ dataDir });
|
|
101
|
+
let result;
|
|
102
|
+
try {
|
|
103
|
+
result = await db.initialize({ tableName, detailedValidate: true });
|
|
104
|
+
} finally {
|
|
105
|
+
await db.close();
|
|
106
|
+
}
|
|
86
107
|
|
|
87
108
|
// Display warnings if any
|
|
88
109
|
if (result.warnings.length > 0) {
|
|
@@ -223,7 +244,33 @@ async function migrateDirectory(
|
|
|
223
244
|
|
|
224
245
|
// Initialize database
|
|
225
246
|
const db = LinesDB.create({ dataDir: dirPath });
|
|
226
|
-
await db.initialize();
|
|
247
|
+
const initResult = await db.initialize({ detailedValidate: true });
|
|
248
|
+
|
|
249
|
+
// Display warnings if any
|
|
250
|
+
if (initResult.warnings.length > 0) {
|
|
251
|
+
for (const warning of initResult.warnings) {
|
|
252
|
+
console.warn(styleText('yellow', `⚠ ${warning}`));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Check for initialization errors
|
|
257
|
+
if (!initResult.valid) {
|
|
258
|
+
console.error(`Error: Failed to initialize database due to validation errors:`);
|
|
259
|
+
const formatter = new ErrorFormatter({ verbose: options.verbose });
|
|
260
|
+
for (const error of initResult.errors) {
|
|
261
|
+
console.error(
|
|
262
|
+
formatter.formatValidationErrors([
|
|
263
|
+
{
|
|
264
|
+
file: error.file,
|
|
265
|
+
rowIndex: error.rowIndex,
|
|
266
|
+
issues: error.issues,
|
|
267
|
+
},
|
|
268
|
+
]),
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
await db.close();
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
227
274
|
|
|
228
275
|
const tableNames = db.getTableNames();
|
|
229
276
|
if (tableNames.length === 0) {
|
|
@@ -275,7 +322,7 @@ async function migrateDirectory(
|
|
|
275
322
|
console.log(` Found ${rowsToMigrate.length} row(s) to migrate`);
|
|
276
323
|
|
|
277
324
|
// Apply transformation
|
|
278
|
-
const transformedRows = rowsToMigrate.map((row) => transform(row));
|
|
325
|
+
const transformedRows = rowsToMigrate.map((row) => transform(row as JsonObject));
|
|
279
326
|
|
|
280
327
|
// Perform the migration in a transaction
|
|
281
328
|
await db.transaction(async () => {
|
|
@@ -370,19 +417,52 @@ async function migrateFile(
|
|
|
370
417
|
const lastSlashIndex = filePath.lastIndexOf('/');
|
|
371
418
|
const dataDir = lastSlashIndex > 0 ? filePath.substring(0, lastSlashIndex) : '.';
|
|
372
419
|
|
|
373
|
-
//
|
|
374
|
-
|
|
375
|
-
await db.initialize();
|
|
376
|
-
|
|
420
|
+
// Parse transform function first (before initialization)
|
|
421
|
+
let transform: (row: JsonObject) => JsonObject;
|
|
377
422
|
try {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
if (typeof transform !== 'function') {
|
|
423
|
+
const parsedTransform = runInSandbox<unknown>(`(${transformStr})`);
|
|
424
|
+
if (typeof parsedTransform !== 'function') {
|
|
382
425
|
console.error('Error: Transform must be a function');
|
|
383
426
|
process.exit(1);
|
|
384
427
|
}
|
|
428
|
+
transform = parsedTransform as (row: JsonObject) => JsonObject;
|
|
429
|
+
} catch (error) {
|
|
430
|
+
console.error('Error: Failed to parse transform function');
|
|
431
|
+
console.error(` ${error instanceof Error ? error.message : String(error)}`);
|
|
432
|
+
process.exit(1);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Initialize database with transform applied
|
|
436
|
+
const db = LinesDB.create({ dataDir });
|
|
437
|
+
const initResult = await db.initialize({ tableName, transform, detailedValidate: true });
|
|
438
|
+
|
|
439
|
+
// Display warnings if any
|
|
440
|
+
if (initResult.warnings.length > 0) {
|
|
441
|
+
for (const warning of initResult.warnings) {
|
|
442
|
+
console.warn(styleText('yellow', `⚠ ${warning}`));
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Check for initialization errors
|
|
447
|
+
if (!initResult.valid) {
|
|
448
|
+
console.error(`Error: Failed to initialize database due to validation errors:`);
|
|
449
|
+
const formatter = new ErrorFormatter({ verbose: options.verbose });
|
|
450
|
+
for (const error of initResult.errors) {
|
|
451
|
+
console.error(
|
|
452
|
+
formatter.formatValidationErrors([
|
|
453
|
+
{
|
|
454
|
+
file: error.file,
|
|
455
|
+
rowIndex: error.rowIndex,
|
|
456
|
+
issues: error.issues,
|
|
457
|
+
},
|
|
458
|
+
]),
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
await db.close();
|
|
462
|
+
process.exit(1);
|
|
463
|
+
}
|
|
385
464
|
|
|
465
|
+
try {
|
|
386
466
|
// Parse filter if provided
|
|
387
467
|
let filter: unknown = undefined;
|
|
388
468
|
if (options.filter) {
|
|
@@ -395,136 +475,156 @@ async function migrateFile(
|
|
|
395
475
|
}
|
|
396
476
|
}
|
|
397
477
|
|
|
398
|
-
//
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
rowsToMigrate
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
478
|
+
// If filter is provided, we need to apply transform only to matching rows
|
|
479
|
+
if (filter) {
|
|
480
|
+
// Get rows to migrate
|
|
481
|
+
let rowsToMigrate;
|
|
482
|
+
try {
|
|
483
|
+
rowsToMigrate = db.find(tableName, filter as Parameters<typeof db.find>[1]);
|
|
484
|
+
} catch (error) {
|
|
485
|
+
console.error(`Error: Failed to access table '${tableName}'`);
|
|
486
|
+
console.error(` ${error instanceof Error ? error.message : String(error)}`);
|
|
487
|
+
console.error(`\nThe table may have failed to load during initialization.`);
|
|
488
|
+
console.error(`Check the table's data and schema for any constraint violations.`);
|
|
489
|
+
await db.close();
|
|
490
|
+
process.exit(1);
|
|
491
|
+
}
|
|
412
492
|
|
|
413
|
-
|
|
493
|
+
console.log(`Found ${rowsToMigrate.length} row(s) to migrate in table '${tableName}'`);
|
|
414
494
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
495
|
+
if (rowsToMigrate.length === 0) {
|
|
496
|
+
console.log('No rows to migrate. Exiting.');
|
|
497
|
+
await db.close();
|
|
498
|
+
process.exit(0);
|
|
499
|
+
}
|
|
420
500
|
|
|
421
|
-
|
|
422
|
-
|
|
501
|
+
// Apply transformation
|
|
502
|
+
const transformedRows = rowsToMigrate.map((row) => transform(row as JsonObject));
|
|
423
503
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
504
|
+
// Perform the migration in a transaction
|
|
505
|
+
try {
|
|
506
|
+
await db.transaction(async () => {
|
|
507
|
+
db.batchUpdate(tableName, transformedRows as Parameters<typeof db.batchUpdate>[1], {
|
|
508
|
+
validate: true,
|
|
509
|
+
});
|
|
429
510
|
});
|
|
430
|
-
});
|
|
431
511
|
|
|
432
|
-
|
|
512
|
+
await db.close();
|
|
433
513
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
514
|
+
console.log(`\nMigration completed successfully:`);
|
|
515
|
+
console.log(` ✓ ${rowsToMigrate.length} row(s) updated`);
|
|
516
|
+
process.exit(0);
|
|
517
|
+
} catch (error) {
|
|
518
|
+
await db.close();
|
|
439
519
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
520
|
+
// Write transformed data to error output file if --errorOutput is specified
|
|
521
|
+
if (options.errorOutput) {
|
|
522
|
+
try {
|
|
523
|
+
const jsonlContent = transformedRows.map((row) => JSON.stringify(row)).join('\n');
|
|
524
|
+
await writeFile(options.errorOutput, jsonlContent, 'utf-8');
|
|
525
|
+
console.error(
|
|
526
|
+
styleText(
|
|
527
|
+
'yellow',
|
|
528
|
+
`\n⚠ Transformed data (${transformedRows.length} rows) written to: ${options.errorOutput}`,
|
|
529
|
+
),
|
|
530
|
+
);
|
|
531
|
+
} catch (writeError) {
|
|
532
|
+
console.error(
|
|
533
|
+
styleText(
|
|
534
|
+
'red',
|
|
535
|
+
`\n✗ Failed to write error output file: ${writeError instanceof Error ? writeError.message : String(writeError)}`,
|
|
536
|
+
),
|
|
537
|
+
);
|
|
538
|
+
}
|
|
458
539
|
}
|
|
459
|
-
}
|
|
460
540
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
// Display all validation errors
|
|
476
|
-
if (validationError.validationErrors) {
|
|
477
|
-
console.error(
|
|
478
|
-
`\nFound ${validationError.validationErrors.length} validation error(s) in transformed data:\n`,
|
|
479
|
-
);
|
|
541
|
+
const formatter = new ErrorFormatter({ verbose: options.verbose });
|
|
542
|
+
console.error(formatter.formatMigrationFailureHeader());
|
|
543
|
+
|
|
544
|
+
// Display detailed error information
|
|
545
|
+
if (error instanceof Error && error.name === 'ValidationError') {
|
|
546
|
+
const validationError = error as ValidationError & {
|
|
547
|
+
validationErrors?: Array<{
|
|
548
|
+
rowIndex: number;
|
|
549
|
+
rowData: unknown;
|
|
550
|
+
pkValue: unknown;
|
|
551
|
+
error: ValidationError;
|
|
552
|
+
}>;
|
|
553
|
+
};
|
|
480
554
|
|
|
481
|
-
|
|
482
|
-
|
|
555
|
+
// Display all validation errors
|
|
556
|
+
if (validationError.validationErrors) {
|
|
557
|
+
console.error(
|
|
558
|
+
`\nFound ${validationError.validationErrors.length} validation error(s) in transformed data:\n`,
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
const errorInfos = validationError.validationErrors.map(
|
|
562
|
+
({ rowIndex, rowData, error: rowError }) => ({
|
|
563
|
+
file: filePath,
|
|
564
|
+
rowIndex,
|
|
565
|
+
issues: rowError.issues,
|
|
566
|
+
data: rowData,
|
|
567
|
+
originalData: rowsToMigrate[rowIndex],
|
|
568
|
+
}),
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
const formatted = formatter.formatValidationErrors(errorInfos);
|
|
572
|
+
console.error(formatted);
|
|
573
|
+
} else {
|
|
574
|
+
// Fallback for single validation error (backward compatibility)
|
|
575
|
+
console.error('\nValidation error:\n');
|
|
576
|
+
const errorInfo = {
|
|
483
577
|
file: filePath,
|
|
484
|
-
rowIndex,
|
|
485
|
-
issues:
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
578
|
+
rowIndex: 0,
|
|
579
|
+
issues: validationError.issues,
|
|
580
|
+
};
|
|
581
|
+
const formatted = formatter.formatValidationErrors([errorInfo]);
|
|
582
|
+
console.error(formatted);
|
|
583
|
+
}
|
|
584
|
+
} else if (error instanceof Error) {
|
|
585
|
+
console.error(`\n ${error.message}`);
|
|
490
586
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
console.error('\nValidation error:\n');
|
|
496
|
-
const errorInfo = {
|
|
497
|
-
file: filePath,
|
|
498
|
-
rowIndex: 0,
|
|
499
|
-
issues: validationError.issues,
|
|
500
|
-
};
|
|
501
|
-
const formatted = formatter.formatValidationErrors([errorInfo]);
|
|
502
|
-
console.error(formatted);
|
|
503
|
-
}
|
|
504
|
-
} else if (error instanceof Error) {
|
|
505
|
-
console.error(`\n ${error.message}`);
|
|
587
|
+
// Output stack trace for debugging
|
|
588
|
+
if (options.verbose && error.stack) {
|
|
589
|
+
console.error(`\nStack trace:\n${error.stack}`);
|
|
590
|
+
}
|
|
506
591
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
592
|
+
// Check if it's a SQLite constraint error
|
|
593
|
+
if (
|
|
594
|
+
error.message.includes('UNIQUE constraint failed') ||
|
|
595
|
+
error.message.includes('FOREIGN KEY constraint failed') ||
|
|
596
|
+
error.message.includes('NOT NULL constraint failed') ||
|
|
597
|
+
error.message.includes('CHECK constraint failed')
|
|
598
|
+
) {
|
|
599
|
+
console.error('\n This is a SQLite constraint violation.');
|
|
600
|
+
console.error(' Please check your data and schema requirements.');
|
|
601
|
+
}
|
|
602
|
+
} else {
|
|
603
|
+
console.error(`\n ${String(error)}`);
|
|
510
604
|
}
|
|
511
605
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
error.message.includes('UNIQUE constraint failed') ||
|
|
515
|
-
error.message.includes('FOREIGN KEY constraint failed') ||
|
|
516
|
-
error.message.includes('NOT NULL constraint failed') ||
|
|
517
|
-
error.message.includes('CHECK constraint failed')
|
|
518
|
-
) {
|
|
519
|
-
console.error('\n This is a SQLite constraint violation.');
|
|
520
|
-
console.error(' Please check your data and schema requirements.');
|
|
521
|
-
}
|
|
522
|
-
} else {
|
|
523
|
-
console.error(`\n ${String(error)}`);
|
|
606
|
+
console.error('');
|
|
607
|
+
process.exit(1);
|
|
524
608
|
}
|
|
609
|
+
} else {
|
|
610
|
+
// No filter - all rows have been transformed during initialization
|
|
611
|
+
// Just sync to write back to JSONL file
|
|
612
|
+
try {
|
|
613
|
+
const allRows = db.find(tableName);
|
|
614
|
+
console.log(`Migrated ${allRows.length} row(s) in table '${tableName}'`);
|
|
525
615
|
|
|
526
|
-
|
|
527
|
-
|
|
616
|
+
await db.sync(tableName);
|
|
617
|
+
await db.close();
|
|
618
|
+
|
|
619
|
+
console.log(`\nMigration completed successfully:`);
|
|
620
|
+
console.log(` ✓ ${allRows.length} row(s) updated`);
|
|
621
|
+
process.exit(0);
|
|
622
|
+
} catch (error) {
|
|
623
|
+
await db.close();
|
|
624
|
+
console.error('Error: Failed to sync changes to file');
|
|
625
|
+
console.error(` ${error instanceof Error ? error.message : String(error)}`);
|
|
626
|
+
process.exit(1);
|
|
627
|
+
}
|
|
528
628
|
}
|
|
529
629
|
} catch (error) {
|
|
530
630
|
await db.close();
|