Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
0afceb2
refactor: host plan entry cursor windows and model entries natively
kvlonge Oct 15, 2026
8e00d6c
refactor: host plan output and static command plans natively
kvlonge Oct 15, 2026
9f01c32
refactor: host clone planning and planner entry facades natively
kvlonge Oct 15, 2026
8e2c96a
test: import native SQL test artifact planning directly
kvlonge Oct 15, 2026
18bab37
refactor: host execution plan orchestration natively
kvlonge Oct 15, 2026
a328b33
refactor: host plan display helpers and planner classes natively
kvlonge Oct 15, 2026
6278842
chore: reconcile integrated planner assembly boundary
kvlonge Oct 15, 2026
6f29a9d
refactor: observe, plan and store source freshness natively
kvlonge Oct 15, 2026
8582bc2
refactor: allocate and collect Snowflake run cost natively
kvlonge Oct 15, 2026
5777c91
docs: describe native source freshness and run-cost modules
kvlonge Oct 15, 2026
f7f3317
refactor: delete unused CLI, cost, dbt and value constants and models
kvlonge Oct 15, 2026
1bd9c3e
refactor: own compile reuse rules and notices as native constants
kvlonge Oct 15, 2026
43eb1a5
refactor: own build, dbt and warehouse-group command constants natively
kvlonge Oct 15, 2026
9c25c46
refactor: track cost ledger failures and the default rate source nati…
kvlonge Oct 15, 2026
efd4929
refactor: raise source freshness and cost artifact errors from native…
kvlonge Oct 15, 2026
9a97450
refactor: carry the compile reuse attempt as a native class
kvlonge Oct 15, 2026
96e0de0
refactor: keep the compile reuse attempt fields in the native constan…
kvlonge Oct 15, 2026
399f2cc
chore: reconcile integrated freshness and data-model boundary
kvlonge Oct 15, 2026
135af6c
refactor: host compiler path, location, class-table and config-error …
kvlonge Oct 15, 2026
54b3972
refactor: collect compile diagnostics and input reads natively
kvlonge Oct 15, 2026
66badd5
refactor: build analysis cache contexts and explicit-reference owners…
kvlonge Oct 15, 2026
aa279b3
refactor: plan pipelines, open connections and write contracts natively
kvlonge Oct 15, 2026
a36666d
refactor: host setting help, expected errors, lint stops and phase tr…
kvlonge Oct 15, 2026
b4df8f3
refactor: host CLI styling, terminal width and the transient line coo…
kvlonge Oct 15, 2026
7c4e981
refactor: drop empty lint packages and the dead compile context key enum
kvlonge Oct 15, 2026
f00707d
refactor: share native context variable creation
kvlonge Oct 15, 2026
8deb23d
test: observe planning connections through the adapter in the rules p…
kvlonge Oct 15, 2026
738751d
chore: reconcile integrated Phase 1 boundary
kvlonge Oct 15, 2026
ad929a9
test: patch the native execution plan in phase timing tests
kvlonge Oct 15, 2026
2df9aee
test: drop scan entries for modules moved to Rust
kvlonge Oct 15, 2026
48252ab
fix: report connection errors only for ordinary exceptions
kvlonge Oct 15, 2026
9e277eb
test: keep only asserted fields from compile benchmark payloads
kvlonge Oct 15, 2026
d026f24
test: guard that compiles after a project config edit keep unchanged …
kvlonge Oct 15, 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
  •  
  •  
  •  
6 changes: 3 additions & 3 deletions crates/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ that can be tested and benchmarked with `cargo test` alone.

| Crate | Responsibility |
|---|---|
| `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-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, a JSON emitter whose output matches Python's `json.dumps` and `orjson` byte for byte, and run-cost allocation with exact Python `decimal` default-context arithmetic and Snowflake query-history SQL. |
| `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-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, age policy and latest-state read SQL, 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 @@ -18,7 +18,7 @@ that can be tested and benchmarked with `cargo test` alone.
| `sqlbuild-model-config` | MODEL configuration, columns and audits, template expansion over caller values, and dynamic-column name patterns using Rust regex syntax. |
| `sqlbuild-render` | Macro call scanning, argument evaluation, expansion, splicing, memoisation and persistent call-store keys. Python executes user macro bodies at the extension boundary. |
| `sqlbuild-attachments` | Native attachment of tests, audits, sources, functions, scenarios and seeds, including argument merging, validation and audit policy resolution. Adapter value rendering remains an extension boundary. |
| `sqlbuild-analysis` | SQL analysis over polyglot: query analysis, binding, semantic validation and usage, column references, type normalization, FAST/RICH lineage, and SQL-test/scenario extraction and compiler-side assembly. |
| `sqlbuild-analysis` | SQL analysis over polyglot: query analysis, binding, semantic validation and usage, column references, type normalization, FAST/RICH lineage, SQL-test/scenario extraction and compiler-side assembly, and Snowflake cost statement classification and query-tag parsing. |
| `sqlbuild-rules` | Built-in rules and the rules engine, the custom-rule host, SQL lint, quality checks and formatting, rules configuration and the request models. It also owns the build identity script. |
| `sqlbuild-refactor` | Native rename/move plans, contract-adoption edits, MODEL/SCHEMA/YAML text edits and atomic staging/commit with rollback. Shared scope placement delegates to the native scope implementation; user Python source inspection remains at the Python AST boundary. |
| `sqlbuild-cli` | The `sqb` and `sqlbuild` executables: `--version`, unchanged compiles replayed from the shared reuse store with Python's exact streams, exit code and compute log capture, and a handoff of every other invocation to the installed `sqlbuild-python` script. Built as its own wheel (`crates/sqlbuild-cli/pyproject.toml`). |
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Python's setting help texts (`sqlbuild.errors.setting_help`) for string settings.
//! Setting help texts for string settings, matching the shared native setting help.

use crate::contracts::constants::{ADDITIONAL_HELP_SEPARATOR, SETTING_SNIPPET_INDENT};

Expand Down
1 change: 1 addition & 0 deletions crates/sqlbuild-analysis/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pub mod resource_diagnostics;
pub mod semantic_checks;
pub mod semantic_usage;
pub mod semantic_validation;
pub mod snowflake_cost;
pub mod sql_test_attachment;
pub mod sql_tokens;
pub mod target_paths;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub(crate) mod statements;
43 changes: 43 additions & 0 deletions crates/sqlbuild-analysis/src/snowflake_cost/_helpers/statements.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//! Query-type prefixes, Python whitespace and CREATE statement parsing.

use polyglot_sql::{Dialect, DialectType, Expression, ParseOptions};

use crate::snowflake_cost::constants::{CREATE_VARIANT_PREFIX, DYNAMIC_TABLE_MODIFIER};

pub(crate) fn of_kind(query_type: &str, kind: &str) -> bool {
query_type == kind
|| query_type
.strip_prefix(kind)
.is_some_and(|rest| rest.starts_with('_'))
}

/// Python's `str.isspace` for one character.
pub(crate) fn python_space(character: char) -> bool {
character.is_whitespace() || ('\u{1c}'..='\u{1f}').contains(&character)
}

/// Whether a CREATE statement's text parses as a definition that needs no warehouse.
pub(crate) fn metadata_only_create(query_text: &str) -> bool {
let options: ParseOptions = ParseOptions {
complexity_guard: None,
};
let Ok(mut statements) =
Dialect::get(DialectType::Snowflake).parse_with_options(query_text, &options)
else {
return false;
};
if statements.len() != 1 {
return false;
}
match statements.remove(0) {
Expression::CreateTable(table) => {
!(table.as_select.is_some()
|| table.clone_source.is_some()
|| table.using_template.is_some()
|| table.table_modifier.as_deref().unwrap_or("").to_uppercase()
== DYNAMIC_TABLE_MODIFIER)
}
Expression::CreateView(view) if view.materialized => false,
statement => statement.variant_name().starts_with(CREATE_VARIANT_PREFIX),
}
}
10 changes: 10 additions & 0 deletions crates/sqlbuild-analysis/src/snowflake_cost/constants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//! Snowflake query types, table modifiers and the SQLBuild query-tag identity.

pub(crate) const USE_QUERY_TYPE: &str = "USE";
pub(crate) const DROP_QUERY_TYPE: &str = "DROP";
pub(crate) const CREATE_QUERY_TYPE: &str = "CREATE";
pub(crate) const DYNAMIC_TABLE_MODIFIER: &str = "DYNAMIC";
pub(crate) const CREATE_VARIANT_PREFIX: &str = "create";
pub(crate) const QUERY_TAG_APP: &str = "sqlbuild";
pub(crate) const QUERY_TAG_APP_KEY: &str = "app";
pub(crate) const QUERY_TAG_VERSION_KEY: &str = "v";
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//! `metadata_only_statement`: Snowflake statements that run without warehouse compute.

use crate::snowflake_cost::_helpers::statements::{metadata_only_create, of_kind, python_space};
use crate::snowflake_cost::constants::{CREATE_QUERY_TYPE, DROP_QUERY_TYPE, USE_QUERY_TYPE};

/// Identify statements Snowflake executes without warehouse compute.
pub fn metadata_only_statement(query_type: &str, query_text: Option<&str>) -> bool {
let normalized: String = query_type.to_uppercase().replace(' ', "_");
if of_kind(&normalized, USE_QUERY_TYPE) || of_kind(&normalized, DROP_QUERY_TYPE) {
return true;
}
if !of_kind(&normalized, CREATE_QUERY_TYPE) {
return false;
}
match query_text {
Some(text) if !text.chars().all(python_space) => metadata_only_create(text),
_ => false,
}
}
2 changes: 2 additions & 0 deletions crates/sqlbuild-analysis/src/snowflake_cost/main/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod metadata_only_statement;
pub mod sqlbuild_query_tag;
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//! `sqlbuild_query_tag`: the run attribution of a SQLBuild Snowflake `QUERY_TAG`.

use serde_json::Value;

use crate::snowflake_cost::constants::{QUERY_TAG_APP, QUERY_TAG_APP_KEY, QUERY_TAG_VERSION_KEY};
use crate::snowflake_cost::models::QueryTag;

/// Python's `value == 1` for a decoded JSON value.
fn equals_one(value: Option<&Value>) -> bool {
match value {
Some(Value::Bool(flag)) => *flag,
Some(Value::Number(number)) => number
.as_i64()
.map(|integer| integer == 1)
.or_else(|| number.as_u64().map(|integer| integer == 1))
.unwrap_or_else(|| number.as_f64() == Some(1.0)),
_ => false,
}
}

fn text(payload: &serde_json::Map<String, Value>, key: &str) -> Option<String> {
match payload.get(key) {
Some(Value::String(value)) if !value.is_empty() => Some(value.clone()),
_ => None,
}
}

/// The tag's attribution when it is a SQLBuild version-1 JSON object, else `None`.
pub fn sqlbuild_query_tag(value: &str) -> Option<QueryTag> {
if value.is_empty() {
return None;
}
let Ok(Value::Object(payload)) = serde_json::from_str::<Value>(value) else {
return None;
};
let app_matches: bool =
matches!(payload.get(QUERY_TAG_APP_KEY), Some(Value::String(app)) if app == QUERY_TAG_APP);
if !app_matches || !equals_one(payload.get(QUERY_TAG_VERSION_KEY)) {
return None;
}
Some(QueryTag {
run_id: text(&payload, "run_id"),
resource_type: text(&payload, "resource_type"),
resource_name: text(&payload, "resource_name"),
})
}
7 changes: 7 additions & 0 deletions crates/sqlbuild-analysis/src/snowflake_cost/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
pub(crate) mod _helpers;
pub(crate) mod constants;
pub mod main;
pub mod models;

