diff --git a/crates/sqlbuild-python/src/bindings/_helpers/model_config/typed_config.rs b/crates/sqlbuild-python/src/bindings/_helpers/model_config/typed_config.rs index 593a3ca009..46ece2225c 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/model_config/typed_config.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/model_config/typed_config.rs @@ -5,9 +5,14 @@ use std::sync::{Arc, Mutex}; use pyo3::exceptions::PyRuntimeError; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyErr, PyResult, Python}; use pyo3::pyclass; +use pyo3::types::{PyString, PyTuple}; use sqlbuild_model_config::config_values::errors::ConfigTypeError; +use sqlbuild_model_config::config_values::main::checked_cursor_bound::checked_cursor_bound; +use sqlbuild_model_config::config_values::main::checked_flag::checked_flag; +use sqlbuild_model_config::config_values::main::checked_integer::checked_integer; use sqlbuild_model_config::config_values::main::checked_text::checked_text; -use sqlbuild_model_config::config_values::models::ConfigNode; +use sqlbuild_model_config::config_values::main::checked_texts::checked_texts; +use sqlbuild_model_config::config_values::models::{ConfigNode, CursorBound}; use crate::bindings::_helpers::compile_driver::stage_work::{TYPED_CONFIG_READS, count_stage}; use crate::bindings::_helpers::model_config::config_nodes::{ @@ -21,6 +26,7 @@ use crate::bindings::_helpers::records::models::NativeRecord; const CONFIG_FIELD: &str = "config"; const VALUES_FIELD: &str = "values"; +const ISOFORMAT_METHOD: &str = "isoformat"; /// The config record's cache slot holding its typed values; no Python attribute reads it. const TYPED_VALUES_SLOT: &str = "__typed_values__"; const POISONED: &str = "typed config values cache is poisoned"; @@ -161,3 +167,87 @@ pub(crate) fn config_text_is( _ => false, }) } + +/// A cursor bound's text object: a string's or integer's `str()`, a date's `isoformat()`. +pub(crate) fn cursor_bound_object<'py>( + py: Python<'py>, + bound: CursorBound<'_, HostHandle>, +) -> PyResult> { + match bound { + CursorBound::Dated(value) => config_object(py, value)?.call_method0(ISOFORMAT_METHOD), + CursorBound::Stringified(ConfigNode::Str(text)) => Ok(PyString::new(py, text).into_any()), + CursorBound::Stringified(ConfigNode::Int(number)) => { + Ok(PyString::new(py, &number.to_string()).into_any()) + } + CursorBound::Stringified(value) => Ok(config_object(py, value)?.str()?.into_any()), + } +} + +/// `key`'s cursor bound as text, or `None` when absent or `None`. +pub(crate) fn config_cursor_bound<'py>( + py: Python<'py>, + values: &ConfigValues<'_>, + key: &str, +) -> PyResult>> { + let node: Arc = node(values, key)?; + checked_cursor_bound(key, Some(&node)) + .map_err(|error| type_error(py, &error))? + .map(|bound| cursor_bound_object(py, bound)) + .transpose() +} + +/// `values.get(key)` as its Python object, `None` when absent. +pub(crate) fn config_value<'py>( + py: Python<'py>, + values: &ConfigValues<'_>, + key: &str, +) -> PyResult> { + let value: Arc = node(values, key)?; + config_object(py, &value) +} + +/// `values.get(key)` as its typed node, `None` (null) when absent. +pub(crate) fn config_node_of(values: &ConfigValues<'_>, key: &str) -> PyResult> { + node(values, key) +} + +/// `key`'s boolean value, or `None` when absent or `None`. +pub(crate) fn config_flag( + py: Python<'_>, + values: &ConfigValues<'_>, + key: &str, +) -> PyResult> { + let node: Arc = node(values, key)?; + checked_flag(key, Some(&node)).map_err(|error| type_error(py, &error)) +} + +/// `key`'s integer value as its Python object, or `None` when absent or `None`. +pub(crate) fn config_integer_object<'py>( + py: Python<'py>, + values: &ConfigValues<'_>, + key: &str, +) -> PyResult>> { + let node: Arc = node(values, key)?; + checked_integer(key, Some(&node)) + .map_err(|error| type_error(py, &error))? + .map(|value| config_object(py, value)) + .transpose() +} + +/// `key`'s strings as a tuple, or `None` when absent or `None`. +pub(crate) fn config_text_tuple<'py>( + py: Python<'py>, + values: &ConfigValues<'_>, + key: &str, +) -> PyResult>> { + let node: Arc = node(values, key)?; + let Some(items) = checked_texts(key, Some(&node)).map_err(|error| type_error(py, &error))? + else { + return Ok(None); + }; + let objects: Vec> = items + .into_iter() + .map(|item| config_object(py, item)) + .collect::>()?; + PyTuple::new(py, objects).map(Some) +} 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 eea4e862e7..14c8f0fc90 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 @@ -4,6 +4,7 @@ use crate::bindings::_helpers::boundary::python_calls::KeywordArguments; use crate::bindings::_helpers::boundary::python_calls::keywords; use crate::bindings::_helpers::compiled_views::model_reads; use crate::bindings::_helpers::graph::project_graph::NativeProjectGraph; +use crate::bindings::_helpers::model_config::typed_config::{config_node_of, model_values}; use crate::bindings::_helpers::planner_entries::models::ModelPlanContext; use crate::bindings::_helpers::planner_entries::values::{ native, planner_model, planner_type, string, @@ -28,17 +29,21 @@ use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use pyo3::PyRef; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; -use pyo3::types::{PyBool, PyDict, PyDictMethods, PyFrozenSet, PySet, PySetMethods, PyTuple}; +use pyo3::types::{PyDict, PyDictMethods, PyFrozenSet, PySet, PySetMethods, PyTuple}; use pyo3::{pyfunction, wrap_pyfunction}; +use sqlbuild_model_config::config_values::models::ConfigNode; + +const ENABLED_KEY: &str = "enabled"; const VIEW_MATERIALIZATION: &str = "view"; const GRAPH_ARGUMENTS: [&str; 4] = ["selected", "upstream", "is_clonable", "is_view"]; /// Whether a model is explicitly disabled with a boolean `enabled = false`. fn disabled_model(model: &Bound<'_, PyAny>) -> PyResult { - let raw = native_field(&native_field(model, "config")?, "values")? - .call_method1("get", ("enabled",))?; - Ok(raw.is_instance_of::() && !raw.is_truthy()?) + Ok(matches!( + *config_node_of(&model_values(model)?, ENABLED_KEY)?, + ConfigNode::Bool(false) + )) } fn tuple_of<'py>(py: Python<'py>, items: Vec>) -> PyResult> { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_identity/metadata.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_identity/metadata.rs index 9f3fbbd822..10e6618d2b 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_identity/metadata.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_identity/metadata.rs @@ -17,6 +17,7 @@ use sqlbuild_analysis::planner_identity::models::FunctionFingerprintFields; use crate::bindings::_helpers::boundary::python_calls::{imported, keywords, within}; use crate::bindings::_helpers::compiled_views::model_reads; use crate::bindings::_helpers::discovery::hook_entries::hook_entries; +use crate::bindings::_helpers::model_config::typed_config::{config_value, model_values}; use crate::bindings::_helpers::planner_identity::hashing::function_definition_hash; use crate::bindings::_helpers::planner_identity::json_values::{COMPACT, SPACED, sorted_dumps}; use crate::bindings::_helpers::records::fields::field as native_field; @@ -250,8 +251,7 @@ pub(crate) fn model_declared_columns_hash(model: &Bound<'_, PyAny>) -> PyResult< let payload: Bound<'_, PyDict> = PyDict::new(py); payload.set_item( "contract", - native_field(&native_field(model, "config")?, "values")? - .call_method1("get", (CONTRACT_CONFIG_KEY,))?, + config_value(py, &model_values(model)?, CONTRACT_CONFIG_KEY)?, )?; let columns: Bound<'_, PyList> = PyList::empty(py); let mut families: Bound<'_, PyList> = PyList::empty(py); diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/compatibility.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/compatibility.rs index 4a0d766153..14ced8eff4 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/compatibility.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/compatibility.rs @@ -4,6 +4,7 @@ use crate::bindings::_helpers::boundary::python_calls::KeywordArguments; use crate::bindings::_helpers::boundary::python_calls::keywords; use crate::bindings::_helpers::compiled_views::model_reads; use crate::bindings::_helpers::compiled_views::model_reads::inferred_rows; +use crate::bindings::_helpers::model_config::typed_config::{config_value, model_values}; use crate::bindings::_helpers::planner_changes::detect::detect_model_changes; use crate::bindings::_helpers::planner_migrations::models::{ CompatibilityCheck, MigrationCompatibilityResult, @@ -19,9 +20,7 @@ use sqlbuild_analysis::planner_migrations::main::compatibility_findings::compati use sqlbuild_analysis::planner_migrations::models::CompatibilityFinding; fn config(model: &Bound<'_, PyAny>, key: &str) -> PyResult> { - native_field(&native_field(model, "config")?, "values")? - .call_method1("get", (key,))? - .extract() + config_value(model.py(), &model_values(model)?, key)?.extract() } /// The schema findings that block migrating one model's history; `snapshots_config` is given for snapshots. diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/model_planning.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/model_planning.rs index 1ad521d07d..453a186d4c 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/model_planning.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/model_planning.rs @@ -2,6 +2,9 @@ use crate::bindings::_helpers::boundary::python_calls::call_with; use crate::bindings::_helpers::boundary::python_calls::keywords; +use crate::bindings::_helpers::model_config::typed_config::{ + config_node_of, config_value, model_values, +}; use crate::bindings::_helpers::planner_changes::renames::plan_reference_renames; use crate::bindings::_helpers::planner_migrations::column_objects::{config, replace}; use crate::bindings::_helpers::planner_migrations::discovery::discover_model_migrations; @@ -16,18 +19,19 @@ use crate::bindings::_helpers::planner_phases::objects::object; use crate::bindings::_helpers::planner_phases::runtime::runtime_field; use crate::bindings::_helpers::records::construct::{enum_member, new_record}; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::fields::mapping_get; use crate::bindings::_helpers::records::fields::mapping_get_or; use crate::bindings::_helpers::records::ids::RecordId; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; -use pyo3::types::{ - PyBool, PyDict, PyDictMethods, PyFrozenSet, PySet, PySetMethods, PyString, PyTuple, -}; +use pyo3::types::{PyDict, PyDictMethods, PyFrozenSet, PySet, PySetMethods, PyString, PyTuple}; +use sqlbuild_model_config::config_values::models::ConfigNode; + +const MIGRATE_FROM_KEY: &str = "migrate_from"; +const MIGRATE_FORCE_KEY: &str = "migrate_force"; fn declared<'py>(item: &Bound<'py, PyAny>) -> PyResult>> { let py = item.py(); - let values = native_field(&native_field(item, "config")?, "values")?; - let origin = mapping_get(&values, "migrate_from")?; + let values = model_values(item)?; + let origin = config_value(py, &values, MIGRATE_FROM_KEY)?; if !origin.is_instance_of::() { return Ok(None); } @@ -38,7 +42,10 @@ fn declared<'py>(item: &Bound<'py, PyAny>) -> PyResult(model: &Bound<'py, PyAny>) -> PyResult> { - native_field(&native_field(model, "config")?, "values")?.call_method1("get", ("full_refresh",)) + config_value(model.py(), &model_values(model)?, FULL_REFRESH_KEY) } /// dbt-compatible nullable model full-refresh semantics. diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/retention.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/retention.rs index 86d36cd3ee..f48921bebd 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/retention.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/retention.rs @@ -5,6 +5,7 @@ use sqlbuild_host::entries::models::AdapterHost; use crate::bindings::_helpers::boundary::python_calls::call_with; use crate::bindings::_helpers::compiled_views::model_reads; use crate::bindings::_helpers::contracts::spec_resolution::resolve_target_config; +use crate::bindings::_helpers::model_config::typed_config::{config_value, typed_values}; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::planner_phases::constants::{ BIGQUERY_ADAPTER, DECREASE_WARNING, DOWNGRADE_HISTORY_DAYS, PERMANENT_TABLE_TYPE, @@ -28,6 +29,8 @@ use sqlbuild_analysis::planner_warehouse::main::retention_direction::retention_d use sqlbuild_analysis::planner_warehouse::main::table_type_copy_name::table_type_copy_name as copy_name; use std::collections::{BTreeMap, BTreeSet}; +const MATERIALIZED_KEY: &str = "materialized"; + /// The resolved config of the planned target, or defaults without one. #[pyfunction] #[pyo3(signature = (*, runtime))] @@ -123,7 +126,7 @@ pub(crate) fn plan_table_types<'py>( &native_field(&materialization, "is_table_backed")?, &[( "materialized", - native_field(&config, "values")?.call_method1("get", ("materialized",))?, + config_value(py, &typed_values(&config)?, MATERIALIZED_KEY)?, )], )?; if !backed.is_truthy()? { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_model.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_model.rs index 5adc03600f..67578a72dd 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_model.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_model.rs @@ -6,8 +6,8 @@ use crate::bindings::_helpers::boundary::python_calls::call_with; use crate::bindings::_helpers::boundary::python_calls::keywords; use crate::bindings::_helpers::compile_attachment::cursor_roles::resolve_cursor_input_roles; use crate::bindings::_helpers::compiled_views::model_reads; -use crate::bindings::_helpers::model_config::config_values::{ - config_cursor_bound, config_str as authored_config_str, +use crate::bindings::_helpers::model_config::typed_config::{ + config_cursor_bound, config_text_object, model_values, }; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::plan_output::display::resolve_bounded_cursor_override; @@ -35,13 +35,14 @@ use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; use pyo3::{IntoPyObject, PyErr}; fn config<'py>(model: &Bound<'py, PyAny>, key: &str) -> PyResult> { - let values: Bound<'py, PyAny> = native_field(&native_field(model, "config")?, "values")?; + let py: Python<'py> = model.py(); + let values = model_values(model)?; let value: Option> = if key == CURSOR_START { - config_cursor_bound(&values, key)? + config_cursor_bound(py, &values, key)? } else { - authored_config_str(&values, key)? + config_text_object(py, &values, key)? }; - Ok(value.unwrap_or_else(|| model.py().None().into_bound(model.py()))) + Ok(value.unwrap_or_else(|| py.None().into_bound(py))) } fn unbounded(py: Python<'_>) -> PyResult> { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_overrides.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_overrides.rs index 47acfc8e63..81b8d045ee 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_overrides.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_overrides.rs @@ -2,7 +2,9 @@ use crate::bindings::_helpers::boundary::python_calls::{KeywordArguments, call_with}; use crate::bindings::_helpers::compiled_views::model_reads; -use crate::bindings::_helpers::model_config::config_values::config_str as authored_config_str; +use crate::bindings::_helpers::model_config::typed_config::{ + config_text_is, config_text_object, model_values, +}; use crate::bindings::_helpers::planner_resolution::values::{method, string}; use crate::bindings::_helpers::records::construct::enum_from_value; use crate::bindings::_helpers::records::construct::error_class; @@ -15,6 +17,13 @@ use pyo3::types::PyDict; use pyo3::{pyfunction, wrap_pyfunction}; use sqlbuild_cursor::planner::main::override_kind::override_kind; +const MATERIALIZED_KEY: &str = "materialized"; +const CURSOR_KEY: &str = "cursor"; +const CURSOR_TYPE_KEY: &str = "cursor_type"; +const INCREMENTAL: &str = "incremental"; +const INTEGER_CURSOR: &str = "integer"; +const TIMESTAMP_CURSOR: &str = "timestamp"; + #[pyfunction(name = "type_cursor_overrides")] #[pyo3(signature = (**kwargs))] fn type_cursor_overrides_of<'py>( @@ -48,16 +57,15 @@ pub(crate) fn type_cursor_overrides<'py>( } let mut models: Vec<(String, String)> = Vec::new(); for model in selected { - let values = native_field(&native_field(model, "config")?, "values")?; - let config = |key: &str| -> PyResult> { - Ok(authored_config_str(&values, key)?.unwrap_or_else(|| py.None().into_bound(py))) - }; - if !config("materialized")?.eq("incremental")? || config("cursor")?.is_none() { + let values = model_values(model)?; + let incremental: bool = config_text_is(py, &values, MATERIALIZED_KEY, INCREMENTAL)?; + if !incremental || config_text_object(py, &values, CURSOR_KEY)?.is_none() { continue; } - let kind = config("cursor_type")?; - if kind.eq("integer")? || kind.eq("timestamp")? { - models.push((model_reads::name_object(model)?.extract()?, kind.extract()?)); + for kind in [INTEGER_CURSOR, TIMESTAMP_CURSOR] { + if config_text_is(py, &values, CURSOR_TYPE_KEY, kind)? { + models.push((model_reads::name_object(model)?.extract()?, kind.to_owned())); + } } } let mut integer_literals = true; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_policies.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_policies.rs index a33e740b2c..a32c19a153 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_policies.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_policies.rs @@ -1,9 +1,9 @@ //! Model-level cursor safety policy overrides over project configuration. use crate::bindings::_helpers::boundary::python_calls::call_with; +use crate::bindings::_helpers::model_config::typed_config::{config_value, model_values}; use crate::bindings::_helpers::records::construct::{enum_from_value, record_class}; 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; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult}; use pyo3::types::PyString; @@ -45,9 +45,9 @@ fn resolve_policy<'py>( } else { class.call0()? }; - let values = native_field(&native_field(model, "config")?, "values")?; - let raw_distance = mapping_get(&values, fields.distance_key)?; - let raw_action = mapping_get(&values, fields.action_key)?; + let values = model_values(model)?; + let raw_distance = config_value(py, &values, fields.distance_key)?; + let raw_action = config_value(py, &values, fields.action_key)?; let mut distance = configured.getattr(fields.distance)?; if raw_distance.is_instance_of::() { distance = if raw_distance.eq(POLICY_DISABLED)? { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_ranges.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_ranges.rs index d1dafb1736..74e9947bdf 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_ranges.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_ranges.rs @@ -8,8 +8,9 @@ use crate::bindings::_helpers::compiled_views::model_reads; use crate::bindings::_helpers::cursor::conversion::CursorSession; use crate::bindings::_helpers::cursor::scalars::{parse_value, sentinel_to_token}; use crate::bindings::_helpers::cursor::values::{local_date_time, replaced}; -use crate::bindings::_helpers::model_config::config_values::config_cursor_bound; -use crate::bindings::_helpers::model_config::config_values::config_str as authored_config_str; +use crate::bindings::_helpers::model_config::typed_config::{ + config_cursor_bound as typed_cursor_bound, config_text_object, model_values, +}; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::plan_output::display::resolve_bounded_cursor_override; use crate::bindings::_helpers::planner_entries::models::ModelPlanContext; @@ -81,11 +82,9 @@ pub(crate) struct CursorWindowInputs<'py> { /// One string-valued model configuration entry. pub(crate) fn config_str<'py>(model: &Bound<'py, PyAny>, key: &str) -> PyResult> { - Ok(authored_config_str( - &native_field(&native_field(model, "config")?, "values")?, - key, - )? - .unwrap_or_else(|| model.py().None().into_bound(model.py()))) + let py: Python<'py> = model.py(); + Ok(config_text_object(py, &model_values(model)?, key)? + .unwrap_or_else(|| py.None().into_bound(py))) } /// One cursor-bound model configuration entry as its token text. @@ -93,13 +92,8 @@ pub(crate) fn config_bound<'py>( model: &Bound<'py, PyAny>, key: &str, ) -> PyResult> { - object( - model.py(), - config_cursor_bound( - &native_field(&native_field(model, "config")?, "values")?, - key, - )?, - ) + let py: Python<'py> = model.py(); + object(py, typed_cursor_bound(py, &model_values(model)?, key)?) } fn boolean(py: Python<'_>, value: bool) -> PyResult> { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/orchestration.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/orchestration.rs index ce6f4cb9c4..4f4a0953e9 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/orchestration.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/orchestration.rs @@ -6,7 +6,9 @@ use crate::bindings::_helpers::boundary::python_calls::{KeywordArguments, call_w use crate::bindings::_helpers::compile_attachment::cursor_roles::resolve_cursor_input_roles; use crate::bindings::_helpers::compiled_views::model_reads; use crate::bindings::_helpers::cursor::scalars::sentinel_to_token; -use crate::bindings::_helpers::model_config::config_values::config_str as authored_config_str; +use crate::bindings::_helpers::model_config::typed_config::{ + config_node_of, config_text_is, config_text_object, config_value, model_values, +}; use crate::bindings::_helpers::planner_entries::models::ModelPlanContext; use crate::bindings::_helpers::planner_phases::contexts::class_field; use crate::bindings::_helpers::planner_resolution::cursor_model::compute_model_cursor_bounds; @@ -19,12 +21,14 @@ use crate::bindings::_helpers::planner_resolution::sources::resolve_sources; use crate::bindings::_helpers::records::construct::error_class; use crate::bindings::_helpers::records::construct::record_class; 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_keywords; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; -use pyo3::types::{PyBool, PyDict, PyDictMethods, PyList, PyString, PyTuple}; +use pyo3::types::{PyDict, PyDictMethods, PyList, PyString, PyTuple}; use pyo3::{PyErr, pyfunction, wrap_pyfunction}; +use sqlbuild_model_config::config_values::models::ConfigNode; + +const APPEND_STRATEGY: &str = "append"; /// What resolving one model's, hook's or function's references reads. pub(crate) struct Resolution<'py> { @@ -194,11 +198,8 @@ pub(crate) fn resolve_model_sql<'py>( ) -> PyResult { let model = &request.model; let py = model.py(); - let cursor_type = authored_config_str( - &native_field(&native_field(model, "config")?, "values")?, - "cursor_type", - )? - .unwrap_or_else(|| py.None().into_bound(py)); + let cursor_type = config_text_object(py, &model_values(model)?, "cursor_type")? + .unwrap_or_else(|| py.None().into_bound(py)); let bounds = compute_model_cursor_bounds(request, context)?; let roles = resolve_cursor_input_roles(model)?; let filtering = CursorFiltering { @@ -230,15 +231,14 @@ pub(crate) fn resolve_model_sql<'py>( /// Effective append cursor lower-bound inclusivity. fn append_cursor_inclusive(model: &Bound<'_, PyAny>) -> PyResult { - let py = model.py(); - let strategy = authored_config_str( - &native_field(&native_field(model, "config")?, "values")?, - "incremental_strategy", - )? - .unwrap_or_else(|| py.None().into_bound(py)); - let raw = native_field(&native_field(model, "config")?, "values")? - .call_method1("get", ("append_cursor_inclusive",))?; - Ok(!strategy.eq("append")? || !raw.is_instance_of::() || raw.is_truthy()?) + let values = model_values(model)?; + let py: Python<'_> = model.py(); + let append: bool = config_text_is(py, &values, "incremental_strategy", APPEND_STRATEGY)?; + let inclusive: bool = match *config_node_of(&values, "append_cursor_inclusive")? { + ConfigNode::Bool(flag) => flag, + _ => true, + }; + Ok(!append || inclusive) } /// `__cursor_start()` / `__cursor_end()` replaced with the adapter's typed bound literals. @@ -341,11 +341,15 @@ pub(crate) fn model_hook_entries<'py>( model: &Bound<'py, PyAny>, resolution: &Resolution<'py>, ) -> PyResult> { - let values = native_field(&native_field(model, "config")?, "values")?; + let py: Python<'py> = model.py(); + let values = model_values(model)?; let name: String = model_reads::model_name(model)?; let mut hooks: Vec> = Vec::new(); for key in ["pre_hooks", "post_hooks"] { - hooks.push(resolution.hook(mapping_get(&values, key)?, &format!("Model '{name}' {key}"))?); + hooks.push(resolution.hook( + config_value(py, &values, key)?, + &format!("Model '{name}' {key}"), + )?); } PyTuple::new(model.py(), hooks) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_selection/model_selectors.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_selection/model_selectors.rs index 9dae06b6c8..34c0701681 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_selection/model_selectors.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_selection/model_selectors.rs @@ -13,11 +13,14 @@ use sqlbuild_analysis::planner_selection::models::{ }; use crate::bindings::_helpers::boundary::python_calls::imported; +use crate::bindings::_helpers::model_config::typed_config::{config_value, model_values}; use crate::bindings::_helpers::planner_selection::carrier::Carrier; use crate::bindings::_helpers::records::construct::new_record_with; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; +const TAGS_KEY: &str = "tags"; + const SHARED_EXCEPTIONS_MODULE: &str = "sqlbuild.errors.contracts.exceptions"; fn shared_input_error(carrier: &Carrier<'_>, message: &str) -> PyErr { @@ -60,8 +63,7 @@ pub(crate) fn split_selector_expansion<'py>( } fn model_tags(model: &Bound<'_, PyAny>) -> PyResult> { - let tags: Bound<'_, PyAny> = - native_field(&native_field(model, "config")?, "values")?.call_method1("get", ("tags",))?; + let tags: Bound<'_, PyAny> = config_value(model.py(), &model_values(model)?, TAGS_KEY)?; if !tags.is_instance_of::() && !tags.is_instance_of::() { return Ok(Vec::new()); } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/cursor_inputs.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/cursor_inputs.rs index c6b56458ed..3c4a567b6f 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/cursor_inputs.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/cursor_inputs.rs @@ -11,7 +11,9 @@ use crate::bindings::_helpers::boundary::keyword_request::Request; use crate::bindings::_helpers::compile_attachment::cursor_roles::resolve_cursor_input_roles; use crate::bindings::_helpers::compiled_views::model_reads; use crate::bindings::_helpers::cursor::scalars::parse_value; -use crate::bindings::_helpers::model_config::config_values::{config_cursor_bound, config_str}; +use crate::bindings::_helpers::model_config::typed_config::{ + config_cursor_bound, config_text_object, config_value, model_values, +}; use crate::bindings::_helpers::planner_identity::metadata::or_empty; use crate::bindings::_helpers::planner_strategy::constants::{ ENFORCED_CONTRACT, INCREMENTAL_MATERIALIZATION, MODEL_REFERENCE, SEED_REFERENCE, @@ -27,17 +29,13 @@ use crate::bindings::_helpers::records::fields::mapping_items; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::references::source_relations::render_source_relation; -/// A model's authored config values. -pub(crate) fn model_values<'py>(model: &Bound<'py, PyAny>) -> PyResult> { - native_field(&native_field(model, "config")?, "values") -} - /// Whether a model is incremental with a cursor column. fn incremental_cursor_flag( model: &Bound<'_, PyAny>, cursor_column: &Bound<'_, PyAny>, ) -> PyResult { - let materialized: Option> = config_str(&model_values(model)?, "materialized")?; + let materialized: Option> = + config_text_object(model.py(), &model_values(model)?, "materialized")?; Ok(!cursor_column.is_none() && match materialized { Some(materialized) => materialized.eq(INCREMENTAL_MATERIALIZATION)?, @@ -129,7 +127,8 @@ pub(crate) fn reject_unexposed_cursor_input_columns<'py>( if kind == MODEL_REFERENCE { let upstream: Bound<'py, PyAny> = mapping_get(models_by_name, &name)?; if upstream.is_none() - || !mapping_get(&model_values(&upstream)?, "contract")?.eq(ENFORCED_CONTRACT)? + || !config_value(py, &model_values(&upstream)?, "contract")? + .eq(ENFORCED_CONTRACT)? { continue; } @@ -218,7 +217,7 @@ fn terminal_bound<'py>( let Some(producer) = producer else { return Ok(py.None().into_bound(py)); }; - match config_cursor_bound(&model_values(producer)?, key)? { + match config_cursor_bound(py, &model_values(producer)?, key)? { Some(raw) => parse_value(&raw, cursor_type), None => Ok(py.None().into_bound(py)), } @@ -263,10 +262,11 @@ pub(crate) fn build_cursor_input_relations<'py>( let functions_by_name: &Bound<'py, PyAny> = &lookups.functions_by_name; let model_locations: &Bound<'py, PyAny> = &lookups.model_locations; let producers: &Bound<'py, PyAny> = &lookups.runtime_cursor_producer_names; - let cursor_type: Bound<'py, PyAny> = match config_str(&model_values(&model)?, "cursor_type")? { - Some(cursor_type) if cursor_type.is_truthy()? => cursor_type, - _ => PyString::new(py, TIMESTAMP_CURSOR).into_any(), - }; + let cursor_type: Bound<'py, PyAny> = + match config_text_object(py, &model_values(&model)?, "cursor_type")? { + Some(cursor_type) if cursor_type.is_truthy()? => cursor_type, + _ => PyString::new(py, TIMESTAMP_CURSOR).into_any(), + }; let mut relations: Vec> = Vec::new(); let watermarks: Bound<'py, PyAny> = native_field(&resolve_cursor_input_roles(&model)?, "watermark_inputs")?; @@ -290,7 +290,7 @@ pub(crate) fn build_cursor_input_relations<'py>( None }; let grain: Bound<'py, PyAny> = match &producer { - Some(producer) => config_str(&model_values(producer)?, "cursor_grain")? + Some(producer) => config_text_object(py, &model_values(producer)?, "cursor_grain")? .unwrap_or_else(|| py.None().into_bound(py)), None => py.None().into_bound(py), }; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/fixture_metadata.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/fixture_metadata.rs index 1ebfb3713c..aed49e1448 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/fixture_metadata.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/fixture_metadata.rs @@ -7,10 +7,14 @@ use sqlbuild_core::text::main::python_casefold::python_casefold; use crate::bindings::_helpers::compiled_views::compiled_model_view::InferredRow; use crate::bindings::_helpers::compiled_views::model_reads::{inferred_rows, star_flags}; +use crate::bindings::_helpers::model_config::typed_config::{config_value, model_values}; +use crate::bindings::_helpers::planner_strategy::constants::ENFORCED_CONTRACT; use crate::bindings::_helpers::planner_strategy::fixture_values::columns; use crate::bindings::_helpers::planner_strategy::models::FixtureRelation; use crate::bindings::_helpers::records::fields::field as native_field; +const CONTRACT_KEY: &str = "contract"; + /// The model's merged inferred and declared columns, authoritative unless its analysis saw a star. pub(crate) fn model_fixture_relation(model: &Bound<'_, PyAny>) -> PyResult { let inferred: Option> = inferred_rows(model)?; @@ -30,9 +34,8 @@ pub(crate) fn model_fixture_relation(model: &Bound<'_, PyAny>) -> PyResult(values: &Bound<'py, PyAny>, key: &str) -> PyResult(model: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = model.py(); - let raw: Bound<'py, PyAny> = mapping_get(&model_values(model)?, "on_schema_change")?; + let raw: Bound<'py, PyAny> = config_value(py, &model_values(model)?, "on_schema_change")?; if raw.is_none() { return Ok(raw); } @@ -210,9 +210,9 @@ fn lookback_of( model: &Bound<'_, PyAny>, effective_grain: Option, ) -> PyResult> { - let values: Bound<'_, PyAny> = model_values(model)?; + let values: ConfigValues<'_> = model_values(model)?; let read = |key: &str| -> PyResult> { - config_str(&values, key)? + config_text_object(model.py(), &values, key)? .map(|value| text(&value)) .transpose() }; @@ -269,7 +269,7 @@ fn logical_ddl<'py>( "create_table" => "render_create_table_as", "incremental_append" => "render_append", "incremental_delete_insert" if !cursor_column.is_none() && !cursor_bounds.is_none() => { - let values: Bound<'py, PyAny> = model_values(model)?; + let values: ConfigValues<'py> = model_values(model)?; let start = sentinel_to_token(&native_field(&cursor_bounds, "start")?)?; let end = sentinel_to_token(&native_field(&cursor_bounds, "end")?)?; pairs.push(("cursor_column", cursor_column)); @@ -277,7 +277,7 @@ fn logical_ddl<'py>( pairs.push(("cursor_end", PyString::new(py, &end).into_any())); pairs.push(( "cursor_type", - or_none(py, config_str(&values, "cursor_type")?), + or_none(py, config_text_object(py, &values, "cursor_type")?), )); "render_delete_insert_cursor" } @@ -324,14 +324,14 @@ fn expected_output_columns<'py>(model: &Bound<'py, PyAny>) -> PyResult( request: &ModelEntryDecisions<'py>, - values: &Bound<'py, PyAny>, + values: &ConfigValues<'py>, entry: &Bound<'py, PyDict>, ) -> PyResult<()> { - let py: Python<'py> = values.py(); + let py: Python<'py> = values.0.py(); let storage: Bound<'py, PyDict> = PyDict::new(py); let mut keys: Vec<&str> = Vec::new(); for key in PHYSICAL_STORAGE_KEYS { - let value: Bound<'py, PyAny> = mapping_get(values, key)?; + let value: Bound<'py, PyAny> = config_value(py, values, key)?; if !value.is_none() { storage.set_item(key, value)?; keys.push(key); @@ -409,7 +409,7 @@ fn model_warnings<'py>( cursor_column: optional_string(&request.cursor_column)?, cursor_type: optional_string(&or_none( py, - config_str(&model_values(model)?, "cursor_type")?, + config_text_object(py, &model_values(model)?, "cursor_type")?, ))?, warehouse_columns: warehouse_column_types(warehouse_columns)?, sql_analysis_enabled: request.sql_analysis_enabled, @@ -474,10 +474,10 @@ fn ddl_cursor_bounds<'py>(request: &ModelEntryDecisions<'py>) -> PyResult( model: &Bound<'py, PyAny>, - values: &Bound<'py, PyAny>, + values: &ConfigValues<'py>, entry: &Bound<'py, PyDict>, ) -> PyResult<()> { - let Some(text) = config_str(values, MODEL_MAX_DURATION_KEY)? else { + let Some(text) = config_text_object(model.py(), values, MODEL_MAX_DURATION_KEY)? else { return Ok(()); }; let text: String = text.extract()?; @@ -506,27 +506,27 @@ fn execution_limit_fields<'py>( /// Copy the model's authored strategy, cursor, batch and snapshot settings onto the entry. fn config_fields<'py>( request: &ModelEntryDecisions<'py>, - values: &Bound<'py, PyAny>, + values: &ConfigValues<'py>, entry: &Bound<'py, PyDict>, ) -> PyResult<()> { let py: Python<'py> = request.model.py(); let model: &Bound<'py, PyAny> = &request.model; for (key, field) in STRING_FIELDS { - entry.set_item(field, or_none(py, config_str(values, key)?))?; + entry.set_item(field, or_none(py, config_text_object(py, values, key)?))?; } for key in SNAPSHOT_POLICY_FIELDS { - entry.set_item(key, or_none(py, config_str(values, key)?))?; + entry.set_item(key, or_none(py, config_text_object(py, values, key)?))?; } for key in ["cursor_start", "cursor_end"] { - entry.set_item(key, or_none(py, config_cursor_bound(values, key)?))?; + entry.set_item(key, or_none(py, config_cursor_bound(py, values, key)?))?; } - let microbatch: bool = match config_str(values, "incremental_mode")? { + let microbatch: bool = match config_text_object(py, values, "incremental_mode")? { Some(mode) => mode.eq(MICROBATCH_MODE)?, None => false, }; let lookback: Option = if microbatch { let read = |key: &str| -> PyResult> { - config_str(values, key)? + config_text_object(py, values, key)? .map(|value| text(&value)) .transpose() }; @@ -540,27 +540,25 @@ fn config_fields<'py>( )?; lookback_of(model, grain)? } else { - config_str(values, "lookback")? + config_text_object(py, values, "lookback")? .map(|value| text(&value)) .transpose()? }; entry.set_item("lookback", lookback)?; - let concurrency: Bound<'py, PyAny> = match config_int(values, "batch_concurrency")? { - Some(concurrency) if concurrency.is_truthy()? => concurrency, - _ => 1_i64.into_pyobject(py)?.into_any(), - }; + let concurrency: Bound<'py, PyAny> = + match config_integer_object(py, values, "batch_concurrency")? { + Some(concurrency) if concurrency.is_truthy()? => concurrency, + _ => 1_i64.into_pyobject(py)?.into_any(), + }; entry.set_item("batch_concurrency", concurrency)?; - let (limit, limit_action) = microbatch_limit_config(values)?; + let (limit, limit_action) = microbatch_limit_config(&values.0)?; entry.set_item("microbatch_limit", or_none(py, limit))?; entry.set_item( "declared_microbatch_limit_action", or_none(py, limit_action), )?; - entry.set_item("check_columns", string_list(values, "check_columns")?)?; - let hard_deletes: Bound<'py, PyAny> = match config_bool(values, "invalidate_hard_deletes")? { - Some(flag) if flag.is_truthy()? => flag, - _ => PyBool::new(py, false).to_owned().into_any(), - }; + entry.set_item("check_columns", string_list(&values.0, "check_columns")?)?; + let hard_deletes: bool = config_flag(py, values, "invalidate_hard_deletes")?.unwrap_or(false); entry.set_item("invalidate_hard_deletes", hard_deletes)?; Ok(()) } @@ -634,7 +632,7 @@ pub(crate) fn assemble_model_plan_entry<'py>( ) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyTuple>)> { let py: Python<'py> = request.model.py(); let model: &Bound<'py, PyAny> = &request.model; - let values: Bound<'py, PyAny> = model_values(model)?; + let values: ConfigValues<'py> = model_values(model)?; let change: &Bound<'py, PyAny> = &request.change_result; let snapshot: &Bound<'py, PyAny> = &request.snapshot; let name: Bound<'py, PyAny> = model_reads::name_object(model)?; @@ -649,9 +647,10 @@ pub(crate) fn assemble_model_plan_entry<'py>( (&schema_actions, &policy), (&enforcement, &warehouse_columns), )?; - let unique_key: Bound<'py, PyTuple> = string_list(&values, "unique_key")?; - let merge_exclude: Bound<'py, PyTuple> = config_string_tuple(&values, "merge_exclude_columns")? - .unwrap_or_else(|| PyTuple::empty(py)); + let unique_key: Bound<'py, PyTuple> = string_list(&values.0, "unique_key")?; + let merge_exclude: Bound<'py, PyTuple> = + config_text_tuple(py, &values, "merge_exclude_columns")? + .unwrap_or_else(|| PyTuple::empty(py)); let table_type: Bound<'py, PyAny> = native_field( &native_field( &native_field(&native_field(model, "config")?, "table_type")?, @@ -684,7 +683,7 @@ pub(crate) fn assemble_model_plan_entry<'py>( entry.set_item("expected_output_columns", expected_output_columns(model)?)?; entry.set_item("on_schema_change", policy)?; entry.set_item("type_enforcement", enforcement)?; - contract_fields(model, &values, &entry)?; + contract_fields(model, &values.0, &entry)?; entry.set_item("pre_hooks", &request.hooks.0)?; entry.set_item("post_hooks", &request.hooks.1)?; let fingerprint: Bound<'py, PyAny> = @@ -700,7 +699,7 @@ pub(crate) fn assemble_model_plan_entry<'py>( )?; entry.set_item("schema_actions", schema_actions)?; custom_fields( - &values, + &values.0, text(&request.materialization_type)? == CUSTOM_MATERIALIZATION, &entry, )?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/values.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/values.rs index 301b939fea..5340e9e146 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/values.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/values.rs @@ -1,5 +1,8 @@ //! Conversions between the planner's Python models and the native strategy rows. +use crate::bindings::_helpers::model_config::typed_config::{ + config_value as typed_config_value, model_values, +}; use crate::bindings::_helpers::planner_changes::results::native_enum; use crate::bindings::_helpers::records::construct::new_record_with; use pyo3::IntoPyObject; @@ -40,7 +43,7 @@ pub(crate) fn config_value<'py>( model: &Bound<'py, PyAny>, key: &str, ) -> PyResult> { - native_field(&native_field(model, "config")?, "values")?.call_method1("get", (key,)) + typed_config_value(model.py(), &model_values(model)?, key) } /// The materialization type of a model's `materialized` config. diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/cursors.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/cursors.rs index d4eb1f0050..974ff73a14 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/cursors.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/cursors.rs @@ -8,7 +8,7 @@ use crate::bindings::_helpers::cursor::scalars; use crate::bindings::_helpers::cursor::scalars::compare_values; use crate::bindings::_helpers::cursor::scalars::render_value; use crate::bindings::_helpers::cursor::scalars::to_inclusive; -use crate::bindings::_helpers::model_config::config_values::config_cursor_bound; +use crate::bindings::_helpers::model_config::typed_config::{config_cursor_bound, model_values}; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::planner_phases::objects::object; use crate::bindings::_helpers::planner_phases::objects::{ @@ -282,13 +282,7 @@ fn terminal_bound<'py>( let Some(upstream) = maps.models.get_item(native_field(reference, "ref_name")?)? else { return Ok(None); }; - let value = object( - py, - config_cursor_bound( - &native_field(&native_field(&upstream, "config")?, "values")?, - key, - )?, - )?; + let value = object(py, config_cursor_bound(py, &model_values(&upstream)?, key)?)?; if value.is_none() { return Ok(None); } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/semantic_sources.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/semantic_sources.rs index cd5d4baacd..a63794be55 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/semantic_sources.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/semantic_sources.rs @@ -3,6 +3,7 @@ use crate::bindings::_helpers::boundary::python_calls::KeywordArguments; use crate::bindings::_helpers::boundary::python_calls::call_with; use crate::bindings::_helpers::compiled_views::model_reads; +use crate::bindings::_helpers::model_config::typed_config::{config_node_of, model_values}; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::planner_phases::objects::{text, tuple}; use crate::bindings::_helpers::planner_warehouse::constants::ACTIVE_CATALOG; @@ -13,8 +14,11 @@ use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; use pyo3::pyfunction; use pyo3::types::PyDict as KeywordDict; use pyo3::types::{PyDict, PyDictMethods, PyTuple}; +use sqlbuild_model_config::config_values::models::ConfigNode; use std::collections::BTreeSet; +const SQL_ANALYSIS_KEY: &str = "sql_analysis"; + fn casefold(value: &Bound<'_, PyAny>) -> PyResult { value.call_method0("casefold")?.extract() } @@ -190,11 +194,11 @@ pub(crate) fn semantic_source_columns<'py>( let mut required: BTreeSet = BTreeSet::new(); for model in native_field(project, "models")?.try_iter()? { let model = model?; - let analysis = native_field(&native_field(&model, "config")?, "values")? - .call_method1("get", ("sql_analysis",))?; - if !selected_keys.contains(model_reads::model_key(&model)?)? - || analysis.is(pyo3::types::PyBool::new(py, false)) - { + let analysis_off: bool = matches!( + *config_node_of(&model_values(&model)?, SQL_ANALYSIS_KEY)?, + ConfigNode::Bool(false) + ); + if !selected_keys.contains(model_reads::model_key(&model)?)? || analysis_off { continue; } for reference in native_field(&model, "references")?.try_iter()? { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/snapshot.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/snapshot.rs index ee0642bf54..eb6a4f00d0 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/snapshot.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/snapshot.rs @@ -7,6 +7,7 @@ use crate::bindings::_helpers::compiled_views::model_reads; use crate::bindings::_helpers::fingerprints::latest_rows::select_latest_fingerprints; use crate::bindings::_helpers::fingerprints::storage_io::read_latest_fingerprints; use crate::bindings::_helpers::graph::algorithms::transitive_closure_many; +use crate::bindings::_helpers::model_config::typed_config::{config_node_of, model_values}; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::planner_identity::graph::build_execution_upstream_deps; use crate::bindings::_helpers::planner_phases::full_refresh::effectively_full_refreshed_model_names; @@ -35,6 +36,7 @@ use sqlbuild_core::constants::FINGERPRINT_TABLE_NAME; use sqlbuild_core::constants::MIGRATION_TABLE_NAME; use sqlbuild_core::constants::OLD_NAME_VIEW_TABLE_NAME; use sqlbuild_core::constants::SOURCE_FRESHNESS_TABLE_NAME; +use sqlbuild_model_config::types::{AuthoredNode, NodeKind}; use std::collections::{BTreeSet, HashSet}; /// Everything one snapshot gather needs. @@ -469,9 +471,7 @@ fn shares_unfiltered(project: &Bound<'_, PyAny>, selected: &Bound<'_, PyAny>) -> if !names.contains(&model_reads::model_name(&model)?) { all_selected = false; } - if native_field(&native_field(&model, "config")?, "values")? - .call_method1("get", (MIGRATE_FROM_CONFIG_KEY,))? - .is_instance_of::() + if config_node_of(&model_values(&model)?, MIGRATE_FROM_CONFIG_KEY)?.kind() == NodeKind::Str { any_migration = true; } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/source_deferral.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/source_deferral.rs index 1ad188cc4c..b0f40a579d 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/source_deferral.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/source_deferral.rs @@ -6,6 +6,7 @@ use crate::bindings::_helpers::authored_values::project_vars::render_project_var use crate::bindings::_helpers::boundary::python_calls::KeywordArguments; use crate::bindings::_helpers::compiled_views::model_reads; use crate::bindings::_helpers::contracts::spec_resolution::resolve_target_config; +use crate::bindings::_helpers::model_config::typed_config::{config_value, model_values}; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::planner_phases::objects::object; use crate::bindings::_helpers::planner_phases::objects::{ @@ -16,7 +17,6 @@ use crate::bindings::_helpers::planner_warehouse::constants::{ }; use crate::bindings::_helpers::records::construct::record_class; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::fields::mapping_get; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; use pyo3::pyfunction; use pyo3::types::PyDict as KeywordDict; @@ -54,9 +54,9 @@ fn hook_source_reads( let py = model.py(); let hook_entry = record_class(py, SQL_HOOK_ENTRY)?; let source_kind = host::entry(py, UserCodeHost::SqlResourceRefKind)?.getattr("SOURCE")?; - let values = native_field(&native_field(model, "config")?, "values")?; + let values = model_values(model)?; for key in MODEL_HOOK_CONFIG_KEYS { - let value = mapping_get(&values, key)?; + let value = config_value(py, &values, key)?; let entries: Vec> = if value.is_instance_of::() || value.is_instance_of::() { value.try_iter()?.collect::>()? diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/watermarks.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/watermarks.rs index cb21ee5eda..899d92932a 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/watermarks.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/watermarks.rs @@ -1,6 +1,7 @@ //! Validate reliable watermark column contracts before warehouse inspection. use crate::bindings::_helpers::compiled_views::model_reads; +use crate::bindings::_helpers::model_config::typed_config::{config_value, model_values}; use crate::bindings::_helpers::planner_phases::objects::object; use crate::bindings::_helpers::planner_phases::objects::{config_str, planner_error, text}; use crate::bindings::_helpers::planner_strategy::cursor_types::classify_cursor_sql_type; @@ -8,12 +9,14 @@ use crate::bindings::_helpers::planner_warehouse::constants::{ CURSOR_TYPE_INTEGER, CURSOR_TYPE_TIMESTAMP, WATERMARK_CONTRACT_CODE, }; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::fields::mapping_get; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyErr, PyResult}; use pyo3::pyfunction; use pyo3::types::{PyDict, PyDictMethods}; use std::collections::HashMap; +const CONTRACT_KEY: &str = "contract"; +const ENFORCED_CONTRACT: &str = "enforced"; + /// Whether a declared column type can hold the model's cursor type. #[pyfunction] #[pyo3(signature = (*, declared_type, cursor_type))] @@ -92,8 +95,8 @@ pub(crate) fn validate_watermark_column<'py>( (Some(WATERMARK_CONTRACT_CODE), None), )); } - let values = native_field(&native_field(&upstream, "config")?, "values")?; - if !mapping_get(&values, "contract")?.eq("enforced")? { + let contract = config_value(py, &model_values(&upstream)?, CONTRACT_KEY)?; + if !contract.eq(ENFORCED_CONTRACT)? { return Ok(()); } let schema = native_field(&upstream, "schema_entry")?; diff --git a/fensu.toml b/fensu.toml index cc5bec7318..0b708c5dce 100644 --- a/fensu.toml +++ b/fensu.toml @@ -1950,7 +1950,7 @@ pending_budgets = [ "crates/sqlbuild-python/src/bindings/_helpers/model_config/model_config_builder.rs=1", "crates/sqlbuild-python/src/bindings/_helpers/model_config/model_validator.rs=1", "crates/sqlbuild-python/src/bindings/_helpers/model_config/template_data.rs=4", - "crates/sqlbuild-python/src/bindings/_helpers/model_config/typed_config.rs=1", + "crates/sqlbuild-python/src/bindings/_helpers/model_config/typed_config.rs=2", "crates/sqlbuild-python/src/bindings/_helpers/model_config/unicode_environment.rs=4", "crates/sqlbuild-python/src/bindings/_helpers/native_threads/exit_signal.rs=4", "crates/sqlbuild-python/src/bindings/_helpers/output_capture/context.rs=6", @@ -1970,7 +1970,7 @@ pending_budgets = [ "crates/sqlbuild-python/src/bindings/_helpers/planner_changes/metadata.rs=2", "crates/sqlbuild-python/src/bindings/_helpers/planner_changes/renames.rs=4", "crates/sqlbuild-python/src/bindings/_helpers/planner_changes/results.rs=1", - "crates/sqlbuild-python/src/bindings/_helpers/planner_entries/clone_plans.rs=6", + "crates/sqlbuild-python/src/bindings/_helpers/planner_entries/clone_plans.rs=5", "crates/sqlbuild-python/src/bindings/_helpers/planner_entries/command_plans.rs=4", "crates/sqlbuild-python/src/bindings/_helpers/planner_entries/execution_plan.rs=1", "crates/sqlbuild-python/src/bindings/_helpers/planner_entries/model_entries.rs=2", @@ -1983,7 +1983,7 @@ pending_budgets = [ "crates/sqlbuild-python/src/bindings/_helpers/planner_identity/hashing.rs=1", "crates/sqlbuild-python/src/bindings/_helpers/planner_identity/identities.rs=4", "crates/sqlbuild-python/src/bindings/_helpers/planner_identity/json_values.rs=4", - "crates/sqlbuild-python/src/bindings/_helpers/planner_identity/metadata.rs=7", + "crates/sqlbuild-python/src/bindings/_helpers/planner_identity/metadata.rs=6", "crates/sqlbuild-python/src/bindings/_helpers/planner_identity/staleness.rs=2", "crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/column_candidates.rs=4", "crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/column_entries.rs=3", @@ -1992,7 +1992,6 @@ pending_budgets = [ "crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/column_planning.rs=1", "crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/column_warnings.rs=1", "crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/columns.rs=1", - "crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/compatibility.rs=1", "crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/discovery.rs=1", "crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/discovery_events.rs=6", "crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/discovery_models.rs=2", @@ -2013,12 +2012,11 @@ pending_budgets = [ "crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/planning_origin.rs=6", "crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/projections.rs=1", "crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/state_inspection.rs=4", - "crates/sqlbuild-python/src/bindings/_helpers/planner_phases/full_refresh.rs=1", "crates/sqlbuild-python/src/bindings/_helpers/planner_phases/identities.rs=2", "crates/sqlbuild-python/src/bindings/_helpers/planner_phases/models.rs=4", "crates/sqlbuild-python/src/bindings/_helpers/planner_phases/objects.rs=2", "crates/sqlbuild-python/src/bindings/_helpers/planner_phases/output_assembly.rs=3", - "crates/sqlbuild-python/src/bindings/_helpers/planner_phases/retention.rs=3", + "crates/sqlbuild-python/src/bindings/_helpers/planner_phases/retention.rs=2", "crates/sqlbuild-python/src/bindings/_helpers/planner_phases/warehouse_state.rs=3", "crates/sqlbuild-python/src/bindings/_helpers/planner_records/constants.rs=2", "crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_bounds.rs=2", @@ -2028,7 +2026,7 @@ pending_budgets = [ "crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_ranges.rs=6", "crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_safety.rs=3", "crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_windows.rs=1", - "crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/orchestration.rs=2", + "crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/orchestration.rs=1", "crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/sources.rs=4", "crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/values.rs=1", "crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/artifacts.rs=3", @@ -2039,12 +2037,11 @@ pending_budgets = [ "crates/sqlbuild-python/src/bindings/_helpers/planner_scope/scope.rs=3", "crates/sqlbuild-python/src/bindings/_helpers/planner_scope/sources.rs=2", "crates/sqlbuild-python/src/bindings/_helpers/planner_selection/carrier.rs=9", - "crates/sqlbuild-python/src/bindings/_helpers/planner_selection/model_selectors.rs=4", + "crates/sqlbuild-python/src/bindings/_helpers/planner_selection/model_selectors.rs=3", "crates/sqlbuild-python/src/bindings/_helpers/planner_selection/unit_tests.rs=1", "crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/audit_entries.rs=7", "crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/audits.rs=8", "crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/cursor_types.rs=2", - "crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/fixture_metadata.rs=1", "crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/fixture_preparation.rs=2", "crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/fixture_reads.rs=4", "crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/fixture_sql.rs=3", @@ -2052,12 +2049,12 @@ pending_budgets = [ "crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/microbatch_limits.rs=1", "crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/model_entries.rs=3", "crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/models.rs=1", - "crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/values.rs=2", + "crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/values.rs=1", "crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/constants.rs=1", "crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/cursors.rs=14", "crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/fingerprint_cache.rs=15", - "crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/semantic_sources.rs=4", - "crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/snapshot.rs=4", + "crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/semantic_sources.rs=3", + "crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/snapshot.rs=3", "crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/source_columns.rs=1", "crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/source_deferral.rs=1", "crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/source_freshness.rs=6",