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
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"plugins": [
{
"name": "kbagent",
"version": "0.58.0",
"version": "0.59.0",
"source": "./plugins/kbagent",
"description": "AI-friendly interface to Keboola Connection projects — explore configs, jobs, lineage, call MCP tools, manage dev branches, and debug SQL in workspaces",
"category": "development"
Expand Down
5 changes: 4 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,8 +402,11 @@ kbagent workspace detail --project ALIAS --workspace-id ID [--branch ID]
kbagent workspace delete --project ALIAS --workspace-id ID
kbagent workspace password --project ALIAS --workspace-id ID
kbagent workspace load --project ALIAS --workspace-id ID --tables TABLE_ID [--tables ...] [--preserve]
kbagent workspace query --project ALIAS --workspace-id ID --sql "SELECT ..." [--transactional]
kbagent workspace query --project ALIAS --workspace-id ID --sql "SELECT ..." [--transactional] [--full] [--limit N]
kbagent workspace query --project ALIAS --workspace-id ID --file query.sql
# query: default reads results inline via Query Service `GET .../results` (fast, JSON columns+rows),
# capped at --limit rows (default 500); pass --full for the complete CSV export (slower, uncapped).
# Each statement carries structured columns+rows + a synthesized csv_data (back-compat) since 0.59.0.
kbagent workspace gc [--project NAME ...] [--dry-run] [--yes]
kbagent workspace from-transformation --project ALIAS --component-id ID --config-id ID [--row-id ID]

Expand Down
2 changes: 1 addition & 1 deletion plugins/kbagent/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "kbagent",
"version": "0.58.0",
"version": "0.59.0",
"description": "AI-friendly interface to Keboola Connection projects — explore configs, jobs, lineage, call MCP tools, manage dev branches, and debug SQL in workspaces",
"author": {
"name": "Keboola",
Expand Down
9 changes: 8 additions & 1 deletion plugins/kbagent/agents/keboola-expert.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ a critical failure.
| Promote typed rebuild back into the original name | `kbagent storage swap-tables --project P --table-id in.c-foo.data --target-table-id in.c-foo.data_change_log --branch <ID> --yes` (0.28.0+) -- async storage job (`tableSwap`); client polls to completion. Service refuses without a branch; any branch incl. prod | -- | renaming or deleting + re-uploading (loses history; downstream configs need to be rewritten) |
| Re-seed a table without losing its schema / PK / dependents | `kbagent storage truncate-table --project P --table-id in.c-foo.data [--branch ID] [--dry-run] [--yes]` (0.32.0+) -- DELETE `/tables/{id}/rows?allowTruncate=1`; endpoint is uniformly async on every branch (returns a queued `tableRowsDelete` job; client polls via `_wait_for_storage_job`). Do NOT pass `async=true` -- the API rejects it. Batch via repeated `--table-id`. Returns `{truncated[], failed[], dry_run, project_alias}` with `truncated[]` entries carrying `{table_id, rows_before, rows_after, branch_id}`. Permission class: `destructive` | `tool call delete_table_rows` if the upstream MCP exposes it | drop + recreate the table (loses descriptions, PK, sharing edges, and breaks every downstream config reference); deleting rows via raw SQL in a workspace (bypasses the Storage API audit trail) |
| Debug a failed job | `kbagent job detail --project P --job-id J --json` + `kbagent job run ... --log-tail-lines 200` | `kbagent workspace from-transformation` for SQL repro | "I think the issue is..." without reading logs |
| Ad-hoc SQL / row-count / type audit | `kbagent workspace create` + `kbagent workspace load` + `kbagent workspace query --sql "..."` | `kbagent workspace from-transformation` for existing transform debugging; `workspace list --qs-compatible` (0.42.0+, #304) for data-app reuse | querying Keboola Storage directly via Snowflake credentials outside the workspace abstraction |
| Ad-hoc SQL / row-count / type audit | `kbagent workspace create` + `kbagent workspace load` + `kbagent workspace query --sql "..."` (0.59.0+: results come back inline+fast but **capped at `--limit`, default 500** -- check `statements[].truncated`/`total_rows`, use `COUNT(*)` for counts, `--full` for the complete set) | `kbagent workspace from-transformation` for existing transform debugging; `workspace list --qs-compatible` (0.42.0+, #304) for data-app reuse | trusting a default `SELECT *` as the full result (it is truncated at 500); querying Storage via raw Snowflake credentials outside the workspace abstraction |
| Inspect dev branch | `kbagent branch list --project P`, `kbagent branch use --project P --branch ID` | `tool call get_branch` | acting on `main` when a dev branch exists |
| Audit project capabilities / features | `kbagent project info --project P` (0.30.0+) -- returns project ID, name, backend, enabled features, quota limits, and metrics | `tool call verify_token` (returns less structured info; no feature list) | inspecting the UI project settings manually |
| Manage feature flags (stack catalogue / project / user) | `kbagent feature list\|project-show\|project-add\|project-remove\|user-show\|user-add\|user-remove --project P [--email E] [--feature NAME] [--dry-run] [--yes]` (0.48.0+) -- Manage API; needs a SUPER-ADMIN manage token (interactive prompt; `--allow-env-manage-token`+`KBC_MANAGE_API_TOKEN` for CI); `--project` resolves the stack URL (+project_id for `project-*`); add=admin, remove=destructive; add body is `{"feature":NAME}` | `kbagent project info` for a project's *enabled* features (read-only, no super-admin) | raw `/manage/...` calls; manage token via a CLI flag |
Expand Down Expand Up @@ -358,6 +358,13 @@ kbagent workspace delete --project P --workspace-id W
Use this for TYPE AUDITS before planning retypes, ROW COUNT COMPARISONS
between branches, and SQL DEBUGGING of failing transformations.

(0.59.0+) `workspace query` returns results inline and fast, but **capped at
`--limit` rows (default 500)**. For ROW COUNTS use `SELECT COUNT(*)` (one row,
never truncated), NOT `len(rows)` of a `SELECT *`. For exact comparisons of a
result set bigger than the cap, raise `--limit` or pass `--full` (complete CSV
export, slower). Always check `statements[].truncated` / `total_rows` in `--json`
before treating the rows as complete.

### 4.5 Cross-project migration (high-risk)

Preconditions (REFUSE if not met):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ Bucket sharing + linking across projects in the same organization. `sharing edge
- `workspace delete --project ALIAS --workspace-id ID` -- delete workspace
- `workspace password --project ALIAS --workspace-id ID` -- reset and return new password
- `workspace load --project ALIAS --workspace-id ID --tables TABLE_ID [...] [--preserve]` -- load storage tables
- `workspace query --project ALIAS --workspace-id ID --sql "..." [--file F] [--transactional]` -- run SQL via Query Service. **Backend-agnostic since v0.58.0**: runs against both Snowflake and BigQuery workspaces (the path was always identical; BigQuery just needed the classification fix). Mind the dialect: Snowflake quotes identifiers with `"..."`, BigQuery with backticks `` `...` ``
- `workspace query --project ALIAS --workspace-id ID --sql "..." [--file F] [--transactional] [--full] [--limit N]` -- run SQL via Query Service. **Fast inline results since v0.59.0**: default reads the result set inline via `GET /api/v1/queries/{job}/{stmt}/results` (JSON `columns`+`rows`, no CSV-file materialization), capped at `--limit` rows (default 500) and marked `truncated` when there are more; pass `--full` for the complete CSV export (slower, uncapped). Each statement still carries `csv_data` (synthesized from the inline rows) so older parsers keep working. **Backend-agnostic since v0.58.0**: runs against both Snowflake and BigQuery workspaces. Mind the dialect: Snowflake quotes identifiers with `"..."`, BigQuery with backticks `` `...` ``
- `workspace gc [--project NAME ...] [--dry-run] [--yes]` -- garbage-collect orphaned workspaces (and any lingering `keboola.sandboxes` configs). `--dry-run` previews without deleting; `--project` repeatable, omit to GC across all connected projects
- `workspace from-transformation --project ALIAS --component-id ID --config-id ID [--row-id ID]` -- workspace from existing transform

Expand Down
31 changes: 31 additions & 0 deletions plugins/kbagent/skills/kbagent/references/gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,37 @@ confirmed-good whitelist". For an unknown loginType, `workspace list`
renders it as `?` (yellow) in the QS column so callers know the policy
is uncertain rather than confirmed-bad.

## `workspace query`: fast inline results vs `--full` CSV export (since v0.59.0)

By default `workspace query` now reads the result set inline via the Query
Service `GET /api/v1/queries/{job}/{stmt}/results` endpoint (JSON `columns` +
`rows`) instead of materializing a CSV file through the warehouse UNLOAD path
(`.../export?fileType=csv`). The inline path skips the file round-trip, so
interactive queries are markedly faster.

- The inline path is **paginated**: it fetches at most `--limit` rows (default
500), walking `offset` in pages. When the warehouse has more rows than
fetched, the statement is marked `truncated: true` (with `total_rows` =
the full count) and the CLI prints `Showing first N of TOTAL rows. Use --full`.
- The `/results` endpoint enforces **`100 <= pageSize <= 100000`** (a smaller
`pageSize` 400s with `Invalid pageSize parameter, must be between 100 and
100000`). kbagent therefore requests a fixed valid page size and trims the
result to `--limit` locally -- `pageSize` is NOT derived from `--limit`, so a
`--limit 5` still works (fetches one valid page, returns 5).
- Each statement carries structured `columns`, `rows`, `row_count`,
`total_rows`, `truncated`, **and** a synthesized `csv_data` string. Parsers
that read `csv_data` (the pre-0.59.0 shape) keep working unchanged.
VARIANT/ARRAY/OBJECT (Snowflake) and STRUCT/ARRAY (BigQuery) cells are
emitted in `csv_data` as compact JSON (`{"k":"v"}`) to match the warehouse
CSV export, not Python `repr`.
- `--full` opts back into the complete CSV export -- slower (warehouse UNLOAD),
but **uncapped**. Use it when you need every row, e.g. a bulk extract
(`workspace query --full --json`). Under `--full` the statement carries only
`csv_data` (no structured `columns`/`rows`).
- The `kbagent serve` `/workspaces/{p}/{w}/query` REST endpoint defaults to
`full=True` so the web UI's "Download CSV" stays complete; REST clients can
pass `full=false` (+ `limit`) in the JSON body to opt into the fast path.

## Snowflake `workspace create` returns `private_key`, not password (since v0.47.1)

Headless `workspace create` on Snowflake requests
Expand Down
55 changes: 55 additions & 0 deletions plugins/kbagent/skills/kbagent/references/workspace-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ kbagent --json workspace from-transformation \

```bash
# Step 2: Run the original SQL to reproduce the error
# Default (0.59.0+): results come back inline as JSON columns+rows (fast),
# capped at --limit rows (default 500). Add --full for the complete CSV export
# (slower, uncapped) when you need every row.
kbagent --json workspace query \
--project ALIAS \
--workspace-id WS_ID \
Expand Down Expand Up @@ -94,6 +97,58 @@ kbagent --json workspace query \
--file query.sql
```

## Fast inline results vs `--full` -- mind the result-set volume (since v0.59.0)

`workspace query` has two ways to retrieve results. **Pick based on how many rows
you actually need**, not by habit.

**Default (fast, inline):** reads the result set straight from the Query Service
as JSON via `GET /api/v1/queries/{job}/{stmt}/results`. No file is produced. Each
statement comes back with structured `columns` + `rows` (plus `row_count`,
`total_rows`, `truncated`) and a synthesized `csv_data` for back-compat.

- **Paginated / capped** at `--limit` rows (default 500). When the warehouse has
more rows than were fetched, the statement is flagged `truncated: true` (with
`total_rows` = the real count) and the CLI prints
`Showing first N of TOTAL rows. Use --full for the complete result set.`
- This is the right default for **inspection, row counts, sampling, schema
checks, and iterating on a fix** -- exactly the workspace-debugging loop.

**`--full` (complete CSV export, slower):** materializes the *entire* result set
as a CSV file through the warehouse UNLOAD path
(`GET .../export?fileType=csv`), then downloads it. Uncapped -- you get every
row -- but it pays a file-export round-trip (warehouse -> object storage ->
download) on every call. Under `--full` the statement carries only `csv_data`
(no structured `columns`/`rows`).

```bash
# Fast: first 500 rows inline (default). Add --limit to widen/narrow the page.
kbagent --json workspace query --project ALIAS --workspace-id WS_ID \
--sql 'SELECT * FROM "in.c-main"."events"' --limit 1000

# Complete: every row via CSV export (slower -- use only when you need them all).
kbagent --json workspace query --project ALIAS --workspace-id WS_ID \
--sql 'SELECT * FROM "in.c-main"."events"' --full
```

**Decision guide:**

- **Just looking / counting / sampling?** Use the default. Faster, and the
`truncated` flag tells you whether there is more.
- **Need a complete extract?** Use `--full` -- but **think about the volume
first**. `--full` pulls the whole result set into a single CSV string in
memory; a `SELECT *` over millions of rows is slow and memory-hungry. Narrow
the query (`SELECT` only the columns you need, add a `WHERE`/`LIMIT`) before
reaching for `--full`.
- **Bulk-exporting an actual Storage table** (not an arbitrary query)? Prefer
`storage unload-table` / `storage download-table` -- they stream sliced files
and are built for volume, whereas `workspace query --full` is for ad-hoc SQL.

**API floor:** the `/results` endpoint requires `100 <= pageSize <= 100000`.
kbagent always requests a valid page size and trims to `--limit` locally, so a
small `--limit` (e.g. `--limit 5`) works fine -- it does not shrink the wire
`pageSize` below the API minimum.

## Shared/linked buckets -- different database/dataset

Linked buckets (shared from another project) live in a **different database
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "keboola-agent-cli"
version = "0.58.0"
version = "0.59.0"
description = "AI-friendly CLI for managing Keboola projects"
readme = "README.md"
requires-python = ">=3.12"
Expand Down
15 changes: 15 additions & 0 deletions src/keboola_agent_cli/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@

# Ordered newest-first. Each value is a list of brief one-line descriptions.
CHANGELOG: dict[str, list[str]] = {
"0.59.0": [
"Faster: `kbagent workspace query` now reads results via the Query Service's inline "
"`GET /api/v1/queries/{job}/{stmt}/results` endpoint by default instead of materializing a "
"CSV file through the warehouse UNLOAD path (`.../export?fileType=csv`). The inline path "
"returns the already-computed result set as JSON -- no file export round-trip -- so interactive "
"queries come back markedly faster. Each statement now carries structured `columns` + `rows` "
"(plus `row_count`, `total_rows`, `truncated`) alongside a synthesized `csv_data`, so the CLI "
"preview, web UI table, and any `--json` consumer keep working unchanged.",
"New: `--limit N` (default 500) caps how many rows the fast inline path fetches; it pages "
"through the result set by `offset` until the limit is reached, marking the result `truncated` "
"when the warehouse has more. `--full` opts back into the complete CSV export (slower, "
"uncapped) when you need every row -- e.g. piping `workspace query --full --json` for a bulk "
"extract. The `kbagent serve` `/workspaces/{p}/{w}/query` REST endpoint defaults to `full=True` "
"to preserve the web UI's complete-CSV download until the frontend learns to paginate.",
],
"0.58.0": [
"New: `kbagent workspace query` runs SQL against BigQuery workspaces, not just Snowflake. "
"The Query Service path was always backend-agnostic (`POST "
Expand Down
38 changes: 38 additions & 0 deletions src/keboola_agent_cli/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
OAUTH_PATH,
QUERY_JOB_MAX_WAIT,
QUERY_JOB_POLL_INTERVAL,
QUERY_RESULTS_PAGE_SIZE,
STORAGE_JOB_MAX_WAIT,
STORAGE_JOB_POLL_INTERVAL,
VALID_POLL_STRATEGIES,
Expand Down Expand Up @@ -2697,6 +2698,43 @@ def export_query_results(
)
return response.text

def get_query_results(
self,
query_job_id: str,
statement_id: str,
offset: int = 0,
page_size: int = QUERY_RESULTS_PAGE_SIZE,
) -> dict[str, Any]:
"""Fetch a page of inline statement results from the Query Service.

Unlike :meth:`export_query_results`, which materializes a CSV file via the
warehouse UNLOAD path (slow), this reads the already-computed result set
inline as JSON -- much faster for interactive queries. The endpoint is
paginated; ``offset``/``page_size`` walk the result set.

Args:
query_job_id: The query job ID.
statement_id: The statement ID within the job.
offset: Row offset to start from (for pagination).
page_size: Maximum rows to return in this page.

Returns:
Raw QueryResult dict, e.g.::

{
"status": "completed",
"columns": [{"name": "id", "type": "INTEGER", "nullable": false}],
"data": [[1, "a"], [2, "b"]],
"numberOfRows": 2,
}
"""
response = self._query_request(
"GET",
f"/api/v1/queries/{query_job_id}/{statement_id}/results",
params={"offset": offset, "pageSize": page_size},
)
return response.json()
Comment thread
padak marked this conversation as resolved.

def get_query_history(
self,
branch_id: int,
Expand Down
4 changes: 3 additions & 1 deletion src/keboola_agent_cli/commands/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -698,8 +698,10 @@
kbagent workspace load --project ALIAS --workspace-id ID --tables TABLE_ID [...] [--preserve]
Load storage tables into workspace. --preserve keeps existing tables.

kbagent workspace query --project ALIAS --workspace-id ID --sql "SQL" [--file F] [--transactional]
kbagent workspace query --project ALIAS --workspace-id ID --sql "SQL" [--file F] [--transactional] [--full] [--limit N]
Execute SQL via Query Service. No Snowflake credentials needed.
Default reads results inline (fast JSON columns+rows), capped at --limit (default 500).
--full uses the complete CSV export instead (slower, uncapped).

kbagent workspace from-transformation --project ALIAS --component-id ID --config-id ID [--row-id ID]
Create workspace from transformation config. Loads input tables automatically.
Expand Down
18 changes: 18 additions & 0 deletions src/keboola_agent_cli/commands/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from rich.markup import escape

from ..config_store import ConfigStore
from ..constants import QUERY_RESULTS_DEFAULT_LIMIT
from ..errors import ConfigError, ErrorCode, KeboolaApiError
from ..output import format_query_results, format_workspaces_table
from ._helpers import (
Expand Down Expand Up @@ -414,10 +415,25 @@ def workspace_query(
"--transactional",
help="Wrap query in a transaction",
),
full: bool = typer.Option(
False,
"--full",
help="Fetch the complete result set via CSV export (slower). "
"Default fetches a fast inline page capped by --limit.",
),
limit: int = typer.Option(
QUERY_RESULTS_DEFAULT_LIMIT,
"--limit",
min=1,
help="Max rows to fetch via the fast inline path (ignored with --full).",
),
) -> None:
"""Execute SQL query in a workspace via Query Service.

Provide SQL via --sql or --file (exactly one required).

By default kbagent reads results inline (fast). For a result set larger than
--limit, pass --full to export the complete CSV instead.
"""
formatter = get_formatter(ctx)
service = get_service(ctx, "workspace_service")
Expand Down Expand Up @@ -450,6 +466,8 @@ def workspace_query(
workspace_id=workspace_id,
sql=effective_sql,
transactional=transactional,
full=full,
limit=limit,
)
if formatter.json_mode:
formatter.output(result)
Expand Down
Loading
Loading