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.
Files changed (59) hide show
  1. package/bin/tova.js +1312 -139
  2. package/package.json +8 -1
  3. package/src/analyzer/analyzer.js +539 -11
  4. package/src/analyzer/browser-analyzer.js +56 -8
  5. package/src/analyzer/deploy-analyzer.js +44 -0
  6. package/src/analyzer/scope.js +7 -0
  7. package/src/analyzer/server-analyzer.js +33 -1
  8. package/src/codegen/base-codegen.js +1296 -23
  9. package/src/codegen/browser-codegen.js +725 -20
  10. package/src/codegen/codegen.js +87 -5
  11. package/src/codegen/deploy-codegen.js +49 -0
  12. package/src/codegen/server-codegen.js +54 -6
  13. package/src/codegen/shared-codegen.js +5 -0
  14. package/src/codegen/theme-codegen.js +69 -0
  15. package/src/codegen/wasm-codegen.js +6 -0
  16. package/src/config/edit-toml.js +6 -2
  17. package/src/config/git-resolver.js +128 -0
  18. package/src/config/lock-file.js +57 -0
  19. package/src/config/module-cache.js +58 -0
  20. package/src/config/module-entry.js +37 -0
  21. package/src/config/module-path.js +63 -0
  22. package/src/config/pkg-errors.js +62 -0
  23. package/src/config/resolve.js +26 -0
  24. package/src/config/resolver.js +139 -0
  25. package/src/config/search.js +28 -0
  26. package/src/config/semver.js +72 -0
  27. package/src/config/toml.js +61 -6
  28. package/src/deploy/deploy.js +217 -0
  29. package/src/deploy/infer.js +218 -0
  30. package/src/deploy/provision.js +315 -0
  31. package/src/diagnostics/security-scorecard.js +111 -0
  32. package/src/lexer/lexer.js +18 -3
  33. package/src/lsp/server.js +482 -0
  34. package/src/parser/animate-ast.js +45 -0
  35. package/src/parser/ast.js +39 -0
  36. package/src/parser/browser-ast.js +19 -1
  37. package/src/parser/browser-parser.js +221 -4
  38. package/src/parser/concurrency-ast.js +15 -0
  39. package/src/parser/concurrency-parser.js +236 -0
  40. package/src/parser/deploy-ast.js +37 -0
  41. package/src/parser/deploy-parser.js +132 -0
  42. package/src/parser/parser.js +42 -5
  43. package/src/parser/select-ast.js +39 -0
  44. package/src/parser/theme-ast.js +29 -0
  45. package/src/parser/theme-parser.js +70 -0
  46. package/src/registry/plugins/concurrency-plugin.js +32 -0
  47. package/src/registry/plugins/deploy-plugin.js +33 -0
  48. package/src/registry/plugins/theme-plugin.js +20 -0
  49. package/src/registry/register-all.js +6 -0
  50. package/src/runtime/charts.js +547 -0
  51. package/src/runtime/embedded.js +6 -2
  52. package/src/runtime/reactivity.js +60 -0
  53. package/src/runtime/router.js +703 -295
  54. package/src/runtime/table.js +606 -33
  55. package/src/stdlib/inline.js +365 -10
  56. package/src/stdlib/runtime-bridge.js +152 -0
  57. package/src/stdlib/string.js +84 -2
  58. package/src/stdlib/validation.js +1 -1
  59. package/src/version.js +1 -1
@@ -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 = 'inner' } = opts;
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
- const rows = [];
225
- const rightIndex = new Map();
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 key = typeof right === 'function' ? right(r) : r[right];
228
- const keyStr = String(key);
229
- if (!rightIndex.has(keyStr)) rightIndex.set(keyStr, []);
230
- rightIndex.get(keyStr).push(r);
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 combinedCols = [...new Set([...table._columns, ...other._columns])];
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 key = typeof left === 'function' ? left(lr) : lr[left];
237
- const keyStr = String(key);
238
- const matches = rightIndex.get(keyStr) || [];
239
-
240
- if (matches.length > 0) {
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
- if (how === 'right' || how === 'outer') {
254
- const leftIndex = new Set();
255
- for (const lr of table._rows) {
256
- const key = typeof left === 'function' ? left(lr) : lr[left];
257
- leftIndex.add(String(key));
258
- }
259
- for (const rr of other._rows) {
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, combinedCols);
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
+ }