Skip to content

feat(parquet): typed promotion classes — writer side (RFC 0042 slice 1) - #649

Merged
jensholdgaard merged 3 commits into
mainfrom
feat/rfc0042-parquet-typed-promotion
Jul 26, 2026
Merged

feat(parquet): typed promotion classes — writer side (RFC 0042 slice 1)#649
jensholdgaard merged 3 commits into
mainfrom
feat/rfc0042-parquet-typed-promotion

Conversation

@jensholdgaard

@jensholdgaard jensholdgaard commented Jul 26, 2026

Copy link
Copy Markdown
Owner

What (RFC 0042 slice 1 of ~4)

The writer half of typed numeric promotion, enacting RFC 0022 §7.1 via RFC 0042 (draftedspecified in this PR):

  • PromotedClass { String, I64, F64 } with per-key declarations on PromotedAttributes::new_typed. The bare-string constructor is byte-for-byte unchanged for existing callers; the implicit service.name stays string-class and cannot be re-typed (pinned by test).
  • Class-typed columns: fields() emits OPTIONAL Utf8/Int64/Float64; the batch projection dispatches per class through a three-variant builder.
  • The §3.1 projection table, exactly: f64 widens ints (cost_usd: 0 as int still lands), i64 never narrows doubles, strings never parse into numeric columns.
  • Bloom split (§3.5): i64 + string promoted columns keep the RFC 0022 bloom; f64 columns get none — float equality is typed-arm-only, so a bloom is dead weight; ordering prunes via statistics. Asserted from the Parquet footer.

Criteria

  • RFC0042.1 — typed projection end-to-end through the encode path, using the captured Claude Code shape (double cost_usd, int tokens) plus the widening/narrowing/string rows; the JSON attributes column is byte-identical to an unpromoted run ("projection, not truth" re-asserted for typed classes).
  • RFC0042.2 — projection totality by proptest over AnyValue variants × classes (including value-absent forms): every cell is the §3.1-table value or NULL, never a panic or parse.

Cross-crate ripples

resource_keys()/log_keys() now return &[PromotedKey]; the three consumers (writer cluster-keys check, MCP query-schema resource, its test) updated mechanically. The RFC 0032 resource keeps publishing plain key strings — whether to expose classes there is a slice-2/RFC 0032 question, not smuggled in here.

Not in this slice

Config parsing ({key, type} entries — slice 2), the querier schema adapter + typed predicates/aggregation (slice 3), compaction re-projection test + dogfood (slice 4). #646 stays drafted until the ladder supports it.

Invariants

Touches hazard #5 territory on paper only: columns remain additive OPTIONAL (RFC 0042 §3.6, same evolution class as RFC 0022). Reconstruction/OTLP fidelity untouched — promoted cells are query-only projections and RFC0042.1 proves the JSON truth column identical.

Verification

cargo fmt --check · clippy --all-targets --all-features clean (pedantic) · cargo nextest run 1269/1269.

Summary by CodeRabbit

  • New Features

    • Added typed promotion for string, integer, and floating-point attributes.
    • Promoted columns now use matching data types and preserve null values for incompatible data.
    • Added typed numeric projections, including integer-to-floating-point widening.
    • Added public support for defining typed promoted keys.
  • Bug Fixes

    • Updated schema responses and clustering behavior for typed promoted attributes.
    • Bloom filters are now applied appropriately by promoted column type.
  • Documentation

    • Marked typed numeric promotion as specified.

PromotedClass {String, I64, F64} with a per-key declaration on
PromotedAttributes (new_typed; the bare-string constructor is unchanged
for existing callers, and the implicit service.name stays string-class
and cannot be re-typed). fields() emits OPTIONAL Utf8/Int64/Float64 by
class; the batch projection dispatches per class — f64 widens ints,
i64 never narrows doubles, strings never parse into numeric columns
(RFC 0042 §3.1). The writer blooms i64 and string promoted columns but
not f64 (§3.5: float equality is typed-arm-only, a bloom would be dead
weight; ordering prunes via statistics).

