Skip to content

feat(storage): export row filters (where/changed-since) + add-column (0.62.0) - #421

Merged
padak merged 2 commits into
mainfrom
feat/storage-export-filters
Jun 14, 2026
Merged

feat(storage): export row filters (where/changed-since) + add-column (0.62.0)#421
padak merged 2 commits into
mainfrom
feat/storage-export-filters

Conversation

@padak

@padak padak commented Jun 14, 2026

Copy link
Copy Markdown
Member

Why

Two HIGH-priority gaps from the sapi-python-client comparison tracked in #417 — the common "filter / incrementally export a table without spinning up a workspace" and "evolve a table's schema in place" patterns that kbagent couldn't do.

What

storage download-table — server-side row filters

  • --where-column + --where-value (repeatable, OR within the set) + --where-operator eq|neq
  • --changed-since / --changed-until (unix ts or strtotime like -2 days)

The credential-only, no-workspace way to pull a filtered or incremental slice. The workspace query path can do arbitrary SQL but needs a live workspace; this is the lightweight Storage-export route.

Filters thread through both export_table_async and get_table_data_preview via a shared _apply_table_filters helper, so the sync-preview and async-export endpoints honor an identical whereColumn/whereOperator/whereValues[]/changedSince/changedUntil contract.

storage add-column

Adds a single column to an existing table — same name:TYPE(length) grammar as create-table --column (--not-null, --default). Closes the long-standing asymmetry: kbagent could delete-column but not add one. The Storage add-column endpoint is synchronous (no job to poll), confirmed against the official API docs.

Layers (3-layer design)

  • client.py: _apply_table_filters helper + filter params on export_table_async / get_table_data_preview; new add_column (POST /tables/{id}/columns, sync).
  • services/storage_service.py: download_table forwards filters; new add_column (reuses _parse_column_spec).
  • commands/storage.py: download-table flags + add-column command.
  • permissions.py: storage.add-column = write.

