Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"plugins": [
{
"name": "kbagent",
"version": "0.43.3",
"version": "0.43.4",
"source": "./plugins/kbagent",
"description": "AI-friendly interface to Keboola Connection projects — explore configs, jobs, lineage, call MCP tools, manage dev branches, and debug SQL in workspaces",
"category": "development"
Expand Down
2 changes: 1 addition & 1 deletion plugins/kbagent/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "kbagent",
"version": "0.43.3",
"version": "0.43.4",
"description": "AI-friendly interface to Keboola Connection projects — explore configs, jobs, lineage, call MCP tools, manage dev branches, and debug SQL in workspaces",
"author": {
"name": "Keboola",
Expand Down
4 changes: 2 additions & 2 deletions plugins/kbagent/skills/kbagent/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ When working inside a git repository or project directory, run `kbagent init` (o
| Validate a semantic-layer model | `kbagent semantic-layer validate --project PROJECT` |
| List all semantic-layer models in a project | `kbagent semantic-layer model list --project PROJECT` |
| Create a new semantic-layer model | `kbagent semantic-layer model create --project PROJECT --name NAME` |
| Delete a semantic-layer model. | `kbagent semantic-layer model delete --project PROJECT --model MODEL` |
| Delete a semantic-layer model and cascade-delete its children | `kbagent semantic-layer model delete --project PROJECT --model MODEL` |
| Add a metric to a semantic-layer model | `kbagent semantic-layer add metric --project PROJECT --name NAME --sql SQL --dataset DATASET` |
| Add a dataset (FQN derived from tableId) | `kbagent semantic-layer add dataset --project PROJECT --name NAME --table-id TABLE-ID` |
| Add a relationship between two datasets | `kbagent semantic-layer add relationship --project PROJECT --name NAME --from FROM- --to TO --on ON` |
Expand All @@ -278,7 +278,7 @@ When working inside a git repository or project directory, run `kbagent init` (o
| Validate a semantic-layer model | `kbagent sl validate --project PROJECT` |
| List all semantic-layer models in a project | `kbagent sl model list --project PROJECT` |
| Create a new semantic-layer model | `kbagent sl model create --project PROJECT --name NAME` |
| Delete a semantic-layer model. | `kbagent sl model delete --project PROJECT --model MODEL` |
| Delete a semantic-layer model and cascade-delete its children | `kbagent sl model delete --project PROJECT --model MODEL` |
| Add a metric to a semantic-layer model | `kbagent sl add metric --project PROJECT --name NAME --sql SQL --dataset DATASET` |
| Add a dataset (FQN derived from tableId) | `kbagent sl add dataset --project PROJECT --name NAME --table-id TABLE-ID` |
| Add a relationship between two datasets | `kbagent sl add relationship --project PROJECT --name NAME --from FROM- --to TO --on ON` |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ Manage Keboola metastore models -- datasets, metrics, relationships, constraints

- `semantic-layer model list --project P` -- list all models in a project. Output: `{models: [{id, name, sql_dialect, description}, ...]}`. Use to disambiguate when `--model` is required and the project has more than one model.
- `semantic-layer model create --project P --name N [--description D] [--sql-dialect Snowflake]` -- create a new model. `--sql-dialect` defaults to `Snowflake`. Returns the new model UUID; subsequent commands accept either name or UUID via `--model`.
- `semantic-layer model delete --project P --model M [--yes]` -- delete a model. Fails if the model has children (datasets, metrics, etc.) -- the Storage API rejects with 500 / "model not empty". Confirmation prompt unless `--yes`.
- `semantic-layer model delete --project P --model M [--yes]` -- delete a model **and cascade-delete every child entity** (datasets, metrics, relationships, constraints, glossary terms) in `reversed(PUSH_ORDER)` (constraints first, datasets last) before the parent. Confirmation prompt unless `--yes`. **Cascade is unconditional in 0.43.4+** -- before that release the call only DELETEd the parent, silently leaking children pointing at the dead `modelUUID` and breaking subsequent `build` / `import` retries with HTTP 422 name collisions (closes #306). On any child-DELETE failure the parent is **preserved** and the response carries `details.cascade = {attempted, deleted, failures: [{type, id, name, error}], parent_deleted: False, model_uuid}` so the user can re-run after fixing the underlying error. Happy-path envelope adds `cascade.deleted` per-type counts. Legacy `orphaned_children` top-level key kept for back-compat (same shape, meaning flipped from "leaked" to "cascaded") but **deprecated -- removal scheduled for a future minor release**; new callers should read `cascade.deleted` instead. See [gotchas.md](gotchas.md) for the meaning-flip + deprecation note.
- `semantic-layer show --project P [--model M] [--type T]` -- show a model's entities. `--type` filters to `dataset | metric | relationship | constraint | glossary`. Without `--type` prints a per-type count summary. `--model` is optional when the project has exactly one model.
- `semantic-layer validate --project P [--model M] [--deep]` -- structural validation. Basic mode runs local checks: duplicate names, dangling rel/metric refs, SUM-on-PCT (warning), constraint orphans (metrics in `metrics[]` that no longer exist), severity-suffix mismatches between API `severity` and the 4-band name suffix. `--deep` adds parallel Snowflake column-existence probes via the in-process StorageService: phantom dataset fields, phantom column refs in metric SQL, AGG-on-STRING errors. Response: `{valid: bool, deep: bool, errors: [{type, item, detail}], warnings: [...]}`.
- `semantic-layer export --project P [--model M] [--output PATH]` -- snapshot the model to a self-describing JSON file (default `./sl_export_{model_name}_{YYYYMMDD_HHMMSS}.json`). Schema-versioned for round-trip via `import` / `diff`.
Expand Down
48 changes: 48 additions & 0 deletions plugins/kbagent/skills/kbagent/references/gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,54 @@ limit, transient 5xx), the detail call still succeeds and
`storage_workspace_id` is set to `null` -- the annotation is UX, not a
contract.

## `semantic-layer model delete` cascade-deletes children (since v0.43.4)

`kbagent semantic-layer model delete --project P --model M` used to DELETE
only the parent `semantic-model` row, leaving every dataset / metric /
relationship / constraint / glossary term on the wire pointing at the
now-dead `modelUUID` (issue #306). The orphans were invisible until the next
`build` or `import` hit HTTP 422 `semantic-dataset with name 'X' already
exists in the target model` on a same-named dataset — names are unique
**per project**, not per model.

Since this release the command walks `reversed(PUSH_ORDER)` (constraints →
glossary → relationships → metrics → datasets) and deletes each child via
`client.delete_item` before the parent. `--yes` still skips the
confirmation prompt; the prompt text now warns explicitly that all children
will be deleted.

**Partial failure semantics (matches `push_built_model` rollback envelope):**

- Every child DELETE is wrapped individually; sibling failures do **not**
abort the cascade.
- If ANY child fails, the parent is **preserved** and a `KeboolaApiError`
is raised with `details.cascade = {attempted, deleted, failures: [{type,
id, name, error}], parent_deleted: False, model_uuid}`.
- Re-run `kbagent semantic-layer model delete --project P --model <uuid>`
after fixing the underlying error to finish the cascade.

**Response envelope changes:**

- New top-level `cascade` block on success: `{attempted, deleted: {datasets,
metrics, relationships, glossary, constraints}, failures: [], parent_deleted}`.
- Legacy `orphaned_children` top-level key kept for back-compat with the
shape unchanged, but its **meaning** flips from 'leaked count' to
'cascaded count'. Happy-path JSON consumers always saw zeros on this key
before — the only way to populate it was the bug.

**Deprecation:** `orphaned_children` is deprecated as of v0.43.4 and
scheduled for **removal in a future minor release** (not before v0.44.0).
Read `cascade.deleted` instead — it carries the same per-type counts plus
the explicit `attempted` / `parent_deleted` / `failures` fields that
disambiguate happy-path from partial-failure responses. JSON callers
should migrate now; the field name is the only thing that changes.

**Implication for AI agents / scripts:** Scripts that called `model delete`
and then assumed they had to teardown children manually can drop that
follow-up. Scripts that scraped `orphaned_children` to detect the bug now
see the same zeros they always wanted — but should switch to
`cascade.deleted` ahead of v0.42.0.

## Web UI `Kai Chat` is gone — replaced by `Local AI` (since v0.41.9)

The web UI dashboard tile / left-nav entry previously labelled **Kai
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "keboola-agent-cli"
version = "0.43.3"
version = "0.43.4"
description = "AI-friendly CLI for managing Keboola projects"
readme = "README.md"
requires-python = ">=3.12"
Expand Down
6 changes: 6 additions & 0 deletions src/keboola_agent_cli/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@

# Ordered newest-first. Each value is a list of brief one-line descriptions.
CHANGELOG: dict[str, list[str]] = {
"0.43.4": [
"Fix: `kbagent semantic-layer model delete` now cascade-deletes every child entity (dataset / metric / relationship / constraint / glossary term) before deleting the parent model (closes #306). Previously the call only DELETEd the parent row in the metastore; the children stayed on the wire pointing at the now-dead `modelUUID`. Because dataset names are unique **per project** (not per model), the next `kbagent semantic-layer build` or `import` that emitted a dataset of the same name (e.g. `addresses`) hit HTTP 422 `semantic-dataset with name 'X' already exists in the target model` with no UI / CLI escape -- the workaround was hand-enumerating `semantic-*` repository endpoints. The bug was always there but only became visible when the `http_base` error-body parser landed in 0.43.0 and started surfacing real metastore messages instead of bare `API error 422: 422`. `services/semantic_layer_service.py:delete_model` (and the extracted `_semantic_layer_cascade.py` helper) walk `reversed(PUSH_ORDER)` (constraints → glossary → relationships → metrics → datasets) and call `client.delete_item` per child before the parent. Partial-failure semantics match the `push_built_model` rollback envelope from PR #295: every child DELETE is wrapped in its own try/except so a sibling failure does not abort the cascade; if ANY child failed, the parent is **preserved** and a `KeboolaApiError` is raised with `details.cascade = {attempted, deleted, failures: [{type, id, name, error}], parent_deleted: False, model_uuid}` plus a recovery hint pointing at `kbagent semantic-layer model delete --project P --model <uuid>` to re-run. Success envelope adds a new `cascade.deleted` block with per-type counts. CLI prompt text updated from `If the model has datasets/metrics/etc. the API will refuse.` (stale -- the API did not refuse, it leaked) to `This cascade-deletes ALL child entities... This is irreversible.` Success renderer appends `+ cascaded N child(ren)` so operators see what was removed. The server router (`server/routers/semantic_layer.py:223`) inherits the new semantics with no code change.",
"Deprecation: legacy `orphaned_children` top-level key on `semantic-layer model delete` JSON responses is deprecated as of this release; its shape is unchanged but its meaning flips from 'leaked count' to 'cascaded count'. Happy-path consumers always saw zeros on this key before this fix anyway -- the only way to populate it was the bug. New callers should read `cascade.deleted` (same per-type counts) plus the explicit `cascade.attempted` / `cascade.parent_deleted` / `cascade.failures` fields that disambiguate happy-path from partial-failure responses. Field removal is scheduled for a future minor release (not before v0.44.0); migration window is the gap between 0.43.4 and that release.",
"Plugin docs: `plugins/kbagent/skills/kbagent/references/gotchas.md` adds a `(since v0.43.4)` entry noting `semantic-layer model delete` is cascade-by-default and that partial-failure responses carry `details.cascade.failures` for re-run targeting; `commands-reference.md` cascade row updated with the new envelope shape; `keboola-expert.md` tool-selection matrix flags the cascade behavior and the `orphaned_children` deprecation. No CLI surface change (flags / arg names unchanged), so `CLAUDE.md ## All CLI Commands` is unchanged.",
"Tests: 282 lines of E2E coverage in `test_e2e.py::test_semantic_layer_delete_cascade` exercise the full regression loop (create model A + children → cascade-delete via CLI → assert envelope → create model B with same names → must succeed). 23 service-layer tests in `test_semantic_layer_service.py::TestDeleteModel` (happy path, partial failure, name conflict regression). 12 CLI tests in `test_semantic_layer_cli.py::TestModelDeleteCascade`. The cascade logic is extracted to `services/_semantic_layer_cascade.py` (146 LOC) keeping `semantic_layer_service.py` under the file-size budget. Total suite: 3341 passed, 26 skipped.",
],
"0.43.3": [
"New: `kbagent update --beta` (alternatively `KBAGENT_INCLUDE_PRERELEASE=1` per-shell env var) opts into pre-release versions. Default behaviour is unchanged -- the startup auto-update hook hits GitHub's `/releases/latest` endpoint, which is defined as the latest non-prerelease, non-draft release; betas marked `--prerelease` are invisible. With `--beta`, the version fetcher switches to `/releases` (plural) and picks the highest PEP 440 version including pre-releases (e.g. `0.44.0b1` beats `0.43.3`). The install command additionally propagates `--prerelease=allow` (uv) / `--pre` (pip) so the resolver accepts PEP 440 pre-release tags that it would otherwise refuse by default, AND appends `@v<version>` to the git+ install URL so uv installs the exact commit pointed to by the tag rather than the default branch (this matters when beta tags live on a feature branch, not main -- without `@v<version>` uv would always install the latest main commit, even though `_fetch_kbagent_latest_prerelease` advertised a different version). `kbagent version --beta` mirrors the same lookup for inspection. No `release_channel: beta` persistent config setting -- each opt-in is ad-hoc and explicit so a beta install is never a forgotten preference. CONTRIBUTING.md gets a new 'Releasing a beta' workflow section documenting the PEP 440 + `gh release create --prerelease` convention. 12 unit tests in `test_version_service.py` (default uses /releases/latest, prerelease uses /releases with PEP 440 sort, skips drafts, falls back to stable, ignores invalid tags, HTTP failure returns None, `build_kbagent_upgrade_command` propagates `--prerelease=allow` for uv + `--pre` for pip, prerelease+target_version appends `@v<version>` to git URL, stable install URL is unchanged when target_version not provided).",
],
Expand Down
24 changes: 15 additions & 9 deletions src/keboola_agent_cli/commands/semantic_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ def model_delete(
model: str = typer.Option(..., "--model", help="Model name or UUID"),
yes: bool = typer.Option(False, "--yes", "-y", help="Skip the confirmation prompt"),
) -> None:
"""Delete a semantic-layer model. Fails if the model still has children."""
"""Delete a semantic-layer model and cascade-delete its children."""
if should_hint(ctx):
emit_hint(ctx, "semantic-layer.model.delete", project=project, model=model)
return
Expand All @@ -174,7 +174,9 @@ def model_delete(
and not formatter.json_mode
and not typer.confirm(
f"Delete model '{model}' in project '{project}'? "
"If the model has datasets/metrics/etc. the API will refuse."
"This cascade-deletes ALL child entities (datasets, metrics, "
"relationships, constraints, glossary terms) belonging to the model. "
"This is irreversible."
)
):
formatter.console.print("Aborted.")
Expand All @@ -186,13 +188,17 @@ def model_delete(
alias=project,
model_name_or_uuid=model,
)
formatter.output(
result,
lambda c, d: c.print(
f"[bold green]Deleted model[/bold green] [cyan]{d['deleted']['name']}[/cyan] "
f"([dim]{d['deleted']['id']}[/dim])"
),
)

def _render(console: Console, data: dict) -> None:
cascaded = sum(data.get("cascade", {}).get("deleted", {}).values())
suffix = f" + cascaded {cascaded} child(ren)" if cascaded else ""
console.print(
f"[bold green]Deleted model[/bold green] "
f"[cyan]{data['deleted']['name']}[/cyan] "
f"([dim]{data['deleted']['id']}[/dim]){suffix}"
)

formatter.output(result, _render)


# ---------------------------------------------------------------------------
Expand Down
Loading
Loading