Skip to content

feat(collector): materialize ClickHouse analytics contracts - #667

Open
proerror77 wants to merge 1 commit into
mainfrom
codex/canonical-parquet-clickhouse-653
Open

feat(collector): materialize ClickHouse analytics contracts#667
proerror77 wants to merge 1 commit into
mainfrom
codex/canonical-parquet-clickhouse-653

Conversation

@proerror77

@proerror77 proerror77 commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Change contract

Materialize manifest-verified canonical Parquet partitions into immutable, lineage-rich ClickHouse analytics rows. The offline plan emits separate replay-event, PIT-feature, and backtest-result writers; optional remote writes claim a pending registry identity and publish complete rows only after all typed inserts succeed.

Issue relationship

Closes #653

Out of scope

ClickHouse provisioning, deployment, backtest replay from ClickHouse, raw/canonical evidence replacement, live-runtime authorization, and production cutover are out of scope. No ClickHouse instance is required for validation.

Dependencies and merge order

#651 and #666 are already on main (main@9b8b36b6f6cfe6d805e7cd6e172b677a3ddd4ac3).

Focused validation

  • cargo test --manifest-path rust_hft/Cargo.toml -p hft-collector --bin clickhouse-analytics-materializer --test clickhouse_analytics_materializer --locked (4 unit + 2 integration passed)
  • cargo clippy --manifest-path rust_hft/Cargo.toml -p hft-collector --all-targets --features collector-binance --no-deps --locked -- -D warnings passed
  • cargo test --manifest-path rust_hft/Cargo.toml -p hft-collector --locked (292 passed, 2 ignored; all collector binaries/tests passed)
  • rustfmt --edition 2021 on the changed Rust files, git diff 9b8b36b6f6cfe6d805e7cd6e172b677a3ddd4ac3...HEAD --check, and .github/scripts/agent-worktree-preflight.sh passed
  • Counterexamples: corrupted Parquet bytes, manifest/artifact SHA mismatch, duplicate identity with different manifest SHA, and immutable output conflict all fail closed without overwriting the prior plan.

Rollout and rollback

No production rollout. The materializer is an offline/optional analytics-plane binary; rollback is removing this one PR. Any remote ClickHouse target must already satisfy the documented ReplacingMergeTree(materialization_version) plus FINAL registry contract; incompatible state fails closed.

Scope exception

One-time atomic exception approved by /root for commit 16b6fff85d15fb98423d791ce4dc522e7fce3796: four scoped files and 1,225 additions exceed the 750-line guideline because issue #653 is one inseparable materialization contract (verified replay input, immutable/idempotent registry, and three distinct writer schemas). Splitting would ship partial writers/lineage semantics and enlarge the trust surface. Remaining helper duplication is a low-risk follow-up only when another consumer needs shared contract code.

Summary by CodeRabbit

  • New Features

    • Added ClickHouse analytics materialization for replay events, point-in-time feature matrices, and backtest results.
    • Added manifest, schema, lineage, and source-evidence validation.
    • Added idempotent materialization with partition tracking and pending/complete states.
    • Added deterministic local plans and cache-aware execution.
  • Documentation

    • Documented supported inputs, table contracts, lineage requirements, and materialization behavior.
  • Tests

    • Added coverage for valid, repeatable, conflicting, and invalid materialization workflows.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a ClickHouse analytics materializer for replay, PIT feature, and backtest-result artifacts. It validates manifests and Parquet inputs, creates immutable plans, preserves lineage, handles idempotent partition writes, and adds unit and integration tests.

Changes

ClickHouse analytics materialization

