tova 0.7.0 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/tova.js +1312 -139
- package/package.json +8 -1
- package/src/analyzer/analyzer.js +539 -11
- package/src/analyzer/browser-analyzer.js +56 -8
- package/src/analyzer/deploy-analyzer.js +44 -0
- package/src/analyzer/scope.js +7 -0
- package/src/analyzer/server-analyzer.js +33 -1
- package/src/codegen/base-codegen.js +1296 -23
- package/src/codegen/browser-codegen.js +725 -20
- package/src/codegen/codegen.js +87 -5
- package/src/codegen/deploy-codegen.js +49 -0
- package/src/codegen/server-codegen.js +54 -6
- package/src/codegen/shared-codegen.js +5 -0
- package/src/codegen/theme-codegen.js +69 -0
- package/src/codegen/wasm-codegen.js +6 -0
- package/src/config/edit-toml.js +6 -2
- package/src/config/git-resolver.js +128 -0
- package/src/config/lock-file.js +57 -0
- package/src/config/module-cache.js +58 -0
- package/src/config/module-entry.js +37 -0
- package/src/config/module-path.js +63 -0
- package/src/config/pkg-errors.js +62 -0
- package/src/config/resolve.js +26 -0
- package/src/config/resolver.js +139 -0
- package/src/config/search.js +28 -0
- package/src/config/semver.js +72 -0
- package/src/config/toml.js +61 -6
- package/src/deploy/deploy.js +217 -0
- package/src/deploy/infer.js +218 -0
- package/src/deploy/provision.js +315 -0
- package/src/diagnostics/security-scorecard.js +111 -0
- package/src/lexer/lexer.js +18 -3
- package/src/lsp/server.js +482 -0
- package/src/parser/animate-ast.js +45 -0
- package/src/parser/ast.js +39 -0
- package/src/parser/browser-ast.js +19 -1
- package/src/parser/browser-parser.js +221 -4
- package/src/parser/concurrency-ast.js +15 -0
- package/src/parser/concurrency-parser.js +236 -0
- package/src/parser/deploy-ast.js +37 -0
- package/src/parser/deploy-parser.js +132 -0
- package/src/parser/parser.js +42 -5
- package/src/parser/select-ast.js +39 -0
- package/src/parser/theme-ast.js +29 -0
- package/src/parser/theme-parser.js +70 -0
- package/src/registry/plugins/concurrency-plugin.js +32 -0
- package/src/registry/plugins/deploy-plugin.js +33 -0
- package/src/registry/plugins/theme-plugin.js +20 -0
- package/src/registry/register-all.js +6 -0
- package/src/runtime/charts.js +547 -0
- package/src/runtime/embedded.js +6 -2
- package/src/runtime/reactivity.js +60 -0
- package/src/runtime/router.js +703 -295
- package/src/runtime/table.js +606 -33
- package/src/stdlib/inline.js +365 -10
- package/src/stdlib/runtime-bridge.js +152 -0
- package/src/stdlib/string.js +84 -2
- package/src/stdlib/validation.js +1 -1
- package/src/version.js +1 -1
package/src/runtime/table.js
CHANGED
|
@@ -218,57 +218,96 @@ export function table_limit(table, n) {
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
export function table_join(table, other, opts = {}) {
|
|
221
|
-
const { left, right, how
|
|
221
|
+
const { left, right, how } = opts;
|
|
222
|
+
|
|
223
|
+
// Cross join
|
|
224
|
+
if (how === 'cross') {
|
|
225
|
+
const rows = [];
|
|
226
|
+
const cc = [...new Set([...table._columns, ...other._columns])];
|
|
227
|
+
for (const lr of table._rows) {
|
|
228
|
+
for (const rr of other._rows) {
|
|
229
|
+
rows.push({ ...lr, ...rr });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return new Table(rows, cc);
|
|
233
|
+
}
|
|
234
|
+
|
|
222
235
|
if (!left || !right) throw new Error('join() requires left and right key functions');
|
|
223
236
|
|
|
224
|
-
|
|
225
|
-
|
|
237
|
+
// Anti join
|
|
238
|
+
if (how === 'anti') {
|
|
239
|
+
const ri = new Set();
|
|
240
|
+
for (const r of other._rows) {
|
|
241
|
+
ri.add(String(typeof right === 'function' ? right(r) : r[right]));
|
|
242
|
+
}
|
|
243
|
+
const rows = [];
|
|
244
|
+
for (const lr of table._rows) {
|
|
245
|
+
const k = typeof left === 'function' ? left(lr) : lr[left];
|
|
246
|
+
if (!ri.has(String(k))) rows.push({ ...lr });
|
|
247
|
+
}
|
|
248
|
+
return new Table(rows, [...table._columns]);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Semi join
|
|
252
|
+
if (how === 'semi') {
|
|
253
|
+
const ri = new Set();
|
|
254
|
+
for (const r of other._rows) {
|
|
255
|
+
ri.add(String(typeof right === 'function' ? right(r) : r[right]));
|
|
256
|
+
}
|
|
257
|
+
const rows = [];
|
|
258
|
+
for (const lr of table._rows) {
|
|
259
|
+
const k = typeof left === 'function' ? left(lr) : lr[left];
|
|
260
|
+
if (ri.has(String(k))) rows.push({ ...lr });
|
|
261
|
+
}
|
|
262
|
+
return new Table(rows, [...table._columns]);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Right join — swap and do left join
|
|
266
|
+
if (how === 'right') {
|
|
267
|
+
const swapped = table_join(other, table, { left: right, right: left, how: 'left' });
|
|
268
|
+
const cc = [...new Set([...table._columns, ...other._columns])];
|
|
269
|
+
return new Table(swapped._rows, cc);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Build hash index on right table
|
|
273
|
+
const ri = new Map();
|
|
226
274
|
for (const r of other._rows) {
|
|
227
|
-
const
|
|
228
|
-
const
|
|
229
|
-
if (!
|
|
230
|
-
|
|
275
|
+
const k = typeof right === 'function' ? right(r) : r[right];
|
|
276
|
+
const ks = String(k);
|
|
277
|
+
if (!ri.has(ks)) ri.set(ks, []);
|
|
278
|
+
ri.get(ks).push(r);
|
|
231
279
|
}
|
|
232
280
|
|
|
233
|
-
const
|
|
281
|
+
const cc = [...new Set([...table._columns, ...other._columns])];
|
|
282
|
+
const rows = [];
|
|
283
|
+
const matchedRightKeys = how === 'outer' ? new Set() : null;
|
|
234
284
|
|
|
235
285
|
for (const lr of table._rows) {
|
|
236
|
-
const
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
for (const rr of matches) {
|
|
242
|
-
rows.push({ ...lr, ...rr });
|
|
243
|
-
}
|
|
286
|
+
const k = typeof left === 'function' ? left(lr) : lr[left];
|
|
287
|
+
const ms = ri.get(String(k)) || [];
|
|
288
|
+
if (ms.length > 0) {
|
|
289
|
+
for (const rr of ms) rows.push({ ...lr, ...rr });
|
|
290
|
+
if (matchedRightKeys) matchedRightKeys.add(String(k));
|
|
244
291
|
} else if (how === 'left' || how === 'outer') {
|
|
245
292
|
const row = { ...lr };
|
|
246
|
-
for (const c of other._columns) {
|
|
247
|
-
if (!(c in row)) row[c] = null;
|
|
248
|
-
}
|
|
293
|
+
for (const c of other._columns) { if (!(c in row)) row[c] = null; }
|
|
249
294
|
rows.push(row);
|
|
250
295
|
}
|
|
251
296
|
}
|
|
252
297
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
for (const
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const key = typeof right === 'function' ? right(rr) : rr[right];
|
|
261
|
-
if (!leftIndex.has(String(key))) {
|
|
262
|
-
const row = { ...rr };
|
|
263
|
-
for (const c of table._columns) {
|
|
264
|
-
if (!(c in row)) row[c] = null;
|
|
265
|
-
}
|
|
298
|
+
// Full outer: add unmatched right rows
|
|
299
|
+
if (how === 'outer') {
|
|
300
|
+
for (const r of other._rows) {
|
|
301
|
+
const k = typeof right === 'function' ? right(r) : r[right];
|
|
302
|
+
if (!matchedRightKeys.has(String(k))) {
|
|
303
|
+
const row = { ...r };
|
|
304
|
+
for (const c of table._columns) { if (!(c in row)) row[c] = null; }
|
|
266
305
|
rows.push(row);
|
|
267
306
|
}
|
|
268
307
|
}
|
|
269
308
|
}
|
|
270
309
|
|
|
271
|
-
return new Table(rows,
|
|
310
|
+
return new Table(rows, cc);
|
|
272
311
|
}
|
|
273
312
|
|
|
274
313
|
export function table_pivot(table, opts = {}) {
|
|
@@ -405,6 +444,200 @@ export function agg_max(fn) {
|
|
|
405
444
|
};
|
|
406
445
|
}
|
|
407
446
|
|
|
447
|
+
// ── Window Functions ──────────────────────────────────
|
|
448
|
+
// window() computes values across partitions without collapsing rows.
|
|
449
|
+
// Each win_* factory returns (rows, index, ctx) => value
|
|
450
|
+
|
|
451
|
+
export function win_row_number() {
|
|
452
|
+
return (_rows, index) => index + 1;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export function win_rank() {
|
|
456
|
+
return (_rows, index, ctx) => {
|
|
457
|
+
if (index === 0) return 1;
|
|
458
|
+
const cur = ctx.orderValues[index];
|
|
459
|
+
// Walk backwards to find first row with same value
|
|
460
|
+
for (let i = index - 1; i >= 0; i--) {
|
|
461
|
+
if (ctx.orderValues[i] !== cur) return i + 2;
|
|
462
|
+
}
|
|
463
|
+
return 1;
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export function win_dense_rank() {
|
|
468
|
+
return (_rows, index, ctx) => {
|
|
469
|
+
if (index === 0) return 1;
|
|
470
|
+
let rank = 1;
|
|
471
|
+
for (let i = 1; i <= index; i++) {
|
|
472
|
+
if (ctx.orderValues[i] !== ctx.orderValues[i - 1]) rank++;
|
|
473
|
+
}
|
|
474
|
+
return rank;
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export function win_percent_rank() {
|
|
479
|
+
return (rows, index, ctx) => {
|
|
480
|
+
const n = ctx.partitionSize;
|
|
481
|
+
if (n <= 1) return 0;
|
|
482
|
+
const r = win_rank()(rows, index, ctx);
|
|
483
|
+
return (r - 1) / (n - 1);
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export function win_ntile(buckets) {
|
|
488
|
+
return (_rows, index, ctx) => {
|
|
489
|
+
const n = ctx.partitionSize;
|
|
490
|
+
return Math.floor(index * buckets / n) + 1;
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export function win_lag(colFn, offset = 1, defaultVal = null) {
|
|
495
|
+
return (rows, index) => {
|
|
496
|
+
const target = index - offset;
|
|
497
|
+
if (target < 0 || target >= rows.length) return defaultVal;
|
|
498
|
+
return typeof colFn === 'function' ? colFn(rows[target]) : rows[target][colFn];
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export function win_lead(colFn, offset = 1, defaultVal = null) {
|
|
503
|
+
return (rows, index) => {
|
|
504
|
+
const target = index + offset;
|
|
505
|
+
if (target < 0 || target >= rows.length) return defaultVal;
|
|
506
|
+
return typeof colFn === 'function' ? colFn(rows[target]) : rows[target][colFn];
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export function win_first_value(colFn) {
|
|
511
|
+
return (rows) => {
|
|
512
|
+
if (rows.length === 0) return null;
|
|
513
|
+
return typeof colFn === 'function' ? colFn(rows[0]) : rows[0][colFn];
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export function win_last_value(colFn) {
|
|
518
|
+
return (rows) => {
|
|
519
|
+
if (rows.length === 0) return null;
|
|
520
|
+
const last = rows[rows.length - 1];
|
|
521
|
+
return typeof colFn === 'function' ? colFn(last) : last[colFn];
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export function win_running_sum(colFn) {
|
|
526
|
+
return (rows, index) => {
|
|
527
|
+
let sum = 0;
|
|
528
|
+
for (let i = 0; i <= index; i++) {
|
|
529
|
+
sum += typeof colFn === 'function' ? colFn(rows[i]) : rows[i][colFn];
|
|
530
|
+
}
|
|
531
|
+
return sum;
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
export function win_running_count() {
|
|
536
|
+
return (_rows, index) => index + 1;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export function win_running_avg(colFn) {
|
|
540
|
+
return (rows, index) => {
|
|
541
|
+
let sum = 0;
|
|
542
|
+
for (let i = 0; i <= index; i++) {
|
|
543
|
+
sum += typeof colFn === 'function' ? colFn(rows[i]) : rows[i][colFn];
|
|
544
|
+
}
|
|
545
|
+
return sum / (index + 1);
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export function win_running_min(colFn) {
|
|
550
|
+
return (rows, index) => {
|
|
551
|
+
let m = typeof colFn === 'function' ? colFn(rows[0]) : rows[0][colFn];
|
|
552
|
+
for (let i = 1; i <= index; i++) {
|
|
553
|
+
const v = typeof colFn === 'function' ? colFn(rows[i]) : rows[i][colFn];
|
|
554
|
+
if (v < m) m = v;
|
|
555
|
+
}
|
|
556
|
+
return m;
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
export function win_running_max(colFn) {
|
|
561
|
+
return (rows, index) => {
|
|
562
|
+
let m = typeof colFn === 'function' ? colFn(rows[0]) : rows[0][colFn];
|
|
563
|
+
for (let i = 1; i <= index; i++) {
|
|
564
|
+
const v = typeof colFn === 'function' ? colFn(rows[i]) : rows[i][colFn];
|
|
565
|
+
if (v > m) m = v;
|
|
566
|
+
}
|
|
567
|
+
return m;
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export function win_moving_avg(colFn, windowSize) {
|
|
572
|
+
return (rows, index) => {
|
|
573
|
+
const start = Math.max(0, index - windowSize + 1);
|
|
574
|
+
let sum = 0;
|
|
575
|
+
for (let i = start; i <= index; i++) {
|
|
576
|
+
sum += typeof colFn === 'function' ? colFn(rows[i]) : rows[i][colFn];
|
|
577
|
+
}
|
|
578
|
+
return sum / (index - start + 1);
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export function table_window(table, opts, windowFns) {
|
|
583
|
+
const partitionFn = opts.partition || null;
|
|
584
|
+
const orderFn = opts.order || null;
|
|
585
|
+
const desc = opts.desc || false;
|
|
586
|
+
|
|
587
|
+
// Group rows into partitions
|
|
588
|
+
const partitions = new Map();
|
|
589
|
+
const rowOriginalIndices = [];
|
|
590
|
+
for (let i = 0; i < table._rows.length; i++) {
|
|
591
|
+
const row = table._rows[i];
|
|
592
|
+
const key = partitionFn ? String(typeof partitionFn === 'function' ? partitionFn(row) : row[partitionFn]) : '__all__';
|
|
593
|
+
if (!partitions.has(key)) partitions.set(key, []);
|
|
594
|
+
partitions.get(key).push({ row, originalIndex: i });
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Sort each partition by order key
|
|
598
|
+
if (orderFn) {
|
|
599
|
+
for (const [, items] of partitions) {
|
|
600
|
+
items.sort((a, b) => {
|
|
601
|
+
const ka = typeof orderFn === 'function' ? orderFn(a.row) : a.row[orderFn];
|
|
602
|
+
const kb = typeof orderFn === 'function' ? orderFn(b.row) : b.row[orderFn];
|
|
603
|
+
let cmp = 0;
|
|
604
|
+
if (ka < kb) cmp = -1;
|
|
605
|
+
else if (ka > kb) cmp = 1;
|
|
606
|
+
return desc ? -cmp : cmp;
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Compute window functions per partition, store results by original index
|
|
612
|
+
const results = new Array(table._rows.length);
|
|
613
|
+
for (let i = 0; i < results.length; i++) results[i] = {};
|
|
614
|
+
|
|
615
|
+
for (const [, items] of partitions) {
|
|
616
|
+
const partRows = items.map(it => it.row);
|
|
617
|
+
// Pre-compute order values for rank functions
|
|
618
|
+
const orderValues = orderFn
|
|
619
|
+
? partRows.map(r => typeof orderFn === 'function' ? orderFn(r) : r[orderFn])
|
|
620
|
+
: partRows.map((_, i) => i);
|
|
621
|
+
const ctx = { orderValues, partitionSize: partRows.length };
|
|
622
|
+
|
|
623
|
+
for (const [colName, winFn] of Object.entries(windowFns)) {
|
|
624
|
+
for (let idx = 0; idx < partRows.length; idx++) {
|
|
625
|
+
const val = winFn(partRows, idx, ctx);
|
|
626
|
+
results[items[idx].originalIndex][colName] = val;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Build new rows with original columns + window columns
|
|
632
|
+
const newCols = [...table._columns];
|
|
633
|
+
for (const colName of Object.keys(windowFns)) {
|
|
634
|
+
if (!newCols.includes(colName)) newCols.push(colName);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const newRows = table._rows.map((r, i) => ({ ...r, ...results[i] }));
|
|
638
|
+
return new Table(newRows, newCols);
|
|
639
|
+
}
|
|
640
|
+
|
|
408
641
|
// ── Data Exploration ──────────────────────────────────
|
|
409
642
|
|
|
410
643
|
export function peek(table, opts = {}) {
|
|
@@ -520,3 +753,343 @@ export function filter_err(table) {
|
|
|
520
753
|
const rows = table._rows.filter(r => r && r.__tag === 'Err').map(r => r.error);
|
|
521
754
|
return new Table(rows);
|
|
522
755
|
}
|
|
756
|
+
|
|
757
|
+
// ── Sampling ────────────────────────────────────────────
|
|
758
|
+
|
|
759
|
+
// Seeded PRNG (xorshift128)
|
|
760
|
+
function _xorshift128(seed) {
|
|
761
|
+
let s = [seed, seed ^ 0xDEADBEEF, seed ^ 0x12345678, seed ^ 0x87654321];
|
|
762
|
+
return function() {
|
|
763
|
+
let t = s[3];
|
|
764
|
+
t ^= t << 11;
|
|
765
|
+
t ^= t >>> 8;
|
|
766
|
+
s[3] = s[2]; s[2] = s[1]; s[1] = s[0];
|
|
767
|
+
t ^= s[0]; t ^= s[0] >>> 19;
|
|
768
|
+
s[0] = t;
|
|
769
|
+
return (t >>> 0) / 4294967296;
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
export function table_sample(table, n, opts = {}) {
|
|
774
|
+
const total = table._rows.length;
|
|
775
|
+
let k = n < 1 ? Math.floor(n * total) : Math.min(n, total);
|
|
776
|
+
if (k <= 0) return new Table([], table._columns);
|
|
777
|
+
if (k >= total) return new Table([...table._rows], table._columns);
|
|
778
|
+
|
|
779
|
+
const rng = opts.seed != null ? _xorshift128(opts.seed) : () => Math.random();
|
|
780
|
+
const indices = Array.from({ length: total }, (_, i) => i);
|
|
781
|
+
for (let i = 0; i < k; i++) {
|
|
782
|
+
const j = i + Math.floor(rng() * (total - i));
|
|
783
|
+
[indices[i], indices[j]] = [indices[j], indices[i]];
|
|
784
|
+
}
|
|
785
|
+
const rows = [];
|
|
786
|
+
for (let i = 0; i < k; i++) rows.push(table._rows[indices[i]]);
|
|
787
|
+
return new Table(rows, table._columns);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
export function table_stratified_sample(table, keyFn, n, opts = {}) {
|
|
791
|
+
const groups = new Map();
|
|
792
|
+
for (const row of table._rows) {
|
|
793
|
+
const key = String(typeof keyFn === 'function' ? keyFn(row) : row[keyFn]);
|
|
794
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
795
|
+
groups.get(key).push(row);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const allRows = [];
|
|
799
|
+
let gi = 0;
|
|
800
|
+
for (const [, groupRows] of groups) {
|
|
801
|
+
const groupTable = new Table(groupRows, table._columns);
|
|
802
|
+
const groupOpts = opts.seed != null ? { seed: opts.seed + gi * 7919 } : {};
|
|
803
|
+
const sampled = table_sample(groupTable, n, groupOpts);
|
|
804
|
+
allRows.push(...sampled._rows);
|
|
805
|
+
gi++;
|
|
806
|
+
}
|
|
807
|
+
return new Table(allRows, table._columns);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// ── SQLite Connector ──────────────────────────────────
|
|
811
|
+
|
|
812
|
+
let _SqliteDatabase = null;
|
|
813
|
+
function _getSqliteDatabase() {
|
|
814
|
+
if (_SqliteDatabase) return _SqliteDatabase;
|
|
815
|
+
try {
|
|
816
|
+
_SqliteDatabase = globalThis.Bun
|
|
817
|
+
? require('bun:sqlite').Database
|
|
818
|
+
: require('better-sqlite3');
|
|
819
|
+
} catch {
|
|
820
|
+
throw new Error('SQLite requires Bun (built-in) or "better-sqlite3" package under Node');
|
|
821
|
+
}
|
|
822
|
+
return _SqliteDatabase;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
export function tova_sqlite(path) {
|
|
826
|
+
const Database = _getSqliteDatabase();
|
|
827
|
+
const db = new Database(path);
|
|
828
|
+
|
|
829
|
+
function _inferSqliteType(value) {
|
|
830
|
+
if (value === null || value === undefined) return 'TEXT';
|
|
831
|
+
if (typeof value === 'boolean') return 'INTEGER';
|
|
832
|
+
if (typeof value === 'number') return Number.isInteger(value) ? 'INTEGER' : 'REAL';
|
|
833
|
+
return 'TEXT';
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
return {
|
|
837
|
+
_isTovaSqlite: true,
|
|
838
|
+
|
|
839
|
+
query(sql, params = []) {
|
|
840
|
+
const stmt = db.prepare(sql);
|
|
841
|
+
const rows = stmt.all(...params);
|
|
842
|
+
return new Table(rows);
|
|
843
|
+
},
|
|
844
|
+
|
|
845
|
+
exec(sql, params = []) {
|
|
846
|
+
const stmt = db.prepare(sql);
|
|
847
|
+
const result = stmt.run(...params);
|
|
848
|
+
return { changes: result.changes };
|
|
849
|
+
},
|
|
850
|
+
|
|
851
|
+
writeTable(tableData, tableName, opts = {}) {
|
|
852
|
+
const t = tableData instanceof Table ? tableData : new Table(tableData);
|
|
853
|
+
if (t._rows.length === 0) return;
|
|
854
|
+
|
|
855
|
+
if (!opts.append) {
|
|
856
|
+
db.run(`DROP TABLE IF EXISTS "${tableName}"`);
|
|
857
|
+
const colDefs = t._columns.map(c => {
|
|
858
|
+
const sampleVal = t._rows[0][c];
|
|
859
|
+
return `"${c}" ${_inferSqliteType(sampleVal)}`;
|
|
860
|
+
}).join(', ');
|
|
861
|
+
db.run(`CREATE TABLE "${tableName}" (${colDefs})`);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const placeholders = t._columns.map(() => '?').join(', ');
|
|
865
|
+
const insertSql = `INSERT INTO "${tableName}" (${t._columns.map(c => `"${c}"`).join(', ')}) VALUES (${placeholders})`;
|
|
866
|
+
const insert = db.prepare(insertSql);
|
|
867
|
+
|
|
868
|
+
const transaction = db.transaction((rows) => {
|
|
869
|
+
for (const row of rows) {
|
|
870
|
+
const values = t._columns.map(c => {
|
|
871
|
+
const v = row[c];
|
|
872
|
+
if (v === undefined || v === null) return null;
|
|
873
|
+
if (typeof v === 'boolean') return v ? 1 : 0;
|
|
874
|
+
return v;
|
|
875
|
+
});
|
|
876
|
+
insert.run(...values);
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
transaction(t._rows);
|
|
880
|
+
},
|
|
881
|
+
|
|
882
|
+
close() {
|
|
883
|
+
db.close();
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// ── Parquet Read/Write ──────────────────────────────
|
|
889
|
+
|
|
890
|
+
// Infer Arrow type from a JS value for column type detection
|
|
891
|
+
function _inferArrowType(values) {
|
|
892
|
+
for (const v of values) {
|
|
893
|
+
if (v === null || v === undefined) continue;
|
|
894
|
+
if (typeof v === 'boolean') return 'bool';
|
|
895
|
+
if (typeof v === 'number') return Number.isInteger(v) ? 'int' : 'float';
|
|
896
|
+
if (typeof v === 'string') return 'string';
|
|
897
|
+
}
|
|
898
|
+
return 'string'; // default for all-null columns
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
const COMPRESSION_MAP = {
|
|
902
|
+
snappy: 1,
|
|
903
|
+
gzip: 2,
|
|
904
|
+
brotli: 3,
|
|
905
|
+
zstd: 5,
|
|
906
|
+
lz4: 6,
|
|
907
|
+
uncompressed: 0,
|
|
908
|
+
none: 0,
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
export async function readParquet(path) {
|
|
912
|
+
const fs = await import('fs');
|
|
913
|
+
const arrow = await import('apache-arrow');
|
|
914
|
+
const pw = await import('parquet-wasm/node');
|
|
915
|
+
|
|
916
|
+
const fileBytes = new Uint8Array(fs.readFileSync(path));
|
|
917
|
+
const wasmTable = pw.readParquet(fileBytes);
|
|
918
|
+
const ipcBytes = wasmTable.intoIPCStream();
|
|
919
|
+
const arrowTable = arrow.tableFromIPC(ipcBytes);
|
|
920
|
+
|
|
921
|
+
const columns = arrowTable.schema.fields.map(f => f.name);
|
|
922
|
+
const rows = [];
|
|
923
|
+
for (let i = 0; i < arrowTable.numRows; i++) {
|
|
924
|
+
const row = {};
|
|
925
|
+
for (const col of columns) {
|
|
926
|
+
const val = arrowTable.getChild(col).get(i);
|
|
927
|
+
row[col] = val === undefined ? null : val;
|
|
928
|
+
}
|
|
929
|
+
rows.push(row);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
return new Table(rows, columns);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
export async function writeParquet(table, path, opts = {}) {
|
|
936
|
+
const fs = await import('fs');
|
|
937
|
+
const arrow = await import('apache-arrow');
|
|
938
|
+
const pw = await import('parquet-wasm/node');
|
|
939
|
+
|
|
940
|
+
const t = table instanceof Table ? table : new Table(table);
|
|
941
|
+
const columns = t._columns;
|
|
942
|
+
|
|
943
|
+
// Build Arrow column vectors with proper type inference
|
|
944
|
+
const columnVectors = {};
|
|
945
|
+
for (const col of columns) {
|
|
946
|
+
const values = t._rows.map(r => {
|
|
947
|
+
const v = r[col];
|
|
948
|
+
return v === undefined ? null : v;
|
|
949
|
+
});
|
|
950
|
+
const arrowType = _inferArrowType(values);
|
|
951
|
+
|
|
952
|
+
if (arrowType === 'int') {
|
|
953
|
+
columnVectors[col] = arrow.vectorFromArray(values, new arrow.Int32());
|
|
954
|
+
} else if (arrowType === 'float') {
|
|
955
|
+
columnVectors[col] = arrow.vectorFromArray(values, new arrow.Float64());
|
|
956
|
+
} else if (arrowType === 'bool') {
|
|
957
|
+
columnVectors[col] = arrow.vectorFromArray(values, new arrow.Bool());
|
|
958
|
+
} else {
|
|
959
|
+
columnVectors[col] = arrow.vectorFromArray(values, new arrow.Utf8());
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
const arrowTable = new arrow.Table(columnVectors);
|
|
964
|
+
const ipcBytes = arrow.tableToIPC(arrowTable, 'stream');
|
|
965
|
+
const wasmTable = pw.Table.fromIPCStream(ipcBytes);
|
|
966
|
+
|
|
967
|
+
// Build writer properties
|
|
968
|
+
let writerProps = null;
|
|
969
|
+
const compression = opts.compression || 'snappy';
|
|
970
|
+
const compCode = COMPRESSION_MAP[compression.toLowerCase()];
|
|
971
|
+
if (compCode !== undefined) {
|
|
972
|
+
writerProps = new pw.WriterPropertiesBuilder()
|
|
973
|
+
.setCompression(compCode)
|
|
974
|
+
.build();
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const parquetBytes = pw.writeParquet(wasmTable, writerProps);
|
|
978
|
+
fs.writeFileSync(path, parquetBytes);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// ── Excel Read/Write ──────────────────────────────────
|
|
982
|
+
|
|
983
|
+
export async function readExcel(path, opts = {}) {
|
|
984
|
+
const ExcelJS = require('exceljs');
|
|
985
|
+
const workbook = new ExcelJS.Workbook();
|
|
986
|
+
await workbook.xlsx.readFile(path);
|
|
987
|
+
|
|
988
|
+
// Get worksheet by name (string), index (number), or first sheet
|
|
989
|
+
let worksheet;
|
|
990
|
+
if (typeof opts.sheet === 'string') {
|
|
991
|
+
worksheet = workbook.getWorksheet(opts.sheet);
|
|
992
|
+
if (!worksheet) throw new Error(`Sheet "${opts.sheet}" not found`);
|
|
993
|
+
} else if (typeof opts.sheet === 'number') {
|
|
994
|
+
// opts.sheet is 1-based index into the worksheets array
|
|
995
|
+
const sheets = workbook.worksheets;
|
|
996
|
+
if (opts.sheet < 1 || opts.sheet > sheets.length) {
|
|
997
|
+
throw new Error(`Sheet index ${opts.sheet} out of range (1-${sheets.length})`);
|
|
998
|
+
}
|
|
999
|
+
worksheet = sheets[opts.sheet - 1];
|
|
1000
|
+
} else {
|
|
1001
|
+
worksheet = workbook.worksheets[0];
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
if (!worksheet) throw new Error('No worksheets found in workbook');
|
|
1005
|
+
|
|
1006
|
+
// Extract header row (row 1)
|
|
1007
|
+
const headerRow = worksheet.getRow(1);
|
|
1008
|
+
const columns = [];
|
|
1009
|
+
headerRow.eachCell({ includeEmpty: false }, (cell, colNumber) => {
|
|
1010
|
+
columns[colNumber] = _excelCellValue(cell);
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
// Filter out empty slots to get contiguous column names, but keep position mapping
|
|
1014
|
+
const colMap = []; // colMap[i] = { colNumber, name }
|
|
1015
|
+
for (let i = 1; i < columns.length; i++) {
|
|
1016
|
+
if (columns[i] !== undefined && columns[i] !== null) {
|
|
1017
|
+
colMap.push({ colNumber: i, name: String(columns[i]) });
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
if (colMap.length === 0) {
|
|
1022
|
+
return new Table([], []);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
const columnNames = colMap.map(c => c.name);
|
|
1026
|
+
|
|
1027
|
+
// Iterate data rows (starting from row 2)
|
|
1028
|
+
const rows = [];
|
|
1029
|
+
const rowCount = worksheet.rowCount;
|
|
1030
|
+
for (let r = 2; r <= rowCount; r++) {
|
|
1031
|
+
const excelRow = worksheet.getRow(r);
|
|
1032
|
+
const row = {};
|
|
1033
|
+
for (const { colNumber, name } of colMap) {
|
|
1034
|
+
const cell = excelRow.getCell(colNumber);
|
|
1035
|
+
const val = _excelCellValue(cell);
|
|
1036
|
+
row[name] = val;
|
|
1037
|
+
}
|
|
1038
|
+
rows.push(row);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
return new Table(rows, columnNames);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function _excelCellValue(cell) {
|
|
1045
|
+
if (!cell || cell.type === 0 /* Null */) return null;
|
|
1046
|
+
const val = cell.value;
|
|
1047
|
+
if (val === null || val === undefined) return null;
|
|
1048
|
+
// Formula cells: use the computed result
|
|
1049
|
+
if (typeof val === 'object' && val.formula !== undefined) {
|
|
1050
|
+
const result = val.result;
|
|
1051
|
+
if (result === null || result === undefined) return null;
|
|
1052
|
+
if (result instanceof Date) return result;
|
|
1053
|
+
return result;
|
|
1054
|
+
}
|
|
1055
|
+
// RichText cells: concatenate text parts
|
|
1056
|
+
if (typeof val === 'object' && val.richText) {
|
|
1057
|
+
return val.richText.map(part => part.text).join('');
|
|
1058
|
+
}
|
|
1059
|
+
// Hyperlink cells: return the text
|
|
1060
|
+
if (typeof val === 'object' && val.hyperlink) {
|
|
1061
|
+
return val.text || val.hyperlink;
|
|
1062
|
+
}
|
|
1063
|
+
// Error cells
|
|
1064
|
+
if (typeof val === 'object' && val.error) {
|
|
1065
|
+
return null;
|
|
1066
|
+
}
|
|
1067
|
+
// Date, number, string, boolean pass through
|
|
1068
|
+
return val;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
export async function writeExcel(table, path, opts = {}) {
|
|
1072
|
+
const ExcelJS = require('exceljs');
|
|
1073
|
+
const t = table instanceof Table ? table : new Table(table);
|
|
1074
|
+
const workbook = new ExcelJS.Workbook();
|
|
1075
|
+
const sheetName = opts.sheet || 'Sheet1';
|
|
1076
|
+
const worksheet = workbook.addWorksheet(sheetName);
|
|
1077
|
+
|
|
1078
|
+
// Add header row
|
|
1079
|
+
const cols = t._columns;
|
|
1080
|
+
if (cols.length > 0) {
|
|
1081
|
+
worksheet.addRow(cols);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// Add data rows
|
|
1085
|
+
for (const row of t._rows) {
|
|
1086
|
+
const values = cols.map(c => {
|
|
1087
|
+
const v = row[c];
|
|
1088
|
+
if (v === undefined) return null;
|
|
1089
|
+
return v;
|
|
1090
|
+
});
|
|
1091
|
+
worksheet.addRow(values);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
await workbook.xlsx.writeFile(path);
|
|
1095
|
+
}
|