Skip to content

Commit 33af370

Browse files
authored
refactor: host migration, cursor and fixture planning in Rust (#1108)
* refactor: port migration compatibility and matching decisions to Rust * chore: update Python boundary after migration decision port * refactor: resolve planner references and sources natively * chore: update Python boundary after reference resolution port * refactor: simplify fixture null projections natively * chore: update Python boundary after fixture simplification port * refactor: host SQL resolution orchestration in native bindings * chore: update Python boundary after resolution orchestration port * refactor: host fixture formatting orchestration in Rust * chore: update Python boundary after fixture formatting port * refactor: host column migration planning in Rust * chore: update Python boundary after column migration port * refactor: resolve planner cursor windows natively * chore: update Python boundary after cursor resolution port * refactor: move fixture alignment and rendering into native core * chore: update Python boundary after fixture alignment port * refactor: move cursor safety policies into native planner * chore: update Python boundary after cursor safety port * refactor: host model migration orchestration in native planner * chore: update Python boundary after model migration port * refactor: move fixture schema and type planning into native code * chore: update Python boundary after fixture type planning port * refactor: move scenario graph and relation planning to Rust * chore: update Python boundary after scenario planning port * refactor: host migration planning boundaries natively * chore: update Python boundary after migration glue port * refactor: host migration discovery orchestration natively * chore: update Python boundary after migration discovery port * fix: scope fixture type conversion to selected models
1 parent 30905fd commit 33af370

219 files changed

Lines changed: 12290 additions & 6123 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: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎crates/sqlbuild-analysis/src/lib.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ pub mod lineage_command;
2020
pub mod lineage_graph;
2121
pub mod manifest;
2222
pub mod planner_changes;
23+
pub mod planner_fixtures;
2324
pub mod planner_identity;
2425
pub mod planner_migrations;
26+
pub mod planner_scenario;
2527
pub mod planner_scope;
2628
pub mod planner_selection;
2729
pub mod planner_strategy;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub(crate) mod schema_order;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//! Fixture alignment phases.
2+
3+
use crate::planner_fixtures::models::AlignmentInput;
4+
5+
pub(crate) fn supplied_in_schema_order(input: &AlignmentInput<'_>) -> bool {
6+
input
7+
.supplied
8+
.iter()
9+
.map(|column| &column.folded_name)
10+
.eq(input
11+
.metadata
12+
.iter()
13+
.take(input.supplied.len())
14+
.map(|column| &column.folded_name))
15+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
//! Empty fixture marker pattern.
2+
3+
pub(crate) const EMPTY_PATTERN: &str = r"(?i)^[\s\x1c-\x1f]*SELECT[\s\x1c-\x1f]+\*[\s\x1c-\x1f]+FROM[\s\x1c-\x1f]+__empty_fixture[\s\x1c-\x1f]*\([\s\x1c-\x1f]*\)[\s\x1c-\x1f]*$";
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
//! Align supplied columns to their authoritative schema without rendering adapter SQL.
2+
3+
use std::collections::{BTreeSet, HashMap, HashSet};
4+
5+
use crate::planner_fixtures::_helpers::schema_order::supplied_in_schema_order;
6+
use crate::planner_fixtures::models::{Alignment, AlignmentInput, RequiredColumn};
7+
8+
/// Resolve missing columns, diagnostics and star-prefix ordering for one fixture.
9+
pub fn align_columns(label: &str, input: AlignmentInput<'_>) -> Alignment {
10+
let missing = missing_columns(&input);
11+
if missing.is_empty() && !input.typed_nulls {
12+
return Alignment::default();
13+
}
14+
let metadata: HashMap<&str, usize> = input
15+
.metadata
16+
.iter()
17+
.enumerate()
18+
.map(|(index, column)| (column.folded_name.as_str(), index))
19+
.collect();
20+
let mut unknown: Vec<&str> = Vec::new();
21+
let mut non_nullable: Vec<&str> = Vec::new();
22+
let mut readers: BTreeSet<&str> = BTreeSet::new();
23+
let mut completed: Vec<usize> = Vec::new();
24+
for required in missing {
25+
match metadata.get(required.folded_name.as_str()).copied() {
26+
Some(index) if input.metadata[index].sql_type.is_some() => {
27+
if input.metadata[index].nullable == Some(false) {
28+
non_nullable.push(required.name.as_str());
29+
} else {
30+
completed.push(index);
31+
}
32+
}
33+
_ => {
34+
unknown.push(required.name.as_str());
35+
readers.extend(required.readers.iter().map(String::as_str));
36+
}
37+
}
38+
}
39+
let diagnostics = missing_diagnostics(label, &unknown, &non_nullable, readers);
40+
if !diagnostics.is_empty() {
41+
return Alignment {
42+
diagnostics,
43+
completed: None,
44+
};
45+
}
46+
if input.star && !supplied_in_schema_order(&input) {
47+
return Alignment {
48+
diagnostics: vec![format!(
49+
"{label} supplies columns out of authoritative order for a SELECT * closure; provide the leading relation columns in schema order"
50+
)],
51+
completed: None,
52+
};
53+
}
54+
completed.sort_unstable();
55+
Alignment {
56+
diagnostics,
57+
completed: Some(completed),
58+
}
59+
}
60+
61+
fn missing_columns<'a>(input: &AlignmentInput<'a>) -> Vec<&'a RequiredColumn> {
62+
let supplied: HashSet<&str> = input
63+
.supplied
64+
.iter()
65+
.map(|column| column.folded_name.as_str())
66+
.collect();
67+
let mut missing: Vec<_> = input
68+
.required
69+
.iter()
70+
.filter(|column| !supplied.contains(column.folded_name.as_str()))
71+
.collect();
72+
missing.sort_by(|left, right| left.name.cmp(&right.name));
73+
missing
74+
}
75+
76+
fn missing_diagnostics(
77+
label: &str,
78+
unknown: &[&str],
79+
non_nullable: &[&str],
80+
readers: BTreeSet<&str>,
81+
) -> Vec<String> {
82+
let mut diagnostics: Vec<String> = Vec::new();
83+
if !unknown.is_empty() {
84+
diagnostics.push(format!(
85+
"{label} is missing required columns: {}; provide explicit values because their types are not authoritative (read by: {})",
86+
unknown.join(", "), readers.into_iter().collect::<Vec<_>>().join(", ")
87+
));
88+
}
89+
if !non_nullable.is_empty() {
90+
diagnostics.push(format!(
91+
"{label} must provide required non-nullable columns: {}",
92+
non_nullable.join(", ")
93+
));
94+
}
95+
diagnostics
96+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//! Authoritative schema requirements for empty fixtures.
2+
3+
use crate::planner_fixtures::models::FixtureColumn;
4+
5+
/// Return the first schema failure before rendering any empty-fixture column.
6+
pub fn empty_fixture_error(authoritative: bool, columns: &[FixtureColumn]) -> Option<String> {
7+
if !authoritative {
8+
return Some("uses __empty_fixture() but its column schema is not authoritative".into());
9+
}
10+
let unknown: Vec<_> = columns
11+
.iter()
12+
.filter(|column| column.sql_type.is_none())
13+
.map(|column| column.name.as_str())
14+
.collect();
15+
if !unknown.is_empty() {
16+
return Some(format!(
17+
"uses __empty_fixture() but columns have unknown types: {}",
18+
unknown.join(", ")
19+
));
20+
}
21+
if columns.is_empty() {
22+
return Some("uses __empty_fixture() but its schema has no columns".into());
23+
}
24+
None
25+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//! Exact empty-fixture marker recognition.
2+
3+
use crate::planner_fixtures::constants::EMPTY_PATTERN;
4+
use regex::Regex;
5+
use std::sync::OnceLock;
6+
7+
/// Whether SQL consists solely of the empty-fixture marker.
8+
pub fn empty_fixture_marker(sql: &str) -> Result<bool, regex::Error> {
9+
static PATTERN: OnceLock<Result<Regex, regex::Error>> = OnceLock::new();
10+
PATTERN
11+
.get_or_init(|| Regex::new(EMPTY_PATTERN))
12+
.as_ref()
13+
.map(|pattern| pattern.is_match(sql))
14+
.map_err(Clone::clone)
15+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//! Derive fixture schema facts from relation metadata.
2+
3+
use crate::planner_fixtures::models::{FixtureColumn, FixtureSchema};
4+
5+
/// Collect authoritative folded names and last-declared known column types.
6+
pub fn fixture_schema(columns: &[FixtureColumn], authoritative: bool) -> FixtureSchema {
7+
let mut expected_types: Vec<(String, String)> = Vec::new();
8+
for column in columns {
9+
if let Some(sql_type) = &column.sql_type {
10+
expected_types.push((column.folded_name.clone(), sql_type.clone()));
11+
}
12+
}
13+
FixtureSchema {
14+
authoritative_names: authoritative.then(|| {
15+
columns
16+
.iter()
17+
.map(|column| column.folded_name.clone())
18+
.collect()
19+
}),
20+
expected_types,
21+
}
22+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//! Merge inferred model columns with declared schema metadata.
2+
3+
use std::collections::HashMap;
4+
5+
use crate::planner_fixtures::models::FixtureColumn;
6+
7+
/// Merge declarations over inferred columns while preserving inferred order.
8+
pub fn merge_metadata(
9+
inferred: Vec<FixtureColumn>,
10+
declared: Vec<FixtureColumn>,
11+
) -> Vec<FixtureColumn> {
12+
let mut order: Vec<String> = Vec::new();
13+
let mut columns: HashMap<String, FixtureColumn> = HashMap::new();
14+
for column in inferred {
15+
order.push(column.folded_name.clone());
16+
columns.insert(column.folded_name.clone(), column);
17+
}
18+
for mut column in declared {
19+
if let Some(inferred) = columns.get(&column.folded_name) {
20+
if column.sql_type.as_ref().is_none_or(String::is_empty) {
21+
column.sql_type.clone_from(&inferred.sql_type);
22+
}
23+
if column.nullable.is_none() {
24+
column.nullable = inferred.nullable;
25+
}
26+
} else {
27+
order.push(column.folded_name.clone());
28+
}
29+
columns.insert(column.folded_name.clone(), column);
30+
}
31+
order
32+
.into_iter()
33+
.filter_map(|name| columns.get(&name).cloned())
34+
.collect()
35+
}

0 commit comments

Comments
 (0)