Skip to content

chore: reconcile shared catalog against ephemeral primary + persistent (#195)#196

Merged
StefanSteiner merged 4 commits into
tableau:mainfrom
StefanSteiner:chore/195-execute-catalog-stub
Jul 12, 2026
Merged

chore: reconcile shared catalog against ephemeral primary + persistent (#195)#196
StefanSteiner merged 4 commits into
tableau:mainfrom
StefanSteiner:chore/195-execute-catalog-stub

Conversation

@StefanSteiner

Copy link
Copy Markdown
Contributor

Summary

Fixes #195. set_table_metadata returned TABLE_NOT_FOUND for a table created via the execute tool on the default (ephemeral) primary — and, more seriously, an already-catalogued ephemeral-primary table (registered via the load_* ingest path with user-set purpose/source_url) had its catalog row silently deleted by any later unrelated structural execute.

Root cause: the persistent _table_catalog is the single shared store of catalog rows for tables that physically live in either the ephemeral primary DB (where unqualified DDL lands) or the persistent attachment. But reconcile_in(engine, None) — run after every structural execute and at bootstrap — computed its "live table set" from the persistent attachment only. So ephemeral-primary tables were (a) never stubbed and (b) classified "disappeared" and reaped.

Fix

Confined to hyperdb-mcp/src/table_catalog.rs. When reconciling the shared persistent catalog (target_db is None or "persistent"), enumerate the union of persistent-attachment tables ∪ ephemeral-primary tables, mapping each live table to the DB its row_count should be computed against. Catalog rows are still stored in the persistent catalog; only the live-set enumeration and COUNT(*) became origin-aware.

  • reconcile_live_tables(engine, target_db) -> BTreeMap<name, alias> — ephemeral primary inserted first, persistent second, so on a name collision the durable persistent table wins the count (authoritative for a name-keyed catalog).
  • public_tables_in / row_count_qualified — per-alias helpers that replace the deleted user_tables_in / row_count_of_in.
  • The rename heuristic's target pool is restricted to new tables in the catalog's own database — persistent for the shared catalog (so a disappeared persistent row can never be false-renamed onto an unrelated new ephemeral scratch table by coincident row count), and the user alias for a per-attached-DB reconcile (so execute-driven renames inside a user DB still preserve prose).

Known limitation (documented, not fixed here)

The catalog is name-keyed with no origin-DB column, and each MCP process owns a distinct ephemeral primary while sharing one persistent catalog. This fix is correct for the realistic single-MCP-process-per-persistent-workspace deployment; under multiple concurrent processes against one shared persistent catalog, a peer's ephemeral-table rows can still be reaped. A principled fix needs an origin_db column and is deferred.

Why chore: and not fix:

This is a real (arguably data-loss) bug fix. It is intentionally typed chore: — and this PR is titled chore: so release-please does not cut a v0.7.1 patch — because other fixes are being held to land before v0.7.1 is released. Consequence: this change will not appear as a fix: line in the eventual v0.7.1 CHANGELOG. Filed here so the reasoning is on record for future archaeology.

Tests

Six regression tests in hyperdb-mcp/tests/table_catalog_tests.rs:

  • set_metadata_finds_execute_created_ephemeral_primary_table — the set_table_metadata returns TABLE_NOT_FOUND for tables created via execute on the ephemeral primary #195 stub-gap (was RED: TABLE_NOT_FOUND).
  • reconcile_preserves_ephemeral_primary_metadata_across_unrelated_ddl — the metadata-loss trap (was RED: row deleted).
  • reconcile_does_not_false_rename_persistent_row_onto_new_ephemeral_table — cross-DB false-rename guard (was RED: ephemeral table unstubbed / would inherit prose under a naive union).
  • reconcile_does_not_delete_persistent_rows_when_reconciling_shared_catalog — persistent rows survive reconcile of the shared catalog.
  • reconcile_reaps_dropped_ephemeral_primary_table — a genuinely-dropped ephemeral table's row is still reaped (no over-correction).
  • reconcile_persistent_wins_name_collision_across_dbs — persistent table is authoritative for the count on a name collision.

Plus one in hyperdb-mcp/tests/per_tool_database_tests.rs:

  • reconcile_in_user_db_preserves_metadata_on_rename — a rename inside a user-attached DB preserves prose (was RED: the persistent-only rename-target restriction emptied the candidate pool for user DBs, deleting + re-stubbing the row).

Verification

  • cargo fmt --all --check — clean.
  • cargo clippy --workspace --all-targets --all-features -- -D warnings — clean.
  • cargo test --workspace --exclude hyperdb-api-node --exclude hyperdb-bootstrap — all green.

- CLAUDE.md: thin auto-load shim that re-exports AGENTS.md as the
  cross-tool source of truth for Claude Code.
- AGENTS.md: document the docs/superpowers/ convention — design specs and
  implementation plans kept in-repo and committed alongside the code that
  implements them, so the rationale is reviewable with the change.
- docs/superpowers/plans/2026-07-09-kv-store-m2-mcp.md: the KV Store
  Milestone 2 (MCP tool surface) implementation plan artifact.
Cover the ephemeral-primary catalog gap and its metadata-loss trap:
- set_metadata_finds_execute_created_ephemeral_primary_table (stub-gap, tableau#195)
- reconcile_preserves_ephemeral_primary_metadata_across_unrelated_ddl (delete-trap)
- reconcile_does_not_false_rename_persistent_row_onto_new_ephemeral_table (C1 guard)
plus three guards that pass today (persistent rows survive, dropped ephemeral
row still reaped, persistent wins a cross-DB name collision).

Tests 1, 2, and the false-rename guard fail on current code because
reconcile_in enumerates only the persistent attachment; the union-reconcile
fix in the following commit turns them green.
tableau#195)

The persistent _table_catalog is the single store for catalog rows of
tables that physically live in EITHER the ephemeral primary (unqualified
DDL, e.g. an `execute` CREATE on the default workspace) OR the persistent
attachment. reconcile_in enumerated its live-table set from the persistent
attachment only, so an execute-created primary table was never stubbed
(set_table_metadata → TABLE_NOT_FOUND), and any pre-existing catalog row
for a primary table was reaped as "disappeared" by a later unrelated
structural execute — silently losing user-set purpose/source_url.

Enumerate the UNION of persistent + ephemeral-primary public tables when
reconciling the shared catalog, mapping each live table to its origin
alias for row counting. Storage stays in the persistent catalog; only the
live-set enumeration and COUNT(*) became origin-aware.

- reconcile_live_tables(engine, target_db) -> BTreeMap<name, alias>:
  ephemeral primary inserted first, persistent second, so persistent wins
  a name collision (authoritative for a name-keyed catalog).
- public_tables_in / row_count_qualified: per-alias helpers replacing
  user_tables_in / row_count_of_in (both deleted; no other callers).
- Rename heuristic TARGET pool restricted to persistent-origin new tables,
  so a disappeared persistent row can never be false-renamed onto a new
  ephemeral scratch table by coincident row count.

Known limitation (documented on reconcile_in): the catalog has no origin
column and each MCP process owns a distinct ephemeral primary, so a peer
process's ephemeral rows can still be reaped under concurrent multi-process
use against one shared persistent catalog. Out of scope here.

Type is `chore:` deliberately: releasing this in a v0.7.1 bump is being
held so other fixes land first; the fix ships without a CHANGELOG line.
The tableau#195 shared-catalog union restricted the reconcile rename-target pool
to a literal PERSISTENT_ALIAS. For a per-DB reconcile (target_db =
Some(user_alias)) every live table maps to the user alias, so the pool was
always empty and the rename heuristic never fired — a rename inside a
user-attached DB deleted + re-stubbed the row, losing purpose/source_url/
notes. Same silent prose-loss class as tableau#195, on a different path.

Generalize the pool to the catalog's OWN resolved alias: persistent for the
shared catalog (byte-for-byte the prior behavior, C1 guard intact — ephemeral
tables still excluded), the user alias for a per-DB reconcile. Count each
candidate against live[t] rather than a hard-coded persistent alias.

Adds reconcile_in_user_db_preserves_metadata_on_rename (RED before, GREEN
after). Caught by the final-sweep deep review.
@StefanSteiner StefanSteiner merged commit 9975209 into tableau:main Jul 12, 2026
12 checks passed
@StefanSteiner StefanSteiner deleted the chore/195-execute-catalog-stub branch July 12, 2026 05: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.

set_table_metadata returns TABLE_NOT_FOUND for tables created via execute on the ephemeral primary

1 participant