Skip to content

fix(storage): write column descriptions where UI and MCP read them (#624) - #631

Merged
padak merged 4 commits into
mainfrom
claude/issue-624-popis-e4f252
Aug 21, 2026
Merged

fix(storage): write column descriptions where UI and MCP read them (#624)#631
padak merged 4 commits into
mainfrom
claude/issue-624-popis-e4f252

Conversation

@padak

@padak padak commented Aug 21, 2026

Copy link
Copy Markdown
Member

Problem

storage describe-column / describe-batch stored column descriptions as flat KBC.column.{name}.description keys in table metadata — a convention nothing but kbagent itself ever read. Documented columns looked blank in the Keboola UI, were invisible to the MCP server's get_table_detail/search, and never reached Snowflake COMMENT / BigQuery column descriptions. The failure was silent in both directions because table-detail read back its own convention.

Analysis of keboola/ui and keboola/connection (details in the issue comment): the UI writes through PUT .../tables/{id}/definition and the backend mirrors that write into columnMetadata KBC.description — one native write is visible to the UI, the MCP server, and the warehouse. The metadata-only fix suggested in the issue would have left descriptions out of the warehouse and unprotected from Output Mapping overwrite (isDescriptionSystemManaged stays true).

Changes

Write pathdescribe-column / describe-batch now write via the native definition endpoint:

  • PUT /v2/storage/branch/{branch}/tables/{id}/definition with columns: [{name, description}] + isDescriptionSystemManaged: false (async; the CLI waits for the tableDefinitionUpdate storage job)
  • unknown column names fail fast before any write (behavior change: the flat write accepted anything)
  • remaining legacy flat entries on the touched table are auto-migrated as part of the same write

Read pathtable-detail resolves each column description with precedence:

  1. native definition.columns[].definition.description (aliases: sourceTable.definition)
  2. columnMetadata[col] KBC.description (aliases: sourceTable.columnMetadata, MCP-server parity)
  3. legacy flat KBC.column.*

plus an always-present legacy_column_descriptions list and a human-mode warning pointing at describe-migrate. Reads never write.

Migration — new storage describe-migrate --project ALIAS [--table-id ID ...] [--bucket-id ID] [--prune-orphans] [--dry-run] [--yes] [--branch ID]:

  • scope: explicit tables XOR one bucket XOR whole project; scan-then-confirm; per-table error accumulation
  • conflict (target already has a differing visible description) → skip + report, newer value wins
  • orphan (column renamed/deleted) → skip; deleted only with --prune-orphans
  • migrated flat entries are deleted so a description later cleared through the native path cannot be resurrected by the read fallback

Client (layer 3)update_table_definition + delete_table_metadata on KeboolaClient; set_table_metadata docstring corrected (the "no user-writable column-metadata endpoint" claim was wrong).

Structure — the Descriptions command panel moved to commands/_storage_describe.py and the service logic to services/_column_descriptions.py (file-size budgets; follows the _storage_snapshots.py precedent).

Docs — 0.88.0 + changelog; AGENT_CONTEXT, CLAUDE.md command list, commands-reference, gotchas (since v0.88.0), storage-describe-workflow rewritten, SKILL.md regenerated, keboola-expert one-line trigger (61 991 / 62 000 bytes).

Testing

  • make check green: 5738 passed, 12 skipped (lint, format, changelog, skill, sentinel-guard, loc checks included)
  • new unit coverage: client payload/branch/job-error tests; service migration rules (conflict / orphan / identical / user-wins / delete-failure); describe-migrate scoping, dry-run, error accumulation; table-detail precedence incl. alias fallbacks; CLI exit codes, confirm-abort, --deny-writes
  • E2E: _test_storage_describe extended with a native round-trip assertion and a seeded-legacy-key migrate cycle (requires E2E_API_TOKEN/E2E_URL to run)
  • not verified live against a real stack in this PR — the E2E harness run is the follow-up

Closes #624


Open in Devin Review

@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 2 potential issues.

Open in Devin Review

Comment thread src/keboola_agent_cli/commands/_storage_describe.py
Comment thread src/keboola_agent_cli/services/_column_descriptions.py
padak added a commit that referenced this pull request Aug 21, 2026
…olumn patch (#624)

Review follow-ups on PR #631.

- serve: add POST /storage/columns/{project}/describe-migrate, the 1:1
  mirror CONTRIBUTING.md requires (its three describe-* siblings already
  had routes, so external callers got a 404). No `yes` field -- a REST
  caller opting into the write IS the confirmation; `dry_run` is the
  shared preview. Two router tests cover scope/flag forwarding and the
  empty-body default.
- e2e: assert a sibling column keeps its description across a partial
  write. describe-batch sends `id` alone, so `name` (described two
  steps earlier) must survive; the PUT .../definition `columns` payload
  is a positive-only patch and nothing asserted that before.
- e2e: seed the legacy migration key on `value`, the one undescribed
  column -- the real pre-0.88.0 shape. Seeding it on `name` made the
  live run report `conflict` (correctly: the column already had a native
  description), which the old assertion mistook for a migration failure.
  The conflict rule now has its own explicit assertions.
@padak

padak commented Aug 21, 2026

Copy link
Copy Markdown
Member Author

Live verification (project 5946, Snowflake)

Ran the E2E suite against a real stack. The storage path is now verified end-to-end, not just mocked.

Full suite: 93 passed, 13 skipped, 9 failed — none of the failures are regressions from this PR:

Failures Cause Status
test_full_cli_e2e (1) This PR's own new E2E assertion seeded the legacy key on a column that already had a native description, so the service correctly returned conflict and the assertion misread it as a migration failure Fixed in 75aaaf7 — re-run passes
TestE2ENotificationSubscriptions (7) Pre-existing: those tests read the CLI envelope one level too high (data["subscriptions"] instead of data["data"][...]). Last touched by ae840ba (#615, 2026-08-19); this PR's diff touches 0 notification lines. The command itself is fine — verified live Out of scope, tracked separately
test_timeout_triggers_remote_kill_and_exits_seven (1) Timing race — job still processing when the test asserted terminated Flaky, unrelated

Re-run of test_full_cli_e2e after the fix: 1 passed (6:51). The migration cycle behaved exactly as designed against the live API:

describe-migrate --dry-run  -> tables_scanned 1, tables_migrated 0, migrated [<the legacy column>]
describe-migrate --yes      -> tables_scanned 1, tables_migrated 1
table-detail                -> description present, legacy_column_descriptions []
describe-migrate --yes      -> tables_migrated 0, migrated []          (idempotent no-op)
describe-migrate --yes      -> tables_migrated 0, migrated []          (conflict correctly skipped)

Positive-only column patch (the review question) was confirmed by a separate live probe: described three columns, re-described one, the other two kept their descriptions verbatim. Details in the resolved review thread.

Remaining unverified: nothing on the write path. The columnMetadata mirror that makes descriptions visible to the MCP server is backed by reading keboola/connection (TableDefinitionUpdateServiceStorageColumnDefinitionSynchronizer.syncColumnDescriptions / syncTypedColumnInforeplaceStorageColumnMetadata), not by an MCP round-trip from here.

@padak
padak force-pushed the claude/issue-624-popis-e4f252 branch from 75aaaf7 to 6864360 Compare August 21, 2026 22:24
padak added a commit that referenced this pull request Aug 21, 2026
…olumn patch (#624)

Review follow-ups on PR #631.

- serve: add POST /storage/columns/{project}/describe-migrate, the 1:1
  mirror CONTRIBUTING.md requires (its three describe-* siblings already
  had routes, so external callers got a 404). No `yes` field -- a REST
  caller opting into the write IS the confirmation; `dry_run` is the
  shared preview. Two router tests cover scope/flag forwarding and the
  empty-body default.
- e2e: assert a sibling column keeps its description across a partial
  write. describe-batch sends `id` alone, so `name` (described two
  steps earlier) must survive; the PUT .../definition `columns` payload
  is a positive-only patch and nothing asserted that before.
- e2e: seed the legacy migration key on `value`, the one undescribed
  column -- the real pre-0.88.0 shape. Seeding it on `name` made the
  live run report `conflict` (correctly: the column already had a native
  description), which the old assertion mistook for a migration failure.
  The conflict rule now has its own explicit assertions.
@padak

padak commented Aug 21, 2026

Copy link
Copy Markdown
Member Author

Rebased onto main (#629 landed first)

fix(storage): surface the table definition on table-detail (#621) (#629) extracted get_table_detail into services/_table_detail.py, which is exactly the function this PR changes — a semantic conflict, not just a textual one. Resolved by rebase (4 commits, linear on top of #629):

No version bump. Both branches had independently claimed 0.88.0; since 0.88.0 is unreleased, this PR's changelog notes now sit under the existing 0.88.0 key alongside #621's, and pyproject.toml is byte-identical to main. The release covering all three in-flight PRs is cut separately.

make check after the rebase: 5752 passed, 12 skipped — includes #629's own tests/test_storage_table_definition.py.

UI verification is also done: all three column descriptions render on the Schema tab of a live table (project 5946), confirming an independent consumer sees what the CLI wrote.

padak added 4 commits August 22, 2026 00:27
…ient methods (#624)

PUT /v2/storage/branch/{branch}/tables/{id}/definition (async, waits for
the tableDefinitionUpdate storage job; production uses the literal branch
ref "default", matching the web UI client) and the synchronous metadata
entry DELETE. Fixes the set_table_metadata docstring: the flat
KBC.column.{name}.description convention is legacy, invisible to the
Keboola UI and MCP server, kept only for migration and read fallback.
…tail fallback (#624)

describe-column/describe-batch write through the native definition
endpoint with isDescriptionSystemManaged=false, fail fast on unknown
columns, and auto-migrate remaining legacy flat entries on the touched
table (conflict -> skip, orphan -> skip, migrated entries deleted so a
later clear cannot be resurrected by the read fallback). New
storage describe-migrate command converts legacy entries in bulk
(dry-run-then-confirm, per-table error accumulation, --prune-orphans).
table-detail resolves descriptions native definition -> columnMetadata
KBC.description (alias source fallback) -> legacy flat key and reports
legacy_column_descriptions with a human-mode warning; reads never write.
Descriptions panel moved to commands/_storage_describe.py and the
service logic to services/_column_descriptions.py to hold the file-size
budgets (snapshot-module precedent).
Version 0.88.0 + changelog; AGENT_CONTEXT, CLAUDE.md command list,
commands-reference, gotchas (since v0.88.0), storage-describe-workflow,
regenerated SKILL.md decision table, keboola-expert one-line trigger
(61991/62000 bytes).
…olumn patch (#624)

Review follow-ups on PR #631.

- serve: add POST /storage/columns/{project}/describe-migrate, the 1:1
  mirror CONTRIBUTING.md requires (its three describe-* siblings already
  had routes, so external callers got a 404). No `yes` field -- a REST
  caller opting into the write IS the confirmation; `dry_run` is the
  shared preview. Two router tests cover scope/flag forwarding and the
  empty-body default.
- e2e: assert a sibling column keeps its description across a partial
  write. describe-batch sends `id` alone, so `name` (described two
  steps earlier) must survive; the PUT .../definition `columns` payload
  is a positive-only patch and nothing asserted that before.
- e2e: seed the legacy migration key on `value`, the one undescribed
  column -- the real pre-0.88.0 shape. Seeding it on `name` made the
  live run report `conflict` (correctly: the column already had a native
  description), which the old assertion mistook for a migration failure.
  The conflict rule now has its own explicit assertions.
@padak
padak force-pushed the claude/issue-624-popis-e4f252 branch from 6864360 to 64c915d Compare August 21, 2026 22:30
@padak
padak merged commit 4ca123b into main Aug 21, 2026
4 checks passed
@padak
padak deleted the claude/issue-624-popis-e4f252 branch August 21, 2026 22:40
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.

storage describe-column writes column descriptions where the MCP server never reads them

1 participant