From 608a9171eb5a506a06785603e6482ea09969425b Mon Sep 17 00:00:00 2001 From: RobbeDeWaele Date: Wed, 10 Dec 2025 16:10:10 +0100 Subject: [PATCH 1/2] preview table without query --- packages/core/src/bigquery.ts | 54 ++++++++++++++ packages/extension/src/previewer.ts | 11 +-- packages/extension/src/runner.ts | 112 ++++++++++++++++++++++++++++ packages/shared/src/types.ts | 2 +- 4 files changed, 171 insertions(+), 8 deletions(-) diff --git a/packages/core/src/bigquery.ts b/packages/core/src/bigquery.ts index 1625a50..0b6b760 100644 --- a/packages/core/src/bigquery.ts +++ b/packages/core/src/bigquery.ts @@ -54,6 +54,9 @@ export type NoRowsError = Err<"NoRows">; export type Client = Readonly<{ getTable(ref: TableReference): Promise>; + getTableRows(ref: TableReference, options?: { maxResults?: number }): Promise< + Result; page: Page; table: Table }> + >; createRunJob( query: Omit & { query: string } ): Promise< @@ -183,8 +186,59 @@ export async function createClient( ); }; + const getTableRows = async ( + ref: TableReference, + options: { maxResults?: number } = {} + ): Promise; page: Page; table: Table }>> => { + return tryCatch( + async () => { + const tableResult = await getTable(ref); + if (!tableResult.success) { + throw new Error(tableResult.value.reason); + } + const table = tableResult.value; + + const [rows] = await bigQuery + .dataset(ref.datasetId) + .table(ref.tableId) + .getRows({ + maxResults: options.maxResults || 100, + wrapIntegers: true, + }); + + if (!rows || rows.length === 0) { + throw new Error("No rows found in the table"); + } + + const structs = rows as Array; + + // Create a simple page object since we're not dealing with query pagination + const page = getPage({ + totalRows: table.numRows || "0", + rows: structs.length, + }); + + return { structs, page, table }; + }, + (err) => { + const errMessage = errorToString(err); + if (errMessage.includes("No rows found")) { + return { + type: "NoRows" as const, + reason: errMessage, + }; + } + return { + type: "Unknown" as const, + reason: errMessage, + }; + } + ); + }; + const client: Client = { getTable, + getTableRows, async createRunJob(jobOptions) { const dryRunJobResult = await runQuery({ diff --git a/packages/extension/src/previewer.ts b/packages/extension/src/previewer.ts index 4a9d6a6..ba4b831 100644 --- a/packages/extension/src/previewer.ts +++ b/packages/extension/src/previewer.ts @@ -17,19 +17,16 @@ export const createPreviewer = ({ }) => { return { async preview(tableReference: TableReference) { - const id = getTableName(tableReference); const { previewer: { rowsPerPage }, } = configManager.get(); - const query = - `SELECT * FROM \`${id}\`` + - (rowsPerPage ? ` LIMIT ${rowsPerPage}` : ""); - logger.log("query:", query); - const runnerResult = await runnerManager.preview({ + logger.log("previewing table:", getTableName(tableReference)); + + const runnerResult = await runnerManager.previewTable({ title: tableReference.tableId, - query, tableReference, + maxResults: rowsPerPage || 100, }); if (!runnerResult.success) { logger.error(runnerResult); diff --git a/packages/extension/src/runner.ts b/packages/extension/src/runner.ts index 9bb2caf..039fc23 100644 --- a/packages/extension/src/runner.ts +++ b/packages/extension/src/runner.ts @@ -502,6 +502,118 @@ export function createRunnerManager({ return createResult; }, + async previewTable({ + title, + tableReference, + maxResults, + }: Readonly<{ + title: string; + tableReference: TableReference; + maxResults?: number; + }>): Promise< + Result< + Err<"Unknown" | "Authentication" | "NoRows">, + { + run(): Promise; + dispose(): void; + } + > + > { + const runnerId: RunnerID = `table://${checksum( + JSON.stringify(tableReference) + )}`; + const logger = parentLogger.createChild(runnerId); + + const status = statusManager.get(runnerId); + + return succeed({ + async run() { + + status.loadBilled(); + + const config = configManager.get(); + + const clientResult = await createClient({ + keyFilename: config.keyFilename, + projectId: tableReference.projectId, + location: config.location, + }); + if (!clientResult.success) { + logger.error(clientResult); + status.errorBilled(); + const err = unwrap(clientResult); + errorManager.show(err); + return; + } + const client = unwrap(clientResult); + + const getTableRowsResult = await client.getTableRows(tableReference, { + maxResults: maxResults || config.previewer.rowsPerPage || 100, + }); + + if (!getTableRowsResult.success) { + logger.error(getTableRowsResult); + status.errorBilled(); + const err = unwrap(getTableRowsResult); + errorManager.show(err); + return; + } + + const { structs, page, table } = unwrap(getTableRowsResult); + + logger.log(`fetched table data: ${structs.length} rows`); + status.succeedBilled({ + bytes: "0 B", // No query cost! + cacheHit: false, + }); + + const getRendererResult = await rendererManager.create({ + runnerId, + title, + }); + if (!getRendererResult.success) { + logger.error(getRendererResult); + errorManager.show(getRendererResult.value); + status.errorBilled(); + return; + } + const renderer = unwrap(getRendererResult); + + const rendererOpenResult = await renderer.startProcessing(); + if (!rendererOpenResult.success) { + logger.error(rendererOpenResult); + await renderer.failProcessing(rendererOpenResult.value); + status.errorBilled(); + return; + } + + renderer.reveal(); + + // Only render the rows, no schema or job information + const renderRowsResult = await renderer.renderRows({ + structs, + table, + page, + }); + if (!renderRowsResult.success) { + logger.error(renderRowsResult); + await renderer.failProcessing(renderRowsResult.value); + return; + } + + await renderer.successProcessing(); + }, + + dispose() { + const renderer = rendererManager.get({ runnerId }); + if (!renderer || renderer.disposed) { + return; + } + runners.delete(runnerId); + }, + }); + }, + get(runnerId: RunnerID) { return runners.get(runnerId); }, diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index 68c6cce..f725a72 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -8,7 +8,7 @@ import type { } from "@google-cloud/bigquery"; import type { Err } from "."; -export type RunnerID = `${"file" | "query"}://${string}`; +export type RunnerID = `${"file" | "query" | "table"}://${string}`; export type Field = PrimitiveField | StructField; export type PrimitiveField = Readonly<{ From dbb7e5af5add1b4cfbc67d9e1dcf42eafe74228c Mon Sep 17 00:00:00 2001 From: RobbeDeWaele Date: Wed, 10 Dec 2025 16:12:40 +0100 Subject: [PATCH 2/2] linting --- packages/core/src/bigquery.ts | 19 +++++++++++++++---- packages/extension/src/runner.ts | 1 - 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/core/src/bigquery.ts b/packages/core/src/bigquery.ts index 0b6b760..450f7f2 100644 --- a/packages/core/src/bigquery.ts +++ b/packages/core/src/bigquery.ts @@ -54,8 +54,14 @@ export type NoRowsError = Err<"NoRows">; export type Client = Readonly<{ getTable(ref: TableReference): Promise>; - getTableRows(ref: TableReference, options?: { maxResults?: number }): Promise< - Result; page: Page; table: Table }> + getTableRows( + ref: TableReference, + options?: { maxResults?: number } + ): Promise< + Result< + UnknownError | NoRowsError, + { structs: Array; page: Page; table: Table } + > >; createRunJob( query: Omit & { query: string } @@ -189,7 +195,12 @@ export async function createClient( const getTableRows = async ( ref: TableReference, options: { maxResults?: number } = {} - ): Promise; page: Page; table: Table }>> => { + ): Promise< + Result< + UnknownError | NoRowsError, + { structs: Array; page: Page; table: Table } + > + > => { return tryCatch( async () => { const tableResult = await getTable(ref); @@ -211,7 +222,7 @@ export async function createClient( } const structs = rows as Array; - + // Create a simple page object since we're not dealing with query pagination const page = getPage({ totalRows: table.numRows || "0", diff --git a/packages/extension/src/runner.ts b/packages/extension/src/runner.ts index 039fc23..45fb889 100644 --- a/packages/extension/src/runner.ts +++ b/packages/extension/src/runner.ts @@ -528,7 +528,6 @@ export function createRunnerManager({ return succeed({ async run() { - status.loadBilled(); const config = configManager.get();