Per-entity ClickHouse table tuning via @storage(clickhouse: {...}) - #1433
Conversation
…1409) The @storage directive's clickhouse arg now accepts a table options object in addition to a boolean (the object form implies the backend is enabled): type Transfer @storage(clickhouse: { partitionBy: "toYYYYMM(timestamp)", orderBy: ["timestamp"], ttl: "timestamp + INTERVAL 2 YEAR" }) { ... } The options tune the generated envio_history_<Entity> table DDL: partitionBy/ttl are raw ClickHouse expressions emitted as PARTITION BY/TTL clauses, and orderBy is a list of entity field names replacing the default ORDER BY (id, envio_checkpoint_id) sorting key. orderBy is validated at codegen: fields must exist on the entity and be non-nullable, non-array, non-derived (ClickHouse rejects such sorting key columns at table creation). Field names are resolved to the actual ClickHouse column names at DDL time, so entity references (token -> token_id) and column_name_format renames keep working. The options travel through the persisted public config JSON mirroring the directive shape, so changing them diffs against the stored config and prompts a reset - the layout of an existing table can't be altered in place. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GwrS1UjDN5bZYrM6sZdkY5
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughChangesPer-entity ClickHouse table options
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant GraphQLSchema
participant CLIConfig
participant RuntimeConfig
participant ClickHouseDDL
GraphQLSchema->>CLIConfig: define per-entity ClickHouse options
CLIConfig->>RuntimeConfig: serialize and parse storage options
RuntimeConfig->>ClickHouseDDL: provide clickhouseOptions
ClickHouseDDL->>ClickHouseDDL: resolve orderBy columns
ClickHouseDDL->>ClickHouseDDL: generate partition, order, and TTL clauses
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/envio/src/bindings/ClickHouse.res (1)
365-387: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftAvoid row-deleting TTL on the history table. The current-state view picks the latest surviving history row per entity, then filters to
SET; expiring aDELETEtombstone can resurrect an olderSET, and expiring the only latest row can hide the entity entirely. If retention is required, keep current state separately or preserve each entity’s latest row/tombstone.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/envio/src/bindings/ClickHouse.res` around lines 365 - 387, Remove the TTL clause from the history table DDL generated by the surrounding table-creation function, including the ttl interpolation represented by ttlClause. Do not apply row-expiring retention to history rows unless the implementation separately preserves each entity’s latest SET or DELETE tombstone; keep the existing schema and ordering behavior unchanged.
🧹 Nitpick comments (1)
packages/envio/src/Config.res (1)
319-320: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRemove narration-only comments from these type declarations.
packages/envio/src/Config.res#L319-L320: remove the comment; the union already shows the boolean/object forms.packages/envio/src/Internal.res#L723-L724: remove the provenance/consumer comment; the fields are self-explanatory.As per coding guidelines: “Don't write a comment that restates what the code already says” and “Never narrate the refactor itself.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/envio/src/Config.res` around lines 319 - 320, Remove the narration-only comment above the entity’s clickhouse storage type declaration in packages/envio/src/Config.res at lines 319-320; the union type is self-explanatory. Also remove the provenance/consumer comment in packages/envio/src/Internal.res at lines 723-724, leaving the existing self-describing fields unchanged.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@packages/envio/src/bindings/ClickHouse.res`:
- Around line 365-387: Remove the TTL clause from the history table DDL
generated by the surrounding table-creation function, including the ttl
interpolation represented by ttlClause. Do not apply row-expiring retention to
history rows unless the implementation separately preserves each entity’s latest
SET or DELETE tombstone; keep the existing schema and ordering behavior
unchanged.
---
Nitpick comments:
In `@packages/envio/src/Config.res`:
- Around line 319-320: Remove the narration-only comment above the entity’s
clickhouse storage type declaration in packages/envio/src/Config.res at lines
319-320; the union type is self-explanatory. Also remove the provenance/consumer
comment in packages/envio/src/Internal.res at lines 723-724, leaving the
existing self-describing fields unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 2fb3849b-d847-42a6-8c91-dca26982d898
📒 Files selected for processing (11)
packages/cli/src/config_parsing/entity_parsing.rspackages/cli/src/config_parsing/public_config.rspackages/cli/src/config_parsing/system_config.rspackages/cli/src/hbs_templating/codegen_templates.rspackages/cli/test/configs/config-clickhouse-options.yamlpackages/cli/test/schemas/schema-with-clickhouse-options.graphqlpackages/envio/src/Config.respackages/envio/src/Internal.respackages/envio/src/bindings/ClickHouse.resscenarios/test_codegen/test/ConfigYaml_test.resscenarios/test_codegen/test/lib_tests/ClickHouse_test.res
The hypersync-health-check CI job started failing on every branch after the HyperSync API began advertising Tron (728126428): the check compares the API's active chains against the Network enum and fails on any gap. Add the entry it prescribes, with no reorg-depth override (the unknown- threshold bucket), to unblock CI. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GwrS1UjDN5bZYrM6sZdkY5
| )} | ||
| ) | ||
| ENGINE = ${tableEngine} | ||
| ORDER BY (${Table.idFieldName}, ${EntityHistory.checkpointIdFieldName})` |
There was a problem hiding this comment.
We need the (${Table.idFieldName}, ${EntityHistory.checkpointIdFieldName}) for internal HyperIndex write/read logic. I wonder whether the complete overwrite will be harmful to indexing performance.
There was a problem hiding this comment.
Checked what actually touches the history table today, to size the risk:
- Writes (
setUpdatesOrThrow) are plain batch INSERTs — no reads, and the part-sort cost at insert/merge time is the same for either key. - Rollback (
resume) runsALTER TABLE … DELETE WHERE envio_checkpoint_id > X. Sinceenvio_checkpoint_idisn't a leading column of the current(id, envio_checkpoint_id)key either, that mutation scans parts regardless of the key. - The entity view sorts by
envio_checkpoint_id DESC LIMIT 1 BY idat query time and gets no read-in-order benefit from the current key, so its cost is unchanged by the overwrite.
So no internal path depends on the sorting key today. But if you want to keep the door open for logic that would (e.g. per-id stale-history pruning), there's a zero-downside variant:
Option B: orderBy sets the leading key columns and the internals are always appended — orderBy: ["timestamp"] → ORDER BY (timestamp, id, envio_checkpoint_id). Time-range pruning only needs the prefix, so the user-facing benefit is identical, and (id, envio_checkpoint_id) stays in the key. Cost is just a slightly wider per-insert sort.
Option A (current PR): the key is exactly what the user writes.
Happy to go either way — B is a small change, say the word and I'll push it.
Generated by Claude Code
DZakh
left a comment
There was a problem hiding this comment.
The implementation looks good. Just one concern about the behavior change.
A custom @storage(clickhouse: {orderBy: [...]}) previously replaced the
whole sorting key, dropping envio_checkpoint_id. Keep it appended so the
key becomes ORDER BY (<user fields>, envio_checkpoint_id): the user's
fields lead (giving them the prune/compression benefit they asked for),
while the trailing checkpoint id gives a deterministic tie-break and a
clean ascending run per prefix for the current-state view's checkpoint
dedup. id is intentionally not kept — ClickHouse entities are read-only,
so no path looks history rows up by id.
Claude-Session: https://claude.ai/code/session_01SX5KpUyG4WHbKTJsHi5b5F
Co-authored-by: Claude <noreply@anthropic.com>
…ity-table-tuning-f8sccm
* Resolve field names in ClickHouse partitionBy/ttl; tighten orderBy validation
partitionBy/ttl expressions now reference entity fields by their schema
name: bare identifiers matching a field are rewritten to the ClickHouse
column at DDL time (renames and linked-entity `_id` suffixes resolved),
while functions, keywords, numbers, string literals and already-backticked
identifiers are left untouched.
Also:
- normalize `clickhouse: {}` to the boolean form so it doesn't diff a
config persisted as `clickhouse: true`
- reject `orderBy` listing `id` (the default sorting key)
- reject `orderBy` on BigInt/BigDecimal fields (stored as String, so
lexicographic ordering) with an actionable error
- trim partitionBy/ttl/orderBy values on store
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RK1nsdb4ybjdyPVLQB5iZL
* Address review: reword id error, drop redundant Rust test, assert full messages
- reword the `orderBy` id rejection to not leak internal column details
- remove the Rust full-options parse test; the options object is already
covered end-to-end (YAML -> parser -> Config.res -> DDL) in ClickHouse_test.res
- assert the complete error message in the ConfigYaml ClickHouse cases
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RK1nsdb4ybjdyPVLQB5iZL
* Assert exact parse-error messages in ConfigYaml_test
expectParseError now checks the thrown message with strict equality instead
of a substring match. Every case's expected string is the complete error
(including the parse context chain) so the assertions can't silently pass on
a partial match.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RK1nsdb4ybjdyPVLQB5iZL
---------
Co-authored-by: Claude <noreply@anthropic.com>
Implements the per-entity ClickHouse table tuning proposal from #1409, with the API shape requested in the issue thread:
What changed
The
@storagedirective'sclickhousearg now accepts a table options object in addition to a boolean. The object form implies the backend is enabled, and every key is optional:envio_history_<Entity>DDLpartitionByStringPARTITION BY <expr>; bare identifiers that match entity fields are resolved to the ClickHouse column namesorderBy[String!]idprefix;envio_checkpoint_idalways stays appended, so the key becomesORDER BY (<fields...>, envio_checkpoint_id)ttlStringTTL <expr>, with the same field-name resolution aspartitionByGenerated DDL for the example above:
Design notes
orderByentries must be existing entity fields written as in the schema, and must be non-nullable, non-array, non-derived.idis rejected (it's the default sorting key), and so are BigInt/BigDecimal fields (stored as String in ClickHouse, so ordering would be lexicographic). Unknown option keys, empty expressions, and empty/duplicateorderBylists are rejected too.orderByentries and bare identifiers insidepartitionBy/ttlexpressions are written as schema field names and rewritten to the actual columns, so entity references (token→token_id) andcolumn_name_format: snake_caserenames keep working. Functions, keywords, numbers, string literals and already-backticked identifiers pass through untouched. Invalid expressions still fail loudly at storage initialization.envio_checkpoint_idis always kept in the sorting key (per review discussion): a customorderBysets the leading columns, and the checkpoint id stays appended for a deterministic tie-break and a clean ascending run per prefix for the current-state view's dedup.idis intentionally not kept — ClickHouse entities are read-only, so no path looks history rows up by id.storage.clickhouseisbool | object), so changing them diffs against the stored config on restart and prompts a reset — a MergeTree table'sPARTITION BY/ORDER BYcan't be altered in place for existing data. Booleans keep serializing exactly as before (an emptyclickhouse: {}normalizes to the boolean form), so existing projects' persisted configs stay byte-identical.ORDER BY ... LIMIT 1 BY idis a query-time sort, independent of the table's sorting key. A custom sorting key mainly buys pruning for time-bounded queries againstenvio_history_*(and TTL gives the bounded-retention story from the issue).Not in this PR (follow-ups from #1409)
recentWindowsugar andisRealtimegating)Happy to tackle those next — they're meatier lifecycle features and deserve their own PRs.
Testing
orderByvalidation errors, storage routing validation, and the public-config JSON shape.Config.res→ DDL): options reachentityConfig.storageand the generatedCREATE TABLE, including snake_case + linked-entity resolution inorderByandpartitionBy/ttlexpressions.Closes #1409's first proposal (per-entity table options).
🤖 Generated with Claude Code
https://claude.ai/code/session_01GwrS1UjDN5bZYrM6sZdkY5
Generated by Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Tests