Summary
set_table_metadata returns TABLE_NOT_FOUND for any table created via the execute tool on the default ephemeral primary — including CREATE TABLE ... AS SELECT and plain CREATE TABLE. The table exists and is fully queryable (describe/query/sample all see it), but no _table_catalog stub row is created, so the metadata write is rejected.
This contradicts both the set_table_metadata tool description ("Requires an existing catalog entry — load the table first (load_file / load_data / execute CREATE TABLE) so the stub row is created automatically") and the error message the tool itself returns ("the catalog is refreshed automatically on those paths").
Verified on v0.7.0 (hyper_rust_api_version: 0.7.0.r5fa668df).
Reproduction
# 1. Create a table via execute on the default (ephemeral) primary
execute(sql: ["CREATE TABLE demo AS SELECT 1 AS id, 'alpha' AS name"])
→ { "statements": 1, "affected_rows": 0, ... } # success
# 2. Confirm the table exists and is queryable
describe(table: "demo")
→ { "tables": [{ "name": "demo", "columns": [...], "row_count": 1 }] } # present
# 3. Try to stamp metadata
set_table_metadata(table: "demo", purpose: "…")
→ ERROR TABLE_NOT_FOUND:
"No catalog entry for table 'demo'. Load the table first
(load_file / load_data / execute CREATE TABLE) or create it and
re-run; the catalog is refreshed automatically on those paths."
Plain CREATE TABLE demo (id INT) (no AS SELECT) reproduces identically — this is not CTAS-specific.
Root cause
The execute handler updates the catalog through a reconcile pass rather than an explicit stub, and that reconcile only enumerates the persistent database's tables — never the ephemeral primary's.
execute → on structural DDL calls after_execute_catalog_update(engine, target_db) (hyperdb-mcp/src/server.rs:2648-2650).
after_execute_catalog_update calls reconcile_in(engine, None) (hyperdb-mcp/src/server.rs:1458). target_db = None because the create targeted the default primary.
reconcile_in discovers tables to stub via user_tables_in (hyperdb-mcp/src/table_catalog.rs:816-835), which queries "<persistent>".pg_catalog.pg_tables WHERE schemaname = 'public'.
- A table created on the ephemeral primary lives in the ephemeral
.hyper file, not the persistent attachment, so it never appears in that pg_tables scan — reconcile never stubs it.
By contrast, the ingest tools (load_file / load_data / load_files) call after_ingest_catalog_update → upsert_stub_in with the explicit table name (hyperdb-mcp/src/server.rs:1425), which stubs correctly regardless of which DB the table lives in. That's why the documented "load the table first" path works but the "execute CREATE TABLE" path doesn't.
Confirming evidence
Creating the same table in the persistent DB stubs correctly and lets metadata through:
execute(sql: ["CREATE TABLE demo AS SELECT 1 AS id"], database: "persistent")
set_table_metadata(table: "demo", purpose: "…", database: "persistent")
→ { "table_name": "demo", "purpose": "…", "load_tool": "unknown", "row_count": 1, ... } # success
load_tool: "unknown" confirms the row was created by the reconcile pass (not an ingest stub) — reconcile does fire and does work, it just only ever looks at the persistent catalog's DB.
Impact
The primary casualty is the common analytics pattern of building a derived table with CREATE TABLE analysis AS SELECT … on the default workspace and then stamping it with provenance (source_url, purpose, refresh recipe) for the next session — exactly what set_table_metadata is for. Today that requires either:
- targeting
database: "persistent" explicitly (works, but changes durability semantics and isn't discoverable from the error), or
- round-tripping the table out to Parquet via
export and back in via load_file purely to trigger the ingest-path stub.
Suggested fixes (in rough order of preference)
- Reconcile the DB that was actually written. When
execute targets the ephemeral primary (target_db = None), reconcile_in should enumerate the ephemeral primary's pg_tables, not (only) the persistent attachment's. The stub rows can still be stored in the persistent catalog (matching where ephemeral-primary ingest stubs already live per the table_catalog module docs) — it's the table-discovery scan that needs to target the ephemeral DB.
- Parse created table names from the executed DDL and
upsert_stub_in them explicitly, mirroring the ingest path, instead of relying on a pg_tables diff. More robust than reconcile for the create case and avoids the extra round-trips.
- At minimum, fix the misleading docs + error message if the behavior is intended: the
set_table_metadata description and the TABLE_NOT_FOUND suggestion should not claim "execute CREATE TABLE … refreshed automatically" for the default-primary case, and should point users at database: "persistent" or the load path.
Environment
hyper_rust_api_version: 0.7.0.r5fa668df-dirty-20260712T003150Z
- Engine mode:
daemon
- Default (ephemeral) primary + persistent attachment both present (
has_persistent: true)
Summary
set_table_metadatareturnsTABLE_NOT_FOUNDfor any table created via theexecutetool on the default ephemeral primary — includingCREATE TABLE ... AS SELECTand plainCREATE TABLE. The table exists and is fully queryable (describe/query/sampleall see it), but no_table_catalogstub row is created, so the metadata write is rejected.This contradicts both the
set_table_metadatatool description ("Requires an existing catalog entry — load the table first (load_file / load_data / execute CREATE TABLE) so the stub row is created automatically") and the error message the tool itself returns ("the catalog is refreshed automatically on those paths").Verified on v0.7.0 (
hyper_rust_api_version: 0.7.0.r5fa668df).Reproduction
Plain
CREATE TABLE demo (id INT)(noAS SELECT) reproduces identically — this is not CTAS-specific.Root cause
The
executehandler updates the catalog through a reconcile pass rather than an explicit stub, and that reconcile only enumerates the persistent database's tables — never the ephemeral primary's.execute→ on structural DDL callsafter_execute_catalog_update(engine, target_db)(hyperdb-mcp/src/server.rs:2648-2650).after_execute_catalog_updatecallsreconcile_in(engine, None)(hyperdb-mcp/src/server.rs:1458).target_db = Nonebecause the create targeted the default primary.reconcile_indiscovers tables to stub viauser_tables_in(hyperdb-mcp/src/table_catalog.rs:816-835), which queries"<persistent>".pg_catalog.pg_tables WHERE schemaname = 'public'..hyperfile, not the persistent attachment, so it never appears in thatpg_tablesscan — reconcile never stubs it.By contrast, the ingest tools (
load_file/load_data/load_files) callafter_ingest_catalog_update→upsert_stub_inwith the explicit table name (hyperdb-mcp/src/server.rs:1425), which stubs correctly regardless of which DB the table lives in. That's why the documented "load the table first" path works but the "execute CREATE TABLE" path doesn't.Confirming evidence
Creating the same table in the persistent DB stubs correctly and lets metadata through:
load_tool: "unknown"confirms the row was created by the reconcile pass (not an ingest stub) — reconcile does fire and does work, it just only ever looks at the persistent catalog's DB.Impact
The primary casualty is the common analytics pattern of building a derived table with
CREATE TABLE analysis AS SELECT …on the default workspace and then stamping it with provenance (source_url,purpose, refresh recipe) for the next session — exactly whatset_table_metadatais for. Today that requires either:database: "persistent"explicitly (works, but changes durability semantics and isn't discoverable from the error), orexportand back in viaload_filepurely to trigger the ingest-path stub.Suggested fixes (in rough order of preference)
executetargets the ephemeral primary (target_db = None),reconcile_inshould enumerate the ephemeral primary'spg_tables, not (only) the persistent attachment's. The stub rows can still be stored in the persistent catalog (matching where ephemeral-primary ingest stubs already live per thetable_catalogmodule docs) — it's the table-discovery scan that needs to target the ephemeral DB.upsert_stub_inthem explicitly, mirroring the ingest path, instead of relying on apg_tablesdiff. More robust than reconcile for the create case and avoids the extra round-trips.set_table_metadatadescription and theTABLE_NOT_FOUNDsuggestion should not claim "execute CREATE TABLE … refreshed automatically" for the default-primary case, and should point users atdatabase: "persistent"or the load path.Environment
hyper_rust_api_version:0.7.0.r5fa668df-dirty-20260712T003150Zdaemonhas_persistent: true)