You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: declare and read back a managed table's storage layout (#69)
* feat: declare and read back a managed table's storage layout
Closes#55
add_managed_table() and create_managed_database() take partition_by /
sorted_by; managed_table_layout() reads back what was actually declared.
TablePartitionKey and TableSortKey are re-exported so callers need one import.
BOTH DIRECTIONS, because either alone is not usable. A layout is fixed when the
table is created and there is no alter path -- a table declared without one keeps
that shape until it is recreated and its data rewritten. So declaring is half the
job: a caller has to be able to confirm it took, and to refuse to load when it
cannot. That is why at least one consumer hand-built the HTTP request rather than
using this package: the write side dropped the fields and the read side had
nowhere to report them.
managed_table_layout() raises KeyError for a table that is not declared instead
of returning an empty layout. "Not there" and "declared without a layout" lead a
caller to opposite decisions.
The generated key models are passed through rather than wrapped in a parallel
type system. This is a thin wrapper over a GENERATED client, so the transform
vocabulary and field names stay exactly the API's, and the strict mypy config is
satisfied without Any.
hotdata floor raised to >=0.9.0,<0.10. 0.9.0 is the first release carrying
partition_by / sorted_by on all three models this needs -- the add-table request,
the create-database table declarations, and the table-info response. On an older
hotdata the fields are silently dropped by the model and the table is declared
without a layout, returning success. Verified against the published 0.9.0 wheel.
TESTS ASSERT THE SERIALISED REQUEST, not that the call succeeded, because the
failure mode is silence: a field the model does not know about vanishes at
to_dict() and the API returns 201. Sabotaged all three:
drop the layout from the request -> caught
send empty arrays instead of omitting -> caught
collapse missing-table into empty -> caught
133 -> 138 passed. ruff 2 -> 1 (the auto-fix also cleaned a pre-existing
import-order issue in client.py; the hotdata.uploads comment stayed with its
import, checked). mypy unchanged at 158, all pre-existing.
* fix: refuse layout for a table not being created, and read it back in one request
Five review points, all taken.
create_managed_database now raises ValueError when partition_by or sorted_by
names a table that is not in `tables`. Previously a typo -- {"fils": ...} against
tables=["files"] -- was dropped in silence and the intended table created flat,
which is permanent because a layout is fixed at creation with no alter path. The
asymmetry with `keys` is deliberate: a wrong key is recoverable, since
load_managed_table takes `key=` per call.
managed_table_layout now filters server-side (var_schema=/table=/limit=1) instead
of paging iter_tables until it finds a match, which cost several round trips for
a table sorting late in the listing. Same KeyError contract.
The absent-vs-unpartitioned test shared one response object across both calls, so
its second assertion inspected a consumed result rather than the case named in
the docstring. Each call now gets a fresh response, and the fake answers by table
name so the "listing has files but not missing" scenario is the one exercised.
TableLayout gains to_dict(), matching every other public dataclass here. Not
asdict(): the key lists hold pydantic models, which asdict copies through
untouched, so each key is mapped through its own to_dict(). The reviewer assumed
the omission was deliberate -- it was not.
CONTRACT.md lists managed_table_layout among the methods accepting an
already-resolved ManagedDatabase, which matters because a create-scoped key that
cannot read /databases is exactly the caller needing this read-back.
Re-sabotaged after the rewrite -- dropping the layout, collapsing missing-table
into an empty layout, and removing the unknown-table guard are each caught.
138 -> 139 passed.
* fix: drop TableLayout.to_dict rather than add strict-mypy debt
I added to_dict() for surface consistency and then checked the gates properly:
it added 8 errors under this package s strict mypy settings (dict[str, Any] plus
comprehensions over the pydantic key models), taking the file from the 158 on
main to 166.
The alternative -- hand-building the dict from named fields -- is Any-free but
silently drops any field a later spec adds to TablePartitionKey or TableSortKey,
which is exactly the silent-drop failure this whole feature exists to prevent.
So taking the reviewer s first option: no to_dict, with a comment recording why
the inconsistency is deliberate, so the next reader does not "fix" it. A caller
wanting dicts can map k.to_dict() itself.
Also collapses a nested `with` flagged by ruff.
Back to main s baseline exactly: mypy 158, ruff only the pre-existing long line
in test_request_timeout.py. 139 passed.
Copy file name to clipboardExpand all lines: CONTRACT.md
+6-1Lines changed: 6 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -31,6 +31,9 @@ The supported import surface is:
31
31
-`WorkspaceSelection`
32
32
-`ManagedDatabase`
33
33
-`ManagedTable`
34
+
-`TableLayout`
35
+
-`TablePartitionKey`
36
+
-`TableSortKey`
34
37
-`LoadManagedTableResult`
35
38
-`CreateIndexResult`
36
39
-`DEFAULT_SCHEMA`
@@ -56,6 +59,8 @@ Adapters should import from `hotdata_framework` and treat this surface as the st
56
59
adapters should pass `connection_id` when known.
57
60
-`uploads()` returns the uploads API wrapper for parquet staging.
58
61
-`list_managed_databases()` returns all databases via the `/databases` API.
62
+
-`add_managed_table(...)` and `create_managed_database(...)` accept `partition_by` / `sorted_by` to declare a table's storage layout. The layout is fixed when the table is created and cannot be altered afterwards, so omitting it is permanent for that table.
63
+
-`managed_table_layout(database, table, schema=...)` returns the declared layout as `TableLayout`. Empty lists mean no layout was declared — sound only because the table is resolved through a managed database. Raises `KeyError` when the table is not declared, keeping "absent" distinct from "declared without a layout".
59
64
-`resolve_managed_database(name_or_id)` resolves a database by id (direct lookup) or description (list scan). A `403` from `/databases` surfaces as `RuntimeError` (forbidden, not absent), preserving the underlying `ApiException` as `__cause__`.
60
65
-`create_managed_database(description=..., schema=..., tables=..., expires_at=...)` creates a database via the `/databases` API and optionally declares tables up front. Returns a `ManagedDatabase` (id + `default_connection_id`) sufficient to load without a further read.
61
66
-`delete_managed_database(name_or_id)` deletes a database via the `/databases` API.
@@ -64,7 +69,7 @@ Adapters should import from `hotdata_framework` and treat this surface as the st
64
69
-`load_managed_table(database, table, schema=..., upload_id=..., file=...)` publishes parquet data into a declared managed table.
65
70
-`delete_managed_table(database, table, schema=...)` deletes a managed table.
66
71
- `create_index(database, table, schema=..., columns=..., index_type=..., index_name=...)` builds a `"sorted"`, `"bm25"`, or `"vector"` index on a managed table and returns a `CreateIndexResult`. It is the framework-side equivalent of the CLI's `hotdata indexes create`; indexing a table on a plain (non-managed) connection is out of scope. `index_name` defaults to `{table}_{columns}_{index_type}`, matching the CLI's derivation when `--name` is omitted. `index_type` is required rather than defaulting to the API's `"sorted"`. The build runs as a background job; the call polls it to a terminal state and raises `RuntimeError` with the job's `error_message` when it fails, because the submit call reports success regardless. `wait=False` returns as soon as the job is accepted, with `status="pending"` and a `job_id` for the caller to poll. For `index_type="vector"`, omitting `embedding_provider_id` indexes an existing vector column and `metric` (`"l2"`, `"cosine"`, `"dot"`) selects the distance function the index accelerates — a query using a different function silently falls back to a full scan; setting `embedding_provider_id` indexes a source *text* column instead, and the returned `source_column` names the column to pass to `vector_distance`. Argument combinations the server would silently ignore raise `ValueError` before any request is sent.
67
-
- The `database` argument of `list_managed_tables`, `load_managed_table`, `add_managed_table`, `delete_managed_table`, `delete_managed_database`, `create_index`, and `execute_sql` accepts a name/id **or** an already-resolved `ManagedDatabase`. Passing a `ManagedDatabase` skips the name/id read probe, so a create-scoped key that cannot read `/databases` can load into a database it just created.
72
+
- The `database` argument of `list_managed_tables`, `load_managed_table`, `add_managed_table`, `delete_managed_table`, `delete_managed_database`, `create_index`, `managed_table_layout`, and `execute_sql` accepts a name/id **or** an already-resolved `ManagedDatabase`. Passing a `ManagedDatabase` skips the name/id read probe, so a create-scoped key that cannot read `/databases` can load into a database it just created.
0 commit comments