Layer / File(s) Summary
Contracts and materializer entry points
rust_hft/tools/collector/CLICKHOUSE_ANALYTICS.md, rust_hft/tools/collector/Cargo.toml, rust_hft/tools/collector/src/bin/clickhouse-analytics-materializer.rs
Documents the materialization contract, registers the binary, and adds dispatch for replay, PIT, and backtest-result inputs.
Artifact validation and lineage derivation
rust_hft/tools/collector/src/bin/clickhouse-analytics-materializer.rs
Validates manifests, hashes, schemas, replay evidence, timestamps, payloads, and PIT modalities. Builds typed rows and lineage metadata.
Immutable plan publication and safety helpers
rust_hft/tools/collector/src/bin/clickhouse-analytics-materializer.rs
Publishes immutable hashed plans, accepts identical retries, rejects conflicts, and validates SQL literals, identifiers, hashes, and filenames.
ClickHouse delivery and integration validation
rust_hft/tools/collector/src/bin/clickhouse-analytics-materializer.rs, rust_hft/tools/collector/tests/clickhouse_analytics_materializer.rs
Adds pending/complete partition writes and identity checks. Tests materialization output, lineage, deterministic retries, cached execution, conflicts, and corrupted Parquet rejection.

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

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant Materializer
  participant PlanFile
  participant ClickHouse
  CLI->>Materializer: select artifact and execution mode
  Materializer->>Materializer: validate manifest, hashes, schema, and lineage
  Materializer->>PlanFile: publish immutable JSONEachRow plan
  Materializer->>ClickHouse: insert pending registry state
  Materializer->>ClickHouse: insert typed analytics rows
  Materializer->>ClickHouse: mark partition complete
Loading

Possibly related issues

  • #650 — Defines the broader manifest-verified ClickHouse materialization scope implemented by this change.
  • #651 — Covers the canonical replay Parquet and manifest artifacts consumed and validated by the materializer.

Possibly related PRs

  • proerror77/monday#48 — Adds collector artifact validation rules used by the materializer’s replay and lineage checks.
  • proerror77/monday#91 — Provides replay and PIT lineage fields preserved by this materializer.
  • proerror77/monday#661 — Shares the collector’s canonical Parquet validation and idempotent artifact handling patterns.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the primary change: adding ClickHouse analytics materialization for the collector.
Description check ✅ Passed The description covers the required contract, issue relationship, scope, dependencies, validation, rollout, rollback, and scope exception.
Linked Issues check ✅ Passed The implementation and tests address all coding objectives in issue #653, including verification, lineage, idempotency, conflict handling, schemas, and documentation.
Out of Scope Changes check ✅ Passed The changed documentation, materializer, manifest, and integration tests are directly related to issue #653 and contain no unrelated scope.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/canonical-parquet-clickhouse-653

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.

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

🧹 Nitpick comments (4)
rust_hft/tools/collector/CLICKHOUSE_ANALYTICS.md (1)

20-22: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Correct the "three-writer inserts" wording.

A single run materializes one input kind. send_to_clickhouse inserts the pending registry row, then the data rows for that one table, then the complete registry row. It never writes all three data tables in one run. The current text suggests three data inserts per run.

📝 Proposed wording
-Remote writes claim the identity as `pending` before data rows and publish a
-`complete` registry row only after all three-writer inserts succeed; a retry
-seeing a pending claim fails closed instead of hiding a partial materialization.
+Remote writes claim the identity as `pending` before data rows and publish a
+`complete` registry row only after every data insert for that input kind
+succeeds; a retry seeing a pending claim fails closed instead of hiding a
+partial materialization.
🤖 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 `@rust_hft/tools/collector/CLICKHOUSE_ANALYTICS.md` around lines 20 - 22,
Update the documentation text describing the remote write sequence to state that
each run inserts data rows for one input table between the pending and complete
registry-row inserts. Remove the wording “all three-writer inserts” so it does
not imply that one run writes all three data tables, while preserving the retry
and partial-materialization behavior.
rust_hft/tools/collector/src/bin/clickhouse-analytics-materializer.rs (2)

454-454: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

with_capacity(manifest.rows) trusts a self-declared count.

manifest.rows comes from the manifest body. The manifest hash proves integrity, not sanity. A very large value causes one large allocation before any row is read. Cap the reservation against the Parquet row count from reader.metadata().file_metadata().num_rows(), or clamp it to a bound.

🤖 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 `@rust_hft/tools/collector/src/bin/clickhouse-analytics-materializer.rs` at
line 454, Update the rows allocation near the materializer’s Parquet reader flow
so it does not trust manifest.rows for an unbounded reservation. Use
reader.metadata().file_metadata().num_rows() to cap the requested capacity (or
apply an equivalent safe upper bound), while preserving row collection behavior
and avoiding large allocations from malicious manifest values.

698-705: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Set explicit ClickHouse request timeouts.

reqwest::Client::new() uses reqwest’s default request timeouts, which may still allow stalled ClickHouse connections to consume the materializer process longer than wanted. Build the client with timeouts and report reqwest::Error as ClickHouse failures if no connection can complete.

