Skip to content

Commit 2cec8a4

Browse files
authored
feat(native): close out M3 shipped-stage ports and delete their Python fallbacks (#1077)
* wip(native): native SQL interpolation and template host (Ports A, paused) * feat(native): interpolate SQL variables and expand templates natively; delete the Python paths * feat(native): declaration files, scopes and @enum/@const scans run natively only; delete their Python fallbacks * feat(native): parse macro arguments natively with located errors and help; delete the Python argument parser * feat(native): expand SQL-test macro mocks through the macro bridge and reuse the native scope lookup * feat(native): reject lone surrogates where env vars, vars and macro output enter; delete the fallbacks they forced * test(native): count template, effective-var, macro-argument and scope-lookup answers; regenerate baselines * docs(macros): note the rejected macro argument literals and located errors * test(native): stage-switch sabotage covers only stages that still switch to Python * refactor(native): satisfy the structure rules for the expansion ports * fix(native): address the expansion port review: no secret echo, located macro argument errors, honest native counts * docs(macros): say where macro argument errors are located * feat(native): scan SQL references natively under every engine; delete the Python reference scanner * feat(native): build and validate model config natively under every engine; delete the Python config builder and validators * feat(native): parse MODEL header columns and audits natively for every model; delete the Python header metadata fallback * feat(native): validate, reject and render cursor intrinsics natively; delete the Python intrinsic scanner * feat(native): render attached generic audits natively without deferrals; delete the Python audit renderer * wip(native): omitted SELECT, scenario CTEs, independence check and relationship scans natively (baselines pending) * feat(native): scan SQL tests and scenarios natively, including the independence check; delete the Python scanners and fallbacks * feat(native): resolve function headers and namespaces natively on every engine; delete the Python header parsers * feat(native): validate function types with in-process Rust Polyglot; drop the wheel parse_data_type call * wip(native): resolve path-only declaration contexts natively (baselines pending) * feat(native): resolve declaration contexts of path-only consumers natively; delete the Python projection * fix(native): adapt the native project-assembly template host to the deferral-free template trait * test(native): regenerate baselines after rebasing onto the native project assembly * refactor(compiler): remove the inert bridge_independent error flag * wip(native): fensu policy fixes (doc comments, layering, dead entries) * chore(native): satisfy the fensu policy for the close-out ports * test(native): switch only stages that still have a Python path in the fallback gate sabotage * test(native): replace the self-comparing function-header parity test with failure-corpus cases * feat(native): count native answers for attachments, cursor intrinsics, declaration contexts and relationship scans * fix(native): reject every config integer beyond 64 bits with sign-aware help in release error order * chore(native): regenerate the fallback allow-list with restored native answer counters * chore(native): name the failure-corpus help arguments for the fensu policy * fix(native): drop the deferred reference-scan arm the batch's SQL-test mock reads still matched * chore(native): regenerate the fallback allow-list on the M4 batch * fix(native): keep the overflow wording out of the setting-help scan and cap header parity counts at 64 bits * test(native): count native MODEL header metadata parses * chore(native): record the close-out crate dependency in Cargo.lock * test(native): give the worst-case reference scan bound headroom for loaded CI runners
1 parent db61d77 commit 2cec8a4

379 files changed

Lines changed: 12428 additions & 13229 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎Cargo.lock‎

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎crates/sqlbuild-analysis/src/assembly/project/_helpers/templates.rs‎

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ use sqlbuild_model_config::templates::models::{
99
use sqlbuild_model_config::templates::types::TemplateHost;
1010

1111
use crate::assembly::project::constants::{
12-
PYTHON_FALSE, PYTHON_NONE, PYTHON_TRUE, TEMPLATE_DEFERRAL, TEMPLATE_OPEN_TOKEN,
12+
PROJECT_VAR_FALSE, PROJECT_VAR_TRUE, PYTHON_FALSE, PYTHON_NONE, PYTHON_TRUE, TEMPLATE_DEFERRAL,
13+
TEMPLATE_LABEL, TEMPLATE_OPEN_TOKEN,
1314
};
1415
use crate::assembly::project::models::{InputRead, Variable};
1516
use crate::assembly::project::types::Fact;
@@ -43,7 +44,7 @@ impl TemplateHost for ScalarHost<'_> {
4344
{
4445
None => Ok(None),
4546
Some((_, Variable::Scalar(value))) => Ok(Some(value.clone())),
46-
Some((_, Variable::Unsupported)) => Err(TemplateFailure::Unsupported),
47+
Some((_, Variable::Unsupported)) => Err(TemplateFailure::Host),
4748
}
4849
}
4950

@@ -58,7 +59,7 @@ impl TemplateHost for ScalarHost<'_> {
5859
.iter()
5960
.find(|(variable, _)| variable == name)
6061
{
61-
None => Err(TemplateFailure::Unsupported),
62+
None => Err(TemplateFailure::Host),
6263
Some((_, value)) => Ok(value.clone().map(Scalar::Text)),
6364
}
6465
}
@@ -87,8 +88,21 @@ impl TemplateHost for ScalarHost<'_> {
8788
Scalar::Null
8889
}
8990

