-
Notifications
You must be signed in to change notification settings - Fork 9
feat(semantic): field catalog — types from ClickHouse + authored roles (#2208) #2211
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| //! Drift test: the committed type snapshot (via the built catalog) must still | ||
| //! agree with the live ClickHouse schema. For every catalogued field, the | ||
| //! warehouse's column type — normalized — must equal the field's type in the | ||
| //! catalog. A mismatch means `types.snapshot.json` is stale and must be | ||
| //! regenerated from ClickHouse. | ||
| //! | ||
| //! Ignored by default; requires a live ClickHouse. Enable with | ||
| //! `INTEGRATION_TESTS_CLICKHOUSE_URL=<url> cargo test -p analytics -- --ignored | ||
| //! field_catalog`. | ||
|
|
||
| use std::collections::BTreeMap; | ||
|
|
||
| use clickhouse::Row; | ||
| use serde::Deserialize; | ||
|
|
||
| use crate::domain::field_catalog::field_catalog; | ||
| use crate::domain::field_catalog::model::FieldType; | ||
|
|
||
| const CH_ENV: &str = "INTEGRATION_TESTS_CLICKHOUSE_URL"; | ||
| const CH_USER_ENV: &str = "INTEGRATION_TESTS_CLICKHOUSE_USER"; | ||
| const CH_PASSWORD_ENV: &str = "INTEGRATION_TESTS_CLICKHOUSE_PASSWORD"; | ||
|
|
||
| #[derive(Debug, Row, Deserialize)] | ||
| struct ColumnRow { | ||
| name: String, | ||
| #[serde(rename = "type")] | ||
| ch_type: String, | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| #[ignore = "requires live ClickHouse; set INTEGRATION_TESTS_CLICKHOUSE_URL to enable"] | ||
| async fn types_snapshot_matches_clickhouse() { | ||
| let Ok(url) = std::env::var(CH_ENV) else { | ||
| eprintln!("skipping: {CH_ENV} not set"); | ||
| return; | ||
| }; | ||
| let mut config = insight_clickhouse::Config::new(&url, "default"); | ||
| if let (Ok(user), Ok(password)) = (std::env::var(CH_USER_ENV), std::env::var(CH_PASSWORD_ENV)) { | ||
| config = config.with_auth(user, password); | ||
| } | ||
| let client = insight_clickhouse::Client::new(config); | ||
|
|
||
| let mut drift: Vec<String> = Vec::new(); | ||
|
|
||
| for dataset in &field_catalog().datasets { | ||
| let rows = client | ||
| .query("SELECT name, type FROM system.columns WHERE database = ? AND table = ?") | ||
| .bind(dataset.database.as_str()) | ||
| .bind(dataset.table.as_str()) | ||
| .fetch_all::<ColumnRow>() | ||
| .await | ||
| .unwrap_or_else(|e| panic!("querying columns of {}: {e}", dataset.relation())); | ||
|
|
||
| let live: BTreeMap<String, String> = | ||
| rows.into_iter().map(|r| (r.name, r.ch_type)).collect(); | ||
|
Comment on lines
+46
to
+55
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== locate file =="
git ls-files | rg 'src/backend/services/analytics/src/domain/field_catalog/live_tests\.rs$|clickhouse|ColumnRow' || true
echo "== target file outline/contents =="
if [ -f src/backend/services/analytics/src/domain/field_catalog/live_tests.rs ]; then
wc -l src/backend/services/analytics/src/domain/field_catalog/live_tests.rs
sed -n '1,140p' src/backend/services/analytics/src/domain/field_catalog/live_tests.rs | nl -ba
fi
echo "== all system.columns query usages =="
rg -n 'system\.columns|fetch_all::<ColumnRow>|fetch_all\(' src/backend/services/analytics || true
echo "== lock/clickhouse deps mentions =="
rg -n 'clickhouse|Client|fetch_all|limit|LIMIT' src/backend/services/analytics/Cargo.toml src/backend/services/analytics -g '*.rs' -g 'Cargo.toml' || trueRepository: constructorfabric/insight Length of output: 1500 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== exact target file contents with line numbers =="
sed -n '1,220p' src/backend/services/analytics/src/domain/field_catalog/live_tests.rs | nl -ba
echo "== lock files for package version context, if present =="
git ls-files '*lock*' 'Cargo.lock' | sed -n '1,40p'
if [ -f Cargo.lock ]; then
python3 - <<'PY'
import re
p='Cargo.lock'
txt=open(p,errors='ignore').read().split('[[package]]\n')[1:]
for blk in txt:
if 'clickhouse' in blk.lower() or 'click' in blk.lower():
m=re.search(r'name\s*=\s*"([^"]*)"\nversion\s*=\s*"([^"]*)"', blk)
print(m.group(0).replace('\n',' | ') if m else blk[:500])
PY
fiRepository: constructorfabric/insight Length of output: 254 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== line count =="
wc -l src/backend/services/analytics/src/domain/field_catalog/live_tests.rs
wc -l src/backend/libs/insight-clickhouse/src/query.rs
echo "== target file contents =="
sed -n '1,130p' src/backend/services/analytics/src/domain/field_catalog/live_tests.rs
echo "== query.rs contents =="
sed -n '1,180p' src/backend/libs/insight-clickhouse/src/query.rs
echo "== all system.columns query usage context =="
rg -n -C 4 'system\.columns|fetch_all::<ColumnRow>|fetch_all\(' src/backend/services/analytics src/backend/libs/insight-clickhouse || trueRepository: constructorfabric/insight Length of output: 25872 Bound the This test calls 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
|
|
||
| if live.is_empty() { | ||
| drift.push(format!( | ||
| "{}: relation absent from ClickHouse", | ||
| dataset.relation() | ||
| )); | ||
| continue; | ||
| } | ||
|
|
||
| for field in &dataset.fields { | ||
| match live.get(&field.name) { | ||
| None => drift.push(format!( | ||
| "{}.{}: column absent from ClickHouse", | ||
| dataset.relation(), | ||
| field.name | ||
| )), | ||
| Some(ch_type) => match FieldType::normalize(ch_type) { | ||
| Some((ty, nullable)) if ty == field.ty && nullable == field.nullable => {} | ||
| other => drift.push(format!( | ||
| "{}.{}: catalog has {:?} (nullable={}), ClickHouse has {ch_type} -> {other:?}", | ||
| dataset.relation(), | ||
| field.name, | ||
| field.ty, | ||
| field.nullable | ||
| )), | ||
| }, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| assert!( | ||
| drift.is_empty(), | ||
| "field-catalog type snapshot is stale — regenerate types.snapshot.json from ClickHouse:\n{}", | ||
| drift.join("\n") | ||
| ); | ||
| } | ||
There was a problem hiding this comment.
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
Align the catalog source and construction model.
This text describes a build-time generator. The implementation constructs the embedded catalog lazily through
OnceLock::get_or_init. It also conflicts with Line 291, which still says dbtschema.ymlmetadata is the source and roles are never hand-maintained. Update this section and the responsibility scope so they describe the same hybridroles.yamland type-snapshot model.🤖 Prompt for AI Agents