🤖 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 `@rust_hft/tools/collector/src/bin/clickhouse-analytics-materializer.rs` around
lines 698 - 705, Update the client construction in the materializer request flow
around reqwest::Client::new to use an explicit request timeout configuration,
including the required connect and overall request limits. Build the client
through the fallible builder path and propagate its reqwest::Error as the
existing ClickHouse failure type before sending the request, preserving the
current authentication and query behavior.
rust_hft/tools/collector/tests/clickhouse_analytics_materializer.rs (1)

179-189: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert partition_identity equality across rows, not just non-emptiness.

The loop checks manifest_sha256, source_revision, venue, market, symbol, and schema_version for equality against rows[0], but only checks that partition_identity is non-empty on each row, not that all rows share the same identity. Since partition_identity is a key lineage field tying rows of one partition together, assert its equality across rows too.

♻️ Proposed strengthened assertion
     for row in &rows {
         assert_eq!(row["manifest_sha256"], rows[0]["manifest_sha256"]);
         assert_eq!(row["source_revision"], rows[0]["source_revision"]);
         assert_eq!(row["venue"], "binance");
         assert_eq!(row["market"], "usdm");
         assert_eq!(row["symbol"], "BTCUSDT");
         assert_eq!(row["schema_version"], "binance-replay-parquet-v1");
         assert_eq!(row["start_time_us"], 1_000);
         assert_eq!(row["end_time_us"], 2_000);
-        assert!(!row["partition_identity"].as_str().unwrap().is_empty());
+        assert_eq!(row["partition_identity"], rows[0]["partition_identity"]);
+        assert!(!row["partition_identity"].as_str().unwrap().is_empty());
     }
🤖 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 `@rust_hft/tools/collector/tests/clickhouse_analytics_materializer.rs` around
lines 179 - 189, Update the row-validation loop in the ClickHouse materializer
test to assert each row’s partition_identity equals
rows[0]["partition_identity"], while retaining the existing non-empty validation
if needed.
🤖 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 `@rust_hft/tools/collector/CLICKHOUSE_ANALYTICS.md`:
- Around line 61-64: Update the result manifest documentation near the existing
backtest-result metadata requirements to state that result_plan requires the
filename to be exactly <manifest-sha256>.result-manifest.json. Clarify that
arbitrary result-manifest paths are rejected, while preserving the existing
replay-manifest filename guidance.

In `@rust_hft/tools/collector/src/bin/clickhouse-analytics-materializer.rs`:
- Around line 587-588: The partition identity built near partition_identity is
ambiguous when venue or symbol contains a colon. Validate the caller-supplied
venue and symbol inputs to reject ':' before constructing the identity, while
preserving the existing non-empty validation and colon-delimited format.
- Around line 713-748: The pending claim around the registry read and insert is
not concurrency-safe, allowing conflicting materializations for one partition
identity. Serialize claims using a lightweight lock mechanism (or equivalent
external per-identity serialization), then read back and verify the winning
manifest_sha256 before inserting data; preserve the existing idempotent and
conflict outcomes. Document and implement the recovery procedure for stale
materialization_state="pending" rows after failed data insertion, including how
they may be safely cleared or retried.
- Around line 247-259: Update the artifact path validation before joining in the
materializer to reject absolute paths as well as relative paths containing
ParentDir components. Ensure manifest.artifact_path cannot bypass the
manifest-directory containment check, while preserving the existing
relative-path rejection and subsequent join behavior.
- Around line 64-65: Update the CLI configuration for the password field in the
argument struct so it is no longer accepted as a plain command-line value; read
it through a secure environment-variable mechanism instead, and enable Clap’s
env feature in the collector dependency configuration. Preserve the existing
password field and downstream usage while preventing exposure through process
listings and shell history.
- Around line 800-809: Update sql_literal to escape backslashes as \\ before
escaping single quotes, ensuring partition_identity values cannot alter the
ClickHouse string literal parsed by identity_query; preserve the existing
surrounding-quote behavior.

---

Nitpick comments:
In `@rust_hft/tools/collector/CLICKHOUSE_ANALYTICS.md`:
- Around line 20-22: Update the documentation text describing the remote write
sequence to state that each run inserts data rows for one input table between
the pending and complete registry-row inserts. Remove the wording “all
three-writer inserts” so it does not imply that one run writes all three data
tables, while preserving the retry and partial-materialization behavior.

