From ad0485797550456541bb6a9d8cdc5bb8c612355f Mon Sep 17 00:00:00 2001 From: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com> Date: Tue, 19 May 2026 19:42:38 -0700 Subject: [PATCH 1/5] fix: always include schema in managed config even when tables list is empty build_managed_config was returning {} when tables=[], silently dropping the schema name. A call like create_database("db", schema="analytics") would send config: {} to the API and the schema declaration was lost. Remove the early return so the schema block is always emitted. Add a regression test for the empty-tables path. Co-Authored-By: Claude Sonnet 4.6 --- src/ibis_hotdata/managed.py | 2 -- tests/test_hotdata_backend.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/ibis_hotdata/managed.py b/src/ibis_hotdata/managed.py index 4f47d24..7357724 100644 --- a/src/ibis_hotdata/managed.py +++ b/src/ibis_hotdata/managed.py @@ -9,8 +9,6 @@ def build_managed_config(schema: str, tables: list[str]) -> dict[str, Any]: - if not tables: - return {} return { "schemas": [ { diff --git a/tests/test_hotdata_backend.py b/tests/test_hotdata_backend.py index 6f4456e..197a8b7 100644 --- a/tests/test_hotdata_backend.py +++ b/tests/test_hotdata_backend.py @@ -499,6 +499,38 @@ def on_create(req: Request) -> Response: con.create_database("sales", schema="public", tables=["orders"]) +def test_create_database_no_tables_still_sends_schema(httpserver: HTTPServer, srv: str): + def on_create(req: Request) -> Response: + body = req.get_json() + assert body == { + "name": "empty_db", + "source_type": "managed", + "config": { + "schemas": [{"name": "analytics", "tables": []}], + }, + "skip_discovery": True, + } + return Response( + json.dumps( + { + "id": MANAGED_CONN, + "name": "empty_db", + "source_type": "managed", + "discovery_status": "skipped", + "tables_discovered": 0, + } + ), + status=201, + content_type="application/json", + ) + + httpserver.expect_request("/v1/connections", method="GET").respond_with_json({"connections": []}) + httpserver.expect_request("/v1/connections", method="POST").respond_with_handler(on_create) + + con = ibis.hotdata.connect(api_url=srv, token="tok", workspace_id="ws", verify_ssl=False) + con.create_database("empty_db", schema="analytics") + + def test_drop_table_deletes_managed_table(httpserver: HTTPServer, srv: str): httpserver.expect_request("/v1/connections").respond_with_json(managed_connections_response()) httpserver.expect_request( From d7dc7fc86749456793eda1a05fb196c11b801650 Mon Sep 17 00:00:00 2001 From: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com> Date: Tue, 19 May 2026 19:42:50 -0700 Subject: [PATCH 2/5] refactor: remove duplicate obj/schema validation from create_table The check was already present in _local_table_to_parquet with the same condition and message. Having it in create_table too was dead code. Co-Authored-By: Claude Sonnet 4.6 --- src/ibis_hotdata/backend.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/ibis_hotdata/backend.py b/src/ibis_hotdata/backend.py index c5ba6d5..5ace76f 100644 --- a/src/ibis_hotdata/backend.py +++ b/src/ibis_hotdata/backend.py @@ -613,9 +613,6 @@ def create_table( if temp: raise NotImplementedError("Hotdata does not support temporary tables.") - if obj is not None and schema is not None: - raise com.IbisInputError("create_table accepts only one of obj or schema") - data = self._local_table_to_parquet(obj, schema) connection_id, schema_name = self._table_location(database) if not overwrite and self._managed_table_synced(connection_id, schema_name, name): From 9aca94cd85d291d78b5b74c5744193bfca229425 Mon Sep 17 00:00:00 2001 From: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com> Date: Tue, 19 May 2026 19:43:06 -0700 Subject: [PATCH 3/5] test: add missing client.close() in test_result_arrow_poll_handles_accepted_result Every other HotdataClient created in this file is closed at the end of the test. This one was missed, leaving a dangling connection pool. Co-Authored-By: Claude Sonnet 4.6 --- tests/test_hotdata_http.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_hotdata_http.py b/tests/test_hotdata_http.py index 8402e44..b1932ca 100644 --- a/tests/test_hotdata_http.py +++ b/tests/test_hotdata_http.py @@ -124,6 +124,7 @@ def test_result_arrow_poll_handles_accepted_result(httpserver: HTTPServer): ) out = client.execute_query("select 1", poll_interval_s=0, poll_timeout_s=5) assert out["pa_table"].to_pydict() == {"n": [42]} + client.close() def test_async_query_run_failure(httpserver: HTTPServer): From ad9f29460b7f09df51145d6bcfa968078220f071 Mon Sep 17 00:00:00 2001 From: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com> Date: Tue, 19 May 2026 19:43:23 -0700 Subject: [PATCH 4/5] docs: explain _managed_table_synced behavior for pending-sync tables The method returns False for tables whose synced flag is False, which lets create_table proceed without overwrite=True while a load is still in progress. This was intentional but undocumented, making the behavior look like a bug. Co-Authored-By: Claude Sonnet 4.6 --- src/ibis_hotdata/backend.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/ibis_hotdata/backend.py b/src/ibis_hotdata/backend.py index 5ace76f..89323e2 100644 --- a/src/ibis_hotdata/backend.py +++ b/src/ibis_hotdata/backend.py @@ -345,6 +345,13 @@ def _managed_table_synced( schema_name: str, table_name: str, ) -> bool: + """Return True only if the table exists and its last load has completed. + + A table whose ``synced`` flag is False is still being loaded; we treat + it as writable (returns False) so that an in-progress load can be + retried without requiring ``overwrite=True``. Tables not present in the + information schema also return False (not yet created). + """ for row in self._iterate_information_schema( {"connection_id": connection_id, "schema": schema_name, "table": table_name}, include_columns=False, From c7457e3f534b83e2ef2f22d0f146c3dfd9185f03 Mon Sep 17 00:00:00 2001 From: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com> Date: Tue, 19 May 2026 19:43:45 -0700 Subject: [PATCH 5/5] nit: document why except Exception is intentionally broad in dtype_from_hotdata_sql_type ibis and sqlglot raise several different exception types depending on which part of type parsing fails (ValueError, AttributeError, parse errors internal to sqlglot). Narrowing to a specific type risks missing one and breaking type discovery for a valid-but-unusual column type. Add a comment so the broad catch is clearly deliberate. Co-Authored-By: Claude Sonnet 4.6 --- src/ibis_hotdata/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ibis_hotdata/types.py b/src/ibis_hotdata/types.py index c3a4bad..bc2f0b9 100644 --- a/src/ibis_hotdata/types.py +++ b/src/ibis_hotdata/types.py @@ -12,5 +12,5 @@ def dtype_from_hotdata_sql_type(sql_type: str | None, *, nullable: bool) -> dt.D return dt.String(nullable=nullable) try: return PostgresType.from_string(sql_type.strip(), nullable=nullable) - except Exception: + except Exception: # ibis/sqlglot raise a variety of parse errors; fall back to String return dt.String(nullable=nullable)