#[cfg(test)]
mod tests;
9 changes: 9 additions & 0 deletions crates/sqlbuild-analysis/src/snowflake_cost/models.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//! The run attribution a SQLBuild Snowflake query tag carries.

/// Non-empty string fields of a SQLBuild query tag.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct QueryTag {
pub run_id: Option<String>,
pub resource_type: Option<String>,
pub resource_name: Option<String>,
}
4 changes: 4 additions & 0 deletions crates/sqlbuild-analysis/src/snowflake_cost/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#[path = "tests/test_snowflake_cost.rs"]
mod snowflake_cost;
#[path = "tests/test_types.rs"]
mod test_types;
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use crate::snowflake_cost::main::metadata_only_statement::metadata_only_statement;
use crate::snowflake_cost::main::sqlbuild_query_tag::sqlbuild_query_tag;
use crate::snowflake_cost::models::QueryTag;
use crate::snowflake_cost::tests::test_types::{MetadataOnlyTestCase, QueryTagTestCase};

#[test]
fn given_snowflake_statements_when_classifying_then_detects_metadata_only_work() {
let test_cases = [
MetadataOnlyTestCase {
description: "use statements need no warehouse",
query_type: "USE_DATABASE",
query_text: None,
expected_metadata_only: true,
},
MetadataOnlyTestCase {
description: "a column-list table is metadata only",
query_type: "CREATE_TABLE",
query_text: Some("CREATE TABLE t (a INT)"),
expected_metadata_only: true,
},
MetadataOnlyTestCase {
description: "a table built from a query uses the warehouse",
query_type: "CREATE_TABLE",
query_text: Some("CREATE TABLE t AS SELECT 1 AS a"),
expected_metadata_only: false,
},
MetadataOnlyTestCase {
description: "a materialized view uses the warehouse",
query_type: "CREATE_VIEW",
query_text: Some("CREATE MATERIALIZED VIEW v AS SELECT 1 AS a"),
expected_metadata_only: false,
},
MetadataOnlyTestCase {
description: "blank create text is not classified",
query_type: "CREATE",
query_text: Some(" "),
expected_metadata_only: false,
},
MetadataOnlyTestCase {
description: "a create-prefixed type without separator is ordinary work",
query_type: "CREATEX",
query_text: Some("CREATE TABLE t (a INT)"),
expected_metadata_only: false,
},
];

for test_case in test_cases {
assert_eq!(
metadata_only_statement(test_case.query_type, test_case.query_text),
test_case.expected_metadata_only,
"{}",
test_case.description
);
}
}

#[test]
fn given_query_tags_when_parsing_then_reads_sqlbuild_attribution() {
let test_cases = [
QueryTagTestCase {
description: "a version-one tag keeps non-empty strings",
value: r#"{"app": "sqlbuild", "v": 1.0, "run_id": "r", "resource_name": ""}"#,
expected_tag: Some(QueryTag {
run_id: Some("r".to_owned()),
resource_type: None,
resource_name: None,
}),
},
QueryTagTestCase {
description: "true equals one",
value: r#"{"app": "sqlbuild", "v": true}"#,
expected_tag: Some(QueryTag::default()),
},
QueryTagTestCase {
description: "other versions are ignored",
value: r#"{"app": "sqlbuild", "v": 2}"#,
expected_tag: None,
},
QueryTagTestCase {
description: "non-objects are ignored",
value: "[]",
expected_tag: None,
},
];

for test_case in test_cases {
assert_eq!(
sqlbuild_query_tag(test_case.value),
test_case.expected_tag,
"{}",
test_case.description
);
}
}
14 changes: 14 additions & 0 deletions crates/sqlbuild-analysis/src/snowflake_cost/tests/test_types.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
use crate::snowflake_cost::models::QueryTag;

pub(crate) struct MetadataOnlyTestCase {
pub(crate) description: &'static str,
pub(crate) query_type: &'static str,
pub(crate) query_text: Option<&'static str>,
pub(crate) expected_metadata_only: bool,
}

