Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
b87676b
refactor(cli): run sqb clone natively
kvlonge Oct 13, 2026
ac2b574
chore: update the Python boundary ratchet
kvlonge Oct 13, 2026
73124f7
refactor(state): build migration event IDs, event SQL and freshness v…
kvlonge Oct 13, 2026
7fe4d6c
fix: read native freshness helpers from the extension in the freshnes…
kvlonge Oct 13, 2026
8da940a
chore: update the Python boundary ratchet
kvlonge Oct 13, 2026
1d108cf
refactor: normalize dbt profiles and write dbt init config natively
kvlonge Oct 13, 2026
9b863bf
refactor: build and render the sqb dbt plan natively
kvlonge Oct 13, 2026
f17a8ed
refactor: index dbt manifests and resolve dbt refs natively
kvlonge Oct 13, 2026
ccb38a6
chore: update the Python boundary ratchet
kvlonge Oct 13, 2026
3001f2b
refactor(cli): run sqb load natively
kvlonge Oct 13, 2026
38740b9
chore: update the Python boundary ratchet
kvlonge Oct 13, 2026
de555b5
refactor: register command handler bindings with the CLI bindings
kvlonge Oct 13, 2026
fc65bd1
fix(cli): route progress to stderr for sqb dbt --json
kvlonge Oct 13, 2026
895ae09
chore: update the Python boundary ratchet
kvlonge Oct 13, 2026
d2d788a
refactor(diagnostics): redact diagnostic secrets natively
kvlonge Oct 13, 2026
85403df
refactor(rules): key and look up the SQL rule result cache natively
kvlonge Oct 13, 2026
7445931
chore: update the Python boundary ratchet
kvlonge Oct 13, 2026
222510d
fix: drop command binding imports from the main registration
kvlonge Oct 13, 2026
00c7dad
refactor(cli): run sqb check natively
kvlonge Oct 13, 2026
5f2b053
chore: update the Python boundary ratchet
kvlonge Oct 13, 2026
1f071c0
refactor(cli): render sqb diff output and resolve its exit codes nati…
kvlonge Oct 13, 2026
1d3c2b7
chore: update the Python boundary ratchet
kvlonge Oct 13, 2026
b409ea6
refactor: import janitor, load, clone and freshness Python by complet…
kvlonge Oct 13, 2026
ad4b9cd
test: check native Python module paths resolve and are never built at…
kvlonge Oct 13, 2026
6b3106f
feat: run sqb test natively
kvlonge Oct 13, 2026
9180ed8
chore: update the Python boundary ratchet
kvlonge Oct 13, 2026
43a85c6
refactor(rules): key, look up and store the custom-rule result cache …
kvlonge Oct 13, 2026
0a7d0b6
refactor(rules): load rules configuration natively
kvlonge Oct 13, 2026
db02f94
refactor(rules): convert rule findings and SQL test rows natively
kvlonge Oct 13, 2026
d79c4b5
refactor(rules): separate rule-run thread and custom-rule dispatch mo…
kvlonge Oct 13, 2026
4351867
refactor(rules): build the built-in rule catalogue natively
kvlonge Oct 13, 2026
f3a7da0
chore: update the Python boundary ratchet
kvlonge Oct 13, 2026
cb88b04
refactor(cli): run sqb diff validation, orchestration and query lifec…
kvlonge Oct 13, 2026
b2885cd
chore: update the Python boundary ratchet
kvlonge Oct 13, 2026
192097b
feat: run sqb seed natively
kvlonge Oct 13, 2026
e38684d
chore: update the Python boundary ratchet
kvlonge Oct 13, 2026
2e9f092
fix(cli): render sqb check rows before encoding check metadata
kvlonge Oct 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
13 changes: 13 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
members = [
"crates/sqlbuild-core",
"crates/sqlbuild-cursor",
"crates/sqlbuild-state",
"crates/sqlbuild-pysource",
"crates/sqlbuild-cache",
"crates/sqlbuild-sqltext",
Expand All @@ -26,6 +27,7 @@ resolver = "2"
order = [
"sqlbuild-core",
"sqlbuild-cursor",
"sqlbuild-state",
"sqlbuild-pysource",
"sqlbuild-cache",
"sqlbuild-sqltext",
Expand Down Expand Up @@ -104,6 +106,7 @@ sqlbuild-model-config = { path = "crates/sqlbuild-model-config" }
sqlbuild-pysource = { path = "crates/sqlbuild-pysource" }
sqlbuild-scopes = { path = "crates/sqlbuild-scopes" }
sqlbuild-sqltext = { path = "crates/sqlbuild-sqltext" }
sqlbuild-state = { path = "crates/sqlbuild-state" }
stacker = "0.1"
sqlparser = { version = "0.58", features = ["visitor"] }
syn = { version = "2", features = ["full", "visit", "extra-traits"] }
Expand Down
18 changes: 17 additions & 1 deletion crates/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ that can be tested and benchmarked with `cargo test` alone.
|---|---|
| `sqlbuild-core` | Constants shared by several crates, the panic boundary that turns an unwinding native failure into a named compiler error, passive identity, diagnostic and SQL value types, text positions, and a JSON emitter whose output matches Python's `json.dumps` and `orjson` byte for byte. |
| `sqlbuild-cursor` | Microbatch cursor algebra: typed cursor values, grain flooring and stepping, half-open interval clamping, merging and batch splitting, timestamp text normalization and calendar durations, with Python `datetime` results and errors. Zones other than fixed offsets are resolved through a caller-supplied lookup. |
| `sqlbuild-state` | Append-only warehouse state rows: content-addressed migration event IDs, typed event SQL literals and statements, and source freshness data-version normalization, hashing, equivalence and age policy, byte-compatible with the rows Python wrote. |
| `sqlbuild-pysource` | Static reading of project Python source with the ruff parser: macro module structure, literal SQL arguments, definition locations and string-literal edits. It never imports or runs user code; CPython stays the authority on whether a file is valid Python. |
| `sqlbuild-cache` | The shared native store every native cache uses (one atomically replaced, checksummed file per cache kind, keyed by content digests) and the one fingerprinting scheme for cache keys: the content digest and the project file fingerprint. |
| `sqlbuild-sqltext` | Lexical SQL text without polyglot: comment, quote and parenthesis scanning, the rules quote policy, model header tokenization and matching, static variable substitution, static reference extraction, and the general reference scan with table-function call arguments under each adapter's lexical rules. |
Expand All @@ -31,7 +32,7 @@ Dependencies point one way, from the top of this graph to the bottom:
sqlbuild-python
-> sqlbuild-refactor -> sqlbuild-rules -> sqlbuild-analysis -> sqlbuild-attachments
-> sqlbuild-model-config -> sqlbuild-scopes -> sqlbuild-render -> sqlbuild-discovery -> sqlbuild-config
-> sqlbuild-sqltext -> sqlbuild-cache -> sqlbuild-pysource -> sqlbuild-cursor -> sqlbuild-core
-> sqlbuild-sqltext -> sqlbuild-cache -> sqlbuild-pysource -> sqlbuild-state -> sqlbuild-cursor -> sqlbuild-core
sqlbuild-cli -> sqlbuild-cache, sqlbuild-core
```

Expand All @@ -56,6 +57,21 @@ The same check fails when the workspace version, the `pyproject.toml` version an
`fensu check` applies the Rust structure rules inside each crate: other crates use items through
`main/` entry points, `models.rs`, `types.rs` and `constants.rs`, not through `_helpers/`.

## Python module paths

`sqlbuild-python` and `sqlbuild-cli` import Python by module path strings. The repository test
`tests/unit/scripts/native_module_paths` (`scripts/native_module_paths`) keeps them honest:

- every `"sqlbuild...."` literal must be a complete dotted path that imports as a module or
resolves as a module attribute; constants whose name contains `LOGGER` hold `logging`
logger names and are not imported;
- attributes are checked where a literal module, or a module constant defined in the same file,
is passed with a literal name to an import helper (`imported(py, MODULE, "name")`), to
`import(MODULE)?.getattr("name")`, or written as a `("sqlbuild.module", "name")` pair;
- module paths are never built at runtime: a `format!` or `concat!` template may not start with
`sqlbuild.` or with a `{..PACKAGE..}.`/`{..MODULE..}.` constant. Write the complete module
path as a constant instead.

## Compiler

The compiler runs discovery, rendering and analysis natively. Generic compiler stages do not
Expand Down
110 changes: 110 additions & 0 deletions crates/sqlbuild-cli/src/diff_report/_helpers/evidence.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//! Bounded, difference-aware rendering of example values and keys.

use sqlbuild_cache::digest::main::sha256_hex::sha256_hex;

use crate::diff_report::_helpers::numbers::grouped;
use crate::diff_report::constants::MAX_RENDERED_KEY_LENGTH;
use crate::diff_report::models::{DiffExampleOptions, RenderedValue};

fn first_difference(left: &[char], right: &[char]) -> Option<usize> {
if let Some(index) = left.iter().zip(right).position(|(a, b)| a != b) {
return Some(index);
}
(left.len() != right.len()).then(|| left.len().min(right.len()))
}

fn render_value(
text: &[char],
first_difference: Option<usize>,
options: DiffExampleOptions,
) -> RenderedValue {
let original_length: usize = text.len();
let full: String = text.iter().collect();
if options.suppress_values {
return RenderedValue {
text: None,
original_length,
truncated: false,
suppressed: true,
first_difference,
};
}
let Some(limit) = options
.max_value_length
.filter(|limit| original_length > *limit)
else {
return RenderedValue {
text: Some(full),
original_length,
truncated: false,
suppressed: false,
first_difference,
};
};
let difference: usize = first_difference.unwrap_or(0);
let context_before: usize = (limit / 3).min(limit.saturating_sub(1));
let start: usize = difference
.saturating_sub(context_before)
.min(original_length - limit);
let end: usize = start + limit;
let fragment: String = text[start..end].iter().collect();
RenderedValue {
text: Some(format!(
"{}{fragment}{}",
if start > 0 { "…" } else { "" },
if end < original_length { "…" } else { "" }
)),
original_length,
truncated: true,
suppressed: false,
first_difference,
}
}

/// Two unequal values rendered around their first differing character.
pub(crate) fn render_example_pair(
left: &str,
right: &str,
options: DiffExampleOptions,
) -> (RenderedValue, RenderedValue) {
let left: Vec<char> = left.chars().collect();
let right: Vec<char> = right.chars().collect();
let difference: Option<usize> = first_difference(&left, &right);
(
render_value(&left, difference, options),
render_value(&right, difference, options),
)
}

/// One bounded value for terminal output.
pub(crate) fn display_example_value(value: &RenderedValue) -> String {
if value.suppressed {
return format!(
"<value suppressed; {} chars>",
grouped(value.original_length as i64)
);
}
let rendered: &str = value.text.as_deref().unwrap_or_default();
if value.truncated {
return format!(
"{rendered} [truncated; {} chars]",
grouped(value.original_length as i64)
);
}
rendered.to_owned()
}

/// Ordinary keys complete; exceptionally long keys shortened and identified by digest.
pub(crate) fn render_key_value(text: &str) -> String {
let length: usize = text.chars().count();
if length <= MAX_RENDERED_KEY_LENGTH {
return text.to_owned();
}
let digest: String = sha256_hex(text.as_bytes());
let prefix: String = text.chars().take(MAX_RENDERED_KEY_LENGTH).collect();
format!(
"{prefix}… [truncated; {} chars; sha256:{}]",
grouped(length as i64),
&digest[..12]
)
}
35 changes: 35 additions & 0 deletions crates/sqlbuild-cli/src/diff_report/_helpers/findings.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//! Whether a diff found differences, and the resulting outcome.

use crate::diff_report::constants::{FINDINGS_OUTCOME, PASS_OUTCOME};
use crate::diff_report::models::DiffModelFacts;

pub(crate) fn has_schema_differences(model: &DiffModelFacts) -> bool {
!(model.added_columns.is_empty()
&& model.removed_columns.is_empty()
&& model.type_changes.is_empty())
}

/// Whether any model has schema or row differences.
pub(crate) fn has_findings(models: &[DiffModelFacts]) -> bool {
models.iter().any(|model| {
has_schema_differences(model)
|| model.rows.as_ref().is_some_and(|rows| {
rows.unequal_count != 0 || rows.left_only_count != 0 || rows.right_only_count != 0
})
})
}

/// The explicit outcome, or `findings`/`pass` from the results.
pub(crate) fn resolved_outcome(models: &[DiffModelFacts], outcome: Option<&str>) -> String {
outcome
.filter(|outcome| !outcome.is_empty())
.map(str::to_owned)
.unwrap_or_else(|| {
if has_findings(models) {
FINDINGS_OUTCOME
} else {
PASS_OUTCOME
}
.to_owned()
})
}
Loading
Loading