From f90d5b46fec9f11c8f63309177a440645883c811 Mon Sep 17 00:00:00 2001 From: Petr Date: Sat, 22 Aug 2026 22:04:28 +0200 Subject: [PATCH 1/2] feat(storage): show column descriptions in `table-detail` human output 0.88.0 (#624) moved the column-description write to the native `PUT .../tables/{id}/definition` endpoint precisely so the text would show up everywhere: the Keboola UI, the MCP server's `get_tables`, and the Snowflake COMMENT / BigQuery column description. `--json` carried it too, on `column_details[].description`. kbagent's own human-mode Columns table was the one surface still showing nothing -- so `storage describe-column` followed by `storage table-detail`, the obvious way to check your own work, was the one way that did not work. Add a `Description` column to that Rich table: * Shown only when at least one column actually has a description, matching `render_table_layout`'s existing "no data, no chrome" rule -- an undocumented table's output is byte-identical to before. * Wrapped and width-capped, never truncated. Verifying what you just wrote is the point, so an ellipsis would defeat it; `overflow="fold"` breaks even a long unbroken token rather than hiding its tail, and `max_width=60` stops a wide terminal from stretching the cell across the screen. * Escaped. Descriptions are user-authored free text, and Rich would otherwise read `[note]` as markup (or raise on an unbalanced tag). `--json` is untouched. All three description tiers the service already resolves (native definition, `columnMetadata` KBC.description, legacy flat key) render, and the legacy-key migration warning still prints below. Tests cover the render, the empty-column guard, markup escaping, wrapping at 80 columns, and the width cap at 200; each was verified to fail against the matching mutation of the production code. Note that the 80-column case has to override conftest's autouse `COLUMNS=200` fixture explicitly -- without that it silently asserts nothing. --- .claude-plugin/marketplace.json | 2 +- CLAUDE.md | 6 +- plugins/kbagent/.claude-plugin/plugin.json | 2 +- .../kbagent/references/commands-reference.md | 2 +- .../skills/kbagent/references/gotchas.md | 5 +- pyproject.toml | 2 +- src/keboola_agent_cli/changelog.py | 14 +++ .../commands/_storage_table_detail.py | 29 +++++- tests/test_storage_table_definition.py | 98 +++++++++++++++++++ uv.lock | 2 +- 10 files changed, 153 insertions(+), 9 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index e744f3ac..11e2e5fa 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "plugins": [ { "name": "kbagent", - "version": "0.88.0", + "version": "0.88.1", "source": "./plugins/kbagent", "description": "AI-friendly interface to Keboola Connection projects — explore configs, jobs, lineage, sync configs as files, manage dev branches, and debug SQL in workspaces", "category": "development" diff --git a/CLAUDE.md b/CLAUDE.md index 1742d612..18ed24a0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -440,7 +440,11 @@ kbagent storage table-detail --project NAME --table-id ID [--branch ID] # ID is unchanged either way, and `create-table` only echoes the layout you REQUESTED (its # --if-not-exists skip path nulls the layout keys outright). Human mode adds Time # partitioning / Range partitioning / Clustering / Partition filter required / Partitions -# (a COUNT) and prints nothing new when there is no layout; --json passes `definition` +# (a COUNT) and prints nothing new when there is no layout. Since 0.88.1 the human +# Columns table also carries a Description column, populated from +# `column_details[].description` and shown only when some column has one -- the one +# surface #624 left blank, so `describe-column` then `table-detail` now verifies +# itself (long text wraps, never truncates). --json passes `definition` # through verbatim, including the unbounded `partitions[]` (one entry per physical # partition from INFORMATION_SCHEMA.PARTITIONS). `definition` is present on EVERY response # -- untyped tables get one too -- so null means the stack omitted the key, NOT "untyped". diff --git a/plugins/kbagent/.claude-plugin/plugin.json b/plugins/kbagent/.claude-plugin/plugin.json index 4c98bbc3..5c460a80 100644 --- a/plugins/kbagent/.claude-plugin/plugin.json +++ b/plugins/kbagent/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "kbagent", - "version": "0.88.0", + "version": "0.88.1", "description": "AI-friendly interface to Keboola Connection projects — explore configs, jobs, lineage, sync configs as files, manage dev branches, and debug SQL in workspaces", "author": { "name": "Keboola", diff --git a/plugins/kbagent/skills/kbagent/references/commands-reference.md b/plugins/kbagent/skills/kbagent/references/commands-reference.md index d420c3ec..769b8861 100644 --- a/plugins/kbagent/skills/kbagent/references/commands-reference.md +++ b/plugins/kbagent/skills/kbagent/references/commands-reference.md @@ -159,7 +159,7 @@ Requires a **super-admin** Manage API token (same kind as `org setup`). Same def - `storage buckets [--project NAME] [--branch ID]` -- list buckets with sharing/linked info (branch-aware) - `storage bucket-detail --project NAME --bucket-id ID [--branch ID]` -- bucket detail with backend-native direct-access paths (branch-aware). Output adapts to backend: Snowflake -> `snowflake_database` / `snowflake_schema` / per-table `snowflake_path` quoted with `"..."`. BigQuery -> `bigquery_dataset` (and `bigquery_project` when surfaced via API `databaseName`) / per-table `bigquery_path` quoted with backticks. Always-present backend-agnostic keys: `sql_dialect` (`"snowflake"` / `"bigquery"`) and per-table `sql_path` -- prefer these in agent code instead of branching on backend yourself - `storage tables [--project NAME ...] [--bucket-id ID] [--branch ID] [--include-usage]` -- list tables across all connected projects in parallel (multi-project by default, same as `storage buckets`); repeat `--project` to target a subset; `--bucket-id` is applied independently per project (missing buckets become per-project errors); `--branch` requires exactly one `--project`. `--include-usage` (0.88.0+) adds `used_by` per table: the configurations naming it in their **storage input/output mapping** only -- a table id inside a transformation's SQL is NOT a reference. Costs one extra component listing per project (not per table), which is the slow call in a big project; unreadable components degrade to an empty `used_by` -- `storage table-detail --project NAME --table-id ID [--branch ID]` -- table detail with columns, types, primary key, row count (branch-aware). Since 0.88.0 (#621) also returns the raw Storage API `definition`: on BigQuery that carries `timePartitioning` / `rangePartitioning` / `clustering` / `requirePartitionFilter` / `partitions[]`, and it is the only way to verify a repartition landed. Human mode prints the layout and a partition COUNT; `--json` passes `definition` through verbatim. Present on every response (untyped tables too), so `null` means the stack omitted the key, not "untyped". Since 0.88.0 (#624) it also RESOLVES column descriptions written by anyone -- the UI, a component, or kbagent -- into `column_details[].description`, with precedence native definition -> `columnMetadata` `KBC.description` -> legacy flat `KBC.column.*` (an alias table falls back to the source table's `columnMetadata`, matching the MCP server). The response always carries `legacy_column_descriptions`, naming the columns still backed by the pre-0.88.0 convention -- human mode warns and points at `storage describe-migrate`. Reading never writes, so it is safe under a read-only token or `--deny-writes` +- `storage table-detail --project NAME --table-id ID [--branch ID]` -- table detail with columns, types, primary key, row count (branch-aware). Since 0.88.0 (#621) also returns the raw Storage API `definition`: on BigQuery that carries `timePartitioning` / `rangePartitioning` / `clustering` / `requirePartitionFilter` / `partitions[]`, and it is the only way to verify a repartition landed. Human mode prints the layout and a partition COUNT; `--json` passes `definition` through verbatim. Present on every response (untyped tables too), so `null` means the stack omitted the key, not "untyped". Since 0.88.0 (#624) it also RESOLVES column descriptions written by anyone -- the UI, a component, or kbagent -- into `column_details[].description`, with precedence native definition -> `columnMetadata` `KBC.description` -> legacy flat `KBC.column.*` (an alias table falls back to the source table's `columnMetadata`, matching the MCP server). The response always carries `legacy_column_descriptions`, naming the columns still backed by the pre-0.88.0 convention -- human mode warns and points at `storage describe-migrate`. Since 0.88.1 human mode also renders a `Description` column in the Columns table (only when at least one column has one; long text wraps rather than truncating) -- on 0.88.0 descriptions were visible in `--json` only. Reading never writes, so it is safe under a read-only token or `--deny-writes` - `storage create-bucket --project NAME --stage STAGE --name NAME [--description D] [--backend B] [--branch ID]` -- create bucket (branch-aware). With `--branch ID` on a project lacking the `storage-branches` feature (legacy fake-branch), response carries `legacy_branch_storage: true` and human mode prints a warning -- the runner will create a parallel `out.c--*` bucket at job time. See `storage-types-workflow.md` - `storage create-table --project NAME --bucket-id ID --name NAME [--column col:TYPE[(length)] ...] [--primary-key COL] [--not-null COL ...] [--default NAME=VALUE ...] [--source-table-id ID] [--source-branch-id N] [--time-partitioning-type DAY|HOUR|MONTH|YEAR] [--time-partitioning-field COL] [--time-partitioning-expiration-ms MS] [--range-partitioning-field COL --range-partitioning-start S --range-partitioning-end E --range-partitioning-interval I] [--clustering-field COL ...] [--branch ID] [--if-not-exists]` -- create typed table. Base types `STRING/INTEGER/NUMERIC/FLOAT/BOOLEAN/DATE/TIMESTAMP` plus native backend types with length (`VARCHAR(40)`, `NUMBER(18,2)`, `TIMESTAMP_TZ`, `VARIANT`, etc.) -- type/length validation delegated to the Storage API. `--not-null` marks a column `nullable=false`; `--default NAME=VALUE` sets a DEFAULT expression (booleans must be lowercase `true`/`false`). In a dev branch, the target bucket is auto-materialized if it has not yet been written to there -- response surfaces this via `auto_created_bucket: bool`. On legacy fake-branch projects (no `storage-branches` feature), `legacy_branch_storage: true` flags that the runner will use a separate `out.c--*` bucket at job time. `--if-not-exists` (0.47.0+) turns a duplicate-display-name failure into `action: skipped` when the table really exists at the expected id (safe for parallel workers). Since 0.47.1 the skipped envelope reports the EXISTING table's actual `columns`/`primary_key`/`name`, mirrors the request under `requested_columns`/`requested_primary_key`, and sets `schema_drift: true` when they diverge. **`--source-table-id` (0.66.0+, BigQuery only)** copies an existing table's data into the requested partition/clustering layout instead of building from `--column` (schema derived from source -> `--column`/`--not-null`/`--default` forbidden; the two are mutually exclusive). This is the supported way to repartition a populated BigQuery table -- then promote it with `storage swap-tables`. Partition/clustering flags (`--time-partitioning-*`, `--range-partitioning-*`, `--clustering-field`) also work on a plain `--column` create (BigQuery only); time vs range partitioning are mutually exclusive and range bounds are strings. When any source/partition/clustering flag is used, a one-call backend pre-flight rejects non-BigQuery projects (exit 2) before the create. See `storage-types-workflow.md` - `storage upload-table --project NAME --table-id ID --file PATH [--incremental] [--branch ID]` -- upload CSV (branch-aware) diff --git a/plugins/kbagent/skills/kbagent/references/gotchas.md b/plugins/kbagent/skills/kbagent/references/gotchas.md index 22a88932..23bc1d8a 100644 --- a/plugins/kbagent/skills/kbagent/references/gotchas.md +++ b/plugins/kbagent/skills/kbagent/references/gotchas.md @@ -2687,7 +2687,10 @@ write descriptive metadata onto storage objects. Three behaviors are easy to mis native definition -> `columnMetadata` `KBC.description` -> legacy flat key, always returns `legacy_column_descriptions`, and warns in human mode when legacy keys remain (it never writes -- safe under a read-only token or - `--deny-writes`). Unknown column names now fail fast BEFORE any write; the old + `--deny-writes`). Its human-mode Columns table gained a `Description` column + *(since v0.88.1)* -- on 0.88.0 the descriptions were readable ONLY through + `--json` `column_details[].description`, so a blank-looking terminal table on + that version does not mean the write failed. Unknown column names now fail fast BEFORE any write; the old flat write accepted typos silently. Table and bucket descriptions are unaffected: still `KBC.description` (provider=user) on the object's metadata. - **`describe-batch` is partial-failure-tolerant.** Item-level errors are diff --git a/pyproject.toml b/pyproject.toml index f833d98e..a7f294bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "keboola-cli" -version = "0.88.0" +version = "0.88.1" description = "AI-friendly CLI for managing Keboola projects" readme = "README.md" requires-python = ">=3.12" diff --git a/src/keboola_agent_cli/changelog.py b/src/keboola_agent_cli/changelog.py index 505abbd1..eb8ee96f 100644 --- a/src/keboola_agent_cli/changelog.py +++ b/src/keboola_agent_cli/changelog.py @@ -24,6 +24,20 @@ # Ordered newest-first. Each value is a list of brief one-line descriptions. CHANGELOG: dict[str, list[str]] = { + "0.88.1": [ + "UX: `kbagent storage table-detail` now shows column descriptions in its " + "human-mode Columns table. 0.88.0 (#624) made a description visible in the " + "Keboola UI, in the MCP server's `get_tables` and in the warehouse's own " + "`COMMENT`, and `--json` has carried it on `column_details[].description` all " + "along -- kbagent's own terminal output was the one surface still showing " + "nothing, so `storage describe-column` followed by `storage table-detail` (the " + "obvious way to check your own work) was the one way that did not work. The " + "column appears only when at least one column actually has a description, the " + "same rule the partitioning block follows, so an undocumented table's output is " + "byte-identical to before. Long text wraps rather than truncating -- verifying " + "what you just wrote is the point -- and the cell is width-capped so a wide " + "terminal does not stretch it across the screen. `--json` is unchanged.", + ], "0.88.0": [ "Fix (#624): column descriptions are now written where the Keboola UI and the " "MCP server actually read them. Until 0.87.0 `storage describe-column` / " diff --git a/src/keboola_agent_cli/commands/_storage_table_detail.py b/src/keboola_agent_cli/commands/_storage_table_detail.py index 9ac8946f..e9f89749 100644 --- a/src/keboola_agent_cli/commands/_storage_table_detail.py +++ b/src/keboola_agent_cli/commands/_storage_table_detail.py @@ -14,6 +14,12 @@ tests/test_storage_empty_definition.py -- it broke the Go CLI's decoder), so the value is type-checked before any `.get()` reaches it. +The columns table shows a `Description` column only when some column has one. +Since 0.88.0 (#624) a description written by `storage describe-column` is visible +in the Keboola UI, in the MCP server and in the warehouse's own COMMENT, and +`--json` has always carried it -- this view was the last surface still blank, so +`describe-column` followed by `table-detail` never confirmed its own write. + `format_time_partitioning` / `format_range_partitioning` are shared with `storage create-table`'s result output on purpose: verifying a repartition means diffing what create-table said it applied against what table-detail reads back, @@ -98,16 +104,35 @@ def render_table_detail(formatter: OutputFormatter, result: dict[str, Any]) -> N if result["column_details"]: formatter.console.print() + # Only grow the column when something fills it -- same rule the layout + # block above follows. `description` is set on a `column_details` entry + # only when one of the three tiers resolved (see + # services/_table_detail._column_details), so this is a real "nothing to + # show" test, not a test for empty strings. + show_description = any(col.get("description") for col in result["column_details"]) + table = Table(title="Columns") table.add_column("Name", style="bold cyan") table.add_column("Type", style="dim") table.add_column("Nullable", style="dim") + if show_description: + # Capped and wrapped, never truncated. A description is what the + # user came here to verify after `storage describe-column`, so an + # ellipsis would defeat the point -- `overflow="fold"` breaks even a + # long unbroken token instead of hiding its tail. `max_width` stops + # a wide terminal from stretching the cell across the screen; on a + # narrow one Rich shrinks it further and the table still fits. + table.add_column("Description", max_width=60, overflow="fold") for col in result["column_details"]: - table.add_row( + cells = [ col["name"], col.get("type", ""), "yes" if col.get("nullable") else "", - ) + ] + if show_description: + # User-authored free text: `[note]` is a note, not Rich markup. + cells.append(escape(col.get("description") or "")) + table.add_row(*cells) formatter.console.print(table) diff --git a/tests/test_storage_table_definition.py b/tests/test_storage_table_definition.py index 670bcfaa..8d4cc6a8 100644 --- a/tests/test_storage_table_definition.py +++ b/tests/test_storage_table_definition.py @@ -33,6 +33,7 @@ from typing import Any from unittest.mock import MagicMock, patch +import pytest from typer.testing import CliRunner from keboola_agent_cli.cli import app @@ -251,3 +252,100 @@ def test_definition_passes_through_json_mode(self, tmp_path: Path) -> None: payload = json.loads(result.output) data = payload.get("data", payload) assert data["definition"] == BIGQUERY_LAYOUT + + +# A native `definition` carrying per-column descriptions -- the tier the Keboola +# UI writes and shows, and the one `storage describe-column` has written since +# 0.88.0 (#624). It rides in on the same `definition` object as the layout, so +# the fixture above already covers it. +DESCRIBED_LAYOUT: dict[str, Any] = { + "primaryKeysNames": ["id"], + "columns": [ + {"name": "id", "definition": {"type": "INTEGER"}}, + { + "name": "created_at", + "definition": {"type": "TIMESTAMP", "description": "Row creation timestamp, UTC."}, + }, + { + "name": "tenant_id", + "definition": {"type": "STRING", "description": "Owning tenant."}, + }, + ], +} + + +class TestColumnDescriptions: + """The Columns table must show what `storage describe-column` just wrote. + + 0.88.0 (#624) moved the write to the native table-definition endpoint so the + description surfaces in the Keboola UI, the MCP server's `get_tables` and the + warehouse's own COMMENT. `column_details[].description` has carried it in + `--json` all along. Human mode was the one place still showing nothing, so + the obvious way to check your own work -- describe, then `table-detail` -- + was the one way that did not work. + """ + + def test_description_is_rendered_in_the_columns_table(self, tmp_path: Path) -> None: + result = _invoke_detail(tmp_path, DESCRIBED_LAYOUT) + + assert result.exit_code == 0, result.output + assert "Description" in result.output + assert "Row creation timestamp, UTC." in result.output + + def test_undescribed_table_grows_no_empty_column(self, tmp_path: Path) -> None: + """Same guard style as the layout block: no data, no chrome.""" + result = _invoke_detail(tmp_path, UNTYPED_DEFINITION) + + assert result.exit_code == 0, result.output + assert "Description" not in result.output + + def test_markup_in_a_description_is_shown_literally(self, tmp_path: Path) -> None: + """Descriptions are user text; Rich would eat (or choke on) square brackets.""" + definition = { + "columns": [{"name": "id", "definition": {"description": "see [note] and [/x]"}}] + } + result = _invoke_detail(tmp_path, definition) + + assert result.exit_code == 0, result.output + assert "[note]" in result.output + + def test_long_description_wraps_inside_an_80_column_terminal( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + """A wrapped cell keeps the table readable; an unwrapped one destroys it. + + The `_deterministic_console_width` fixture in conftest pins every test to + a 200-column console, so the narrow case only gets exercised if a test + asks for it by name. + """ + monkeypatch.setenv("COLUMNS", "80") + long_text = ( + "Timestamp the row was created, in UTC, as written by the ingestion " + "job; backfilled rows carry the original source timestamp instead." + ) + definition = {"columns": [{"name": "created_at", "definition": {"description": long_text}}]} + result = _invoke_detail(tmp_path, definition) + + assert result.exit_code == 0, result.output + widest = max((len(line) for line in result.output.splitlines()), default=0) + assert widest <= 80, f"widest line was {widest} chars" + # Wrapped, not truncated -- the last word must survive. + assert "instead." in result.output + + def test_wide_terminal_does_not_stretch_the_description_cell(self, tmp_path: Path) -> None: + """`max_width` is load-bearing: without it the cell fills the whole console. + + Runs at the conftest default of 200 columns. A single-line description + would otherwise pull the table out to the full terminal width and put + yards of whitespace between the column name and its text. + """ + long_text = ( + "Timestamp the row was created, in UTC, as written by the ingestion " + "job; backfilled rows carry the original source timestamp instead." + ) + definition = {"columns": [{"name": "created_at", "definition": {"description": long_text}}]} + result = _invoke_detail(tmp_path, definition) + + assert result.exit_code == 0, result.output + widest = max((len(line) for line in result.output.splitlines()), default=0) + assert widest < 120, f"table sprawled to {widest} chars on a 200-column console" diff --git a/uv.lock b/uv.lock index 6f789a5f..ec13e577 100644 --- a/uv.lock +++ b/uv.lock @@ -581,7 +581,7 @@ wheels = [ [[package]] name = "keboola-cli" -version = "0.88.0" +version = "0.88.1" source = { editable = "." } dependencies = [ { name = "croniter" }, From 4953b4563a7b66e5a05dabef7f35afbb15e3908e Mon Sep 17 00:00:00 2001 From: Petr Date: Sat, 22 Aug 2026 22:29:32 +0200 Subject: [PATCH 2/2] chore: drop the version bump, renumber at release time v0.88.0 shipped earlier today, so this change belongs to whatever version comes next -- but that number is the release step's to choose, not this PR's. Reverts pyproject/plugin.json/marketplace.json/uv.lock to 0.88.0 and removes the 0.88.1 changelog key (an entry with no matching release fails `make changelog-check`). The doc surfaces no longer name 0.88.1 either. `make version-gate-check` rejects any `(since vX.Y.Z)` marker that is not a CHANGELOG key -- an agent reading a gate for a version that never ships refuses a command the user actually has. The three notes now anchor on 0.88.0, a real release, and say what that version does NOT do: "Human mode's Columns table shows a Description column. On 0.88.0 it did NOT -- there the descriptions were readable only through --json, so a blank-looking terminal table on that version does not mean the write failed." That is true regardless of the number this eventually ships under. gotchas.md carries an explicit release-step reminder to tag it once known. --- .claude-plugin/marketplace.json | 2 +- CLAUDE.md | 4 ++-- plugins/kbagent/.claude-plugin/plugin.json | 2 +- .../kbagent/references/commands-reference.md | 2 +- .../kbagent/skills/kbagent/references/gotchas.md | 9 +++++---- pyproject.toml | 2 +- src/keboola_agent_cli/changelog.py | 14 -------------- uv.lock | 2 +- 8 files changed, 12 insertions(+), 25 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 11e2e5fa..e744f3ac 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "plugins": [ { "name": "kbagent", - "version": "0.88.1", + "version": "0.88.0", "source": "./plugins/kbagent", "description": "AI-friendly interface to Keboola Connection projects — explore configs, jobs, lineage, sync configs as files, manage dev branches, and debug SQL in workspaces", "category": "development" diff --git a/CLAUDE.md b/CLAUDE.md index 18ed24a0..43df74f2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -440,8 +440,8 @@ kbagent storage table-detail --project NAME --table-id ID [--branch ID] # ID is unchanged either way, and `create-table` only echoes the layout you REQUESTED (its # --if-not-exists skip path nulls the layout keys outright). Human mode adds Time # partitioning / Range partitioning / Clustering / Partition filter required / Partitions -# (a COUNT) and prints nothing new when there is no layout. Since 0.88.1 the human -# Columns table also carries a Description column, populated from +# (a COUNT) and prints nothing new when there is no layout. The human +# Columns table also carries a Description column (NOT on 0.88.0), populated from # `column_details[].description` and shown only when some column has one -- the one # surface #624 left blank, so `describe-column` then `table-detail` now verifies # itself (long text wraps, never truncates). --json passes `definition` diff --git a/plugins/kbagent/.claude-plugin/plugin.json b/plugins/kbagent/.claude-plugin/plugin.json index 5c460a80..4c98bbc3 100644 --- a/plugins/kbagent/.claude-plugin/plugin.json +++ b/plugins/kbagent/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "kbagent", - "version": "0.88.1", + "version": "0.88.0", "description": "AI-friendly interface to Keboola Connection projects — explore configs, jobs, lineage, sync configs as files, manage dev branches, and debug SQL in workspaces", "author": { "name": "Keboola", diff --git a/plugins/kbagent/skills/kbagent/references/commands-reference.md b/plugins/kbagent/skills/kbagent/references/commands-reference.md index 769b8861..7baac186 100644 --- a/plugins/kbagent/skills/kbagent/references/commands-reference.md +++ b/plugins/kbagent/skills/kbagent/references/commands-reference.md @@ -159,7 +159,7 @@ Requires a **super-admin** Manage API token (same kind as `org setup`). Same def - `storage buckets [--project NAME] [--branch ID]` -- list buckets with sharing/linked info (branch-aware) - `storage bucket-detail --project NAME --bucket-id ID [--branch ID]` -- bucket detail with backend-native direct-access paths (branch-aware). Output adapts to backend: Snowflake -> `snowflake_database` / `snowflake_schema` / per-table `snowflake_path` quoted with `"..."`. BigQuery -> `bigquery_dataset` (and `bigquery_project` when surfaced via API `databaseName`) / per-table `bigquery_path` quoted with backticks. Always-present backend-agnostic keys: `sql_dialect` (`"snowflake"` / `"bigquery"`) and per-table `sql_path` -- prefer these in agent code instead of branching on backend yourself - `storage tables [--project NAME ...] [--bucket-id ID] [--branch ID] [--include-usage]` -- list tables across all connected projects in parallel (multi-project by default, same as `storage buckets`); repeat `--project` to target a subset; `--bucket-id` is applied independently per project (missing buckets become per-project errors); `--branch` requires exactly one `--project`. `--include-usage` (0.88.0+) adds `used_by` per table: the configurations naming it in their **storage input/output mapping** only -- a table id inside a transformation's SQL is NOT a reference. Costs one extra component listing per project (not per table), which is the slow call in a big project; unreadable components degrade to an empty `used_by` -- `storage table-detail --project NAME --table-id ID [--branch ID]` -- table detail with columns, types, primary key, row count (branch-aware). Since 0.88.0 (#621) also returns the raw Storage API `definition`: on BigQuery that carries `timePartitioning` / `rangePartitioning` / `clustering` / `requirePartitionFilter` / `partitions[]`, and it is the only way to verify a repartition landed. Human mode prints the layout and a partition COUNT; `--json` passes `definition` through verbatim. Present on every response (untyped tables too), so `null` means the stack omitted the key, not "untyped". Since 0.88.0 (#624) it also RESOLVES column descriptions written by anyone -- the UI, a component, or kbagent -- into `column_details[].description`, with precedence native definition -> `columnMetadata` `KBC.description` -> legacy flat `KBC.column.*` (an alias table falls back to the source table's `columnMetadata`, matching the MCP server). The response always carries `legacy_column_descriptions`, naming the columns still backed by the pre-0.88.0 convention -- human mode warns and points at `storage describe-migrate`. Since 0.88.1 human mode also renders a `Description` column in the Columns table (only when at least one column has one; long text wraps rather than truncating) -- on 0.88.0 descriptions were visible in `--json` only. Reading never writes, so it is safe under a read-only token or `--deny-writes` +- `storage table-detail --project NAME --table-id ID [--branch ID]` -- table detail with columns, types, primary key, row count (branch-aware). Since 0.88.0 (#621) also returns the raw Storage API `definition`: on BigQuery that carries `timePartitioning` / `rangePartitioning` / `clustering` / `requirePartitionFilter` / `partitions[]`, and it is the only way to verify a repartition landed. Human mode prints the layout and a partition COUNT; `--json` passes `definition` through verbatim. Present on every response (untyped tables too), so `null` means the stack omitted the key, not "untyped". Since 0.88.0 (#624) it also RESOLVES column descriptions written by anyone -- the UI, a component, or kbagent -- into `column_details[].description`, with precedence native definition -> `columnMetadata` `KBC.description` -> legacy flat `KBC.column.*` (an alias table falls back to the source table's `columnMetadata`, matching the MCP server). The response always carries `legacy_column_descriptions`, naming the columns still backed by the pre-0.88.0 convention -- human mode warns and points at `storage describe-migrate`. Human mode also renders a `Description` column in the Columns table (only when at least one column has one; long text wraps rather than truncating); on 0.88.0 descriptions were visible in `--json` only. Reading never writes, so it is safe under a read-only token or `--deny-writes` - `storage create-bucket --project NAME --stage STAGE --name NAME [--description D] [--backend B] [--branch ID]` -- create bucket (branch-aware). With `--branch ID` on a project lacking the `storage-branches` feature (legacy fake-branch), response carries `legacy_branch_storage: true` and human mode prints a warning -- the runner will create a parallel `out.c--*` bucket at job time. See `storage-types-workflow.md` - `storage create-table --project NAME --bucket-id ID --name NAME [--column col:TYPE[(length)] ...] [--primary-key COL] [--not-null COL ...] [--default NAME=VALUE ...] [--source-table-id ID] [--source-branch-id N] [--time-partitioning-type DAY|HOUR|MONTH|YEAR] [--time-partitioning-field COL] [--time-partitioning-expiration-ms MS] [--range-partitioning-field COL --range-partitioning-start S --range-partitioning-end E --range-partitioning-interval I] [--clustering-field COL ...] [--branch ID] [--if-not-exists]` -- create typed table. Base types `STRING/INTEGER/NUMERIC/FLOAT/BOOLEAN/DATE/TIMESTAMP` plus native backend types with length (`VARCHAR(40)`, `NUMBER(18,2)`, `TIMESTAMP_TZ`, `VARIANT`, etc.) -- type/length validation delegated to the Storage API. `--not-null` marks a column `nullable=false`; `--default NAME=VALUE` sets a DEFAULT expression (booleans must be lowercase `true`/`false`). In a dev branch, the target bucket is auto-materialized if it has not yet been written to there -- response surfaces this via `auto_created_bucket: bool`. On legacy fake-branch projects (no `storage-branches` feature), `legacy_branch_storage: true` flags that the runner will use a separate `out.c--*` bucket at job time. `--if-not-exists` (0.47.0+) turns a duplicate-display-name failure into `action: skipped` when the table really exists at the expected id (safe for parallel workers). Since 0.47.1 the skipped envelope reports the EXISTING table's actual `columns`/`primary_key`/`name`, mirrors the request under `requested_columns`/`requested_primary_key`, and sets `schema_drift: true` when they diverge. **`--source-table-id` (0.66.0+, BigQuery only)** copies an existing table's data into the requested partition/clustering layout instead of building from `--column` (schema derived from source -> `--column`/`--not-null`/`--default` forbidden; the two are mutually exclusive). This is the supported way to repartition a populated BigQuery table -- then promote it with `storage swap-tables`. Partition/clustering flags (`--time-partitioning-*`, `--range-partitioning-*`, `--clustering-field`) also work on a plain `--column` create (BigQuery only); time vs range partitioning are mutually exclusive and range bounds are strings. When any source/partition/clustering flag is used, a one-call backend pre-flight rejects non-BigQuery projects (exit 2) before the create. See `storage-types-workflow.md` - `storage upload-table --project NAME --table-id ID --file PATH [--incremental] [--branch ID]` -- upload CSV (branch-aware) diff --git a/plugins/kbagent/skills/kbagent/references/gotchas.md b/plugins/kbagent/skills/kbagent/references/gotchas.md index 23bc1d8a..599b33f7 100644 --- a/plugins/kbagent/skills/kbagent/references/gotchas.md +++ b/plugins/kbagent/skills/kbagent/references/gotchas.md @@ -2687,10 +2687,11 @@ write descriptive metadata onto storage objects. Three behaviors are easy to mis native definition -> `columnMetadata` `KBC.description` -> legacy flat key, always returns `legacy_column_descriptions`, and warns in human mode when legacy keys remain (it never writes -- safe under a read-only token or - `--deny-writes`). Its human-mode Columns table gained a `Description` column - *(since v0.88.1)* -- on 0.88.0 the descriptions were readable ONLY through - `--json` `column_details[].description`, so a blank-looking terminal table on - that version does not mean the write failed. Unknown column names now fail fast BEFORE any write; the old + `--deny-writes`). Its human-mode Columns table shows a `Description` + column. On 0.88.0 it did NOT -- there the descriptions were readable only + through `--json` `column_details[].description`, so a blank-looking terminal + table on that version does not mean the write failed. (Release step: once this + ships, tag this sentence `(since vX.Y.Z)` with the version that carried it.) Unknown column names now fail fast BEFORE any write; the old flat write accepted typos silently. Table and bucket descriptions are unaffected: still `KBC.description` (provider=user) on the object's metadata. - **`describe-batch` is partial-failure-tolerant.** Item-level errors are diff --git a/pyproject.toml b/pyproject.toml index a7f294bb..f833d98e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "keboola-cli" -version = "0.88.1" +version = "0.88.0" description = "AI-friendly CLI for managing Keboola projects" readme = "README.md" requires-python = ">=3.12" diff --git a/src/keboola_agent_cli/changelog.py b/src/keboola_agent_cli/changelog.py index eb8ee96f..505abbd1 100644 --- a/src/keboola_agent_cli/changelog.py +++ b/src/keboola_agent_cli/changelog.py @@ -24,20 +24,6 @@ # Ordered newest-first. Each value is a list of brief one-line descriptions. CHANGELOG: dict[str, list[str]] = { - "0.88.1": [ - "UX: `kbagent storage table-detail` now shows column descriptions in its " - "human-mode Columns table. 0.88.0 (#624) made a description visible in the " - "Keboola UI, in the MCP server's `get_tables` and in the warehouse's own " - "`COMMENT`, and `--json` has carried it on `column_details[].description` all " - "along -- kbagent's own terminal output was the one surface still showing " - "nothing, so `storage describe-column` followed by `storage table-detail` (the " - "obvious way to check your own work) was the one way that did not work. The " - "column appears only when at least one column actually has a description, the " - "same rule the partitioning block follows, so an undocumented table's output is " - "byte-identical to before. Long text wraps rather than truncating -- verifying " - "what you just wrote is the point -- and the cell is width-capped so a wide " - "terminal does not stretch it across the screen. `--json` is unchanged.", - ], "0.88.0": [ "Fix (#624): column descriptions are now written where the Keboola UI and the " "MCP server actually read them. Until 0.87.0 `storage describe-column` / " diff --git a/uv.lock b/uv.lock index ec13e577..6f789a5f 100644 --- a/uv.lock +++ b/uv.lock @@ -581,7 +581,7 @@ wheels = [ [[package]] name = "keboola-cli" -version = "0.88.1" +version = "0.88.0" source = { editable = "." } dependencies = [ { name = "croniter" },