fix(storage): surface the table definition on table-detail (#621) - #629
Conversation
`keboola-expert.md` sat at 61 960 B against a 62 000 B ceiling -- under 100 bytes of headroom. At that margin the budget stopped doing its job and became a tripwire: any PR needing to add a line to the prompt first had to find and justify an unrelated trim, which is review cost with no reviewer benefit. Raise it to 70 000 B. That is still ~12% under the ~80 kB (~20k token) reference point the budget was originally derived from, and the standing guidance is unchanged and restated in the constant's comment: exhaustive per-command detail belongs in `AGENT_CONTEXT`, which loads on demand, and the real answer to sustained growth is splitting the prompt into per-domain specialists rather than another bump. The three prose sites still said "60 KB" -- stale since v0.48.0 moved the ceiling to 62 000 B -- so an author trimming to the documented figure trimmed ~2 kB more than CI required. They now quote the enforced number in the `70 000 B` form and point at the test that asserts it. This overlaps with the open PR #586 (issue #585), which adds a test gating exactly this doc/enforced drift; the wording here is written to satisfy that gate on either merge order.
`storage create-table --source-table-id ... --time-partitioning-field ...`
followed by `storage swap-tables` is the documented way to repartition a
populated BigQuery table. Nothing in kbagent could read the result back.
`StorageService.get_table_detail()` assembled its response from an explicit
field allowlist and `definition` was never on it, so the one object carrying
the registered `timePartitioning` / `rangePartitioning` / `clustering` was
dropped on the floor. The write half of the flow was supported, the verify
half was not -- confirming a swap meant leaving kbagent for raw Storage API
calls or BigQuery metadata access, which is not available at all in a
Keboola-managed BigQuery project without `bigquery.jobs.create`. The table ID
is identical whether or not the swap happened; the layout is the only thing
that tells the two apart. `create-table` is no substitute: its JSON echoes
the layout that was REQUESTED, and its `--if-not-exists` skip path nulls the
layout keys outright rather than re-deriving the existing table's.
The data was already in hand -- `StorageTablesClient.get_table_detail()`
returns `GET /v2/storage/tables/{id}` unfiltered, and that response carries
`definition` with no `include=` needed.
Two upstream details, read off connection's `TableDetailResponseProvider` and
`BigqueryDriverConfig::extendTableDefinitionResponse`, shape the rendering:
* `definition` is set on EVERY table-detail response, untyped and Snowflake
tables included. A null therefore means the stack omitted the key -- never
"this table is untyped" -- so the human-mode block keys off the layout
fields themselves. Verified live against a Snowflake project: output is
byte-identical to before.
* When partitioning is set the response also carries `requirePartitionFilter`
and `partitions[]`, one entry per physical partition from
INFORMATION_SCHEMA.PARTITIONS. That list is unbounded -- thousands of
entries on a long-lived daily table -- so human mode prints its length and
never its contents. `--json` passes the whole thing through: re-dropping an
API field is the bug being fixed here, so it is not repeated one level down.
The value is also type-checked before any `.get()` reaches it. The Storage API
has really served `definition` as `[]` rather than an object (SUPPORT-16581,
pinned in tests/test_storage_empty_definition.py) -- that shape broke the Go
CLI's decoder, and `definition.get(...)` would raise on it.
`GET /storage/table-detail/...` on `kbagent serve` picks the field up for free
via the same service. `storage tables` (the LIST endpoint) is deliberately
untouched: the Storage API's `include=` there accepts no `definition` value
(`TablesListRequest::ALLOWED_INCLUDE`), so surfacing the layout in a listing
would cost one detail request per table -- a different change with a different
cost profile.
Both files this touches are grandfathered over their file-size ceiling and may
only shrink, so the change is made by extracting rather than appending:
response assembly moves to the pure, client-free
`services/_table_detail.py`, and human rendering to
`commands/_storage_table_detail.py`. Net effect is -49 code lines in
`storage_service.py` and -25 in `commands/storage.py`, both re-recorded in the
size baseline. `create-table` now shares the layout formatters, so both
commands print the same string for the same layout -- which is what makes
diffing "what I asked for" against "what landed" trustworthy.
E2E gains the assertion that matters: after the BigQuery repartition + swap,
`table-detail` on the production name must report the clustering that
`create-table` applied.
`kbagent changelog` and the "What's new" banner show only the first sentence, capped at 160 chars; the original opener was cut off mid-clause.
padak
left a comment
There was a problem hiding this comment.
Review of #629 — fix(storage): surface the table definition on table-detail (#621)
Generated by
kbagent-pr-reviewersubagent. Verdict and findings below
are advisory; the human author retains every veto. CI-coverable issues
(lint, format, tests) are confirmed viamake check, not duplicated here.
Summary
This PR fixes storage table-detail dropping the Storage API's definition object — on BigQuery the only readable record of a table's registered timePartitioning/rangePartitioning/clustering layout — which left the "verify a repartition" half of the documented create-table --source-table-id + swap-tables flow unsupported. The service now returns definition verbatim (extracted into a new pure services/_table_detail.py to stay inside the grandfathered file-size ceiling), human mode renders the layout via a new commands/_storage_table_detail.py, and the whole Plugin synchronization map (context.py, CLAUDE.md, keboola-expert.md matrix + gotchas budget bump, SKILL.md, commands-reference.md, gotchas.md with a (since v0.88.0, #621) tag, storage-types-workflow.md) plus service/CLI/E2E tests were all updated in the same PR. Verdict: COMMENT — no blocking findings, one small non-blocking style nit on a newly-extracted tuple return.
Verdict
- Verdict: COMMENT
- Blocking findings: 0
- Non-blocking findings: 1
- Nits: 1
Blocking findings
(none)
Non-blocking findings
[NB-1] src/keboola_agent_cli/services/_table_detail.py:538 — newly-extracted _split_descriptions returns a bare 2-tuple of semantically distinct values
_split_descriptions(raw_metadata: list[dict]) -> tuple[str, dict[str, str]] is a new function created by this PR's services/storage_service.py -> services/_table_detail.py extraction (it did not exist as a named function before; the logic was inline). It returns (description, col_descriptions) — a scalar table description and a per-column description map, which is exactly the "semantically distinct values" case CONTRIBUTING.md's "Return values -- name them with dataclasses, not tuples" section calls out (resolve_project example). The call site description, col_descriptions = _split_descriptions(raw_metadata) already relies on positional order to disambiguate.
Fix: wrap in a small @dataclass(frozen=True) (e.g. class DescriptionSplit: table: str; columns: dict[str, str]) or simply return a dict with those two keys. Small surface, no urgency, but flagging so it doesn't get grandfathered by accident.
Nits
[NIT-1]PR bundles a genuinely unrelated internal change (raisingPROMPT_BYTE_BUDGET62 000 B -> 70 000 B intests/test_agent_prompt.py, plus matchingCONTRIBUTING.md/kbagent-pr-reviewer.mdedits) into afix:-scoped PR as a second commit. It's well justified and clearly disclosed in the PR description ("Prompt budget (second commit)"), and was evidently necessary here (keboola-expert.mdwas at 61 960/62 000 B before this PR, i.e. ~40 bytes of headroom — not enough for the new matrix row + gotcha), so this is not a real scope objection, just noting it for the record since strictly it's a separate logical change from the#621fix.
Verification log
git rev-parse --abbrev-ref HEAD->claude/issue-621-table-definition(already on PR branch, matches<branch>input) ✓gh auth status-> authenticated aspadak✓gh pr view 629 --json title,body,files,additions,deletions,baseRefName,headRefName,labels,state-> OPEN,main<-claude/issue-621-table-definition, 23 files, +663/-? (mergeable) ✓- Read
CONTRIBUTING.md(Checklist / Plugin synchronization map / Releasing),CLAUDE.mdconv. #17 +## All CLI Commands,plugins/kbagent/agents/keboola-expert.md§1/§2/§3 ✓ gh pr diff 629(1086 lines) reviewed in full ✓- Layer-violation greps (typer/console in services; httpx in commands) -> empty, no violations ✓
- Magic-number / raw error-code / bare-except /
print()/ token-leak greps on the diff -> empty (only a doc line mentioning "budget" matched the token regex, false positive) ✓ - New
-> tuple[...]grep on the diff -> found_split_descriptions(see NB-1); no other new tuple returns ✓ grep -n '"storage.table-detail"' src/keboola_agent_cli/permissions.py->"read", already registered — this PR adds a field, not a new command, soOPERATION_REGISTRYcorrectly needs no change ✓grep -n "table-detail" src/keboola_agent_cli/server/routers/storage.py-> route exists, delegates toregistry.storage.get_table_detail(...), sokbagent servepicks updefinitionautomatically as the PR description claims ✓uv run python scripts/check_command_sync.py->OK: all 261 CLI commands are registered (OPERATION_REGISTRY) and documented (CLAUDE.md, context.py, commands-reference.md).✓uv run python scripts/check_sentinel_guards.py->OK: ... all 10 guards covered by SESSION_UNSUPPORTED_FEATURES✓make loc-check->OK: 214 modules within budget (7 over soft, 6 grandfathered); new files_table_detail.py(125 lines) /_storage_table_detail.py(113 lines) well under ceilings; grandfatheredstorage_service.py/commands/storage.pyboth shrank perscripts/file_size_baseline.jsondiff (1733->1684, 2246->2221) ✓make check(background, 133s) ->5712 passed, 12 skipped, 161 deselectedexit 0 ✓uv run ruff check/ruff format --check/ty checkon the touched/new files -> all clean ✓make changelog-check->All 45 stable releases have changelog entries.(0.88.0 entry present) ✓uv run pytest tests/test_storage_table_definition.py tests/test_storage_empty_definition.py -v-> 14/14 passed, including thedefinition: []defensive-shape regression test referenced by the PR description ✓- Behavior claim reproduction: could not hit a live BigQuery project with a partitioned table (no credentials in this environment / per repo convention AI agents never handle tokens) — relied on the unit/CLI-layer tests above, which directly assert the claimed human-mode strings (
Time partitioning: DAY on created_at,Partitions: 2not the full list,Partition filter required: yes) and the--jsonpassthrough shape. Marking this NON-VERIFIED-LIVE rather than a finding, since the test coverage is a faithful proxy and the PR also adds a real E2E assertion intests/test_e2e.py(_test_create_table_from_source,_test_table_listing) that will run against a live project on the nextmake test-e2e/ nightly E2E workflow. - Plugin synchronization map walk:
commands/context.py(AGENT_CONTEXT) ✓,CLAUDE.md✓,keboola-expert.md§2 matrix row updated + §3 gotchas not needed (existing "Storage" gotcha section extended ingotchas.mdinstead, appropriately) ✓,SKILL.mdrow updated ✓,commands-reference.mdbullet updated with(since 0.88.0)✓,gotchas.mdnew entry tagged(since v0.88.0, #621)✓,storage-types-workflow.mdextended with a verification step ✓ — every applicable "NO" row from the table was addressed.
Open questions for the author
(none)
…w NB-1)
`_split_descriptions` was extracted in the previous commit and returned a bare
`tuple[str, dict[str, str]]` -- a scalar table description and a per-column
description map, disambiguated only by position. CONTRIBUTING.md's "Return
values -- name them with dataclasses, not tuples" calls out exactly this case
("even two-element tuples should use a dataclass when the values are
semantically distinct"), and the rule applies since the function is new here,
not grandfathered inline code.
Returns a frozen `Descriptions(table=..., columns=...)` instead. No behavior
change; covered by the existing table-detail and describe-service tests.
|
Review addressed. NB-1 (bare tuple return) — fixed in 4th commit. NIT-1 (bundled prompt-budget change) — acknowledged, keeping it. It is a separate logical change and it is deliberately a separate commit, but it is not separable in practice:
|
The agent prompt sat at 61999 B against what was then a 62000 B PROMPT_BYTE_BUDGET -- 1 byte free, so the next command group that genuinely needed a tool-matrix row could not get one. v0.88.0 has since raised the cap to 70000 B, and the comment introducing that bump says the quiet part out loud: "It is NOT a licence to grow the file... Trim before you add." This is that trim. The budget is a real runtime cost -- the prompt loads into every subagent invocation -- so headroom should come from removing duplication, not from the ceiling. The bloat was not the version tags: ~95 (since vX.Y.Z) notes are only ~1 KB in total. It was section 2, the tool selection matrix, at 32 KB -- 52% of the file -- because its cells had grown into a second manual. The 37 files in skills/kbagent/references/ already carry that prose; semantic-layer- workflow.md even opens with a "When to use what" table in the same intent-to-command shape, making the 11 semantic-layer rows (7.3 KB) near verbatim duplication. - section 2: 32044 -> 21092 B. Cells now carry the decision only (command, outcome-changing flags, the NEVER list) and point at the matching *-workflow.md for the rationale. The semantic-layer block collapses from 11 rows to 2 (read, and "any write: export first, then read the workflow"). - section 3: 14768 -> 10978 B. Entries whose prose duplicated gotchas.md are back to one-line triggers, per the section's own stated design rule. - Retired version tags whose floor no longer bites. Kept every gate where an older version is silently wrong: 0.54.0 plaintext #-secrets, 0.66.1 dormant cron, 0.86.0 Azure cipher, 0.87.0 data-app workspace flag. - Dropped the hand-copied session_unsupported_features list, which the surrounding text already tells the agent not to reconstruct from memory -- auth login --json ships it. Kept the dev-portal and flow list/detail carve-outs so the agent does not pre-emptively refuse working commands. Rule 6 is reworded in the same pass. It used to say every command carries its own since-tag and to treat those as the authoritative floor; under that wording a stripped tag would silently read as "safe on any version". It now states that a tag marks a floor that still bites, that its absence is not a promise, and that a No such command error is a failed version gate. The 0.88.0 verification guidance added to the BigQuery repartition row in #629 is preserved through the rebase: table-detail --json -> .definition.timePartitioning / .clustering, because create-table only echoes the layout you requested. Nothing moved into gotchas.md -- every trimmed block was already documented there or in a topical workflow file, so no reference file changed. The freed headroom immediately absorbs the two features that had to skip this file: config clone (#587) and config state-get/state-set (#593) now have matrix rows instead of living only in gotchas.md. 47830 B against the 70000 B budget, 22170 free. PROMPT_BYTE_BUDGET itself is untouched by this PR. All 41 tests in tests/test_agent_prompt.py pass.
[B-1] Wire all five new flags through the `kbagent serve` routers. Each
router docstring claims it "Mirrors" its CLI command, and `search` already
exposed `--regex`, so the 1:1 contract demonstrably covers flags and not just
commands -- leaving these out made those docstrings false and left scheduled
agent tasks (which reach kbagent over REST, not argv) without the parity this
work is about:
- `GET /jobs` gains `offset`, `sort_by`, `sort_order`
- `GET /jobs/{project}/{job_id}` gains `log_tail_lines`
- `GET /search` gains repeatable `scope`
- `GET /storage/tables` gains `include_usage`
- `POST /sharing/{project}/link` body gains `stage`
[NB-1] `_fetch_tables` no longer conflates two meanings in its third tuple
slot. It was `True` without a usage scan and the component listing with one,
forcing the consumer to sniff `len(result) > 2` and widening the annotation to
`Any`. `_run_parallel` tells success from error by tuple LENGTH (base.py:253),
never by that element, so the slot now always carries the component listing
(empty when no scan was asked for) and the consumer destructures directly.
[NB-2] Added E2E coverage for all five flags in `TestE2EMcpParityCommands`:
the live-stack contracts the mocked suite cannot check (Queue API accepting
`sortBy`/`sortOrder`, `logTail` on a real finished job, `used_by` shape from a
real component listing) plus the three exit-2 validation paths. NOT RUN here
-- `make test-e2e` needs E2E_API_TOKEN against a real project.
Rebased onto #629. This branch carries no version bump: main is already at an
unreleased 0.88.0, so the changelog bullets join that entry and the version is
bumped once for the whole batch at release time. The byte-budget trims to
`keboola-expert.md` are reverted -- #629 raised that ceiling to 70 000 B, so
the prose they paid for is affordable again.
make check green against the rebased base: 5761 passed.
The agent prompt sat at 61999 B against what was then a 62000 B PROMPT_BYTE_BUDGET -- 1 byte free, so the next command group that genuinely needed a tool-matrix row could not get one. v0.88.0 has since raised the cap to 70000 B, and the comment introducing that bump says the quiet part out loud: "It is NOT a licence to grow the file... Trim before you add." This is that trim. The budget is a real runtime cost -- the prompt loads into every subagent invocation -- so headroom should come from removing duplication, not from the ceiling. The bloat was not the version tags: ~95 (since vX.Y.Z) notes are only ~1 KB in total. It was section 2, the tool selection matrix, at 32 KB -- 52% of the file -- because its cells had grown into a second manual. The 37 files in skills/kbagent/references/ already carry that prose; semantic-layer- workflow.md even opens with a "When to use what" table in the same intent-to-command shape, making the 11 semantic-layer rows (7.3 KB) near verbatim duplication. - section 2: 32044 -> 21092 B. Cells now carry the decision only (command, outcome-changing flags, the NEVER list) and point at the matching *-workflow.md for the rationale. The semantic-layer block collapses from 11 rows to 2 (read, and "any write: export first, then read the workflow"). - section 3: 14768 -> 10978 B. Entries whose prose duplicated gotchas.md are back to one-line triggers, per the section's own stated design rule. - Retired version tags whose floor no longer bites. Kept every gate where an older version is silently wrong: 0.54.0 plaintext #-secrets, 0.66.1 dormant cron, 0.86.0 Azure cipher, 0.87.0 data-app workspace flag. - Dropped the hand-copied session_unsupported_features list, which the surrounding text already tells the agent not to reconstruct from memory -- auth login --json ships it. Kept the dev-portal and flow list/detail carve-outs so the agent does not pre-emptively refuse working commands. Rule 6 is reworded in the same pass. It used to say every command carries its own since-tag and to treat those as the authoritative floor; under that wording a stripped tag would silently read as "safe on any version". It now states that a tag marks a floor that still bites, that its absence is not a promise, and that a No such command error is a failed version gate. The 0.88.0 verification guidance added to the BigQuery repartition row in #629 is preserved through the rebase: table-detail --json -> .definition.timePartitioning / .clustering, because create-table only echoes the layout you requested. Nothing moved into gotchas.md -- every trimmed block was already documented there or in a topical workflow file, so no reference file changed. The freed headroom immediately absorbs the two features that had to skip this file: config clone (#587) and config state-get/state-set (#593) now have matrix rows instead of living only in gotchas.md. 47830 B against the 70000 B budget, 22170 free. PROMPT_BYTE_BUDGET itself is untouched by this PR. All 41 tests in tests/test_agent_prompt.py pass.
[B-1] Wire all five new flags through the `kbagent serve` routers. Each
router docstring claims it "Mirrors" its CLI command, and `search` already
exposed `--regex`, so the 1:1 contract demonstrably covers flags and not just
commands -- leaving these out made those docstrings false and left scheduled
agent tasks (which reach kbagent over REST, not argv) without the parity this
work is about:
- `GET /jobs` gains `offset`, `sort_by`, `sort_order`
- `GET /jobs/{project}/{job_id}` gains `log_tail_lines`
- `GET /search` gains repeatable `scope`
- `GET /storage/tables` gains `include_usage`
- `POST /sharing/{project}/link` body gains `stage`
[NB-1] `_fetch_tables` no longer conflates two meanings in its third tuple
slot. It was `True` without a usage scan and the component listing with one,
forcing the consumer to sniff `len(result) > 2` and widening the annotation to
`Any`. `_run_parallel` tells success from error by tuple LENGTH (base.py:253),
never by that element, so the slot now always carries the component listing
(empty when no scan was asked for) and the consumer destructures directly.
[NB-2] Added E2E coverage for all five flags in `TestE2EMcpParityCommands`:
the live-stack contracts the mocked suite cannot check (Queue API accepting
`sortBy`/`sortOrder`, `logTail` on a real finished job, `used_by` shape from a
real component listing) plus the three exit-2 validation paths. NOT RUN here
-- `make test-e2e` needs E2E_API_TOKEN against a real project.
Rebased onto #629. This branch carries no version bump: main is already at an
unreleased 0.88.0, so the changelog bullets join that entry and the version is
bumped once for the whole batch at release time. The byte-budget trims to
`keboola-expert.md` are reverted -- #629 raised that ceiling to 70 000 B, so
the prose they paid for is affordable again.
make check green against the rebased base: 5761 passed.
[B-1] Wire all five new flags through the `kbagent serve` routers. Each
router docstring claims it "Mirrors" its CLI command, and `search` already
exposed `--regex`, so the 1:1 contract demonstrably covers flags and not just
commands -- leaving these out made those docstrings false and left scheduled
agent tasks (which reach kbagent over REST, not argv) without the parity this
work is about:
- `GET /jobs` gains `offset`, `sort_by`, `sort_order`
- `GET /jobs/{project}/{job_id}` gains `log_tail_lines`
- `GET /search` gains repeatable `scope`
- `GET /storage/tables` gains `include_usage`
- `POST /sharing/{project}/link` body gains `stage`
[NB-1] `_fetch_tables` no longer conflates two meanings in its third tuple
slot. It was `True` without a usage scan and the component listing with one,
forcing the consumer to sniff `len(result) > 2` and widening the annotation to
`Any`. `_run_parallel` tells success from error by tuple LENGTH (base.py:253),
never by that element, so the slot now always carries the component listing
(empty when no scan was asked for) and the consumer destructures directly.
[NB-2] Added E2E coverage for all five flags in `TestE2EMcpParityCommands`:
the live-stack contracts the mocked suite cannot check (Queue API accepting
`sortBy`/`sortOrder`, `logTail` on a real finished job, `used_by` shape from a
real component listing) plus the three exit-2 validation paths. NOT RUN here
-- `make test-e2e` needs E2E_API_TOKEN against a real project.
Rebased onto #629. This branch carries no version bump: main is already at an
unreleased 0.88.0, so the changelog bullets join that entry and the version is
bumped once for the whole batch at release time. The byte-budget trims to
`keboola-expert.md` are reverted -- #629 raised that ceiling to 70 000 B, so
the prose they paid for is affordable again.
make check green against the rebased base: 5761 passed.
[B-1] Wire all five new flags through the `kbagent serve` routers. Each
router docstring claims it "Mirrors" its CLI command, and `search` already
exposed `--regex`, so the 1:1 contract demonstrably covers flags and not just
commands -- leaving these out made those docstrings false and left scheduled
agent tasks (which reach kbagent over REST, not argv) without the parity this
work is about:
- `GET /jobs` gains `offset`, `sort_by`, `sort_order`
- `GET /jobs/{project}/{job_id}` gains `log_tail_lines`
- `GET /search` gains repeatable `scope`
- `GET /storage/tables` gains `include_usage`
- `POST /sharing/{project}/link` body gains `stage`
[NB-1] `_fetch_tables` no longer conflates two meanings in its third tuple
slot. It was `True` without a usage scan and the component listing with one,
forcing the consumer to sniff `len(result) > 2` and widening the annotation to
`Any`. `_run_parallel` tells success from error by tuple LENGTH (base.py:253),
never by that element, so the slot now always carries the component listing
(empty when no scan was asked for) and the consumer destructures directly.
[NB-2] Added E2E coverage for all five flags in `TestE2EMcpParityCommands`:
the live-stack contracts the mocked suite cannot check (Queue API accepting
`sortBy`/`sortOrder`, `logTail` on a real finished job, `used_by` shape from a
real component listing) plus the three exit-2 validation paths. NOT RUN here
-- `make test-e2e` needs E2E_API_TOKEN against a real project.
Rebased onto #629. This branch carries no version bump: main is already at an
unreleased 0.88.0, so the changelog bullets join that entry and the version is
bumped once for the whole batch at release time. The byte-budget trims to
`keboola-expert.md` are reverted -- #629 raised that ceiling to 70 000 B, so
the prose they paid for is affordable again.
make check green against the rebased base: 5761 passed.
[B-1] Wire all five new flags through the `kbagent serve` routers. Each
router docstring claims it "Mirrors" its CLI command, and `search` already
exposed `--regex`, so the 1:1 contract demonstrably covers flags and not just
commands -- leaving these out made those docstrings false and left scheduled
agent tasks (which reach kbagent over REST, not argv) without the parity this
work is about:
- `GET /jobs` gains `offset`, `sort_by`, `sort_order`
- `GET /jobs/{project}/{job_id}` gains `log_tail_lines`
- `GET /search` gains repeatable `scope`
- `GET /storage/tables` gains `include_usage`
- `POST /sharing/{project}/link` body gains `stage`
[NB-1] `_fetch_tables` no longer conflates two meanings in its third tuple
slot. It was `True` without a usage scan and the component listing with one,
forcing the consumer to sniff `len(result) > 2` and widening the annotation to
`Any`. `_run_parallel` tells success from error by tuple LENGTH (base.py:253),
never by that element, so the slot now always carries the component listing
(empty when no scan was asked for) and the consumer destructures directly.
[NB-2] Added E2E coverage for all five flags in `TestE2EMcpParityCommands`:
the live-stack contracts the mocked suite cannot check (Queue API accepting
`sortBy`/`sortOrder`, `logTail` on a real finished job, `used_by` shape from a
real component listing) plus the three exit-2 validation paths. NOT RUN here
-- `make test-e2e` needs E2E_API_TOKEN against a real project.
Rebased onto #629. This branch carries no version bump: main is already at an
unreleased 0.88.0, so the changelog bullets join that entry and the version is
bumped once for the whole batch at release time. The byte-budget trims to
`keboola-expert.md` are reverted -- #629 raised that ceiling to 70 000 B, so
the prose they paid for is affordable again.
make check green against the rebased base: 5761 passed.
* feat: close the remaining keboola-mcp-server parity gaps (0.88.0) Five read-side flags, each covering a capability the MCP server had and kbagent did not. Verified against keboola/mcp-server v1.76.2: 41 registered tools, all now mapped in docs/mcp-migration.md. - `storage tables --include-usage` -- MCP `get_tables(include_usage=True)`. Only storage input/output mappings count as a reference; a table id inside a transformation's SQL is text that happens to match, and counting it would make "what breaks if I drop this table?" answer with false positives. One extra component listing per project, not per table. - `job detail --log-tail-lines N` -- MCP `get_jobs(include_logs=True)`. The log tail previously existed only on the `job run --wait` path, so reading the logs of an already-finished job had no CLI route. - `job list --offset/--sort-by/--sort-order` -- MCP `get_jobs` paging. - `search --scope PATH` -- MCP `search(scopes=...)`. Scopes are written relative to the configuration body; the `configuration.` / `rows[N].configuration.` wrapper is normalised away. - `sharing link --stage in|out` -- MCP `link_shared_bucket(target_stage=...)`. The default stays `in` rather than deriving from the source bucket the way MCP does: silently relocating where existing scripts' buckets land is the worse surprise. docs/mcp-migration.md gains the two Data Catalog tools it was missing (`get_shared_buckets`, `link_shared_bucket`) and records the mcp-server version the map was verified against. `commands/storage.py` and `services/storage_service.py` are both over their file-size budget, so the code that would have grown them moved out instead: `commands/_storage_tables_render.py`, `services/_storage_tables.py` and `services/table_usage.py`. Both files end up smaller than before. * fix: address PR #632 review -- REST parity, tuple typing, E2E coverage [B-1] Wire all five new flags through the `kbagent serve` routers. Each router docstring claims it "Mirrors" its CLI command, and `search` already exposed `--regex`, so the 1:1 contract demonstrably covers flags and not just commands -- leaving these out made those docstrings false and left scheduled agent tasks (which reach kbagent over REST, not argv) without the parity this work is about: - `GET /jobs` gains `offset`, `sort_by`, `sort_order` - `GET /jobs/{project}/{job_id}` gains `log_tail_lines` - `GET /search` gains repeatable `scope` - `GET /storage/tables` gains `include_usage` - `POST /sharing/{project}/link` body gains `stage` [NB-1] `_fetch_tables` no longer conflates two meanings in its third tuple slot. It was `True` without a usage scan and the component listing with one, forcing the consumer to sniff `len(result) > 2` and widening the annotation to `Any`. `_run_parallel` tells success from error by tuple LENGTH (base.py:253), never by that element, so the slot now always carries the component listing (empty when no scan was asked for) and the consumer destructures directly. [NB-2] Added E2E coverage for all five flags in `TestE2EMcpParityCommands`: the live-stack contracts the mocked suite cannot check (Queue API accepting `sortBy`/`sortOrder`, `logTail` on a real finished job, `used_by` shape from a real component listing) plus the three exit-2 validation paths. NOT RUN here -- `make test-e2e` needs E2E_API_TOKEN against a real project. Rebased onto #629. This branch carries no version bump: main is already at an unreleased 0.88.0, so the changelog bullets join that entry and the version is bumped once for the whole batch at release time. The byte-budget trims to `keboola-expert.md` are reverted -- #629 raised that ceiling to 70 000 B, so the prose they paid for is affordable again. make check green against the rebased base: 5761 passed.
Closes #621.
The gap
storage create-table --source-table-id ... --time-partitioning-field ...followed bystorage swap-tablesis the documented way to repartition a populated BigQuery table. Nothing in kbagent could read the result back.StorageService.get_table_detail()built its response from an explicit field allowlist anddefinitionwas never on it, so the one object carrying the registeredtimePartitioning/rangePartitioning/clusteringwas dropped. The write half of the flow was supported, the verify half was not.That matters more than "a field is missing":
create-tableis not a second opinion. Its--jsonechoes the layout you requested (storage_service.pypasses the input args straight into the result), and its--if-not-existsskip path sets all three layout keys tonullon purpose — the existing table's layout is never re-derived.bigquery.jobs.create, the Storagedefinitionis the only reachable view of the registered layout.The data was already in hand:
StorageTablesClient.get_table_detail()returnsGET /v2/storage/tables/{id}unfiltered, and that response carriesdefinitionwith noinclude=required.Three things the issue got wrong or did not know
Verified against connection's
TableDetailResponseProvider::getResponseArray()andBigqueryDriverConfig::extendTableDefinitionResponse(), then confirmed live.1.
definitionis never null for an untyped table. The issue proposedtable.get("definition")on the grounds that it "returnsNonefor untyped tables, which is the expected shape". It does not — connection sets the key on every table-detail response, building one viacreateUntypedTableDefinitionResponseFromMetadata()when the table is untyped. So a null means the stack omitted the key, never "this table is untyped", and the human-mode guard keys off the layout fields rather than offdefinition's presence. Confirmed live on a Snowflake project:definitionpresent ({columns, primaryKeysNames}), human output byte-identical to before.2. The response carries more than the issue lists. With partitioning set it also returns
requirePartitionFilterandpartitions[]— one entry per physical partition, read fromINFORMATION_SCHEMA.PARTITIONS. That list is unbounded; a DAY-partitioned table with three years of history returns ~1,100 entries. Human mode therefore prints the count, never the contents.--jsonpasses the whole thing through: re-dropping an API field is the bug this PR fixes, so it is not repeated one level down. The size caveat is documented ingotchas.mdand the workflow example usesjqto select keys rather than dumping the object.3. There is a wire shape that would crash the proposed render.
tests/test_storage_empty_definition.pyrecords a real support incident where the Storage API served"definition": []instead of an object and broke the Go CLI's strict decoder. Its docstring warns that a futurex.get("definition", {}).get(...)would reintroduce the bug — which is exactly the shape the issue proposed. Every access here isisinstance-guarded, with a test for it.What changed
Service —
definitionpasses through verbatim.Human mode — between the existing
Primary keyandLast importlines, and only when there is a layout:Time partitioning:/Range partitioning:rather than the issue'sPartitioning:— those are the labelsstorage create-tablealready prints, and this PR makes both commands share the formatters. Verifying a repartition means diffing "what create-table said it applied" against "what table-detail reads back"; that comparison is only trustworthy if both render the same layout the same way.JSON mode —
definitionverbatim,partitions[]included.GET /storage/table-detail/...onkbagent servepicks it up for free through the same service.Not changed:
storage tables. The list endpoint'sinclude=accepts nodefinitionvalue (TablesListRequest::ALLOWED_INCLUDEisattributes, buckets, bucketsWithoutSourceTables, columns, metadata, columnMetadata, sourceMetadata, sourceColumnMetadata), so a layout in the listing costs one detail request per table. The issue scoped that out and it was right to.Why this is a refactor and not a one-liner
commands/storage.pyandservices/storage_service.pyare both grandfathered over their file-size ceiling inscripts/file_size_baseline.jsonand may only shrink —loc-checkfails on a single added code line. Per CONTRIBUTING's "extract pure helpers into a sibling module" guidance:services/storage_service.pycommands/storage.pyservices/_table_detail.py(new, pure, client-free)commands/_storage_table_detail.py(new)Both baselines are re-recorded down. Only the two files this PR shrank are touched —
make loc-baselinewould also have ratchetedcommands/config.pyandcommands/lineage.py, which this PR never opened, and tightening those would create merge friction for in-flight work for no benefit here.Prompt budget (second commit)
keboola-expert.mdwas at 61 960 B against a 62 000 B ceiling — under 100 bytes free. At that margin the budget is a tripwire rather than a budget: this PR's one-line verify hint could not land without first finding an unrelated trim to pay for it. Raised to 70 000 B, still ~12% under the ~80 kB / ~20k-token reference the number came from. The standing guidance is restated in the constant's comment, not weakened:AGENT_CONTEXTremains the place for per-command detail, and splitting into per-domain specialists remains the answer to sustained growth.The three prose sites still said "60 KB", stale since v0.48.0 moved the ceiling to 62 000 B — so an author trimming to the documented figure trimmed ~2 kB more than CI required. They now quote the enforced number and point at the test.
Testing
tests/test_storage_table_definition.py(new, 12 tests, TDD — all watched failing first): passthrough for the BigQuery layout, for an untyped definition, for an absent key, and for the[]wire shape; human rendering of time and range partitioning, clustering, partition-filter and partition count; the negative case that a table without a layout prints nothing new; and JSON passthrough._test_table_listingasserts the key is present on any backend;_test_create_table_from_sourcenow asserts that after the BigQuery repartition + swap,table-detailon the production name reports the clusteringcreate-tableapplied. That is the issue's exact scenario, end to end.make checkgreen (lint, format, skill, version, changelog, error-codes, sentinel-guards, loc, 5,724 tests).make typecheckclean — the singlehatchlingdiagnostic is pre-existing onmainand unrelated.definitionpresent, human output unchanged.Doc surfaces
AGENT_CONTEXT(commands/context.py),CLAUDE.md,commands-reference.md,gotchas.md(taggedsince v0.88.0),storage-types-workflow.md(the repartition example already said "inspect withtable-detail" — a step that could not show the layout until now; it gains an explicit verify step),keboola-expert.md,docs/e2e-scenarios.md, the--helpdocstring, and the regeneratedSKILL.mdtable.