@dbsp/cli 1.0.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/README.md +64 -0
- package/dist/chunk-AQC34IO5.js +107 -0
- package/dist/chunk-AQC34IO5.js.map +1 -0
- package/dist/chunk-U5DSGBS2.js +123 -0
- package/dist/chunk-U5DSGBS2.js.map +1 -0
- package/dist/chunk-UZEMCTNH.js +243 -0
- package/dist/chunk-UZEMCTNH.js.map +1 -0
- package/dist/chunk-ZSGVJFWG.js +2304 -0
- package/dist/chunk-ZSGVJFWG.js.map +1 -0
- package/dist/generators/schema-codegen.d.ts +39 -0
- package/dist/generators/schema-codegen.js +7 -0
- package/dist/generators/schema-codegen.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1138 -0
- package/dist/index.js.map +1 -0
- package/dist/repl/batch.d.ts +343 -0
- package/dist/repl/batch.js +407 -0
- package/dist/repl/batch.js.map +1 -0
- package/dist/repl-4OFERLKZ.js +1454 -0
- package/dist/repl-4OFERLKZ.js.map +1 -0
- package/dist/utils/schema-loader.d.ts +47 -0
- package/dist/utils/schema-loader.js +15 -0
- package/dist/utils/schema-loader.js.map +1 -0
- package/package.json +94 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ReplEngine,
|
|
3
|
+
processDotCommand
|
|
4
|
+
} from "../chunk-ZSGVJFWG.js";
|
|
5
|
+
import "../chunk-U5DSGBS2.js";
|
|
6
|
+
|
|
7
|
+
// src/repl/batch.ts
|
|
8
|
+
import { readFileSync } from "fs";
|
|
9
|
+
import { isOverallSuccess } from "@dbsp/core";
|
|
10
|
+
|
|
11
|
+
// src/repl/assertion-parser.ts
|
|
12
|
+
import {
|
|
13
|
+
ASSERTION_TYPES,
|
|
14
|
+
parseAssertionFile,
|
|
15
|
+
requiresDatabase,
|
|
16
|
+
resolveQueryIndex,
|
|
17
|
+
validateAssertionBlocks
|
|
18
|
+
} from "@dbsp/core";
|
|
19
|
+
|
|
20
|
+
// src/repl/assertion-runner.ts
|
|
21
|
+
import { normalizeSQL, runAssertions } from "@dbsp/core";
|
|
22
|
+
|
|
23
|
+
// src/repl/batch-internals.ts
|
|
24
|
+
function coalesceContinuations(lines) {
|
|
25
|
+
const result = [];
|
|
26
|
+
let pending = "";
|
|
27
|
+
for (const q of lines) {
|
|
28
|
+
const trimmed = q.trim();
|
|
29
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
30
|
+
pending = "";
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (trimmed.endsWith("\\")) {
|
|
34
|
+
pending += (pending ? "\n" : "") + trimmed.slice(0, -1).trimEnd();
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
result.push(pending ? `${pending}
|
|
38
|
+
${trimmed}` : trimmed);
|
|
39
|
+
pending = "";
|
|
40
|
+
}
|
|
41
|
+
if (pending) result.push(pending);
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/repl/output-formatter.ts
|
|
46
|
+
function formatOutput(rows, columns, mode) {
|
|
47
|
+
switch (mode) {
|
|
48
|
+
case "json":
|
|
49
|
+
return formatAsJson(rows);
|
|
50
|
+
case "table":
|
|
51
|
+
return formatAsTable(rows, columns);
|
|
52
|
+
case "csv":
|
|
53
|
+
return formatAsCsv(rows, columns);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function formatAsJson(rows) {
|
|
57
|
+
if (rows.length === 0) {
|
|
58
|
+
return "[]";
|
|
59
|
+
}
|
|
60
|
+
return JSON.stringify(rows, null, 2);
|
|
61
|
+
}
|
|
62
|
+
function formatAsTable(rows, columns) {
|
|
63
|
+
if (rows.length === 0) {
|
|
64
|
+
return "(empty result set)";
|
|
65
|
+
}
|
|
66
|
+
const flattenedRows = rows.map((row) => flattenObject(row));
|
|
67
|
+
const allColumns = getAllColumns(flattenedRows, columns);
|
|
68
|
+
const widths = allColumns.map((col) => {
|
|
69
|
+
const maxDataWidth = Math.max(
|
|
70
|
+
...flattenedRows.map((row) => formatValue(row[col]).length)
|
|
71
|
+
);
|
|
72
|
+
return Math.max(col.length, maxDataWidth);
|
|
73
|
+
});
|
|
74
|
+
const header = allColumns.map((col, i) => col.padEnd(widths[i] ?? 0)).join(" | ");
|
|
75
|
+
const separator = widths.map((w) => "-".repeat(w)).join("-+-");
|
|
76
|
+
const dataRows = flattenedRows.map(
|
|
77
|
+
(row) => allColumns.map((col, i) => formatValue(row[col]).padEnd(widths[i] ?? 0)).join(" | ")
|
|
78
|
+
);
|
|
79
|
+
return [header, separator, ...dataRows].join("\n");
|
|
80
|
+
}
|
|
81
|
+
function formatAsCsv(rows, columns) {
|
|
82
|
+
if (rows.length === 0) {
|
|
83
|
+
return "";
|
|
84
|
+
}
|
|
85
|
+
const flattenedRows = rows.map((row) => flattenObject(row));
|
|
86
|
+
const allColumns = getAllColumns(flattenedRows, columns);
|
|
87
|
+
const header = allColumns.map((col) => escapeCsvValue(col)).join(",");
|
|
88
|
+
const dataRows = flattenedRows.map(
|
|
89
|
+
(row) => allColumns.map((col) => escapeCsvValue(formatValue(row[col]))).join(",")
|
|
90
|
+
);
|
|
91
|
+
return [header, ...dataRows].join("\n");
|
|
92
|
+
}
|
|
93
|
+
function flattenObject(obj, prefix = "") {
|
|
94
|
+
const result = {};
|
|
95
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
96
|
+
const newKey = prefix ? `${prefix}_${key}` : key;
|
|
97
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
98
|
+
Object.assign(
|
|
99
|
+
result,
|
|
100
|
+
flattenObject(value, newKey)
|
|
101
|
+
);
|
|
102
|
+
} else if (Array.isArray(value)) {
|
|
103
|
+
result[newKey] = JSON.stringify(value);
|
|
104
|
+
} else {
|
|
105
|
+
result[newKey] = value;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
function getAllColumns(flattenedRows, baseColumns) {
|
|
111
|
+
const allKeys = /* @__PURE__ */ new Set();
|
|
112
|
+
for (const col of baseColumns) {
|
|
113
|
+
if (flattenedRows.some((row) => col in row)) {
|
|
114
|
+
allKeys.add(col);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
for (const row of flattenedRows) {
|
|
118
|
+
for (const key of Object.keys(row)) {
|
|
119
|
+
allKeys.add(key);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return Array.from(allKeys);
|
|
123
|
+
}
|
|
124
|
+
function formatValue(value) {
|
|
125
|
+
if (value === null || value === void 0) {
|
|
126
|
+
return "null";
|
|
127
|
+
}
|
|
128
|
+
if (typeof value === "string") {
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
return String(value);
|
|
132
|
+
}
|
|
133
|
+
function escapeCsvValue(value) {
|
|
134
|
+
if (value.includes(",") || value.includes("\n") || value.includes('"')) {
|
|
135
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
136
|
+
}
|
|
137
|
+
return value;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/repl/batch.ts
|
|
141
|
+
function mapEventsToBatchResult(query, events, outputMode) {
|
|
142
|
+
const queryResultEvent = events.find((e) => e.type === "query-result");
|
|
143
|
+
const execResultEvent = events.find((e) => e.type === "execution-result");
|
|
144
|
+
const infoEvent = events.find((e) => e.type === "info");
|
|
145
|
+
const errorEvent = events.find((e) => e.type === "error");
|
|
146
|
+
if (queryResultEvent?.type === "query-result") {
|
|
147
|
+
const qr = queryResultEvent.result;
|
|
148
|
+
if (qr.error) {
|
|
149
|
+
return {
|
|
150
|
+
query,
|
|
151
|
+
success: false,
|
|
152
|
+
error: qr.error,
|
|
153
|
+
output: qr.error,
|
|
154
|
+
type: "query"
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
const resultType = qr.intent && qr.intent.type !== "query" && qr.intent.type !== "setOperation" ? "mutation" : "query";
|
|
158
|
+
const opLabel = qr.plan?.strategy ?? "QUERY";
|
|
159
|
+
const outputLines = [`[${opLabel}]`, "", "SQL:", qr.sql];
|
|
160
|
+
if (qr.params.length > 0) {
|
|
161
|
+
outputLines.push(
|
|
162
|
+
"",
|
|
163
|
+
`Parameters: [${qr.params.map((p) => JSON.stringify(p)).join(", ")}]`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
const base = {
|
|
167
|
+
query,
|
|
168
|
+
success: true,
|
|
169
|
+
output: outputLines.join("\n"),
|
|
170
|
+
sql: qr.sql,
|
|
171
|
+
params: qr.params,
|
|
172
|
+
type: resultType,
|
|
173
|
+
...qr.intent && { intent: qr.intent }
|
|
174
|
+
};
|
|
175
|
+
if (execResultEvent?.type === "execution-result") {
|
|
176
|
+
const er = execResultEvent.result;
|
|
177
|
+
if (er.error) {
|
|
178
|
+
base.dbSuccess = false;
|
|
179
|
+
base.error = `Database error: ${er.error}`;
|
|
180
|
+
base.output = `\u274C Error: Database error: ${er.error}`;
|
|
181
|
+
} else {
|
|
182
|
+
base.dbSuccess = true;
|
|
183
|
+
base.rowCount = er.rowCount;
|
|
184
|
+
base.rows = er.rows;
|
|
185
|
+
base.columns = er.columns;
|
|
186
|
+
base.output = [
|
|
187
|
+
...outputLines,
|
|
188
|
+
"",
|
|
189
|
+
`Rows: ${er.rowCount}`,
|
|
190
|
+
formatOutput(er.rows, er.columns, outputMode)
|
|
191
|
+
].join("\n");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return base;
|
|
195
|
+
}
|
|
196
|
+
if (errorEvent?.type === "error") {
|
|
197
|
+
return {
|
|
198
|
+
query,
|
|
199
|
+
success: false,
|
|
200
|
+
error: errorEvent.message,
|
|
201
|
+
output: errorEvent.message,
|
|
202
|
+
type: "command"
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
if (infoEvent?.type === "info") {
|
|
206
|
+
return {
|
|
207
|
+
query,
|
|
208
|
+
success: true,
|
|
209
|
+
output: infoEvent.message,
|
|
210
|
+
type: "command"
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
query,
|
|
215
|
+
success: true,
|
|
216
|
+
output: "",
|
|
217
|
+
type: "command"
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
async function executeBatch(options) {
|
|
221
|
+
const { queries, schema, schemaPath, databaseUrl, assertFile, dbCasing } = options;
|
|
222
|
+
const engine = new ReplEngine({
|
|
223
|
+
schema,
|
|
224
|
+
schemaPath,
|
|
225
|
+
...databaseUrl && { databaseUrl },
|
|
226
|
+
...dbCasing && { dbCasing },
|
|
227
|
+
initialExecMode: !!databaseUrl
|
|
228
|
+
});
|
|
229
|
+
const initEvents = [];
|
|
230
|
+
const unsubInit = engine.on((e) => initEvents.push(e));
|
|
231
|
+
await engine.init();
|
|
232
|
+
unsubInit();
|
|
233
|
+
const initError = initEvents.find((e) => e.type === "init-error");
|
|
234
|
+
if (initError) {
|
|
235
|
+
await engine.destroy();
|
|
236
|
+
throw new Error(`Database connection failed: ${initError.message}`);
|
|
237
|
+
}
|
|
238
|
+
let assertionBlocks;
|
|
239
|
+
if (assertFile) {
|
|
240
|
+
let assertContent;
|
|
241
|
+
try {
|
|
242
|
+
assertContent = readFileSync(assertFile, "utf-8");
|
|
243
|
+
} catch (error) {
|
|
244
|
+
await engine.destroy();
|
|
245
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
246
|
+
throw new Error(
|
|
247
|
+
`Failed to read assertion file: ${assertFile} \u2014 ${message}`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
const parseResult = parseAssertionFile(assertContent);
|
|
251
|
+
if (parseResult.errors.length > 0) {
|
|
252
|
+
await engine.destroy();
|
|
253
|
+
const errorMessages = parseResult.errors.map((err) => `Line ${err.line}: ${err.message}`).join("\n");
|
|
254
|
+
throw new Error(`Assertion file parse errors:
|
|
255
|
+
${errorMessages}`);
|
|
256
|
+
}
|
|
257
|
+
const preExecExecutableQueries = coalesceContinuations(queries);
|
|
258
|
+
const validationErrors = validateAssertionBlocks(
|
|
259
|
+
parseResult.blocks,
|
|
260
|
+
preExecExecutableQueries.length,
|
|
261
|
+
preExecExecutableQueries
|
|
262
|
+
);
|
|
263
|
+
if (validationErrors.length > 0) {
|
|
264
|
+
await engine.destroy();
|
|
265
|
+
const errorMessages = validationErrors.map((err) => `Line ${err.line}: ${err.message}`).join("\n");
|
|
266
|
+
throw new Error(`Assertion validation errors:
|
|
267
|
+
${errorMessages}`);
|
|
268
|
+
}
|
|
269
|
+
assertionBlocks = parseResult.blocks;
|
|
270
|
+
}
|
|
271
|
+
const results = [];
|
|
272
|
+
let outputMode = engine.getState().outputMode ?? "json";
|
|
273
|
+
try {
|
|
274
|
+
for (const query of queries) {
|
|
275
|
+
const events = [];
|
|
276
|
+
const unsub = engine.on((e) => {
|
|
277
|
+
events.push(e);
|
|
278
|
+
if (e.type === "state-change") {
|
|
279
|
+
outputMode = e.state.outputMode ?? outputMode;
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
await engine.submit(query);
|
|
283
|
+
unsub();
|
|
284
|
+
if (events.length === 0) {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const result = mapEventsToBatchResult(query, events, outputMode);
|
|
288
|
+
results.push(result);
|
|
289
|
+
if (events.some((e) => e.type === "exit")) {
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
} finally {
|
|
294
|
+
await engine.destroy();
|
|
295
|
+
}
|
|
296
|
+
let assertionSummary;
|
|
297
|
+
if (assertionBlocks) {
|
|
298
|
+
const hasDb = !!databaseUrl;
|
|
299
|
+
const executableResults = [];
|
|
300
|
+
const executableQueries = [];
|
|
301
|
+
for (const result of results) {
|
|
302
|
+
const q = result.query.trim();
|
|
303
|
+
if (q.length > 0 && !q.startsWith("#")) {
|
|
304
|
+
executableResults.push(result);
|
|
305
|
+
executableQueries.push(result.query);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
assertionSummary = runAssertions(
|
|
309
|
+
assertionBlocks,
|
|
310
|
+
executableResults,
|
|
311
|
+
executableQueries,
|
|
312
|
+
hasDb
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
return { results, assertionSummary };
|
|
316
|
+
}
|
|
317
|
+
async function runBatchMode(options) {
|
|
318
|
+
const { format } = options;
|
|
319
|
+
let execution;
|
|
320
|
+
try {
|
|
321
|
+
execution = await executeBatch(options);
|
|
322
|
+
} catch (error) {
|
|
323
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
324
|
+
if (format === "json") {
|
|
325
|
+
console.log(JSON.stringify({ error: message, status: "error" }));
|
|
326
|
+
} else {
|
|
327
|
+
console.error(`\u274C ${message}`);
|
|
328
|
+
}
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
const { results, assertionSummary } = execution;
|
|
332
|
+
if (format === "text") {
|
|
333
|
+
for (const result of results) {
|
|
334
|
+
console.log(`
|
|
335
|
+
> ${result.query}`);
|
|
336
|
+
if (result.success) {
|
|
337
|
+
console.log(result.output);
|
|
338
|
+
} else {
|
|
339
|
+
console.error(
|
|
340
|
+
result.error ? `\u274C Error: ${result.error}` : result.output
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (assertionSummary && format === "text") {
|
|
346
|
+
console.log("\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
347
|
+
console.log("ASSERTION RESULTS");
|
|
348
|
+
console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
349
|
+
for (const qResult of assertionSummary.results) {
|
|
350
|
+
const icon = qResult.passed ? "\u2705" : "\u274C";
|
|
351
|
+
console.log(
|
|
352
|
+
`
|
|
353
|
+
${icon} Query ${qResult.queryIndex}: ${qResult.query.slice(0, 50)}${qResult.query.length > 50 ? "..." : ""}`
|
|
354
|
+
);
|
|
355
|
+
for (const assertion of qResult.assertions) {
|
|
356
|
+
if (assertion.skipped) {
|
|
357
|
+
console.log(
|
|
358
|
+
` \u23ED ${assertion.type} (skipped: ${assertion.skipReason})`
|
|
359
|
+
);
|
|
360
|
+
} else if (assertion.passed) {
|
|
361
|
+
console.log(` \u2713 ${assertion.type}`);
|
|
362
|
+
} else {
|
|
363
|
+
console.log(` \u2717 ${assertion.type}`);
|
|
364
|
+
console.log("");
|
|
365
|
+
console.log(` Expected: ${JSON.stringify(assertion.expected)}`);
|
|
366
|
+
if (assertion.actual !== void 0) {
|
|
367
|
+
const actualStr = typeof assertion.actual === "string" ? assertion.actual : JSON.stringify(assertion.actual);
|
|
368
|
+
console.log(` Actual: ${actualStr}`);
|
|
369
|
+
}
|
|
370
|
+
console.log("");
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
console.log("\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
375
|
+
console.log(
|
|
376
|
+
`Summary: ${assertionSummary.passed}/${assertionSummary.total} passed`
|
|
377
|
+
);
|
|
378
|
+
if (assertionSummary.failed > 0) {
|
|
379
|
+
console.log(` ${assertionSummary.failed} FAILED`);
|
|
380
|
+
}
|
|
381
|
+
if (assertionSummary.skipped > 0) {
|
|
382
|
+
console.log(` ${assertionSummary.skipped} skipped (no DB)`);
|
|
383
|
+
}
|
|
384
|
+
console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
385
|
+
}
|
|
386
|
+
if (format === "json") {
|
|
387
|
+
const output = {
|
|
388
|
+
queries: results
|
|
389
|
+
};
|
|
390
|
+
if (assertionSummary) {
|
|
391
|
+
output.assertions = assertionSummary;
|
|
392
|
+
}
|
|
393
|
+
console.log(JSON.stringify(output, null, 2));
|
|
394
|
+
}
|
|
395
|
+
const hasFailedQueries = results.some((r) => !isOverallSuccess(r));
|
|
396
|
+
const failed = hasFailedQueries || assertionSummary !== void 0 && assertionSummary.failed > 0;
|
|
397
|
+
if (failed) {
|
|
398
|
+
process.exit(1);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
export {
|
|
402
|
+
executeBatch,
|
|
403
|
+
mapEventsToBatchResult,
|
|
404
|
+
processDotCommand,
|
|
405
|
+
runBatchMode
|
|
406
|
+
};
|
|
407
|
+
//# sourceMappingURL=batch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/repl/batch.ts","../../src/repl/assertion-parser.ts","../../src/repl/assertion-runner.ts","../../src/repl/batch-internals.ts","../../src/repl/output-formatter.ts"],"sourcesContent":["/**\n * CLI-022: Batch Mode for REPL\n *\n * Executes queries from files or command line without interactive UI.\n * Routes all input through ReplEngine.submit() for a single processing path.\n */\n\nimport { readFileSync } from 'node:fs';\nimport type { IntentSummary } from '@dbsp/core';\nimport { isOverallSuccess } from '@dbsp/core';\nimport type { LoadedSchema } from '@dbsp/types';\nimport {\n\tparseAssertionFile,\n\tvalidateAssertionBlocks,\n} from './assertion-parser.js';\nimport { type AssertionSummary, runAssertions } from './assertion-runner.js';\nimport { coalesceContinuations } from './batch-internals.js';\nimport type { EngineEvent } from './engine/engine-types.js';\nimport { ReplEngine } from './engine/repl-engine.js';\nimport { formatOutput } from './output-formatter.js';\n\nexport interface BatchModeOptions {\n\tqueries: string[];\n\tschema: LoadedSchema;\n\tschemaPath: string;\n\tformat: 'text' | 'json';\n\tdatabaseUrl?: string;\n\t/** DEMO-E2E: Path to assertion file (.assert.dbsp) */\n\tassertFile?: string;\n\t/** DB column casing (intuitive: describes what the DB looks like). */\n\tdbCasing?: 'snake_case' | 'camelCase' | 'preserve';\n}\n\nexport interface BatchResult {\n\tquery: string;\n\t/** Compile-only success of the query (NQL compilation passed).\n\t *\n\t * Does NOT reflect DB execution outcome — see `dbSuccess` for that.\n\t * Combine with `dbSuccess` via `@dbsp/core`'s `isOverallSuccess()` for\n\t * end-to-end status; exit-code logic in `runBatchMode` does exactly that. */\n\tsuccess: boolean;\n\t/** DB execution success only (compile-only mode leaves this undefined).\n\t * When present: `true` = DB query executed without error. */\n\tdbSuccess?: boolean;\n\toutput?: string;\n\tsql?: string;\n\tparams?: readonly unknown[];\n\terror?: string;\n\ttype: 'command' | 'query' | 'mutation';\n\t/** Row count from DB execution (for db.* assertions) */\n\trowCount?: number;\n\t/** Column names from DB result (for db.column.exists) */\n\tcolumns?: string[];\n\t/** Row data from DB result (for db.value.equals) */\n\trows?: unknown[];\n\t/** Intent summary for intent.* assertions */\n\tintent?: IntentSummary;\n}\n\n/**\n * Result of executing a batch of queries (without side effects).\n * Used by tests to programmatically run examples.\n */\nexport interface BatchExecutionResult {\n\tresults: BatchResult[];\n\tassertionSummary?: AssertionSummary | undefined;\n}\n\n// Re-export BatchState type for dot-commands compatibility\nexport type { BatchState } from './dot-commands.js';\n// Re-export processDotCommand from dot-commands (used by batch.test.ts)\nexport { processDotCommand } from './dot-commands.js';\n\n/**\n * Map collected engine events to a BatchResult for a single query.\n *\n * Event patterns:\n * NQL success: query-result → [execution-result]\n * NQL error: query-result (with .error)\n * Raw SQL: query-result → [execution-result]\n * Dot command: info | error [+ state-change]\n *\n * @internal Exported for unit testing — not part of the public API.\n */\nexport function mapEventsToBatchResult(\n\tquery: string,\n\tevents: EngineEvent[],\n\toutputMode: 'json' | 'table' | 'csv',\n): BatchResult {\n\tconst queryResultEvent = events.find((e) => e.type === 'query-result');\n\tconst execResultEvent = events.find((e) => e.type === 'execution-result');\n\tconst infoEvent = events.find((e) => e.type === 'info');\n\tconst errorEvent = events.find((e) => e.type === 'error');\n\n\t// --- Query result (NQL or raw SQL) ---\n\tif (queryResultEvent?.type === 'query-result') {\n\t\tconst qr = queryResultEvent.result;\n\n\t\t// Compilation error\n\t\tif (qr.error) {\n\t\t\treturn {\n\t\t\t\tquery,\n\t\t\t\tsuccess: false,\n\t\t\t\terror: qr.error,\n\t\t\t\toutput: qr.error,\n\t\t\t\ttype: 'query',\n\t\t\t};\n\t\t}\n\n\t\t// Determine type from intent\n\t\tconst resultType: 'query' | 'mutation' =\n\t\t\tqr.intent &&\n\t\t\tqr.intent.type !== 'query' &&\n\t\t\tqr.intent.type !== 'setOperation'\n\t\t\t\t? 'mutation'\n\t\t\t\t: 'query';\n\n\t\t// Build output text (compile-only display)\n\t\tconst opLabel = qr.plan?.strategy ?? 'QUERY';\n\t\tconst outputLines = [`[${opLabel}]`, '', 'SQL:', qr.sql];\n\t\tif (qr.params.length > 0) {\n\t\t\toutputLines.push(\n\t\t\t\t'',\n\t\t\t\t`Parameters: [${qr.params.map((p) => JSON.stringify(p)).join(', ')}]`,\n\t\t\t);\n\t\t}\n\n\t\tconst base: BatchResult = {\n\t\t\tquery,\n\t\t\tsuccess: true,\n\t\t\toutput: outputLines.join('\\n'),\n\t\t\tsql: qr.sql,\n\t\t\tparams: qr.params,\n\t\t\ttype: resultType,\n\t\t\t...(qr.intent && { intent: qr.intent }),\n\t\t};\n\n\t\t// Augment with execution result if present\n\t\tif (execResultEvent?.type === 'execution-result') {\n\t\t\tconst er = execResultEvent.result;\n\n\t\t\tif (er.error) {\n\t\t\t\t// Note: `success` is intentionally left as compile-success (true here).\n\t\t\t\t// `dbSuccess: false` signals DB execution failure separately. The\n\t\t\t\t// `overallSuccess` redesign (combining compile + db) is tracked as\n\t\t\t\t// a follow-up — until then, `success` retains compile-only semantics\n\t\t\t\t// to match the GUI sidecar and existing .assert.dbsp files.\n\t\t\t\tbase.dbSuccess = false;\n\t\t\t\tbase.error = `Database error: ${er.error}`;\n\t\t\t\tbase.output = `❌ Error: Database error: ${er.error}`;\n\t\t\t} else {\n\t\t\t\tbase.dbSuccess = true;\n\t\t\t\tbase.rowCount = er.rowCount;\n\t\t\t\tbase.rows = er.rows;\n\t\t\t\tbase.columns = er.columns;\n\t\t\t\tbase.output = [\n\t\t\t\t\t...outputLines,\n\t\t\t\t\t'',\n\t\t\t\t\t`Rows: ${er.rowCount}`,\n\t\t\t\t\tformatOutput(er.rows, er.columns, outputMode),\n\t\t\t\t].join('\\n');\n\t\t\t}\n\t\t}\n\n\t\treturn base;\n\t}\n\n\t// --- Dot command (info/error events) ---\n\tif (errorEvent?.type === 'error') {\n\t\treturn {\n\t\t\tquery,\n\t\t\tsuccess: false,\n\t\t\terror: errorEvent.message,\n\t\t\toutput: errorEvent.message,\n\t\t\ttype: 'command',\n\t\t};\n\t}\n\n\tif (infoEvent?.type === 'info') {\n\t\treturn {\n\t\t\tquery,\n\t\t\tsuccess: true,\n\t\t\toutput: infoEvent.message,\n\t\t\ttype: 'command',\n\t\t};\n\t}\n\n\t// Fallback: events we don't map (exit, clear, show-panel, etc.)\n\treturn {\n\t\tquery,\n\t\tsuccess: true,\n\t\toutput: '',\n\t\ttype: 'command',\n\t};\n}\n\n/**\n * Core batch execution logic — runs queries and optional assertions,\n * returning structured results without printing or calling process.exit().\n *\n * All input is routed through ReplEngine.submit() for a single processing path.\n */\nexport async function executeBatch(\n\toptions: BatchModeOptions,\n): Promise<BatchExecutionResult> {\n\tconst { queries, schema, schemaPath, databaseUrl, assertFile, dbCasing } =\n\t\toptions;\n\n\t// Create engine with same config as interactive REPL\n\tconst engine = new ReplEngine({\n\t\tschema,\n\t\tschemaPath,\n\t\t...(databaseUrl && { databaseUrl }),\n\t\t...(dbCasing && { dbCasing }),\n\t\tinitialExecMode: !!databaseUrl,\n\t});\n\n\t// Initialize (connects to DB if configured)\n\t// Suppress init events (connection messages) — batch doesn't display them\n\tconst initEvents: EngineEvent[] = [];\n\tconst unsubInit = engine.on((e) => initEvents.push(e));\n\tawait engine.init();\n\tunsubInit();\n\n\t// Check for DB connection failure during init (typed event check)\n\tconst initError = initEvents.find((e) => e.type === 'init-error');\n\tif (initError) {\n\t\tawait engine.destroy();\n\t\tthrow new Error(`Database connection failed: ${initError.message}`);\n\t}\n\n\t// Parse assertion file if provided\n\tlet assertionBlocks: Parameters<typeof runAssertions>[0] | undefined;\n\tif (assertFile) {\n\t\tlet assertContent: string;\n\t\ttry {\n\t\t\tassertContent = readFileSync(assertFile, 'utf-8');\n\t\t} catch (error) {\n\t\t\tawait engine.destroy();\n\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\tthrow new Error(\n\t\t\t\t`Failed to read assertion file: ${assertFile} — ${message}`,\n\t\t\t);\n\t\t}\n\t\tconst parseResult = parseAssertionFile(assertContent);\n\n\t\tif (parseResult.errors.length > 0) {\n\t\t\tawait engine.destroy();\n\t\t\tconst errorMessages = parseResult.errors\n\t\t\t\t.map((err) => `Line ${err.line}: ${err.message}`)\n\t\t\t\t.join('\\n');\n\t\t\tthrow new Error(`Assertion file parse errors:\\n${errorMessages}`);\n\t\t}\n\n\t\t// C4 + F2: Validate against executable queries returned by coalesceContinuations.\n\t\t// The helper drops blank + comment lines AND coalesces continuation chains, so\n\t\t// preExecExecutableQueries.length matches what runAssertions receives at runtime\n\t\t// — assertion indexes align with the engine's actual emit order.\n\t\t// engine.submit() internally accumulates lines ending with '\\' and emits no\n\t\t// events until the full statement arrives, so without coalescing here the\n\t\t// validation count would inflate every continuation fragment as a separate slot.\n\t\t// Pass `queries` raw so the helper handles all three cases in one place; an\n\t\t// upstream pre-filter would defeat the flush semantics for the comment-inside-\n\t\t// continuation edge case (engine flushes; pre-filter silently merges).\n\t\tconst preExecExecutableQueries = coalesceContinuations(queries);\n\t\tconst validationErrors = validateAssertionBlocks(\n\t\t\tparseResult.blocks,\n\t\t\tpreExecExecutableQueries.length,\n\t\t\tpreExecExecutableQueries,\n\t\t);\n\t\tif (validationErrors.length > 0) {\n\t\t\tawait engine.destroy();\n\t\t\tconst errorMessages = validationErrors\n\t\t\t\t.map((err) => `Line ${err.line}: ${err.message}`)\n\t\t\t\t.join('\\n');\n\t\t\tthrow new Error(`Assertion validation errors:\\n${errorMessages}`);\n\t\t}\n\n\t\tassertionBlocks = parseResult.blocks;\n\t}\n\n\tconst results: BatchResult[] = [];\n\t// Track output mode from engine state for formatting\n\tlet outputMode: 'json' | 'table' | 'csv' =\n\t\tengine.getState().outputMode ?? 'json';\n\n\ttry {\n\t\tfor (const query of queries) {\n\t\t\t// M-2 (CODEX-4): Coalesce continuation lines before submit.\n\t\t\t// engine.submit() accumulates lines ending with '\\' internally and emits\n\t\t\t// no events until the full statement is submitted. We track whether any\n\t\t\t// events were emitted to skip the synthetic success result for\n\t\t\t// continuation lines.\n\t\t\tconst events: EngineEvent[] = [];\n\t\t\tconst unsub = engine.on((e) => {\n\t\t\t\tevents.push(e);\n\t\t\t\t// Track output mode changes from .output command\n\t\t\t\tif (e.type === 'state-change') {\n\t\t\t\t\toutputMode = e.state.outputMode ?? outputMode;\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tawait engine.submit(query);\n\t\t\tunsub();\n\n\t\t\t// If no events were emitted, this was a continuation line (backslash\n\t\t\t// continuation accumulation inside the engine) — do not add a result.\n\t\t\tif (events.length === 0) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Map events → BatchResult\n\t\t\tconst result = mapEventsToBatchResult(query, events, outputMode);\n\t\t\tresults.push(result);\n\n\t\t\t// M-2 (CODEX-5): '.exit'/'.quit' inside batch terminates the run.\n\t\t\t// The exit event is emitted for .exit/.quit dot commands.\n\t\t\tif (events.some((e) => e.type === 'exit')) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t} finally {\n\t\tawait engine.destroy();\n\t}\n\n\t// Run assertions if provided\n\t// Assertion query indexes count only executable queries (skip comments and blank lines)\n\tlet assertionSummary: AssertionSummary | undefined;\n\tif (assertionBlocks) {\n\t\tconst hasDb = !!databaseUrl;\n\t\tconst executableResults: BatchResult[] = [];\n\t\tconst executableQueries: string[] = [];\n\t\t// Results no longer map 1:1 with queries (continuation lines are skipped),\n\t\t// so filter by result.query directly rather than by queries[i] index.\n\t\tfor (const result of results) {\n\t\t\tconst q = result.query.trim();\n\t\t\tif (q.length > 0 && !q.startsWith('#')) {\n\t\t\t\texecutableResults.push(result);\n\t\t\t\texecutableQueries.push(result.query);\n\t\t\t}\n\t\t}\n\t\tassertionSummary = runAssertions(\n\t\t\tassertionBlocks,\n\t\t\texecutableResults,\n\t\t\texecutableQueries,\n\t\t\thasDb,\n\t\t);\n\t}\n\n\treturn { results, assertionSummary };\n}\n\nexport async function runBatchMode(options: BatchModeOptions): Promise<void> {\n\tconst { format } = options;\n\n\tlet execution: BatchExecutionResult;\n\ttry {\n\t\texecution = await executeBatch(options);\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t// EH-11: In JSON mode, errors go to stdout as JSON (not plain text to stderr)\n\t\tif (format === 'json') {\n\t\t\tconsole.log(JSON.stringify({ error: message, status: 'error' }));\n\t\t} else {\n\t\t\tconsole.error(`❌ ${message}`);\n\t\t}\n\t\tprocess.exit(1);\n\t}\n\n\tconst { results, assertionSummary } = execution;\n\n\t// Output results in text format\n\t// M-3: use result.query — results is no longer 1:1 with queries after\n\t// continuation-line coalescing (CODEX-4). queries[i] would misalign labels.\n\tif (format === 'text') {\n\t\tfor (const result of results) {\n\t\t\tconsole.log(`\\n> ${result.query}`);\n\t\t\tif (result.success) {\n\t\t\t\tconsole.log(result.output);\n\t\t\t} else {\n\t\t\t\tconsole.error(\n\t\t\t\t\tresult.error ? `❌ Error: ${result.error}` : result.output,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Output assertion results in text format\n\tif (assertionSummary && format === 'text') {\n\t\tconsole.log('\\n─────────────────────────────────');\n\t\tconsole.log('ASSERTION RESULTS');\n\t\tconsole.log('─────────────────────────────────');\n\n\t\tfor (const qResult of assertionSummary.results) {\n\t\t\tconst icon = qResult.passed ? '✅' : '❌';\n\t\t\tconsole.log(\n\t\t\t\t`\\n${icon} Query ${qResult.queryIndex}: ${qResult.query.slice(0, 50)}${qResult.query.length > 50 ? '...' : ''}`,\n\t\t\t);\n\n\t\t\tfor (const assertion of qResult.assertions) {\n\t\t\t\tif (assertion.skipped) {\n\t\t\t\t\tconsole.log(\n\t\t\t\t\t\t` ⏭ ${assertion.type} (skipped: ${assertion.skipReason})`,\n\t\t\t\t\t);\n\t\t\t\t} else if (assertion.passed) {\n\t\t\t\t\tconsole.log(` ✓ ${assertion.type}`);\n\t\t\t\t} else {\n\t\t\t\t\t// Vitest-style expected vs actual output\n\t\t\t\t\tconsole.log(` ✗ ${assertion.type}`);\n\t\t\t\t\tconsole.log('');\n\t\t\t\t\tconsole.log(` Expected: ${JSON.stringify(assertion.expected)}`);\n\t\t\t\t\tif (assertion.actual !== undefined) {\n\t\t\t\t\t\tconst actualStr =\n\t\t\t\t\t\t\ttypeof assertion.actual === 'string'\n\t\t\t\t\t\t\t\t? assertion.actual\n\t\t\t\t\t\t\t\t: JSON.stringify(assertion.actual);\n\t\t\t\t\t\tconsole.log(` Actual: ${actualStr}`);\n\t\t\t\t\t}\n\t\t\t\t\tconsole.log('');\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconsole.log('\\n─────────────────────────────────');\n\t\tconsole.log(\n\t\t\t`Summary: ${assertionSummary.passed}/${assertionSummary.total} passed`,\n\t\t);\n\t\tif (assertionSummary.failed > 0) {\n\t\t\tconsole.log(` ${assertionSummary.failed} FAILED`);\n\t\t}\n\t\tif (assertionSummary.skipped > 0) {\n\t\t\tconsole.log(` ${assertionSummary.skipped} skipped (no DB)`);\n\t\t}\n\t\tconsole.log('─────────────────────────────────');\n\t}\n\n\t// Output in JSON format (all at once)\n\tif (format === 'json') {\n\t\tconst output: { queries: BatchResult[]; assertions?: AssertionSummary } = {\n\t\t\tqueries: results,\n\t\t};\n\t\tif (assertionSummary) {\n\t\t\toutput.assertions = assertionSummary;\n\t\t}\n\t\tconsole.log(JSON.stringify(output, null, 2));\n\t}\n\n\t// CODEX-6: Exit code considers BOTH query failures AND assertion failures.\n\t// When assertions are present, exit 1 if any query failed OR any assertion failed.\n\t// When no assertions, exit 1 if any query failed.\n\tconst hasFailedQueries = results.some((r) => !isOverallSuccess(r));\n\tconst failed =\n\t\thasFailedQueries ||\n\t\t(assertionSummary !== undefined && assertionSummary.failed > 0);\n\tif (failed) {\n\t\tprocess.exit(1);\n\t}\n}\n","/**\n * Assertion Parser — re-exported from @dbsp/core.\n *\n * CLI consumers continue importing from this path.\n * Implementation lives in packages/core/src/assert/.\n */\n\nexport type {\n\tAssertion,\n\tAssertionBlock,\n\tAssertionType,\n\tParseError,\n\tParseResult,\n\tTableAssertionData,\n} from '@dbsp/core';\nexport {\n\tASSERTION_TYPES,\n\tparseAssertionFile,\n\trequiresDatabase,\n\tresolveQueryIndex,\n\tvalidateAssertionBlocks,\n} from '@dbsp/core';\n","/**\n * Assertion Runner — re-exported from @dbsp/core.\n *\n * CLI consumers continue importing from this path.\n * Implementation lives in packages/core/src/assert/.\n */\n\nexport type {\n\tAssertionOutcome,\n\tAssertionQueryResult,\n\tAssertionSummary,\n\tQueryAssertionResult,\n} from '@dbsp/core';\nexport { normalizeSQL, runAssertions } from '@dbsp/core';\n","/**\n * Internal helpers for batch mode. Not part of the public API surface.\n *\n * Imported via relative path from batch.ts and from test files. NOT re-exported\n * from the package barrel — the package's public API does not include these\n * helpers, and they may be removed or refactored without notice.\n */\n\n/**\n * Coalesce backslash-continuation lines into single logical query strings,\n * mirroring ReplEngine.submit() semantics:\n * - Lines ending in '\\' are joined with '\\n' to the next non-continuation line\n * - Blank lines and comment lines (starting with '#') flush the continuation\n * buffer and are dropped from the output\n * - Trailing pending text at EOF is emitted as a final entry (so malformed\n * input ending on a continuation is still observable to validators)\n *\n * Used by batch mode to count distinct executable queries before passing them\n * to engine.submit() so that assertion validation counts match what the\n * engine actually executes.\n */\nexport function coalesceContinuations(lines: string[]): string[] {\n\tconst result: string[] = [];\n\tlet pending = '';\n\tfor (const q of lines) {\n\t\tconst trimmed = q.trim();\n\n\t\t// Blank or comment — flush continuation buffer (separator) and skip\n\t\tif (!trimmed || trimmed.startsWith('#')) {\n\t\t\tpending = '';\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Backslash continuation — accumulate and wait for next line\n\t\tif (trimmed.endsWith('\\\\')) {\n\t\t\tpending += (pending ? '\\n' : '') + trimmed.slice(0, -1).trimEnd();\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Merge pending + current\n\t\tresult.push(pending ? `${pending}\\n${trimmed}` : trimmed);\n\t\tpending = '';\n\t}\n\t// EOF flush — only emit if there's accumulated text (matches engine: dangling\n\t// continuation buffer at end-of-input has nothing to merge with, but we keep\n\t// its content available so callers can detect malformed input.)\n\tif (pending) result.push(pending);\n\treturn result;\n}\n","/**\n * NQL v2.1: Output formatters for REPL results\n *\n * Formats query results according to the selected output mode:\n * - json: Nested JSON structure (default, preserves relations)\n * - table: Flattened tabular format (nested objects become columns)\n * - csv: CSV export format (same flattening as table)\n */\n\nexport type OutputMode = 'json' | 'table' | 'csv';\n\n/**\n * Format rows according to the specified output mode\n */\nexport function formatOutput(\n\trows: Record<string, unknown>[],\n\tcolumns: string[],\n\tmode: OutputMode,\n): string {\n\tswitch (mode) {\n\t\tcase 'json':\n\t\t\treturn formatAsJson(rows);\n\t\tcase 'table':\n\t\t\treturn formatAsTable(rows, columns);\n\t\tcase 'csv':\n\t\t\treturn formatAsCsv(rows, columns);\n\t}\n}\n\n/**\n * Format as nested JSON (default mode)\n */\nfunction formatAsJson(rows: Record<string, unknown>[]): string {\n\tif (rows.length === 0) {\n\t\treturn '[]';\n\t}\n\treturn JSON.stringify(rows, null, 2);\n}\n\n/**\n * Format as ASCII table (flattens nested objects)\n */\nfunction formatAsTable(\n\trows: Record<string, unknown>[],\n\tcolumns: string[],\n): string {\n\tif (rows.length === 0) {\n\t\treturn '(empty result set)';\n\t}\n\n\t// Flatten nested objects and get all columns\n\tconst flattenedRows = rows.map((row) => flattenObject(row));\n\tconst allColumns = getAllColumns(flattenedRows, columns);\n\n\t// Calculate column widths\n\tconst widths = allColumns.map((col) => {\n\t\tconst maxDataWidth = Math.max(\n\t\t\t...flattenedRows.map((row) => formatValue(row[col]).length),\n\t\t);\n\t\treturn Math.max(col.length, maxDataWidth);\n\t});\n\n\t// Header row\n\tconst header = allColumns\n\t\t.map((col, i) => col.padEnd(widths[i] ?? 0))\n\t\t.join(' | ');\n\tconst separator = widths.map((w) => '-'.repeat(w)).join('-+-');\n\n\t// Data rows\n\tconst dataRows = flattenedRows.map((row) =>\n\t\tallColumns\n\t\t\t.map((col, i) => formatValue(row[col]).padEnd(widths[i] ?? 0))\n\t\t\t.join(' | '),\n\t);\n\n\treturn [header, separator, ...dataRows].join('\\n');\n}\n\n/**\n * Format as CSV (flattens nested objects)\n */\nfunction formatAsCsv(\n\trows: Record<string, unknown>[],\n\tcolumns: string[],\n): string {\n\tif (rows.length === 0) {\n\t\treturn '';\n\t}\n\n\t// Flatten nested objects and get all columns\n\tconst flattenedRows = rows.map((row) => flattenObject(row));\n\tconst allColumns = getAllColumns(flattenedRows, columns);\n\n\t// Header row\n\tconst header = allColumns.map((col) => escapeCsvValue(col)).join(',');\n\n\t// Data rows\n\tconst dataRows = flattenedRows.map((row) =>\n\t\tallColumns.map((col) => escapeCsvValue(formatValue(row[col]))).join(','),\n\t);\n\n\treturn [header, ...dataRows].join('\\n');\n}\n\n/**\n * Flatten a nested object into a single-level object\n * Uses underscore convention: { a: { b: 1 } } → { \"a_b\": 1 }\n */\nfunction flattenObject(\n\tobj: Record<string, unknown>,\n\tprefix = '',\n): Record<string, unknown> {\n\tconst result: Record<string, unknown> = {};\n\n\tfor (const [key, value] of Object.entries(obj)) {\n\t\tconst newKey = prefix ? `${prefix}_${key}` : key;\n\n\t\tif (value !== null && typeof value === 'object' && !Array.isArray(value)) {\n\t\t\t// Recursively flatten nested objects\n\t\t\tObject.assign(\n\t\t\t\tresult,\n\t\t\t\tflattenObject(value as Record<string, unknown>, newKey),\n\t\t\t);\n\t\t} else if (Array.isArray(value)) {\n\t\t\t// For arrays, JSON stringify them\n\t\t\tresult[newKey] = JSON.stringify(value);\n\t\t} else {\n\t\t\tresult[newKey] = value;\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Get all unique column names from flattened rows\n * Preserves original column order when possible\n */\nfunction getAllColumns(\n\tflattenedRows: Record<string, unknown>[],\n\tbaseColumns: string[],\n): string[] {\n\tconst allKeys = new Set<string>();\n\n\t// First add base columns that exist in flattened data\n\tfor (const col of baseColumns) {\n\t\tif (flattenedRows.some((row) => col in row)) {\n\t\t\tallKeys.add(col);\n\t\t}\n\t}\n\n\t// Then add any new columns from flattening\n\tfor (const row of flattenedRows) {\n\t\tfor (const key of Object.keys(row)) {\n\t\t\tallKeys.add(key);\n\t\t}\n\t}\n\n\treturn Array.from(allKeys);\n}\n\n/**\n * Format a value for display\n */\nfunction formatValue(value: unknown): string {\n\tif (value === null || value === undefined) {\n\t\treturn 'null';\n\t}\n\tif (typeof value === 'string') {\n\t\treturn value;\n\t}\n\treturn String(value);\n}\n\n/**\n * Escape a value for CSV output\n */\nfunction escapeCsvValue(value: string): string {\n\t// If value contains comma, newline, or quote, wrap in quotes\n\tif (value.includes(',') || value.includes('\\n') || value.includes('\"')) {\n\t\treturn `\"${value.replace(/\"/g, '\"\"')}\"`;\n\t}\n\treturn value;\n}\n"],"mappings":"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","names":[]}
|