From 561c1f13fc90869e1db38220e585a0838477a9bb Mon Sep 17 00:00:00 2001 From: Yannick Utard Date: Thu, 5 Nov 2026 20:00:00 +0100 Subject: [PATCH] feat(query): list running queries Expose GET /queries so operators can see what is running before inspecting a query. Co-authored-by: Cursor --- CHANGELOG.md | 1 + COMMANDS.md | 14 +++- DEVELOPMENT.md | 1 + README.md | 1 + cli-reference.json | 16 +++- cli/AGENTS.md | 2 +- cli/scripts/command-reference.test.ts | 2 +- cli/src/commands/query/index.test.ts | 101 +++++++++++++++++++++++++- cli/src/commands/query/index.ts | 3 + cli/src/commands/query/list.ts | 30 ++++++++ cli/src/lib/lakehouse/query.test.ts | 12 +++ cli/src/lib/lakehouse/query.ts | 9 +++ cli/src/lib/usage.test.ts | 5 +- tests/lakehouse.test.ts | 20 +++++ 14 files changed, 209 insertions(+), 8 deletions(-) create mode 100644 cli/src/commands/query/list.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f56c0e91..40a5d92e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- List running queries with `altertable query list`. - Preview a single-file data app locally with `altertable app dev --file index.tsx`. The file's `defineDataApp` scope must match the selected profile, and queries run through the CLI's loopback proxy. ### Changed diff --git a/COMMANDS.md b/COMMANDS.md index b947aaca..e117ed1c 100644 --- a/COMMANDS.md +++ b/COMMANDS.md @@ -884,7 +884,7 @@ Run SQL queries against the lakehouse. ```bash altertable query [options] -altertable query show|cancel +altertable query list|show|cancel ``` **Arguments** @@ -912,6 +912,7 @@ altertable query show|cancel **Subcommands** +- `list` — List running queries in the current environment. - `show` — Fetch metadata for a completed or running query. - `cancel` — Cancel a running query. @@ -922,9 +923,20 @@ altertable query "SELECT id, email, plan FROM analytics.main.users LIMIT 10" altertable query "SELECT event, timestamp FROM analytics.main.events ORDER BY timestamp DESC LIMIT 10" --json altertable query "SELECT 1" --format csv --output results.csv altertable query @report.sql --format csv --output results.csv +altertable query list altertable query show ``` +#### `altertable query list` + +List running queries in the current environment. + +**Usage** + +```bash +altertable query list +``` + #### `altertable query show` Fetch metadata for a completed or running query. diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 2ee8a2cc..cce91685 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -275,6 +275,7 @@ When bumping the `specs/` submodule, extend the mapped tests before merge. | --------------------------- | ---------------------------- | -------------------------------- | --------------------- | | POST /query (streamed) | `query ""` | `lakehouse.test.ts` stream tests | `integration.e2e.ts` | | POST /query (buffered json) | `query --json` | `lakehouse.test.ts` | `integration.e2e.ts` | +| GET /queries | `query list` | `lakehouse.test.ts` | — | | GET/DELETE /query/{id} | `query show`, `query cancel` | `lakehouse.test.ts` | `integration.e2e.ts` | | POST /append + GET /tasks | `append`, `append status` | `lakehouse.test.ts` | `integration.e2e.ts` | | POST /upload | `upload` | `lakehouse.test.ts` | `integration.e2e.ts` | diff --git a/README.md b/README.md index eab706e8..4aecba82 100644 --- a/README.md +++ b/README.md @@ -397,6 +397,7 @@ inside a component, for example `customer%2E360.main.users`. **Inspect async operations** ```bash +altertable query list altertable query show altertable query cancel --session-id altertable append status diff --git a/cli-reference.json b/cli-reference.json index a0aa8f01..01048579 100644 --- a/cli-reference.json +++ b/cli-reference.json @@ -1526,7 +1526,7 @@ "description": "Run SQL queries against the lakehouse.", "usage": [ "altertable query [options] ", - "altertable query show|cancel" + "altertable query list|show|cancel" ], "aliases": [], "arguments": [ @@ -1685,9 +1685,23 @@ "altertable query \"SELECT event, timestamp FROM analytics.main.events ORDER BY timestamp DESC LIMIT 10\" --json", "altertable query \"SELECT 1\" --format csv --output results.csv", "altertable query @report.sql --format csv --output results.csv", + "altertable query list", "altertable query show " ], "subcommands": [ + { + "id": "altertable-query-list", + "command": "altertable query list", + "description": "List running queries in the current environment.", + "usage": [ + "altertable query list" + ], + "aliases": [], + "arguments": [], + "options": [], + "examples": [], + "subcommands": [] + }, { "id": "altertable-query-show", "command": "altertable query show", diff --git a/cli/AGENTS.md b/cli/AGENTS.md index 06067798..4288c397 100644 --- a/cli/AGENTS.md +++ b/cli/AGENTS.md @@ -135,7 +135,7 @@ altertable ├── catalog │ └── create ├── query -│ └── show, cancel +│ └── list, show, cancel ├── schema, duckdb ├── append │ └── status diff --git a/cli/scripts/command-reference.test.ts b/cli/scripts/command-reference.test.ts index 3f4736c5..d976d1ff 100644 --- a/cli/scripts/command-reference.test.ts +++ b/cli/scripts/command-reference.test.ts @@ -62,7 +62,7 @@ describe('command reference', () => { const reference = renderCommandReferenceMarkdown(await resolveReference()); expect(reference).toContain( - '```bash\naltertable query [options] \naltertable query show|cancel\n```' + '```bash\naltertable query [options] \naltertable query list|show|cancel\n```' ); expect(reference).toContain('```bash\naltertable profile env [NAME]\n```'); expect(reference).toContain( diff --git a/cli/src/commands/query/index.test.ts b/cli/src/commands/query/index.test.ts index 81c83299..096f33e1 100644 --- a/cli/src/commands/query/index.test.ts +++ b/cli/src/commands/query/index.test.ts @@ -174,11 +174,28 @@ describe('query command', () => { test('dispatches show and cancel without run arguments', async () => { const queryId = '11111111-2222-3333-4444-555555555555'; + const showBody = { + uuid: queryId, + worker: { + slug: 'api-worker-s-example', + compute_size: 'S', + threads: 4, + memory_limit_bytes: 8589934592, + memory_usage_bytes: 123456, + memory_by_tag: [ + { + tag: 'BASE_TABLE', + memory_usage_bytes: 0, + temporary_storage_bytes: 0, + }, + ], + }, + }; workspace.writeMocks([ { urlPattern: `/query/${queryId}`, method: 'GET', - body: `{"uuid":"${queryId}"}`, + body: JSON.stringify(showBody), }, { urlPattern: `/query/${queryId}`, @@ -187,7 +204,7 @@ describe('query command', () => { }, ]); - await runCommandWithTestRuntime(['query', 'show', queryId]); + const show = await runCommandWithTestRuntime(['query', 'show', queryId]); await runCommandWithTestRuntime([ 'query', 'cancel', @@ -196,6 +213,7 @@ describe('query command', () => { 'session-1', ]); + expect(JSON.parse(show.stdout.join(''))).toEqual(showBody); const log = workspace.readHttpLog(); expect(log).toContain( `METHOD=GET\nURL=https://example.com/query/${queryId}` @@ -205,6 +223,85 @@ describe('query command', () => { ); }); + test('lists running queries as a table', async () => { + const queries = { + queries: [ + { + id: '11111111-2222-3333-4444-555555555555', + started_at: '2026-11-05T17:49:18Z', + }, + { + id: '22222222-3333-4444-5555-666666666666', + started_at: '2026-11-05T17:50:00Z', + }, + ], + }; + workspace.writeMocks([ + { + urlPattern: '/queries', + method: 'GET', + body: JSON.stringify(queries), + }, + ]); + + const harness = await runCommandWithTestRuntime(['query', 'list'], { + debug: false, + json: false, + agent: false, + }); + + expect(workspace.readHttpLog()).toContain( + 'METHOD=GET\nURL=https://example.com/queries' + ); + const output = harness.stdout.join(''); + expect(output.indexOf('11111111-2222-33')).toBeLessThan( + output.indexOf('22222222-3333-44') + ); + expect(output).toContain('2026-11-05T17:49:18Z'); + expect(output).toContain('2026-11-05T17:50:00Z'); + expect(output).toContain('started_at'); + }); + + test('prints a message when no queries are running', async () => { + workspace.writeMocks([ + { + urlPattern: '/queries', + method: 'GET', + body: '{"queries":[]}', + }, + ]); + + const harness = await runCommandWithTestRuntime(['query', 'list'], { + debug: false, + json: false, + agent: false, + }); + + expect(harness.stdout.join('')).toBe('No running queries.'); + }); + + test('passes through the running query list as JSON', async () => { + const queries = { + queries: [ + { + id: '11111111-2222-3333-4444-555555555555', + started_at: '2026-11-05T17:49:18Z', + }, + ], + }; + workspace.writeMocks([ + { + urlPattern: '/queries', + method: 'GET', + body: JSON.stringify(queries), + }, + ]); + + const harness = await runCommandWithTestRuntime(['query', 'list']); + + expect(JSON.parse(harness.stdout.join(''))).toEqual(queries); + }); + test('requires a query id for show instead of running bare show as SQL', async () => { expect(runCommandWithTestRuntime(['query', 'show'])).rejects.toThrow( 'Missing required argument: query-id.' diff --git a/cli/src/commands/query/index.ts b/cli/src/commands/query/index.ts index ec24f01a..8dba733f 100644 --- a/cli/src/commands/query/index.ts +++ b/cli/src/commands/query/index.ts @@ -1,6 +1,7 @@ import { queryCancelCommand } from '@/commands/query/cancel.ts'; import { queryRunArgs } from '@/commands/query/lib/args.ts'; import { parseQueryStatement } from '@/commands/query/lib/statement.ts'; +import { queryListCommand } from '@/commands/query/list.ts'; import { queryShowCommand } from '@/commands/query/show.ts'; import { optionalStringArg } from '@/lib/args.ts'; import { defineCommand } from '@/lib/command.ts'; @@ -27,11 +28,13 @@ export const queryCommand = defineCommand({ 'altertable query "SELECT event, timestamp FROM analytics.main.events ORDER BY timestamp DESC LIMIT 10" --json', 'altertable query "SELECT 1" --format csv --output results.csv', 'altertable query @report.sql --format csv --output results.csv', + 'altertable query list', 'altertable query show ', ], }, args: queryRunArgs, subcommands: { + list: queryListCommand, show: queryShowCommand, cancel: queryCancelCommand, }, diff --git a/cli/src/commands/query/list.ts b/cli/src/commands/query/list.ts new file mode 100644 index 00000000..9b8be9a7 --- /dev/null +++ b/cli/src/commands/query/list.ts @@ -0,0 +1,30 @@ +import { writeCommandOutput } from '@/lib/command-output.ts'; +import { defineCommand } from '@/lib/command.ts'; +import { sendHttp } from '@/lib/http-request.ts'; +import { buildLakehouseQueryListRequest } from '@/lib/lakehouse/query.ts'; +import { extractManagementRows } from '@/lib/management-output.ts'; +import { parseApiJson } from '@/lib/parse-api-json.ts'; + +export const queryListCommand = defineCommand({ + metadata: { + name: 'list', + description: 'List running queries in the current environment.', + }, + async run({ execution, sink }) { + const response = await sendHttp( + buildLakehouseQueryListRequest(), + execution + ); + if ( + !sink.json && + extractManagementRows(parseApiJson(response)).length === 0 + ) { + await writeCommandOutput( + { kind: 'human', text: 'No running queries.' }, + sink + ); + return; + } + await writeCommandOutput({ kind: 'tabular', body: response }, sink); + }, +}); diff --git a/cli/src/lib/lakehouse/query.test.ts b/cli/src/lib/lakehouse/query.test.ts index 732472a3..6a27345a 100644 --- a/cli/src/lib/lakehouse/query.test.ts +++ b/cli/src/lib/lakehouse/query.test.ts @@ -13,6 +13,7 @@ import { type LakehouseRow, } from '@/lib/lakehouse-ndjson.ts'; import { + buildLakehouseQueryListRequest, buildLakehouseQueryRequest, executeLakehouseQuery, executeLakehouseQueryBytes, @@ -254,6 +255,17 @@ describe('parseLakehouseQueryStream', () => { }); }); +describe('buildLakehouseQueryListRequest', () => { + test('GETs /queries with retry', () => { + expect(buildLakehouseQueryListRequest()).toEqual({ + plane: 'lakehouse', + method: 'GET', + endpoint: '/queries', + retry: true, + }); + }); +}); + describe('buildLakehouseQueryRequest', () => { test.each([ [undefined, 5], diff --git a/cli/src/lib/lakehouse/query.ts b/cli/src/lib/lakehouse/query.ts index 3d58e7e2..ef5229b0 100644 --- a/cli/src/lib/lakehouse/query.ts +++ b/cli/src/lib/lakehouse/query.ts @@ -85,6 +85,15 @@ export async function executeLakehouseQueryBytes( return sendHttpStream(buildLakehouseQueryRequest(input, true), execution); } +export function buildLakehouseQueryListRequest(): HttpRequest { + return { + plane: 'lakehouse', + method: 'GET', + endpoint: '/queries', + retry: true, + }; +} + export function buildLakehouseQueryShowRequest(queryId: string): HttpRequest { return { plane: 'lakehouse', diff --git a/cli/src/lib/usage.test.ts b/cli/src/lib/usage.test.ts index 93106cc2..d9a4a10e 100644 --- a/cli/src/lib/usage.test.ts +++ b/cli/src/lib/usage.test.ts @@ -211,7 +211,7 @@ describe('renderAltertableUsage', () => { expect(help.command).toBe('altertable query'); expect(help.usage).toEqual([ 'altertable query [flags] ', - 'altertable query show|cancel', + 'altertable query list|show|cancel', ]); expect(help.arguments).toContainEqual( expect.objectContaining({ name: 'statement', type: 'positional' }) @@ -232,6 +232,7 @@ describe('renderAltertableUsage', () => { false ); expect(help.subcommands.map(command => command.name)).toEqual([ + 'list', 'show', 'cancel', ]); @@ -248,7 +249,7 @@ describe('renderAltertableUsage', () => { const queryUsage = await renderAltertableUsage(query, queryParent); expect(queryUsage).toContain( - 'Usage\n altertable query [flags] \n altertable query show|cancel' + 'Usage\n altertable query [flags] \n altertable query list|show|cancel' ); expect(queryUsage).not.toContain('[STATEMENT] show|cancel'); diff --git a/tests/lakehouse.test.ts b/tests/lakehouse.test.ts index 5ad7f995..4751aac1 100644 --- a/tests/lakehouse.test.ts +++ b/tests/lakehouse.test.ts @@ -27,6 +27,26 @@ describe("lakehouse command routing", () => { expect(JSON.parse(result.stdout)).toEqual({ ok: true, task_id: appendId }); }); + test("query list fetches running queries", async () => { + const queries = { + queries: [ + { + id: "11111111-2222-3333-4444-555555555555", + started_at: "2026-11-05T17:49:18Z", + }, + ], + }; + await workspace.setupHttpLog(); + await workspace.setupMockHttp([jsonMock("GET", "/queries", queries)]); + + const result = await workspace.runCommand("altertable --json query list"); + + expect(result.exitCode).toBe(0); + expect(await workspace.httpLogValue("METHOD")).toBe("GET"); + expect(await workspace.httpLogValue("URL")).toBe("https://lakehouse.example.test/queries"); + expect(JSON.parse(result.stdout)).toEqual(queries); + }); + test("append status fetches status without append-run flags", async () => { await workspace.setupHttpLog(); await workspace.setupMockHttp([jsonMock("GET", `/tasks/${appendId}`, { task_id: appendId, status: "completed" })]);