RFC0042.1: typed columns materialize through the encode path with the
captured Claude Code shape (double cost_usd, int tokens), the JSON
attributes column byte-identical to an unpromoted run, and the bloom
split asserted from the footer. RFC0042.2: projection totality by
proptest over AnyValue variants (incl. absent forms) — the cell is the
§3.1-table value or NULL, never a parse.

RFC 0042 flips drafted -> specified with implementation underway.

Verified: cargo fmt --check, clippy --all-targets --all-features clean,
nextest 1269/1269.

Signed-off-by: Jens Holdgaard Pedersen <Jens@holdgaard.org>
@jensholdgaard
jensholdgaard requested a review from Copilot July 26, 2026 19:48
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

RFC 0042 typed promotion adds class-aware promoted keys, nullable Arrow fields, typed record-batch projections, class-specific bloom-filter handling, and server serialization updates. New unit, integration, property, and NaN tests validate the behavior.

Changes

Typed promotion model

Layer / File(s) Summary
Typed promotion contracts and schema generation
crates/ourios-parquet/src/lib.rs, crates/ourios-parquet/src/promoted.rs, docs/rfcs/0042-typed-numeric-promotion.md
Promoted keys now declare string, i64, or f64 classes; schemas use matching nullable Arrow types; typed projection helpers and constructors enforce service-name handling.
Typed column materialization
crates/ourios-parquet/src/record_batch.rs
Promoted values are captured as KeyValue entries and emitted through string, Int64, or Float64 Arrow builders.
Writer integration and validation
crates/ourios-parquet/src/writer.rs, crates/ourios-parquet/tests/it/*
Clustering and bloom-filter configuration account for typed fields, with integration and property tests covering projections, NaN, nullability, and bloom filters.
Query-schema compatibility
crates/ourios-server/src/mcp.rs, crates/ourios-server/tests/it/rfc0032_query_schema.rs
Server serialization and fixtures extract string names from typed promoted-key objects.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PromotedAttributes
  participant record_batch
  participant writer
  participant QuerySchema
  PromotedAttributes->>record_batch: provide typed promoted keys
  record_batch->>writer: emit nullable typed Arrow columns
  writer->>writer: configure clustering and bloom filters
  PromotedAttributes->>QuerySchema: provide promoted key metadata
  QuerySchema-->>QuerySchema: serialize key strings
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR is detailed, but it misses the template's ## Related section and the checklist items. Add a ## Related section with RFC/issue links and a ## Checklist covering fmt, clippy, tests, docs, and RFC linkage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: typed promotion classes on the parquet writer side for RFC 0042 slice 1.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/rfc0042-parquet-typed-promotion

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements the writer-side half of RFC 0042 typed numeric promotion for promoted attributes in ourios-parquet, extending RFC 0022 promotion from string-only to class-typed (String, I64, F64) projected columns while keeping the JSON attributes column as the source of truth.

Changes:

  • Introduces PromotedClass/PromotedKey and updates PromotedAttributes to carry typed promoted keys, emitting OPTIONAL Utf8/Int64/Float64 columns accordingly.
  • Updates record-batch projection and Parquet writer properties to dispatch by class and to skip bloom filters for f64 promoted columns (per RFC 0042 §3.5).
  • Adds RFC 0042 writer-side integration + property tests and updates MCP/query-schema consumers to continue publishing plain key strings.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
docs/rfcs/0042-typed-numeric-promotion.md Advances RFC 0042 status to specified.
crates/ourios-server/tests/it/rfc0032_query_schema.rs Adjusts schema test to compare promoted key strings after PromotedKey introduction.
crates/ourios-server/src/mcp.rs Keeps MCP query-schema output as arrays of plain key strings despite internal typed keys.
crates/ourios-parquet/tests/it/rfc0042_typed_promotion.rs Adds RFC0042.1 integration test and RFC0042.2 proptest for typed projection behavior.
crates/ourios-parquet/tests/it/main.rs Registers the new RFC 0042 integration test module.
crates/ourios-parquet/src/writer.rs Updates cluster key check for service.name and skips bloom filters for Float64 promoted columns.
crates/ourios-parquet/src/record_batch.rs Implements class-dispatched promoted column builders and typed projection in batch materialization.
crates/ourios-parquet/src/promoted.rs Adds PromotedClass/PromotedKey, typed fields(), and i64_value/f64_value projections.
crates/ourios-parquet/src/lib.rs Re-exports PromotedClass and PromotedKey.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/ourios-parquet/tests/it/rfc0042_typed_promotion.rs
any::<f64>() draws NaN among its special values, and Some(NaN) !=
Some(NaN) under IEEE equality would falsely fail the totality property.
Compare bits instead — the f64 projection is pass-through and the int
widening deterministic, so exact bit equality is the contract, stricter
than the ==-based assertion it replaces.

Adds a deterministic NaN case alongside (per-fix test obligation): NaN
passes through the f64 projection as a value, not an absence, and
projects NULL under i64 — not left to the generator's draw.

Signed-off-by: Jens Holdgaard Pedersen <jens@holdgaard.org>
@jensholdgaard
jensholdgaard force-pushed the feat/rfc0042-parquet-typed-promotion branch from 729d2f8 to 94fbb24 Compare July 26, 2026 19:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Comment thread crates/ourios-parquet/src/promoted.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
crates/ourios-parquet/tests/it/rfc0042_typed_promotion.rs (1)

223-235: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the composite AnyValue cases in the totality generator.

arb_any_value() says it covers every AnyValue shape, but it only generates absent/primitive values. Since the OTLP value set includes ArrayValue and KvlistValue, add shallow generator arms for them so RFC0042.2 actually exercises promoted projections on structured attribute values.

🤖 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 `@crates/ourios-parquet/tests/it/rfc0042_typed_promotion.rs` around lines 223 -
235, Extend arb_any_value() with shallow proptest arms for AnyValue::ArrayValue
and AnyValue::KvlistValue, while retaining the existing absent and primitive
variants. Generate valid structured values without recursive or unbounded
nesting so the totality generator covers both composite shapes.
🤖 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.

Nitpick comments:
In `@crates/ourios-parquet/tests/it/rfc0042_typed_promotion.rs`:
- Around line 223-235: Extend arb_any_value() with shallow proptest arms for
AnyValue::ArrayValue and AnyValue::KvlistValue, while retaining the existing
absent and primitive variants. Generate valid structured values without
recursive or unbounded nesting so the totality generator covers both composite
shapes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 95c46169-d7cf-41ab-8e34-5c9251071506

📥 Commits

Reviewing files that changed from the base of the PR and between 5a4681a and f69a8f5.

📒 Files selected for processing (9)
  • crates/ourios-parquet/src/lib.rs
  • crates/ourios-parquet/src/promoted.rs
  • crates/ourios-parquet/src/record_batch.rs
  • crates/ourios-parquet/src/writer.rs
  • crates/ourios-parquet/tests/it/main.rs
  • crates/ourios-parquet/tests/it/rfc0042_typed_promotion.rs
  • crates/ourios-server/src/mcp.rs
  • crates/ourios-server/tests/it/rfc0032_query_schema.rs
  • docs/rfcs/0042-typed-numeric-promotion.md

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

crates/ourios-parquet/src/promoted.rs:33

  • PromotedClass is a new public enum and looks likely to grow additional promotion classes over time (e.g., bool/u64/etc). Without #[non_exhaustive], adding a new variant later is a semver-breaking change for downstream exhaustive matches. Mark it #[non_exhaustive] now while the API is new to keep future additive variants non-breaking.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PromotedClass {

@jensholdgaard
jensholdgaard requested a review from Copilot July 26, 2026 20:46
@jensholdgaard
jensholdgaard merged commit 9bc8436 into main Jul 26, 2026
30 checks passed
@jensholdgaard
jensholdgaard deleted the feat/rfc0042-parquet-typed-promotion branch July 26, 2026 20:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Comment on lines +1093 to 1099
for field in promoted.fields() {
if *field.data_type() == arrow_schema::DataType::Float64 {
continue;
}
let path = ColumnPath::new(vec![field.name().clone()]);
builder = builder.set_column_bloom_filter_enabled(path, true);
}
jensholdgaard added a commit that referenced this pull request Jul 26, 2026
…lasses

The FinOps loop's headline query is sum(attr.cost_usd) by attr.model,
but #608's promotion set carries no cost or usage key, so it errors
with the promotion hint. This PR's first draft promoted the keys
bare — and review caught that RFC 0022 §3.1's string-only projection
would have made always-NULL columns and a silently EMPTY sum (worse
than the loud error). That finding became RFC 0042; with its typed
classes now green in-repo (#649-#653), the promotion lands the way it
must: classes matching the AnyValue variants Claude Code actually
emits, verified against captured api_request events —

  - { key: cost_usd, type: f64 }        # doubleValue
  - { key: input_tokens, type: i64 }    # intValue
  - { key: output_tokens, type: i64 }   # intValue

cost_usd_micros is skipped as redundant under f64 aggregation; cache
token counts are additive later if a query wants them.

Cost stays the source's responsibility (Claude Code computes and
stamps it); Ourios stores, attributes, and aggregates. Pricing tables
and FOCUS-shaped output belong to consumers of the query surface.

Verified: the server boots the config through the RFC0042.6 validation
path (fails only on the port the running dogfood server holds).
Promotion is a write-side projection — RFC0042.9's live sum lands with
the first capture after the dogfood server restarts on this config.

Signed-off-by: Jens Holdgaard Pedersen <jens@holdgaard.org>
jensholdgaard added a commit that referenced this pull request Jul 26, 2026
…ice 4) (#653)

RFC0042.8: two input files — the key promoted under the string class
in one epoch, unpromoted in the other — consolidate under the current
i64 declaration into an Int64 column projected from JSON truth (both
epochs' values present), and the rewrite is deterministic:
byte-identical across two identical runs, the RFC0036.4 property under
a fixed config.

The RFC banner records where the ladder stands: .1-.8 green in-repo
across #649-#652 + this test; RFC0042.9 (the dogfood corpus gate)
outstanding until the first agent capture under the typed set, so the
status stays specified with the implementation ahead of the flip.

Verified: cargo fmt --check, clippy clean, nextest 165/165 (parquet),
mdbook build.

Signed-off-by: Jens Holdgaard Pedersen <Jens@holdgaard.org>
jensholdgaard added a commit that referenced this pull request Jul 26, 2026
)

* feat(dogfood): promote cost_usd + token counts under RFC 0042 typed classes

The FinOps loop's headline query is sum(attr.cost_usd) by attr.model,
but #608's promotion set carries no cost or usage key, so it errors
with the promotion hint. This PR's first draft promoted the keys
bare — and review caught that RFC 0022 §3.1's string-only projection
would have made always-NULL columns and a silently EMPTY sum (worse
than the loud error). That finding became RFC 0042; with its typed
classes now green in-repo (#649-#653), the promotion lands the way it
must: classes matching the AnyValue variants Claude Code actually
emits, verified against captured api_request events —

  - { key: cost_usd, type: f64 }        # doubleValue
  - { key: input_tokens, type: i64 }    # intValue
  - { key: output_tokens, type: i64 }   # intValue

cost_usd_micros is skipped as redundant under f64 aggregation; cache
token counts are additive later if a query wants them.

Cost stays the source's responsibility (Claude Code computes and
stamps it); Ourios stores, attributes, and aggregates. Pricing tables
and FOCUS-shaped output belong to consumers of the query surface.

Verified: the server boots the config through the RFC0042.6 validation
path (fails only on the port the running dogfood server holds).
Promotion is a write-side projection — RFC0042.9's live sum lands with
the first capture after the dogfood server restarts on this config.

Signed-off-by: Jens Holdgaard Pedersen <jens@holdgaard.org>

* docs(dogfood): a wrong-class promotion yields a NULL aggregate, not an empty result

The comment claimed sum over an always-NULL column silently returns
empty; the RFC0002.18 semantics is a NULL aggregate (every input
excluded, value null). Same silence, precise shape.

Signed-off-by: Jens Holdgaard Pedersen <Jens@holdgaard.org>

---------

Signed-off-by: Jens Holdgaard Pedersen <jens@holdgaard.org>
Signed-off-by: Jens Holdgaard Pedersen <Jens@holdgaard.org>
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.

2 participants