Skip to content

feat: alias-index write path v1 — alias events persisted + querier-derived map (#148, per #183) - #184

Merged
jensholdgaard merged 7 commits into
mainfrom
feat/148-alias-index-write-path-v1
Jun 12, 2026
Merged

jensholdgaard merged 7 commits into
mainfrom
feat/148-alias-index-write-path-v1

Conversation

@jensholdgaard

@jensholdgaard jensholdgaard commented Jun 12, 2026

Copy link
Copy Markdown
Owner

Implements the #148 alias-index write path v1, spec-first to the just-merged #183 amendment (RFC 0005 §3.7 / §3.7.1) + RFC 0001 §6.7. Four phased commits, each independently gated.

What lands

1. ourios-parquet — write side (RFC 0005 §3.7 amendment 2026-06-12)

  • Three new OPTIONAL columns: alias_representative_id (UInt64), alias_member_ids (LIST<UInt64>), alias_actor (Utf8). AliasAsserted / AliasRetracted map to event_kind 4 / 5 with the paired canonical event_type strings (ordinals imported from ourios-core, never redefined).
  • member_ids stored verbatim (no sort/dedup — the semantic value is the set {representative_id} ∪ member_ids, folded by consumers); empty list is valid and distinct from NULL; in-memory empty-string reason ↔ on-disk NULL ("" ↔ NULL).
  • §3.7 encoding-policy rows applied: alias_representative_id keeps the page-index default (same shape as template_id); alias_member_ids list leaf + alias_actor downgraded to chunk stats.
  • The interim AliasEventNotYetPersistable writer rejection is retired per the amendment.

2. ourios-parquet / ourios-core — read side

  • The reader decodes kinds 4/5 back into the payloads (non-null required-by-convention; an empty alias_actor is a loud writer-invariant error — aliasing is never anonymous, RFC 0001 §6.7).
  • The UnknownEventKind hard error is replaced by the now-pinned §3.7 tolerance rule: an ordinal above the known range surfaces as the new envelope-only AuditPayload::Unknown { event_kind, event_type } variant (the ParamType::Unknown discipline applied to the kind enum), preserved verbatim on read-then-write. Folds over named kinds (AliasMap::apply, the RFC 0010 drift filter) ignore it by construction — the compiler enforces the new arm at every exhaustive match site.

3. ourios-querier — the §3.7.1 v1 derivation

  • drift.rs's tenant-rooted audit walk (canonical-path tenant-escape backstop, canonical de-dup, conservative day prune) is extracted into a shared audit_scan module; the day window is now optional and the file set returns in lexicographic path order.
  • New alias_store::derive_alias_map: scans the tenant's audit/ partition root (no window — the fold covers the whole alias history; alias events are rare operator actions, §3.7.1), filters kinds 4/5, orders events by the §3.7.1 total order (timestamp, file path lexicographic, within-file row index) — sorted file walk + in-file row order under a stable sort by timestamp — and folds via ourios_core::alias::AliasMap::from_events (RFC 0001 §6.7 semantics owned there, not restated).
  • A row claiming another tenant under the tenant's partition root is a loud Storage error, not a silent drop.

4. Wiring choice (called out per the issue): run_query's alias parameter becomes Option<&AliasMap>None (the production default) selects the §3.7.1 storage derivation, skipped entirely when the query has no resolves_to; Some(map) remains the test/operator override that bypasses storage. This was the most natural fit for compile.rs's structure: compile() keeps taking a plain &AliasMap, and run_query resolves the source before compiling. Existing test call sites were updated mechanically (Some(&…) wrapping) — no assertion changes.

Tests (RFC0005.14 + #148 step 3)

  • (a) Round-trip through the real AuditWriterAuditReader: member set verbatim (order + duplicate preserved), empty-list retraction ≠ NULL, actor, "" ↔ NULL reason, plus raw-column NULL discipline per kind (§3.8 rule 6).
  • (b) Derived-map fold: assert→retract by event time, and both directions of the cross-file same-timestamp tiebreak — two single-event files renamed to a.parquet / b.parquet so identical timestamps force the outcome onto the file-path component of the total order; the mirrored case proves the tiebreak (not luck) decides.
  • (c) Tenant isolation: tenant B's stored alias events never fold into A's derived map.
  • (d) Storage-backed RFC0002.9: the assertion is written via the production ParquetAuditSink, run_query(…, None) derives the map end-to-end — resolves_to(A) returns A ∪ {B} while template_id == A stays exactly A. The injected-map RFC0002.9 test stays alongside, unweakened.

RFC-gated test flips (CLAUDE.md §6.2 — explicit approval surface)

Two existing tests asserted exactly the behaviour the #183 amendment removes, and flip rather than weaken (each cites the amendment in its doc comment):

  1. alias_events_are_rejected_pending_the_rfc_0005_splitalias_events_build_a_batch_with_kinds_4_and_5 (the writer rejection is retired by the amendment).
  2. The forged-ordinal reader test: expect-UnknownEventKind-error → expect-opaque-AuditPayload::Unknown (the documented deferral was "until a real new variant lands"; kinds 4–5 are that variant and the amendment pins the tolerance rule). An Unknown round-trip test lands alongside.

Invariants / hazards addressed

  • §3.1-adjacent (no silent merges / audited aliases): the alias mechanism stays operator-driven and audited end-to-end — this PR makes the audited events durable in Parquet and the querier's expansion derived from that durable stream, closing the gap where resolves_to only saw an in-memory injected map. Nothing is auto-inferred; bare template_id == never follows alias chains (pinned by tests).
  • §3.5 (schema changes need a migration plan): §3.8 rule 1 (additive OPTIONAL columns — old files read back None, no migration) plus the rule 6 required-by-convention conventions for kinds 4–5, writer-enforced and test-pinned. The schema-pin fixture (RFC0005.10) moves in lockstep. Reader forward-compat is strengthened (unknown ordinals no longer fail files).
  • §3.7 (multi-tenancy): derivation is partition-rooted per tenant with the canonical-path escape backstop and a row-level tenant check; isolation pinned at the storage layer by test (c).
  • H5 (template schema evolution / explicit alias mechanism): this is the storage+query half of H5's alias mechanism; drift detection (RFC 0010) is untouched and its walk now shares one audited code path.

Verification (per commit and at HEAD)

cargo test --all-features (exit 0, 0 failed), cargo fmt --all --check, cargo clippy --all-targets --all-features -- -D warnings, cargo doc --workspace --no-deps --all-features, cargo bench -p ourios-bench --no-run.

Closes the storage/querier slices of #148 (steps 2–3); per #183.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Alias events are now persisted to audit storage and included in the audit schema.
    • Queries may derive alias maps from audit streams when needed; callers can optionally supply a map.
    • Audit file discovery supports tenant-scoped listing with time-window pruning and deterministic ordering.
  • Bug Fixes

    • Unknown/unsupported audit event kinds are tolerated on read and round-trip preserved rather than failing.
  • Tests

    • Expanded round-trip, schema, derivation, pruning, tenant-isolation, and invalid-input tests.

…RFC 0005 §3.7, #148)

Adds the three OPTIONAL alias columns the 2026-06-12 amendment pins
(alias_representative_id, alias_member_ids LIST<UInt64> with the
empty-list-vs-NULL distinction, alias_actor), maps AliasAsserted /
AliasRetracted to event_kind 4 / 5 with member_ids stored verbatim and
the "" <-> NULL reason rule, applies the §3.7 encoding-policy rows for
the new columns, and retires the writer's interim
AliasEventNotYetPersistable rejection. Schema change is §3.8 rule 1
(additive OPTIONAL); the schema-pin fixture moves in lockstep. The
colocated expect-error test flips per the RFC-gated contract change
(CLAUDE.md §6.2), citing the amendment in its doc comment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jensholdgaard
jensholdgaard requested a review from Copilot June 12, 2026 09:06
@jensholdgaard

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@jensholdgaard, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 18 minutes and 26 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1e31310a-0a6b-481d-b213-006d1d644ff3

📥 Commits

Reviewing files that changed from the base of the PR and between 2043f85 and 7a0bd34.

📒 Files selected for processing (2)
  • crates/ourios-parquet/src/audit_reader.rs
  • crates/ourios-querier/src/lib.rs
📝 Walkthrough

Walkthrough

Adds alias-event persistence and tolerant unknown-event reads; extends audit Parquet schema/writers/readers for alias fields; introduces shared audit-file discovery; derives alias maps from audit stream at compile time when queries use resolves_to; makes Querier::run_query accept an optional alias_map and updates tests.

Changes

Alias Event Persistence and Derivation

Layer / File(s) Summary
Core audit payload: Unknown and alias variants
crates/ourios-core/src/audit.rs, crates/ourios-core/src/alias.rs
AuditPayload::Unknown { event_kind: u8, event_type: String } added; event_type(&self) -> &str now returns stored string for Unknown; counts_as_merge treats Unknown as non-merge; AliasMap::apply ignores Unknown during replay folding.
Parquet schema and column definitions
crates/ourios-parquet/src/lib.rs
Adds audit_columns constants ALIAS_REPRESENTATIVE_ID, ALIAS_MEMBER_IDS, ALIAS_ACTOR and appends optional alias_representative_id: UInt64, alias_member_ids: List<UInt64>, alias_actor: Utf8 to audit_schema().
Parquet writer: record-batch, serialization, and stats
crates/ourios-parquet/src/audit_record_batch.rs, crates/ourios-parquet/src/audit_writer.rs, tests
Removes alias rejection error; serializes AliasAsserted/AliasRetracted into alias_* columns, preserves verbatim member_ids list (empty list vs NULL), maps empty-string reason ↔ NULL on disk, adds alias column builders and finish order, and adjusts writer statistics/page-index rules for alias columns. Tests updated to round-trip alias rows and per-column NULL discipline.
Parquet reader: alias decoding and unknown tolerance
crates/ourios-parquet/src/audit_reader.rs
Reader no longer errors on unknown event_kind; constructs AuditPayload::Unknown with preserved event_type; refactors payload decoding into decode_compaction_payload/decode_alias_payload; adds optional_u64_list to preserve NULL vs empty list distinctions; updates tests accordingly.
Shared audit file discovery with window pruning
crates/ourios-querier/src/audit_scan.rs
New audit_files(bucket_root, tenant, window) walks tenant audit subtree, optionally prunes day= partitions conservatively, canonicalizes/validates tenant root, rejects escapes, deduplicates, and returns deterministic lexicographic paths. Includes unit tests for pruning and conservative behavior.
Alias-map derivation from audit stream
crates/ourios-querier/src/alias_store.rs
Adds derive_alias_map(bucket_root, tenant) that reads audit files, maps storage errors, validates per-row tenant_id for alias rows, filters to asserted/retracted events, sorts by timestamp, and folds them into an AliasMap.
Query API and compile integration
crates/ourios-querier/src/compile.rs, crates/ourios-querier/src/lib.rs, crates/ourios-querier/src/drift.rs
Adds validate(...) and uses_resolves_to(...); changes Querier::run_query(..., alias_map: Option<&AliasMap>, ...) to validate before derivation, derive the alias map only when resolves_to is used (or use empty map), delegates audit-file discovery to shared audit_scan::audit_files, and updates docs.
Round-trip and integration tests
crates/ourios-parquet/tests/*, crates/ourios-querier/tests/*
Adds audit_round_trip alias event test, updates schema pin fixture, updates many query tests to pass Some(&map) or None, and adds RFC0005.14 storage-backed alias derivation tests (folding, cross-file tiebreaks, tenant isolation, symlink rejection, validation precedence).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • jensholdgaard/ourios#183: Implements RFC0005 alias-event persistence and unknown-event tolerance described in related RFC updates.
  • jensholdgaard/ourios#153: Earlier alias-event / AliasMap replay work that this PR builds upon for persistence and query-time derivation.
  • jensholdgaard/ourios#46: Prior work touching audit reader event_kind handling; related to the reader tolerance and decoding changes here.

Poem

🐇 I hop through audits, columns bright and new,
Unknown kinds I carry, safely through the queue,
Writers store the lists, reasons wink to NULL,
Readers round-trip faithfully — the fold stays calm and full,
Queries fetch the map from the stream, and spring returns to view.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately describes the main change: implementing alias-index write path v1 with alias events persisted to Parquet and a querier-derived alias map, directly matching the four phased commits and overall PR objective.
Description check ✅ Passed The PR description comprehensively covers all required template sections: a detailed summary of what lands, related issue references (#148, #183, RFC citations), and a complete checklist. The description is specific, well-structured, and clearly explains the implementation across all four commits.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/148-alias-index-write-path-v1

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 and usage tips.

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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 RFC 0005 §3.7 / §3.7.1 alias-index v1 end-to-end: persist alias audit events in Parquet, make the reader forward-compatible with unknown event_kinds, and derive the per-tenant AliasMap in the querier when resolves_to(...) is used.

Changes:

  • Extend the audit Parquet schema with alias_* columns and implement writer/reader support for kinds 4–5 (alias_asserted / alias_retracted), including "" ↔ NULL for reason.
  • Replace unknown-ordinal hard-failure with AuditPayload::Unknown { event_kind, event_type } and preserve the envelope on read-then-write.
  • Add querier-side storage derivation of the alias map (scanning audit/tenant_id=...), and wire Querier::run_query(..., alias_map: Option<&AliasMap>) so None triggers derivation only when needed.

Reviewed changes

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

Show a summary per file
File Description
crates/ourios-querier/tests/rfc0005_14_alias_derivation.rs New integration tests pin §3.7.1 derived-map ordering and tenant isolation.
crates/ourios-querier/tests/rfc0005_13.rs Update run_query calls for the new Option<&AliasMap> parameter.
crates/ourios-querier/tests/rfc0002_dsl.rs Update run_query calls; add storage-backed RFC0002.9 resolves_to test using derived map.
crates/ourios-querier/tests/rfc0001_time_preserved.rs Update run_query call signature for alias map injection.
crates/ourios-querier/tests/rfc0001_query_semantics.rs Update run_query call signature for alias map injection.
crates/ourios-querier/src/lib.rs Wire alias_map: Option<&AliasMap> and derive from storage when resolves_to is present.
crates/ourios-querier/src/drift.rs Refactor drift audit-file discovery to use shared audit_scan.
crates/ourios-querier/src/compile.rs Add uses_resolves_to helper to avoid unnecessary alias derivation scans.
crates/ourios-querier/src/audit_scan.rs New shared audit subtree walk: tenant isolation, canonical-path backstop, optional day-prune, lexicographic file ordering.
crates/ourios-querier/src/alias_store.rs New v1 alias-map derivation by scanning and folding alias events with §3.7.1 total order.
crates/ourios-parquet/tests/schema_pin.rs Update pinned audit schema field list to include alias_* columns.
crates/ourios-parquet/tests/audit_round_trip.rs Add RFC0005.14 round-trip coverage for alias events and raw-column NULL discipline.
crates/ourios-parquet/src/lib.rs Add audit_columns::ALIAS_* constants and extend audit_schema() with new optional fields.
crates/ourios-parquet/src/audit_writer.rs Apply §3.7 encoding policy for alias_actor and alias_member_ids list leaf stats/page-index behavior.
crates/ourios-parquet/src/audit_record_batch.rs Retire alias-event rejection; encode kinds 4–5 into new columns; support writing AuditPayload::Unknown envelope-only.
crates/ourios-parquet/src/audit_reader.rs Decode kinds 4–5; add unknown-ordinal tolerance via AuditPayload::Unknown; add LIST<UInt64> decoding for alias_member_ids.
crates/ourios-core/src/audit.rs Add AuditPayload::Unknown and adjust event_type() to return &str (preserve unknown strings).
crates/ourios-core/src/alias.rs Ensure alias fold ignores AuditPayload::Unknown by construction.

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

Comment thread crates/ourios-querier/src/alias_store.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.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/ourios-parquet/src/audit_reader.rs (1)

301-324: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Defer OPTIONAL payload decoding until after event_kind dispatch.

Lines 301-324 still parse every payload group up front, so the unknown-kind fallback on Lines 374-380 is not actually sufficient. A pre-amendment file that omits OPTIONAL columns like positions_widened / slots_expanded still hard-fails before row dispatch, and a future unknown kind can also fail if irrelevant payload columns contain data this reader doesn't understand. That breaks the reader-compat contract this change is trying to introduce.

Please gate payload decoding by the named kind being reconstructed, or make the optional column decoders lazy/non-failing until a row actually needs that column. As per coding guidelines, "Parquet readers MUST handle absent columns (old files) and unknown columns (future files) without error."

Also applies to: 374-380

🤖 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/src/audit_reader.rs` around lines 301 - 324, The code
eagerly decodes OPTIONAL payload columns (e.g., positions_widened_lists via
decode_positions_column, slots_expanded_lists via decode_slots_column,
optional_string/optional_u64/optional_u32 calls that set template_id,
old_version, new_version, old_template, new_template, triggering_line_hash,
reason, compaction_*, alias_*) before dispatching on event_kind, causing
failures for pre-amendment or future files; fix by deferring or making lazy
those decodes: move the payload-specific decoders into the event_kind match arms
so you only call decode_positions_column, decode_slots_column and the optional_*
decoders when reconstructing kinds that actually need them, or change those
decoders to return a non-failing lazy wrapper/closure (e.g., Option<Result<...>>
or a thunk) that only performs parsing when accessed; ensure the unknown-kind
fallback remains reachable and that optional_* helpers tolerate missing columns
until invoked.

Source: Coding guidelines

🧹 Nitpick comments (1)
crates/ourios-querier/src/alias_store.rs (1)

40-83: ⚡ Quick win

Add colocated unit tests for the ordering and tenant-mismatch contract.

This module owns the §3.7.1 ordering glue plus the row-level tenant backstop, but it has no in-file tests. Please add a small #[cfg(test)] block here that pins same-timestamp ordering and foreign-tenant rejection next to the implementation.

As per coding guidelines, crates/**/src/**/*.rs: Unit tests must be colocated next to the code for anything non-trivial.

🤖 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-querier/src/alias_store.rs` around lines 40 - 83, Add a
#[cfg(test)] mod tests next to derive_alias_map that exercises the ordering and
tenant-backstop: create temporary audit files under a temp bucket_root and
tenant partition (use audit_scan::audit_files expectations and
AuditReader::open_file/read_all format) — one test writes two files with
AliasAsserted events that share the same timestamp but are created in a
lexicographic file order so calling derive_alias_map(bucket_root, tenant) yields
an AliasMap whose events preserve file+row ordering (assert the resulting
AliasMap ordering or resulting alias resolution); another test writes a file
containing an AuditEvent whose tenant_id != tenant and asserts derive_alias_map
returns Err(QueryError::Storage) with a message mentioning the offending path;
keep tests colocated in the same file and use
AliasMap::from_events/derive_alias_map/AuditReader symbols to construct and
validate behavior.

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.

Inline comments:
In `@crates/ourios-querier/src/audit_scan.rs`:
- Around line 48-49: The tenant_dir path may be a symlink allowing traversal
outside the intended audit tree; before walking or canonicalizing it, resolve
and verify its real location by canonicalizing the bucket_root.join("audit")
tree and the tenant_dir (or explicitly reject if symlink_metadata shows
tenant_dir is a symlink), then ensure the canonicalized tenant_dir starts_with
the canonicalized audit root; update the logic around the tenant_dir variable
and any later starts_with checks (affecting the block that canonicalizes
tenant_root and the run_drift caller) to use the canonical audit root or reject
symlinked tenant roots up front.

In `@crates/ourios-querier/src/lib.rs`:
- Around line 501-515: The code derives an alias map
(alias_store::derive_alias_map) before letting compile::compile perform query
validation/window-resolution, which can cause unnecessary audit-tree access for
invalid queries; change the control flow so you validate the query first (call a
validation/preflight function on compile—e.g. extract and call a
compile::validate_query or a new compile::preflight_validate that performs
unsupported-stage and window-resolution checks with tenant, now_unix_nano and
default_window_nanos) and only when that validation succeeds proceed to call
alias_store::derive_alias_map when compile::uses_resolves_to(&query.predicate)
is true, then pass the derived or empty AliasMap into compile::compile; if
compile::compile currently contains validation logic, split that
validation/window-resolution into a callable helper and invoke it before
derive_alias_map.

In `@crates/ourios-querier/tests/rfc0005_14_alias_derivation.rs`:
- Around line 203-219: The test currently can't prove T2's derived alias map was
applied because T2 only has an A data row; add a data row in T2 for the aliased
class (e.g., a B row) so that after writing the audit event with
write_audit_file_named(alias_asserted(...)) the
resolves_to_a_rows(bucket.path(),"T2").await will reflect folding (expect count
>1). Update the fixture by calling write_all or write_data_rows for T2 to
include the extra B row and change the assertion on resolves_to_a_rows for "T2"
accordingly so the test fails if alias derivation isn't applied.

---

Outside diff comments:
In `@crates/ourios-parquet/src/audit_reader.rs`:
- Around line 301-324: The code eagerly decodes OPTIONAL payload columns (e.g.,
positions_widened_lists via decode_positions_column, slots_expanded_lists via
decode_slots_column, optional_string/optional_u64/optional_u32 calls that set
template_id, old_version, new_version, old_template, new_template,
triggering_line_hash, reason, compaction_*, alias_*) before dispatching on
event_kind, causing failures for pre-amendment or future files; fix by deferring
or making lazy those decodes: move the payload-specific decoders into the
event_kind match arms so you only call decode_positions_column,
decode_slots_column and the optional_* decoders when reconstructing kinds that
actually need them, or change those decoders to return a non-failing lazy
wrapper/closure (e.g., Option<Result<...>> or a thunk) that only performs
parsing when accessed; ensure the unknown-kind fallback remains reachable and
that optional_* helpers tolerate missing columns until invoked.

---

Nitpick comments:
In `@crates/ourios-querier/src/alias_store.rs`:
- Around line 40-83: Add a #[cfg(test)] mod tests next to derive_alias_map that
exercises the ordering and tenant-backstop: create temporary audit files under a
temp bucket_root and tenant partition (use audit_scan::audit_files expectations
and AuditReader::open_file/read_all format) — one test writes two files with
AliasAsserted events that share the same timestamp but are created in a
lexicographic file order so calling derive_alias_map(bucket_root, tenant) yields
an AliasMap whose events preserve file+row ordering (assert the resulting
AliasMap ordering or resulting alias resolution); another test writes a file
containing an AuditEvent whose tenant_id != tenant and asserts derive_alias_map
returns Err(QueryError::Storage) with a message mentioning the offending path;
keep tests colocated in the same file and use
AliasMap::from_events/derive_alias_map/AuditReader symbols to construct and
validate behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ca6b2570-df30-481e-b467-adde0f902446

📥 Commits

Reviewing files that changed from the base of the PR and between fa66a6a and 507b3e3.

📒 Files selected for processing (18)
  • crates/ourios-core/src/alias.rs
  • crates/ourios-core/src/audit.rs
  • crates/ourios-parquet/src/audit_reader.rs
  • crates/ourios-parquet/src/audit_record_batch.rs
  • crates/ourios-parquet/src/audit_writer.rs
  • crates/ourios-parquet/src/lib.rs
  • crates/ourios-parquet/tests/audit_round_trip.rs
  • crates/ourios-parquet/tests/schema_pin.rs
  • crates/ourios-querier/src/alias_store.rs
  • crates/ourios-querier/src/audit_scan.rs
  • crates/ourios-querier/src/compile.rs
  • crates/ourios-querier/src/drift.rs
  • crates/ourios-querier/src/lib.rs
  • crates/ourios-querier/tests/rfc0001_query_semantics.rs
  • crates/ourios-querier/tests/rfc0001_time_preserved.rs
  • crates/ourios-querier/tests/rfc0002_dsl.rs
  • crates/ourios-querier/tests/rfc0005_13.rs
  • crates/ourios-querier/tests/rfc0005_14_alias_derivation.rs

Comment thread crates/ourios-querier/src/audit_scan.rs
Comment thread crates/ourios-querier/src/lib.rs
Comment thread crates/ourios-querier/tests/rfc0005_14_alias_derivation.rs Outdated
jensholdgaard and others added 5 commits June 12, 2026 12:12
…e envelope (RFC 0005 §3.7)

The reader rebuilds AliasAsserted / AliasRetracted payloads from the
alias_* columns (empty member list != NULL; NULL reason decodes to the
in-memory empty string; an empty actor is a writer-invariant error —
aliasing is never anonymous, RFC 0001 §6.7).

The UnknownEventKind hard error is replaced by the now-pinned §3.7
tolerance rule: an ordinal above the known range surfaces as the new
envelope-only AuditPayload::Unknown variant (the ParamType::Unknown
discipline applied to the kind enum), preserved verbatim on
read-then-write; folds over named kinds (AliasMap::apply, the drift
filter) ignore it by construction. The forged-ordinal reader test
flips from expect-error to expect-opaque-event per the RFC-gated
contract change (#183, CLAUDE.md §6.2), with a round-trip test
alongside.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…_scan

Pure relocation of drift.rs's tenant-rooted audit-tree walk (canonical
escape backstop, canonical-path de-dup, conservative day prune) into a
shared module, with the day window now optional and the resolved file
set sorted lexicographically — the file-path component of the RFC 0005
§3.7.1 total fold order the alias-map derivation needs next. The two
prune unit tests move with the helpers, unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… 0005 §3.7.1, #148)

run_query's alias parameter becomes Option<&AliasMap>: None — the
production default — folds the requesting tenant's map from its
audit/ partition at compile time (alias_store::derive_alias_map),
reading kinds 4/5 via AuditReader in the §3.7.1 total order
(timestamp, file path lexicographic, within-file row index — the
shared walk's sorted file set + in-file row order under a stable
sort by timestamp) and handing the events to
ourios-core::alias::AliasMap::from_events, whose RFC 0001 §6.7
semantics this does not restate. Some(map) stays the test/operator
override, bypassing storage. Queries with no resolves_to skip the
audit scan entirely. A row claiming another tenant under the
tenant's partition root is a loud Storage error, not a silent drop
(CLAUDE.md §3.7). Test updates are mechanical Option-wrapping at the
call sites; no assertion changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(a) round-trip through the real audit writer/reader: member set
verbatim (order + duplicate preserved), empty-list retraction != NULL,
actor, the "" <-> NULL reason rule, and raw-column NULL discipline per
kind (§3.8 rule 6). (b) the §3.7.1 derived fold: assert-then-retract by
event time, plus both directions of the cross-file same-timestamp
tiebreak — one event per file, files renamed into a crafted
lexicographic order so the outcome is decided by the file-path
component of the total order and nothing else. (c) tenant isolation:
a second tenant's stored alias events never fold into the requesting
tenant's derived map (CLAUDE.md §3.7). (d) storage-backed RFC0002.9:
the assertion is written through ParquetAuditSink and run_query(None)
derives the map end-to-end — resolves_to(A) returns A ∪ {B} while
template_id == A stays exactly A; the injected-map RFC0002.9 test
stays alongside, unweakened.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…efore alias derivation

Review round 1:
- a symlinked audit/tenant_id=... root is now rejected outright (the
  per-file starts_with backstop trusted the symlink's own resolution)
- run_query validates the query before paying the alias-derivation IO,
  so compile errors precede Storage errors deterministically
- tenant-mismatch error renders tenant IDs with Display, not Debug
- tests: symlinked-root rejection, error precedence, and the T2
  isolation test now proves its own map was derived (resolves_to == 2)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jensholdgaard
jensholdgaard force-pushed the feat/148-alias-index-write-path-v1 branch from 507b3e3 to 2043f85 Compare June 12, 2026 10:17

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/ourios-querier/src/compile.rs (1)

93-145: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

validate only enforces part of the new pre-derivation error contract.

Lines 99-135 reject unsupported stages and resolve the window/limit, but predicate-level compile failures still happen later in paths like string_call_column, attr_match, and column_comparison. A query such as resolves_to(10) and trace_id contains "x" will pass validate, hit uses_resolves_to, derive the alias map from storage, and only then fail as InvalidQuery. If run_query is meant to guarantee “invalid query before alias-scan IO,” this needs a pure predicate-validation pass folded into validate, not just stage validation.

🤖 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-querier/src/compile.rs` around lines 93 - 145, validate
currently only checks stages/window/limit but lets predicate-level errors slip
through until alias-map derivation; add a pure predicate validation pass inside
validate that walks the query predicates and runs the same checks used by
string_call_column, attr_match, column_comparison, and uses_resolves_to (or
extract their pure parts into non-IO helpers) so any predicate that would later
produce QueryError::InvalidQuery is caught before alias lookup; ensure these new
validation helpers do not touch storage/alias_map and call them from validate
before resolving window/limit return.
♻️ Duplicate comments (1)
crates/ourios-querier/src/audit_scan.rs (1)

48-52: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate the tenant root before the walk can return successfully.

Line 51 seeds the traversal with tenant_dir before it has been checked against the canonical bucket root, and Line 84 can return Ok([]) before Lines 93-116 ever run. That means a symlinked audit/tenant_id=<enc> can still masquerade as a normal empty scan when no .parquet files are found (or the window prunes them all), and run_drift will treat that as “no data” instead of a tenancy failure. Move the canonical tenant-root check ahead of the walk/early return so the scan fails before any foreign subtree is traversed or silently accepted. As per coding guidelines, Every code path that touches data must take a tenant ID; every Parquet file partitioned by tenant; every template tree scoped per tenant.

Also applies to: 84-116

🤖 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-querier/src/audit_scan.rs` around lines 48 - 52, The tenant
directory is seeded into the traversal before it is validated against the
canonical bucket root, allowing a symlinked audit/tenant_id=<enc> to be treated
as empty; move and perform the canonical-tenant-root check for tenant_dir (the
path built from bucket_root.join("audit").join(format!("tenant_id={enc}")))
immediately after construction and before creating stack/files or returning
early (before the logic that can return Ok([])); if the canonicalized tenant_dir
is not under the canonical bucket_root, return an error so the subsequent walk
(stack loop and the code that collects .parquet files/filters used by run_drift)
never proceeds on an unvalidated subtree.

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 `@crates/ourios-querier/src/compile.rs`:
- Around line 93-145: validate currently only checks stages/window/limit but
lets predicate-level errors slip through until alias-map derivation; add a pure
predicate validation pass inside validate that walks the query predicates and
runs the same checks used by string_call_column, attr_match, column_comparison,
and uses_resolves_to (or extract their pure parts into non-IO helpers) so any
predicate that would later produce QueryError::InvalidQuery is caught before
alias lookup; ensure these new validation helpers do not touch storage/alias_map
and call them from validate before resolving window/limit return.

---

Duplicate comments:
In `@crates/ourios-querier/src/audit_scan.rs`:
- Around line 48-52: The tenant directory is seeded into the traversal before it
is validated against the canonical bucket root, allowing a symlinked
audit/tenant_id=<enc> to be treated as empty; move and perform the
canonical-tenant-root check for tenant_dir (the path built from
bucket_root.join("audit").join(format!("tenant_id={enc}"))) immediately after
construction and before creating stack/files or returning early (before the
logic that can return Ok([])); if the canonicalized tenant_dir is not under the
canonical bucket_root, return an error so the subsequent walk (stack loop and
the code that collects .parquet files/filters used by run_drift) never proceeds
on an unvalidated subtree.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 84ab8b35-7340-4245-8331-c2a4bf11d05f

📥 Commits

Reviewing files that changed from the base of the PR and between 507b3e3 and 2043f85.

📒 Files selected for processing (15)
  • crates/ourios-core/src/alias.rs
  • crates/ourios-core/src/audit.rs
  • crates/ourios-parquet/src/audit_reader.rs
  • crates/ourios-parquet/src/audit_record_batch.rs
  • crates/ourios-parquet/tests/audit_round_trip.rs
  • crates/ourios-querier/src/alias_store.rs
  • crates/ourios-querier/src/audit_scan.rs
  • crates/ourios-querier/src/compile.rs
  • crates/ourios-querier/src/drift.rs
  • crates/ourios-querier/src/lib.rs
  • crates/ourios-querier/tests/rfc0001_query_semantics.rs
  • crates/ourios-querier/tests/rfc0001_time_preserved.rs
  • crates/ourios-querier/tests/rfc0002_dsl.rs
  • crates/ourios-querier/tests/rfc0005_13.rs
  • crates/ourios-querier/tests/rfc0005_14_alias_derivation.rs
🚧 Files skipped from review as they are similar to previous changes (12)
  • crates/ourios-core/src/alias.rs
  • crates/ourios-querier/tests/rfc0001_time_preserved.rs
  • crates/ourios-parquet/tests/audit_round_trip.rs
  • crates/ourios-querier/tests/rfc0005_13.rs
  • crates/ourios-querier/src/alias_store.rs
  • crates/ourios-querier/tests/rfc0001_query_semantics.rs
  • crates/ourios-querier/tests/rfc0002_dsl.rs
  • crates/ourios-querier/src/lib.rs
  • crates/ourios-querier/src/drift.rs
  • crates/ourios-parquet/src/audit_reader.rs
  • crates/ourios-parquet/src/audit_record_batch.rs
  • crates/ourios-core/src/audit.rs

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 18 out of 18 changed files in this pull request and generated 2 comments.

Comment thread crates/ourios-querier/src/lib.rs Outdated
Comment thread crates/ourios-parquet/src/audit_reader.rs
…row index in messages

Copilot round 2, wording only: the run_query comment no longer implies
all invalid-query errors precede the audit scan (predicate compilation
needs the map), and the non-nullable-element conversion messages say
"batch row" — the index is batch-local, not file-global.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jensholdgaard
jensholdgaard merged commit fa646a3 into main Jun 12, 2026
11 checks passed
@jensholdgaard
jensholdgaard deleted the feat/148-alias-index-write-path-v1 branch June 12, 2026 11:03
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