Skip to content

feat(querier): execute minimal tenant/time/template queries via DataFusion (slice 1) - #87

Merged
jensholdgaard merged 7 commits into
mainfrom
feat/querier-execution-min
Jun 2, 2026
Merged

feat(querier): execute minimal tenant/time/template queries via DataFusion (slice 1)#87
jensholdgaard merged 7 commits into
mainfrom
feat/querier-execution-min

Conversation

@jensholdgaard

@jensholdgaard jensholdgaard commented Jun 1, 2026

Copy link
Copy Markdown
Owner

What

Slice 1 of the querier execution pathQuerier::run now genuinely executes against the RFC 0005 Parquet store via DataFusion. This is the first time the read path runs end-to-end, and it's the start of answering the query thesis (B1/B2) that actually justifies the architecture.

Per the maintainer decision, the query surface is throwaway (minimal predicates, no RFC 0002 DSL) until B1/B2 prove it's worth a stable language.

How it works

  • Roots a DataFusion ListingTable at the request tenant's data/tenant_id=<enc>/ dir — using ourios-parquet's canonical percent_encode_tenant so the path matches the writer byte-for-byte. Tenant isolation is structural (RFC0007.5): no other tenant's files are reachable.
  • year/month/day/hour are declared as path-only Hive partition cols; tenant_id stays a plain file column relative to that root (resolving the "name is both partition + file column" conflict).
  • Filters: time_unix_nano half-open range (typed as Timestamp(ns, UTC) to match the schema) + template_id equality; collects; returns the matching-row count.
  • §4.6 boundary held: no datafusion/arrow/SQL type in any public signature; datafusion errors map to QueryError::Storage.

API (throwaway surface)

QueryRequest gains template_id: Option<u64>; QueryResult gains rows: u64; Querier::new(bucket_root).

Deps