Tests & docs

  • New test_storage_export_filters.py (11 tests: filter-helper validation, client forwarding, add_column client/service, CLI). Updated 5 existing download_table assertions for the new kwargs. E2E add-column step (convention Close must-have gaps in explorer command #16).
  • Synced CLAUDE.md, context.py AGENT_CONTEXT, keboola-expert.md matrix, commands-reference.md. command-sync-check green (231 commands).
  • make check: 4012 passed; lint / format / typecheck / skill / command-sync / changelog all green. Version 0.62.0.

Implements 2 of the HIGH items in #417.


Open in Devin Review

…(0.62.0) (#417)

Two HIGH-value gaps from the sapi-python-client comparison (#417):

- `storage download-table` gains server-side row filtering: --where-column +
  --where-value (repeatable, OR within the set) + --where-operator eq|neq, and
  --changed-since / --changed-until (unix ts or strtotime). The credential-only,
  no-workspace way to pull a filtered or incremental slice of a table. Threaded
  through export_table_async + get_table_data_preview via a shared
  _apply_table_filters helper so preview + export honor an identical contract.
- `storage add-column` adds a single column to an existing table (same
  name:TYPE(length) grammar as create-table --column; --not-null, --default),
  closing the delete-column/add-column asymmetry. Synchronous Storage endpoint.

Unit + E2E tests; docs synced (CLAUDE.md, context.py, keboola-expert.md,
commands-reference.md); permission registry (storage.add-column = write);
version 0.62.0.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

⚠️ 1 issue in files not directly in the diff

⚠️ Missing server router endpoint for new storage add-column command (src/keboola_agent_cli/server/routers/storage.py:329)

CONTRIBUTING.md mandates a 1:1 CLI-to-REST mapping: "every command in a group has a matching endpoint in that group's router [...] If you add a new command, add the corresponding route. Skip allowed only for genuinely terminal-only commands." The new storage add-column command (src/keboola_agent_cli/commands/storage.py:1113) has a service method (src/keboola_agent_cli/services/storage_service.py:1315) and a client method (src/keboola_agent_cli/client.py:2073), but no corresponding route was added to src/keboola_agent_cli/server/routers/storage.py. This means external consumers of kbagent serve (Web UI, scheduled AI agents, CI pipelines) cannot add columns via the REST API.

Open in Devin Review

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 File-size budgets exceeded across all three layers (pre-existing, worsened by PR)

CONTRIBUTING.md defines hard ceilings: commands at 1200 LOC, services at 1500 LOC, clients at 2000 LOC. Current sizes: client.py = 3422 LOC (hard ceiling 2000), storage_service.py = 2524 LOC (hard ceiling 1500), commands/storage.py = 2630 LOC (hard ceiling 1200). All three were already over the hard ceiling before this PR, but the PR adds ~115 lines to client.py, ~55 lines to storage_service.py, and ~80 lines to commands/storage.py. CONTRIBUTING.md says "When a file crosses the hard ceiling, splitting is required before merging more functionality into it." This is a pre-existing debt that the PR incrementally worsens. The splitting guidance in CONTRIBUTING.md (client by endpoint family, services by extracting helpers) provides a clear path.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pre-existing: client.py (3422), storage_service (2524), commands/storage (2630) were all over the hard ceiling before this PR. The split is tracked as dedicated tech-debt in #417 (NB-1 from the #416 review). This feature PR adds ~115 LOC to client.py (mostly docstrings); splitting a 3400-line client is a separate refactor PR, kept out of this diff so the feature stays reviewable.

Comment thread src/keboola_agent_cli/commands/context.py Outdated

@padak padak left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review of #421 — feat(storage): export row filters (where/changed-since) + add-column (0.62.0)

Generated by kbagent-pr-reviewer subagent. Verdict and findings below
are advisory; the human author retains every veto. CI-coverable issues
(lint, format, tests) are confirmed via make check, not duplicated here.

Summary

This PR adds two features to the storage group: server-side row filters (--where-column, --where-operator, --changed-since, --changed-until) on storage download-table, and a new storage add-column command that mirrors the name:TYPE(length) grammar of create-table. The 3-layer split is respected, OPERATION_REGISTRY, CLAUDE.md, context.py, keboola-expert.md, commands-reference.md, and SKILL.md are all updated, and make check passes with 4012 tests green. The verdict is REQUEST CHANGES solely because gotchas.md is missing the mandatory (since v0.62.0) version tags for both new behaviors, which is a BLOCKING gap per the playbook (AI agents recommend behavior to users on older installs that don't have the command). A secondary NON-BLOCKING issue is that the kbagent serve REST layer (server/routers/storage.py) was not updated to expose the new capability.

Verdict

  • Verdict: REQUEST CHANGES
  • Blocking findings: 1
  • Non-blocking findings: 2
  • Nits: 2

Blocking findings

[B-1] plugins/kbagent/skills/kbagent/references/gotchas.md — missing (since v0.62.0) version tags for both new features

Neither storage download-table filter flags nor storage add-column have a (since v0.62.0) entry in gotchas.md. Per CONTRIBUTING.md > "Documentation changes (mandatory!)" and the Plugin synchronization map: "every new gotcha MUST be tagged with (since vX.Y.Z). The version tag is non-optional; gotchas without versions are how AI agents end up recommending behavior that does not exist on older kbagent installs." There are at least two non-obvious behaviors that warrant entries: (1) storage download-table --where-column sends the parameter to the API as whereValues[] (array notation with brackets), not whereValues — agents that try to call the underlying Storage API directly will get the name wrong; (2) storage add-column --not-null without --default on a non-empty table is rejected by the backend with an API error, not a local validation error — agents need to know to add --default when the table already has rows.

Fix: add a ## storage download-table: server-side row filters (since v0.62.0) section and a ## storage add-column: --not-null on non-empty table (since v0.62.0) section in gotchas.md with the above notes.

Non-blocking findings

[NB-1] src/keboola_agent_cli/server/routers/storage.py:205download-table REST route does not expose the new filter parameters

server/routers/storage.py:205 defines download_table_v2 but calls registry.storage.download_table(...) without forwarding where_column, where_operator, where_values, changed_since, or changed_until. REST API callers via kbagent serve (Web UI, scheduled agents, Slack bots) can request the new filtered-export behavior from the CLI but not through the REST surface. Per CONTRIBUTING.md > "Server router HTTP API endpoint": "the current convention is 1:1: every command in a group has a matching endpoint in that group's router." The same gap applies to preview_table_v2 (line 171) which calls client.get_table_data_preview without filters. The PR description does not document a skip reason.

Fix: add the five filter query params to download_table_v2 and forward them; optionally also extend preview_table_v2.

[NB-2] src/keboola_agent_cli/server/routers/storage.pyadd-column command has no REST endpoint

The storage router has delete-column at DELETE /columns/{project}/{table_id:path} (line 310) and describe-column at POST /columns/.../describe (line 388), but the new add-column command has no corresponding route. This is the same 1:1 convention gap: kbagent serve REST callers cannot use add-column via the API even though the mirror command delete-column is exposed.

Fix: add POST /columns/{project}/{table_id:path} endpoint in server/routers/storage.py that accepts column, not_null, default, and branch_id and delegates to registry.storage.add_column(...).

Nits

  • [NIT-1] src/keboola_agent_cli/commands/context.py:383 — the add-column entry is inserted between the download-table signature line and its continuation text ("Default filename: TABLE_NAME.csv. Use --columns ..."). After the diff, lines 385-386 read as if they describe add-column, not download-table. Move the "Default filename" and "Use --limit" continuation lines to immediately follow the updated download-table entry (before the add-column block) so the association is unambiguous to agents parsing AGENT_CONTEXT linearly.
  • [NIT-2] tests/test_storage_export_filters.py:127TestAddColumnService.test_parses_spec_and_calls_client does not assert mock_client.close.assert_called_once(). Per CONTRIBUTING.md Testing Guidelines: "Verify client.close() is called." The finally: client.close() path in storage_service.py:1360 is already covered by the production code, but the test does not confirm it fires.

Verification log

  • gh pr view 421 --json title,body,files,additions,deletions,baseRefName,headRefName,labels,state → 17 files, +590/-7, feat(storage): conventional-commit prefix matches two new features, state OPEN, headRefName matches worktree branch feat/storage-export-filters
  • git rev-parse --abbrev-ref HEADfeat/storage-export-filters ✓ (worktree on the correct branch)
  • 3-layer compliance: grep -E '^\+' diff | grep typer ... services/ → empty; grep httpx commands/ → empty; grep formatter client.py → empty ✓ no layer violations
  • Convention compliance: no magic numbers, no raw error_code="..." strings, no bare except:, no print() in production code, no token exposure ✓
  • make check4012 passed, 8 skipped, 124 deselected, 17 warnings in 105.85s
  • Plugin synchronization map walk:
    • src/keboola_agent_cli/permissions.py OPERATION_REGISTRY"storage.add-column": "write" added at line 453 ✓
    • CLAUDE.md ## All CLI Commandsdownload-table signature updated, add-column line added ✓
    • src/keboola_agent_cli/commands/context.py AGENT_CONTEXT → both commands updated ✓ (layout issue noted as NIT-1)
    • plugins/kbagent/agents/keboola-expert.md §2 Tool Selection Matrix → two new rows added for add-column and filtered-export ✓
    • plugins/kbagent/skills/kbagent/references/commands-reference.mddownload-table and add-column entries updated ✓
    • plugins/kbagent/skills/kbagent/SKILL.mdadd-column row added ✓
    • plugins/kbagent/skills/kbagent/references/gotchas.md → NO new version-tagged entry ✗ [B-1]
    • src/keboola_agent_cli/server/routers/storage.pyadd-column endpoint absent; download_table_v2 does not forward filter params ✗ [NB-1, NB-2]
  • Backward compat on download_table service signature: new params are keyword-only with safe defaults (where_column=None, where_operator="eq", etc.); existing callers in the server router and old tests remain unaffected ✓
  • client.py file-size budget: 3422 LOC against a 2000 LOC hard ceiling — pre-existing violation (113 LOC pre-PR, 115 added); this PR is not the first to cross it but the soft and hard ceilings are both exceeded. Not flagged as a finding since the violation precedes this PR, but worth noting as a split-debt signal.
  • Behavior reproduction: did not execute against a live Keboola project (no E2E_API_TOKEN in scope for the reviewer run). The E2E test at tests/test_e2e.py:2561 covers add-column against a real API when credentials are present; the new filter flags have no dedicated E2E test (they would require a table with sufficient rows to exercise server-side filtering — acceptable deferral, noted per CONTRIBUTING.md §16).

Open questions for the author

  • For storage add-column, the service discards the actual API response (client.add_column() returns the updated table resource) and instead returns a synthetic dict built from _parse_column_spec. The human-mode output therefore shows the requested type, not the accepted type the API stored (these can differ on BigQuery where the backend normalises types). Is this intentional? If so, a comment in storage_service.py around line 1361 explaining the design choice would prevent future contributors from "fixing" it to return the raw API response.

…rder

kbagent-pr-reviewer + Devin findings on PR #421:

- REST parity (1:1 CLI-to-REST): add POST /storage/columns/{project}/{table_id}
  for add-column, and forward where/changed filters in download_table_v2 +
  preview_table_v2. The add-column route is registered AFTER /columns/.../describe
  so the greedy {table_id:path} doesn't shadow that POST (caught by a server
  router test).
- gotchas.md: two (since v0.62.0) entries -- the whereValues[] array notation for
  raw-API callers, and add-column --not-null needing --default on a non-empty table.
- context.py: move the download-table continuation lines before the add-column
  block so they aren't misattributed to add-column in AGENT_CONTEXT.
- test: assert client.close() in the add_column service test.

File-size budgets (client.py/storage_service/commands.storage over ceiling) are
pre-existing debt tracked in #417 -- not split in this feature PR to keep the diff
reviewable.
@padak

padak commented Jun 14, 2026

Copy link
Copy Markdown
Member Author

Addressed the kbagent-pr-reviewer review + Devin findings in eee703e:

  • B-1 (blocking): added two `(since v0.62.0)` gotchas — the `whereValues[]` array notation for raw-API callers, and `add-column --not-null` needing `--default` on a non-empty table.
  • REST parity (NB-1/NB-2 + Devin): `download_table_v2` + `preview_table_v2` now forward the five filter params, and added `POST /storage/columns/{project}/{table_id}` for add-column. The add-column route is registered after `/columns/.../describe` so the greedy `{table_id:path}` does not shadow it — a real route-ordering bug a server router test caught.
  • NIT-1: moved the download-table continuation lines before the add-column block in AGENT_CONTEXT.
  • NIT-2: the `add_column` service test now asserts `client.close()`.
  • File-size budgets (Devin): pre-existing — all three files were over the hard ceiling before this PR. Tracked as dedicated tech-debt in Storage-client parity gaps & robustness improvements (from sapi-python-client comparison) #417 (NB-1 from the feat(lib): importable in-process library facade (Client/Files) (0.61.0) #416 review); not split here to keep this feature diff reviewable.

`make check` green (4012 passed); server router + route-order tests pass.

@padak
padak merged commit dac93f3 into main Jun 14, 2026
4 checks passed
@padak
padak deleted the feat/storage-export-filters branch June 14, 2026 19:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant