diff --git a/crates/sqlbuild-core/src/graph_walk/main/clone_ancestors.rs b/crates/sqlbuild-core/src/graph_walk/main/clone_ancestors.rs new file mode 100644 index 0000000000..264a6fdede --- /dev/null +++ b/crates/sqlbuild-core/src/graph_walk/main/clone_ancestors.rs @@ -0,0 +1,61 @@ +//! Deferred-clone ancestors in depth-first order, with caller-owned keys and predicates. + +use crate::graph_walk::models::ClonePolicy; +use crate::graph_walk::types::NodeSet; + +/// Find clone boundaries, or the skipped views rebuilt over them, without crossing a host boundary. +pub fn clone_ancestors( + starts: impl IntoIterator>, + (visited, found): (&mut S, &mut S), + mut parents: N, + policy: ClonePolicy, +) -> Result<(), S::Error> +where + K: Clone, + S: NodeSet, + N: FnMut(&K) -> Result, S::Error>, + P: FnMut(&K) -> Result, + C: FnMut(&K) -> Result, + V: FnMut(&K) -> Result, +{ + let ClonePolicy { + mut selected, + mut clonable, + mut view, + skipped_views, + } = policy; + for start in starts { + let start = start?; + if !visited.insert_new(&start)? { + continue; + } + let mut stack = vec![(parents(&start)?, 0)]; + while let Some((neighbours, index)) = stack.last_mut() { + let Some(parent) = neighbours.get(*index).cloned() else { + stack.pop(); + continue; + }; + *index += 1; + let descend = if selected(&parent)? { + true + } else if skipped_views { + let is_view = view(&parent)?; + if is_view && clonable(&parent)? { + found.insert_new(&parent)?; + } + is_view + } else if view(&parent)? { + true + } else { + if clonable(&parent)? { + found.insert_new(&parent)?; + } + false + }; + if descend && visited.insert_new(&parent)? { + stack.push((parents(&parent)?, 0)); + } + } + } + Ok(()) +} diff --git a/crates/sqlbuild-core/src/graph_walk/main/mod.rs b/crates/sqlbuild-core/src/graph_walk/main/mod.rs index 3787cafe64..6ae289a7d2 100644 --- a/crates/sqlbuild-core/src/graph_walk/main/mod.rs +++ b/crates/sqlbuild-core/src/graph_walk/main/mod.rs @@ -1,5 +1,6 @@ pub mod breadth_first_path; pub mod breadth_first_reach; +pub mod clone_ancestors; pub mod depth_first_postorder; pub mod directed_path_nodes; pub mod find_cycle; diff --git a/crates/sqlbuild-core/src/graph_walk/models.rs b/crates/sqlbuild-core/src/graph_walk/models.rs index f98c6e8cd6..7ce91ba76e 100644 --- a/crates/sqlbuild-core/src/graph_walk/models.rs +++ b/crates/sqlbuild-core/src/graph_walk/models.rs @@ -6,6 +6,15 @@ use std::hash::Hash; use crate::graph_walk::types::NodeSet; +/// Caller-owned selection and materialization predicates, and the ancestor kind to collect. +#[derive(Debug)] +pub struct ClonePolicy { + pub selected: P, + pub clonable: C, + pub view: V, + pub skipped_views: bool, +} + /// Distinct nodes in the order a walk first recorded them. #[derive(Debug, Clone)] pub struct VisitOrder { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/build_command/defer_clone.rs b/crates/sqlbuild-python/src/bindings/_helpers/build_command/defer_clone.rs index ce26daf791..1af60b9f36 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/build_command/defer_clone.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/build_command/defer_clone.rs @@ -1,5 +1,6 @@ //! `sqb build --defer-clone-from`: clone out-of-selection boundaries before build planning. +use std::collections::{HashMap, HashSet}; use std::time::Instant; use pyo3::prelude::{ @@ -7,7 +8,10 @@ use pyo3::prelude::{ }; use pyo3::types::{PyCFunction, PyDict, PyFrozenSet, PyString, PyTuple, PyTupleMethods}; use pyo3::{IntoPyObject, IntoPyObjectExt, pyfunction, wrap_pyfunction}; +use sqlbuild_analysis::assembly::project::types::ObjectKey; use sqlbuild_core::build_command::constants::EXECUTION_LIMIT_BUILD_NOTE; +use sqlbuild_core::graph_walk::main::clone_ancestors::clone_ancestors; +use sqlbuild_core::graph_walk::models::ClonePolicy; use sqlbuild_core::build_command::errors::BuildCommandError; use sqlbuild_host::entries::models::ExecutorHost; @@ -21,14 +25,11 @@ use crate::bindings::_helpers::compile_driver::project::build_compiled_project; use crate::bindings::_helpers::contracts::spec_resolution::resolve_target_config; use crate::bindings::_helpers::dbt_interop::reference_resolver::resolve_external_sql_reference_resolver; use crate::bindings::_helpers::native_host::host; -use crate::bindings::_helpers::planner_entries::clone_plans::{ - resolve_clone_boundary, resolve_skipped_view_chain, -}; +use crate::bindings::_helpers::planner_scope::retained::{ScopeData, scope_data}; use crate::bindings::_helpers::planner_scope::scope::build_planner_scope; use crate::bindings::_helpers::planner_selection::unit_tests::split_unit_test_selectors; use crate::bindings::_helpers::records::construct::{enum_member, new_record_with}; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::fields::mapping_get; use crate::bindings::_helpers::records::fields::replaced; use crate::bindings::_helpers::records::ids::RecordId; @@ -52,19 +53,14 @@ fn user_error(py: Python<'_>, message: String, code: &str) -> PyErr { ) } -fn resource_type(key: &Bound<'_, PyAny>) -> PyResult { - native_field(key, "resource_type")? - .getattr("value")? - .extract() -} - /// Functions and view models are recreated over cloned boundaries instead of cloned. -fn recreated_over_clones(scope: &Bound<'_, PyAny>, key: &Bound<'_, PyAny>) -> PyResult { - if RECREATED_TYPES.contains(&resource_type(key)?.as_str()) { +fn recreated_over_clones(py: Python<'_>, scope: &ScopeData, key: &ObjectKey) -> PyResult { + if RECREATED_TYPES.contains(&key.0.as_str()) { return Ok(true); } - let model: Bound<'_, PyAny> = native_field(scope, "models_by_name")? - .call_method1("get", (native_field(key, "name")?,))?; + let Some(model) = scope.model(py, &key.1) else { + return Ok(false); + }; if model.is_none() { return Ok(false); } @@ -76,83 +72,59 @@ fn recreated_over_clones(scope: &Bound<'_, PyAny>, key: &Bound<'_, PyAny>) -> Py } /// Every key the scope's traversal can visit: the selection and the upstream graph. -fn scope_keys<'py>(scope: &Bound<'py, PyAny>) -> PyResult>> { - let mut keys: Vec> = native_field(scope, "selected_keys")? - .try_iter()? - .collect::>()?; - for item in native_field(scope, "upstream_deps")? - .call_method0("items")? - .try_iter()? - { - let (key, upstream): (Bound<'py, PyAny>, Bound<'py, PyAny>) = item?.extract()?; - keys.push(key); - for upstream_key in upstream.try_iter()? { - keys.push(upstream_key?); - } +fn scope_keys(scope: &ScopeData) -> Vec { + let mut keys: Vec<_> = scope.selected.iter().cloned().collect(); + for (key, upstream) in &scope.upstream { + keys.push(key.clone()); + keys.extend(upstream.iter().cloned()); } - Ok(keys) + keys } -/// `frozenset.__contains__` over the keys a predicate accepts, as the traversal callback. -fn membership<'py>( - py: Python<'py>, - keys: &[Bound<'py, PyAny>], - accepted: impl Fn(&Bound<'py, PyAny>) -> PyResult, -) -> PyResult> { - let mut members: Vec> = Vec::new(); - for key in keys { - if accepted(key)? { - members.push(key.clone()); - } - } - PyFrozenSet::new(py, &members)?.getattr("__contains__") -} - -/// A clone-plan traversal from selected keys over upstream dependencies. -type TraversalFunction = - for<'py> fn(Python<'py>, Option>) -> PyResult>; - -fn traversed_names<'py>( - scope: &Bound<'py, PyAny>, - function: TraversalFunction, - (is_clonable, is_view): (&Bound<'py, PyAny>, &Bound<'py, PyAny>), -) -> PyResult> { - let py: Python<'py> = scope.py(); - let keys: Bound<'py, PyFrozenSet> = function( - py, - Some(keywords( - py, - &[ - ("selected", native_field(scope, "selected_keys")?), - ("upstream", native_field(scope, "upstream_deps")?), - ("is_clonable", is_clonable.clone()), - ("is_view", is_view.clone()), - ], - )?), - )?; - let mut names: Vec = Vec::new(); - for key in keys.try_iter()? { - names.push(native_field(&key?, "name")?.extract()?); - } +/// Clone boundaries or recreated ancestors, without Python graph or predicate projections. +fn traversed_names( + scope: &ScopeData, + upstream: &HashMap<&ObjectKey, &Vec>, + recreated: &HashSet, + skipped_views: bool, +) -> Vec { + let mut visited: HashSet = HashSet::new(); + let mut found: HashSet = HashSet::new(); + let Ok(()) = clone_ancestors( + scope.selected.iter().cloned().map(Ok), + (&mut visited, &mut found), + |node| { + Ok(upstream + .get(node) + .map_or_else(Vec::new, |parents| (*parents).clone())) + }, + ClonePolicy { + selected: |node: &ObjectKey| Ok(scope.selected.contains(node)), + clonable: |node: &ObjectKey| Ok(CLONABLE_TYPES.contains(&node.0.as_str())), + view: |node: &ObjectKey| Ok(recreated.contains(node)), + skipped_views, + }, + ); + let mut names: Vec<_> = found.into_iter().map(|key| key.1).collect(); names.sort(); - Ok(names) + names } /// Selected models except those explicitly disabled. -fn selected_executable_model_count(scope: &Bound<'_, PyAny>) -> PyResult { - let models: Bound<'_, PyAny> = native_field(scope, "models_by_name")?; +fn selected_executable_model_count(py: Python<'_>, scope: &ScopeData) -> PyResult { let mut count: usize = 0; - for key in native_field(scope, "selected_keys")?.try_iter()? { - let key: Bound<'_, PyAny> = key?; - if resource_type(&key)? != MODEL_TYPE { + for key in &scope.selected { + if key.0 != MODEL_TYPE { continue; } - let model: Bound<'_, PyAny> = mapping_get(&models, native_field(&key, "name")?)?; + let model = scope + .model(py, &key.1) + .unwrap_or_else(|| py.None().into_bound(py)); if !model.is_none() && native_field(&model, "config")? .getattr("values")? .call_method1("get", ("enabled",))? - .is(false.into_pyobject(scope.py())?.to_owned().into_any()) + .is(false.into_pyobject(py)?.to_owned().into_any()) { continue; } @@ -164,16 +136,22 @@ fn selected_executable_model_count(scope: &Bound<'_, PyAny>) -> PyResult /// Boundary clone selectors, rebuilt view-chain selectors and the selected model count. fn defer_clone_selection(scope: &Bound<'_, PyAny>) -> PyResult<(Vec, Vec, usize)> { let py: Python<'_> = scope.py(); - let keys: Vec> = scope_keys(scope)?; - let is_clonable: Bound<'_, PyAny> = membership(py, &keys, |key| { - Ok(CLONABLE_TYPES.contains(&resource_type(key)?.as_str())) - })?; - let is_view: Bound<'_, PyAny> = membership(py, &keys, |key| recreated_over_clones(scope, key))?; - let callbacks: (&Bound<'_, PyAny>, &Bound<'_, PyAny>) = (&is_clonable, &is_view); + let scope = scope_data(scope)?; + let mut recreated: HashSet = HashSet::new(); + for key in scope_keys(&scope) { + if recreated_over_clones(py, &scope, &key)? { + recreated.insert(key); + } + } + let upstream: HashMap<&ObjectKey, &Vec> = scope + .upstream + .iter() + .map(|(key, parents)| (key, parents)) + .collect(); Ok(( - traversed_names(scope, resolve_clone_boundary, callbacks)?, - traversed_names(scope, resolve_skipped_view_chain, callbacks)?, - selected_executable_model_count(scope)?, + traversed_names(&scope, &upstream, &recreated, false), + traversed_names(&scope, &upstream, &recreated, true), + selected_executable_model_count(py, &scope)?, )) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/freshness/planning.rs b/crates/sqlbuild-python/src/bindings/_helpers/freshness/planning.rs index 02de3f71e1..33854ecfda 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/freshness/planning.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/freshness/planning.rs @@ -1,10 +1,15 @@ //! Planning-time source freshness: observe, compare with latest state and map downstream impact. +use std::collections::{HashMap, HashSet}; + use pyo3::prelude::{ Bound, PyAny, PyAnyMethods, PyErr, PyModule, PyModuleMethods, PyResult, Python, }; use pyo3::types::{PyDict, PyDictMethods, PyFrozenSet, PySet, PySetMethods, PyTuple}; use pyo3::{pyfunction, wrap_pyfunction}; +use sqlbuild_analysis::assembly::project::types::ObjectKey; +use sqlbuild_analysis::planner_identity::constants::{MODEL_RESOURCE, SOURCE_RESOURCE}; +use sqlbuild_core::graph_walk::main::breadth_first_reach::breadth_first_reach; use sqlbuild_host::entries::models::AdapterHost; use sqlbuild_state::freshness::main::configuration_error::configuration_error; @@ -17,7 +22,7 @@ use crate::bindings::_helpers::freshness::observation::{ use crate::bindings::_helpers::freshness::state_io::{StateRead, read_latest}; use crate::bindings::_helpers::freshness::support::text; use crate::bindings::_helpers::native_host::host; -use crate::bindings::_helpers::planner_identity::graph::build_downstream_model_name_closure; +use crate::bindings::_helpers::planner_scope::retained::scope_data; use crate::bindings::_helpers::records::construct::{enum_member, new_record_with}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::fields::mapping_get; @@ -295,23 +300,22 @@ fn frozen<'py>(items: &Bound<'py, PySet>) -> PyResult> { items.py().get_type::().call1((items,)) } -fn downstream_models<'py>( - source_name: &Bound<'py, PyAny>, - downstream_deps: &Bound<'py, PyAny>, -) -> PyResult> { - let py: Python<'py> = source_name.py(); - let key: Bound<'py, PyAny> = new_record_with( - py, - RecordId::CompiledObjectKey, - &[ - ( - "resource_type", - enum_member(py, RecordId::CompiledResourceType, "SOURCE")?, - ), - ("name", source_name.clone()), - ], - )?; - build_downstream_model_name_closure(&PyTuple::new(py, [key])?.into_any(), downstream_deps) +fn downstream_models( + source_name: String, + downstream_deps: &HashMap<&ObjectKey, &Vec>, +) -> HashSet { + let key = (SOURCE_RESOURCE.to_owned(), source_name); + let mut visited: HashSet<&ObjectKey> = HashSet::new(); + let Ok(()) = breadth_first_reach([&key], None, &mut visited, |node| { + Ok(downstream_deps + .get(node) + .map_or(&[][..], |deps| deps.as_slice())) + }); + visited + .into_iter() + .filter(|key| key.0 == MODEL_RESOURCE) + .map(|key| key.1.clone()) + .collect() } /// Map changed and unknown source freshness roots to downstream model names. @@ -322,24 +326,31 @@ pub(crate) fn build_direct_source_freshness_propagation_result<'py>( scope: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = scope.py(); - let downstream_deps: Bound<'py, PyAny> = native_field(scope, "downstream_deps")?; + let scope = scope_data(scope)?; + let downstream_deps: HashMap<&ObjectKey, &Vec> = scope + .downstream + .iter() + .map(|(key, deps)| (key, deps)) + .collect(); let changed: Bound<'py, PyDict> = PyDict::new(py); let unknown_sources: Bound<'py, PyDict> = PyDict::new(py); let errors: Bound<'py, PyDict> = PyDict::new(py); - let stale: Bound<'py, PySet> = PySet::empty(py)?; - let blocked: Bound<'py, PySet> = PySet::empty(py)?; + let mut stale: HashSet = HashSet::new(); + let mut blocked: HashSet = HashSet::new(); for identity in native_field(source_freshness, "changed_identities")?.try_iter()? { let identity: Bound<'py, PyAny> = identity?; - let models: Bound<'py, PyFrozenSet> = - downstream_models(&native_field(&identity, "source_name")?, &downstream_deps)?; - stale.call_method1("update", (&models,))?; - changed.set_item(identity, models)?; + let models = downstream_models( + native_field(&identity, "source_name")?.extract()?, + &downstream_deps, + ); + stale.extend(models.iter().cloned()); + changed.set_item(identity, PyFrozenSet::new(py, models)?)?; } for name in native_field(source_freshness, "unknown_source_names")?.try_iter()? { let name: Bound<'py, PyAny> = name?; - let models: Bound<'py, PyFrozenSet> = downstream_models(&name, &downstream_deps)?; - stale.call_method1("update", (&models,))?; - unknown_sources.set_item(name, models)?; + let models = downstream_models(name.extract()?, &downstream_deps); + stale.extend(models.iter().cloned()); + unknown_sources.set_item(name, PyFrozenSet::new(py, models)?)?; } let error_status: Bound<'py, PyAny> = enum_member(py, RecordId::SourceFreshnessAgeStatus, "ERROR")?; @@ -350,10 +361,12 @@ pub(crate) fn build_direct_source_freshness_propagation_result<'py>( if status.ne(&error_status)? { continue; } - let models: Bound<'py, PyFrozenSet> = - downstream_models(&native_field(&identity, "source_name")?, &downstream_deps)?; - blocked.call_method1("update", (&models,))?; - errors.set_item(identity, models)?; + let models = downstream_models( + native_field(&identity, "source_name")?.extract()?, + &downstream_deps, + ); + blocked.extend(models.iter().cloned()); + errors.set_item(identity, PyFrozenSet::new(py, models)?)?; } new_record_with( py, @@ -362,8 +375,11 @@ pub(crate) fn build_direct_source_freshness_propagation_result<'py>( ("changed_source_model_names", changed.into_any()), ("unknown_source_model_names", unknown_sources.into_any()), ("error_source_model_names", errors.into_any()), - ("stale_model_names", frozen(&stale)?), - ("blocked_model_names", frozen(&blocked)?), + ("stale_model_names", PyFrozenSet::new(py, stale)?.into_any()), + ( + "blocked_model_names", + PyFrozenSet::new(py, blocked)?.into_any(), + ), ], ) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/clone_plans.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/clone_plans.rs index b90ca8da71..0ce54fbd37 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/clone_plans.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/clone_plans.rs @@ -8,6 +8,7 @@ use crate::bindings::_helpers::compiled_views::project_reads; use crate::bindings::_helpers::compiled_views::seed_reads; use crate::bindings::_helpers::compiled_views::source_reads; use crate::bindings::_helpers::graph::keys::KeyCache; +use crate::bindings::_helpers::graph::node_set::PyNodeSet; use crate::bindings::_helpers::graph::project_graph::NativeProjectGraph; use crate::bindings::_helpers::graph::views::edge_dict; use crate::bindings::_helpers::model_config::typed_config::{config_node_of, model_values}; @@ -35,6 +36,8 @@ use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyRes use pyo3::types::{PyDict, PyDictMethods, PyFrozenSet, PySet, PySetMethods, PyTuple}; use pyo3::{pyfunction, wrap_pyfunction}; use sqlbuild_analysis::graph::main::downstream_edges::downstream_edges; +use sqlbuild_core::graph_walk::main::clone_ancestors::clone_ancestors; +use sqlbuild_core::graph_walk::models::ClonePolicy; use sqlbuild_model_config::config_values::models::ConfigNode; const ENABLED_KEY: &str = "enabled"; @@ -647,44 +650,20 @@ impl<'py> CloneGraph<'py> { /// Depth-first walk from every selected node in Python's recursion order. fn walk(&self, skipped_views: bool) -> PyResult> { let py = self.selected.py(); - let visited = PySet::empty(py)?; - let found = PySet::empty(py)?; - for start in self.selected.try_iter()? { - let start = start?; - if visited.contains(&start)? { - continue; - } - visited.add(&start)?; - let mut stack: Vec<(Vec>, usize)> = vec![(self.parents(&start)?, 0)]; - while let Some((parents, index)) = stack.last_mut() { - let Some(parent) = parents.get(*index).cloned() else { - stack.pop(); - continue; - }; - *index += 1; - let descend = if self.selected.contains(&parent)? { - true - } else if skipped_views { - let view = self.holds(&self.is_view, &parent)?; - if view && self.holds(&self.is_clonable, &parent)? { - found.add(&parent)?; - } - view - } else if self.holds(&self.is_view, &parent)? { - true - } else { - if self.holds(&self.is_clonable, &parent)? { - found.add(&parent)?; - } - false - }; - if descend && !visited.contains(&parent)? { - visited.add(&parent)?; - stack.push((self.parents(&parent)?, 0)); - } - } - } - PyFrozenSet::new(py, found.iter()) + let mut visited = PyNodeSet(PySet::empty(py)?); + let mut found = PyNodeSet(PySet::empty(py)?); + clone_ancestors( + self.selected.try_iter()?, + (&mut visited, &mut found), + |node| self.parents(node), + ClonePolicy { + selected: |node: &Bound<'py, PyAny>| self.selected.contains(node), + clonable: |node: &Bound<'py, PyAny>| self.holds(&self.is_clonable, node), + view: |node: &Bound<'py, PyAny>| self.holds(&self.is_view, node), + skipped_views, + }, + )?; + PyFrozenSet::new(py, found.0.iter()) } } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/column_overlay.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/column_overlay.rs index 6e1fd8aa45..76b3b602c1 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/column_overlay.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/column_overlay.rs @@ -3,6 +3,7 @@ use crate::bindings::_helpers::boundary::python_calls::imported; use crate::bindings::_helpers::boundary::python_calls::keywords; use crate::bindings::_helpers::compiled_views::model_reads; +use crate::bindings::_helpers::native_host::model_lookup::ModelLookup; use crate::bindings::_helpers::planner_migrations::column_objects::{ config, definition_hash, lower, native, replace, }; @@ -12,6 +13,7 @@ use crate::bindings::_helpers::planner_migrations::models::{ }; use crate::bindings::_helpers::planner_migrations::projections::renames_explain_change; use crate::bindings::_helpers::planner_phases::objects::object; +use crate::bindings::_helpers::planner_scope::retained::scope_data; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::fields::mapping_get; use crate::bindings::_helpers::records::ids::RecordId; @@ -131,12 +133,11 @@ pub(super) fn cursors<'py>( entries: &[Bound<'py, PyAny>], ) -> PyResult> { let py = snapshot.py(); - let scope = phase.scope.clone(); - let models = native_field(&scope, "models_by_name")?; + let models = ModelLookup::Native(scope_data(&phase.scope)?); let origins = PyDict::new(py); for entry in entries { let name = native_field(entry, "model_name")?; - let model = mapping_get(&models, &name)?; + let model = models.get(py, &name.extract::()?)?; let cursor = if model.is_truthy()? { config(&model, "cursor")? } else { @@ -159,7 +160,7 @@ pub(super) fn cursors<'py>( physical } else { let qualified = native_field( - &native_field(&models.get_item(&name)?, "destination")?, + &model_reads::destination(&models.get(py, &name.extract::()?)?)?, "qualified_name", )?; if qualified.is_truthy()? { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/old_name_planning.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/old_name_planning.rs index b85d9f1923..a6ae60868c 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/old_name_planning.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/old_name_planning.rs @@ -16,8 +16,9 @@ use crate::bindings::_helpers::planner_phases::runtime::runtime_field; use crate::bindings::_helpers::planner_scope::retained::scope_data; use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; -use pyo3::types::{PyDict, PyDictMethods, PySet, PySetMethods, PyTuple}; +use pyo3::types::{PySet, PySetMethods, PyTuple}; use sqlbuild_analysis::planner_identity::constants::MODEL_RESOURCE; +use std::collections::HashMap; fn schemas<'py>( (phase, migration_entries): (&MigrationPhase<'py>, &Bound<'py, PyAny>), @@ -82,17 +83,17 @@ pub(crate) fn plan_old_name_views<'py>( let models: Vec> = project_reads::models(&project)? .try_iter()? .collect::>()?; - let by_name = PyDict::new(py); + let mut by_name: HashMap> = HashMap::new(); for item in &models { - by_name.set_item(model_reads::name_object(item)?, item)?; + by_name.insert(model_reads::model_name(item)?, item.clone()); } let mut selected: Vec> = Vec::new(); for key in &scope.order { if scope.selected.contains(key) && key.0 == MODEL_RESOURCE - && let Some(item) = by_name.get_item(&key.1)? + && let Some(item) = by_name.get(&key.1) { - selected.push(item); + selected.push(item.clone()); } } let state = super::state_inspection::inspection(&runtime, None)?; @@ -131,8 +132,8 @@ pub(crate) fn plan_old_name_views<'py>( for entry in moves.0.try_iter()? { let entry = entry?; let name = native_field(&entry, "model_name")?; - if let Some(item) = by_name.get_item(&name)? - && let Some(planned) = planner.for_migration(&entry, &item)? + if let Some(item) = by_name.get(&name.extract::()?) + && let Some(planned) = planner.for_migration(&entry, item)? { entries.push(planned); covered.add(name)?; diff --git a/fensu.toml b/fensu.toml index f7a2915372..7e2ae7561a 100644 --- a/fensu.toml +++ b/fensu.toml @@ -1690,7 +1690,7 @@ pending_budgets = [ "crates/sqlbuild-python/src/bindings/_helpers/boundary/python_calls.rs=13", "crates/sqlbuild-python/src/bindings/_helpers/build_command/command.rs=3", "crates/sqlbuild-python/src/bindings/_helpers/build_command/completion.rs=9", - "crates/sqlbuild-python/src/bindings/_helpers/build_command/defer_clone.rs=20", + "crates/sqlbuild-python/src/bindings/_helpers/build_command/defer_clone.rs=16", "crates/sqlbuild-python/src/bindings/_helpers/build_command/execution.rs=11", "crates/sqlbuild-python/src/bindings/_helpers/build_command/invocation.rs=5", "crates/sqlbuild-python/src/bindings/_helpers/build_command/lifecycle.rs=22", @@ -1913,7 +1913,7 @@ pending_budgets = [ "crates/sqlbuild-python/src/bindings/_helpers/freshness/freshness_command.rs=17", "crates/sqlbuild-python/src/bindings/_helpers/freshness/freshness_observe.rs=6", "crates/sqlbuild-python/src/bindings/_helpers/freshness/observation.rs=3", - "crates/sqlbuild-python/src/bindings/_helpers/freshness/planning.rs=4", + "crates/sqlbuild-python/src/bindings/_helpers/freshness/planning.rs=1", "crates/sqlbuild-python/src/bindings/_helpers/freshness/publication.rs=9", "crates/sqlbuild-python/src/bindings/_helpers/freshness/state_io.rs=3", "crates/sqlbuild-python/src/bindings/_helpers/freshness/support.rs=2", diff --git a/python-boundary.toml b/python-boundary.toml index 532ec56b91..ee2346f85d 100644 --- a/python-boundary.toml +++ b/python-boundary.toml @@ -5,6 +5,11 @@ # 1 = executor or adapter runtime and the shapes it consumes; 2 = thin shim that invokes user # Python; 3 = public facade (facade only); 4 = glue around a Python-only third-party library. # Native failures must surface as errors; `handled_native_errors` lists the reviewed exceptions. +# Zero movable Python lines does not mean zero Python objects at the native boundary. Public +# records and callback-based graph APIs retain Python values, hashing and equality; caller-authored +# scopes are imported at entry. Adapter/executor requests and results, user code and opaque config +# leaves also cross intentionally. Native-owned graph walks and model lookups use retained scope +# data instead of projecting those public containers and immediately reading them back. remaining_lines = 0 [[stays]]