datafusion 53 is now a normal dep (pulls arrow/parquet 58 alongside ourios-parquet's 55). Harmless: the two never meet in memory — DataFusion reads Parquet files from disk that ourios-parquet wrote. ourios-parquet promoted to a normal dep for percent_encode_tenant.

Tests (live)

tests/execution.rs, writing fixtures via ourios_parquet::Writer:

  • executes_and_counts_matching_rows — tenant + template-exact + half-open time-range filtering all return correct counts.
  • rfc0007_5_tenant_isolation — each tenant sees only its rows; an unknown tenant is an empty result, not an error.

RFC0007.5 flipped from #[ignore] stub to live. Workspace: 329 passed / 0 failed / 48 ignored; fmt/clippy/mdbook green.

Next slices

  • Slice 2 (B1): extract row_groups_scanned/pruned + bytes_read from the DataFusion ParquetExec metrics into QueryStats, flip RFC0007.1 live.
  • Slice 3 (B2): wire the latency-vs-corpus-size bench into ourios-bench against corpus/otel-demo-v* → the first query-thesis number.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Query execution with tenant scoping, optional template-id filtering, half-open time-range queries, and returned row counts. Tenants with no committed data return empty results.
  • Bug Fixes

    • Engine/storage errors surfaced as a generic storage error to avoid leaking backend details.
  • Tests

    • New integration tests cover end-to-end query execution, counting, time-range filtering, tenant isolation, and handling of uncommitted files.
  • Chores

    • Added runtime and async test support for parquet-backed integration tests.

…usion (slice 1)

Querier::run now actually runs. It roots a DataFusion ListingTable
at the request tenant's data/tenant_id=<enc>/ partition dir
(structural tenant isolation — RFC0007.5), declares year/month/
day/hour as path-only Hive partition cols (tenant_id stays a
plain file column relative to that root), filters on
time_unix_nano range + template_id, collects, and returns the
matching-row count. DataFusion/arrow types stay strictly
internal — only Ourios types cross the public API (§4.6);
datafusion errors map to QueryError::Storage.

QueryRequest gains template_id; QueryResult gains rows;
Querier::new takes the bucket_root. datafusion 53 is a normal
dep (pulls arrow/parquet 58 alongside ourios-parquet's 55 —
harmless, they only meet on disk).

Live tests (tests/execution.rs): tenant + template + half-open
time-range filtering counts, and RFC0007.5 tenant isolation +
empty-tenant. Pruning stats (RFC0007.1 / B1) and the B2 latency
bench are the next slices; their stubs stay #[ignore]'d.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jensholdgaard
jensholdgaard requested a review from Copilot June 1, 2026 23:42
@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Implements Querier::run using DataFusion over tenant-scoped Hive-partitioned Parquet directories, adds ourios-parquet and test dev-deps, extends QueryRequest with template_id and QueryResult with rows, updates QueryError display, and adds integration tests validating filtering, counting, and tenant isolation.

Changes

Querier Execution Implementation

Layer / File(s) Summary
Dependencies and Query API
crates/ourios-querier/Cargo.toml, crates/ourios-querier/src/lib.rs
Adds ourios-parquet, datafusion, tokio, and tempfile; extends QueryRequest with template_id: Option<u64> and QueryResult with rows: u64; updates QueryError::Storage display/docs and a unit test expectation.
Querier Implementation with DataFusion Backend
crates/ourios-querier/src/lib.rs
Implements Querier storing bucket_root with new(bucket_root) and run: percent-encodes tenant to bucket_root/data/tenant_id=<encoded>/, returns empty result if missing, registers a Parquet ListingTable with year/month/day/hour partitions, applies optional time_unix_nano and template_id filters, maps DataFusion errors to QueryError::Storage, and computes matching rows via df.count().await.
Integration Tests with Parquet Store
crates/ourios-querier/tests/execution.rs, crates/ourios-querier/tests/acceptance.rs
Adds async integration tests that synthesize MinedRecords, write partitioned Parquet via ourios-parquet, and assert Querier filtering/counting, tenant isolation, and behavior when only uncommitted *.parquet.tmp files exist; updates acceptance test to point to the new execution test.

Sequence Diagram

sequenceDiagram
  participant Client
  participant Querier
  participant DataFusion
  participant ParquetStore
  Client->>Querier: run(QueryRequest)
  Querier->>Querier: build tenant path (bucket_root/data/tenant_id=<encoded>/)
  Querier->>ParquetStore: check tenant directory
  alt tenant dir exists
    Querier->>DataFusion: register ListingTable (year/month/day/hour)
    alt time_range present
      Querier->>DataFusion: apply time_unix_nano → TimestampNanosecond filter
    end
    alt template_id present
      Querier->>DataFusion: apply template_id equality filter
    end
    Querier->>DataFusion: execute count (df.count().await)
    DataFusion-->>Querier: row counts
  else tenant dir missing
    ParquetStore-->>Querier: not found
    Querier->>Client: return empty QueryResult
  end
  Querier-->>Client: QueryResult { rows, stats }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

  • jensholdgaard/ourios#44: Introduced the PartitionKey and tenant percent-encoding and partition layout used by the ourios-parquet writer that this querier now reads.
  • jensholdgaard/ourios#86: Earlier ourios-querier scaffold that this PR replaces/extends by implementing run and adding execution tests.

Poem

🐰 I hop through parquet rows with cheer,
Tenants kept tidy, templates clear,
Time windows slice the midnight air,
Counts come back neat — no stray row to spare,
The rabbit drums a tiny drum.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: implementing minimal tenant/time/template query execution via DataFusion in the querier as slice 1 of the execution path.
Description check ✅ Passed The description covers all key requirements: it explains what changed, references RFC 0005/0007.5, documents API changes, lists dependencies, and confirms testing. All template sections are meaningfully filled.
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/querier-execution-min

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

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

This PR implements the first end-to-end execution “slice” for ourios-querier: Querier::run now runs a minimal predicate query (tenant + optional time range + optional template_id) against the RFC 0005 Parquet layout using DataFusion, returning a matching row count while keeping DataFusion/Arrow types out of the public API.

Changes:

  • Implement Querier::run execution via DataFusion ListingTable, including tenant-rooted table setup and filter application.
  • Extend the public throwaway query surface (QueryRequest.template_id, QueryResult.rows, Querier::new(bucket_root)).
  • Add live execution integration tests (including RFC0007.5 tenant isolation) and wire in required dependencies.

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
crates/ourios-querier/src/lib.rs Implements DataFusion-backed execution, adds request/result fields, and enforces tenant isolation by table rooting.
crates/ourios-querier/Cargo.toml Adds datafusion + ourios-parquet dependency and test-only deps for integration testing.
crates/ourios-querier/tests/execution.rs New integration tests that write Parquet fixtures and validate row-count results and tenant isolation.
crates/ourios-querier/tests/acceptance.rs Removes the ignored RFC0007.5 stub and points to the new live execution test.
Cargo.lock Locks new transitive dependencies introduced by DataFusion and test dependencies.

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

Comment thread crates/ourios-querier/src/lib.rs Outdated
Comment thread crates/ourios-querier/src/lib.rs Outdated

@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: 2

🤖 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/lib.rs`:
- Around line 176-177: The ListingTableUrl construction currently builds a
file:// URI from tenant_dir (ListingTableUrl::parse(format!("file://{}/",
tenant_dir.display()))) which should instead pass a scheme-less local directory
path (preserve the trailing "/") so DataFusion handles encoding correctly;
update the call to construct the path from tenant_dir (with trailing slash)
before calling ListingTableUrl::parse and keep the existing map_err(storage_err)
behavior. In Querier::run replace the materializing count pattern that does
df.collect().await and sums batch.num_rows() with the DataFusion aggregate API
by calling let rows = df.count().await?; and return that aggregated count
instead of iterating collected batches.
- Around line 228-234: The current code calls df.collect().await and sums
RecordBatch::num_rows(), which materializes all rows; instead build and run a
global aggregate on the DataFrame to compute COUNT(*) (use df.aggregate with an
expression like count(lit(1)) or equivalent), execute the plan as a stream
(e.g., execute_stream or similar DataFusion executor) and read the single scalar
from the first output batch/row, then set QueryResult.rows to that scalar u64;
update the code paths around df.collect(), batches, and rows so only the scalar
result is read and missing/empty results are handled safely before returning
QueryResult with stats unchanged.
🪄 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: 2c0bde0d-43c9-4b79-b4b7-f198c78b7124

📥 Commits

Reviewing files that changed from the base of the PR and between 767d01a and 964e2c7.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • crates/ourios-querier/Cargo.toml
  • crates/ourios-querier/src/lib.rs
  • crates/ourios-querier/tests/acceptance.rs
  • crates/ourios-querier/tests/execution.rs

Comment thread crates/ourios-querier/src/lib.rs Outdated
Comment thread crates/ourios-querier/src/lib.rs Outdated

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

Comment thread crates/ourios-querier/Cargo.toml
Comment thread crates/ourios-querier/src/lib.rs Outdated

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

Comment thread crates/ourios-querier/src/lib.rs Outdated
Comment thread crates/ourios-querier/src/lib.rs Outdated
Comment thread crates/ourios-querier/src/lib.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 4 out of 5 changed files in this pull request and generated 1 comment.

Comment thread crates/ourios-querier/src/lib.rs Outdated

@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-querier/tests/execution.rs (1)

150-158: ⚡ Quick win

Hardcoded partition path: only the tenant-scoped root controls the tmp-only empty-result behavior.

  • Querier::run roots the ListingTable at <bucket_root>/data/tenant_id={percent_encode_tenant(tenant)} and returns empty when there are no published *.parquet under that tenant dir—even if it contains only *.parquet.tmp; so the hardcoded year=/month=/day=/hour= subpath isn’t what determines whether the test hits the tmp-only branch.
  • The real coupling is that tenant_id=a must match percent_encode_tenant("a"); consider deriving the on-disk path via the same Writer/PartitionKey::derive and then renaming the emitted <uuid>.parquet to <uuid>.parquet.tmp so the test stays aligned with any future layout changes.
🤖 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/tests/execution.rs` around lines 150 - 158, The test
hardcodes a deep partition path instead of using the same tenant-root logic as
Querier::run, so change the setup to derive the on-disk path using the same
utilities: call percent_encode_tenant("a") (or reuse PartitionKey::derive/Writer
used by producers) to compute the tenant-scoped root (the same root ListingTable
is rooted at in Querier::run), create that directory and then write a file by
taking a real emitted "<uuid>.parquet" name from the Writer/PartitionKey
generation and renaming it to "<uuid>.parquet.tmp" (or create the tmp name from
the generated uuid) so the test hits the tmp-only branch even if layout changes;
reference Querier::run, ListingTable, percent_encode_tenant, Writer, and
PartitionKey::derive to locate the relevant helpers.
🤖 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-querier/tests/execution.rs`:
- Around line 150-158: The test hardcodes a deep partition path instead of using
the same tenant-root logic as Querier::run, so change the setup to derive the
on-disk path using the same utilities: call percent_encode_tenant("a") (or reuse
PartitionKey::derive/Writer used by producers) to compute the tenant-scoped root
(the same root ListingTable is rooted at in Querier::run), create that directory
and then write a file by taking a real emitted "<uuid>.parquet" name from the
Writer/PartitionKey generation and renaming it to "<uuid>.parquet.tmp" (or
create the tmp name from the generated uuid) so the test hits the tmp-only
branch even if layout changes; reference Querier::run, ListingTable,
percent_encode_tenant, Writer, and PartitionKey::derive to locate the relevant
helpers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 52cbd0fa-56c9-4ffa-85c3-abcad80a70f9

📥 Commits

Reviewing files that changed from the base of the PR and between 5071333 and 899ae49.

📒 Files selected for processing (2)
  • crates/ourios-querier/src/lib.rs
  • crates/ourios-querier/tests/execution.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/ourios-querier/src/lib.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 4 out of 5 changed files in this pull request and generated 1 comment.

Comment thread crates/ourios-querier/src/lib.rs Outdated
jensholdgaard and others added 2 commits June 2, 2026 10:42
…s (slice 1)

Closes the two test gaps from PR review where a behavior fix
lacked a test (per the test-per-fix discipline):
- bucket_path_with_spaces_resolves — a bucket path with a space
  queries correctly (the canonicalize + scheme-less URL fix).
- read_dir_error_surfaces_as_storage_not_empty — a tenant path
  that is a file (ENOTDIR, not NotFound) surfaces as
  QueryError::Storage rather than being masked as empty.

The other accepted fixes were already covered (count() by the
existing count asserts, §4.6 Display by the colocated test,
empty-store by tenant_dir_without_committed_parquet_is_empty) or
exempt (doc-only, refactor to columns:: constants).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jensholdgaard
jensholdgaard merged commit b287e0c into main Jun 2, 2026
8 checks passed
@jensholdgaard
jensholdgaard requested a review from Copilot June 2, 2026 09:20

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 4 out of 5 changed files in this pull request and generated no new comments.

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