In `@rust_hft/tools/collector/src/bin/clickhouse-analytics-materializer.rs`:
- Line 454: Update the rows allocation near the materializer’s Parquet reader
flow so it does not trust manifest.rows for an unbounded reservation. Use
reader.metadata().file_metadata().num_rows() to cap the requested capacity (or
apply an equivalent safe upper bound), while preserving row collection behavior
and avoiding large allocations from malicious manifest values.
- Around line 698-705: Update the client construction in the materializer
request flow around reqwest::Client::new to use an explicit request timeout
configuration, including the required connect and overall request limits. Build
the client through the fallible builder path and propagate its reqwest::Error as
the existing ClickHouse failure type before sending the request, preserving the
current authentication and query behavior.

In `@rust_hft/tools/collector/tests/clickhouse_analytics_materializer.rs`:
- Around line 179-189: Update the row-validation loop in the ClickHouse
materializer test to assert each row’s partition_identity equals
rows[0]["partition_identity"], while retaining the existing non-empty validation
if needed.
🪄 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: 1618514d-be10-4563-bbbb-3fdb83c43fe2

📥 Commits

Reviewing files that changed from the base of the PR and between 9b8b36b and 16b6fff.

📒 Files selected for processing (4)
  • rust_hft/tools/collector/CLICKHOUSE_ANALYTICS.md
  • rust_hft/tools/collector/Cargo.toml
  • rust_hft/tools/collector/src/bin/clickhouse-analytics-materializer.rs
  • rust_hft/tools/collector/tests/clickhouse_analytics_materializer.rs

Comment on lines +61 to +64
For multimodal PIT manifests, pass `--venue` and `--market` explicitly. A
backtest-result manifest uses `backtest-result-metadata-v1` and must include a
SHA-256 `source_revision`; the result JSON is stored as metadata, never as a
replay event tape.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document the content-addressed filename rule for result manifests.

result_plan requires the manifest filename to equal <manifest-sha256>.result-manifest.json and fails closed otherwise. The document states the opposite exemption for replay manifests but is silent for result manifests. Add the requirement so operators do not pass an arbitrary path.

🤖 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 `@rust_hft/tools/collector/CLICKHOUSE_ANALYTICS.md` around lines 61 - 64,
Update the result manifest documentation near the existing backtest-result
metadata requirements to state that result_plan requires the filename to be
exactly <manifest-sha256>.result-manifest.json. Clarify that arbitrary
result-manifest paths are rejected, while preserving the existing
replay-manifest filename guidance.

Comment on lines +64 to +65
#[arg(long, default_value = "")]
password: String,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

clap 4 arg attribute env hide_env_values

💡 Result:

In clap v4, the hide_env_values functionality is available via the Arg::hide_env_values builder method [1]. When using the derive API, this is accessed through the #[arg(hide_env_values = true)] attribute [2][3]. Key details regarding hide_env_values: 1. Purpose: This setting prevents the current value of an associated environment variable from being displayed in the generated --help message [1][4]. This is a security feature intended to prevent sensitive information (like API keys or secrets) from being inadvertently exposed [1][4]. 2. Implementation (Builder API): You can apply it to an Arg instance using:.env("MY_ENV_VAR").hide_env_values(true) [1] 3. Implementation (Derive API): When using #[derive(Parser)], you apply it as an attribute on your struct fields [3]: #[arg(long, env = "MY_ENV_VAR", hide_env_values = true)] field: String, Note that you must explicitly set it to true (i.e., hide_env_values = true), as clap derive attributes for boolean settings generally require an explicit value rather than being implicitly enabled by presence alone [2]. 4. Requirements: The env feature must be enabled in your Cargo.toml for environment variable support to function [5][6]. This approach replaces the older ArgSettings::HideEnvValues used in earlier versions of clap, which has been deprecated [7][8].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate file and relevant lines =="
fd -a 'clickhouse-analytics-materializer.rs' . | sed 's#^\./##'
file="$(fd 'clickhouse-analytics-materializer.rs' . | head -n1)"
if [ -n "${file:-}" ]; then
  wc -l "$file"
  sed -n '1,120p' "$file" | cat -n
fi

echo
echo "== Find clap version/features in Cargo manifests =="
rg -n 'name = "tools-collector"|name = "collector"|clap|collect' Cargo.toml Cargo.lock rust_hft/tools/collector -g 'Cargo.toml' -g 'Cargo.lock' | head -200

Repository: proerror77/monday

Length of output: 5900


Do not accept the ClickHouse password as a plain CLI argument.

