Skip to content

Widen the provisioning tools: expires_at and keys on create, mode on load, and mark the destructive set #91

Closed

Description

@rohan-hotdata

Corrected 1 Sep 2026. This issue originally opened by claiming all three changes were
available on hotdata_framework>=0.10.0 with "no SDK bump, no upstream PR". That is wrong
for two of them
, and the error was inherited from docs/ddl-tools-plan.md:5, whose
self-containment claim rests on the same false premise. Corrected inline below; see the
comments for the version table that pinned it down.

Three changes. mode is free today; format and result_id are not — see the correction
under item 2. The tool signatures are narrower than the calls they make, but in two cases the
framework client is narrower than the SDK model as well.

Context: an internal audit (18 Aug) counted 3 tools exposed against 8 framework capabilities
available but unwrapped. This issue is the subset an agent that provisions its own storage needs
first; the broader lifecycle question stays in #61.

1. hotdata_create_managed_database drops four parameters

The tool takes (name, schema_name, tables). The framework call underneath takes
keys, expires_at, partition_by and sorted_by as well.

Two of those are load-bearing for an agent that provisions its own storage:

  • expires_at — accepts RFC 3339 or a relative "24h" / "7d". Without it, every database an
    agent creates lives forever, and lifetime becomes a cleanup script instead of a property. This is
    the whole answer to "who deletes investigation number 4,281".
  • keys — declares each table's natural key, which is what makes the key-based load modes
    legal on it. A table declared through the tool today is keyless, so upsert, update and
    delete loads are rejected against it for the rest of its life.