90-
fn scalar(&self, value: &Self::Value) -> Option<Scalar> {
91-
Some(value.clone())
91+
fn scalar(&self, value: &Self::Value) -> Result<Scalar, TemplateFailure> {
92+
Ok(value.clone())
93+
}
94+
95+
fn render(&self, value: &Self::Value, _label: &str) -> Result<String, TemplateFailure> {
96+
Ok(match value {
97+
Scalar::Null => String::new(),
98+
Scalar::Bool(true) => PROJECT_VAR_TRUE.to_owned(),
99+
Scalar::Bool(false) => PROJECT_VAR_FALSE.to_owned(),
100+
Scalar::Text(text) => text.clone(),
101+
})
102+
}
103+
104+
fn label(&self) -> &str {
105+
TEMPLATE_LABEL
92106
}
93107
}
94108

‎crates/sqlbuild-analysis/src/assembly/project/constants.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ pub(crate) const DESTINATION_TABLE_CONTEXT: &str = "destination.table";
2828
pub(crate) const DESTINATION_QUALIFIED_CONTEXT: &str = "destination.qualified";
2929
pub(crate) const AUDIT_TARGET_DEFERRAL: &str = "audit_target_kind";
3030
pub(crate) const TEMPLATE_DEFERRAL: &str = "target_template";
31+
/// `render_project_var_text` of booleans inside a larger template string.
32+
pub(crate) const PROJECT_VAR_TRUE: &str = "true";
33+
pub(crate) const PROJECT_VAR_FALSE: &str = "false";
34+
/// The label template errors would name; assembly defers every template error.
35+
pub(crate) const TEMPLATE_LABEL: &str = "project assembly template";
3136
pub(crate) const PRESERVED_NAMESPACE_DEFERRAL: &str = "preserved_namespace";
3237
pub(crate) const SYNTAX_DEFERRAL: &str = "syntax_error";
3338
pub(crate) const PLACEHOLDER_DEFERRAL: &str = "placeholder_word";
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
pub(crate) mod polyglot;
12
pub mod sql_tests;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//! Function argument and return types parsed with the adapter's SQL analysis dialect.
2+
3+
use polyglot_sql::Dialect;
4+
5+
use crate::compiler::_helpers::polyglot::parse_options::guarded_parse_options;
6+
use crate::compiler::constants::FUNCTION_TYPE_DIALECTS;
7+
8+
/// The error for a type the adapter's dialect cannot parse; adapters without a dialect pass.
9+
pub(crate) fn function_type_error(
10+
type_sql: &str,
11+
adapter_name: &str,
12+
context: &str,
13+
) -> Result<Option<String>, String> {
14+
let Some((_, dialect_name, dialect)) = FUNCTION_TYPE_DIALECTS
15+
.iter()
16+
.find(|(adapter, _, _)| *adapter == adapter_name)
17+
else {
18+
return Ok(None);
19+
};
20+
let options = guarded_parse_options().map_err(|error| error.to_string())?;
21+
Ok(Dialect::get(*dialect)
22+
.parse_data_type_with_options(type_sql, &options)
23+
.err()
24+
.map(|error| {
25+
format!(
26+
"{context} type '{type_sql}' is not valid for adapter '{adapter_name}' SQL \
27+
analysis dialect '{dialect_name}': {error}"
28+
)
29+
}))
30+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pub(crate) mod function_types;
2+
pub(crate) mod parse_options;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//! The Polyglot parse options SQLBuild's wheel proxy applies to every parse.
2+
3+
use polyglot_sql::{ComplexityGuardOptions, ParseOptions};
4+
5+
use crate::lineage::constants::MAX_FUNCTION_CALL_DEPTH;
6+
7+
/// The proxy's complexity guard, decoded as the wheel decodes it.
8+
pub(crate) fn guarded_parse_options() -> Result<ParseOptions, serde_json::Error> {
9+
let guard: ComplexityGuardOptions = serde_json::from_value(serde_json::json!({
10+
"maxFunctionCallDepth": MAX_FUNCTION_CALL_DEPTH,
11+
}))?;
12+
Ok(ParseOptions {
13+
complexity_guard: Some(guard),
14+
})
15+
}
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
//! Expected results and assertions must not define or read each other, even through helpers.
2+
3+
use std::collections::{BTreeSet, HashMap, HashSet, VecDeque};
4+
5+
use polyglot_sql::{Dialect, DialectType, Expression, ExpressionWalk};
6+
use serde_json::Value;
7+
use sqlbuild_sqltext::sql_scan::main::dialect_non_code_end::dialect_non_code_end;
8+
use sqlbuild_sqltext::sql_scan::models::{LexicalSyntax, Unclosed};
9+
10+
use crate::compiler::_helpers::polyglot::parse_options::guarded_parse_options;
11+
use crate::compiler::constants::{ALIAS_FIELD, CTES_FIELD, NAME_FIELD};
12+
13+
const DIRECT_DEPENDENCY_PATH_LENGTH: usize = 2;
14+
const TABLE_KIND: &str = "table";
15+
const IDENTIFIER_QUOTES: &[u8] = b"\"`";
16+
17+
/// The check CTEs of one file and the labels its messages name.
18+
pub(crate) struct IndependenceCheck<'a> {
19+
pub(crate) ctes: &'a [(String, String)],
20+
pub(crate) expected_prefix: &'a str,
21+
pub(crate) assertion_prefix: &'a str,
22+
pub(crate) file_label: &'a str,
23+
pub(crate) context_label: &'a str,
24+
}
25+
26+
/// The first independence error of `check`, if any.
27+
pub(crate) fn independence_error(
28+
check: &IndependenceCheck<'_>,
29+
syntax: &LexicalSyntax,
30+
) -> Option<String> {
31+
let mut names_by_key: HashMap<String, &str> = HashMap::new();
32+
for (name, _) in check.ctes {
33+
names_by_key.insert(casefold(name), name);
34+
}
35+
let keys_with_prefix = |prefix: &str| -> BTreeSet<String> {
36+
names_by_key
37+
.iter()
38+
.filter(|(_, name)| name.starts_with(prefix))
39+
.map(|(key, _)| key.clone())
40+
.collect()
41+
};
42+
let expected: BTreeSet<String> = keys_with_prefix(check.expected_prefix);
43+
let assertions: BTreeSet<String> = keys_with_prefix(check.assertion_prefix);
44+
for (name, body) in check.ctes {
45+
let key: String = casefold(name);
46+
let (prohibited_prefix, prohibited_label) = if expected.contains(&key) {
47+
(check.assertion_prefix, "assertion")
48+
} else if assertions.contains(&key) {
49+
(check.expected_prefix, "expected result")
50+
} else {
51+
continue;
52+
};
53+
if !casefold(body).contains(&casefold(prohibited_prefix)) {
54+
continue;
55+
}
56+
if let Some(nested) = defined_cte_names(body)
57+
.into_iter()
58+
.find(|nested| nested.starts_with(prohibited_prefix))
59+
{
60+
return Some(format!(
61+
"{} '{}' check CTE '{name}' must not define {prohibited_label} CTE '{nested}'; \
62+
expected results and assertions must be independent",
63+
check.context_label, check.file_label
64+
));
65+
}
66+
}
67+
if expected.is_empty() || assertions.is_empty() {
68+
return None;
69+
}
70+
let mut dependencies: HashMap<String, Vec<String>> = HashMap::new();
71+
for (name, body) in check.ctes {
72+
let references = match known_cte_references(body, &names_by_key, syntax) {
73+
Ok(references) => references,
74+
Err(message) => return Some(message),
75+
};
76+
dependencies.insert(casefold(name), references);
77+
}
78+
for (origins, prohibited) in [(&expected, &assertions), (&assertions, &expected)] {
79+
for origin in origins {
80+
let Some(path) = dependency_path(origin, prohibited, &dependencies) else {
81+
continue;
82+
};
83+
let rendered: Vec<&str> = path.iter().map(|key| names_by_key[key]).collect();
84+
let through: String = if rendered.len() > DIRECT_DEPENDENCY_PATH_LENGTH {
85+
format!(
86+
" through {}",
87+
rendered[1..rendered.len() - 1]
88+
.iter()
89+
.map(|name| format!("'{name}'"))
90+
.collect::<Vec<String>>()
91+
.join(" -> ")
92+
)
93+
} else {
94+
String::new()
95+
};
96+
return Some(format!(
97+
"{} '{}' check CTE '{}' must not depend on '{}'{through}; expected results and \
98+
assertions must be independent",
99+
check.context_label,
100+
check.file_label,
101+
rendered[0],
102+
rendered[rendered.len() - 1]
103+
));
104+
}
105+
}
106+
None
107+
}
108+
109+
/// `str.casefold`, through Rust's Unicode lower-case mapping.
110+
fn casefold(text: &str) -> String {
111+
text.to_lowercase()
112+
}
113+
114+
/// `parse_one(sql, dialect="generic")` under SQLBuild's complexity guard, or `None` on error.
115+
fn parse_one(sql: &str) -> Option<Expression> {
116+
let Ok(options) = guarded_parse_options() else {
117+
return None;
118+
};
119+
let Ok(mut statements) = Dialect::get(DialectType::Generic).parse_with_options(sql, &options)
120+
else {
121+
return None;
122+
};
123+
(statements.len() == 1).then(|| statements.remove(0))
124+
}
125+
126+
/// Every CTE alias defined anywhere in `sql`, in the order `to_dict()` lists them.
127+
fn defined_cte_names(sql: &str) -> Vec<String> {
128+
let Some(parsed) = parse_one(sql) else {
129+
return Vec::new();
130+
};
131+
let Ok(value) = serde_json::to_value(&parsed) else {
132+
return Vec::new();
133+
};
134+
collect_cte_names(&value, Vec::new())
135+
}
136+
137+
/// `names` followed by each new CTE alias under `value`, depth first.
138+
fn collect_cte_names(value: &Value, mut names: Vec<String>) -> Vec<String> {
139+
match value {
140+
Value::Object(fields) => {
141+
if let Some(Value::Array(ctes)) = fields.get(CTES_FIELD) {
142+
for cte in ctes {
143+
if let Some(Value::String(name)) = cte
144+
.get(ALIAS_FIELD)
145+
.filter(|alias| alias.is_object())
146+
.and_then(|alias| alias.get(NAME_FIELD))
147+
&& !names.contains(name)
148+
{
149+
names.push(name.clone());
150+
}
151+
}
152+
}
153+
fields
154+
.values()
155+
.fold(names, |names, child| collect_cte_names(child, names))
156+
}
157+
Value::Array(items) => items
158+
.iter()
159+
.fold(names, |names, item| collect_cte_names(item, names)),
160+
_ => names,
161+
}
162+
}
163+
164+
/// The keys of the file's CTEs that `sql` reads, in reading order.
165+
fn known_cte_references(
166+
sql: &str,
167+
names_by_key: &HashMap<String, &str>,
168+
syntax: &LexicalSyntax,
169+
) -> Result<Vec<String>, String> {
170+
let folded: String = casefold(sql);
171+
if !names_by_key.keys().any(|key| folded.contains(key.as_str())) {
172+
return Ok(Vec::new());
173+
}
174+
let Some(parsed) = parse_one(sql) else {
175+
return identifier_references(sql, names_by_key, syntax);
176+
};
177+
let mut references: Vec<String> = Vec::new();
178+
for table in parsed
179+
.dfs()
180+
.skip(1)
181+
.filter(|node| node.variant_name() == TABLE_KIND)
182+
{
183+
references = push_known(references, casefold(table.get_name()), names_by_key);
184+
}
185+
Ok(references)
186+
}
187+
188+
/// The identifier scan used when Polyglot cannot parse a body.
189+
fn identifier_references(
190+
sql: &str,
191+
names_by_key: &HashMap<String, &str>,
192+
syntax: &LexicalSyntax,
193+
) -> Result<Vec<String>, String> {
194+
let bytes: &[u8] = sql.as_bytes();
195+
let mut references: Vec<String> = Vec::new();
196+
let mut index: usize = 0;
197+
while let Some(character) = sql[index..].chars().next() {
198+
match dialect_non_code_end(bytes, index, syntax) {
199+
Err(construct) => return Err(unclosed_message(construct)),
200+
Ok(Some(end)) if IDENTIFIER_QUOTES.contains(&bytes[index]) => {
201+
let quote: &str = &sql[index..=index];
202+
let name: String = sql[index + 1..end - 1].replace(&quote.repeat(2), quote);
203+
references = push_known(references, casefold(&name), names_by_key);
204+
index = end;
205+
}
206+
Ok(Some(end)) => index = end,
207+
Ok(None) if character.is_alphabetic() || character == '_' => {
208+
let end: usize = sql[index..]
209+
.char_indices()
210+
.skip(1)
211+
.find(|(_, next)| !(next.is_alphanumeric() || *next == '_'))
212+
.map_or(sql.len(), |(offset, _)| index + offset);
213+
references = push_known(references, casefold(&sql[index..end]), names_by_key);
214+
index = end;
215+
}
216+
Ok(None) => index += character.len_utf8(),
217+
}
218+
}
219+
Ok(references)
220+
}
221+
222+
/// `references` with `key` appended when it names a file CTE not yet read.
223+
fn push_known(
224+
mut references: Vec<String>,
225+
key: String,
226+
names_by_key: &HashMap<String, &str>,
227+
) -> Vec<String> {
228+
if names_by_key.contains_key(&key) && !references.contains(&key) {
229+
references.push(key);
230+
}
231+
references
232+
}
233+
234+
fn unclosed_message(construct: Unclosed) -> String {
235+
let construct: &str = match construct {
236+
Unclosed::BlockComment => "block comment",
237+
Unclosed::Quote => "quoted string",
238+
Unclosed::Parenthesis => "parenthesis",
239+
};
240+
format!("SQL test contains an unclosed {construct}")
241+
}
242+
243+
/// The shortest dependency path from `origin` to a prohibited CTE, breadth first.
244+
fn dependency_path(
245+
origin: &str,
246+
prohibited: &BTreeSet<String>,
247+
dependencies: &HashMap<String, Vec<String>>,
248+
) -> Option<Vec<String>> {
249+
let mut pending: VecDeque<Vec<String>> = VecDeque::from([vec![origin.to_owned()]]);
250+
let mut visited: HashSet<String> = HashSet::from([origin.to_owned()]);
251+
while let Some(path) = pending.pop_front() {
252+
let last: &String = path.last()?;
253+
for dependency in dependencies.get(last).into_iter().flatten() {
254+
let mut candidate: Vec<String> = path.clone();
255+
candidate.push(dependency.clone());
256+
if prohibited.contains(dependency) {
257+
return Some(candidate);
258+
}
259+
if visited.insert(dependency.clone()) {
260+
pending.push_back(candidate);
261+
}
262+
}
263+
}
264+
None
265+
}

0 commit comments

Comments
 (0)