Skip to content

Commit dc0efaa

Browse files
committed
fix: retain native planner data through graph traversal
Keep deferred-clone and freshness graph walks native, and avoid temporary Python model maps in migration planning. Preserve public callback and executor boundaries. Amp-Thread-ID: https://ampcode.com/threads/T-01a18715-4e00-708e-a87d-6bd109748d66
1 parent 2065d96 commit dc0efaa

10 files changed

Lines changed: 222 additions & 171 deletions

File tree

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//! Deferred-clone ancestors in depth-first order, with caller-owned keys and predicates.
2+
3+
use crate::graph_walk::models::ClonePolicy;
4+
use crate::graph_walk::types::NodeSet;
5+
6+
/// Find clone boundaries, or the skipped views rebuilt over them, without crossing a host boundary.
7+
pub fn clone_ancestors<K, S, N, P, C, V>(
8+
starts: impl IntoIterator<Item = Result<K, S::Error>>,
9+
(visited, found): (&mut S, &mut S),
10+
mut parents: N,
11+
policy: ClonePolicy<P, C, V>,
12+
) -> Result<(), S::Error>
13+
where
14+
K: Clone,
15+
S: NodeSet<K>,
16+
N: FnMut(&K) -> Result<Vec<K>, S::Error>,
17+
P: FnMut(&K) -> Result<bool, S::Error>,
18+
C: FnMut(&K) -> Result<bool, S::Error>,
19+
V: FnMut(&K) -> Result<bool, S::Error>,
20+
{
21+
let ClonePolicy {
22+
mut selected,
23+
mut clonable,
24+
mut view,
25+
skipped_views,
26+
} = policy;
27+
for start in starts {
28+
let start = start?;
29+
if !visited.insert_new(&start)? {
30+
continue;
31+
}
32+
let mut stack = vec![(parents(&start)?, 0)];
33+
while let Some((neighbours, index)) = stack.last_mut() {
34+
let Some(parent) = neighbours.get(*index).cloned() else {
35+
stack.pop();
36+
continue;
37+
};
38+
*index += 1;
39+
let descend = if selected(&parent)? {
40+
true
41+
} else if skipped_views {
42+
let is_view = view(&parent)?;
43+
if is_view && clonable(&parent)? {
44+
found.insert_new(&parent)?;
45+
}
46+
is_view
47+
} else if view(&parent)? {
48+
true
49+
} else {
50+
if clonable(&parent)? {
51+
found.insert_new(&parent)?;
52+
}
53+
false
54+
};
55+
if descend && visited.insert_new(&parent)? {
56+
stack.push((parents(&parent)?, 0));
57+
}
58+
}
59+
}
60+
Ok(())
61+
}

‎crates/sqlbuild-core/src/graph_walk/main/mod.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod breadth_first_path;
22
pub mod breadth_first_reach;
3+
pub mod clone_ancestors;
34
pub mod depth_first_postorder;
45
pub mod directed_path_nodes;
56
pub mod find_cycle;

‎crates/sqlbuild-core/src/graph_walk/models.rs‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ use std::hash::Hash;
66

77
use crate::graph_walk::types::NodeSet;
88

9+
/// Caller-owned selection and materialization predicates, and the ancestor kind to collect.
10+
#[derive(Debug)]
11+
pub struct ClonePolicy<P, C, V> {
12+
pub selected: P,
13+
pub clonable: C,
14+
pub view: V,
15+
pub skipped_views: bool,
16+
}
17+
918
/// Distinct nodes in the order a walk first recorded them.
1019
#[derive(Debug, Clone)]
1120
pub struct VisitOrder<K> {

‎crates/sqlbuild-python/src/bindings/_helpers/build_command/defer_clone.rs‎

Lines changed: 64 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
//! `sqb build --defer-clone-from`: clone out-of-selection boundaries before build planning.
22
3+
use std::collections::{HashMap, HashSet};
34
use std::time::Instant;
45

56
use pyo3::prelude::{
67
Bound, Py, PyAny, PyAnyMethods, PyErr, PyModule, PyModuleMethods, PyResult, Python,
78
};
89
use pyo3::types::{PyCFunction, PyDict, PyFrozenSet, PyString, PyTuple, PyTupleMethods};
910
use pyo3::{IntoPyObject, IntoPyObjectExt, pyfunction, wrap_pyfunction};
11+
use sqlbuild_analysis::assembly::project::types::ObjectKey;
1012
use sqlbuild_core::build_command::constants::EXECUTION_LIMIT_BUILD_NOTE;
13+
use sqlbuild_core::graph_walk::main::clone_ancestors::clone_ancestors;
14+
use sqlbuild_core::graph_walk::models::ClonePolicy;
1115

1216
use sqlbuild_core::build_command::errors::BuildCommandError;
1317
use sqlbuild_host::entries::models::ExecutorHost;
@@ -21,14 +25,11 @@ use crate::bindings::_helpers::compile_driver::project::build_compiled_project;
2125
use crate::bindings::_helpers::contracts::spec_resolution::resolve_target_config;
2226
use crate::bindings::_helpers::dbt_interop::reference_resolver::resolve_external_sql_reference_resolver;
2327
use crate::bindings::_helpers::native_host::host;
24-
use crate::bindings::_helpers::planner_entries::clone_plans::{
25-
resolve_clone_boundary, resolve_skipped_view_chain,
26-
};
28+
use crate::bindings::_helpers::planner_scope::retained::{ScopeData, scope_data};
2729
use crate::bindings::_helpers::planner_scope::scope::build_planner_scope;
2830
use crate::bindings::_helpers::planner_selection::unit_tests::split_unit_test_selectors;
2931
use crate::bindings::_helpers::records::construct::{enum_member, new_record_with};
3032
use crate::bindings::_helpers::records::fields::field as native_field;
31-
use crate::bindings::_helpers::records::fields::mapping_get;
3233
use crate::bindings::_helpers::records::fields::replaced;
3334
use crate::bindings::_helpers::records::ids::RecordId;
3435

@@ -52,19 +53,14 @@ fn user_error(py: Python<'_>, message: String, code: &str) -> PyErr {
5253
)
5354
}
5455

55-
fn resource_type(key: &Bound<'_, PyAny>) -> PyResult<String> {
56-
native_field(key, "resource_type")?
57-
.getattr("value")?
58-
.extract()
59-
}
60-
6156
/// Functions and view models are recreated over cloned boundaries instead of cloned.
62-
fn recreated_over_clones(scope: &Bound<'_, PyAny>, key: &Bound<'_, PyAny>) -> PyResult<bool> {
63-
if RECREATED_TYPES.contains(&resource_type(key)?.as_str()) {
57+
fn recreated_over_clones(py: Python<'_>, scope: &ScopeData, key: &ObjectKey) -> PyResult<bool> {
58+
if RECREATED_TYPES.contains(&key.0.as_str()) {
6459
return Ok(true);
6560
}
66-
let model: Bound<'_, PyAny> = native_field(scope, "models_by_name")?
67-
.call_method1("get", (native_field(key, "name")?,))?;
61+
let Some(model) = scope.model(py, &key.1) else {
62+
return Ok(false);
63+
};
6864
if model.is_none() {
6965
return Ok(false);
7066
}
@@ -76,83 +72,59 @@ fn recreated_over_clones(scope: &Bound<'_, PyAny>, key: &Bound<'_, PyAny>) -> Py
7672
}
7773

7874
/// Every key the scope's traversal can visit: the selection and the upstream graph.
79-
fn scope_keys<'py>(scope: &Bound<'py, PyAny>) -> PyResult<Vec<Bound<'py, PyAny>>> {
80-
let mut keys: Vec<Bound<'py, PyAny>> = native_field(scope, "selected_keys")?
81-
.try_iter()?
82-
.collect::<PyResult<_>>()?;
83-
for item in native_field(scope, "upstream_deps")?
84-
.call_method0("items")?
85-
.try_iter()?
86-
{
87-
let (key, upstream): (Bound<'py, PyAny>, Bound<'py, PyAny>) = item?.extract()?;
88-
keys.push(key);
89-
for upstream_key in upstream.try_iter()? {
90-
keys.push(upstream_key?);
91-
}
75+
fn scope_keys(scope: &ScopeData) -> Vec<ObjectKey> {
76+
let mut keys: Vec<_> = scope.selected.iter().cloned().collect();
77+
for (key, upstream) in &scope.upstream {
78+
keys.push(key.clone());
79+
keys.extend(upstream.iter().cloned());
9280
}
93-
Ok(keys)
81+
keys
9482
}
9583

96-
/// `frozenset.__contains__` over the keys a predicate accepts, as the traversal callback.
97-
fn membership<'py>(
98-
py: Python<'py>,
99-
keys: &[Bound<'py, PyAny>],
100-
accepted: impl Fn(&Bound<'py, PyAny>) -> PyResult<bool>,
101-
) -> PyResult<Bound<'py, PyAny>> {
102-
let mut members: Vec<Bound<'py, PyAny>> = Vec::new();
103-
for key in keys {
104-
if accepted(key)? {
105-
members.push(key.clone());
106-
}
107-
}
108-
PyFrozenSet::new(py, &members)?.getattr("__contains__")
109-
}
110-
111-
/// A clone-plan traversal from selected keys over upstream dependencies.
112-
type TraversalFunction =
113-
for<'py> fn(Python<'py>, Option<Bound<'py, PyDict>>) -> PyResult<Bound<'py, PyFrozenSet>>;
114-
115-
fn traversed_names<'py>(
116-
scope: &Bound<'py, PyAny>,
117-
function: TraversalFunction,
118-
(is_clonable, is_view): (&Bound<'py, PyAny>, &Bound<'py, PyAny>),
119-
) -> PyResult<Vec<String>> {
120-
let py: Python<'py> = scope.py();
121-
let keys: Bound<'py, PyFrozenSet> = function(
122-
py,
123-
Some(keywords(
124-
py,
125-
&[
126-
("selected", native_field(scope, "selected_keys")?),
127-
("upstream", native_field(scope, "upstream_deps")?),
128-
("is_clonable", is_clonable.clone()),
129-
("is_view", is_view.clone()),
130-
],
131-
)?),
132-
)?;
133-
let mut names: Vec<String> = Vec::new();
134-
for key in keys.try_iter()? {
135-
names.push(native_field(&key?, "name")?.extract()?);
136-
}
84+
/// Clone boundaries or recreated ancestors, without Python graph or predicate projections.
85+
fn traversed_names(
86+
scope: &ScopeData,
87+
upstream: &HashMap<&ObjectKey, &Vec<ObjectKey>>,
88+
recreated: &HashSet<ObjectKey>,
89+
skipped_views: bool,
90+
) -> Vec<String> {
91+
let mut visited: HashSet<ObjectKey> = HashSet::new();
92+
let mut found: HashSet<ObjectKey> = HashSet::new();
93+
let Ok(()) = clone_ancestors(
94+
scope.selected.iter().cloned().map(Ok),
95+
(&mut visited, &mut found),
96+
|node| {
97+
Ok(upstream
98+
.get(node)
99+
.map_or_else(Vec::new, |parents| (*parents).clone()))
100+
},
101+
ClonePolicy {
102+
selected: |node: &ObjectKey| Ok(scope.selected.contains(node)),
103+
clonable: |node: &ObjectKey| Ok(CLONABLE_TYPES.contains(&node.0.as_str())),
104+
view: |node: &ObjectKey| Ok(recreated.contains(node)),
105+
skipped_views,
106+
},
107+
);
108+
let mut names: Vec<_> = found.into_iter().map(|key| key.1).collect();
137109
names.sort();
138-
Ok(names)
110+
names
139111
}
140112

141113
/// Selected models except those explicitly disabled.
142-
fn selected_executable_model_count(scope: &Bound<'_, PyAny>) -> PyResult<usize> {
143-
let models: Bound<'_, PyAny> = native_field(scope, "models_by_name")?;
114+
fn selected_executable_model_count(py: Python<'_>, scope: &ScopeData) -> PyResult<usize> {
144115
let mut count: usize = 0;
145-
for key in native_field(scope, "selected_keys")?.try_iter()? {
146-
let key: Bound<'_, PyAny> = key?;
147-
if resource_type(&key)? != MODEL_TYPE {
116+
for key in &scope.selected {
117+
if key.0 != MODEL_TYPE {
148118
continue;
149119
}
150-
let model: Bound<'_, PyAny> = mapping_get(&models, native_field(&key, "name")?)?;
120+
let model = scope
121+
.model(py, &key.1)
122+
.unwrap_or_else(|| py.None().into_bound(py));
151123
if !model.is_none()
152124
&& native_field(&model, "config")?
153125
.getattr("values")?
154126
.call_method1("get", ("enabled",))?
155-
.is(false.into_pyobject(scope.py())?.to_owned().into_any())
127+
.is(false.into_pyobject(py)?.to_owned().into_any())
156128
{
157129
continue;
158130
}
@@ -164,16 +136,22 @@ fn selected_executable_model_count(scope: &Bound<'_, PyAny>) -> PyResult<usize>
164136
/// Boundary clone selectors, rebuilt view-chain selectors and the selected model count.
165137
fn defer_clone_selection(scope: &Bound<'_, PyAny>) -> PyResult<(Vec<String>, Vec<String>, usize)> {
166138
let py: Python<'_> = scope.py();
167-
let keys: Vec<Bound<'_, PyAny>> = scope_keys(scope)?;
168-
let is_clonable: Bound<'_, PyAny> = membership(py, &keys, |key| {
169-
Ok(CLONABLE_TYPES.contains(&resource_type(key)?.as_str()))
170-
})?;
171-
let is_view: Bound<'_, PyAny> = membership(py, &keys, |key| recreated_over_clones(scope, key))?;
172-
let callbacks: (&Bound<'_, PyAny>, &Bound<'_, PyAny>) = (&is_clonable, &is_view);
139+
let scope = scope_data(scope)?;
140+
let mut recreated: HashSet<ObjectKey> = HashSet::new();
141+
for key in scope_keys(&scope) {
142+
if recreated_over_clones(py, &scope, &key)? {
143+
recreated.insert(key);
144+
}
145+
}
146+
let upstream: HashMap<&ObjectKey, &Vec<ObjectKey>> = scope
147+
.upstream
148+
.iter()
149+
.map(|(key, parents)| (key, parents))
150+
.collect();
173151
Ok((
174-
traversed_names(scope, resolve_clone_boundary, callbacks)?,
175-
traversed_names(scope, resolve_skipped_view_chain, callbacks)?,
176-
selected_executable_model_count(scope)?,
152+
traversed_names(&scope, &upstream, &recreated, false),
153+
traversed_names(&scope, &upstream, &recreated, true),
154+
selected_executable_model_count(py, &scope)?,
177155
))
178156
}
179157

0 commit comments

Comments
 (0)