Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
c0834dd
wip(native): native SQL interpolation and template host (Ports A, pau…
kvlonge Oct 9, 2026
61f2897
feat(native): interpolate SQL variables and expand templates natively…
kvlonge Oct 9, 2026
3d5ef0f
feat(native): declaration files, scopes and @enum/@const scans run na…
kvlonge Oct 9, 2026
102c76a
feat(native): parse macro arguments natively with located errors and …
kvlonge Oct 9, 2026
f629b36
feat(native): expand SQL-test macro mocks through the macro bridge an…
kvlonge Oct 9, 2026
302fbba
feat(native): reject lone surrogates where env vars, vars and macro o…
kvlonge Oct 9, 2026
4cce632
test(native): count template, effective-var, macro-argument and scope…
kvlonge Oct 9, 2026
e5d4eec
docs(macros): note the rejected macro argument literals and located e…
kvlonge Oct 9, 2026
131503b
test(native): stage-switch sabotage covers only stages that still swi…
kvlonge Oct 9, 2026
a3e7ef3
refactor(native): satisfy the structure rules for the expansion ports
kvlonge Oct 9, 2026
8e69e54
fix(native): address the expansion port review: no secret echo, locat…
kvlonge Oct 9, 2026
2390d0f
docs(macros): say where macro argument errors are located
kvlonge Oct 9, 2026
12064cd
feat(native): scan SQL references natively under every engine; delete…
kvlonge Oct 9, 2026
da69a4f
feat(native): build and validate model config natively under every en…
kvlonge Oct 9, 2026
208eedd
feat(native): parse MODEL header columns and audits natively for ever…
kvlonge Oct 9, 2026
44ac5a7
feat(native): validate, reject and render cursor intrinsics natively;…
kvlonge Oct 9, 2026
764d56c
feat(native): render attached generic audits natively without deferra…
kvlonge Oct 9, 2026
71eca3e
wip(native): omitted SELECT, scenario CTEs, independence check and re…
kvlonge Oct 9, 2026
79e9207
feat(native): scan SQL tests and scenarios natively, including the in…
kvlonge Oct 9, 2026
ffa316a
feat(native): resolve function headers and namespaces natively on eve…
kvlonge Oct 9, 2026
958fd84
feat(native): validate function types with in-process Rust Polyglot; …
kvlonge Oct 9, 2026
602d7ca
wip(native): resolve path-only declaration contexts natively (baselin…
kvlonge Oct 9, 2026
6beda27
feat(native): resolve declaration contexts of path-only consumers nat…
kvlonge Oct 9, 2026
63a9aca
fix(native): adapt the native project-assembly template host to the d…
kvlonge Oct 9, 2026
8bfccdb
test(native): regenerate baselines after rebasing onto the native pro…
kvlonge Oct 9, 2026
bae2af0
refactor(compiler): remove the inert bridge_independent error flag
kvlonge Oct 9, 2026
71961e0
wip(native): fensu policy fixes (doc comments, layering, dead entries)
kvlonge Oct 9, 2026
cbc86bb
chore(native): satisfy the fensu policy for the close-out ports
kvlonge Oct 9, 2026
4e46040
test(native): switch only stages that still have a Python path in the…
kvlonge Oct 9, 2026
d633c11
test(native): replace the self-comparing function-header parity test …
kvlonge Oct 9, 2026
8cb0972
feat(native): count native answers for attachments, cursor intrinsics…
kvlonge Oct 9, 2026
224b1df
fix(native): reject every config integer beyond 64 bits with sign-awa…
kvlonge Oct 9, 2026
a6a7f56
chore(native): regenerate the fallback allow-list with restored nativ…
kvlonge Oct 9, 2026
df300a3
chore(native): name the failure-corpus help arguments for the fensu p…
kvlonge Oct 9, 2026
d01a42e
fix(native): drop the deferred reference-scan arm the batch's SQL-tes…
kvlonge Oct 9, 2026
4ff6b8c
chore(native): regenerate the fallback allow-list on the M4 batch
kvlonge Oct 9, 2026
0cb62d9
fix(native): keep the overflow wording out of the setting-help scan a…
kvlonge Oct 9, 2026
b7a60ae
test(native): count native MODEL header metadata parses
kvlonge Oct 9, 2026
516661d
chore(native): record the close-out crate dependency in Cargo.lock
kvlonge Oct 10, 2026
22482a6
Merge remote-tracking branch 'origin/main' into feat/native-model-con…
kvlonge Oct 10, 2026
450f9d1
test(native): give the worst-case reference scan bound headroom for l…
kvlonge Oct 10, 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
  •  
  •  
  •  
3 changes: 3 additions & 0 deletions Cargo.lock

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

Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ use sqlbuild_model_config::templates::models::{
use sqlbuild_model_config::templates::types::TemplateHost;

use crate::assembly::project::constants::{
PYTHON_FALSE, PYTHON_NONE, PYTHON_TRUE, TEMPLATE_DEFERRAL, TEMPLATE_OPEN_TOKEN,
PROJECT_VAR_FALSE, PROJECT_VAR_TRUE, PYTHON_FALSE, PYTHON_NONE, PYTHON_TRUE, TEMPLATE_DEFERRAL,
TEMPLATE_LABEL, TEMPLATE_OPEN_TOKEN,
};
use crate::assembly::project::models::{InputRead, Variable};
use crate::assembly::project::types::Fact;
Expand Down Expand Up @@ -43,7 +44,7 @@ impl TemplateHost for ScalarHost<'_> {
{
None => Ok(None),
Some((_, Variable::Scalar(value))) => Ok(Some(value.clone())),
Some((_, Variable::Unsupported)) => Err(TemplateFailure::Unsupported),
Some((_, Variable::Unsupported)) => Err(TemplateFailure::Host),
}
}

Expand All @@ -58,7 +59,7 @@ impl TemplateHost for ScalarHost<'_> {
.iter()
.find(|(variable, _)| variable == name)
{
None => Err(TemplateFailure::Unsupported),
None => Err(TemplateFailure::Host),
Some((_, value)) => Ok(value.clone().map(Scalar::Text)),
}
}
Expand Down Expand Up @@ -87,8 +88,21 @@ impl TemplateHost for ScalarHost<'_> {
Scalar::Null
}

