@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 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
- foreignKeys = (await import(`${pathToFileURL$1(tableConfig.jsonlPath.replace(".jsonl", ".schema.ts")).href}?t=${Date.now()}`)).foreignKeys;
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
- if (schemaModule.primaryKey) schemaMetadata.primaryKey = schemaModule.primaryKey;
560
- if (schemaModule.foreignKeys) schemaMetadata.foreignKeys = schemaModule.foreignKeys;
561
- if (schemaModule.indexes) schemaMetadata.indexes = schemaModule.indexes;
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)) for (const pkColumn of primaryKey) {
598
- const col = schema.columns.find((c) => c.name === pkColumn);
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 (${fk.columns.map((col) => this.quoteIdentifier(col)).join(", ")})`, `REFERENCES ${this.quoteTableName(fk.references.table)}(${fk.references.columns.map((col) => this.quoteIdentifier(col)).join(", ")})`];
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 {