Skip to content

fix(cq): send the whole definition on update, and read the shapes Arc returns - #5

Draft
xe-nvdk wants to merge 1 commit into
mainfrom
fix/cq-update-full-definition
Draft

xe-nvdk wants to merge 1 commit into
mainfrom
fix/cq-update-full-definition

Conversation

@xe-nvdk

@xe-nvdk xe-nvdk commented Oct 2, 2026

Copy link
Copy Markdown
Member

Fixes #4. Draft on purpose — holding until Basekick-Labs/arc#1023 decides whether Arc keeps strict full-replace semantics on PUT or grows a merge route. This fix is correct under either outcome (sending a complete definition always works), so it is ready whenever that lands; released with Arc 26.09.3.

The break

PUT /api/v1/continuous_queries/:id replaces the stored definition — Arc writes every column from the body, so a field the body omits is stored blank or zeroed. update() built a sparse payload from only the kwargs passed, and had no database, source_measurement or destination_measurement parameter at all, so it could not send a complete definition even in principle. Against current Arc it answers 400 database is required (Basekick-Labs/arc#993; Basekick-Labs/arc#1022 adds the other five required fields).

update() now reads the definition and sends it back with the arguments given applied. A one-field change still carries a whole definition, and the five fields Arc overwrites without requiring are preserved: description, tag_columns, retention_days, delete_source_after_days, is_active. Omitting that last one stored false and silently stopped the continuous query.

Two response shapes were wrong, which is why the read half could not work

Found while implementing the above — update() cannot read-modify-write if get() raises:

  • create, get and update unwrapped data.get("query", data). Arc answers with the definition flat, and a flat definition has a query key holding SQL — so this passed a string where a mapping belongs and raised TypeError: argument after ** must be a mapping, not str on all three calls. Now ContinuousQuery.from_response, which treats query as an envelope only when it actually holds a mapping.
  • list read data.get("queries", []), but GET /api/v1/continuous_queries answers with a bare array — 'list' object has no attribute 'get'. Both forms accepted now.

Also: the model was missing tag_columns, so a round trip through it dropped the metadata compaction uses to dedup continuous-query output (Basekick-Labs/arc#521).

Same change in the async twin.

Test plan

  • pytest tests/unit/ — 109 pass (7 new: the full-definition body, the preserved fields, applying only what is given, both response shapes)
  • ruff check and ruff format --check clean; mypy reports nothing in the changed files (its 10 pre-existing errors are missing third-party stubs in query/)
  • Live against a local Arc build carrying arc#1022: list, get, update(is_active=False) then back to True, and the async twin with update(interval=...) — each round-trips and the definition survives (db1 cpu -> cpu_1h unchanged throughout)
  • Live negative case: a grandfathered row whose stored source_measurement is blank fails with Arc's own 400 source_measurement is required rather than being silently re-sent — the SDK reports the server's reason, which is the intended behaviour for a row only an operator can repair

Known gap left alone

create() still has no tag_columns parameter, so a continuous query created through this SDK cannot declare its grouping dimensions. Pre-existing and separate from this fix.

… returns

PUT /api/v1/continuous_queries/:id replaces the stored definition: Arc writes
every column from the body, so a field the body omits is stored blank or
zeroed. update() built a sparse payload from only the kwargs passed, and had no
database, source_measurement or destination_measurement parameter at all, so it
could not send a complete definition even in principle — against current Arc it
answers 400 database is required (Basekick-Labs/arc#993, and #1011 adds the
other five required fields).

update() now reads the definition and sends it back with the arguments given
applied, so a one-field change still carries a whole definition and preserves
the five fields Arc overwrites without requiring: description, tag_columns,
retention_days, delete_source_after_days and is_active. Omitting the last one
set it false and silently stopped the continuous query.

Two response shapes were also wrong, which is why the read half could not work:

- create, get and update unwrapped data.get("query", data). Arc answers with the
  definition flat, and a flat definition has a query key holding SQL, so this
  passed a string where a mapping belongs and raised TypeError on all three.
  Replaced with ContinuousQuery.from_response, which unwraps only a mapping.
- list read data.get("queries", []), but that endpoint answers with a bare
  array. Both forms are accepted now.

The model was missing tag_columns, so a round trip through it dropped the
metadata compaction uses to dedup continuous-query output (arc#521).

Verified against a local Arc build: list, get, a one-field update and the async
twin all round-trip, and the definition survives.
@xe-nvdk

xe-nvdk commented Oct 12, 2026

Copy link
Copy Markdown
Member Author

Basekick-Labs/arc#1228 makes PUT a partial update on Arc 27.01.1 for both continuous queries and retention policies. The SDK's existing update() methods, which send only the arguments given, work as-is there, so this PR's full-definition workaround is only needed for Arc 26.09.x servers.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ContinuousQueries.update() cannot send the fields the server now requires

1 participant