Fix OpenAPI drift: ClickStack expansion, quotas, and model updates - #311
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
bugbot review |
|
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 0709f62. Configure here.
There was a problem hiding this comment.
Pull request overview
Remediates ClickHouse Cloud OpenAPI drift in the clickhouse-cloud-api crate by adding newly missing ClickStack + organization quota endpoints, expanding/adjusting ClickStack models/enums, and extending unit/integration-style tests to lock in the updated wire shapes.
Changes:
- Added new
Clientmethods for ClickStack (sources, connections, roles, saved searches, webhooks, dashboard validation) and organization quota endpoints. - Expanded/adjusted model types and enums to match the live spec (new fields, new enum values, and new ClickStack union variants).
- Added/extended tests validating serialization/deserialization and client request/response shapes for the new endpoints/models.
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
crates/clickhouse-cloud-api/src/client.rs |
Adds new API operations (ClickStack CRUD + dashboard validation; organization quotas). |
crates/clickhouse-cloud-api/src/models.rs |
Updates/extends models and enums for ClickStack + quotas; adds new union members and supporting types. |
crates/clickhouse-cloud-api/src/meta.rs |
Updates BETA_OPERATIONS list to include newly beta-classified endpoints. |
crates/clickhouse-cloud-api/tests/client_test.rs |
Adds wiremock-backed tests for the new client methods/endpoints. |
crates/clickhouse-cloud-api/tests/models_test.rs |
Adds serialization/deserialization regression tests for new/changed models and union dispatch ordering. |
Comments suppressed due to low confidence (6)
crates/clickhouse-cloud-api/src/models.rs:7926
- After changing
Unknowntoserde_json::Value, theDisplayimpl should format that value (otherwise this won’t compile).
Self::ClickStackCategoricalBarRawSqlChartConfig(_) => {
write!(f, "ClickStackCategoricalBarRawSqlChartConfig")
}
Self::Unknown(s) => write!(f, "{s}"),
}
crates/clickhouse-cloud-api/src/models.rs:8028
- After changing
Unknowntoserde_json::Value, update theDisplaymatch arm accordingly.
Self::ClickStackEqualityColorCondition(_) => {
write!(f, "ClickStackEqualityColorCondition")
}
Self::Unknown(s) => write!(f, "{s}"),
}
crates/clickhouse-cloud-api/src/models.rs:8134
- After changing
ClickStackSource::Unknowntoserde_json::Value, update theDisplayimpl match arm to print the JSON value.
Self::ClickStackSessionSource(_) => write!(f, "ClickStackSessionSource"),
Self::ClickStackPromqlSource(_) => write!(f, "ClickStackPromqlSource"),
Self::Unknown(s) => write!(f, "{s}"),
}
crates/clickhouse-cloud-api/src/models.rs:8186
ClickStackTileConfigis an untagged enum over object variants, butUnknown(String)only matches JSON strings. If the API adds a new tile config object, deserialization will error rather than landing inUnknown. UseUnknown(serde_json::Value)to make the catch-all effective.
This issue also appears on line 8204 of the same file.
ClickStackEventPatternsChartConfig(ClickStackEventPatternsChartConfig),
ClickStackMarkdownChartConfig(ClickStackMarkdownChartConfig),
/// Catch-all for unknown or newly-added values.
Unknown(String),
}
crates/clickhouse-cloud-api/src/models.rs:8206
- After changing
ClickStackTileConfig::Unknowntoserde_json::Value, update theDisplayimpl match arm accordingly.
Self::ClickStackMarkdownChartConfig(_) => write!(f, "ClickStackMarkdownChartConfig"),
Self::Unknown(s) => write!(f, "{s}"),
}
crates/clickhouse-cloud-api/src/models.rs:8057
- If
ClickStackOnClick::Unknownbecomesserde_json::Value, theDefault+Displayimpls should be updated accordingly (they currently assumeString).
Self::ClickStackOnClickDashboard(_) => write!(f, "ClickStackOnClickDashboard"),
Self::ClickStackOnClickExternal(_) => write!(f, "ClickStackOnClickExternal"),
Self::Unknown(s) => write!(f, "{s}"),
}
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
sdairs
left a comment
There was a problem hiding this comment.
Two valid-response deserialization bugs remain in the untagged ClickStack unions. The current tests pass because the trace test bypasses ClickStackSource, while the webhook endpoint tests exercise only the first (Slack) union arm.
ClickStackSource was #[serde(untagged)] with ClickStackLogSource first; every source struct's kind enum carries an untagged Unknown(String) catch-all, so kind:"trace" parsed as Unknown and, because the spec requires defaultTableSelectExpression on both log and trace, every valid trace payload satisfied all Log-required fields and misdispatched to the Log variant, silently dropping trace-only fields. Metric and session had the same hazard. Replace the derived Deserialize with a hand-written discriminator dispatch on "kind" (log/trace/metric/session/promql), mirroring BackupBucket. Change the catch-all to Unknown(serde_json::Value) so unknown payloads round-trip faithfully. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…emediation) The untagged ClickStackWebhook union tried variants in order, and since all five webhook variants share identical required fields with catch-all service enums, every response was greedily classified as the Slack variant -- silently discarding ClickStackGenericWebhook's optional `body` field. Follow the BackupBucket pattern: keep #[serde(untagged)] + Serialize but drop the derived Deserialize, and hand-write dispatch keyed on the `service` discriminator. The catch-all becomes Unknown(serde_json::Value) storing the raw value so unknown payloads round-trip faithfully. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ion) The legacy builder tile variants (Line, Bar, Table, Number, Pie) share identical required fields and their displayType enums carried Unknown(String) catch-alls, so an untagged union let the first structurally-satisfiable variant greedily win and misdispatched valid payloads (e.g. a "table" payload parsed as a line config with Unknown display_type). Replace ordering-dependence with hand-written discriminator dispatch keyed on "displayType", matching the BackupBucket/ClickStackSource pattern. The union's catch-all now stores serde_json::Value so unknown payloads round-trip faithfully. The six Builder/RawSql sub-unions gain a trailing Unknown(Value) catch-all so novel object shapes under a known displayType no longer hard-error. Restore the standard Unknown(String) catch-all to the three strict displayType value enums this PR introduced, since strictness is no longer load-bearing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ClickStackOnClick and ClickStackNumberTileColorCondition were untagged unions whose variants shared a single-value discriminator enum with a catch-all, so the first structurally-satisfiable variant greedily won and misdispatched valid payloads (e.g. type:"dashboard" resolved to Search). Replace the derived Deserialize with hand-written discriminator dispatch following the BackupBucket pattern: ClickStackOnClick routes on "type" (search/dashboard/external) and ClickStackNumberTileColorCondition routes on "operator" (gt|gte|lt|lte -> Numeric, between -> Between, eq|neq -> Equality). Both catch-alls now carry serde_json::Value so unknown payloads round-trip faithfully. Restore standard Unknown(String) catch-alls to the three strict operator value enums now that dispatch is manual. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Re: the tile-config follow-up above — this is now fixed in this PR (3cea4c3) rather than deferred. Instead of strictening the legacy One residual instance of the same defect class was confirmed during a final audit and is deliberately not fixed here: 🤖 Generated with Claude Code |
Review follow-up: 4 items to address in this PRDeep review of the remediation commits (1842d75, 001cd95, 3cea4c3, 8874b03) against the long-term model conventions. The discriminator-dispatch pattern is correct and well-tested; the four items below should land here rather than as follow-ups. 1.
|
…ew remediation)
The BackupBucket, BackupBucketPatchRequest, BackupBucketPostRequest, and
BackupBucketProperties discriminated unions carried their catch-all as
Unknown(String) with a fallback of Unknown(value.to_string()). Since the
enums serialize via #[serde(untagged)], an Unknown re-serialized as a JSON
string rather than the original object, so an unrecognized bucketProvider
did not round-trip faithfully.
Change all four Unknown variants to Unknown(serde_json::Value) and the
fallback arms to Ok(X::Unknown(value)), matching the five ClickStack unions
that already round-trip losslessly. Display keeps write!(f, "{s}"):
serde_json::Value's Display emits compact JSON, byte-identical to the prior
stringified-String output, so CLI human output is unchanged. Document that
choice on each catch-all doc comment across all nine unions so the
convention reads as deliberate.
Update the existing deserialize_backup_bucket_unknown_provider test to
assert the Unknown payload is the original object and round-trips, add
matching round-trip tests for the other three unions, and add a Display
test asserting the compact-JSON payload is emitted.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
review remediation) ClickStackDashboardChartSeries was a derived #[serde(untagged)] union with ClickStackTimeChartSeries ordered first. Its inline *Type enums each carry an Unknown(String) catch-all that accepts any type string, and the Time and Table variants share identical required fields (aggFn, groupBy, sourceId, type, where, whereLanguage), so a type:"table" payload greedily resolved to the Time variant and silently dropped table-only fields like sortOrder. Replace the derived Deserialize with hand-written discriminator dispatch on the "type" key (time/table/number/search/markdown), matching the BackupBucket and 8874b03 pattern. The Unknown catch-all now carries serde_json::Value so unknown payloads round-trip faithfully, and its Display emits raw compact JSON. The per-variant *Type enums keep their Unknown(String) catch-alls now that dispatch is manual. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…review remediation)
models.rs had ten structurally identical hand-written Deserialize impls for
externally-discriminated `#[serde(untagged)]` enums: buffer the payload as a
serde_json::Value, read a string discriminator field, route each known wire
value to a variant via serde_json::from_value, and fall through to the
Unknown(serde_json::Value) catch-all. The repetition was error-prone and
obscured the per-enum discriminator contract.
Extract a `discriminated_union!` macro that emits only the Deserialize impl
from a declaration listing the discriminator key and wire-value -> variant
arms (one arm may list several wire values, e.g.
`"gt" | "gte" | "lt" | "lte" => ClickStackNumericColorCondition`). Migrate all
ten impls: BackupBucket, BackupBucketPatchRequest, BackupBucketPostRequest,
BackupBucketProperties ("bucketProvider"); ClickStackDashboardChartSeries,
ClickStackOnClick ("type"); ClickStackNumberTileColorCondition ("operator");
ClickStackSource ("kind"); ClickStackTileConfig ("displayType"); and
ClickStackWebhook ("service"). All ten are single-key, single-level dispatch,
so none had to stay hand-written.
The enum declarations, their derives/serde attributes, and their Display impls
remain literal source so the syn-based OpenAPI drift analyzer can inventory
them structurally; the macro emits only the impl. The analyzer parses the
macro definition and invocations as skipped Item::Macro nodes. Generated
behavior is byte-for-byte identical to the previous impls; the existing
dispatch/round-trip tests in models_test.rs are unchanged and still pass, and
spec_coverage_test parses the updated models.rs cleanly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ew remediation)
ClickStackAlertChannel and ClickStackOnClickTarget are `#[serde(untagged)]`
OBJECT unions whose variants are distinguished by required fields, so they keep
derived Deserialize (no discriminator dispatch). But both still carried the
lossy pre-remediation catch-all `Unknown(String)`. An unknown-shape object
payload therefore HARD-FAILS to deserialize: untagged tries each struct
variant, all fail on missing required fields, then the `Unknown(String)`
catch-all is tried and a JSON object cannot coerce to a String, so serde errors
instead of preserving the payload.
Change both catch-alls to `Unknown(serde_json::Value)` with the same deliberate
doc comment used by the dispatched unions (holds the raw payload losslessly;
Display emits compact JSON), and update ClickStackOnClickTarget's Default impl
to `Self::Unknown(serde_json::Value::Null)` to match ClickStackOnClick. Display
stays `write!(f, "{s}")` (Value implements Display as compact JSON).
Add round-trip tests for each union that deserialize an unknown-shape object,
assert it lands in Unknown holding the original object, and re-serializes to the
same JSON object (not a string); these fail against the pre-fix code which
errors on deserialize. Add a known-variant sanity test for ClickStackAlertChannel
(email/webhook), which previously had no coverage.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Re: the 4-item follow-up — all four are addressed on this branch (d85bb9d..23c7e46), plus two more instances of the same defect class that a final review sweep caught. 1. BackupBucket catch-alls →
|
Review of the remediation work (all threads + follow-ups)Independently verified the follow-up commits (1842d75..23c7e46) against the review threads, the vendored OpenAPI snapshot, and an exhaustive sweep of every One item was marked resolved without being fixed, plus some minors. Should fix before merge1. Bugbot's "Trace table expression omits deserialize" is unaddressed (
Fix: drop Minor (non-blocking)2. Multi-value dispatch arms are only sampled. 3. Raw-SQL sub-variants under-tested. Only the categorical-bar sub-union exercises its RawSql variant through the union; line/stacked-bar/table/number/pie RawSql variants and five of six sub-union 4. Doc-comment drift surface. Each dispatched union's doc comment duplicates the discriminator wire values that also live in the macro invocation directly below it. Naming just the key in the doc would remove the duplication. 5. Test organization. Dispatch tests are grouped by drift-issue era rather than by type (the heatmap tile test sits ~800 lines from the other nine tile tests), naming mixes 6. Consistency option. 🤖 Generated with Claude Code |
…elect_expression (PR #311 review remediation) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… operators (PR #311 review remediation) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s (PR #311 review remediation) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… (PR #311 review remediation) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… remediation) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…per (PR #311 review remediation) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…expression Reverts a768db6, which stripped serde(default) from this field and added strictness regression tests in response to a review-bot finding. That pulled the model in the opposite direction of the tolerant-response policy (issue #312) and contradicts CLAUDE.md's "Retain #[serde(default)] on model fields": a server-side field drop would fail the entire list sources response instead of degrading to "". The trace test is inverted into a missing-field-degrades-to-default round-trip (asserted at both the struct and kind-dispatched union level), per issue #312 phase 4. The log-source strictness test is dropped rather than inverted: ClickStackLogSource was already strict on main and its sweep belongs to the legacy allowlist phase. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit dcdb3aa. Configure here.
| #[serde(rename = "excludedColumns", default)] | ||
| pub excluded_columns: Vec<String>, | ||
| #[serde(rename = "partitionByExpr", default)] | ||
| pub partition_by_expr: String, |
There was a problem hiding this comment.
Empty partitionByExpr on serialize
Medium Severity
ClickPipePostgresPipeTableMapping maps missing partitionByExpr to an empty string on deserialize but always serializes that field, so a mapping loaded from API JSON without partitionByExpr re-emits "partitionByExpr":"" on update instead of omitting it. That can change ClickPipe table configuration on get-modify-put flows.
Reviewed by Cursor Bugbot for commit dcdb3aa. Configure here.
| } | ||
|
|
||
| #[test] | ||
| fn backup_bucket_properties_unknown_provider_round_trips() { |
There was a problem hiding this comment.
Any chance we split tests by domains?
| } | ||
|
|
||
| /// ClickStack: List Saved Searches | ||
| pub async fn click_stack_list_saved_searches( |
There was a problem hiding this comment.
and maybe it would be cool to split client methods by domain


Closes #308
Remediates the OpenAPI drift between the live ClickHouse Cloud spec and
clickhouse-cloud-api. The drift had grown since the issue was filed; this PR remediates today's live document (findings at remediation time: 25 missing client methods, 29 missing model types, 27 missing struct fields, 5 optionality mismatches, 5 missing enum values, 25 newly-beta operations).What's added
ClickStackPromqlSource(new union member), get/create/update/delete endpoints, and restructured source schemas — newsection/disabled/knownColumnsListExpression/useTextIndexForImplicitColumnfields (the last is a string enumauto/enabled/disabledper the live spec, not a boolean).serde_json::Valueconditions).ClickStackWebhookInput+ create/update/delete (spec has no get-by-id); responses reuse the existingClickStackWebhookunion.click_stack_validate_dashboard+ response models.OrganizationQuota+ list/get endpoints.ClickStackChartColorpalette enum, background charts, number-tile color conditions (numeric/between/equality union), categorical-bar and event-patterns tile configs wired intoClickStackTileConfig,ClickStackOnClickExternalwired intoClickStackOnClick.numConsecutiveWindows, filterappliesToSourceIds, linefitYAxisToData, pielimit/orderBy, ClickPipespartitionByExpr(required onClickPipePostgresPipeTableMapping, nullable on the patch-remove mapping), 4 newActivityTypevalues, alert statePENDING.BETA_OPERATIONSregenerated (+25 entries).Breaking changes (library consumers, pre-1.0)
idis nowOption<String>(spec: server-generated, no longer required).ClickStackTraceSource::default_table_select_expressionis now requiredString.ClickStackSource,ClickStackTileConfig, andClickStackOnClickuntagged unions gained variants.ClickPipePostgresPipeTableMappinggained a requiredpartition_by_expr: Stringfield (#[serde(default)], so deserialization of old payloads is unaffected).No CLI surface changes — ClickStack is not exposed in the CLI; exposure is a separate follow-up decision.
Notes for reviewers
ClickStackTileConfigis deliberate and commented: the new strict-displayTypevariants (bar,event_patterns) are placed where the existing catch-all builder variants can't swallow them. A pre-existing quirk (a plaindisplayType:"table"tile parses asClickStackLineChartConfig) exists onmaintoo and is out of scope here.cargo test --workspace, workspace clippy-D warnings,cargo fmt --all --check,python3 -m unittest discover -s scripts/tests, andpython3 scripts/check-openapi-drift.py --dry-run→ Actionable drift: 0. Library fully covers the live spec.🤖 Generated with Claude Code