@toiroakr/lines-db 0.3.0 → 0.4.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 +22 -0
- package/bin/cli.js +250 -134
- package/dist/index.cjs +169 -53
- package/dist/index.d.cts +31 -10
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +31 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +169 -53
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/database.ts +32 -15
- package/src/schema.ts +6 -6
- package/src/types.ts +2 -2
- package/src/validator.test.ts +140 -0
- package/src/validator.ts +272 -57
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @toiroakr/lines-db
|
|
2
2
|
|
|
3
|
+
## 0.4.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- b281dc8: Fix constraint validation in validator to properly detect primary key and unique index violations
|
|
8
|
+
|
|
9
|
+
Previously, the validator was not creating indexes from schema metadata and was missing the default primaryKey behavior, causing constraint violations to go undetected. This fix ensures:
|
|
10
|
+
- Indexes (both unique and non-unique) are now properly created from schema metadata in the validation database
|
|
11
|
+
- Primary key defaults to 'id' column when not explicitly specified, matching database.ts behavior
|
|
12
|
+
- Constraint violations are properly detected by inserting rows into an in-memory database and catching SQLite exceptions
|
|
13
|
+
- Detailed error information is extracted from SQLite error messages for better diagnostics
|
|
14
|
+
|
|
15
|
+
Added comprehensive regression tests to prevent this issue from recurring.
|
|
16
|
+
|
|
17
|
+
## 0.4.0
|
|
18
|
+
|
|
19
|
+
### Minor Changes
|
|
20
|
+
|
|
21
|
+
- a662484: - Allow flexible schema export methods (support loading from `schema` or `default` exports)
|
|
22
|
+
- Enhance constraint validation by loading data into an actual database (catches unique, primary key, and foreign key violations)
|
|
23
|
+
- Add fallback logic to automatically use `id` column as primary key when it exists and no primary key is explicitly defined
|
|
24
|
+
|
|
3
25
|
## 0.3.0
|
|
4
26
|
|
|
5
27
|
### Minor Changes
|
package/bin/cli.js
CHANGED
|
@@ -248,133 +248,6 @@ var SchemaLoader = class {
|
|
|
248
248
|
}
|
|
249
249
|
};
|
|
250
250
|
|
|
251
|
-
//#endregion
|
|
252
|
-
//#region src/validator.ts
|
|
253
|
-
var Validator = class {
|
|
254
|
-
path;
|
|
255
|
-
projectRoot;
|
|
256
|
-
constructor(options) {
|
|
257
|
-
this.path = options.path;
|
|
258
|
-
this.projectRoot = options.projectRoot || process.cwd();
|
|
259
|
-
}
|
|
260
|
-
/**
|
|
261
|
-
* Validate JSONL file(s)
|
|
262
|
-
*/
|
|
263
|
-
async validate() {
|
|
264
|
-
const fullPath = this.path.startsWith("/") ? this.path : join(this.projectRoot, this.path);
|
|
265
|
-
const stats = await stat(fullPath);
|
|
266
|
-
if (stats.isDirectory()) return this.validateDirectory(fullPath);
|
|
267
|
-
else if (stats.isFile() && fullPath.endsWith(".jsonl")) return this.validateFile(fullPath);
|
|
268
|
-
else throw new Error(`Invalid path: ${this.path}. Must be a directory or .jsonl file.`);
|
|
269
|
-
}
|
|
270
|
-
/**
|
|
271
|
-
* Validate all JSONL files in a directory
|
|
272
|
-
*/
|
|
273
|
-
async validateDirectory(dirPath) {
|
|
274
|
-
const jsonlFiles = (await readdir(dirPath, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).map((entry) => join(dirPath, entry.name));
|
|
275
|
-
if (jsonlFiles.length === 0) throw new Error(`No JSONL files found in directory: ${dirPath}`);
|
|
276
|
-
const allErrors = [];
|
|
277
|
-
const allWarnings = [];
|
|
278
|
-
const filesWithSchema = [];
|
|
279
|
-
for (const file of jsonlFiles) if (await SchemaLoader.hasSchema(file)) filesWithSchema.push(file);
|
|
280
|
-
else {
|
|
281
|
-
const tableName = basename(file, ".jsonl");
|
|
282
|
-
allWarnings.push(`Skipping validation for '${tableName}': schema file not found`);
|
|
283
|
-
}
|
|
284
|
-
for (const file of filesWithSchema) {
|
|
285
|
-
const result = await this.validateFile(file);
|
|
286
|
-
allErrors.push(...result.errors);
|
|
287
|
-
allWarnings.push(...result.warnings);
|
|
288
|
-
}
|
|
289
|
-
if (filesWithSchema.length > 0) {
|
|
290
|
-
const fkErrors = await this.validateForeignKeys(dirPath, filesWithSchema);
|
|
291
|
-
allErrors.push(...fkErrors);
|
|
292
|
-
}
|
|
293
|
-
return {
|
|
294
|
-
valid: allErrors.length === 0,
|
|
295
|
-
errors: allErrors,
|
|
296
|
-
warnings: allWarnings
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* Validate foreign key constraints across all tables
|
|
301
|
-
*/
|
|
302
|
-
async validateForeignKeys(dirPath, jsonlFiles) {
|
|
303
|
-
const errors = [];
|
|
304
|
-
const tableData = /* @__PURE__ */ new Map();
|
|
305
|
-
const tableSchemas = /* @__PURE__ */ new Map();
|
|
306
|
-
for (const file of jsonlFiles) {
|
|
307
|
-
const tableName = basename(file, ".jsonl");
|
|
308
|
-
const data = await JsonlReader.read(file);
|
|
309
|
-
const schema = await SchemaLoader.loadSchema(file);
|
|
310
|
-
tableData.set(tableName, data);
|
|
311
|
-
tableSchemas.set(tableName, schema);
|
|
312
|
-
}
|
|
313
|
-
for (const file of jsonlFiles) {
|
|
314
|
-
const tableName = basename(file, ".jsonl");
|
|
315
|
-
const schema = tableSchemas.get(tableName);
|
|
316
|
-
const data = tableData.get(tableName);
|
|
317
|
-
if (!schema || !data || !schema.foreignKeys) continue;
|
|
318
|
-
for (const fk of schema.foreignKeys) {
|
|
319
|
-
const referencedTable = fk.references.table;
|
|
320
|
-
const referencedData = tableData.get(referencedTable);
|
|
321
|
-
if (!referencedData) continue;
|
|
322
|
-
const referencedValues = /* @__PURE__ */ new Set();
|
|
323
|
-
for (const refRow of referencedData) {
|
|
324
|
-
const keyValues = fk.references.columns.map((col) => refRow[col]);
|
|
325
|
-
const compositeKey = JSON.stringify(keyValues);
|
|
326
|
-
referencedValues.add(compositeKey);
|
|
327
|
-
}
|
|
328
|
-
for (let i = 0; i < data.length; i++) {
|
|
329
|
-
const row = data[i];
|
|
330
|
-
const foreignKeyValues = fk.columns.map((col) => row[col]);
|
|
331
|
-
const compositeKey = JSON.stringify(foreignKeyValues);
|
|
332
|
-
if (!referencedValues.has(compositeKey)) errors.push({
|
|
333
|
-
file,
|
|
334
|
-
tableName,
|
|
335
|
-
rowIndex: i,
|
|
336
|
-
issues: [],
|
|
337
|
-
type: "foreignKey",
|
|
338
|
-
foreignKeyError: {
|
|
339
|
-
column: fk.columns.join(", "),
|
|
340
|
-
value: foreignKeyValues.length === 1 ? foreignKeyValues[0] : foreignKeyValues,
|
|
341
|
-
referencedTable,
|
|
342
|
-
referencedColumn: fk.references.columns.join(", ")
|
|
343
|
-
}
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
return errors;
|
|
349
|
-
}
|
|
350
|
-
/**
|
|
351
|
-
* Validate a single JSONL file
|
|
352
|
-
*/
|
|
353
|
-
async validateFile(filePath) {
|
|
354
|
-
const tableName = basename(filePath, ".jsonl");
|
|
355
|
-
const data = await JsonlReader.read(filePath);
|
|
356
|
-
const schema = await SchemaLoader.loadSchema(filePath);
|
|
357
|
-
const errors = [];
|
|
358
|
-
for (let i = 0; i < data.length; i++) {
|
|
359
|
-
const row = data[i];
|
|
360
|
-
const result = schema["~standard"].validate(row);
|
|
361
|
-
if (result instanceof Promise) throw new Error("Asynchronous validation is not supported.");
|
|
362
|
-
if (result.issues && result.issues.length > 0) errors.push({
|
|
363
|
-
file: filePath,
|
|
364
|
-
tableName,
|
|
365
|
-
rowIndex: i,
|
|
366
|
-
issues: result.issues,
|
|
367
|
-
type: "schema"
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
return {
|
|
371
|
-
valid: errors.length === 0,
|
|
372
|
-
errors,
|
|
373
|
-
warnings: []
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
};
|
|
377
|
-
|
|
378
251
|
//#endregion
|
|
379
252
|
//#region src/runtime.ts
|
|
380
253
|
function detectRuntime() {
|
|
@@ -529,7 +402,8 @@ var LinesDB = class LinesDB {
|
|
|
529
402
|
let foreignKeys;
|
|
530
403
|
try {
|
|
531
404
|
const { pathToFileURL: pathToFileURL$1 } = await import("node:url");
|
|
532
|
-
|
|
405
|
+
const schemaModule = await import(`${pathToFileURL$1(tableConfig.jsonlPath.replace(".jsonl", ".schema.ts")).href}?t=${Date.now()}`);
|
|
406
|
+
foreignKeys = (schemaModule.schema || schemaModule.default)?.foreignKeys || schemaModule.foreignKeys;
|
|
533
407
|
} catch {}
|
|
534
408
|
if (foreignKeys && foreignKeys.length > 0) for (const fk of foreignKeys) {
|
|
535
409
|
const referencedTable = fk.references.table;
|
|
@@ -556,9 +430,13 @@ var LinesDB = class LinesDB {
|
|
|
556
430
|
if (!config.validationSchema) try {
|
|
557
431
|
const { pathToFileURL: pathToFileURL$1 } = await import("node:url");
|
|
558
432
|
const schemaModule = await import(`${pathToFileURL$1(config.jsonlPath.replace(".jsonl", ".schema.ts")).href}?t=${Date.now()}`);
|
|
559
|
-
|
|
560
|
-
if (
|
|
561
|
-
if (schemaModule.
|
|
433
|
+
const schemaExport = schemaModule.schema || schemaModule.default;
|
|
434
|
+
if (schemaExport?.primaryKey) schemaMetadata.primaryKey = schemaExport.primaryKey;
|
|
435
|
+
else if (schemaModule.primaryKey) schemaMetadata.primaryKey = schemaModule.primaryKey;
|
|
436
|
+
if (schemaExport?.foreignKeys) schemaMetadata.foreignKeys = schemaExport.foreignKeys;
|
|
437
|
+
else if (schemaModule.foreignKeys) schemaMetadata.foreignKeys = schemaModule.foreignKeys;
|
|
438
|
+
if (schemaExport?.indexes) schemaMetadata.indexes = schemaExport.indexes;
|
|
439
|
+
else if (schemaModule.indexes) schemaMetadata.indexes = schemaModule.indexes;
|
|
562
440
|
} catch (_error) {}
|
|
563
441
|
this.validationSchemas.set(tableName, validationSchema);
|
|
564
442
|
const validationErrors = [];
|
|
@@ -594,9 +472,12 @@ var LinesDB = class LinesDB {
|
|
|
594
472
|
const primaryKey = biSchema?.primaryKey || schemaMetadata.primaryKey;
|
|
595
473
|
const foreignKeys = biSchema?.foreignKeys || schemaMetadata.foreignKeys;
|
|
596
474
|
const indexes = biSchema?.indexes || schemaMetadata.indexes;
|
|
597
|
-
if (primaryKey && !schema.columns.some((col) => col.primaryKey))
|
|
598
|
-
const col = schema.columns.find((c) => c.name ===
|
|
475
|
+
if (primaryKey && !schema.columns.some((col) => col.primaryKey)) {
|
|
476
|
+
const col = schema.columns.find((c) => c.name === primaryKey);
|
|
599
477
|
if (col) col.primaryKey = true;
|
|
478
|
+
} else if (!primaryKey && !schema.columns.some((col) => col.primaryKey)) {
|
|
479
|
+
const idColumn = schema.columns.find((c) => c.name === "id");
|
|
480
|
+
if (idColumn) idColumn.primaryKey = true;
|
|
600
481
|
}
|
|
601
482
|
if (foreignKeys) schema.foreignKeys = foreignKeys;
|
|
602
483
|
if (indexes) schema.indexes = indexes;
|
|
@@ -620,7 +501,7 @@ var LinesDB = class LinesDB {
|
|
|
620
501
|
});
|
|
621
502
|
const foreignKeyDefs = [];
|
|
622
503
|
if (schema.foreignKeys && schema.foreignKeys.length > 0) for (const fk of schema.foreignKeys) {
|
|
623
|
-
const fkParts = [`FOREIGN KEY (${
|
|
504
|
+
const fkParts = [`FOREIGN KEY (${this.quoteIdentifier(fk.column)})`, `REFERENCES ${this.quoteTableName(fk.references.table)}(${this.quoteIdentifier(fk.references.column)})`];
|
|
624
505
|
if (fk.onDelete) fkParts.push(`ON DELETE ${fk.onDelete}`);
|
|
625
506
|
if (fk.onUpdate) fkParts.push(`ON UPDATE ${fk.onUpdate}`);
|
|
626
507
|
foreignKeyDefs.push(fkParts.join(" "));
|
|
@@ -1111,6 +992,241 @@ var LinesDB = class LinesDB {
|
|
|
1111
992
|
}
|
|
1112
993
|
};
|
|
1113
994
|
|
|
995
|
+
//#endregion
|
|
996
|
+
//#region src/validator.ts
|
|
997
|
+
var Validator = class {
|
|
998
|
+
path;
|
|
999
|
+
projectRoot;
|
|
1000
|
+
constructor(options) {
|
|
1001
|
+
this.path = options.path;
|
|
1002
|
+
this.projectRoot = options.projectRoot || process.cwd();
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Validate JSONL file(s)
|
|
1006
|
+
*/
|
|
1007
|
+
async validate() {
|
|
1008
|
+
const fullPath = this.path.startsWith("/") ? this.path : join(this.projectRoot, this.path);
|
|
1009
|
+
const stats = await stat(fullPath);
|
|
1010
|
+
if (stats.isDirectory()) return this.validateDirectory(fullPath);
|
|
1011
|
+
else if (stats.isFile() && fullPath.endsWith(".jsonl")) return this.validateFile(fullPath);
|
|
1012
|
+
else throw new Error(`Invalid path: ${this.path}. Must be a directory or .jsonl file.`);
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Validate all JSONL files in a directory
|
|
1016
|
+
*/
|
|
1017
|
+
async validateDirectory(dirPath) {
|
|
1018
|
+
const jsonlFiles = (await readdir(dirPath, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).map((entry) => join(dirPath, entry.name));
|
|
1019
|
+
if (jsonlFiles.length === 0) throw new Error(`No JSONL files found in directory: ${dirPath}`);
|
|
1020
|
+
const allErrors = [];
|
|
1021
|
+
const allWarnings = [];
|
|
1022
|
+
const filesWithSchema = [];
|
|
1023
|
+
for (const file of jsonlFiles) if (await SchemaLoader.hasSchema(file)) filesWithSchema.push(file);
|
|
1024
|
+
else {
|
|
1025
|
+
const tableName = basename(file, ".jsonl");
|
|
1026
|
+
allWarnings.push(`Skipping validation for '${tableName}': schema file not found`);
|
|
1027
|
+
}
|
|
1028
|
+
for (const file of filesWithSchema) {
|
|
1029
|
+
const result = await this.validateFile(file);
|
|
1030
|
+
allErrors.push(...result.errors);
|
|
1031
|
+
allWarnings.push(...result.warnings);
|
|
1032
|
+
}
|
|
1033
|
+
if (filesWithSchema.length > 0 && allErrors.length === 0) {
|
|
1034
|
+
const dbErrors = await this.validateWithDatabase(dirPath, filesWithSchema);
|
|
1035
|
+
allErrors.push(...dbErrors);
|
|
1036
|
+
}
|
|
1037
|
+
return {
|
|
1038
|
+
valid: allErrors.length === 0,
|
|
1039
|
+
errors: allErrors,
|
|
1040
|
+
warnings: allWarnings
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Validate by loading data into database one row at a time
|
|
1045
|
+
* This catches constraint violations and extracts detailed error information
|
|
1046
|
+
*/
|
|
1047
|
+
async validateWithDatabase(dirPath, jsonlFiles) {
|
|
1048
|
+
const errors = [];
|
|
1049
|
+
try {
|
|
1050
|
+
const db = LinesDB.create({ dataDir: ":memory:" });
|
|
1051
|
+
for (const file of jsonlFiles) {
|
|
1052
|
+
const tableName = basename(file, ".jsonl");
|
|
1053
|
+
const data = await JsonlReader.read(file);
|
|
1054
|
+
let schema;
|
|
1055
|
+
let foreignKeys = [];
|
|
1056
|
+
let indexes = [];
|
|
1057
|
+
let primaryKey;
|
|
1058
|
+
try {
|
|
1059
|
+
schema = await SchemaLoader.loadSchema(file);
|
|
1060
|
+
const { pathToFileURL: pathToFileURL$1 } = await import("node:url");
|
|
1061
|
+
const schemaModule = await import(`${pathToFileURL$1(file.replace(".jsonl", ".schema.ts")).href}?t=${Date.now()}`);
|
|
1062
|
+
const schemaExport = schemaModule.schema || schemaModule.default;
|
|
1063
|
+
if (schemaExport?.foreignKeys) foreignKeys = schemaExport.foreignKeys;
|
|
1064
|
+
if (schemaExport?.indexes) indexes = schemaExport.indexes;
|
|
1065
|
+
if (schemaExport?.primaryKey) primaryKey = schemaExport.primaryKey;
|
|
1066
|
+
} catch (_error) {
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
try {
|
|
1070
|
+
const tableSchema = this.createTableSchema(tableName, data, schema, foreignKeys, indexes, primaryKey);
|
|
1071
|
+
this.createTableInDb(db, tableSchema);
|
|
1072
|
+
for (let rowIndex = 0; rowIndex < data.length; rowIndex++) {
|
|
1073
|
+
const row = data[rowIndex];
|
|
1074
|
+
try {
|
|
1075
|
+
this.insertRowIntoDb(db, tableName, tableSchema, row);
|
|
1076
|
+
} catch (error) {
|
|
1077
|
+
const constraintError = this.analyzeConstraintError(error, file, tableName, rowIndex, row, foreignKeys, db);
|
|
1078
|
+
if (constraintError) errors.push(constraintError);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
} catch (_error) {
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
await db.close();
|
|
1086
|
+
} catch (error) {
|
|
1087
|
+
errors.push({
|
|
1088
|
+
file: dirPath,
|
|
1089
|
+
tableName: "database",
|
|
1090
|
+
rowIndex: 0,
|
|
1091
|
+
issues: [{
|
|
1092
|
+
message: `Database initialization failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1093
|
+
path: []
|
|
1094
|
+
}],
|
|
1095
|
+
type: "schema"
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
return errors;
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Create table schema from data and validation schema
|
|
1102
|
+
*/
|
|
1103
|
+
createTableSchema(tableName, data, validationSchema, foreignKeys, indexes, primaryKey) {
|
|
1104
|
+
if (data.length === 0) throw new Error(`No data found in ${tableName}`);
|
|
1105
|
+
const schema = JsonlReader.inferSchema(tableName, data);
|
|
1106
|
+
if (primaryKey) {
|
|
1107
|
+
const pkColumn = schema.columns.find((col) => col.name === primaryKey);
|
|
1108
|
+
if (pkColumn) pkColumn.primaryKey = true;
|
|
1109
|
+
} else if (!schema.columns.some((col) => col.primaryKey)) {
|
|
1110
|
+
const idColumn = schema.columns.find((c) => c.name === "id");
|
|
1111
|
+
if (idColumn) idColumn.primaryKey = true;
|
|
1112
|
+
}
|
|
1113
|
+
if (foreignKeys && foreignKeys.length > 0) schema.foreignKeys = foreignKeys;
|
|
1114
|
+
if (indexes && indexes.length > 0) schema.indexes = indexes;
|
|
1115
|
+
return schema;
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Create table in database
|
|
1119
|
+
*/
|
|
1120
|
+
createTableInDb(db, schema) {
|
|
1121
|
+
const columns = schema.columns.map((col) => {
|
|
1122
|
+
let colDef = `${this.quoteIdentifier(col.name)} ${col.type.toUpperCase()}`;
|
|
1123
|
+
if (col.primaryKey) colDef += " PRIMARY KEY";
|
|
1124
|
+
return colDef;
|
|
1125
|
+
});
|
|
1126
|
+
if (schema.foreignKeys && schema.foreignKeys.length > 0) for (const fk of schema.foreignKeys) columns.push(`FOREIGN KEY (${this.quoteIdentifier(fk.column)}) REFERENCES ${this.quoteIdentifier(fk.references.table)}(${this.quoteIdentifier(fk.references.column)})`);
|
|
1127
|
+
const sql = `CREATE TABLE IF NOT EXISTS ${this.quoteIdentifier(schema.name)} (${columns.join(", ")})`;
|
|
1128
|
+
db.execute(sql);
|
|
1129
|
+
if (schema.indexes && schema.indexes.length > 0) for (const index of schema.indexes) {
|
|
1130
|
+
const indexName = index.name || `idx_${schema.name}_${index.columns.join("_")}`;
|
|
1131
|
+
const uniqueKeyword = index.unique ? "UNIQUE" : "";
|
|
1132
|
+
const indexColumns = index.columns.map((col) => this.quoteIdentifier(col)).join(", ");
|
|
1133
|
+
const indexSql = `CREATE ${uniqueKeyword} INDEX IF NOT EXISTS ${this.quoteIdentifier(indexName)} ON ${this.quoteIdentifier(schema.name)} (${indexColumns})`;
|
|
1134
|
+
db.execute(indexSql);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Insert a row into database
|
|
1139
|
+
*/
|
|
1140
|
+
insertRowIntoDb(db, tableName, schema, row) {
|
|
1141
|
+
const columnNames = schema.columns.map((col) => col.name);
|
|
1142
|
+
const quotedColumns = columnNames.map((name) => this.quoteIdentifier(name));
|
|
1143
|
+
const placeholders = columnNames.map(() => "?").join(", ");
|
|
1144
|
+
const sql = `INSERT INTO ${this.quoteIdentifier(tableName)} (${quotedColumns.join(", ")}) VALUES (${placeholders})`;
|
|
1145
|
+
const values = columnNames.map((col) => {
|
|
1146
|
+
const value = row[col];
|
|
1147
|
+
if (value === null || value === void 0) return null;
|
|
1148
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
1149
|
+
if (typeof value === "boolean") return value ? 1 : 0;
|
|
1150
|
+
return value;
|
|
1151
|
+
});
|
|
1152
|
+
db.execute(sql, values);
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Analyze constraint error and extract detailed information
|
|
1156
|
+
*/
|
|
1157
|
+
analyzeConstraintError(error, file, tableName, rowIndex, row, foreignKeys, db) {
|
|
1158
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1159
|
+
if (errorMessage.includes("FOREIGN KEY constraint failed")) for (const fk of foreignKeys) {
|
|
1160
|
+
const fkValue = row[fk.column];
|
|
1161
|
+
if (fkValue === null || fkValue === void 0) continue;
|
|
1162
|
+
try {
|
|
1163
|
+
const result = db.query(`SELECT COUNT(*) as count FROM ${this.quoteIdentifier(fk.references.table)} WHERE ${this.quoteIdentifier(fk.references.column)} = ?`, [fkValue]);
|
|
1164
|
+
if (result.length > 0 && result[0].count === 0) return {
|
|
1165
|
+
file,
|
|
1166
|
+
tableName,
|
|
1167
|
+
rowIndex,
|
|
1168
|
+
issues: [],
|
|
1169
|
+
type: "foreignKey",
|
|
1170
|
+
foreignKeyError: {
|
|
1171
|
+
column: fk.column,
|
|
1172
|
+
value: fkValue,
|
|
1173
|
+
referencedTable: fk.references.table,
|
|
1174
|
+
referencedColumn: fk.references.column
|
|
1175
|
+
}
|
|
1176
|
+
};
|
|
1177
|
+
} catch (_) {}
|
|
1178
|
+
}
|
|
1179
|
+
return {
|
|
1180
|
+
file,
|
|
1181
|
+
tableName,
|
|
1182
|
+
rowIndex,
|
|
1183
|
+
issues: [{
|
|
1184
|
+
message: errorMessage,
|
|
1185
|
+
path: []
|
|
1186
|
+
}],
|
|
1187
|
+
type: "schema"
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* Quote SQL identifier
|
|
1192
|
+
*/
|
|
1193
|
+
quoteIdentifier(name) {
|
|
1194
|
+
return `"${name.replace(/"/g, "\"\"")}"`;
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Validate a single JSONL file
|
|
1198
|
+
*/
|
|
1199
|
+
async validateFile(filePath) {
|
|
1200
|
+
const tableName = basename(filePath, ".jsonl");
|
|
1201
|
+
const data = await JsonlReader.read(filePath);
|
|
1202
|
+
const schema = await SchemaLoader.loadSchema(filePath);
|
|
1203
|
+
const errors = [];
|
|
1204
|
+
for (let i = 0; i < data.length; i++) {
|
|
1205
|
+
const row = data[i];
|
|
1206
|
+
const result = schema["~standard"].validate(row);
|
|
1207
|
+
if (result instanceof Promise) throw new Error("Asynchronous validation is not supported.");
|
|
1208
|
+
if (result.issues && result.issues.length > 0) errors.push({
|
|
1209
|
+
file: filePath,
|
|
1210
|
+
tableName,
|
|
1211
|
+
rowIndex: i,
|
|
1212
|
+
issues: result.issues,
|
|
1213
|
+
type: "schema"
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
if (errors.length === 0) {
|
|
1217
|
+
const dirPath = dirname(filePath);
|
|
1218
|
+
const allJsonlFiles = (await readdir(dirPath, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).map((entry) => join(dirPath, entry.name));
|
|
1219
|
+
const dbErrors = await this.validateWithDatabase(dirPath, allJsonlFiles);
|
|
1220
|
+
errors.push(...dbErrors.filter((e) => e.file === filePath));
|
|
1221
|
+
}
|
|
1222
|
+
return {
|
|
1223
|
+
valid: errors.length === 0,
|
|
1224
|
+
errors,
|
|
1225
|
+
warnings: []
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
};
|
|
1229
|
+
|
|
1114
1230
|
//#endregion
|
|
1115
1231
|
//#region src/error-formatter.ts
|
|
1116
1232
|
var ErrorFormatter = class {
|