Skip to content

Commit 4f3769d

Browse files
Merge pull request #49 from hotdata-dev/feat/per-load-key
feat: add per-load key to load_managed_table
2 parents 268c9e2 + c266493 commit 4f3769d

7 files changed

Lines changed: 68 additions & 6 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
12+
- `load_managed_table` accepts a `key` argument — the merge key columns for
13+
`delete`/`update`/`upsert` loads, matched per-load instead of requiring a key
14+
declared at table creation. Omit it to use the table's declared key; ignored
15+
for `replace`/`append`. Requires `hotdata>=0.8.0`.
16+
1017

1118
## [0.7.3] - 2026-07-16
1219

‎hotdata_framework/client.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@ def load_managed_table(
340340
upload_id: str | None = None,
341341
file: str | None = None,
342342
mode: ManagedLoadMode = "replace",
343+
key: list[str] | None = None,
343344
) -> LoadManagedTableResult:
344345
if (upload_id is None) == (file is None):
345346
raise ValueError("Exactly one of upload_id or file is required")
@@ -352,6 +353,7 @@ def load_managed_table(
352353
request = LoadManagedTableRequest(
353354
mode=mode,
354355
upload_id=resolved_upload_id,
356+
key=key,
355357
)
356358
try:
357359
loaded = self.connections().load_managed_table(

‎hotdata_framework/managed_client.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,17 +205,23 @@ def load_managed_table(
205205
schema: str,
206206
upload_id: str,
207207
mode: ManagedLoadMode = "replace",
208+
key: list[str] | None = None,
208209
) -> LoadManagedTableResult:
209210
# append is the only non-idempotent mode: if the server commits the load
210211
# but the response is lost, a retry re-appends the same rows. Run it
211212
# at-most-once; every other mode is safe to retry.
213+
#
214+
# `key` is the merge key for delete/update/upsert loads: when set it is
215+
# matched per-load instead of a key declared at table creation. Omit it
216+
# to use the table's declared key. Ignored for replace/append.
212217
return self._request_with_retry(
213218
lambda: self._runtime.load_managed_table(
214219
database,
215220
table,
216221
schema=schema,
217222
upload_id=upload_id,
218223
mode=mode,
224+
key=key,
219225
),
220226
retryable=(mode != "append"),
221227
)

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ classifiers = [
2727
]
2828
dependencies = [
2929
# 0.7.0 adds `key` to table decls (create_managed_database(keys=) / add_managed_table(key=))
30-
"hotdata>=0.7.0",
30+
"hotdata>=0.8.0",
3131
"pandas>=2.0",
3232
"pyarrow>=14.0",
3333
]

‎tests/test_databases.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,16 @@ def test_load_managed_table_defaults_to_replace():
308308
assert _load_and_capture_request(_client()).mode == "replace"
309309

310310

311+
def test_load_managed_table_passes_key():
312+
request = _load_and_capture_request(_client(), mode="delete", key=["id"])
313+
assert request.mode == "delete"
314+
assert request.key == ["id"]
315+
316+
317+
def test_load_managed_table_key_defaults_to_none():
318+
assert _load_and_capture_request(_client()).key is None
319+
320+
311321
@pytest.mark.parametrize("mode", ["append", "delete", "update", "upsert"])
312322
def test_load_managed_table_forwards_mode(mode: str):
313323
assert _load_and_capture_request(_client(), mode=mode).mode == mode

‎tests/test_managed_client.py‎

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,13 @@ def _load_recording_runtime(calls: list[str]) -> SimpleNamespace:
163163
with a transient error, so retry behaviour is observable via ``calls``."""
164164

165165
def load_managed_table(
166-
database: str, table: str, *, schema: str, upload_id: str, mode: str
166+
database: str,
167+
table: str,
168+
*,
169+
schema: str,
170+
upload_id: str,
171+
mode: str,
172+
key: list[str] | None = None,
167173
) -> SimpleNamespace:
168174
calls.append(mode)
169175
raise TimeoutError("commit succeeded but response was lost")
@@ -209,3 +215,34 @@ def test_idempotent_load_retries_on_transient(monkeypatch: pytest.MonkeyPatch) -
209215
client.load_managed_table("db", "orders", schema="public", upload_id="u1", mode="replace")
210216

211217
assert calls == ["replace", "replace", "replace"] # retried up to max_retries
218+
219+
220+
def test_load_managed_table_forwards_key(monkeypatch: pytest.MonkeyPatch) -> None:
221+
"""A per-load ``key`` is passed straight through to the runtime client."""
222+
monkeypatch.setattr(mc.time, "sleep", lambda _seconds: None)
223+
captured: dict[str, Any] = {}
224+
225+
def load_managed_table(
226+
database: str,
227+
table: str,
228+
*,
229+
schema: str,
230+
upload_id: str,
231+
mode: str,
232+
key: list[str] | None = None,
233+
) -> SimpleNamespace:
234+
captured["mode"] = mode
235+
captured["key"] = key
236+
return SimpleNamespace(
237+
connection_id="c", schema_name=schema, table_name=table, row_count=0
238+
)
239+
240+
client = _managed_client(max_retries=1)
241+
runtime = _fake_runtime()
242+
runtime.load_managed_table = load_managed_table
243+
client._runtime = runtime
244+
245+
client.load_managed_table(
246+
"db", "orders", schema="public", upload_id="u1", mode="delete", key=["id"]
247+
)
248+
assert captured == {"mode": "delete", "key": ["id"]}

‎uv.lock‎

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)