⚠️ Note partition_by / sorted_by are permanent once a table exists — there is no ALTER path
anywhere in the API, and the only undo is delete-and-reload, which currently burns the table name
(#36). Whatever the tool description says about these two should state that outright, in the same
"state the consequence, not just the parameter" style as today's load tool.

2. hotdata_load_managed_table hardcodes mode="replace"

databases.load_managed_table never passes mode, so the framework default applies and
append, upsert, update and delete are unreachable from a tool. An agent cannot top up a
table it already loaded, and cannot make a re-run idempotent.

⚠️ format and result_id are not reachable from the framework client on any version.
hotdata_framework/client.py:396 builds LoadManagedTableRequest(mode=…, upload_id=…, key=…)
— three fields — and load_managed_table is the only load path on the client. Checked across
0.10.0, 0.11.0, 0.12.0, 0.12.1 and 0.13.0: absent from all five. So these two need a framework
change, or a raw-DatabasesApi bypass, and are not in the same "pure wrapping" bucket as
mode.

A working bypass already exists downstream, if it helps whoever picks this up: the
connection-scoped route fails with X-Database-Id header is required, and
DatabasesApi(client.api).load_database_table(database_id=…, var_schema=…, table=…, load_managed_table_request=LoadManagedTableRequest(mode="replace", result_id=…)) works. That
said, a framework change is the cleaner fix in a package that goes through the client
everywhere else.

The underlying LoadManagedTableRequest accepts both, and they are worth having:

  • format removes a pyarrow step from every caller materialising data it already holds (this is
    gap 2 in Provisioning boundary: agent-driven database, table, and index lifecycle #61).
  • result_id publishes a persisted query result as a table's contents — effectively
    CREATE TABLE AS SELECT, including across an attached catalog. Worth flagging: the DDL memo
    recorded query materialization as "found nowhere, needs upstream design" after looking in
    ResultsApi, SavedQueriesApi and QueryRunsApi. It exists — it is just on the load endpoint.

3. Mark the destructive tools so interrupt_on can be configured

HumanInTheLoopMiddleware(interrupt_on=…) is keyed by tool name, so a developer wiring approval
has to know which HDLC tool names are mutating. Today that is inferred from naming.

  • Export DESTRUCTIVE_TOOL_NAMES from the package so it can be passed straight through.
  • Set metadata={"destructive": True} on the mutating tools — BaseTool supports tags and
    metadata, settable via StructuredTool.from_function, and it echoes MCP's destructiveHint.

Worth stating plainly in the docs while doing this: management_tools=False is not a safety
flag (the source already says so), and "read-only" currently appears only in tool descriptions
written for the model — there is no validator on the SQL path in this library. DDL through SQL is
blocked by runtimedb's write_guard.rs rather than by anything here, and that guard does not
cover the Databases/Indexes/Jobs REST endpoints.

Activity

  1. rohan-hotdata commented on Sep 1, 2026

    @rohan-hotdata
    ContributorAuthor

    Correction: "no SDK bump, no upstream PR" holds for items 1 and 3, but not for half of item 2

    Checked while sequencing this against a downstream consumer. The framing at the top — "All of it is
    already available on hotdata_framework>=0.10.0, which HDLC already requires"
    — is the load-bearing
    claim for scoping this issue, and it is true for most of it and false for two parameters.

    What is genuinely just a widening

    Verified against the installed 0.12.1:

    create_managed_database(description=None, *, schema='public', tables=None,
                            keys=None, partition_by=None, sorted_by=None, expires_at=None)
    load_managed_table(database, table, *, schema='public', upload_id=None, file=None,
                       mode='replace', key=None)
    

    So item 1 (keys, expires_at, partition_by, sorted_by) and item 2's mode are exactly as
    described — the framework already takes them and HDLC's tool signatures are simply narrower. Item 3
    touches only this package. No upstream work for any of that.

    What is not reachable

    format and result_id are not on the framework client. They exist on the
    LoadManagedTableRequest model, which is what the issue quotes, but HotdataClient.load_managed_table
    hardcodes the fields it forwards:

    LoadManagedTableRequest(
        mode=mode,
        upload_id=resolved_upload_id,
        key=key,
        var_async=True,
        async_after_ms=_LOAD_INLINE_WAIT_MS,
    )

    Neither parameter appears in the signature, and it is the only load path on the client — there is no
    sibling method that reaches them.

    Checked across the whole supported range rather than only what happens to be installed:

    hotdata-framework format result_id
    0.10.0 (the floor in pyproject.toml) no no
    0.11.0 no no
    0.12.1 (installed) no no
    0.13.0 (latest, #94 open) no no

    What this changes

    Item 2 splits in two, and the halves have different costs:

    • mode — pure HDLC widening, ship whenever.
    • format and result_id — need a hotdata-framework change, or HDLC reaching past the client
      to DatabasesApi.load_database_table directly. The second is worth resisting: this package goes
      through the framework client everywhere else, and one raw-API call would be the only place that
      does not.

    That matters most for result_id, since it is the CREATE TABLE AS SELECT equivalent and the thing
    the DDL memo had recorded as missing entirely. It is real, but it is a version further away than this
    issue implies, so it should not be scoped as free.

    Unrelated, and in the other direction

    While enumerating the client for this: add_managed_table, list_managed_tables,
    managed_table_layout, create_index, delete_managed_database and delete_managed_table are
    all present on the framework client today. So the later phases in docs/ddl-tools-plan.md
    (Group A and Group B) are pure wrapping with no upstream dependency at all — which is the opposite
    of the situation here, and worth knowing before ordering the phases by risk.

  2. rohan-hotdata commented on Sep 1, 2026

    @rohan-hotdata
    ContributorAuthor

    Follow-up: two amendments to the comment above, and a floor problem it exposes

    Cross-checked against a downstream consumer, which was reading 0.10.0 where this repo was reading the
    installed 0.12.1. Both readings were right about their own version, and the disagreement is the
    useful part.

    The closing paragraph above said six methods are "present on the framework client today". That
    is true of 0.12.1 and false of pyproject.toml's declared floor — the same version ambiguity the
    rest of the comment was correcting. Pinning it properly:

    0.10.0 (floor) 0.11.0 0.12.0 0.12.1 0.13.0
    add_managed_table, list_managed_tables, create_index, delete_managed_database, delete_managed_table yes yes yes yes yes
    managed_table_layout no no yes yes yes
    load_managed_table forwards var_async / async_after_ms no no yes yes yes
    format, result_id no no no no no
    fork_database no no no no no

    So the "pure wrapping, no upstream dependency" claim holds for five of the six, not all six.

    What that means for scoping

    • managed_table_layout needs the floor raised to hotdata-framework>=0.12.0. It is not an
      upstream ask like format/result_id — the capability exists — but it cannot be wrapped while
      this package advertises support for 0.10.0. Cheap, and worth doing deliberately rather than
      discovering it when someone on the floor version gets an AttributeError.
    • Async loads are also unreachable from the client below 0.12.0, which widens the earlier point
      slightly: at the floor, load_managed_table forwards exactly mode, upload_id and key.
    • fork_database is on DatabasesApi but on no framework version. Relevant to the
      "golden template database" idea, which was already ruled out on other grounds (a fork re-attaches
      external catalogs but does not carry indexes over).

    The raw-SDK bypass is not hypothetical

    Also worth recording from the same cross-check: the DatabasesApi(client.api).load_database_table(...)
    route was actually exercised while probing result_id. The client-scoped path fails with
    X-Database-Id header is required, and dropping to the raw API is what made the load work. So
    whoever picks up format/result_id has a working code path to lift rather than one to discover —
    though the preference for going through the framework client stands, so the cleaner fix is still a
    framework change.

  3. added a commit that references this issue on Sep 1, 2026
    3d6b0e1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    ai-native-layerPart of the AI-native query layer effort for LangChainenhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions