@toiroakr/lines-db 0.1.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/bin/cli.js ADDED
@@ -0,0 +1,1373 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ import { access, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
4
+ import { basename, dirname, extname, isAbsolute, join, normalize, relative } from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+ import { styleText } from "node:util";
7
+ import { Command } from "commander";
8
+ import { runInNewContext } from "node:vm";
9
+
10
+ //#region rolldown:runtime
11
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
12
+
13
+ //#endregion
14
+ //#region src/type-generator.ts
15
+ var TypeGenerator = class {
16
+ dataDir;
17
+ projectRoot;
18
+ outputFile;
19
+ dataDirPath;
20
+ constructor(options) {
21
+ const envProjectRoot = process.env.LINES_DB_TEST_PROJECT_ROOT;
22
+ this.projectRoot = envProjectRoot !== void 0 ? envProjectRoot : options.projectRoot || process.cwd();
23
+ this.dataDir = options.dataDir;
24
+ this.dataDirPath = isAbsolute(this.dataDir) ? this.dataDir : join(this.projectRoot, this.dataDir);
25
+ this.outputFile = join(this.dataDirPath, "db.ts");
26
+ }
27
+ /**
28
+ * Generate types file from JSONL files and their optional schema files.
29
+ */
30
+ async generate() {
31
+ const tables = await this.findTables();
32
+ if (tables.length === 0) throw new Error(`No JSONL files found in ${this.dataDirPath}. Place one or more *.jsonl files in the directory.`);
33
+ const content = this.generateTypeDeclarations(tables);
34
+ await mkdir(dirname(this.outputFile), { recursive: true });
35
+ await writeFile(this.outputFile, content, "utf-8");
36
+ console.log(`Generated types at ${this.outputFile}`);
37
+ return this.outputFile;
38
+ }
39
+ /**
40
+ * Find all *.jsonl files and check if they have corresponding *.schema.ts files
41
+ */
42
+ async findTables() {
43
+ try {
44
+ const entries = await readdir(this.dataDirPath, { withFileTypes: true });
45
+ const tables = [];
46
+ for (const entry of entries) if (entry.isFile() && entry.name.endsWith(".jsonl")) {
47
+ const tableName = basename(entry.name, ".jsonl");
48
+ const schemaFileName = `${tableName}.schema.ts`;
49
+ const schemaFilePath = join(this.dataDirPath, schemaFileName);
50
+ const hasSchema = entries.some((e) => e.isFile() && e.name === schemaFileName);
51
+ tables.push({
52
+ tableName,
53
+ schemaFile: hasSchema ? schemaFilePath : void 0
54
+ });
55
+ }
56
+ return tables;
57
+ } catch (error) {
58
+ if (error.code === "ENOENT") throw new Error(`Data directory not found: ${this.dataDirPath}. Set lines-db.dataDir to the correct location.`);
59
+ throw error;
60
+ }
61
+ }
62
+ /**
63
+ * Generate type declaration content
64
+ */
65
+ generateTypeDeclarations(tables) {
66
+ const imports = [];
67
+ const tableEntries = [];
68
+ const usedAliases = /* @__PURE__ */ new Set();
69
+ for (const table of tables) {
70
+ const tableKey = this.formatTableKey(table.tableName);
71
+ if (table.schemaFile) {
72
+ const schemaIdentifier = this.createSchemaIdentifier(table.tableName, usedAliases);
73
+ usedAliases.add(schemaIdentifier);
74
+ let relativePath = relative(join(this.outputFile, ".."), table.schemaFile).replace(/\\/g, "/").replace(".ts", ".js");
75
+ if (!relativePath.startsWith(".")) relativePath = "./" + relativePath;
76
+ imports.push(`import { schema as ${schemaIdentifier} } from '${relativePath}';`);
77
+ tableEntries.push(` ${tableKey}: InferOutput<typeof ${schemaIdentifier}>;`);
78
+ } else tableEntries.push(` ${tableKey}: Record<string, unknown>;`);
79
+ }
80
+ return `// Auto-generated by lines-db
81
+ // Do not edit this file manually
82
+
83
+ ${imports.length > 0 ? `${imports.join("\n")}\n` : ""}import type { DatabaseConfig${imports.length > 0 ? ", InferOutput" : ""} } from 'lines-db';
84
+ import { fileURLToPath } from 'node:url';
85
+ import { dirname } from 'node:path';
86
+
87
+ const __filename = fileURLToPath(import.meta.url);
88
+ const __dirname = dirname(__filename);
89
+
90
+ export type Tables = {
91
+ ${tableEntries.join("\n")}
92
+ };
93
+
94
+ export const config: DatabaseConfig<Tables> = {
95
+ dataDir: __dirname,
96
+ };
97
+ `;
98
+ }
99
+ createSchemaIdentifier(tableName, usedAliases) {
100
+ let base = sanitizeIdentifier(toCamelCase(tableName)) || "table";
101
+ if (!/^[A-Za-z_$]/.test(base)) base = `_${base}`;
102
+ let candidate = `${base}Schema`;
103
+ let suffix = 1;
104
+ while (usedAliases.has(candidate)) candidate = `${base}${++suffix}Schema`;
105
+ return candidate;
106
+ }
107
+ formatTableKey(tableName) {
108
+ if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(tableName)) return tableName;
109
+ return `'${tableName.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
110
+ }
111
+ };
112
+ function toCamelCase(value) {
113
+ const parts = value.split(/[^A-Za-z0-9]+/).filter(Boolean).map((part) => part.toLowerCase());
114
+ if (parts.length === 0) return value;
115
+ const [first, ...rest] = parts;
116
+ return first + rest.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
117
+ }
118
+ function sanitizeIdentifier(value) {
119
+ return value.replace(/[^A-Za-z0-9_$]/g, "");
120
+ }
121
+
122
+ //#endregion
123
+ //#region src/jsonl-reader.ts
124
+ var JsonlReader = class {
125
+ static overrides = null;
126
+ /**
127
+ * Temporarily override the data returned for specific JSONL files.
128
+ * Useful for scenarios like migration validation where in-memory data should be used.
129
+ */
130
+ static async withOverrides(overrides, fn) {
131
+ const normalized = /* @__PURE__ */ new Map();
132
+ for (const [filePath, rows] of overrides) normalized.set(normalize(filePath), rows);
133
+ const previousOverrides = this.overrides;
134
+ this.overrides = normalized;
135
+ try {
136
+ return await fn();
137
+ } finally {
138
+ this.overrides = previousOverrides;
139
+ }
140
+ }
141
+ /**
142
+ * Read JSONL file and parse each line as JSON
143
+ */
144
+ static async read(filePath) {
145
+ const overrideRows = this.overrides?.get(normalize(filePath));
146
+ if (overrideRows) return overrideRows.map((row) => JSON.parse(JSON.stringify(row)));
147
+ return (await readFile(filePath, "utf-8")).trim().split("\n").filter((line) => line.trim().length > 0).map((line) => {
148
+ try {
149
+ return JSON.parse(line);
150
+ } catch (error) {
151
+ throw new Error(`Failed to parse JSON line: ${line}`, { cause: error });
152
+ }
153
+ });
154
+ }
155
+ /**
156
+ * Infer schema from JSONL data
157
+ */
158
+ static inferSchema(tableName, data) {
159
+ if (data.length === 0) throw new Error("Cannot infer schema from empty data");
160
+ const columnTypes = /* @__PURE__ */ new Map();
161
+ const booleanColumns = /* @__PURE__ */ new Set();
162
+ const nonBooleanColumns = /* @__PURE__ */ new Set();
163
+ for (const row of data) for (const [key, value] of Object.entries(row)) {
164
+ if (!columnTypes.has(key)) columnTypes.set(key, /* @__PURE__ */ new Set());
165
+ columnTypes.get(key).add(this.inferType(value));
166
+ if (typeof value === "boolean") booleanColumns.add(key);
167
+ else if (value !== null) nonBooleanColumns.add(key);
168
+ }
169
+ const columns = [];
170
+ for (const [columnName, types] of columnTypes.entries()) {
171
+ const typeArray = Array.from(types);
172
+ let sqlType = "TEXT";
173
+ if (typeArray.length === 1) sqlType = typeArray[0];
174
+ else if (typeArray.every((t) => t === "INTEGER" || t === "REAL")) sqlType = "REAL";
175
+ else if (!typeArray.includes("NULL")) sqlType = "TEXT";
176
+ else if (typeArray.length === 2 && typeArray.includes("NULL")) sqlType = typeArray.find((t) => t !== "NULL");
177
+ const isBooleanColumn = booleanColumns.has(columnName) && !nonBooleanColumns.has(columnName);
178
+ columns.push({
179
+ name: columnName,
180
+ type: sqlType,
181
+ notNull: !typeArray.includes("NULL"),
182
+ valueType: isBooleanColumn ? "boolean" : void 0
183
+ });
184
+ }
185
+ const idColumn = columns.find((col) => col.name === "id");
186
+ if (idColumn) idColumn.primaryKey = true;
187
+ return {
188
+ name: tableName,
189
+ columns
190
+ };
191
+ }
192
+ static inferType(value) {
193
+ if (value === null) return "NULL";
194
+ if (typeof value === "number") return Number.isInteger(value) ? "INTEGER" : "REAL";
195
+ if (typeof value === "string") return "TEXT";
196
+ if (typeof value === "boolean") return "INTEGER";
197
+ if (typeof value === "object") return "JSON";
198
+ return "TEXT";
199
+ }
200
+ };
201
+
202
+ //#endregion
203
+ //#region src/schema-loader.ts
204
+ var SchemaLoader = class {
205
+ /**
206
+ * Load a validation schema file for a table
207
+ * Requires ${tableName}.schema.ts to exist alongside the JSONL file
208
+ */
209
+ static async loadSchema(jsonlPath) {
210
+ const dir = dirname(jsonlPath);
211
+ const tableName = basename(jsonlPath, ".jsonl");
212
+ const schemaPath = join(dir, `${tableName}.schema.ts`);
213
+ try {
214
+ await access(schemaPath);
215
+ } catch (error) {
216
+ throw new Error(`Schema file not found for table '${tableName}'. Expected: ${schemaPath}`, { cause: error instanceof Error ? error : void 0 });
217
+ }
218
+ try {
219
+ const module = await import(`${pathToFileURL(schemaPath).href}?t=${Date.now()}`);
220
+ const schema = module.default || module.schema;
221
+ if (schema && this.isStandardSchema(schema)) return schema;
222
+ throw new Error(`Schema file ${schemaPath} does not export a valid StandardSchema`);
223
+ } catch (error) {
224
+ throw new Error(`Failed to load schema for table '${tableName}' from ${schemaPath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error instanceof Error ? error : void 0 });
225
+ }
226
+ }
227
+ /**
228
+ * Check if an object implements the StandardSchema interface
229
+ */
230
+ static isStandardSchema(obj) {
231
+ if (!obj || typeof obj !== "object") return false;
232
+ const standard = obj["~standard"];
233
+ if (!standard || typeof standard !== "object") return false;
234
+ const standardObj = standard;
235
+ return standardObj.version === 1 && typeof standardObj.vendor === "string" && typeof standardObj.validate === "function";
236
+ }
237
+ };
238
+
239
+ //#endregion
240
+ //#region src/validator.ts
241
+ var Validator = class {
242
+ path;
243
+ projectRoot;
244
+ constructor(options) {
245
+ this.path = options.path;
246
+ this.projectRoot = options.projectRoot || process.cwd();
247
+ }
248
+ /**
249
+ * Validate JSONL file(s)
250
+ */
251
+ async validate() {
252
+ const fullPath = this.path.startsWith("/") ? this.path : join(this.projectRoot, this.path);
253
+ const stats = await stat(fullPath);
254
+ if (stats.isDirectory()) return this.validateDirectory(fullPath);
255
+ else if (stats.isFile() && fullPath.endsWith(".jsonl")) return this.validateFile(fullPath);
256
+ else throw new Error(`Invalid path: ${this.path}. Must be a directory or .jsonl file.`);
257
+ }
258
+ /**
259
+ * Validate all JSONL files in a directory
260
+ */
261
+ async validateDirectory(dirPath) {
262
+ const jsonlFiles = (await readdir(dirPath, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).map((entry) => join(dirPath, entry.name));
263
+ if (jsonlFiles.length === 0) throw new Error(`No JSONL files found in directory: ${dirPath}`);
264
+ const allErrors = [];
265
+ for (const file of jsonlFiles) {
266
+ const result = await this.validateFile(file);
267
+ allErrors.push(...result.errors);
268
+ }
269
+ const fkErrors = await this.validateForeignKeys(dirPath, jsonlFiles);
270
+ allErrors.push(...fkErrors);
271
+ return {
272
+ valid: allErrors.length === 0,
273
+ errors: allErrors
274
+ };
275
+ }
276
+ /**
277
+ * Validate foreign key constraints across all tables
278
+ */
279
+ async validateForeignKeys(dirPath, jsonlFiles) {
280
+ const errors = [];
281
+ const tableData = /* @__PURE__ */ new Map();
282
+ const tableSchemas = /* @__PURE__ */ new Map();
283
+ for (const file of jsonlFiles) {
284
+ const tableName = basename(file, ".jsonl");
285
+ const data = await JsonlReader.read(file);
286
+ const schema = await SchemaLoader.loadSchema(file);
287
+ tableData.set(tableName, data);
288
+ tableSchemas.set(tableName, schema);
289
+ }
290
+ for (const file of jsonlFiles) {
291
+ const tableName = basename(file, ".jsonl");
292
+ const schema = tableSchemas.get(tableName);
293
+ const data = tableData.get(tableName);
294
+ if (!schema || !data || !schema.foreignKeys) continue;
295
+ for (const fk of schema.foreignKeys) {
296
+ const referencedTable = fk.references.table;
297
+ const referencedData = tableData.get(referencedTable);
298
+ if (!referencedData) continue;
299
+ const referencedValues = /* @__PURE__ */ new Set();
300
+ for (const refRow of referencedData) {
301
+ const keyValues = fk.references.columns.map((col) => refRow[col]);
302
+ const compositeKey = JSON.stringify(keyValues);
303
+ referencedValues.add(compositeKey);
304
+ }
305
+ for (let i = 0; i < data.length; i++) {
306
+ const row = data[i];
307
+ const foreignKeyValues = fk.columns.map((col) => row[col]);
308
+ const compositeKey = JSON.stringify(foreignKeyValues);
309
+ if (!referencedValues.has(compositeKey)) errors.push({
310
+ file,
311
+ tableName,
312
+ rowIndex: i,
313
+ issues: [],
314
+ type: "foreignKey",
315
+ foreignKeyError: {
316
+ column: fk.columns.join(", "),
317
+ value: foreignKeyValues.length === 1 ? foreignKeyValues[0] : foreignKeyValues,
318
+ referencedTable,
319
+ referencedColumn: fk.references.columns.join(", ")
320
+ }
321
+ });
322
+ }
323
+ }
324
+ }
325
+ return errors;
326
+ }
327
+ /**
328
+ * Validate a single JSONL file
329
+ */
330
+ async validateFile(filePath) {
331
+ const tableName = basename(filePath, ".jsonl");
332
+ const data = await JsonlReader.read(filePath);
333
+ const schema = await SchemaLoader.loadSchema(filePath);
334
+ const errors = [];
335
+ for (let i = 0; i < data.length; i++) {
336
+ const row = data[i];
337
+ const result = schema["~standard"].validate(row);
338
+ if (result instanceof Promise) throw new Error("Asynchronous validation is not supported.");
339
+ if (result.issues && result.issues.length > 0) errors.push({
340
+ file: filePath,
341
+ tableName,
342
+ rowIndex: i,
343
+ issues: result.issues,
344
+ type: "schema"
345
+ });
346
+ }
347
+ return {
348
+ valid: errors.length === 0,
349
+ errors
350
+ };
351
+ }
352
+ };
353
+
354
+ //#endregion
355
+ //#region src/runtime.ts
356
+ function detectRuntime() {
357
+ if (typeof globalThis !== "undefined" && "Bun" in globalThis && typeof globalThis.Bun !== "undefined") return "bun";
358
+ if (typeof globalThis !== "undefined" && "Deno" in globalThis && typeof globalThis.Deno !== "undefined") return "deno";
359
+ if (typeof process !== "undefined" && process.versions && process.versions.node) return "node";
360
+ return "unknown";
361
+ }
362
+ const RUNTIME = detectRuntime();
363
+
364
+ //#endregion
365
+ //#region src/sqlite-adapter.ts
366
+ /**
367
+ * Create a SQLite database instance based on the runtime environment
368
+ */
369
+ function createDatabase(path = ":memory:") {
370
+ if (RUNTIME === "bun") return createBunDatabase(path);
371
+ else if (RUNTIME === "node" || RUNTIME === "deno") return createNodeDatabase(path);
372
+ else throw new Error(`Unsupported runtime: ${RUNTIME}`);
373
+ }
374
+ /**
375
+ * Create a Bun SQLite database
376
+ */
377
+ function createBunDatabase(path) {
378
+ const { Database } = __require("bun:sqlite");
379
+ const db = new Database(path);
380
+ return {
381
+ prepare(sql) {
382
+ const stmt = db.prepare(sql);
383
+ return {
384
+ run(...params) {
385
+ return stmt.run(...params);
386
+ },
387
+ get(...params) {
388
+ return stmt.get(...params);
389
+ },
390
+ all(...params) {
391
+ return stmt.all(...params);
392
+ }
393
+ };
394
+ },
395
+ exec(sql) {
396
+ db.exec(sql);
397
+ },
398
+ close() {
399
+ db.close();
400
+ }
401
+ };
402
+ }
403
+ /**
404
+ * Create a Node.js SQLite database
405
+ */
406
+ function createNodeDatabase(path) {
407
+ const { DatabaseSync } = __require("node:sqlite");
408
+ const db = new DatabaseSync(path);
409
+ return {
410
+ prepare(sql) {
411
+ const stmt = db.prepare(sql);
412
+ return {
413
+ run(...params) {
414
+ return stmt.run(...params);
415
+ },
416
+ get(...params) {
417
+ return stmt.get(...params);
418
+ },
419
+ all(...params) {
420
+ return stmt.all(...params);
421
+ }
422
+ };
423
+ },
424
+ exec(sql) {
425
+ db.exec(sql);
426
+ },
427
+ close() {
428
+ db.close();
429
+ }
430
+ };
431
+ }
432
+
433
+ //#endregion
434
+ //#region src/jsonl-writer.ts
435
+ var JsonlWriter = class {
436
+ /**
437
+ * Write data to JSONL file
438
+ */
439
+ static async write(filePath, data) {
440
+ await writeFile(filePath, data.map((obj) => JSON.stringify(obj)).join("\n") + "\n", "utf-8");
441
+ }
442
+ /**
443
+ * Append data to JSONL file
444
+ */
445
+ static async append(filePath, data) {
446
+ const { readFile: readFile$1, writeFile: writeFile$1 } = await import("node:fs/promises");
447
+ try {
448
+ const existing = await readFile$1(filePath, "utf-8");
449
+ const lines = data.map((obj) => JSON.stringify(obj)).join("\n");
450
+ await writeFile$1(filePath, existing.trim() + "\n" + lines + "\n", "utf-8");
451
+ } catch (error) {
452
+ if (error.code === "ENOENT") await this.write(filePath, data);
453
+ else throw error;
454
+ }
455
+ }
456
+ };
457
+
458
+ //#endregion
459
+ //#region src/directory-scanner.ts
460
+ var DirectoryScanner = class {
461
+ /**
462
+ * Scan directory for JSONL files and create table configurations
463
+ */
464
+ static async scanDirectory(dataDir) {
465
+ const tables = /* @__PURE__ */ new Map();
466
+ try {
467
+ const files = await readdir(dataDir);
468
+ for (const file of files) if (extname(file) === ".jsonl") {
469
+ const tableName = basename(file, ".jsonl");
470
+ const jsonlPath = join(dataDir, file);
471
+ tables.set(tableName, {
472
+ jsonlPath,
473
+ autoInferSchema: true
474
+ });
475
+ }
476
+ if (tables.size === 0) console.warn(`Warning: No JSONL files found in directory: ${dataDir}`);
477
+ return tables;
478
+ } catch (error) {
479
+ throw new Error(`Failed to scan directory ${dataDir}: ${error instanceof Error ? error.message : String(error)}`);
480
+ }
481
+ }
482
+ };
483
+
484
+ //#endregion
485
+ //#region src/schema.ts
486
+ /**
487
+ * Check if a schema has backward transformation
488
+ */
489
+ function hasBackward(schema) {
490
+ return "backward" in schema && typeof schema.backward === "function";
491
+ }
492
+
493
+ //#endregion
494
+ //#region src/database.ts
495
+ var LinesDB = class LinesDB {
496
+ db;
497
+ config;
498
+ schemas = /* @__PURE__ */ new Map();
499
+ validationSchemas = /* @__PURE__ */ new Map();
500
+ tables = /* @__PURE__ */ new Map();
501
+ inTransaction = false;
502
+ constructor(config, dbPath) {
503
+ this.config = config;
504
+ this.db = createDatabase(dbPath ?? ":memory:");
505
+ }
506
+ static create(config, dbPath) {
507
+ return new LinesDB(config, dbPath);
508
+ }
509
+ /**
510
+ * Initialize database by loading all JSONL files
511
+ */
512
+ async initialize() {
513
+ this.tables = await DirectoryScanner.scanDirectory(this.config.dataDir);
514
+ for (const [tableName, tableConfig] of this.tables) try {
515
+ await this.loadTable(tableName, tableConfig);
516
+ } catch (error) {
517
+ console.warn(`Warning: Failed to load table '${tableName}':`, error instanceof Error ? error.message : String(error));
518
+ this.tables.delete(tableName);
519
+ }
520
+ }
521
+ /**
522
+ * Load a single table from JSONL file
523
+ */
524
+ async loadTable(tableName, config) {
525
+ const data = await JsonlReader.read(config.jsonlPath);
526
+ if (data.length === 0) {
527
+ console.warn(`Warning: Table ${tableName} has no data`);
528
+ return;
529
+ }
530
+ let validationSchema = config.validationSchema;
531
+ if (!validationSchema) try {
532
+ validationSchema = await SchemaLoader.loadSchema(config.jsonlPath);
533
+ } catch (error) {
534
+ console.log(`[LinesDB] No validation schema for table '${tableName}':`, error instanceof Error ? error.message : String(error));
535
+ }
536
+ console.log(`[LinesDB] Loaded validation schema for table '${tableName}':`, validationSchema ? "FOUND" : "NOT FOUND");
537
+ if (validationSchema) {
538
+ console.log(`[LinesDB] Schema type:`, typeof validationSchema);
539
+ console.log(`[LinesDB] Schema has '~standard':`, "~standard" in validationSchema);
540
+ }
541
+ this.validationSchemas.set(tableName, validationSchema);
542
+ let schema;
543
+ if (config.schema) schema = config.schema;
544
+ else if (config.autoInferSchema !== false) schema = JsonlReader.inferSchema(tableName, data);
545
+ else throw new Error(`No schema provided for table ${tableName} and autoInferSchema is disabled`);
546
+ if (validationSchema) {
547
+ const biSchema = validationSchema;
548
+ if (biSchema.primaryKey && !schema.columns.some((col) => col.primaryKey)) for (const pkColumn of biSchema.primaryKey) {
549
+ const col = schema.columns.find((c) => c.name === pkColumn);
550
+ if (col) col.primaryKey = true;
551
+ }
552
+ if (biSchema.foreignKeys) schema.foreignKeys = biSchema.foreignKeys;
553
+ if (biSchema.indexes) schema.indexes = biSchema.indexes;
554
+ }
555
+ this.schemas.set(tableName, schema);
556
+ this.createTable(schema);
557
+ const validationErrors = [];
558
+ for (let rowIndex = 0; rowIndex < data.length; rowIndex++) {
559
+ const row = data[rowIndex];
560
+ try {
561
+ this.validateData(tableName, row);
562
+ } catch (error) {
563
+ if (error instanceof Error && error.name === "ValidationError") validationErrors.push({
564
+ rowIndex,
565
+ rowData: row,
566
+ error
567
+ });
568
+ else throw error;
569
+ }
570
+ }
571
+ if (validationErrors.length > 0) {
572
+ const enhancedError = /* @__PURE__ */ new Error(`Validation failed for ${validationErrors.length} row(s) in table ${tableName}`);
573
+ enhancedError.name = "ValidationError";
574
+ enhancedError.validationErrors = validationErrors;
575
+ enhancedError.issues = validationErrors[0].error.issues;
576
+ throw enhancedError;
577
+ }
578
+ this.insertData(tableName, schema, data);
579
+ }
580
+ /**
581
+ * Create table in SQLite with constraints and indexes
582
+ */
583
+ createTable(schema) {
584
+ this.db.exec("PRAGMA foreign_keys = ON");
585
+ const quotedTableName = this.quoteTableName(schema.name);
586
+ const columnDefs = schema.columns.map((col) => {
587
+ const sqlType = col.type === "JSON" ? "TEXT" : col.type;
588
+ const parts = [this.quoteIdentifier(col.name), sqlType];
589
+ if (col.primaryKey) parts.push("PRIMARY KEY");
590
+ if (col.notNull) parts.push("NOT NULL");
591
+ if (col.unique) parts.push("UNIQUE");
592
+ return parts.join(" ");
593
+ });
594
+ const foreignKeyDefs = [];
595
+ if (schema.foreignKeys && schema.foreignKeys.length > 0) for (const fk of schema.foreignKeys) {
596
+ 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(", ")})`];
597
+ if (fk.onDelete) fkParts.push(`ON DELETE ${fk.onDelete}`);
598
+ if (fk.onUpdate) fkParts.push(`ON UPDATE ${fk.onUpdate}`);
599
+ foreignKeyDefs.push(fkParts.join(" "));
600
+ }
601
+ const sql = `CREATE TABLE IF NOT EXISTS ${quotedTableName} (${[...columnDefs, ...foreignKeyDefs].join(", ")})`;
602
+ this.db.exec(sql);
603
+ if (schema.indexes && schema.indexes.length > 0) for (let i = 0; i < schema.indexes.length; i++) {
604
+ const index = schema.indexes[i];
605
+ const safeTableName = schema.name.replace(/[^a-zA-Z0-9]/g, "_");
606
+ const resolvedIndexName = index.name || `idx_${safeTableName}_${index.columns.join("_")}_${i}`;
607
+ const indexSql = `CREATE ${index.unique ? "UNIQUE " : ""}INDEX IF NOT EXISTS ${this.quoteIdentifier(resolvedIndexName)} ON ${quotedTableName} (${index.columns.map((col) => this.quoteIdentifier(col)).join(", ")})`;
608
+ this.db.exec(indexSql);
609
+ }
610
+ }
611
+ /**
612
+ * Quote table name to handle special characters in SQL
613
+ */
614
+ quoteTableName(tableName) {
615
+ return this.quoteIdentifier(tableName);
616
+ }
617
+ /**
618
+ * Quote identifier for SQL statements, escaping embedded quotes
619
+ */
620
+ quoteIdentifier(identifier) {
621
+ return `"${identifier.replace(/"/g, "\"\"")}"`;
622
+ }
623
+ /**
624
+ * Insert data into table
625
+ */
626
+ insertData(tableName, schema, data) {
627
+ const columnNames = schema.columns.map((col) => col.name);
628
+ const quotedColumns = columnNames.map((name) => this.quoteIdentifier(name));
629
+ const placeholders = columnNames.map(() => "?").join(", ");
630
+ const sql = `INSERT INTO ${this.quoteTableName(tableName)} (${quotedColumns.join(", ")}) VALUES (${placeholders})`;
631
+ const stmt = this.db.prepare(sql);
632
+ for (const row of data) {
633
+ const values = columnNames.map((col) => this.normalizeValue(row[col]));
634
+ stmt.run(...values);
635
+ }
636
+ }
637
+ /**
638
+ * Execute a raw SQL query
639
+ */
640
+ query(sql, params = []) {
641
+ return this.db.prepare(sql).all(...params);
642
+ }
643
+ /**
644
+ * Execute a SQL query that returns a single row
645
+ */
646
+ queryOne(sql, params = []) {
647
+ const result = this.db.prepare(sql).get(...params);
648
+ return result === void 0 ? null : result;
649
+ }
650
+ /**
651
+ * Execute a SQL statement (INSERT, UPDATE, DELETE)
652
+ */
653
+ execute(sql, params = []) {
654
+ return this.db.prepare(sql).run(...params);
655
+ }
656
+ /**
657
+ * Find rows by condition (supports OR/AND with arrays and function filters)
658
+ * If where is not provided, returns all rows
659
+ */
660
+ find(tableName, where) {
661
+ if (where === void 0) return this.query(`SELECT * FROM ${this.quoteTableName(tableName)}`).map((row) => this.deserializeRow(tableName, row));
662
+ if (Array.isArray(where) && where.length === 0) return [];
663
+ const { sql, values, functionFilters, hasOrWithFunctionFilters } = this.buildWhereClause(where);
664
+ let rows;
665
+ if (hasOrWithFunctionFilters) {
666
+ rows = this.query(`SELECT * FROM ${this.quoteTableName(tableName)}`).map((row) => this.deserializeRow(tableName, row));
667
+ return this.applyOrConditionWithFilters(rows, where);
668
+ }
669
+ if (sql) rows = this.query(`SELECT * FROM ${this.quoteTableName(tableName)} WHERE ${sql}`, values).map((row) => this.deserializeRow(tableName, row));
670
+ else rows = this.query(`SELECT * FROM ${this.quoteTableName(tableName)}`).map((row) => this.deserializeRow(tableName, row));
671
+ return this.applyFunctionFilters(rows, functionFilters);
672
+ }
673
+ /**
674
+ * Find a single row by condition (supports OR/AND with arrays and function filters)
675
+ */
676
+ findOne(tableName, where) {
677
+ const { sql, values, functionFilters } = this.buildWhereClause(where);
678
+ let rows;
679
+ if (sql) rows = this.query(`SELECT * FROM ${this.quoteTableName(tableName)} WHERE ${sql}`, values).map((row) => this.deserializeRow(tableName, row));
680
+ else rows = this.query(`SELECT * FROM ${this.quoteTableName(tableName)}`).map((row) => this.deserializeRow(tableName, row));
681
+ const filtered = this.applyFunctionFilters(rows, functionFilters);
682
+ return filtered.length > 0 ? filtered[0] : null;
683
+ }
684
+ /**
685
+ * Deserialize JSON columns in a row
686
+ */
687
+ deserializeRow(tableName, row) {
688
+ const schema = this.schemas.get(tableName);
689
+ if (!schema) return row;
690
+ const deserializedRow = { ...row };
691
+ for (const column of schema.columns) {
692
+ const colName = column.name;
693
+ if (!(colName in deserializedRow)) continue;
694
+ const value = deserializedRow[colName];
695
+ if (column.type === "JSON" && typeof value === "string") {
696
+ try {
697
+ deserializedRow[colName] = JSON.parse(value);
698
+ } catch (error) {
699
+ console.warn(`Failed to parse JSON column ${colName}:`, error);
700
+ }
701
+ continue;
702
+ }
703
+ if (column.valueType === "boolean") {
704
+ if (typeof value === "number") deserializedRow[colName] = value === 0 ? false : true;
705
+ else if (typeof value === "bigint") deserializedRow[colName] = value === 0n ? false : true;
706
+ }
707
+ }
708
+ return deserializedRow;
709
+ }
710
+ /**
711
+ * Validate data using StandardSchema
712
+ * Note: Only synchronous validation is supported
713
+ */
714
+ validateData(tableName, data) {
715
+ const schema = this.validationSchemas.get(tableName);
716
+ console.log(`[LinesDB] validateData called for table '${tableName}', schema exists:`, !!schema);
717
+ if (!schema) {
718
+ console.log(`[LinesDB] No validation schema found for table '${tableName}', skipping validation`);
719
+ return;
720
+ }
721
+ console.log(`[LinesDB] Validating data:`, JSON.stringify(data));
722
+ const result = schema["~standard"].validate(data);
723
+ if (result instanceof Promise) throw new Error("Asynchronous validation is not supported. Please use synchronous validation schemas.");
724
+ if (result.issues && result.issues.length > 0) {
725
+ const errorMessage = `Validation failed for table '${tableName}':\n${result.issues.map((issue) => {
726
+ let pathStr = "root";
727
+ if (issue.path && issue.path.length > 0) pathStr = issue.path.map((segment) => {
728
+ if (typeof segment === "object" && segment !== null && "key" in segment) return String(segment.key);
729
+ return String(segment);
730
+ }).join(".");
731
+ return ` - ${pathStr}: ${issue.message}`;
732
+ }).join("\n")}`;
733
+ const error = new Error(errorMessage);
734
+ error.name = "ValidationError";
735
+ error.issues = result.issues;
736
+ throw error;
737
+ }
738
+ }
739
+ /**
740
+ * Insert a row into a table with validation
741
+ */
742
+ insert(tableName, data) {
743
+ this.validateData(tableName, data);
744
+ if (!this.schemas.get(tableName)) throw new Error(`Table ${tableName} does not exist`);
745
+ const columnNames = Object.keys(data);
746
+ const quotedColumns = columnNames.map((col) => this.quoteIdentifier(col));
747
+ const placeholders = columnNames.map(() => "?").join(", ");
748
+ const sql = `INSERT INTO ${this.quoteTableName(tableName)} (${quotedColumns.join(", ")}) VALUES (${placeholders})`;
749
+ const values = Object.values(data).map((v) => this.normalizeValue(v));
750
+ const result = this.execute(sql, values);
751
+ if (!this.inTransaction) this.syncTable(tableName).catch((err) => {
752
+ console.error(`Failed to sync table ${tableName}:`, err);
753
+ });
754
+ return result;
755
+ }
756
+ /**
757
+ * Batch insert rows with validation per record.
758
+ */
759
+ batchInsert(tableName, records) {
760
+ if (!this.schemas.get(tableName)) throw new Error(`Table ${tableName} does not exist`);
761
+ if (records.length === 0) return {
762
+ changes: 0,
763
+ lastInsertRowid: 0
764
+ };
765
+ let totalChanges = 0n;
766
+ let lastRowid = 0n;
767
+ for (const record of records) {
768
+ this.validateData(tableName, record);
769
+ const columnNames = Object.keys(record);
770
+ const quotedColumns = columnNames.map((col) => this.quoteIdentifier(col));
771
+ const placeholders = columnNames.map(() => "?").join(", ");
772
+ const sql = `INSERT INTO ${this.quoteTableName(tableName)} (${quotedColumns.join(", ")}) VALUES (${placeholders})`;
773
+ const values = columnNames.map((col) => this.normalizeValue(record[col]));
774
+ const result = this.execute(sql, values);
775
+ totalChanges += BigInt(result.changes);
776
+ lastRowid = BigInt(result.lastInsertRowid);
777
+ }
778
+ if (!this.inTransaction) this.syncTable(tableName).catch((err) => {
779
+ console.error(`Failed to sync table ${tableName}:`, err);
780
+ });
781
+ return {
782
+ changes: totalChanges,
783
+ lastInsertRowid: lastRowid
784
+ };
785
+ }
786
+ /**
787
+ * Update rows in a table with validation (supports OR/AND with arrays)
788
+ * Note: Function filters are not supported for update operations
789
+ * Note: By default, validation is enabled. For partial updates, existing data is fetched
790
+ * and merged before validation. Set options.validate = false to disable validation.
791
+ */
792
+ update(tableName, data, where, options) {
793
+ if (!this.schemas.get(tableName)) throw new Error(`Table ${tableName} does not exist`);
794
+ const shouldValidate = options?.validate !== false;
795
+ const hasValidationSchema = this.validationSchemas.has(tableName);
796
+ if (shouldValidate && hasValidationSchema) {
797
+ const existingRows = this.find(tableName, where);
798
+ for (const existingRow of existingRows) {
799
+ const mergedData = {
800
+ ...existingRow,
801
+ ...data
802
+ };
803
+ this.validateData(tableName, mergedData);
804
+ }
805
+ }
806
+ const { sql: whereSql, values: whereValues, functionFilters } = this.buildWhereClause(where);
807
+ if (functionFilters.length > 0) throw new Error("Function filters are not supported in update operations");
808
+ const setClauses = Object.keys(data).map((key) => `${this.quoteIdentifier(key)} = ?`).join(", ");
809
+ const sql = `UPDATE ${this.quoteTableName(tableName)} SET ${setClauses} WHERE ${whereSql}`;
810
+ const values = [...Object.values(data).map((v) => this.normalizeValue(v)), ...whereValues];
811
+ const result = this.execute(sql, values);
812
+ if (!this.inTransaction) this.syncTable(tableName).catch((err) => {
813
+ console.error(`Failed to sync table ${tableName}:`, err);
814
+ });
815
+ return result;
816
+ }
817
+ /**
818
+ * Batch update rows with record-specific values and validation.
819
+ * Each record must include the primary key to identify the target row.
820
+ * Validation runs once per merged record unless explicitly disabled.
821
+ */
822
+ batchUpdate(tableName, records, options) {
823
+ const schema = this.schemas.get(tableName);
824
+ if (!schema) throw new Error(`Table ${tableName} does not exist`);
825
+ if (records.length === 0) return {
826
+ changes: 0,
827
+ lastInsertRowid: 0
828
+ };
829
+ const pkColumn = schema.columns.find((col) => col.primaryKey);
830
+ if (!pkColumn) throw new Error(`Table ${tableName} does not have a primary key`);
831
+ const pkName = pkColumn.name;
832
+ const pkValues = [];
833
+ for (const record of records) {
834
+ const pkValue = record[pkName];
835
+ if (pkValue === void 0) throw new Error(`Record is missing primary key '${String(pkName)}': ${JSON.stringify(record)}`);
836
+ pkValues.push(pkValue);
837
+ }
838
+ const shouldValidate = options?.validate !== false;
839
+ const hasValidationSchema = this.validationSchemas.has(tableName);
840
+ if (shouldValidate && hasValidationSchema) {
841
+ const orCondition = pkValues.map((pkValue) => ({ [pkName]: pkValue }));
842
+ const existingRows = this.find(tableName, orCondition);
843
+ const existingRowsMap = /* @__PURE__ */ new Map();
844
+ for (const row of existingRows) {
845
+ const pkValue = row[pkName];
846
+ existingRowsMap.set(pkValue, row);
847
+ }
848
+ const validationErrors = [];
849
+ for (let i = 0; i < records.length; i++) {
850
+ const record = records[i];
851
+ const pkValue = record[pkName];
852
+ const existingRow = existingRowsMap.get(pkValue);
853
+ if (!existingRow) throw new Error(`No existing row found with ${String(pkName)}=${JSON.stringify(pkValue)}`);
854
+ const mergedData = {
855
+ ...existingRow,
856
+ ...record
857
+ };
858
+ try {
859
+ this.validateData(tableName, mergedData);
860
+ } catch (error) {
861
+ if (error instanceof Error && error.name === "ValidationError") validationErrors.push({
862
+ rowIndex: i,
863
+ rowData: mergedData,
864
+ pkValue,
865
+ error
866
+ });
867
+ else throw error;
868
+ }
869
+ }
870
+ if (validationErrors.length > 0) {
871
+ const enhancedError = /* @__PURE__ */ new Error(`Validation failed for ${validationErrors.length} row(s)`);
872
+ enhancedError.name = "ValidationError";
873
+ enhancedError.validationErrors = validationErrors;
874
+ enhancedError.issues = validationErrors[0].error.issues;
875
+ throw enhancedError;
876
+ }
877
+ }
878
+ let totalChanges = 0n;
879
+ let lastRowid = 0n;
880
+ for (const record of records) {
881
+ const pkValue = record[pkName];
882
+ const where = { [pkName]: pkValue };
883
+ const result = this.update(tableName, record, where, { validate: false });
884
+ totalChanges += BigInt(result.changes);
885
+ lastRowid = BigInt(result.lastInsertRowid);
886
+ }
887
+ return {
888
+ changes: totalChanges,
889
+ lastInsertRowid: lastRowid
890
+ };
891
+ }
892
+ /**
893
+ * Delete rows from a table (supports OR/AND with arrays)
894
+ * Note: Function filters are not supported for delete operations
895
+ */
896
+ delete(tableName, where) {
897
+ if (!this.schemas.get(tableName)) throw new Error(`Table ${tableName} does not exist`);
898
+ const { sql: whereSql, values, functionFilters } = this.buildWhereClause(where);
899
+ if (functionFilters.length > 0) throw new Error("Function filters are not supported in delete operations");
900
+ const sql = `DELETE FROM ${this.quoteTableName(tableName)} WHERE ${whereSql}`;
901
+ const result = this.execute(sql, values);
902
+ if (!this.inTransaction) this.syncTable(tableName).catch((err) => {
903
+ console.error(`Failed to sync table ${tableName}:`, err);
904
+ });
905
+ return result;
906
+ }
907
+ /**
908
+ * Batch delete rows by primary key.
909
+ */
910
+ batchDelete(tableName, records) {
911
+ const schema = this.schemas.get(tableName);
912
+ if (!schema) throw new Error(`Table ${tableName} does not exist`);
913
+ if (records.length === 0) return {
914
+ changes: 0,
915
+ lastInsertRowid: 0
916
+ };
917
+ const pkColumn = schema.columns.find((col) => col.primaryKey);
918
+ if (!pkColumn) throw new Error(`Table ${tableName} does not have a primary key`);
919
+ const pkName = pkColumn.name;
920
+ const pkValues = records.map((record, index) => {
921
+ const pkValue = record[pkName];
922
+ if (pkValue === void 0) throw new Error(`Record at index ${index} is missing primary key '${String(pkName)}'`);
923
+ return pkValue;
924
+ });
925
+ const placeholders = pkValues.map(() => "?").join(", ");
926
+ const sql = `DELETE FROM ${this.quoteTableName(tableName)} WHERE ${this.quoteIdentifier(pkName)} IN (${placeholders})`;
927
+ const values = pkValues.map((value) => this.normalizeValue(value));
928
+ const result = this.execute(sql, values);
929
+ if (!this.inTransaction) this.syncTable(tableName).catch((err) => {
930
+ console.error(`Failed to sync table ${tableName}:`, err);
931
+ });
932
+ return {
933
+ changes: BigInt(result.changes),
934
+ lastInsertRowid: BigInt(result.lastInsertRowid)
935
+ };
936
+ }
937
+ /**
938
+ * Normalize value for SQLite
939
+ */
940
+ normalizeValue(value) {
941
+ if (value === null || value === void 0) return null;
942
+ if (typeof value === "boolean") return value ? 1 : 0;
943
+ if (typeof value === "string" || typeof value === "number" || typeof value === "bigint") return value;
944
+ if (value instanceof Uint8Array) return value;
945
+ return JSON.stringify(value);
946
+ }
947
+ /**
948
+ * Build WHERE clause from condition (supports OR/AND with arrays and functions)
949
+ */
950
+ buildWhereClause(condition) {
951
+ const values = [];
952
+ const functionFilters = [];
953
+ let hasOrWithFunctionFilters = false;
954
+ const buildCondition = (cond, isInOr = false) => {
955
+ if (Array.isArray(cond)) return cond.map((item) => {
956
+ const clause = Array.isArray(item) ? buildCondition(item, true) : buildCondition(item, true);
957
+ return clause ? `(${clause})` : "";
958
+ }).filter((clause) => clause !== "").join(" OR ");
959
+ const conditions = [];
960
+ let hasFunctionFilter = false;
961
+ for (const [key, value] of Object.entries(cond)) if (typeof value === "function") {
962
+ functionFilters.push({
963
+ key,
964
+ fn: value
965
+ });
966
+ hasFunctionFilter = true;
967
+ } else {
968
+ conditions.push(`${this.quoteIdentifier(key)} = ?`);
969
+ values.push(this.normalizeValue(value));
970
+ }
971
+ if (isInOr && hasFunctionFilter) hasOrWithFunctionFilters = true;
972
+ return conditions.join(" AND ");
973
+ };
974
+ return {
975
+ sql: buildCondition(condition),
976
+ values,
977
+ functionFilters,
978
+ hasOrWithFunctionFilters
979
+ };
980
+ }
981
+ /**
982
+ * Apply OR condition with function filters by evaluating each row against the condition
983
+ */
984
+ applyOrConditionWithFilters(rows, condition) {
985
+ return rows.filter((row) => this.matchesOrCondition(row, condition));
986
+ }
987
+ /**
988
+ * Check if a row matches an OR/AND condition (recursively)
989
+ */
990
+ matchesOrCondition(row, condition) {
991
+ if (Array.isArray(condition)) return condition.some((item) => this.matchesOrCondition(row, item));
992
+ return Object.entries(condition).every(([key, value]) => {
993
+ const rowValue = row[key];
994
+ if (typeof value === "function") return value(rowValue);
995
+ return rowValue === value;
996
+ });
997
+ }
998
+ /**
999
+ * Apply function filters to rows
1000
+ */
1001
+ applyFunctionFilters(rows, functionFilters) {
1002
+ if (functionFilters.length === 0) return rows;
1003
+ return rows.filter((row) => {
1004
+ return functionFilters.every(({ key, fn }) => {
1005
+ const value = row[key];
1006
+ return fn(value);
1007
+ });
1008
+ });
1009
+ }
1010
+ /**
1011
+ * Get table schema
1012
+ */
1013
+ getSchema(tableName) {
1014
+ return this.schemas.get(tableName);
1015
+ }
1016
+ /**
1017
+ * Get all table names
1018
+ */
1019
+ getTableNames() {
1020
+ return Array.from(this.schemas.keys());
1021
+ }
1022
+ /**
1023
+ * Sync a specific table back to its JSONL file
1024
+ * Uses backward transformation when available
1025
+ */
1026
+ async syncTable(tableName) {
1027
+ const tableConfig = this.tables.get(tableName);
1028
+ if (!tableConfig) throw new Error(`Table ${tableName} not found`);
1029
+ const deserializedRows = this.query(`SELECT * FROM ${this.quoteTableName(tableName)}`).map((row) => this.deserializeRow(tableName, row));
1030
+ const validationSchema = this.validationSchemas.get(tableName);
1031
+ let finalRows = deserializedRows;
1032
+ if (validationSchema && hasBackward(validationSchema)) {
1033
+ const biSchema = validationSchema;
1034
+ finalRows = deserializedRows.map((row) => biSchema.backward(row));
1035
+ }
1036
+ await JsonlWriter.write(tableConfig.jsonlPath, finalRows);
1037
+ }
1038
+ /**
1039
+ * Sync database changes back to JSONL files
1040
+ * Uses backward transformation when available
1041
+ */
1042
+ async sync() {
1043
+ for (const [tableName] of this.tables) await this.syncTable(tableName);
1044
+ }
1045
+ /**
1046
+ * Execute a function within a transaction
1047
+ * Automatically commits on success or rolls back on error
1048
+ */
1049
+ async transaction(fn) {
1050
+ if (this.inTransaction) throw new Error("Nested transactions are not supported");
1051
+ this.db.exec("BEGIN TRANSACTION");
1052
+ this.inTransaction = true;
1053
+ try {
1054
+ const result = await fn(this);
1055
+ this.db.exec("COMMIT");
1056
+ this.inTransaction = false;
1057
+ await this.sync();
1058
+ return result;
1059
+ } catch (error) {
1060
+ this.db.exec("ROLLBACK");
1061
+ this.inTransaction = false;
1062
+ throw error;
1063
+ }
1064
+ }
1065
+ /**
1066
+ * Close the database connection
1067
+ */
1068
+ async close() {
1069
+ try {
1070
+ this.db.close();
1071
+ } catch (_error) {}
1072
+ }
1073
+ /**
1074
+ * Get the underlying SQLite database instance
1075
+ */
1076
+ getDb() {
1077
+ return this.db;
1078
+ }
1079
+ };
1080
+
1081
+ //#endregion
1082
+ //#region src/error-formatter.ts
1083
+ var ErrorFormatter = class {
1084
+ verbose;
1085
+ constructor(options = {}) {
1086
+ this.verbose = options.verbose ?? false;
1087
+ }
1088
+ /**
1089
+ * Format validation errors
1090
+ */
1091
+ formatValidationErrors(errors) {
1092
+ if (this.verbose) return this.formatValidationErrorsVerbose(errors);
1093
+ return this.formatValidationErrorsCompact(errors);
1094
+ }
1095
+ /**
1096
+ * Format foreign key error
1097
+ */
1098
+ formatForeignKeyError(error) {
1099
+ if (this.verbose) return this.formatForeignKeyErrorVerbose(error);
1100
+ return this.formatForeignKeyErrorCompact(error);
1101
+ }
1102
+ /**
1103
+ * Format compact (default) validation errors
1104
+ */
1105
+ formatValidationErrorsCompact(errors) {
1106
+ const lines = [];
1107
+ for (const error of errors) for (const issue of error.issues) {
1108
+ const fieldPath = this.getFieldPath(issue);
1109
+ const line = `${error.file}:${error.rowIndex + 1} • ${fieldPath}: ${issue.message}`;
1110
+ lines.push(line);
1111
+ }
1112
+ return lines.join("\n");
1113
+ }
1114
+ /**
1115
+ * Format verbose validation errors
1116
+ */
1117
+ formatValidationErrorsVerbose(errors) {
1118
+ const blocks = [];
1119
+ for (let i = 0; i < errors.length; i++) {
1120
+ const error = errors[i];
1121
+ const isLast = i === errors.length - 1;
1122
+ const prefix = isLast ? "└─" : "├─";
1123
+ const linePrefix = isLast ? " " : "│ ";
1124
+ const lines = [`${prefix} ${error.file}:${error.rowIndex + 1}`];
1125
+ for (const issue of error.issues) {
1126
+ const fieldPath = this.getFieldPath(issue);
1127
+ lines.push(`${linePrefix}Field: ${fieldPath}`);
1128
+ lines.push(`${linePrefix}Error: ${issue.message}`);
1129
+ }
1130
+ if (error.originalData !== void 0) lines.push(`${linePrefix}Original data: ${JSON.stringify(error.originalData)}`);
1131
+ if (error.data !== void 0) {
1132
+ const label = error.originalData !== void 0 ? "Transformed data" : "Data";
1133
+ lines.push(`${linePrefix}${label}: ${JSON.stringify(error.data)}`);
1134
+ }
1135
+ blocks.push(lines.join("\n"));
1136
+ }
1137
+ return blocks.join("\n│\n");
1138
+ }
1139
+ /**
1140
+ * Format compact foreign key error
1141
+ */
1142
+ formatForeignKeyErrorCompact(error) {
1143
+ return `${error.file}:${error.rowIndex + 1} • ${error.column}: Foreign key constraint failed - Referenced value ${JSON.stringify(error.value)} does not exist in ${error.referencedTable}(${error.referencedColumn})`;
1144
+ }
1145
+ /**
1146
+ * Format verbose foreign key error
1147
+ */
1148
+ formatForeignKeyErrorVerbose(error) {
1149
+ const lines = [
1150
+ `└─ ${error.file}:${error.rowIndex + 1}`,
1151
+ ` Type: Foreign Key Violation`,
1152
+ ` Field: ${error.column}`,
1153
+ ` Value: ${JSON.stringify(error.value)}`,
1154
+ ` References: ${error.referencedTable}(${error.referencedColumn})`,
1155
+ ` Error: Referenced value does not exist in target table`
1156
+ ];
1157
+ if (error.data !== void 0) lines.push(` Data: ${JSON.stringify(error.data)}`);
1158
+ return lines.join("\n");
1159
+ }
1160
+ /**
1161
+ * Get field path from issue
1162
+ */
1163
+ getFieldPath(issue) {
1164
+ if (!issue.path || issue.path.length === 0) return "root";
1165
+ return issue.path.map((segment) => {
1166
+ if (typeof segment === "object" && segment !== null && "key" in segment) return String(segment.key);
1167
+ return String(segment);
1168
+ }).join(".");
1169
+ }
1170
+ /**
1171
+ * Format error header with count
1172
+ */
1173
+ formatErrorHeader(count, file) {
1174
+ return styleText("red", `✗ Found ${count} error(s)${file ? ` in ${file}` : ""}`);
1175
+ }
1176
+ /**
1177
+ * Format migration failure header
1178
+ */
1179
+ formatMigrationFailureHeader() {
1180
+ return styleText("red", "\n✗ Migration failed and was rolled back");
1181
+ }
1182
+ };
1183
+
1184
+ //#endregion
1185
+ //#region src/cli.ts
1186
+ const originalEmitWarning = process.emitWarning;
1187
+ process.emitWarning = (warning, ...args) => {
1188
+ if (typeof warning === "string" && warning.startsWith("SQLite") && args[0] === "ExperimentalWarning") return;
1189
+ originalEmitWarning(warning, ...args);
1190
+ };
1191
+ const SANDBOX_GLOBALS = Object.freeze({
1192
+ console: Object.freeze({
1193
+ log: console.log.bind(console),
1194
+ warn: console.warn.bind(console),
1195
+ error: console.error.bind(console),
1196
+ info: console.info.bind(console)
1197
+ }),
1198
+ Math,
1199
+ Number,
1200
+ String,
1201
+ Boolean,
1202
+ Date,
1203
+ JSON,
1204
+ RegExp,
1205
+ Array,
1206
+ Object
1207
+ });
1208
+ function runInSandbox(expression, context = {}) {
1209
+ const sandbox = Object.create(null);
1210
+ Object.assign(sandbox, SANDBOX_GLOBALS, context);
1211
+ sandbox.globalThis = sandbox;
1212
+ return runInNewContext(expression, sandbox, { timeout: 1e3 });
1213
+ }
1214
+ const program = new Command();
1215
+ program.name("lines-db").description("Database utilities for JSONL files").version("1.0.0");
1216
+ program.command("generate").description("Generate TypeScript type definitions from schema files").argument("<dataDir>", "Directory containing JSONL and schema files").action(async (dataDir) => {
1217
+ try {
1218
+ await new TypeGenerator({ dataDir }).generate();
1219
+ console.log("Type generation completed successfully!");
1220
+ } catch (error) {
1221
+ console.error("Error:", error instanceof Error ? error.message : String(error));
1222
+ process.exit(1);
1223
+ }
1224
+ });
1225
+ program.command("validate").description("Validate JSONL file(s) against schema").argument("<path>", "File or directory path to validate").option("-v, --verbose", "Show verbose error output", false).action(async (path, options) => {
1226
+ try {
1227
+ const result = await new Validator({ path }).validate();
1228
+ if (result.valid) {
1229
+ console.log("✓ All records are valid");
1230
+ process.exit(0);
1231
+ } else {
1232
+ const formatter = new ErrorFormatter({ verbose: options.verbose });
1233
+ const errorsByFile = /* @__PURE__ */ new Map();
1234
+ for (const error of result.errors) {
1235
+ const fileErrors = errorsByFile.get(error.file) || [];
1236
+ fileErrors.push(error);
1237
+ errorsByFile.set(error.file, fileErrors);
1238
+ }
1239
+ for (const [file, fileErrors] of errorsByFile) {
1240
+ console.error(formatter.formatErrorHeader(fileErrors.length, file));
1241
+ console.error("");
1242
+ const validationErrors = fileErrors.filter((e) => e.type !== "foreignKey" || !e.foreignKeyError);
1243
+ const foreignKeyErrors = fileErrors.filter((e) => e.type === "foreignKey" && e.foreignKeyError);
1244
+ if (validationErrors.length > 0) {
1245
+ const formattedValidation = formatter.formatValidationErrors(validationErrors.map((e) => ({
1246
+ file: e.file,
1247
+ rowIndex: e.rowIndex,
1248
+ issues: e.issues
1249
+ })));
1250
+ console.error(formattedValidation);
1251
+ }
1252
+ for (const error of foreignKeyErrors) if (error.foreignKeyError) {
1253
+ const formattedFk = formatter.formatForeignKeyError({
1254
+ file: error.file,
1255
+ rowIndex: error.rowIndex,
1256
+ column: error.foreignKeyError.column,
1257
+ value: error.foreignKeyError.value,
1258
+ referencedTable: error.foreignKeyError.referencedTable,
1259
+ referencedColumn: error.foreignKeyError.referencedColumn
1260
+ });
1261
+ console.error(formattedFk);
1262
+ }
1263
+ console.error("");
1264
+ }
1265
+ process.exit(1);
1266
+ }
1267
+ } catch (error) {
1268
+ console.error("Error:", error instanceof Error ? error.message : String(error));
1269
+ process.exit(1);
1270
+ }
1271
+ });
1272
+ program.command("migrate").description("Migrate data with transformation function").argument("<file>", "JSONL file to migrate").argument("<transform>", "Transform function (e.g., \"(row) => ({ ...row, age: row.age + 1 })\")").option("-f, --filter <expr>", "Filter expression").option("-e, --errorOutput <path>", "Output file path for transformed data when migration fails").option("-v, --verbose", "Show verbose error output", false).action(async (filePath, transformStr, options) => {
1273
+ const tableName = (filePath.split("/").pop() || "").replace(".jsonl", "");
1274
+ if (!tableName) {
1275
+ console.error("Error: Invalid file path. Must be a .jsonl file");
1276
+ process.exit(1);
1277
+ }
1278
+ const lastSlashIndex = filePath.lastIndexOf("/");
1279
+ const dataDir = lastSlashIndex > 0 ? filePath.substring(0, lastSlashIndex) : ".";
1280
+ const db = LinesDB.create({ dataDir });
1281
+ await db.initialize();
1282
+ try {
1283
+ const transform = runInSandbox(`(${transformStr})`);
1284
+ if (typeof transform !== "function") {
1285
+ console.error("Error: Transform must be a function");
1286
+ process.exit(1);
1287
+ }
1288
+ let filter = void 0;
1289
+ if (options.filter) try {
1290
+ filter = JSON.parse(options.filter);
1291
+ } catch {
1292
+ filter = runInSandbox(`(${options.filter})`);
1293
+ }
1294
+ let rowsToMigrate;
1295
+ try {
1296
+ rowsToMigrate = filter ? db.find(tableName, filter) : db.find(tableName);
1297
+ } catch (error) {
1298
+ console.error(`Error: Failed to access table '${tableName}'`);
1299
+ console.error(` ${error instanceof Error ? error.message : String(error)}`);
1300
+ console.error(`\nThe table may have failed to load during initialization.`);
1301
+ console.error(`Check the table's data and schema for any constraint violations.`);
1302
+ await db.close();
1303
+ process.exit(1);
1304
+ }
1305
+ console.log(`Found ${rowsToMigrate.length} row(s) to migrate in table '${tableName}'`);
1306
+ if (rowsToMigrate.length === 0) {
1307
+ console.log("No rows to migrate. Exiting.");
1308
+ await db.close();
1309
+ process.exit(0);
1310
+ }
1311
+ const transformedRows = rowsToMigrate.map((row) => transform(row));
1312
+ console.log("\nApplying migration in transaction...\n");
1313
+ try {
1314
+ await db.transaction(async () => {
1315
+ db.batchUpdate(tableName, transformedRows, { validate: true });
1316
+ });
1317
+ await db.close();
1318
+ console.log(`\nMigration completed successfully:`);
1319
+ console.log(` ✓ ${rowsToMigrate.length} row(s) updated`);
1320
+ process.exit(0);
1321
+ } catch (error) {
1322
+ await db.close();
1323
+ if (options.errorOutput) try {
1324
+ const jsonlContent = transformedRows.map((row) => JSON.stringify(row)).join("\n");
1325
+ await writeFile(options.errorOutput, jsonlContent, "utf-8");
1326
+ console.error(styleText("yellow", `\n⚠ Transformed data (${transformedRows.length} rows) written to: ${options.errorOutput}`));
1327
+ } catch (writeError) {
1328
+ console.error(styleText("red", `\n✗ Failed to write error output file: ${writeError instanceof Error ? writeError.message : String(writeError)}`));
1329
+ }
1330
+ const formatter = new ErrorFormatter({ verbose: options.verbose });
1331
+ console.error(formatter.formatMigrationFailureHeader());
1332
+ if (error instanceof Error && error.name === "ValidationError") {
1333
+ const validationError = error;
1334
+ if (validationError.validationErrors) {
1335
+ console.error(`\nFound ${validationError.validationErrors.length} validation error(s) in transformed data:\n`);
1336
+ const errorInfos = validationError.validationErrors.map(({ rowIndex, rowData, error: rowError }) => ({
1337
+ file: filePath,
1338
+ rowIndex,
1339
+ issues: rowError.issues,
1340
+ data: rowData,
1341
+ originalData: rowsToMigrate[rowIndex]
1342
+ }));
1343
+ const formatted = formatter.formatValidationErrors(errorInfos);
1344
+ console.error(formatted);
1345
+ } else {
1346
+ console.error("\nValidation error:\n");
1347
+ const errorInfo = {
1348
+ file: filePath,
1349
+ rowIndex: 0,
1350
+ issues: validationError.issues
1351
+ };
1352
+ const formatted = formatter.formatValidationErrors([errorInfo]);
1353
+ console.error(formatted);
1354
+ }
1355
+ } else if (error instanceof Error) {
1356
+ console.error(`\n ${error.message}`);
1357
+ if (error.message.includes("UNIQUE constraint failed") || error.message.includes("FOREIGN KEY constraint failed") || error.message.includes("NOT NULL constraint failed") || error.message.includes("CHECK constraint failed")) {
1358
+ console.error("\n This is a SQLite constraint violation.");
1359
+ console.error(" Please check your data and schema requirements.");
1360
+ }
1361
+ } else console.error(`\n ${String(error)}`);
1362
+ console.error("");
1363
+ process.exit(1);
1364
+ }
1365
+ } catch (error) {
1366
+ await db.close();
1367
+ throw error;
1368
+ }
1369
+ });
1370
+ program.parse();
1371
+
1372
+ //#endregion
1373
+ export { };