parquetlens 0.2.0 → 0.3.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.
@@ -375,8 +375,31 @@ async function readParquetTableFromStdin(filenameHint = "stdin.parquet", options
375
375
  await temp.cleanup();
376
376
  }
377
377
  }
378
+ async function runSqlOnParquet(input, query) {
379
+ const source = await openParquetSource(input);
380
+ try {
381
+ return await source.runSql(query);
382
+ } finally {
383
+ await source.close();
384
+ }
385
+ }
386
+ async function runSqlOnParquetFromStdin(query, filenameHint = "stdin.parquet") {
387
+ const temp = await bufferStdinToTempFile(filenameHint);
388
+ try {
389
+ return await runSqlOnParquet(temp.path, query);
390
+ } finally {
391
+ await temp.cleanup();
392
+ }
393
+ }
378
394
  function createParquetSource(db, conn, fileName) {
379
395
  let metadataPromise = null;
396
+ let viewCreated = false;
397
+ const ensureDataView = () => {
398
+ if (!viewCreated) {
399
+ conn.query(`CREATE OR REPLACE VIEW data AS SELECT * FROM read_parquet(${quoteLiteral(fileName)})`);
400
+ viewCreated = true;
401
+ }
402
+ };
380
403
  return {
381
404
  readTable: async (options) => {
382
405
  const query = buildSelectQuery(fileName, options);
@@ -388,6 +411,10 @@ function createParquetSource(db, conn, fileName) {
388
411
  }
389
412
  return metadataPromise;
390
413
  },
414
+ runSql: async (query) => {
415
+ ensureDataView();
416
+ return conn.query(query);
417
+ },
391
418
  close: async () => {
392
419
  conn.close();
393
420
  db.dropFile(fileName);
@@ -480,6 +507,7 @@ export {
480
507
  openParquetSource,
481
508
  readParquetTableFromPath,
482
509
  readParquetTableFromUrl,
483
- readParquetTableFromStdin
510
+ readParquetTableFromStdin,
511
+ runSqlOnParquet,
512
+ runSqlOnParquetFromStdin
484
513
  };
485
- //# sourceMappingURL=chunk-NRRDNC7S.js.map
package/dist/main.js CHANGED
@@ -9,14 +9,17 @@ import {
9
9
  readParquetTableFromPath,
10
10
  readParquetTableFromStdin,
11
11
  readParquetTableFromUrl,
12
- resolveParquetUrl
13
- } from "./chunk-NRRDNC7S.js";
12
+ resolveParquetUrl,
13
+ runSqlOnParquet,
14
+ runSqlOnParquetFromStdin
15
+ } from "./chunk-YZLOXMC4.js";
14
16
 
15
17
  // src/main.ts
18
+ import CliTable3 from "cli-table3";
16
19
  import { spawnSync } from "child_process";
17
20
  import path from "path";
18
- import { fileURLToPath } from "url";
19
- var __filename = typeof globalThis.__filename !== "undefined" ? globalThis.__filename : fileURLToPath(import.meta.url);
21
+ import { fileURLToPath as nodeFileURLToPath } from "url";
22
+ var __parquetlens_filename = typeof __filename !== "undefined" ? __filename : nodeFileURLToPath(import.meta.url);
20
23
  var DEFAULT_LIMIT = 20;
21
24
  function parseArgs(argv) {
22
25
  const options = {
@@ -48,6 +51,7 @@ function parseArgs(argv) {
48
51
  }
49
52
  if (arg === "--plain" || arg === "--no-tui") {
50
53
  options.tuiMode = "off";
54
+ options.showSchema = false;
51
55
  continue;
52
56
  }
53
57
  if (arg === "--schema") {
@@ -86,6 +90,14 @@ function parseArgs(argv) {
86
90
  }
87
91
  continue;
88
92
  }
93
+ const sqlValue = readOptionValue(arg, "--sql", argv[i + 1]);
94
+ if (sqlValue) {
95
+ options.sql = sqlValue.value;
96
+ if (sqlValue.usedNext) {
97
+ i += 1;
98
+ }
99
+ continue;
100
+ }
89
101
  if (arg === "-") {
90
102
  if (input) {
91
103
  return { options, limitSpecified, help: false, error: "unexpected extra argument: -" };
@@ -121,6 +133,7 @@ function printUsage() {
121
133
  options:
122
134
  --limit, --limit=<n> number of rows to show (default: ${DEFAULT_LIMIT})
123
135
  --columns, --columns=<c> comma-separated column list
136
+ --sql, --sql=<query> run SQL query (use 'data' as table name)
124
137
  --schema print schema only
125
138
  --no-schema skip schema output
126
139
  --json output rows as json lines
@@ -131,6 +144,7 @@ options:
131
144
  examples:
132
145
  parquetlens data.parquet --limit 25
133
146
  parquetlens data.parquet --columns=city,state
147
+ parquetlens data.parquet --sql "SELECT city, COUNT(*) FROM data GROUP BY city"
134
148
  parquetlens hf://datasets/cfahlgren1/hub-stats/daily_papers.parquet
135
149
  parquetlens https://huggingface.co/datasets/cfahlgren1/hub-stats/resolve/main/daily_papers.parquet
136
150
  parquetlens data.parquet --plain
@@ -164,6 +178,57 @@ function resolveColumns(table, requested) {
164
178
  indices: names.map((name) => nameToIndex.get(name) ?? -1)
165
179
  };
166
180
  }
181
+ function getColumnDefs(table, requestedColumns) {
182
+ const fields = table.schema.fields;
183
+ if (requestedColumns.length === 0) {
184
+ return fields.map((f) => ({ name: f.name, type: String(f.type) }));
185
+ }
186
+ const fieldMap = new Map(fields.map((f) => [f.name, f]));
187
+ return requestedColumns.map((name) => {
188
+ const field = fieldMap.get(name);
189
+ return { name, type: field ? String(field.type) : "unknown" };
190
+ });
191
+ }
192
+ function truncateCell(value, maxWidth) {
193
+ const oneLine = value.replace(/\n/g, " ");
194
+ if (oneLine.length <= maxWidth) {
195
+ return oneLine;
196
+ }
197
+ return oneLine.slice(0, maxWidth - 3) + "...";
198
+ }
199
+ function printTable(rows, columns) {
200
+ if (rows.length === 0) {
201
+ process.stdout.write("(no rows)\n");
202
+ return;
203
+ }
204
+ const termWidth = process.stdout.columns || 120;
205
+ const numCols = columns.length;
206
+ const borderOverhead = 1 + 3 * numCols;
207
+ const availableWidth = Math.max(termWidth - borderOverhead, numCols * 6);
208
+ const idealWidths = columns.map((c) => {
209
+ const headerLen = `${c.name}: ${c.type}`.length;
210
+ const maxContent = rows.reduce((max, row) => {
211
+ const val = String(row[c.name] ?? "").replace(/\n/g, " ");
212
+ return Math.max(max, val.length);
213
+ }, 0);
214
+ return Math.min(60, Math.max(headerLen, maxContent));
215
+ });
216
+ const totalIdeal = idealWidths.reduce((sum, w) => sum + w, 0);
217
+ const scale = Math.min(1, availableWidth / totalIdeal);
218
+ const colWidths = idealWidths.map((ideal) => Math.max(6, Math.floor(ideal * scale)));
219
+ const headers = columns.map((c, i) => truncateCell(`${c.name}: ${c.type}`, colWidths[i]));
220
+ const table = new CliTable3({
221
+ head: headers,
222
+ style: { head: [], border: [] },
223
+ colWidths: colWidths.map((w) => w + 2),
224
+ // +2 for padding
225
+ wordWrap: false
226
+ });
227
+ for (const row of rows) {
228
+ table.push(columns.map((c, i) => truncateCell(String(row[c.name] ?? ""), colWidths[i])));
229
+ }
230
+ process.stdout.write(table.toString() + "\n");
231
+ }
167
232
  function formatCell(value) {
168
233
  if (value === null || value === void 0) {
169
234
  return null;
@@ -232,6 +297,41 @@ async function main() {
232
297
  process.exitCode = 1;
233
298
  return;
234
299
  }
300
+ if (options.sql) {
301
+ const stdinFallback = process.stdin.isTTY ? void 0 : "-";
302
+ const source = input ?? stdinFallback;
303
+ if (!source) {
304
+ process.stderr.write("parquetlens: missing input file for SQL query\n");
305
+ process.exitCode = 1;
306
+ return;
307
+ }
308
+ const table2 = source === "-" ? await runSqlOnParquetFromStdin(options.sql) : await runSqlOnParquet(source, options.sql);
309
+ const wantsSqlTui = !options.json && options.tuiMode !== "off" && source !== "-" && process.stdin.isTTY && process.stdout.isTTY;
310
+ if (wantsSqlTui) {
311
+ if (isBunRuntime()) {
312
+ const { runTuiWithTable } = await importTuiModule();
313
+ const title = `SQL: ${options.sql.slice(0, 50)}${options.sql.length > 50 ? "..." : ""}`;
314
+ await runTuiWithTable(table2, title, { columns: [], maxRows: options.limit });
315
+ return;
316
+ }
317
+ const spawned = spawnBun(process.argv.slice(1));
318
+ if (spawned) {
319
+ return;
320
+ }
321
+ process.stderr.write("parquetlens: bun not found, falling back to plain output\n");
322
+ }
323
+ const sqlColumns = getColumnDefs(table2, []);
324
+ const rows2 = previewRows(table2, options.limit, []);
325
+ if (options.json) {
326
+ for (const row of rows2) {
327
+ process.stdout.write(`${JSON.stringify(row)}
328
+ `);
329
+ }
330
+ } else {
331
+ printTable(rows2, sqlColumns);
332
+ }
333
+ return;
334
+ }
235
335
  const wantsTui = resolveTuiMode(options.tuiMode, options);
236
336
  if (wantsTui) {
237
337
  if (!input || input === "-") {
@@ -276,6 +376,7 @@ rows loaded: ${rowsCount}${limitSuffix}
276
376
  if (options.schemaOnly) {
277
377
  return;
278
378
  }
379
+ const columnDefs = getColumnDefs(table, options.columns);
279
380
  const rows = previewRows(table, options.limit, options.columns);
280
381
  if (options.json) {
281
382
  for (const row of rows) {
@@ -284,7 +385,7 @@ rows loaded: ${rowsCount}${limitSuffix}
284
385
  }
285
386
  return;
286
387
  }
287
- console.table(rows);
388
+ printTable(rows, columnDefs);
288
389
  }
289
390
  function resolveTuiMode(mode, options) {
290
391
  if (mode === "on") {
@@ -306,7 +407,7 @@ function spawnBun(argv) {
306
407
  if (bunCheck.error || bunCheck.status !== 0) {
307
408
  return false;
308
409
  }
309
- const result = spawnSync("bun", [__filename, ...argv.slice(1)], {
410
+ const result = spawnSync("bun", [__parquetlens_filename, ...argv.slice(1)], {
310
411
  stdio: "inherit",
311
412
  env: { ...process.env, PARQUETLENS_BUN: "1" }
312
413
  });
@@ -314,7 +415,7 @@ function spawnBun(argv) {
314
415
  return true;
315
416
  }
316
417
  async function importTuiModule() {
317
- const extension = path.extname(__filename);
418
+ const extension = path.extname(__parquetlens_filename);
318
419
  const modulePath = extension === ".js" ? "./tui.js" : "./tui.tsx";
319
420
  return import(modulePath);
320
421
  }
@@ -324,4 +425,3 @@ main().catch((error) => {
324
425
  `);
325
426
  process.exitCode = 1;
326
427
  });
327
- //# sourceMappingURL=main.js.map
package/dist/tui.js CHANGED
@@ -6,7 +6,7 @@ const __filename = fileURLToPath(import.meta.url);
6
6
  const __dirname = dirname(__filename);
7
7
  import {
8
8
  openParquetSource
9
- } from "./chunk-NRRDNC7S.js";
9
+ } from "./chunk-YZLOXMC4.js";
10
10
 
11
11
  // src/tui.tsx
12
12
  import { createCliRenderer } from "@opentui/core";
@@ -38,8 +38,7 @@ var THEME = {
38
38
  muted: "#6272a4",
39
39
  stripe: "#252733"
40
40
  };
41
- async function runTui(input, options) {
42
- const source = await openParquetSource(input);
41
+ async function createTuiRenderer() {
43
42
  const renderer = await createCliRenderer({
44
43
  exitOnCtrlC: true,
45
44
  useAlternateScreen: true,
@@ -53,92 +52,66 @@ async function runTui(input, options) {
53
52
  root.unmount();
54
53
  renderer.destroy();
55
54
  };
56
- root.render(/* @__PURE__ */ jsx(App, { source, filePath: input, options, onExit: handleExit }));
55
+ return { root, handleExit };
57
56
  }
58
- function App({ source, filePath, options, onExit }) {
59
- const { width, height } = useTerminalDimensions();
60
- const pageSize = Math.max(1, height - RESERVED_LINES);
61
- const [offset, setOffset] = useState(0);
57
+ async function runTui(input, options) {
58
+ const source = await openParquetSource(input);
59
+ const initialLimit = options.maxRows ? Math.min(50, options.maxRows) : 50;
60
+ const table = await source.readTable({
61
+ batchSize: options.batchSize ?? 1024,
62
+ columns: options.columns.length > 0 ? options.columns : void 0,
63
+ limit: initialLimit,
64
+ offset: 0
65
+ });
66
+ const initialColumns = table.schema.fields.map((field) => ({
67
+ name: field.name,
68
+ type: formatArrowType(field.type)
69
+ }));
70
+ const initialRows = tableToGridRows(table, initialColumns.map((c) => c.name));
71
+ const initialGrid = { columns: initialColumns, rows: initialRows };
72
+ const metadata = await source.readMetadata().catch(() => null);
73
+ const { root, handleExit } = await createTuiRenderer();
74
+ root.render(
75
+ /* @__PURE__ */ jsx(
76
+ App,
77
+ {
78
+ source,
79
+ filePath: input,
80
+ options,
81
+ onExit: handleExit,
82
+ initialGrid,
83
+ initialMetadata: metadata,
84
+ initialKnownTotal: initialRows.length < initialLimit ? initialRows.length : null
85
+ }
86
+ )
87
+ );
88
+ }
89
+ async function runTuiWithTable(table, title, options) {
90
+ const { root, handleExit } = await createTuiRenderer();
91
+ root.render(/* @__PURE__ */ jsx(StaticApp, { table, title, options, onExit: handleExit }));
92
+ }
93
+ function TableViewer({
94
+ grid,
95
+ title,
96
+ offset,
97
+ setOffset,
98
+ maxOffset,
99
+ totalRows,
100
+ pageSize,
101
+ loading = false,
102
+ error = null,
103
+ notice = null,
104
+ metadata = null,
105
+ onExit,
106
+ onCopyError
107
+ }) {
108
+ const { width } = useTerminalDimensions();
62
109
  const [xOffset, setXOffset] = useState(0);
63
- const [grid, setGrid] = useState({ columns: [], rows: [] });
64
110
  const [selection, setSelection] = useState(null);
65
111
  const [sidebarOpen, setSidebarOpen] = useState(false);
66
- const [loading, setLoading] = useState(true);
67
- const [error, setError] = useState(null);
68
- const [notice, setNotice] = useState(null);
69
- const [metadata, setMetadata] = useState(null);
70
- const [knownTotalRows, setKnownTotalRows] = useState(null);
71
- const noticeTimer = useRef(null);
72
- const effectiveTotal = options.maxRows ?? knownTotalRows;
73
- const maxOffset = effectiveTotal === void 0 || effectiveTotal === null ? void 0 : Math.max(0, effectiveTotal - pageSize);
74
112
  const sidebarWidth = sidebarOpen ? Math.min(width, Math.max(SIDEBAR_MIN_WIDTH, Math.floor(width * SIDEBAR_WIDTH_RATIO))) : 0;
75
113
  const tableWidth = Math.max(0, width - (sidebarOpen ? sidebarWidth + PANEL_GAP : 0));
76
114
  const tableContentWidth = Math.max(0, tableWidth - CONTENT_BORDER_WIDTH);
77
- const columnsToRead = options.columns;
78
- useEffect(() => {
79
- return () => {
80
- void source.close();
81
- };
82
- }, [source]);
83
- useEffect(() => {
84
- let canceled = false;
85
- const loadWindow = async () => {
86
- setLoading(true);
87
- setError(null);
88
- try {
89
- const limit = options.maxRows ? Math.max(0, Math.min(pageSize, options.maxRows - offset)) : pageSize;
90
- const readOptions = {
91
- batchSize: options.batchSize ?? 1024,
92
- columns: columnsToRead.length > 0 ? columnsToRead : void 0,
93
- limit,
94
- offset
95
- };
96
- const table = await source.readTable(readOptions);
97
- const columns = table.schema.fields.map((field) => ({
98
- name: field.name,
99
- type: formatArrowType(field.type)
100
- }));
101
- const rows = tableToRows(
102
- table,
103
- columns.map((c) => c.name)
104
- );
105
- if (!canceled) {
106
- setGrid({ columns, rows });
107
- if (rows.length < limit) {
108
- setKnownTotalRows(offset + rows.length);
109
- }
110
- }
111
- } catch (caught) {
112
- const message = caught instanceof Error ? caught.message : String(caught);
113
- if (!canceled) {
114
- setError(message);
115
- }
116
- } finally {
117
- if (!canceled) {
118
- setLoading(false);
119
- }
120
- }
121
- };
122
- loadWindow();
123
- return () => {
124
- canceled = true;
125
- };
126
- }, [columnsToRead, offset, options.batchSize, options.maxRows, pageSize, source]);
127
- useEffect(() => {
128
- let canceled = false;
129
- source.readMetadata().then((meta) => {
130
- if (!canceled) {
131
- setMetadata(meta);
132
- }
133
- }).catch(() => {
134
- if (!canceled) {
135
- setMetadata(null);
136
- }
137
- });
138
- return () => {
139
- canceled = true;
140
- };
141
- }, [source]);
142
115
  const gridLines = useMemo(
143
116
  () => buildGridLines(grid, offset, tableContentWidth),
144
117
  [grid, offset, tableContentWidth]
@@ -163,22 +136,6 @@ function App({ source, filePath, options, onExit }) {
163
136
  setSidebarOpen(true);
164
137
  }
165
138
  }, [error]);
166
- useEffect(() => {
167
- return () => {
168
- if (noticeTimer.current) {
169
- clearTimeout(noticeTimer.current);
170
- }
171
- };
172
- }, []);
173
- const showNotice = (message) => {
174
- setNotice(message);
175
- if (noticeTimer.current) {
176
- clearTimeout(noticeTimer.current);
177
- }
178
- noticeTimer.current = setTimeout(() => {
179
- setNotice(null);
180
- }, 2e3);
181
- };
182
139
  useKeyboard((key) => {
183
140
  if (key.ctrl && key.name === "c" || key.name === "escape" || key.name === "q") {
184
141
  if (sidebarOpen && key.name === "escape") {
@@ -216,7 +173,7 @@ function App({ source, filePath, options, onExit }) {
216
173
  setOffset(maxOffset);
217
174
  return;
218
175
  }
219
- if (key.name === "return" || key.name === "enter") {
176
+ if (key.name === "return" || key.name === "enter" || key.name === "s") {
220
177
  setSidebarOpen((current) => !current);
221
178
  return;
222
179
  }
@@ -224,17 +181,12 @@ function App({ source, filePath, options, onExit }) {
224
181
  setSidebarOpen(false);
225
182
  return;
226
183
  }
227
- if (key.name === "s") {
228
- setSidebarOpen((current) => !current);
229
- return;
230
- }
231
184
  if (key.name === "e" && error) {
232
185
  setSidebarOpen(true);
233
186
  return;
234
187
  }
235
- if (key.name === "y" && error) {
236
- const copied = copyToClipboard(error);
237
- showNotice(copied ? "copied error to clipboard" : "clipboard unavailable");
188
+ if (key.name === "y" && error && onCopyError) {
189
+ onCopyError();
238
190
  return;
239
191
  }
240
192
  if (key.name === "left" || key.name === "h") {
@@ -255,13 +207,13 @@ function App({ source, filePath, options, onExit }) {
255
207
  const metaFlags = getMetadataFlags(metadata);
256
208
  return /* @__PURE__ */ jsxs("box", { flexDirection: "column", width: "100%", height: "100%", backgroundColor: THEME.background, children: [
257
209
  /* @__PURE__ */ jsx("box", { backgroundColor: THEME.header, border: true, borderColor: THEME.border, children: renderHeader({
258
- filePath,
210
+ filePath: title,
259
211
  offset,
260
212
  rows: grid.rows.length,
261
213
  columns: grid.columns.length,
262
214
  loading,
263
215
  error,
264
- maxRows: effectiveTotal ?? void 0,
216
+ maxRows: totalRows,
265
217
  optimized: metaFlags.optimized,
266
218
  createdBy: metaFlags.createdBy
267
219
  }) }),
@@ -275,9 +227,7 @@ function App({ source, filePath, options, onExit }) {
275
227
  flexGrow: 1,
276
228
  width: sidebarOpen ? tableWidth : "100%",
277
229
  onMouseScroll: (event) => {
278
- if (!event.scroll) {
279
- return;
280
- }
230
+ if (!event.scroll) return;
281
231
  const delta = Math.max(1, event.scroll.delta);
282
232
  const step = delta * SCROLL_STEP;
283
233
  if (event.scroll.direction === "up") {
@@ -308,9 +258,7 @@ function App({ source, filePath, options, onExit }) {
308
258
  bg: isSelected ? THEME.header : index % 2 === 0 ? THEME.background : THEME.stripe,
309
259
  onMouseDown: (event) => {
310
260
  const target = event.target;
311
- if (!target) {
312
- return;
313
- }
261
+ if (!target) return;
314
262
  const localX = Math.max(0, event.x - target.x);
315
263
  const absoluteX = localX + xOffset;
316
264
  const colIndex = findColumnIndex(absoluteX, gridLines.columnRanges);
@@ -356,9 +304,7 @@ function App({ source, filePath, options, onExit }) {
356
304
  wrapMode: "none",
357
305
  truncate: true,
358
306
  fg: THEME.accent,
359
- onMouseDown: () => {
360
- setSidebarOpen(false);
361
- },
307
+ onMouseDown: () => setSidebarOpen(false),
362
308
  children: "[ close ]"
363
309
  }
364
310
  )
@@ -366,9 +312,162 @@ function App({ source, filePath, options, onExit }) {
366
312
  }
367
313
  ) : null
368
314
  ] }),
369
- /* @__PURE__ */ jsx("box", { backgroundColor: THEME.header, border: true, borderColor: THEME.border, children: renderFooter(Boolean(error), notice) })
315
+ /* @__PURE__ */ jsx("box", { backgroundColor: THEME.header, border: true, borderColor: THEME.border, children: renderFooter(!!error, notice) })
370
316
  ] });
371
317
  }
318
+ function App({ source, filePath, options, onExit, initialGrid, initialMetadata, initialKnownTotal }) {
319
+ const { height } = useTerminalDimensions();
320
+ const pageSize = Math.max(1, height - RESERVED_LINES);
321
+ const [offset, setOffset] = useState(0);
322
+ const [grid, setGrid] = useState(initialGrid ?? { columns: [], rows: [] });
323
+ const [loading, setLoading] = useState(!initialGrid);
324
+ const [error, setError] = useState(null);
325
+ const [notice, setNotice] = useState(null);
326
+ const [metadata, setMetadata] = useState(initialMetadata ?? null);
327
+ const [knownTotalRows, setKnownTotalRows] = useState(initialKnownTotal ?? null);
328
+ const noticeTimer = useRef(null);
329
+ const initialLoadDone = useRef(!!initialGrid);
330
+ const effectiveTotal = options.maxRows ?? knownTotalRows;
331
+ const maxOffset = effectiveTotal === void 0 || effectiveTotal === null ? void 0 : Math.max(0, effectiveTotal - pageSize);
332
+ const columnsToRead = options.columns;
333
+ useEffect(() => {
334
+ return () => {
335
+ void source.close();
336
+ };
337
+ }, [source]);
338
+ useEffect(() => {
339
+ if (offset === 0 && initialLoadDone.current) {
340
+ initialLoadDone.current = false;
341
+ return;
342
+ }
343
+ let canceled = false;
344
+ const loadWindow = async () => {
345
+ setLoading(true);
346
+ setError(null);
347
+ try {
348
+ const limit = options.maxRows ? Math.max(0, Math.min(pageSize, options.maxRows - offset)) : pageSize;
349
+ const readOptions = {
350
+ batchSize: options.batchSize ?? 1024,
351
+ columns: columnsToRead.length > 0 ? columnsToRead : void 0,
352
+ limit,
353
+ offset
354
+ };
355
+ const table = await source.readTable(readOptions);
356
+ const columns = table.schema.fields.map((field) => ({
357
+ name: field.name,
358
+ type: formatArrowType(field.type)
359
+ }));
360
+ const rows = tableToGridRows(
361
+ table,
362
+ columns.map((c) => c.name)
363
+ );
364
+ if (!canceled) {
365
+ setGrid({ columns, rows });
366
+ if (rows.length < limit) {
367
+ setKnownTotalRows(offset + rows.length);
368
+ }
369
+ }
370
+ } catch (caught) {
371
+ const message = caught instanceof Error ? caught.message : String(caught);
372
+ if (!canceled) {
373
+ setError(message);
374
+ }
375
+ } finally {
376
+ if (!canceled) {
377
+ setLoading(false);
378
+ }
379
+ }
380
+ };
381
+ loadWindow();
382
+ return () => {
383
+ canceled = true;
384
+ };
385
+ }, [columnsToRead, offset, options.batchSize, options.maxRows, pageSize, source]);
386
+ useEffect(() => {
387
+ if (initialMetadata !== void 0) return;
388
+ let canceled = false;
389
+ source.readMetadata().then((meta) => {
390
+ if (!canceled) setMetadata(meta);
391
+ }).catch(() => {
392
+ if (!canceled) setMetadata(null);
393
+ });
394
+ return () => {
395
+ canceled = true;
396
+ };
397
+ }, [initialMetadata, source]);
398
+ useEffect(() => {
399
+ return () => {
400
+ if (noticeTimer.current) clearTimeout(noticeTimer.current);
401
+ };
402
+ }, []);
403
+ const handleCopyError = () => {
404
+ if (!error) return;
405
+ const copied = copyToClipboard(error);
406
+ setNotice(copied ? "copied error to clipboard" : "clipboard unavailable");
407
+ if (noticeTimer.current) clearTimeout(noticeTimer.current);
408
+ noticeTimer.current = setTimeout(() => setNotice(null), 2e3);
409
+ };
410
+ return /* @__PURE__ */ jsx(
411
+ TableViewer,
412
+ {
413
+ grid,
414
+ title: filePath,
415
+ offset,
416
+ setOffset,
417
+ maxOffset,
418
+ totalRows: effectiveTotal ?? void 0,
419
+ pageSize,
420
+ loading,
421
+ error,
422
+ notice,
423
+ metadata,
424
+ onExit,
425
+ onCopyError: handleCopyError
426
+ }
427
+ );
428
+ }
429
+ function StaticApp({ table, title, options, onExit }) {
430
+ const { height } = useTerminalDimensions();
431
+ const pageSize = Math.max(1, height - RESERVED_LINES);
432
+ const [offset, setOffset] = useState(0);
433
+ const columns = useMemo(
434
+ () => table.schema.fields.map((field) => ({
435
+ name: field.name,
436
+ type: formatArrowType(field.type)
437
+ })),
438
+ [table]
439
+ );
440
+ const allRows = useMemo(
441
+ () => tableToGridRows(
442
+ table,
443
+ columns.map((c) => c.name)
444
+ ),
445
+ [table, columns]
446
+ );
447
+ const totalRows = allRows.length;
448
+ const maxOffset = Math.max(0, totalRows - pageSize);
449
+ const visibleRows = useMemo(
450
+ () => allRows.slice(offset, offset + pageSize),
451
+ [allRows, offset, pageSize]
452
+ );
453
+ const grid = useMemo(
454
+ () => ({ columns, rows: visibleRows }),
455
+ [columns, visibleRows]
456
+ );
457
+ return /* @__PURE__ */ jsx(
458
+ TableViewer,
459
+ {
460
+ grid,
461
+ title,
462
+ offset,
463
+ setOffset,
464
+ maxOffset,
465
+ totalRows,
466
+ pageSize,
467
+ onExit
468
+ }
469
+ );
470
+ }
372
471
  function renderHeader(props) {
373
472
  const { filePath, offset, rows, columns, loading, error, maxRows, createdBy, optimized } = props;
374
473
  const start = rows > 0 ? offset + 1 : offset;
@@ -587,7 +686,7 @@ function getMetadataFlags(metadata) {
587
686
  createdBy: metadata.createdBy
588
687
  };
589
688
  }
590
- function tableToRows(table, columns) {
689
+ function tableToGridRows(table, columns) {
591
690
  const rows = [];
592
691
  for (const batch of table.batches) {
593
692
  const vectors = columns.map((_, index) => batch.getChildAt(index));
@@ -642,6 +741,6 @@ function copyToClipboard(value) {
642
741
  return false;
643
742
  }
644
743
  export {
645
- runTui
744
+ runTui,
745
+ runTuiWithTable
646
746
  };
647
- //# sourceMappingURL=tui.js.map