@toiroakr/lines-db 0.4.0 → 0.5.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 +20 -0
- package/bin/cli.js +241 -32
- package/dist/index.cjs +136 -30
- package/dist/index.d.cts +22 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +22 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +136 -30
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +336 -152
- package/src/validator.test.ts +140 -0
- package/src/validator.ts +259 -42
package/src/cli.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { ErrorFormatter } from './error-formatter.js';
|
|
|
11
11
|
import type { ValidationError } from './types.js';
|
|
12
12
|
import { Command } from 'commander';
|
|
13
13
|
import { styleText } from 'node:util';
|
|
14
|
-
import { writeFile } from 'node:fs/promises';
|
|
14
|
+
import { writeFile, stat, readdir } from 'node:fs/promises';
|
|
15
15
|
import { runInNewContext } from 'node:vm';
|
|
16
16
|
|
|
17
17
|
const originalEmitWarning = process.emitWarning;
|
|
@@ -161,191 +161,375 @@ program
|
|
|
161
161
|
program
|
|
162
162
|
.command('migrate')
|
|
163
163
|
.description('Migrate data with transformation function')
|
|
164
|
-
.argument('<
|
|
164
|
+
.argument('<path>', 'File or directory path to migrate')
|
|
165
165
|
.argument('<transform>', 'Transform function (e.g., "(row) => ({ ...row, age: row.age + 1 })")')
|
|
166
166
|
.option('-f, --filter <expr>', 'Filter expression')
|
|
167
167
|
.option('-e, --errorOutput <path>', 'Output file path for transformed data when migration fails')
|
|
168
168
|
.option('-v, --verbose', 'Show verbose error output', false)
|
|
169
169
|
.action(
|
|
170
170
|
async (
|
|
171
|
-
|
|
171
|
+
path: string,
|
|
172
172
|
transformStr: string,
|
|
173
173
|
options: { filter?: string; errorOutput?: string; verbose: boolean },
|
|
174
174
|
) => {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
175
|
+
try {
|
|
176
|
+
const stats = await stat(path);
|
|
177
|
+
|
|
178
|
+
if (stats.isDirectory()) {
|
|
179
|
+
// Migrate all JSONL files in directory
|
|
180
|
+
await migrateDirectory(path, transformStr, options);
|
|
181
|
+
} else if (stats.isFile() && path.endsWith('.jsonl')) {
|
|
182
|
+
// Migrate single file
|
|
183
|
+
await migrateFile(path, transformStr, options);
|
|
184
|
+
} else {
|
|
185
|
+
console.error(`Error: Invalid path: ${path}. Must be a directory or .jsonl file.`);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (
|
|
190
|
+
error instanceof Error &&
|
|
191
|
+
'code' in error &&
|
|
192
|
+
(error as NodeJS.ErrnoException).code === 'ENOENT'
|
|
193
|
+
) {
|
|
194
|
+
console.error(`Error: Path not found: ${path}`);
|
|
195
|
+
} else {
|
|
196
|
+
console.error(`Error: ${String(error)}`);
|
|
197
|
+
}
|
|
181
198
|
process.exit(1);
|
|
182
199
|
}
|
|
200
|
+
},
|
|
201
|
+
);
|
|
183
202
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
203
|
+
/**
|
|
204
|
+
* Migrate all JSONL files in a directory
|
|
205
|
+
*/
|
|
206
|
+
async function migrateDirectory(
|
|
207
|
+
dirPath: string,
|
|
208
|
+
transformStr: string,
|
|
209
|
+
options: { filter?: string; errorOutput?: string; verbose: boolean },
|
|
210
|
+
) {
|
|
211
|
+
// Find all JSONL files in directory
|
|
212
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
213
|
+
const jsonlFiles = entries
|
|
214
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.jsonl'))
|
|
215
|
+
.map((entry) => entry.name);
|
|
216
|
+
|
|
217
|
+
if (jsonlFiles.length === 0) {
|
|
218
|
+
console.error(`Error: No JSONL files found in directory: ${dirPath}`);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
console.log(`Found ${jsonlFiles.length} JSONL file(s) in directory`);
|
|
223
|
+
|
|
224
|
+
// Initialize database
|
|
225
|
+
const db = LinesDB.create({ dataDir: dirPath });
|
|
226
|
+
await db.initialize();
|
|
227
|
+
|
|
228
|
+
const tableNames = db.getTableNames();
|
|
229
|
+
if (tableNames.length === 0) {
|
|
230
|
+
console.error(`Error: No tables could be loaded from directory: ${dirPath}`);
|
|
231
|
+
await db.close();
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
console.log(`Loaded ${tableNames.length} table(s): ${tableNames.join(', ')}\n`);
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
// Parse transform function
|
|
239
|
+
const transform = runInSandbox<unknown>(`(${transformStr})`);
|
|
240
|
+
|
|
241
|
+
if (typeof transform !== 'function') {
|
|
242
|
+
console.error('Error: Transform must be a function');
|
|
243
|
+
await db.close();
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
191
246
|
|
|
247
|
+
// Parse filter if provided
|
|
248
|
+
let filter: unknown = undefined;
|
|
249
|
+
if (options.filter) {
|
|
192
250
|
try {
|
|
193
|
-
|
|
194
|
-
|
|
251
|
+
filter = JSON.parse(options.filter);
|
|
252
|
+
} catch {
|
|
253
|
+
filter = runInSandbox(`(${options.filter})`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
195
256
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
process.exit(1);
|
|
199
|
-
}
|
|
257
|
+
let totalRowsMigrated = 0;
|
|
258
|
+
let hasErrors = false;
|
|
200
259
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
}
|
|
260
|
+
// Process each table
|
|
261
|
+
for (const tableName of tableNames) {
|
|
262
|
+
try {
|
|
263
|
+
console.log(`Processing table '${tableName}'...`);
|
|
212
264
|
|
|
213
265
|
// Get rows to migrate
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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}'`);
|
|
266
|
+
const rowsToMigrate = filter
|
|
267
|
+
? db.find(tableName, filter as Parameters<typeof db.find>[1])
|
|
268
|
+
: db.find(tableName);
|
|
229
269
|
|
|
230
270
|
if (rowsToMigrate.length === 0) {
|
|
231
|
-
console.log(
|
|
232
|
-
|
|
233
|
-
process.exit(0);
|
|
271
|
+
console.log(` No rows to migrate`);
|
|
272
|
+
continue;
|
|
234
273
|
}
|
|
235
274
|
|
|
275
|
+
console.log(` Found ${rowsToMigrate.length} row(s) to migrate`);
|
|
276
|
+
|
|
236
277
|
// Apply transformation
|
|
237
278
|
const transformedRows = rowsToMigrate.map((row) => transform(row));
|
|
238
279
|
|
|
239
280
|
// Perform the migration in a transaction
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
validate: true,
|
|
244
|
-
});
|
|
281
|
+
await db.transaction(async () => {
|
|
282
|
+
db.batchUpdate(tableName, transformedRows as Parameters<typeof db.batchUpdate>[1], {
|
|
283
|
+
validate: true,
|
|
245
284
|
});
|
|
285
|
+
});
|
|
246
286
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
}
|
|
274
|
-
}
|
|
287
|
+
console.log(` ✓ ${rowsToMigrate.length} row(s) updated\n`);
|
|
288
|
+
totalRowsMigrated += rowsToMigrate.length;
|
|
289
|
+
} catch (error) {
|
|
290
|
+
hasErrors = true;
|
|
291
|
+
console.error(styleText('red', ` ✗ Failed to migrate table '${tableName}'`));
|
|
275
292
|
|
|
276
|
-
|
|
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
|
-
};
|
|
289
|
-
|
|
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}`);
|
|
293
|
+
const formatter = new ErrorFormatter({ verbose: options.verbose });
|
|
321
294
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
295
|
+
if (error instanceof Error && error.name === 'ValidationError') {
|
|
296
|
+
const validationError = error as ValidationError & {
|
|
297
|
+
validationErrors?: Array<{
|
|
298
|
+
rowIndex: number;
|
|
299
|
+
rowData: unknown;
|
|
300
|
+
pkValue: unknown;
|
|
301
|
+
error: ValidationError;
|
|
302
|
+
}>;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
if (validationError.validationErrors) {
|
|
306
|
+
console.error(
|
|
307
|
+
` Found ${validationError.validationErrors.length} validation error(s):\n`,
|
|
308
|
+
);
|
|
326
309
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
310
|
+
const rowsToMigrate = filter
|
|
311
|
+
? db.find(tableName, filter as Parameters<typeof db.find>[1])
|
|
312
|
+
: db.find(tableName);
|
|
313
|
+
|
|
314
|
+
const errorInfos = validationError.validationErrors.map(
|
|
315
|
+
({ rowIndex, rowData, error: rowError }) => ({
|
|
316
|
+
file: `${dirPath}/${tableName}.jsonl`,
|
|
317
|
+
rowIndex,
|
|
318
|
+
issues: rowError.issues,
|
|
319
|
+
data: rowData,
|
|
320
|
+
originalData: rowsToMigrate[rowIndex],
|
|
321
|
+
}),
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
const formatted = formatter.formatValidationErrors(errorInfos);
|
|
325
|
+
console.error(formatted);
|
|
339
326
|
}
|
|
327
|
+
} else if (error instanceof Error) {
|
|
328
|
+
console.error(` ${error.message}`);
|
|
329
|
+
}
|
|
340
330
|
|
|
341
|
-
|
|
342
|
-
|
|
331
|
+
console.error('');
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
await db.close();
|
|
336
|
+
|
|
337
|
+
if (hasErrors) {
|
|
338
|
+
console.error(styleText('red', `\n✗ Migration completed with errors for some tables`));
|
|
339
|
+
console.log(`Total rows migrated: ${totalRowsMigrated}`);
|
|
340
|
+
process.exit(1);
|
|
341
|
+
} else {
|
|
342
|
+
console.log(styleText('green', `\n✓ Migration completed successfully for all tables`));
|
|
343
|
+
console.log(`Total rows migrated: ${totalRowsMigrated}`);
|
|
344
|
+
process.exit(0);
|
|
345
|
+
}
|
|
346
|
+
} catch (error) {
|
|
347
|
+
await db.close();
|
|
348
|
+
throw error;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Migrate a single JSONL file
|
|
354
|
+
*/
|
|
355
|
+
async function migrateFile(
|
|
356
|
+
filePath: string,
|
|
357
|
+
transformStr: string,
|
|
358
|
+
options: { filter?: string; errorOutput?: string; verbose: boolean },
|
|
359
|
+
) {
|
|
360
|
+
// Extract table name from file path
|
|
361
|
+
const fileName = filePath.split('/').pop() || '';
|
|
362
|
+
const tableName = fileName.replace('.jsonl', '');
|
|
363
|
+
|
|
364
|
+
if (!tableName) {
|
|
365
|
+
console.error('Error: Invalid file path. Must be a .jsonl file');
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Get directory from file path
|
|
370
|
+
const lastSlashIndex = filePath.lastIndexOf('/');
|
|
371
|
+
const dataDir = lastSlashIndex > 0 ? filePath.substring(0, lastSlashIndex) : '.';
|
|
372
|
+
|
|
373
|
+
// Initialize database
|
|
374
|
+
const db = LinesDB.create({ dataDir });
|
|
375
|
+
await db.initialize();
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
// Parse transform function
|
|
379
|
+
const transform = runInSandbox<unknown>(`(${transformStr})`);
|
|
380
|
+
|
|
381
|
+
if (typeof transform !== 'function') {
|
|
382
|
+
console.error('Error: Transform must be a function');
|
|
383
|
+
process.exit(1);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Parse filter if provided
|
|
387
|
+
let filter: unknown = undefined;
|
|
388
|
+
if (options.filter) {
|
|
389
|
+
try {
|
|
390
|
+
// Try JSON parse first
|
|
391
|
+
filter = JSON.parse(options.filter);
|
|
392
|
+
} catch {
|
|
393
|
+
// Fall back to eval for JavaScript expressions
|
|
394
|
+
filter = runInSandbox(`(${options.filter})`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Get rows to migrate
|
|
399
|
+
let rowsToMigrate;
|
|
400
|
+
try {
|
|
401
|
+
rowsToMigrate = filter
|
|
402
|
+
? db.find(tableName, filter as Parameters<typeof db.find>[1])
|
|
403
|
+
: db.find(tableName);
|
|
404
|
+
} catch (error) {
|
|
405
|
+
console.error(`Error: Failed to access table '${tableName}'`);
|
|
406
|
+
console.error(` ${error instanceof Error ? error.message : String(error)}`);
|
|
407
|
+
console.error(`\nThe table may have failed to load during initialization.`);
|
|
408
|
+
console.error(`Check the table's data and schema for any constraint violations.`);
|
|
409
|
+
await db.close();
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
console.log(`Found ${rowsToMigrate.length} row(s) to migrate in table '${tableName}'`);
|
|
414
|
+
|
|
415
|
+
if (rowsToMigrate.length === 0) {
|
|
416
|
+
console.log('No rows to migrate. Exiting.');
|
|
417
|
+
await db.close();
|
|
418
|
+
process.exit(0);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Apply transformation
|
|
422
|
+
const transformedRows = rowsToMigrate.map((row) => transform(row));
|
|
423
|
+
|
|
424
|
+
// Perform the migration in a transaction
|
|
425
|
+
try {
|
|
426
|
+
await db.transaction(async () => {
|
|
427
|
+
db.batchUpdate(tableName, transformedRows as Parameters<typeof db.batchUpdate>[1], {
|
|
428
|
+
validate: true,
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
await db.close();
|
|
433
|
+
|
|
434
|
+
console.log(`\nMigration completed successfully:`);
|
|
435
|
+
console.log(` ✓ ${rowsToMigrate.length} row(s) updated`);
|
|
436
|
+
process.exit(0);
|
|
437
|
+
} catch (error) {
|
|
438
|
+
await db.close();
|
|
439
|
+
|
|
440
|
+
// Write transformed data to error output file if --errorOutput is specified
|
|
441
|
+
if (options.errorOutput) {
|
|
442
|
+
try {
|
|
443
|
+
const jsonlContent = transformedRows.map((row) => JSON.stringify(row)).join('\n');
|
|
444
|
+
await writeFile(options.errorOutput, jsonlContent, 'utf-8');
|
|
445
|
+
console.error(
|
|
446
|
+
styleText(
|
|
447
|
+
'yellow',
|
|
448
|
+
`\n⚠ Transformed data (${transformedRows.length} rows) written to: ${options.errorOutput}`,
|
|
449
|
+
),
|
|
450
|
+
);
|
|
451
|
+
} catch (writeError) {
|
|
452
|
+
console.error(
|
|
453
|
+
styleText(
|
|
454
|
+
'red',
|
|
455
|
+
`\n✗ Failed to write error output file: ${writeError instanceof Error ? writeError.message : String(writeError)}`,
|
|
456
|
+
),
|
|
457
|
+
);
|
|
343
458
|
}
|
|
344
|
-
} catch (error) {
|
|
345
|
-
await db.close();
|
|
346
|
-
throw error;
|
|
347
459
|
}
|
|
348
|
-
|
|
349
|
-
|
|
460
|
+
|
|
461
|
+
const formatter = new ErrorFormatter({ verbose: options.verbose });
|
|
462
|
+
console.error(formatter.formatMigrationFailureHeader());
|
|
463
|
+
|
|
464
|
+
// Display detailed error information
|
|
465
|
+
if (error instanceof Error && error.name === 'ValidationError') {
|
|
466
|
+
const validationError = error as ValidationError & {
|
|
467
|
+
validationErrors?: Array<{
|
|
468
|
+
rowIndex: number;
|
|
469
|
+
rowData: unknown;
|
|
470
|
+
pkValue: unknown;
|
|
471
|
+
error: ValidationError;
|
|
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
|
+
);
|
|
480
|
+
|
|
481
|
+
const errorInfos = validationError.validationErrors.map(
|
|
482
|
+
({ rowIndex, rowData, error: rowError }) => ({
|
|
483
|
+
file: filePath,
|
|
484
|
+
rowIndex,
|
|
485
|
+
issues: rowError.issues,
|
|
486
|
+
data: rowData,
|
|
487
|
+
originalData: rowsToMigrate[rowIndex],
|
|
488
|
+
}),
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
const formatted = formatter.formatValidationErrors(errorInfos);
|
|
492
|
+
console.error(formatted);
|
|
493
|
+
} else {
|
|
494
|
+
// Fallback for single validation error (backward compatibility)
|
|
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}`);
|
|
506
|
+
|
|
507
|
+
// Output stack trace for debugging
|
|
508
|
+
if (options.verbose && error.stack) {
|
|
509
|
+
console.error(`\nStack trace:\n${error.stack}`);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Check if it's a SQLite constraint error
|
|
513
|
+
if (
|
|
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)}`);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
console.error('');
|
|
527
|
+
process.exit(1);
|
|
528
|
+
}
|
|
529
|
+
} catch (error) {
|
|
530
|
+
await db.close();
|
|
531
|
+
throw error;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
350
534
|
|
|
351
535
|
program.parse();
|
package/src/validator.test.ts
CHANGED
|
@@ -364,4 +364,144 @@ describe('Validator', () => {
|
|
|
364
364
|
);
|
|
365
365
|
});
|
|
366
366
|
});
|
|
367
|
+
|
|
368
|
+
describe('constraint validation', () => {
|
|
369
|
+
it('should detect primary key constraint violations', async () => {
|
|
370
|
+
const jsonlPath = join(testDir, 'users.jsonl');
|
|
371
|
+
const schemaPath = join(testDir, 'users.schema.ts');
|
|
372
|
+
|
|
373
|
+
// Write data with duplicate id
|
|
374
|
+
await writeFile(
|
|
375
|
+
jsonlPath,
|
|
376
|
+
'{"id":"1","name":"Alice","email":"alice@example.com"}\n{"id":"1","name":"Bob","email":"bob@example.com"}\n',
|
|
377
|
+
);
|
|
378
|
+
await writeFile(
|
|
379
|
+
schemaPath,
|
|
380
|
+
`
|
|
381
|
+
export const schema = {
|
|
382
|
+
'~standard': {
|
|
383
|
+
version: 1,
|
|
384
|
+
vendor: 'test',
|
|
385
|
+
validate: (data) => ({ value: data, issues: [] })
|
|
386
|
+
},
|
|
387
|
+
primaryKey: 'id'
|
|
388
|
+
};
|
|
389
|
+
`,
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
const validator = new Validator({ path: jsonlPath });
|
|
393
|
+
const result = await validator.validate();
|
|
394
|
+
|
|
395
|
+
expect(result.valid).toBe(false);
|
|
396
|
+
expect(result.errors).toHaveLength(1);
|
|
397
|
+
expect(result.errors[0].rowIndex).toBe(1);
|
|
398
|
+
expect(result.errors[0].issues[0].message).toContain('UNIQUE constraint failed');
|
|
399
|
+
expect(result.errors[0].issues[0].message).toContain('id');
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('should detect unique index constraint violations', async () => {
|
|
403
|
+
const jsonlPath = join(testDir, 'users.jsonl');
|
|
404
|
+
const schemaPath = join(testDir, 'users.schema.ts');
|
|
405
|
+
|
|
406
|
+
// Write data with duplicate email
|
|
407
|
+
await writeFile(
|
|
408
|
+
jsonlPath,
|
|
409
|
+
'{"id":"1","name":"Alice","email":"alice@example.com"}\n{"id":"2","name":"Bob","email":"alice@example.com"}\n',
|
|
410
|
+
);
|
|
411
|
+
await writeFile(
|
|
412
|
+
schemaPath,
|
|
413
|
+
`
|
|
414
|
+
export const schema = {
|
|
415
|
+
'~standard': {
|
|
416
|
+
version: 1,
|
|
417
|
+
vendor: 'test',
|
|
418
|
+
validate: (data) => ({ value: data, issues: [] })
|
|
419
|
+
},
|
|
420
|
+
primaryKey: 'id',
|
|
421
|
+
indexes: [
|
|
422
|
+
{ name: 'users_email_unique', columns: ['email'], unique: true }
|
|
423
|
+
]
|
|
424
|
+
};
|
|
425
|
+
`,
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
const validator = new Validator({ path: jsonlPath });
|
|
429
|
+
const result = await validator.validate();
|
|
430
|
+
|
|
431
|
+
expect(result.valid).toBe(false);
|
|
432
|
+
expect(result.errors).toHaveLength(1);
|
|
433
|
+
expect(result.errors[0].rowIndex).toBe(1);
|
|
434
|
+
expect(result.errors[0].issues[0].message).toContain('UNIQUE constraint failed');
|
|
435
|
+
expect(result.errors[0].issues[0].message).toContain('email');
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('should use id column as primary key when primaryKey is not specified', async () => {
|
|
439
|
+
const jsonlPath = join(testDir, 'users.jsonl');
|
|
440
|
+
const schemaPath = join(testDir, 'users.schema.ts');
|
|
441
|
+
|
|
442
|
+
// Write data with duplicate id (no primaryKey specified in schema)
|
|
443
|
+
await writeFile(jsonlPath, '{"id":"1","name":"Alice"}\n{"id":"1","name":"Bob"}\n');
|
|
444
|
+
await writeFile(
|
|
445
|
+
schemaPath,
|
|
446
|
+
`
|
|
447
|
+
export const schema = {
|
|
448
|
+
'~standard': {
|
|
449
|
+
version: 1,
|
|
450
|
+
vendor: 'test',
|
|
451
|
+
validate: (data) => ({ value: data, issues: [] })
|
|
452
|
+
}
|
|
453
|
+
// Note: no primaryKey specified
|
|
454
|
+
};
|
|
455
|
+
`,
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
const validator = new Validator({ path: jsonlPath });
|
|
459
|
+
const result = await validator.validate();
|
|
460
|
+
|
|
461
|
+
expect(result.valid).toBe(false);
|
|
462
|
+
expect(result.errors).toHaveLength(1);
|
|
463
|
+
expect(result.errors[0].rowIndex).toBe(1);
|
|
464
|
+
expect(result.errors[0].issues[0].message).toContain('UNIQUE constraint failed');
|
|
465
|
+
expect(result.errors[0].issues[0].message).toContain('id');
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it('should detect multiple constraint violations in single file', async () => {
|
|
469
|
+
const jsonlPath = join(testDir, 'users.jsonl');
|
|
470
|
+
const schemaPath = join(testDir, 'users.schema.ts');
|
|
471
|
+
|
|
472
|
+
// Write data with both duplicate id and duplicate email
|
|
473
|
+
await writeFile(
|
|
474
|
+
jsonlPath,
|
|
475
|
+
'{"id":"1","name":"Alice","email":"alice@example.com"}\n{"id":"1","name":"Bob","email":"bob@example.com"}\n{"id":"2","name":"Charlie","email":"alice@example.com"}\n',
|
|
476
|
+
);
|
|
477
|
+
await writeFile(
|
|
478
|
+
schemaPath,
|
|
479
|
+
`
|
|
480
|
+
export const schema = {
|
|
481
|
+
'~standard': {
|
|
482
|
+
version: 1,
|
|
483
|
+
vendor: 'test',
|
|
484
|
+
validate: (data) => ({ value: data, issues: [] })
|
|
485
|
+
},
|
|
486
|
+
primaryKey: 'id',
|
|
487
|
+
indexes: [
|
|
488
|
+
{ name: 'users_email_unique', columns: ['email'], unique: true }
|
|
489
|
+
]
|
|
490
|
+
};
|
|
491
|
+
`,
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
const validator = new Validator({ path: jsonlPath });
|
|
495
|
+
const result = await validator.validate();
|
|
496
|
+
|
|
497
|
+
expect(result.valid).toBe(false);
|
|
498
|
+
expect(result.errors).toHaveLength(2);
|
|
499
|
+
// First error: duplicate id
|
|
500
|
+
expect(result.errors[0].rowIndex).toBe(1);
|
|
501
|
+
expect(result.errors[0].issues[0].message).toContain('id');
|
|
502
|
+
// Second error: duplicate email
|
|
503
|
+
expect(result.errors[1].rowIndex).toBe(2);
|
|
504
|
+
expect(result.errors[1].issues[0].message).toContain('email');
|
|
505
|
+
});
|
|
506
|
+
});
|
|
367
507
|
});
|