Command-line arguments are readable by other local users through the process list and can be written to shell history or logs. Read the password from an environment variable or a file instead.

🔒 Proposed fix
-    #[arg(long, default_value = "")]
-    password: String,
+    /// Read the ClickHouse password from this environment variable.
+    #[arg(long, env = "CLICKHOUSE_PASSWORD", default_value = "", hide_env_values = true)]
+    password: String,

Add the env feature to the collector’s clap dependency until the crate has managed credentials handling.

🤖 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 `@rust_hft/tools/collector/src/bin/clickhouse-analytics-materializer.rs` around
lines 64 - 65, Update the CLI configuration for the password field in the
argument struct so it is no longer accepted as a plain command-line value; read
it through a secure environment-variable mechanism instead, and enable Clap’s
env feature in the collector dependency configuration. Preserve the existing
password field and downstream usage while preventing exposure through process
listings and shell history.

Comment on lines +247 to +259
if manifest.artifact_path.is_relative()
&& manifest
.artifact_path
.components()
.any(|component| matches!(component, std::path::Component::ParentDir))
{
bail!("PIT feature artifact path escapes its manifest directory");
}
let artifact = manifest_path
.parent()
.context("PIT feature manifest has no parent")?
.join(&manifest.artifact_path);
manifest.artifact_path = artifact;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

An absolute artifact_path bypasses the PIT containment check.

The guard runs only when manifest.artifact_path.is_relative(). If the manifest declares an absolute path, no check runs, and Path::join discards the manifest parent and uses the absolute path directly. The materializer then reads a file outside the manifest directory. validate_replay_manifest (Line 395) and result_plan (Line 322) both reject absolute paths, so this kind is the outlier.

