@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/LICENSE +21 -0
- package/bin/cli.js +1373 -0
- package/dist/index.cjs +1212 -0
- package/dist/index.d.cts +486 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +486 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1181 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
- package/src/cli.ts +333 -0
- package/src/database.test.ts +493 -0
- package/src/database.ts +1025 -0
- package/src/directory-scanner.test.ts +91 -0
- package/src/directory-scanner.ts +38 -0
- package/src/error-formatter.ts +166 -0
- package/src/index.ts +35 -0
- package/src/jsonl-migration.ts +76 -0
- package/src/jsonl-reader.test.ts +168 -0
- package/src/jsonl-reader.ts +135 -0
- package/src/jsonl-writer.test.ts +101 -0
- package/src/jsonl-writer.ts +33 -0
- package/src/runtime.ts +34 -0
- package/src/schema-loader.test.ts +136 -0
- package/src/schema-loader.ts +64 -0
- package/src/schema.ts +135 -0
- package/src/sqlite-adapter.ts +99 -0
- package/src/type-generator.ts +201 -0
- package/src/types.ts +99 -0
- package/src/validator.test.ts +337 -0
- package/src/validator.ts +207 -0
- package/tsconfig.json +20 -0
- package/tsconfig.test.json +8 -0
- package/tsdown.config.ts +26 -0
- package/vitest.config.ts +9 -0
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 { };
|