Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 13 additions & 1 deletion COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -884,7 +884,7 @@ Run SQL queries against the lakehouse.

```bash
altertable query [options] <STATEMENT>
altertable query show|cancel
altertable query list|show|cancel
```

**Arguments**
Expand Down Expand Up @@ -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.

Expand All @@ -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 <query-id>
```

#### `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.
Expand Down
1 change: 1 addition & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ When bumping the `specs/` submodule, extend the mapped tests before merge.
| --------------------------- | ---------------------------- | -------------------------------- | --------------------- |
| POST /query (streamed) | `query "<SQL>"` | `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` |
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,7 @@ inside a component, for example `customer%2E360.main.users`.
**Inspect async operations**

```bash
altertable query list
altertable query show <query-uuid>
altertable query cancel <query-uuid> --session-id <uuid>
altertable append status <append-id>
Expand Down
16 changes: 15 additions & 1 deletion cli-reference.json
Original file line number Diff line number Diff line change
Expand Up @@ -1526,7 +1526,7 @@
"description": "Run SQL queries against the lakehouse.",
"usage": [
"altertable query [options] <STATEMENT>",
"altertable query show|cancel"
"altertable query list|show|cancel"
],
"aliases": [],
"arguments": [
Expand Down Expand Up @@ -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 <query-id>"
],
"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",
Expand Down
2 changes: 1 addition & 1 deletion cli/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ altertable
├── catalog
│ └── create
├── query <SQL>
│ └── show, cancel
│ └── list, show, cancel
├── schema, duckdb
├── append <DATA>
│ └── status
Expand Down
2 changes: 1 addition & 1 deletion cli/scripts/command-reference.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ describe('command reference', () => {
const reference = renderCommandReferenceMarkdown(await resolveReference());

expect(reference).toContain(
'```bash\naltertable query [options] <STATEMENT>\naltertable query show|cancel\n```'
'```bash\naltertable query [options] <STATEMENT>\naltertable query list|show|cancel\n```'
);
expect(reference).toContain('```bash\naltertable profile env [NAME]\n```');
expect(reference).toContain(
Expand Down
101 changes: 99 additions & 2 deletions cli/src/commands/query/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
Expand All @@ -187,7 +204,7 @@ describe('query command', () => {
},
]);

await runCommandWithTestRuntime(['query', 'show', queryId]);
const show = await runCommandWithTestRuntime(['query', 'show', queryId]);
await runCommandWithTestRuntime([
'query',
'cancel',
Expand All @@ -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}`
Expand All @@ -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.'
Expand Down
3 changes: 3 additions & 0 deletions cli/src/commands/query/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 <query-id>',
],
},
args: queryRunArgs,
subcommands: {
list: queryListCommand,
show: queryShowCommand,
cancel: queryCancelCommand,
},
Expand Down
30 changes: 30 additions & 0 deletions cli/src/commands/query/list.ts
Original file line number Diff line number Diff line change
@@ -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);
},
});
12 changes: 12 additions & 0 deletions cli/src/lib/lakehouse/query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
type LakehouseRow,
} from '@/lib/lakehouse-ndjson.ts';
import {
buildLakehouseQueryListRequest,
buildLakehouseQueryRequest,
executeLakehouseQuery,
executeLakehouseQueryBytes,
Expand Down Expand Up @@ -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],
Expand Down
9 changes: 9 additions & 0 deletions cli/src/lib/lakehouse/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
5 changes: 3 additions & 2 deletions cli/src/lib/usage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ describe('renderAltertableUsage', () => {
expect(help.command).toBe('altertable query');
expect(help.usage).toEqual([
'altertable query [flags] <STATEMENT>',
'altertable query show|cancel',
'altertable query list|show|cancel',
]);
expect(help.arguments).toContainEqual(
expect.objectContaining({ name: 'statement', type: 'positional' })
Expand All @@ -232,6 +232,7 @@ describe('renderAltertableUsage', () => {
false
);
expect(help.subcommands.map(command => command.name)).toEqual([
'list',
'show',
'cancel',
]);
Expand All @@ -248,7 +249,7 @@ describe('renderAltertableUsage', () => {

const queryUsage = await renderAltertableUsage(query, queryParent);
expect(queryUsage).toContain(
'Usage\n altertable query [flags] <STATEMENT>\n altertable query show|cancel'
'Usage\n altertable query [flags] <STATEMENT>\n altertable query list|show|cancel'
);
expect(queryUsage).not.toContain('[STATEMENT] show|cancel');

Expand Down
20 changes: 20 additions & 0 deletions tests/lakehouse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" })]);
Expand Down
Loading