From ebed9667e05273dfdcb4d22e061686b745ebaa85 Mon Sep 17 00:00:00 2001 From: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com> Date: Tue, 30 Jun 2026 09:59:06 -0700 Subject: [PATCH] feat(managed): add add_managed_table for additive schema evolution Wraps the SDK add_database_table endpoint so callers can declare a new table on an existing managed database without recreating it. The table is added empty (declared-but-unloaded) and populated via load_managed_table. --- CHANGELOG.md | 4 ++++ hotdata_framework/client.py | 28 ++++++++++++++++++++++++++++ tests/test_client.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0816bef..8fe0627 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- `HotdataClient.add_managed_table(database, table, *, schema)` declares a new table on an existing managed database (wrapping the SDK `add_database_table` endpoint). This allows additive schema evolution without recreating the database. + ## [0.5.0] - 2026-06-28 diff --git a/hotdata_framework/client.py b/hotdata_framework/client.py index a789c49..fc954f4 100644 --- a/hotdata_framework/client.py +++ b/hotdata_framework/client.py @@ -14,6 +14,7 @@ from hotdata.api.results_api import ResultsApi from hotdata.api.uploads_api import UploadsApi from hotdata.exceptions import ApiException +from hotdata.models.add_managed_table_request import AddManagedTableRequest from hotdata.models.async_query_response import AsyncQueryResponse from hotdata.models.create_database_request import CreateDatabaseRequest from hotdata.models.database_default_schema_decl import DatabaseDefaultSchemaDecl @@ -300,6 +301,33 @@ def load_managed_table( full_name=f"{db.id}.{loaded.schema_name}.{loaded.table_name}", ) + def add_managed_table( + self, + database: str, + table: str, + *, + schema: str = DEFAULT_SCHEMA, + ) -> ManagedTable: + """Declare a new table on an existing managed database. + + The table is added empty (declared-but-unloaded); populate it with + :meth:`load_managed_table`. Use this to evolve a managed database's + schema after creation without recreating it. + """ + db = self.resolve_managed_database(database) + request = AddManagedTableRequest(name=table) + try: + self._databases_api().add_database_table(db.id, schema, request) + except ApiException as e: + raise RuntimeError(api_error_message(e)) from e + return ManagedTable( + full_name=f"{db.id}.{schema}.{table}", + schema=schema, + table=table, + synced=False, + last_sync=None, + ) + def delete_managed_table( self, database: str, diff --git a/tests/test_client.py b/tests/test_client.py index 441aa18..ea536ce 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -178,6 +178,35 @@ def information_schema(self, **kwargs): assert fake_api.kwargs["connection_id"] == "conn_explicit" +def test_add_managed_table_declares_table_on_existing_database(): + client = HotdataClient("k", "ws", host="https://api.hotdata.dev") + fake_db = SimpleNamespace(id="db_1", default_connection_id="conn") + + class FakeDatabasesApi: + def __init__(self): + self.calls: list[tuple[str, str, str]] = [] + + def add_database_table(self, database_id, var_schema, request): + self.calls.append((database_id, var_schema, request.name)) + return SimpleNamespace( + connection_id="conn", var_schema=var_schema, table=request.name + ) + + fake_api = FakeDatabasesApi() + with ( + patch.object(client, "resolve_managed_database", return_value=fake_db), + patch.object(client, "_databases_api", return_value=fake_api), + ): + result = client.add_managed_table("mydb", "orders", schema="public") + + assert fake_api.calls == [("db_1", "public", "orders")] + assert result.full_name == "db_1.public.orders" + assert result.schema == "public" + assert result.table == "orders" + assert result.synced is False + assert result.last_sync is None + + def test_list_recent_results_returns_normalized_summaries(): client = HotdataClient("k", "ws", host="https://api.hotdata.dev") listing = SimpleNamespace(