🔒 Proposed fix
-    if manifest.artifact_path.is_relative()
-        && manifest
-            .artifact_path
-            .components()
-            .any(|component| matches!(component, std::path::Component::ParentDir))
+    if manifest.artifact_path.is_absolute()
+        || manifest
+            .artifact_path
+            .components()
+            .any(|component| matches!(component, std::path::Component::ParentDir))
     {
         bail!("PIT feature artifact path escapes its manifest directory");
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if manifest.artifact_path.is_relative()
&& manifest
.artifact_path
.components()
.any(|component| matches!(component, std::path::Component::ParentDir))
{
bail!("PIT feature artifact path escapes its manifest directory");
}
let artifact = manifest_path
.parent()
.context("PIT feature manifest has no parent")?
.join(&manifest.artifact_path);
manifest.artifact_path = artifact;
if manifest.artifact_path.is_absolute()
|| manifest
.artifact_path
.components()
.any(|component| matches!(component, std::path::Component::ParentDir))
{
bail!("PIT feature artifact path escapes its manifest directory");
}
let artifact = manifest_path
.parent()
.context("PIT feature manifest has no parent")?
.join(&manifest.artifact_path);
manifest.artifact_path = artifact;
🤖 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 `@rust_hft/tools/collector/src/bin/clickhouse-analytics-materializer.rs` around
lines 247 - 259, Update the artifact path validation before joining in the
materializer to reject absolute paths as well as relative paths containing
ParentDir components. Ensure manifest.artifact_path cannot bypass the
manifest-directory containment check, while preserving the existing
relative-path rejection and subsequent join behavior.

Comment on lines +587 to +588
let partition_identity =
format!("{schema_version}:{venue}:{market}:{symbol}:{start_time_us}:{end_time_us}");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

partition_identity can be ambiguous when a component contains :.

The identity is a :-joined string and it is the immutability key for the registry and for conflict detection. venue and symbol are only checked for non-emptiness, and --venue is caller supplied. Two different partitions can then produce the same identity string. Reject : in venue and symbol, or use a hash of the tuple.

🤖 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 `@rust_hft/tools/collector/src/bin/clickhouse-analytics-materializer.rs` around
lines 587 - 588, The partition identity built near partition_identity is
ambiguous when venue or symbol contains a colon. Validate the caller-supplied
venue and symbol inputs to reject ':' before constructing the identity, while
preserving the existing non-empty validation and colon-delimited format.

Comment on lines +713 to +748
for row in existing
.lines()
.filter_map(|line| serde_json::from_str::<Value>(line).ok())
{
let Some(existing_hash) = row
.get("manifest_sha256")
.and_then(Value::as_str)
.map(str::to_string)
else {
continue;
};
if existing_hash != plan.lineage.manifest_sha256 {
bail!("ClickHouse partition identity conflicts with existing manifest");
}
match row.get("materialization_state").and_then(Value::as_str) {
Some("complete") => return Ok("idempotent-hit".to_string()),
Some("pending") => {
bail!("ClickHouse partition identity has an incomplete materialization")
}
_ => bail!("ClickHouse partition registry state is unsupported"),
}
}

let mut pending = plan.rows[0].1.clone();
pending["materialization_state"] = "pending".into();
pending["materialization_version"] = 1.into();
insert_rows(
&client,
&query_url,
database,
user,
password,
"cex_analytics_partitions",
serde_json::to_vec(&pending)?,
)
.await?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

The pending claim is not atomic, so two concurrent runs can both claim one identity.

The code reads the registry, then inserts the pending row. Between those two steps another process can run the same read. cex_analytics_partitions is a ReplacingMergeTree, which deduplicates by key at merge time and enforces no uniqueness at insert time. Two runs with different manifest_sha256 values for one partition_identity can therefore both pass the conflict check and both write data rows. That defeats the "prevent conflicting content" requirement.

Options: gate the claim with a lightweight lock table plus a read-back verification of the winning manifest_sha256 before data inserts, or serialize materialization per identity outside this binary.

Also document the recovery path for a stuck pending row. After a failed data insert the claim stays, and every later retry fails closed with no documented way to clear it.

🤖 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 `@rust_hft/tools/collector/src/bin/clickhouse-analytics-materializer.rs` around
lines 713 - 748, The pending claim around the registry read and insert is not
concurrency-safe, allowing conflicting materializations for one partition
identity. Serialize claims using a lightweight lock mechanism (or equivalent
external per-identity serialization), then read back and verify the winning
manifest_sha256 before inserting data; preserve the existing idempotent and
conflict outcomes. Document and implement the recovery procedure for stale
materialization_state="pending" rows after failed data insertion, including how
they may be safely cleared or retried.

Comment on lines +800 to +809
fn sql_literal(value: &str) -> String {
format!("'{}'", value.replace('\'', "''"))
}

fn identity_query(database: &str, partition_identity: &str) -> String {
format!(
"SELECT manifest_sha256, materialization_state, materialization_version FROM {database}.cex_analytics_partitions FINAL WHERE partition_identity = {} LIMIT 1 FORMAT JSONEachRow",
sql_literal(partition_identity)
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

ClickHouse string literal backslash escaping HTTP param_ query parameters

💡 Result:

In ClickHouse, HTTP query parameters (prefixed with param_) are parsed using an escaped text format to allow for the unambiguous representation of special characters, including NULLs (encoded as \N) [1][2][3]. Key rules for handling backslashes and special characters in these parameters include: 1. Escape Sequences: ClickHouse interprets backslashes as the start of escape sequences (e.g., \t for tab, \n for newline, \N for NULL) [1][2][4]. 2. Encoding Requirements: To include a literal backslash or a special character in a URL query parameter, you must ensure it is correctly encoded for the URL and then escaped for the ClickHouse parser [1][2]. - For example, to pass a literal tab character (\t), it must be provided as the string \t in the parameter value. - Because this string contains a backslash, it must be URL-encoded, resulting in %5C%09 (where %5C is the URL encoding for \ and %09 is for the tab) [1][2]. 3. Consistency Challenges: Users have reported that inconsistent interpretation of backslashes can occur in some versions [5]. Specifically, a single backslash followed by certain characters may trigger parsing errors (CANNOT_PARSE_ESCAPE_SEQUENCE), while double backslashes (\) are sometimes interpreted as a literal backslash [5]. To ensure reliable data transmission, applications should implement robust serialization that escapes special characters (backslash, tab, newline, carriage return) with a preceding backslash before passing them to the ClickHouse HTTP interface [4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the relevant Rust file and nearby definitions/usages.
file="rust_hft/tools/collector/src/bin/clickhouse-analytics-materializer.rs"
if [ ! -f "$file" ]; then
  echo "missing $file"
  fd -a 'clickhouse-analytics-materializer\.rs$' .
  exit 1
fi

echo "== file stats =="
wc -l "$file"

echo "== relevant sql_literal and identity_query =="
sed -n '760,825p' "$file" | cat -n

echo "== partition_identity construction and validation sites =="
rg -n "partition_identity|sql_identifier|sql_literal|identity_query|--venue|parameter|param_|to_ascii_uppercase|market|symbol|venue" "$file" -C 3

echo "== diff stat/name status =="
git diff --stat HEAD~1..HEAD 2>/dev/null || git diff --stat 2>/dev/null || true

Repository: proerror77/monday

Length of output: 13056


🏁 Script executed:

#!/bin/bash
set -euo pipefail
file="rust_hft/tools/collector/src/bin/clickhouse-analytics-materializer.rs"

echo "== clickhouse docs mentions in repo =="
rg -n "ClickHouse|clickhouse|cex_analytics_partitions|partition_identity|sql_literal|bind|param_|JSONEachRow" rust_hft/tools/collector -C 2

echo "== tests around sql_literal and identity =="
sed -n '860,875p' "$file" | cat -n

echo "== deterministic Rust string transformation probe =="
python3 - <<'PY'
samples = ["binance:spot:BTCUSDT:1:2", "a\\b", "a\\'", "\\q", "a\\\\b", "a\\\\'"]
for s in samples:
    quoted = "'" + s.replace("'", "''") + "'"
    print(f"input={repr(s)} quoted={repr(quoted)}")
print("partition_identity with trailing backslash contains no quote escape:")
s = "pit:v1\\:binance\\:spot\\:BTCUSDT\\:1\\:2"
quoted = "'" + s.replace("'", "''") + "'"
print(repr(quoted), "' inside quoted:", "'" in quoted)
PY

Repository: proerror77/monday

Length of output: 50375


🌐 Web query:

ClickHouse documentation String quoted literal backslash escape

💡 Result:

In ClickHouse, string literals must be enclosed in single quotes (') [1][2]. Double quotes are not supported for string literals [1][2]. To include special characters or quotes within a string, ClickHouse supports two primary escaping methods: 1. Backslash Escaping: You can use a backslash () followed by a supported escape sequence [1][2]. 2. Single Quote Doubling: The single quote character (') can be escaped by using two consecutive single quotes ('') [1][2]. Supported Backslash Escape Sequences When a backslash is used, it interprets the following character according to the table below [1][2]. If a backslash precedes a character not listed in this table, it loses its special meaning and is interpreted literally (e.g., \w is treated as \w, not a special escape sequence) [1][2]. | Supported Escape | Description | | ----------------- | ------------------------------------------------------------- | | \xHH | 8-bit character (hex digits) | | \N | Reserved (does nothing) | | \a | Alert | | \b | Backspace | | \e | Escape character | | \f | Form feed | | \n | Line feed | | \r | Carriage return | | \t | Horizontal tab | | \v | Vertical tab | | \0 | Null character | | \ | Backslash | | ' | Single quote | | " | Double quote | | ` | Backtick | | / | Forward slash | | = | Equal sign | Important Notes - Mandatory Escaping: You must escape at least the single quote (') and the backslash () characters using either their respective escape sequences (' or \ and \ respectively) or by doubling the quote ('') [1][2]. - LIKE Operator Context: When using the LIKE operator, there is an additional layer of escaping required because LIKE uses \ as an escape character for its own metacharacters (%, _, and ). Consequently, to match a literal backslash in a LIKE pattern, you often need to write four backslashes (\\) [3]. - Heredoc: For scenarios where you wish to avoid extensive escaping, ClickHouse also supports heredoc syntax ($tag$content$tag$), where no escaping is required [4].

Citations:


Escape backslashes in ClickHouse string literals

sql_literal doubles ', but ClickHouse also treats \ as an escape character inside single-quoted literals. A partition_identity containing a backslash before the closing quote can change the parsed string. Escape \ as \\ and \' before '\'', or use ClickHouse HTTP bound parameters.

🤖 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 `@rust_hft/tools/collector/src/bin/clickhouse-analytics-materializer.rs` around
lines 800 - 809, Update sql_literal to escape backslashes as \\ before escaping
single quotes, ensuring partition_identity values cannot alter the ClickHouse
string literal parsed by identity_query; preserve the existing surrounding-quote
behavior.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 16b6fff85d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

let mut pending = plan.rows[0].1.clone();
pending["materialization_state"] = "pending".into();
pending["materialization_version"] = 1.into();
insert_rows(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Make the ClickHouse identity claim atomic

When two materializers for different manifests sharing one partition identity run concurrently, both can complete the preceding SELECT before either reaches this unconditional pending-row insert. The documented ReplacingMergeTree only resolves versions during reads; it does not make this check-and-insert sequence exclusive, so both writers can insert typed rows and publish version-2 registry rows, leaving mixed analytics data and a nondeterministically selected manifest. Use an atomic/external claim mechanism or serialize writers and verify ownership before inserting data.

AGENTS.md reference: AGENTS.md:L38-L39

Useful? React with 👍 / 👎.

output.write_all(bytes)?;
output.sync_all()?;
drop(output);
match fs::rename(&temporary, path) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Publish immutable plans without overwriting race winners

On Unix, fs::rename replaces an existing destination, so if two processes pass the earlier path.exists() check with different plan bytes, the later rename silently overwrites the first plan and returns Ok(()); the conflict branches never run. This violates the immutable-output contract under concurrent publication. Use a no-replace primitive such as the crate's existing hard-link publication pattern, then compare the winning file on an already-exists error.

AGENTS.md reference: AGENTS.md:L40-L41

Useful? React with 👍 / 👎.

Comment on lines +649 to +651
let hash = sha256_bytes(&bytes);
valid_sha256(&hash, "manifest sha256")?;
Ok((path, bytes, hash))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Require a trusted digest for input manifests

For replay and PIT inputs, the CLI receives only a manifest path, and this function computes a hash from those same untrusted bytes without comparing it to an expected digest, signed envelope, content-addressed filename, or verified cache marker; checking that the newly computed SHA string is syntactically valid is tautological. A fabricated but structurally valid manifest and matching artifact can therefore claim arbitrary source-segment revisions and be published as verified analytics lineage. Require an independently supplied/verified manifest identity while still allowing relocated cache filenames.

AGENTS.md reference: AGENTS.md:L38-L39

Useful? React with 👍 / 👎.

{
bail!("canonical Parquet schema does not match its manifest");
}
let mut rows = Vec::with_capacity(manifest.rows);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Stream replay partitions instead of retaining every copy

For realistic multi-million-row LOB partitions, this allocation retains the entire decompressed event tape, after which replay_plan duplicates it into a Vec<Value>, encode_plan creates another full byte buffer, and remote sending constructs another grouped buffer. Peak memory therefore scales to several times the uncompressed partition size and can OOM before any output or ClickHouse insert occurs. Validate and encode/send rows through bounded streaming or staged files rather than materializing every representation simultaneously.

Useful? React with 👍 / 👎.

manifest.artifact_sha256
);
}
let rows = read_replay_rows(&artifact, &manifest)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Verify and consume artifacts through one immutable handle

If a replay artifact is replaced after sha256_file closes it but before this call reopens the pathname, the rows can come from different bytes than the SHA recorded in lineage; result_plan repeats the same hash-then-fs::read pattern. This permits cache corruption or a concurrent replacement to pass verification and publish falsely attributed analytics. Open once and hash plus parse that same handle or retained byte snapshot.

AGENTS.md reference: AGENTS.md:L38-L39

Useful? React with 👍 / 👎.

Comment on lines +587 to +588
let partition_identity =
format!("{schema_version}:{venue}:{market}:{symbol}:{start_time_us}:{end_time_us}");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Include experiment identity in PIT and result partitions

When two valid backtests over the same venue, symbol, and time window produce different strategy results, both receive the same partition identity because the key omits the manifest, artifact, source revision, and run identity, so the second result is rejected as a conflict. The same happens for PIT matrices covering the same window but using different feature sets or label horizons. This prevents the analytics plane from retaining multiple experiments over one market interval; include a dataset-specific feature/run identity in these partition keys.

Useful? React with 👍 / 👎.

}

fn sql_literal(value: &str) -> String {
format!("'{}'", value.replace('\'', "''"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Parameterize the partition-identity query

For PIT and result inputs, venue and symbol values are only checked for non-emptiness, and the PIT --venue override is similarly unrestricted. An identity containing a backslash before a quote is not safely escaped by merely doubling quotes because ClickHouse also interprets backslash escapes, allowing the literal to terminate early and the identity SELECT predicate to be altered. Use ClickHouse HTTP query parameters or comprehensively validate/encode identity components instead of constructing this SQL literal.

AGENTS.md reference: AGENTS.md:L38-L39

Useful? React with 👍 / 👎.

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.

CEX: canonical Parquet → ClickHouse 分析物化契约

1 participant