fn scalar(&self, value: &Self::Value) -> Option<Scalar> {
Some(value.clone())
fn scalar(&self, value: &Self::Value) -> Result<Scalar, TemplateFailure> {
Ok(value.clone())
}

fn render(&self, value: &Self::Value, _label: &str) -> Result<String, TemplateFailure> {
Ok(match value {
Scalar::Null => String::new(),
Scalar::Bool(true) => PROJECT_VAR_TRUE.to_owned(),
Scalar::Bool(false) => PROJECT_VAR_FALSE.to_owned(),
Scalar::Text(text) => text.clone(),
})
}

fn label(&self) -> &str {
TEMPLATE_LABEL
}
}

Expand Down
5 changes: 5 additions & 0 deletions crates/sqlbuild-analysis/src/assembly/project/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ pub(crate) const DESTINATION_TABLE_CONTEXT: &str = "destination.table";
pub(crate) const DESTINATION_QUALIFIED_CONTEXT: &str = "destination.qualified";
pub(crate) const AUDIT_TARGET_DEFERRAL: &str = "audit_target_kind";
pub(crate) const TEMPLATE_DEFERRAL: &str = "target_template";
/// `render_project_var_text` of booleans inside a larger template string.
pub(crate) const PROJECT_VAR_TRUE: &str = "true";
pub(crate) const PROJECT_VAR_FALSE: &str = "false";
/// The label template errors would name; assembly defers every template error.
pub(crate) const TEMPLATE_LABEL: &str = "project assembly template";
pub(crate) const PRESERVED_NAMESPACE_DEFERRAL: &str = "preserved_namespace";
pub(crate) const SYNTAX_DEFERRAL: &str = "syntax_error";
pub(crate) const PLACEHOLDER_DEFERRAL: &str = "placeholder_word";
Expand Down
1 change: 1 addition & 0 deletions crates/sqlbuild-analysis/src/compiler/_helpers/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub(crate) mod polyglot;
pub mod sql_tests;
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//! Function argument and return types parsed with the adapter's SQL analysis dialect.

use polyglot_sql::Dialect;

use crate::compiler::_helpers::polyglot::parse_options::guarded_parse_options;
use crate::compiler::constants::FUNCTION_TYPE_DIALECTS;

/// The error for a type the adapter's dialect cannot parse; adapters without a dialect pass.
pub(crate) fn function_type_error(
type_sql: &str,
adapter_name: &str,
context: &str,
) -> Result<Option<String>, String> {
let Some((_, dialect_name, dialect)) = FUNCTION_TYPE_DIALECTS
.iter()
.find(|(adapter, _, _)| *adapter == adapter_name)
else {
return Ok(None);
};
let options = guarded_parse_options().map_err(|error| error.to_string())?;
Ok(Dialect::get(*dialect)
.parse_data_type_with_options(type_sql, &options)
.err()
.map(|error| {
format!(
"{context} type '{type_sql}' is not valid for adapter '{adapter_name}' SQL \
analysis dialect '{dialect_name}': {error}"
)
}))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub(crate) mod function_types;
pub(crate) mod parse_options;
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//! The Polyglot parse options SQLBuild's wheel proxy applies to every parse.

use polyglot_sql::{ComplexityGuardOptions, ParseOptions};

use crate::lineage::constants::MAX_FUNCTION_CALL_DEPTH;

/// The proxy's complexity guard, decoded as the wheel decodes it.
pub(crate) fn guarded_parse_options() -> Result<ParseOptions, serde_json::Error> {
let guard: ComplexityGuardOptions = serde_json::from_value(serde_json::json!({
"maxFunctionCallDepth": MAX_FUNCTION_CALL_DEPTH,
}))?;
Ok(ParseOptions {
complexity_guard: Some(guard),
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
//! Expected results and assertions must not define or read each other, even through helpers.

use std::collections::{BTreeSet, HashMap, HashSet, VecDeque};

use polyglot_sql::{Dialect, DialectType, Expression, ExpressionWalk};
use serde_json::Value;
use sqlbuild_sqltext::sql_scan::main::dialect_non_code_end::dialect_non_code_end;
use sqlbuild_sqltext::sql_scan::models::{LexicalSyntax, Unclosed};

use crate::compiler::_helpers::polyglot::parse_options::guarded_parse_options;
use crate::compiler::constants::{ALIAS_FIELD, CTES_FIELD, NAME_FIELD};

const DIRECT_DEPENDENCY_PATH_LENGTH: usize = 2;
const TABLE_KIND: &str = "table";
const IDENTIFIER_QUOTES: &[u8] = b"\"`";

/// The check CTEs of one file and the labels its messages name.
pub(crate) struct IndependenceCheck<'a> {
pub(crate) ctes: &'a [(String, String)],
pub(crate) expected_prefix: &'a str,
pub(crate) assertion_prefix: &'a str,
pub(crate) file_label: &'a str,
pub(crate) context_label: &'a str,
}

/// The first independence error of `check`, if any.
pub(crate) fn independence_error(
check: &IndependenceCheck<'_>,
syntax: &LexicalSyntax,
) -> Option<String> {
let mut names_by_key: HashMap<String, &str> = HashMap::new();
for (name, _) in check.ctes {
names_by_key.insert(casefold(name), name);
}
let keys_with_prefix = |prefix: &str| -> BTreeSet<String> {
names_by_key
.iter()
.filter(|(_, name)| name.starts_with(prefix))
.map(|(key, _)| key.clone())
.collect()
};
let expected: BTreeSet<String> = keys_with_prefix(check.expected_prefix);
let assertions: BTreeSet<String> = keys_with_prefix(check.assertion_prefix);
for (name, body) in check.ctes {
let key: String = casefold(name);
let (prohibited_prefix, prohibited_label) = if expected.contains(&key) {
(check.assertion_prefix, "assertion")
} else if assertions.contains(&key) {
(check.expected_prefix, "expected result")
} else {
continue;
};
if !casefold(body).contains(&casefold(prohibited_prefix)) {
continue;
}
if let Some(nested) = defined_cte_names(body)
.into_iter()
.find(|nested| nested.starts_with(prohibited_prefix))
{
return Some(format!(
"{} '{}' check CTE '{name}' must not define {prohibited_label} CTE '{nested}'; \
expected results and assertions must be independent",
check.context_label, check.file_label
));
}
}
if expected.is_empty() || assertions.is_empty() {
return None;
}
let mut dependencies: HashMap<String, Vec<String>> = HashMap::new();
for (name, body) in check.ctes {
let references = match known_cte_references(body, &names_by_key, syntax) {
Ok(references) => references,
Err(message) => return Some(message),
};
dependencies.insert(casefold(name), references);
}
for (origins, prohibited) in [(&expected, &assertions), (&assertions, &expected)] {
for origin in origins {
let Some(path) = dependency_path(origin, prohibited, &dependencies) else {
continue;
};
let rendered: Vec<&str> = path.iter().map(|key| names_by_key[key]).collect();
let through: String = if rendered.len() > DIRECT_DEPENDENCY_PATH_LENGTH {
format!(
" through {}",
rendered[1..rendered.len() - 1]
.iter()
.map(|name| format!("'{name}'"))
.collect::<Vec<String>>()
.join(" -> ")
)
} else {
String::new()
};
return Some(format!(
"{} '{}' check CTE '{}' must not depend on '{}'{through}; expected results and \
assertions must be independent",
check.context_label,
check.file_label,
rendered[0],
rendered[rendered.len() - 1]
));
}
}
None
}

/// `str.casefold`, through Rust's Unicode lower-case mapping.
fn casefold(text: &str) -> String {
text.to_lowercase()
}

/// `parse_one(sql, dialect="generic")` under SQLBuild's complexity guard, or `None` on error.
fn parse_one(sql: &str) -> Option<Expression> {
let Ok(options) = guarded_parse_options() else {
return None;
};
let Ok(mut statements) = Dialect::get(DialectType::Generic).parse_with_options(sql, &options)
else {
return None;
};
(statements.len() == 1).then(|| statements.remove(0))
}

/// Every CTE alias defined anywhere in `sql`, in the order `to_dict()` lists them.
fn defined_cte_names(sql: &str) -> Vec<String> {
let Some(parsed) = parse_one(sql) else {
return Vec::new();
};
let Ok(value) = serde_json::to_value(&parsed) else {
return Vec::new();
};
collect_cte_names(&value, Vec::new())
}

/// `names` followed by each new CTE alias under `value`, depth first.
fn collect_cte_names(value: &Value, mut names: Vec<String>) -> Vec<String> {
match value {
Value::Object(fields) => {
if let Some(Value::Array(ctes)) = fields.get(CTES_FIELD) {
for cte in ctes {
if let Some(Value::String(name)) = cte
.get(ALIAS_FIELD)
.filter(|alias| alias.is_object())
.and_then(|alias| alias.get(NAME_FIELD))
&& !names.contains(name)
{
names.push(name.clone());
}
}
}
fields
.values()
.fold(names, |names, child| collect_cte_names(child, names))
}
Value::Array(items) => items
.iter()
.fold(names, |names, item| collect_cte_names(item, names)),
_ => names,
}
}

/// The keys of the file's CTEs that `sql` reads, in reading order.
fn known_cte_references(
sql: &str,
names_by_key: &HashMap<String, &str>,
syntax: &LexicalSyntax,
) -> Result<Vec<String>, String> {
let folded: String = casefold(sql);
if !names_by_key.keys().any(|key| folded.contains(key.as_str())) {
return Ok(Vec::new());
}
let Some(parsed) = parse_one(sql) else {
return identifier_references(sql, names_by_key, syntax);
};
let mut references: Vec<String> = Vec::new();
for table in parsed
.dfs()
.skip(1)
.filter(|node| node.variant_name() == TABLE_KIND)
{
references = push_known(references, casefold(table.get_name()), names_by_key);
}
Ok(references)
}

/// The identifier scan used when Polyglot cannot parse a body.
fn identifier_references(
sql: &str,
names_by_key: &HashMap<String, &str>,
syntax: &LexicalSyntax,
) -> Result<Vec<String>, String> {
let bytes: &[u8] = sql.as_bytes();
let mut references: Vec<String> = Vec::new();
let mut index: usize = 0;
while let Some(character) = sql[index..].chars().next() {
match dialect_non_code_end(bytes, index, syntax) {
Err(construct) => return Err(unclosed_message(construct)),
Ok(Some(end)) if IDENTIFIER_QUOTES.contains(&bytes[index]) => {
let quote: &str = &sql[index..=index];
let name: String = sql[index + 1..end - 1].replace(&quote.repeat(2), quote);
references = push_known(references, casefold(&name), names_by_key);
index = end;
}
Ok(Some(end)) => index = end,
Ok(None) if character.is_alphabetic() || character == '_' => {
let end: usize = sql[index..]
.char_indices()
.skip(1)
.find(|(_, next)| !(next.is_alphanumeric() || *next == '_'))
.map_or(sql.len(), |(offset, _)| index + offset);
references = push_known(references, casefold(&sql[index..end]), names_by_key);
index = end;
}
Ok(None) => index += character.len_utf8(),
}
}
Ok(references)
}

/// `references` with `key` appended when it names a file CTE not yet read.
fn push_known(
mut references: Vec<String>,
key: String,
names_by_key: &HashMap<String, &str>,
) -> Vec<String> {
if names_by_key.contains_key(&key) && !references.contains(&key) {
references.push(key);
}
references
}

fn unclosed_message(construct: Unclosed) -> String {
let construct: &str = match construct {
Unclosed::BlockComment => "block comment",
Unclosed::Quote => "quoted string",
Unclosed::Parenthesis => "parenthesis",
};
format!("SQL test contains an unclosed {construct}")
}

/// The shortest dependency path from `origin` to a prohibited CTE, breadth first.
fn dependency_path(
origin: &str,
prohibited: &BTreeSet<String>,
dependencies: &HashMap<String, Vec<String>>,
) -> Option<Vec<String>> {
let mut pending: VecDeque<Vec<String>> = VecDeque::from([vec![origin.to_owned()]]);
let mut visited: HashSet<String> = HashSet::from([origin.to_owned()]);
while let Some(path) = pending.pop_front() {
let last: &String = path.last()?;
for dependency in dependencies.get(last).into_iter().flatten() {
let mut candidate: Vec<String> = path.clone();
candidate.push(dependency.clone());
if prohibited.contains(dependency) {
return Some(candidate);
}
if visited.insert(dependency.clone()) {
pending.push_back(candidate);
}
}
}
None
}
Loading
Loading