pub(crate) struct QueryTagTestCase {
pub(crate) description: &'static str,
pub(crate) value: &'static str,
pub(crate) expected_tag: Option<QueryTag>,
}
32 changes: 32 additions & 0 deletions crates/sqlbuild-config/src/command_connection/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,35 @@ pub(crate) const UNSUPPORTED_WAREHOUSE_CODE: &str = "C260";
pub(crate) const SETTING_SNIPPET_INDENT: &str = " ";
/// Two quotes around at least one character.
pub(crate) const MIN_QUOTED_IDENTIFIER_LENGTH: usize = 3;
/// The warehouse group each CLI command selects a target's default warehouse from.
pub const COMMAND_WAREHOUSE_GROUPS: [(&str, Option<&str>); 29] = [
("audit", Some("build")),
("build", Some("build")),
("check", Some("build")),
("clone", Some("build")),
("dbt", Some("build")),
("load", Some("build")),
("plan", Some("build")),
("seed", Some("build")),
("contract", Some("query")),
("cost", Some("query")),
("debug", Some("query")),
("diff", Some("query")),
("freshness", Some("query")),
("query", Some("query")),
("janitor", Some("query")),
("scenario", Some("query")),
("test", Some("query")),
("clean", None),
("compile", None),
("dag", None),
("format", None),
("init", None),
("lineage", None),
("mv", None),
("playground", None),
("rename", None),
("rules", None),
("scope", None),
("skills", None),
];
3 changes: 3 additions & 0 deletions crates/sqlbuild-config/src/project/_helpers/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ use crate::project::errors::ConfigFailure;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum SettingValue<'a> {
Bool(bool),
/// An integer, as its decimal text.
Integer(&'a str),
Text(&'a str),
}

Expand All @@ -29,6 +31,7 @@ fn toml_value(value: SettingValue<'_>) -> String {
match value {
SettingValue::Bool(true) => "true".to_owned(),
SettingValue::Bool(false) => "false".to_owned(),
SettingValue::Integer(digits) => digits.to_owned(),
SettingValue::Text(text) => {
format!("\"{}\"", text.replace('\\', "\\\\").replace('"', "\\\""))
}
Expand Down
2 changes: 2 additions & 0 deletions crates/sqlbuild-config/src/project/main/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@ pub mod load_project_config;
pub mod local_sql_analysis_failure;
pub mod microbatch_guidance;
pub mod resolve_config_file;
pub mod setting_help_text;
pub mod snippet_help_text;
pub mod sql_analysis_off_guidance;
pub mod unsupported_target_warehouse;
23 changes: 23 additions & 0 deletions crates/sqlbuild-config/src/project/main/setting_help_text.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//! Help that shows the exact setting to write.

use crate::project::_helpers::help::{Setting, SettingValue, setting_help};
use crate::project::models::{SettingSite, SettingText};

/// `<purpose>, set this in <file>:` followed by the exact TOML section and key.
#[must_use]
pub fn setting_help_text(purpose: &str, site: SettingSite<'_>, value: &SettingText) -> String {
let value: SettingValue<'_> = match value {
SettingText::Bool(flag) => SettingValue::Bool(*flag),
SettingText::Integer(digits) => SettingValue::Integer(digits),
SettingText::Text(text) => SettingValue::Text(text),
};
setting_help(
purpose,
&Setting {
file_name: site.file_name,
section: site.section,
key: site.key,
value,
},
)
}
13 changes: 13 additions & 0 deletions crates/sqlbuild-config/src/project/main/snippet_help_text.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//! Help that shows an exact authored snippet to add.

use crate::project::constants::SETTING_SNIPPET_INDENT;

/// `<purpose>, <target>:` followed by the exact lines to write, one per line.
#[must_use]
pub fn snippet_help_text(purpose: &str, target: &str, lines: &[String]) -> String {
let snippet: Vec<String> = lines
.iter()
.map(|line| format!("{SETTING_SNIPPET_INDENT}{line}"))
.collect();
format!("{purpose}, {target}:\n{}", snippet.join("\n"))
}
Loading
Loading