@toiroakr/lines-db 0.4.1 → 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/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
- import { writeFile } from 'node:fs/promises';
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
- const validator = new Validator({ path });
85
- const result = await validator.validate();
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) {
@@ -161,191 +182,454 @@ program
161
182
  program
162
183
  .command('migrate')
163
184
  .description('Migrate data with transformation function')
164
- .argument('<file>', 'JSONL file to migrate')
185
+ .argument('<path>', 'File or directory path to migrate')
165
186
  .argument('<transform>', 'Transform function (e.g., "(row) => ({ ...row, age: row.age + 1 })")')
166
187
  .option('-f, --filter <expr>', 'Filter expression')
167
188
  .option('-e, --errorOutput <path>', 'Output file path for transformed data when migration fails')
168
189
  .option('-v, --verbose', 'Show verbose error output', false)
169
190
  .action(
170
191
  async (
171
- filePath: string,
192
+ path: string,
172
193
  transformStr: string,
173
194
  options: { filter?: string; errorOutput?: string; verbose: boolean },
174
195
  ) => {
175
- // Extract table name from file path
176
- const fileName = filePath.split('/').pop() || '';
177
- const tableName = fileName.replace('.jsonl', '');
178
-
179
- if (!tableName) {
180
- console.error('Error: Invalid file path. Must be a .jsonl file');
196
+ try {
197
+ const stats = await stat(path);
198
+
199
+ if (stats.isDirectory()) {
200
+ // Migrate all JSONL files in directory
201
+ await migrateDirectory(path, transformStr, options);
202
+ } else if (stats.isFile() && path.endsWith('.jsonl')) {
203
+ // Migrate single file
204
+ await migrateFile(path, transformStr, options);
205
+ } else {
206
+ console.error(`Error: Invalid path: ${path}. Must be a directory or .jsonl file.`);
207
+ process.exit(1);
208
+ }
209
+ } catch (error) {
210
+ if (
211
+ error instanceof Error &&
212
+ 'code' in error &&
213
+ (error as NodeJS.ErrnoException).code === 'ENOENT'
214
+ ) {
215
+ console.error(`Error: Path not found: ${path}`);
216
+ } else {
217
+ console.error(`Error: ${String(error)}`);
218
+ }
181
219
  process.exit(1);
182
220
  }
221
+ },
222
+ );
183
223
 
184
- // Get directory from file path
185
- const lastSlashIndex = filePath.lastIndexOf('/');
186
- const dataDir = lastSlashIndex > 0 ? filePath.substring(0, lastSlashIndex) : '.';
187
-
188
- // Initialize database
189
- const db = LinesDB.create({ dataDir });
190
- await db.initialize();
224
+ /**
225
+ * Migrate all JSONL files in a directory
226
+ */
227
+ async function migrateDirectory(
228
+ dirPath: string,
229
+ transformStr: string,
230
+ options: { filter?: string; errorOutput?: string; verbose: boolean },
231
+ ) {
232
+ // Find all JSONL files in directory
233
+ const entries = await readdir(dirPath, { withFileTypes: true });
234
+ const jsonlFiles = entries
235
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.jsonl'))
236
+ .map((entry) => entry.name);
237
+
238
+ if (jsonlFiles.length === 0) {
239
+ console.error(`Error: No JSONL files found in directory: ${dirPath}`);
240
+ process.exit(1);
241
+ }
242
+
243
+ console.log(`Found ${jsonlFiles.length} JSONL file(s) in directory`);
244
+
245
+ // Initialize database
246
+ const db = LinesDB.create({ dataDir: dirPath });
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
+ }
274
+
275
+ const tableNames = db.getTableNames();
276
+ if (tableNames.length === 0) {
277
+ console.error(`Error: No tables could be loaded from directory: ${dirPath}`);
278
+ await db.close();
279
+ process.exit(1);
280
+ }
281
+
282
+ console.log(`Loaded ${tableNames.length} table(s): ${tableNames.join(', ')}\n`);
283
+
284
+ try {
285
+ // Parse transform function
286
+ const transform = runInSandbox<unknown>(`(${transformStr})`);
287
+
288
+ if (typeof transform !== 'function') {
289
+ console.error('Error: Transform must be a function');
290
+ await db.close();
291
+ process.exit(1);
292
+ }
191
293
 
294
+ // Parse filter if provided
295
+ let filter: unknown = undefined;
296
+ if (options.filter) {
192
297
  try {
193
- // Parse transform function
194
- const transform = runInSandbox<unknown>(`(${transformStr})`);
298
+ filter = JSON.parse(options.filter);
299
+ } catch {
300
+ filter = runInSandbox(`(${options.filter})`);
301
+ }
302
+ }
195
303
 
196
- if (typeof transform !== 'function') {
197
- console.error('Error: Transform must be a function');
198
- process.exit(1);
199
- }
304
+ let totalRowsMigrated = 0;
305
+ let hasErrors = false;
200
306
 
201
- // Parse filter if provided
202
- let filter: unknown = undefined;
203
- if (options.filter) {
204
- try {
205
- // Try JSON parse first
206
- filter = JSON.parse(options.filter);
207
- } catch {
208
- // Fall back to eval for JavaScript expressions
209
- filter = runInSandbox(`(${options.filter})`);
210
- }
211
- }
307
+ // Process each table
308
+ for (const tableName of tableNames) {
309
+ try {
310
+ console.log(`Processing table '${tableName}'...`);
212
311
 
213
312
  // Get rows to migrate
214
- let rowsToMigrate;
215
- try {
216
- rowsToMigrate = filter
217
- ? db.find(tableName, filter as Parameters<typeof db.find>[1])
218
- : db.find(tableName);
219
- } catch (error) {
220
- console.error(`Error: Failed to access table '${tableName}'`);
221
- console.error(` ${error instanceof Error ? error.message : String(error)}`);
222
- console.error(`\nThe table may have failed to load during initialization.`);
223
- console.error(`Check the table's data and schema for any constraint violations.`);
224
- await db.close();
225
- process.exit(1);
226
- }
227
-
228
- console.log(`Found ${rowsToMigrate.length} row(s) to migrate in table '${tableName}'`);
313
+ const rowsToMigrate = filter
314
+ ? db.find(tableName, filter as Parameters<typeof db.find>[1])
315
+ : db.find(tableName);
229
316
 
230
317
  if (rowsToMigrate.length === 0) {
231
- console.log('No rows to migrate. Exiting.');
232
- await db.close();
233
- process.exit(0);
318
+ console.log(` No rows to migrate`);
319
+ continue;
234
320
  }
235
321
 
322
+ console.log(` Found ${rowsToMigrate.length} row(s) to migrate`);
323
+
236
324
  // Apply transformation
237
- const transformedRows = rowsToMigrate.map((row) => transform(row));
325
+ const transformedRows = rowsToMigrate.map((row) => transform(row as JsonObject));
238
326
 
239
327
  // Perform the migration in a transaction
240
- try {
241
- await db.transaction(async () => {
242
- db.batchUpdate(tableName, transformedRows as Parameters<typeof db.batchUpdate>[1], {
243
- validate: true,
244
- });
328
+ await db.transaction(async () => {
329
+ db.batchUpdate(tableName, transformedRows as Parameters<typeof db.batchUpdate>[1], {
330
+ validate: true,
245
331
  });
332
+ });
246
333
 
247
- await db.close();
248
-
249
- console.log(`\nMigration completed successfully:`);
250
- console.log(` ✓ ${rowsToMigrate.length} row(s) updated`);
251
- process.exit(0);
252
- } catch (error) {
253
- await db.close();
254
-
255
- // Write transformed data to error output file if --errorOutput is specified
256
- if (options.errorOutput) {
257
- try {
258
- const jsonlContent = transformedRows.map((row) => JSON.stringify(row)).join('\n');
259
- await writeFile(options.errorOutput, jsonlContent, 'utf-8');
260
- console.error(
261
- styleText(
262
- 'yellow',
263
- `\n⚠ Transformed data (${transformedRows.length} rows) written to: ${options.errorOutput}`,
264
- ),
265
- );
266
- } catch (writeError) {
267
- console.error(
268
- styleText(
269
- 'red',
270
- `\n✗ Failed to write error output file: ${writeError instanceof Error ? writeError.message : String(writeError)}`,
271
- ),
272
- );
273
- }
334
+ console.log(` ✓ ${rowsToMigrate.length} row(s) updated\n`);
335
+ totalRowsMigrated += rowsToMigrate.length;
336
+ } catch (error) {
337
+ hasErrors = true;
338
+ console.error(styleText('red', ` ✗ Failed to migrate table '${tableName}'`));
339
+
340
+ const formatter = new ErrorFormatter({ verbose: options.verbose });
341
+
342
+ if (error instanceof Error && error.name === 'ValidationError') {
343
+ const validationError = error as ValidationError & {
344
+ validationErrors?: Array<{
345
+ rowIndex: number;
346
+ rowData: unknown;
347
+ pkValue: unknown;
348
+ error: ValidationError;
349
+ }>;
350
+ };
351
+
352
+ if (validationError.validationErrors) {
353
+ console.error(
354
+ ` Found ${validationError.validationErrors.length} validation error(s):\n`,
355
+ );
356
+
357
+ const rowsToMigrate = filter
358
+ ? db.find(tableName, filter as Parameters<typeof db.find>[1])
359
+ : db.find(tableName);
360
+
361
+ const errorInfos = validationError.validationErrors.map(
362
+ ({ rowIndex, rowData, error: rowError }) => ({
363
+ file: `${dirPath}/${tableName}.jsonl`,
364
+ rowIndex,
365
+ issues: rowError.issues,
366
+ data: rowData,
367
+ originalData: rowsToMigrate[rowIndex],
368
+ }),
369
+ );
370
+
371
+ const formatted = formatter.formatValidationErrors(errorInfos);
372
+ console.error(formatted);
274
373
  }
374
+ } else if (error instanceof Error) {
375
+ console.error(` ${error.message}`);
376
+ }
275
377
 
276
- const formatter = new ErrorFormatter({ verbose: options.verbose });
277
- console.error(formatter.formatMigrationFailureHeader());
278
-
279
- // Display detailed error information
280
- if (error instanceof Error && error.name === 'ValidationError') {
281
- const validationError = error as ValidationError & {
282
- validationErrors?: Array<{
283
- rowIndex: number;
284
- rowData: unknown;
285
- pkValue: unknown;
286
- error: ValidationError;
287
- }>;
288
- };
378
+ console.error('');
379
+ }
380
+ }
289
381
 
290
- // Display all validation errors
291
- if (validationError.validationErrors) {
292
- console.error(
293
- `\nFound ${validationError.validationErrors.length} validation error(s) in transformed data:\n`,
294
- );
295
-
296
- const errorInfos = validationError.validationErrors.map(
297
- ({ rowIndex, rowData, error: rowError }) => ({
298
- file: filePath,
299
- rowIndex,
300
- issues: rowError.issues,
301
- data: rowData,
302
- originalData: rowsToMigrate[rowIndex],
303
- }),
304
- );
305
-
306
- const formatted = formatter.formatValidationErrors(errorInfos);
307
- console.error(formatted);
308
- } else {
309
- // Fallback for single validation error (backward compatibility)
310
- console.error('\nValidation error:\n');
311
- const errorInfo = {
312
- file: filePath,
313
- rowIndex: 0,
314
- issues: validationError.issues,
315
- };
316
- const formatted = formatter.formatValidationErrors([errorInfo]);
317
- console.error(formatted);
318
- }
319
- } else if (error instanceof Error) {
320
- console.error(`\n ${error.message}`);
382
+ await db.close();
321
383
 
322
- // Output stack trace for debugging
323
- if (options.verbose && error.stack) {
324
- console.error(`\nStack trace:\n${error.stack}`);
325
- }
384
+ if (hasErrors) {
385
+ console.error(styleText('red', `\n✗ Migration completed with errors for some tables`));
386
+ console.log(`Total rows migrated: ${totalRowsMigrated}`);
387
+ process.exit(1);
388
+ } else {
389
+ console.log(styleText('green', `\n✓ Migration completed successfully for all tables`));
390
+ console.log(`Total rows migrated: ${totalRowsMigrated}`);
391
+ process.exit(0);
392
+ }
393
+ } catch (error) {
394
+ await db.close();
395
+ throw error;
396
+ }
397
+ }
326
398
 
327
- // Check if it's a SQLite constraint error
328
- if (
329
- error.message.includes('UNIQUE constraint failed') ||
330
- error.message.includes('FOREIGN KEY constraint failed') ||
331
- error.message.includes('NOT NULL constraint failed') ||
332
- error.message.includes('CHECK constraint failed')
333
- ) {
334
- console.error('\n This is a SQLite constraint violation.');
335
- console.error(' Please check your data and schema requirements.');
336
- }
399
+ /**
400
+ * Migrate a single JSONL file
401
+ */
402
+ async function migrateFile(
403
+ filePath: string,
404
+ transformStr: string,
405
+ options: { filter?: string; errorOutput?: string; verbose: boolean },
406
+ ) {
407
+ // Extract table name from file path
408
+ const fileName = filePath.split('/').pop() || '';
409
+ const tableName = fileName.replace('.jsonl', '');
410
+
411
+ if (!tableName) {
412
+ console.error('Error: Invalid file path. Must be a .jsonl file');
413
+ process.exit(1);
414
+ }
415
+
416
+ // Get directory from file path
417
+ const lastSlashIndex = filePath.lastIndexOf('/');
418
+ const dataDir = lastSlashIndex > 0 ? filePath.substring(0, lastSlashIndex) : '.';
419
+
420
+ // Parse transform function first (before initialization)
421
+ let transform: (row: JsonObject) => JsonObject;
422
+ try {
423
+ const parsedTransform = runInSandbox<unknown>(`(${transformStr})`);
424
+ if (typeof parsedTransform !== 'function') {
425
+ console.error('Error: Transform must be a function');
426
+ process.exit(1);
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
+ }
464
+
465
+ try {
466
+ // Parse filter if provided
467
+ let filter: unknown = undefined;
468
+ if (options.filter) {
469
+ try {
470
+ // Try JSON parse first
471
+ filter = JSON.parse(options.filter);
472
+ } catch {
473
+ // Fall back to eval for JavaScript expressions
474
+ filter = runInSandbox(`(${options.filter})`);
475
+ }
476
+ }
477
+
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
+ }
492
+
493
+ console.log(`Found ${rowsToMigrate.length} row(s) to migrate in table '${tableName}'`);
494
+
495
+ if (rowsToMigrate.length === 0) {
496
+ console.log('No rows to migrate. Exiting.');
497
+ await db.close();
498
+ process.exit(0);
499
+ }
500
+
501
+ // Apply transformation
502
+ const transformedRows = rowsToMigrate.map((row) => transform(row as JsonObject));
503
+
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
+ });
510
+ });
511
+
512
+ await db.close();
513
+
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();
519
+
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
+ }
539
+ }
540
+
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
+ };
554
+
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);
337
573
  } else {
338
- console.error(`\n ${String(error)}`);
574
+ // Fallback for single validation error (backward compatibility)
575
+ console.error('\nValidation error:\n');
576
+ const errorInfo = {
577
+ file: filePath,
578
+ rowIndex: 0,
579
+ issues: validationError.issues,
580
+ };
581
+ const formatted = formatter.formatValidationErrors([errorInfo]);
582
+ console.error(formatted);
339
583
  }
584
+ } else if (error instanceof Error) {
585
+ console.error(`\n ${error.message}`);
340
586
 
341
- console.error('');
342
- process.exit(1);
587
+ // Output stack trace for debugging
588
+ if (options.verbose && error.stack) {
589
+ console.error(`\nStack trace:\n${error.stack}`);
590
+ }
591
+
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)}`);
343
604
  }
605
+
606
+ console.error('');
607
+ process.exit(1);
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}'`);
615
+
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);
344
622
  } catch (error) {
345
623
  await db.close();
346
- throw error;
624
+ console.error('Error: Failed to sync changes to file');
625
+ console.error(` ${error instanceof Error ? error.message : String(error)}`);
626
+ process.exit(1);
347
627
  }
348
- },
349
- );
628
+ }
629
+ } catch (error) {
630
+ await db.close();
631
+ throw error;
632
+ }
633
+ }
350
634
 
351
635
  program.parse();