diff --git a/crates/sqlbuild-python/src/bindings/_helpers/analysis/binding_catalog.rs b/crates/sqlbuild-python/src/bindings/_helpers/analysis/binding_catalog.rs index 4402d6e877..5345f27c3c 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/analysis/binding_catalog.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/analysis/binding_catalog.rs @@ -11,9 +11,9 @@ use sqlbuild_analysis::semantic_validation::types::{BindingRequest, DiagnosticRo use crate::bindings::_helpers::analysis::conversions; use crate::bindings::_helpers::boundary::panics::compiler_error; +use crate::bindings::_helpers::records::construct::new_record; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use crate::bindings::models::{CatalogInput, ProjectCatalog}; use crate::bindings::types::CompilerDetach; @@ -323,9 +323,7 @@ pub(crate) fn schema_validations<'py>( None => groups.push((catalog, own, vec![index])), } } - let result_type: Bound<'py, PyAny> = registry::class(py, RecordId::SqlBindingResult)?; - let diagnostic_type: Bound<'py, PyAny> = registry::class(py, RecordId::SqlBindingDiagnostic)?; - let empty: Bound<'py, PyAny> = result_type.call0()?; + let empty: Bound<'py, PyAny> = new_record(py, RecordId::SqlBindingResult, (), None)?; let mut results: Vec> = vec![empty; requests.len()]; for (catalog, any_explicit, members) in groups { let catalog = catalog.borrow(py); @@ -351,11 +349,11 @@ pub(crate) fn schema_validations<'py>( for (index, diagnostics) in members.into_iter().zip(rows) { let built: Vec> = diagnostics .into_iter() - .map(|row| diagnostic_type.call1(row)) + .map(|row| new_record(py, RecordId::SqlBindingDiagnostic, row, None)) .collect::>()?; let keywords: Bound<'py, PyDict> = PyDict::new(py); keywords.set_item("diagnostics", PyTuple::new(py, built)?)?; - results[index] = result_type.call((), Some(&keywords))?; + results[index] = new_record(py, RecordId::SqlBindingResult, (), Some(&keywords))?; } } PyTuple::new(py, results) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/analysis/binding_locations.rs b/crates/sqlbuild-python/src/bindings/_helpers/analysis/binding_locations.rs index 63c25a40e3..4ac680f26c 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/analysis/binding_locations.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/analysis/binding_locations.rs @@ -19,7 +19,7 @@ type LocatedRow = ( /// Each reported diagnostic of `rows` through `(authored, query, cleaned)` and the expansion. #[pyfunction] -fn locate_model_binding_diagnostics( +pub(crate) fn locate_model_binding_diagnostics( rows: Vec, texts: (String, String, String), expansion: Option<(String, Vec>)>, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/analysis/explicit_references.rs b/crates/sqlbuild-python/src/bindings/_helpers/analysis/explicit_references.rs index 9b40f131af..c3027e71e0 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/analysis/explicit_references.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/analysis/explicit_references.rs @@ -26,10 +26,12 @@ use sqlbuild_pysource::literal_sql::models::FunctionSource; use crate::bindings::_helpers::analysis::reference_facts::{ProjectFacts, project_facts}; use crate::bindings::_helpers::boundary::panics::compiler_error; use crate::bindings::_helpers::compile_driver::python_calls::imported; +use crate::bindings::_helpers::graph::execution_gates::model_python_hook_names; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::native_host::user_code::UserCodeHost; +use crate::bindings::_helpers::records::construct::{enum_from_value, new_record, record_class}; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr, RecordId}; +use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; use crate::bindings::types::CompilerDetach; @@ -157,7 +159,7 @@ fn python_sql_owners<'py>( let py: Python<'py> = project.py(); let mut owners: Vec> = Vec::new(); for (label, attribute) in NODE_OWNERS { - for node in discovered_inputs.getattr(attribute)?.try_iter()? { + for node in native_field(discovered_inputs, attribute)?.try_iter()? { let node: Bound<'py, PyAny> = node?; let name: String = native_field(&node, "name")?.extract()?; owners.push(PythonSqlOwner::new( @@ -168,14 +170,11 @@ fn python_sql_owners<'py>( } } owners.extend(loader_owners(project, discovered_inputs)?); - let hook_names = registry::native(py, NativeAttr::ModelPythonHookNames)?; let mut attached: BTreeMap> = BTreeMap::new(); for model in native_field(project, "models")?.try_iter()? { let model: Bound<'py, PyAny> = model?; - let keywords: Bound<'py, PyDict> = PyDict::new(py); - keywords.set_item("model", &model)?; let model_name: String = native_field(&model, "name")?.extract()?; - for hook_name in hook_names.call((), Some(&keywords))?.try_iter()? { + for hook_name in model_python_hook_names(py, &model)?.try_iter()? { attached .entry(hook_name?.extract()?) .or_default() @@ -309,11 +308,8 @@ fn python_report<'py>( report: &ReferenceReport, owners: &[PythonSqlOwner<'py>], ) -> PyResult> { - let owner_kind: Bound<'py, PyAny> = registry::class(py, RecordId::HardCodedRelationOwnerKind)?; let reference_models = py.import(REFERENCE_MODELS_MODULE)?; - let source_location: Bound<'py, PyAny> = registry::class(py, RecordId::SourceLocation)?; let path_class: Bound<'py, PyAny> = py.import("pathlib")?.getattr("Path")?; - let compiler_diagnostic: Bound<'py, PyAny> = registry::class(py, RecordId::CompilerDiagnostic)?; let mut diagnostics: Vec> = Vec::with_capacity(report.diagnostics.len()); for row in &report.diagnostics { let fields: Bound<'py, PyDict> = PyDict::new(py); @@ -333,15 +329,23 @@ fn python_report<'py>( location.set_item("path", &owners[owner].relative_path)?; location.set_item("line", row.line)?; location.set_item("column", 1)?; - fields.set_item("location", source_location.call((), Some(&location))?)?; + fields.set_item( + "location", + new_record(py, RecordId::SourceLocation, (), Some(&location))?, + )?; } None => fields.set_item("location", py.None())?, } fields.set_item("help", &row.help)?; - diagnostics.push(compiler_diagnostic.call((), Some(&fields))?); + diagnostics.push(new_record( + py, + RecordId::CompilerDiagnostic, + (), + Some(&fields), + )?); } let literal_relation: Bound<'py, PyAny> = - registry::class(reference_models.py(), RecordId::LiteralSqlRelation)?; + record_class(reference_models.py(), RecordId::LiteralSqlRelation)?; let relation_name: Bound<'py, PyAny> = native_field(&reference_models, "RelationName")?; let mut unmatched: Vec> = Vec::with_capacity(report.unmatched.len()); for row in &report.unmatched { @@ -352,7 +356,10 @@ fn python_report<'py>( relation.set_item("database", &row.relation.database)?; let fields: Bound<'py, PyDict> = PyDict::new(py); fields.set_item("owner_label", &owner.label)?; - fields.set_item("owner_kind", owner_kind.call1((owner.kind,))?)?; + fields.set_item( + "owner_kind", + enum_from_value(py, RecordId::HardCodedRelationOwnerKind, owner.kind)?, + )?; fields.set_item("relative_path", &owner.relative_path)?; fields.set_item("line", row.line)?; fields.set_item("method", &row.method)?; @@ -362,7 +369,7 @@ fn python_report<'py>( let fields: Bound<'py, PyDict> = PyDict::new(py); fields.set_item("diagnostics", PyTuple::new(py, diagnostics)?)?; fields.set_item("unmatched", PyTuple::new(py, unmatched)?)?; - registry::class(py, RecordId::PythonSqlReferenceReport)?.call((), Some(&fields)) + new_record(py, RecordId::PythonSqlReferenceReport, (), Some(&fields)) } /// `(method, sql, line)` for each literal SQL call to the context in a dedented function source. diff --git a/crates/sqlbuild-python/src/bindings/_helpers/analysis/normalization.rs b/crates/sqlbuild-python/src/bindings/_helpers/analysis/normalization.rs index d5f3dda1be..396c065d76 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/analysis/normalization.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/analysis/normalization.rs @@ -8,7 +8,7 @@ use crate::bindings::_helpers::boundary::panics::value_error; use crate::bindings::types::CompilerDetach; #[pyfunction] -fn normalize_analysis_sql( +pub(crate) fn normalize_analysis_sql( py: Python<'_>, request: crate::bindings::models::NormalizationInput, ) -> PyResult { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/analysis/positions.rs b/crates/sqlbuild-python/src/bindings/_helpers/analysis/positions.rs index 40b440c5bb..740a7b2e4e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/analysis/positions.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/analysis/positions.rs @@ -9,7 +9,7 @@ use sqlbuild_analysis::semantic_validation::models as validation; #[pymethods] impl BindingPositions { #[new] - fn new(request: PositionInput) -> PyResult { + pub(crate) fn new(request: PositionInput) -> PyResult { compiler_guard(|| { validation::BindingPositions::new(request.into()) .map(|inner| Self { inner }) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/analysis/reference_facts.rs b/crates/sqlbuild-python/src/bindings/_helpers/analysis/reference_facts.rs index be5d4b5aed..dbdc3b4589 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/analysis/reference_facts.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/analysis/reference_facts.rs @@ -1,8 +1,8 @@ //! The explicit-reference facts of a compiled project, read from its Python views. +use crate::bindings::_helpers::records::construct::record_class; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; use pyo3::types::{PyBool, PyList, PyTuple}; use sqlbuild_analysis::explicit_references::models::{ @@ -108,8 +108,8 @@ struct HookEntries<'py> { impl<'py> HookEntries<'py> { fn imported(py: Python<'py>) -> PyResult { Ok(Self { - python: registry::class(py, RecordId::PythonHookEntry)?, - sql: registry::class(py, RecordId::SqlHookEntry)?, + python: record_class(py, RecordId::PythonHookEntry)?, + sql: record_class(py, RecordId::SqlHookEntry)?, }) } } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/analysis/required_columns.rs b/crates/sqlbuild-python/src/bindings/_helpers/analysis/required_columns.rs index ca168965cf..4afeef9f3c 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/analysis/required_columns.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/analysis/required_columns.rs @@ -9,10 +9,9 @@ use sqlbuild_analysis::required_columns::main::resolve_required_columns::resolve use sqlbuild_analysis::required_columns::models::{ColumnRead, ReferenceInput}; use crate::bindings::_helpers::boundary::panics::value_error; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; +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; -use crate::bindings::_helpers::records::registry; use crate::bindings::types::CompilerDetach; /// CTE-aware column reads of `query_sql`, resolved to the external resources it references. @@ -41,11 +40,11 @@ fn analyze_resolved_column_reads<'py>( } RequiredColumnsError::Normalization(message) => value_error(message), })?; - let fact: Bound<'py, PyAny> = registry::class(py, RecordId::CompiledLineageSourceFact)?; let mut facts: Vec> = Vec::with_capacity(reads.len()); for read in reads { - facts.push(call_with( - &fact, + facts.push(new_record_with( + py, + RecordId::CompiledLineageSourceFact, &[ ( "resource_type", diff --git a/crates/sqlbuild-python/src/bindings/_helpers/analysis/schema_request.rs b/crates/sqlbuild-python/src/bindings/_helpers/analysis/schema_request.rs index 13d49e2f2b..b2fc9f5f25 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/analysis/schema_request.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/analysis/schema_request.rs @@ -8,8 +8,8 @@ use sqlbuild_analysis::semantic_validation::main::normalize::normalize_analysis_ use sqlbuild_analysis::semantic_validation::models::NormalizationInput; use crate::bindings::_helpers::boundary::panics::value_error; +use crate::bindings::_helpers::records::construct::new_record; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use crate::bindings::types::CompilerDetach; const GENERIC_DIALECT: &str = "generic"; @@ -61,5 +61,10 @@ pub(crate) fn schema_validation_request<'py>( parts.quoted_identifiers_ignore_case, )?; arguments.set_item("catalog", parts.catalog)?; - registry::class(py, RecordId::SqlSchemaValidationRequest)?.call((), Some(&arguments)) + new_record( + py, + RecordId::SqlSchemaValidationRequest, + (), + Some(&arguments), + ) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/analysis_session/fixture_columns.rs b/crates/sqlbuild-python/src/bindings/_helpers/analysis_session/fixture_columns.rs index b8d14bb4b6..43554846ca 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/analysis_session/fixture_columns.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/analysis_session/fixture_columns.rs @@ -2,7 +2,8 @@ use std::collections::HashMap; -use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; +use pyo3::prelude::{Bound, Py, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; +use pyo3::sync::PyOnceLock; use pyo3::types::{PyDict, PyDictMethods, PyFrozenSet, PyTuple}; use pyo3::{pyfunction, wrap_pyfunction}; use sqlbuild_analysis::assembly::analysis_session::main::projection_facts::projection_facts; @@ -10,34 +11,38 @@ use sqlbuild_analysis::assembly::analysis_session::models::ProjectionFacts; use sqlbuild_core::panics::main::native_failure::native_failure; use sqlbuild_core::text::main::python_casefold::python_casefold; +use crate::bindings::_helpers::analysis_session::models::FixtureInferenceCount; use crate::bindings::_helpers::analysis_session::query_columns::ColumnsRow; use crate::bindings::_helpers::analysis_session::stage_functions::query_column_rows; use crate::bindings::_helpers::boundary::keyword_request::Request; use crate::bindings::_helpers::boundary::panics::compiler_error; +use crate::bindings::_helpers::records::construct::{enum_from_value, new_record}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use crate::bindings::types::CompilerDetach; const PROJECTION_FACTS_CONTEXT: &str = "native fixture projection facts"; const LEGACY_MODE: &str = "legacy"; +/// Fixture column inferences computed in this process. +static INFERENCES: PyOnceLock> = PyOnceLock::new(); + /// `infer_fixture_column_facts(*, query_sql, inference_profile)`: a `FixtureColumnInference`. #[pyfunction] #[pyo3(signature = (**request))] -fn infer_fixture_column_facts<'py>( +pub(crate) fn infer_fixture_column_facts<'py>( request: Option<&Bound<'py, PyDict>>, ) -> PyResult> { let request = Request::new("infer_fixture_column_facts", request)?; let py: Python<'py> = request.py(); + inference_count(py)?.borrow_mut().computed += 1; let query_sql: String = request.get("query_sql")?.extract()?; let profile: Bound<'py, PyAny> = request.get("inference_profile")?; let query: Bound<'py, PyDict> = PyDict::new(py); query.set_item("sql", &query_sql)?; query.set_item("mode", LEGACY_MODE)?; query.set_item("recover_cte_facts", false)?; - let query: Bound<'py, PyAny> = - registry::class(py, RecordId::NativeColumnQuery)?.call((), Some(&query))?; + let query: Bound<'py, PyAny> = new_record(py, RecordId::NativeColumnQuery, (), Some(&query))?; let empty: Bound<'py, PyAny> = PyDict::new(py).into_any(); let rows: Vec = query_column_rows( PyTuple::new(py, [query])?.as_any(), @@ -52,8 +57,6 @@ fn infer_fixture_column_facts<'py>( .compiler_detach(|| projection_facts(&query_sql, dialect.as_deref())) .map_err(|reason| compiler_error(native_failure(PROJECTION_FACTS_CONTEXT, &reason)))?; let simple_types: HashMap = facts.types.into_iter().collect(); - let column_class: Bound<'py, PyAny> = registry::class(py, RecordId::InferredColumn)?; - let nullability_class: Bound<'py, PyAny> = registry::class(py, RecordId::InferredNullability)?; let mut built: Vec> = Vec::with_capacity(columns.len()); for (name, data_type, nullability) in columns { let data_type: Option = data_type @@ -62,8 +65,16 @@ fn infer_fixture_column_facts<'py>( let arguments: Bound<'py, PyDict> = PyDict::new(py); arguments.set_item("name", name)?; arguments.set_item("type", data_type)?; - arguments.set_item("nullability", nullability_class.call1((nullability,))?)?; - built.push(column_class.call((), Some(&arguments))?); + arguments.set_item( + "nullability", + enum_from_value(py, RecordId::InferredNullability, nullability)?, + )?; + built.push(new_record( + py, + RecordId::InferredColumn, + (), + Some(&arguments), + )?); } let arguments: Bound<'py, PyDict> = PyDict::new(py); arguments.set_item("columns", PyTuple::new(py, built)?)?; @@ -72,9 +83,30 @@ fn infer_fixture_column_facts<'py>( PyFrozenSet::new(py, facts.null_literal_names)?, )?; arguments.set_item("quoted_names", PyFrozenSet::new(py, facts.quoted_names)?)?; - registry::class(py, RecordId::FixtureColumnInference)?.call((), Some(&arguments)) + new_record(py, RecordId::FixtureColumnInference, (), Some(&arguments)) +} + +/// Fixture column inferences computed in this process, optionally clearing the count first. +#[pyfunction] +#[pyo3(signature = (*, reset = false))] +fn fixture_inference_count(py: Python<'_>, reset: bool) -> PyResult { + let count = inference_count(py)?; + let mut count = count.borrow_mut(); + if reset { + count.computed = 0; + } + Ok(count.computed) +} + +/// The extension-wide fixture inference counter. +fn inference_count(py: Python<'_>) -> PyResult> { + Ok(INFERENCES + .get_or_try_init(py, || Py::new(py, FixtureInferenceCount::default()))? + .bind(py) + .clone()) } pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> { - module.add_function(wrap_pyfunction!(infer_fixture_column_facts, module)?) + module.add_function(wrap_pyfunction!(infer_fixture_column_facts, module)?)?; + module.add_function(wrap_pyfunction!(fixture_inference_count, module)?) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/analysis_session/fixture_inferences.rs b/crates/sqlbuild-python/src/bindings/_helpers/analysis_session/fixture_inferences.rs index 1af733ffbd..706bc92199 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/analysis_session/fixture_inferences.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/analysis_session/fixture_inferences.rs @@ -1,9 +1,8 @@ //! Fixture column inference computed once per fixture body and inference profile. -use crate::bindings::_helpers::compile_driver::python_calls::call_with; +use crate::bindings::_helpers::analysis_session::fixture_columns::infer_fixture_column_facts; +use crate::bindings::_helpers::compile_driver::python_calls::keywords; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::NativeAttr; -use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, Py, PyAny, PyAnyMethods, PyResult, Python}; use pyo3::types::{PyDict, PyDictMethods}; use pyo3::{pyclass, pymethods}; @@ -33,13 +32,13 @@ impl FixtureColumnInferences { ) -> PyResult> { let py = query_sql.py(); let compute = || { - call_with( - ®istry::native(py, NativeAttr::InferFixtureColumnFacts)?, + infer_fixture_column_facts(Some(&keywords( + py, &[ ("query_sql", query_sql.clone()), ("inference_profile", inference_profile.clone()), ], - ) + )?)) }; if !native_field(inference_profile, "binding_catalog")?.is_none() { return compute(); diff --git a/crates/sqlbuild-python/src/bindings/_helpers/analysis_session/mod.rs b/crates/sqlbuild-python/src/bindings/_helpers/analysis_session/mod.rs index 5773febd9c..64da3a80e1 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/analysis_session/mod.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/analysis_session/mod.rs @@ -1,6 +1,7 @@ pub(crate) mod fixture_columns; pub(crate) mod fixture_inferences; pub(crate) mod model_analyses; +pub(crate) mod models; pub(crate) mod nullability_rules; pub(crate) mod oracle; pub(crate) mod outcome_objects; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/analysis_session/model_analyses.rs b/crates/sqlbuild-python/src/bindings/_helpers/analysis_session/model_analyses.rs index d305219977..ce409db471 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/analysis_session/model_analyses.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/analysis_session/model_analyses.rs @@ -22,9 +22,9 @@ use crate::bindings::_helpers::compiled_project::model_input_view::{ }; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; +use crate::bindings::_helpers::records::construct::enum_member; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::semantic_shapes::shapes::{VALID_FROM_DEFAULT, VALID_TO_DEFAULT}; const LOGGING_MODULE: &str = "logging"; @@ -152,8 +152,7 @@ fn run_session<'py>( PyString::new(py, &failure).as_any(), )?; } - let unknown: Bound<'py, PyAny> = - registry::class(py, RecordId::InferredNullability)?.getattr("UNKNOWN")?; + let unknown: Bound<'py, PyAny> = enum_member(py, RecordId::InferredNullability, "UNKNOWN")?; for (name, shape) in publications { let published: Bound<'py, PyDict> = shape.into_py_dict(py)?; types.call_method1("setdefault", (&name, &published))?; @@ -256,10 +255,8 @@ fn model_rows<'py>( py: Python<'py>, model_inputs: &[Bound<'py, PyAny>], ) -> PyResult> { - let enforced: Bound<'py, PyAny> = - registry::class(py, RecordId::ContractPolicy)?.getattr("ENFORCED")?; - let snapshot: Bound<'py, PyAny> = - registry::class(py, RecordId::MaterializationType)?.getattr("SNAPSHOT")?; + let enforced: Bound<'py, PyAny> = enum_member(py, RecordId::ContractPolicy, "ENFORCED")?; + let snapshot: Bound<'py, PyAny> = enum_member(py, RecordId::MaterializationType, "SNAPSHOT")?; let mut rows: Vec> = Vec::with_capacity(model_inputs.len()); for model_input in model_inputs { let values: Bound<'py, PyAny> = native_field(model_input, "config")?.getattr("values")?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/analysis_session/models.rs b/crates/sqlbuild-python/src/bindings/_helpers/analysis_session/models.rs new file mode 100644 index 0000000000..8fd2e82dbf --- /dev/null +++ b/crates/sqlbuild-python/src/bindings/_helpers/analysis_session/models.rs @@ -0,0 +1,10 @@ +//! Native state kept by the analysis-session bindings. + +use pyo3::pyclass; + +/// Fixture column inferences computed since the last reset. +#[pyclass] +#[derive(Debug, Default)] +pub(crate) struct FixtureInferenceCount { + pub(crate) computed: u64, +} diff --git a/crates/sqlbuild-python/src/bindings/_helpers/analysis_session/outcome_objects.rs b/crates/sqlbuild-python/src/bindings/_helpers/analysis_session/outcome_objects.rs index a93bbd7512..02ae2be7b0 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/analysis_session/outcome_objects.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/analysis_session/outcome_objects.rs @@ -9,8 +9,8 @@ use crate::bindings::_helpers::boundary::keyword_request::Request; use crate::bindings::_helpers::record_behaviours::lineage_facts::{ confidence_member, transform_kind_member, }; +use crate::bindings::_helpers::records::construct::{enum_from_value, new_record, record_class}; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; const LINEAGE_NATIVE: &str = "native"; @@ -40,7 +40,7 @@ type ProofObjects<'py> = ( /// The outcome at `position` of a finished session as `ModelSqlAnalysis`, for hand-built inputs. #[pyfunction] #[pyo3(signature = (**request))] -fn session_outcome_analysis<'py>( +pub(crate) fn session_outcome_analysis<'py>( request: Option<&Bound<'py, PyDict>>, ) -> PyResult> { let request = Request::new("session_outcome_analysis", request)?; @@ -57,15 +57,14 @@ fn session_outcome_analysis<'py>( } else { PyTuple::empty(py).into_any() }; - let binding_class: Bound<'py, PyAny> = registry::class(py, RecordId::SqlBindingDiagnostic)?; let mut bindings: Vec> = Vec::new(); for row in raw.try_iter()? { let row: Bound<'py, PyTuple> = PyTuple::new(py, row?.try_iter()?.collect::>>()?)?; - bindings.push(binding_class.call1(row)?); + bindings.push(new_record(py, RecordId::SqlBindingDiagnostic, row, None)?); } let result: Bound<'py, PyAny> = call( - ®istry::class(py, RecordId::PolyglotAnalysisResult)?, + &record_class(py, RecordId::PolyglotAnalysisResult)?, &[ ("analysis_succeeded", succeeded), ("columns", inferred_columns(&columns)?), @@ -80,7 +79,7 @@ fn session_outcome_analysis<'py>( ], )?; call( - ®istry::class(py, RecordId::ModelSqlAnalysis)?, + &record_class(py, RecordId::ModelSqlAnalysis)?, &[ ("polyglot_analysis", result), ("placeholders", request.get("placeholders")?), @@ -93,7 +92,7 @@ fn session_outcome_analysis<'py>( /// A native dynamic pivot proof row as Python's proof, None when absent. #[pyfunction] -fn dynamic_contract_proof<'py>(row: &Bound<'py, PyAny>) -> PyResult> { +pub(crate) fn dynamic_contract_proof<'py>(row: &Bound<'py, PyAny>) -> PyResult> { proof(row) } @@ -104,7 +103,7 @@ fn proof<'py>(row: &Bound<'py, PyAny>) -> PyResult> { } let (output_proven, columns, families, inputs, failure_reason, bare): ProofObjects<'py> = row.extract()?; - let family_class: Bound<'py, PyAny> = registry::class(py, RecordId::DynamicColumnFamilyProof)?; + let family_class: Bound<'py, PyAny> = record_class(py, RecordId::DynamicColumnFamilyProof)?; let mut proofs: Vec> = Vec::new(); for family in families.try_iter()? { let (name, inferred_type): (Bound<'py, PyAny>, Bound<'py, PyAny>) = family?.extract()?; @@ -120,7 +119,7 @@ fn proof<'py>(row: &Bound<'py, PyAny>) -> PyResult> { PyTuple::empty(py).into_any() }; call( - ®istry::class(py, RecordId::DynamicColumnContractProof)?, + &record_class(py, RecordId::DynamicColumnContractProof)?, &[ ("output_proven", output_proven), ("fixed_columns", fixed), @@ -138,8 +137,7 @@ fn inferred_columns<'py>(rows: &Bound<'py, PyAny>) -> PyResult if rows.is_none() { return Ok(rows.clone()); } - let column_class: Bound<'py, PyAny> = registry::class(py, RecordId::InferredColumn)?; - let nullability_class: Bound<'py, PyAny> = registry::class(py, RecordId::InferredNullability)?; + let column_class: Bound<'py, PyAny> = record_class(py, RecordId::InferredColumn)?; let mut columns: Vec> = Vec::new(); for row in rows.try_iter()? { let (name, data_type, nullability): ( @@ -152,7 +150,10 @@ fn inferred_columns<'py>(rows: &Bound<'py, PyAny>) -> PyResult &[ ("name", name), ("type", data_type), - ("nullability", nullability_class.call1((nullability,))?), + ( + "nullability", + enum_from_value(py, RecordId::InferredNullability, nullability)?, + ), ], )?); } @@ -209,7 +210,7 @@ fn compact_lineage<'py>(rows: &Bound<'py, PyAny>) -> PyResult> } let keys: Bound<'py, PyAny> = PyTuple::new(py, pool.keys())?.into_any(); call( - ®istry::class(py, RecordId::CompactLineageFacts)?, + &record_class(py, RecordId::CompactLineageFacts)?, &[ ("string_pool", keys), ("rows", PyTuple::new(py, compact)?.into_any()), @@ -220,8 +221,8 @@ fn compact_lineage<'py>(rows: &Bound<'py, PyAny>) -> PyResult> /// Native lineage rows as the plain facts Python's re-analysis returns. fn lineage_facts<'py>(rows: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = rows.py(); - let column_class: Bound<'py, PyAny> = registry::class(py, RecordId::CompiledLineageColumnFact)?; - let source_class: Bound<'py, PyAny> = registry::class(py, RecordId::CompiledLineageSourceFact)?; + let column_class: Bound<'py, PyAny> = record_class(py, RecordId::CompiledLineageColumnFact)?; + let source_class: Bound<'py, PyAny> = record_class(py, RecordId::CompiledLineageSourceFact)?; let mut facts: Vec> = Vec::new(); for row in rows.try_iter()? { let (output_column, transform_code, confidence_code, sources): ( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/analysis_session/profile_inputs.rs b/crates/sqlbuild-python/src/bindings/_helpers/analysis_session/profile_inputs.rs index f93ebe17f3..1cf052871d 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/analysis_session/profile_inputs.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/analysis_session/profile_inputs.rs @@ -9,9 +9,9 @@ use crate::bindings::_helpers::analysis::binding_catalog::{ }; use crate::bindings::_helpers::native_host::adapter::AdapterHost; use crate::bindings::_helpers::native_host::host; +use crate::bindings::_helpers::records::construct::record_class; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; const GENERIC_DIALECT: &str = "generic"; const ADAPTER_RULE_ID: &str = "python"; @@ -55,7 +55,7 @@ pub(crate) fn nullability_rules(profile: &Bound<'_, PyAny>) -> PyResult<(Pairs, if adapter.is_empty() { return Ok((rows, None)); } - let nullability: Bound<'_, PyAny> = registry::class(py, RecordId::InferredNullability)?; + let nullability: Bound<'_, PyAny> = record_class(py, RecordId::InferredNullability)?; Ok((rows, Some((adapter.unbind(), nullability.unbind())))) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/analysis_session/stage_functions.rs b/crates/sqlbuild-python/src/bindings/_helpers/analysis_session/stage_functions.rs index 0239b771e3..4ded9cda1a 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/analysis_session/stage_functions.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/analysis_session/stage_functions.rs @@ -23,9 +23,9 @@ use crate::bindings::_helpers::boundary::keyword_request::Request; use crate::bindings::_helpers::compile_driver::stage_work::{ EXPRESSION_SHAPE_BATCHES, EXPRESSION_SHAPES_INFERRED, count_stage, }; +use crate::bindings::_helpers::records::construct::{enum_from_value, new_record}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use crate::bindings::models::ProjectCatalog; const BATCH_MODE: &str = "batch"; @@ -59,7 +59,7 @@ fn infer_native_query_column_rows<'py>( /// Each query's columns as `NativeQueryColumns`; a native internal failure raises. #[pyfunction] #[pyo3(signature = (**request))] -fn infer_native_query_columns<'py>( +pub(crate) fn infer_native_query_columns<'py>( request: Option<&Bound<'py, PyDict>>, ) -> PyResult> { let request = Request::new("infer_native_query_columns", request)?; @@ -73,9 +73,6 @@ fn infer_native_query_columns<'py>( &request.get("profile")?, (&types, &nullability), )?; - let query_columns: Bound<'py, PyAny> = registry::class(py, RecordId::NativeQueryColumns)?; - let column_class: Bound<'py, PyAny> = registry::class(py, RecordId::InferredColumn)?; - let nullability_class: Bound<'py, PyAny> = registry::class(py, RecordId::InferredNullability)?; let mut analyses: Vec> = Vec::with_capacity(rows.len()); for (succeeded, columns, has_star) in rows { let columns: Bound<'py, PyAny> = match columns { @@ -86,8 +83,16 @@ fn infer_native_query_columns<'py>( let arguments: Bound<'py, PyDict> = PyDict::new(py); arguments.set_item("name", name)?; arguments.set_item("type", data_type)?; - arguments.set_item("nullability", nullability_class.call1((nullability,))?)?; - built.push(column_class.call((), Some(&arguments))?); + arguments.set_item( + "nullability", + enum_from_value(py, RecordId::InferredNullability, nullability)?, + )?; + built.push(new_record( + py, + RecordId::InferredColumn, + (), + Some(&arguments), + )?); } PyTuple::new(py, built)?.into_any() } @@ -96,7 +101,12 @@ fn infer_native_query_columns<'py>( arguments.set_item("succeeded", succeeded)?; arguments.set_item("columns", columns)?; arguments.set_item("has_star", has_star)?; - analyses.push(query_columns.call((), Some(&arguments))?); + analyses.push(new_record( + py, + RecordId::NativeQueryColumns, + (), + Some(&arguments), + )?); } PyTuple::new(py, analyses) } @@ -104,7 +114,7 @@ fn infer_native_query_columns<'py>( /// One shape per expression, cached on the profile's binding catalog; a failure raises. #[pyfunction] #[pyo3(signature = (**request))] -fn infer_native_expression_source_shapes<'py>( +pub(crate) fn infer_native_expression_source_shapes<'py>( request: Option<&Bound<'py, PyDict>>, ) -> PyResult> { let request = Request::new("infer_native_expression_source_shapes", request)?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/audit_command/command.rs b/crates/sqlbuild-python/src/bindings/_helpers/audit_command/command.rs index e0550e59f0..a5278bae9f 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/audit_command/command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/audit_command/command.rs @@ -11,13 +11,25 @@ use sqlbuild_core::audit_command::main::render_audit_summary::render_audit_summa use sqlbuild_core::audit_command::models::AuditCounts; use sqlbuild_core::cli_text::models::CliStyle; +use crate::bindings::_helpers::build_progress::audit_progress::AuditProgressReporter; +use crate::bindings::_helpers::cli::progress::current_native_progress_projector; +use crate::bindings::_helpers::cli::progress_reporters::ConnectionProgressReporter; +use crate::bindings::_helpers::cli::progress_text::write_execution_header; use crate::bindings::_helpers::command_runtime::invocation::resolve_reported_command_invocation; -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, within}; +use crate::bindings::_helpers::compile_driver::pipeline_entries::build_static_pipeline_result; +use crate::bindings::_helpers::compile_driver::python_calls::{call_with, keywords, within}; +use crate::bindings::_helpers::compile_driver::selection_phases::compile_static_command_context; +use crate::bindings::_helpers::cost_telemetry::context::CostContext; +use crate::bindings::_helpers::dbt_interop::reference_resolver::resolve_external_sql_reference_resolver; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; +use crate::bindings::_helpers::planner_entries::command_plans::build_audit_command_plan; +use crate::bindings::_helpers::records::construct::{new_record_with, record_class}; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::ids::RecordId; +use crate::bindings::_helpers::result_output::documents::format_audit_execution_json; +use crate::bindings::_helpers::result_output::documents::write_execution_json_output; +use crate::bindings::_helpers::result_output::event_writer::ExecutionEventWriter; const EXECUTION_COMMAND: &str = "sqb audit"; const PASS_OUTCOME: &str = "pass"; @@ -104,18 +116,13 @@ fn compile_audits<'py>( let py: Python<'py> = request.py(); let discovered: Bound<'py, PyAny> = native_field(invocation, "discovered_inputs")?; let adapter: Bound<'py, PyAny> = native_field(invocation, "adapter")?; - let resolver: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ResolveExternalSqlReferenceResolver)?, - &[ - ( - "project_dir", - native_field(invocation, "effective_project_dir")?, - ), - ("discovered_inputs", discovered.clone()), - ], + let resolver: Bound<'py, PyAny> = resolve_external_sql_reference_resolver( + native_field(invocation, "effective_project_dir")?, + discovered.clone(), )?; - let options: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::CompilePipelineOptions)?, + let options: Bound<'py, PyAny> = new_record_with( + py, + RecordId::CompilePipelineOptions, &[ ("selected_target", native_field(request, "selected_target")?), ( @@ -133,8 +140,8 @@ fn compile_audits<'py>( ("external_sql_reference_resolver", resolver), ], )?; - let context: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::CompileStaticCommandContext)?, + let context: Bound<'py, PyAny> = compile_static_command_context(Some(&keywords( + py, &[ ("discovered_inputs", discovered), ("adapter", adapter.clone()), @@ -146,19 +153,19 @@ fn compile_audits<'py>( .getattr("on_progress")?, ), ], + )?))?; + let plan_output: Bound<'py, PyAny> = build_audit_command_plan( + &native_field(&context, "project")?, + &adapter, + &native_field(&context, "scope")?, + &native_field(&context, "relations")?, )?; - let plan_output: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::BuildAuditCommandPlan)?, - &[ - ("project", native_field(&context, "project")?), - ("adapter", adapter), - ("scope", native_field(&context, "scope")?), - ("relations", native_field(&context, "relations")?), - ], - )?; - call_with( - ®istry::native(py, NativeAttr::BuildStaticPipelineResult)?, - &[("context", context), ("plan_output", plan_output)], + build_static_pipeline_result( + py, + Some(keywords( + py, + &[("context", context), ("plan_output", plan_output)], + )?), ) } @@ -193,17 +200,21 @@ fn prepare_run<'py>( let worker_count: i64 = concurrency.min(i64::try_from(audit_count).unwrap_or(i64::MAX)); let stream: Bound<'py, PyAny> = native_field(invocation, "progress_stream")?; let use_color: Bound<'py, PyAny> = native_field(invocation, "use_color")?; - let progress: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::AuditProgressReporter)?, - &[ - ("entries", entries.clone()), - ("worker_limit", PyInt::new(py, worker_count).into_any()), - ("stream", stream.clone()), - ("use_color", use_color.clone()), - ], - )?; - let projector: Bound<'py, PyAny> = - registry::native(py, NativeAttr::CurrentNativeProgressProjector)?.call0()?; + let progress: Bound<'py, PyAny> = AuditProgressReporter::new( + py, + Some(keywords( + py, + &[ + ("entries", entries.clone()), + ("worker_limit", PyInt::new(py, worker_count).into_any()), + ("stream", stream.clone()), + ("use_color", use_color.clone()), + ], + )?), + ) + .and_then(|value| pyo3::Bound::new(py, value)) + .map(pyo3::Bound::into_any)?; + let projector: Bound<'py, PyAny> = current_native_progress_projector(py)?; if !projector.is_none() { for entry in entries.try_iter()? { call_with( @@ -212,14 +223,19 @@ fn prepare_run<'py>( )?; } } - let connection: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ConnectionProgressReporter)?, - &[ - ("adapter_name", native_field(invocation, "adapter_name")?), - ("stream", stream), - ("use_color", use_color), - ], - )?; + let connection: Bound<'py, PyAny> = ConnectionProgressReporter::new( + py, + Some(keywords( + py, + &[ + ("adapter_name", native_field(invocation, "adapter_name")?), + ("stream", stream), + ("use_color", use_color), + ], + )?), + ) + .and_then(|value| pyo3::Bound::new(py, value)) + .map(pyo3::Bound::into_any)?; Ok(AuditRun { progress, connection, @@ -244,7 +260,8 @@ fn write_headers( header.set_item("target", py.None())?; header.set_item("concurrency", concurrency)?; header.set_item("use_color", &use_color)?; - registry::native(py, NativeAttr::WriteExecutionHeader)?.call((), Some(&header))?; + write_execution_header(py, Some(header.clone())) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; let entries: Bound<'_, PyAny> = native_field(pipeline, "plan_output")?.getattr("audit_entries")?; let mut models: BTreeSet = BTreeSet::new(); @@ -274,8 +291,9 @@ fn execute<'py>( let py: Python<'py> = invocation.py(); let plan: Bound<'py, PyAny> = native_field(pipeline, "plan_output")?; let project: Bound<'py, PyAny> = native_field(pipeline, "project")?; - let event_writer: Bound<'py, PyAny> = - registry::native(py, NativeAttr::ExecutionEventWriter)?.call0()?; + let event_writer: Bound<'py, PyAny> = ExecutionEventWriter::new(py, None) + .and_then(|value| pyo3::Bound::new(py, value)) + .map(pyo3::Bound::into_any)?; let relay: Bound<'py, AuditCallbackRelay> = Bound::new( py, AuditCallbackRelay { @@ -287,8 +305,7 @@ fn execute<'py>( let executed: PyResult>> = (|| { run.progress .call_method1("set_result_callback", (native_field(&relay, "on_result")?,))?; - let projector: Bound<'py, PyAny> = - registry::native(py, NativeAttr::CurrentNativeProgressProjector)?.call0()?; + let projector: Bound<'py, PyAny> = current_native_progress_projector(py)?; if !projector.is_none() { let ordinals: Bound<'py, PyDict> = PyDict::new(py); let mut total: usize = 0; @@ -311,16 +328,21 @@ fn execute<'py>( .call_method1("__truediv__", ("executions",))? .call_method1("__truediv__", (&run_id,))? .call_method1("__truediv__", ("statements.jsonl",))?; - let scope: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::CostContext)?.getattr("scope")?, - &[ - ("run_id", run_id.clone()), - ("resource_type", text(py, "run")), - ("resource_name", text(py, "audit")), - ("ledger_path", ledger_path), - ("phase", text(py, "audit")), - ], - )?; + let scope: Bound<'py, PyAny> = CostContext::scope( + py, + Some(keywords( + py, + &[ + ("run_id", run_id.clone()), + ("resource_type", text(py, "run")), + ("resource_name", text(py, "audit")), + ("ledger_path", ledger_path), + ("phase", text(py, "audit")), + ], + )?), + ) + .and_then(|scope| Bound::new(py, scope))? + .into_any(); within(&scope, |_| { let callbacks: Bound<'py, PyAny> = call_with( &host::entry(py, ExecutorHost::AuditPipelineCallbacks)?, @@ -398,17 +420,13 @@ fn audit_payload<'py>( (concurrency, worker_count): (i64, i64), ) -> PyResult> { let py: Python<'py> = results.py(); - let payload: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::FormatAuditExecutionJson)?, - &[ - ("results", results.clone()), - ( - "configured_concurrency", - PyInt::new(py, concurrency).into_any(), - ), - ("worker_count", PyInt::new(py, worker_count).into_any()), - ], - )?; + let payload: Bound<'py, PyAny> = format_audit_execution_json( + py, + results.clone(), + Some(PyInt::new(py, concurrency).into_any()).filter(|value| !value.is_none()), + Some(PyInt::new(py, worker_count).into_any()).filter(|value| !value.is_none()), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; let projection: Bound<'py, PyAny> = host::entry(py, ExecutorHost::CurrentAuditResultProjection)?.call0()?; if projection.is_none() { @@ -418,7 +436,7 @@ fn audit_payload<'py>( let document: Bound<'py, PyAny> = json.call_method1("loads", (payload,))?; let counts: Bound<'py, PyDict> = PyDict::new(py); for field in ["attempted_count", "written_count", "failed_count"] { - counts.set_item(field, projection.getattr(field)?)?; + counts.set_item(field, native_field(&projection, field)?)?; } document.set_item("audit_result_projection", &counts)?; if native_field(&projection, "degraded")?.is_truthy()? { @@ -426,7 +444,7 @@ fn audit_payload<'py>( let reason: Bound<'py, PyDict> = PyDict::new(py); reason.set_item("reason", PROJECTION_FAILURE)?; for field in ["attempted_count", "written_count", "failed_count"] { - reason.set_item(field, projection.getattr(field)?)?; + reason.set_item(field, native_field(&projection, field)?)?; } let reasons: Bound<'py, PyList> = PyList::empty(py); reasons.append(reason)?; @@ -476,27 +494,23 @@ fn complete( &PyTuple::new(py, results)?.into_any(), (run.concurrency, run.worker_count), )?; - call_with( - ®istry::native(py, NativeAttr::WriteExecutionJsonOutput)?, - &[ - ("payload", payload), - ("json_output", native_field(request, "json_output")?), - ( - "json_output_path", - native_field(request, "json_output_path")?, - ), - ], - )?; + write_execution_json_output( + py, + payload, + native_field(request, "json_output")?.extract::()?, + native_field(request, "json_output_path")?, + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; Ok(audit_exit_code(counts.failed)) } /// Run one `sqb audit` request. #[pyfunction] #[pyo3(signature = (*, request))] -fn run_audit_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { +pub(crate) fn run_audit_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { let invocation: Bound<'_, PyAny> = resolve_reported_command_invocation( request, - ®istry::class(py, RecordId::AuditInvocation)?, + &record_class(py, RecordId::AuditInvocation)?, true, )?; let pipeline: Bound<'_, PyAny> = compile_audits(request, &invocation)?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/auditing/identity.rs b/crates/sqlbuild-python/src/bindings/_helpers/auditing/identity.rs index b7b2dbef43..10daf8c960 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/auditing/identity.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/auditing/identity.rs @@ -1,8 +1,8 @@ //! Audit gate identities of planned audits, built as `AuditGateIdentity` objects. +use crate::bindings::_helpers::records::construct::record_class; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; use pyo3::types::{PyDict, PyDictMethods, PyTuple}; use pyo3::{IntoPyObject, pyfunction, wrap_pyfunction}; @@ -95,7 +95,7 @@ fn build_audit_gate_identity<'py>( let entries: Vec> = audits.try_iter()?.collect::>()?; let planned: Vec = entries.iter().map(planned_audit).collect::>()?; let gate: GateIdentity = audit_gate_identity(&planned); - let identity_type: Bound<'py, PyAny> = registry::class(py, RecordId::AuditIdentity)?; + let identity_type: Bound<'py, PyAny> = record_class(py, RecordId::AuditIdentity)?; let mut identities: Vec> = Vec::with_capacity(gate.audits.len()); for identity in gate.audits { let entry: &Bound<'py, PyAny> = &entries[identity.index]; @@ -140,7 +140,7 @@ fn build_audit_gate_identity<'py>( )?); } keyword_call( - ®istry::class(py, RecordId::AuditGateIdentity)?, + &record_class(py, RecordId::AuditGateIdentity)?, vec![ ( "binding_set_hash", diff --git a/crates/sqlbuild-python/src/bindings/_helpers/build_command/command.rs b/crates/sqlbuild-python/src/bindings/_helpers/build_command/command.rs index 3dad08bd91..31db14c22d 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/build_command/command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/build_command/command.rs @@ -16,12 +16,11 @@ use crate::bindings::_helpers::build_command::planning::{ write_compile_target, write_plan_text, }; use crate::bindings::_helpers::compile_driver::python_calls::{call_with, within}; +use crate::bindings::_helpers::diagnostics::trackers::BuildPhaseTimingTracker; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::native_host::user_code::UserCodeHost; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::NativeAttr; -use crate::bindings::_helpers::records::registry; fn run_with_providers(context: &BuildContext<'_>) -> PyResult { let py: Python<'_> = context.py(); @@ -89,9 +88,10 @@ fn run_within_limits( /// Run one `sqb build` request. #[pyfunction] #[pyo3(signature = (*, request))] -fn run_build_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { - let tracker: Bound<'_, PyAny> = - registry::native(py, NativeAttr::BuildPhaseTimingTracker)?.call0()?; +pub(crate) fn run_build_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { + let tracker: Bound<'_, PyAny> = BuildPhaseTimingTracker::new(py, None) + .and_then(|value| pyo3::Bound::new(py, value)) + .map(pyo3::Bound::into_any)?; let reporting: Bound<'_, PyAny> = call_with( &host::entry(py, ExecutorHost::ProcessResourceReporting)?, &[("enabled", native_field(request, "debug")?)], diff --git a/crates/sqlbuild-python/src/bindings/_helpers/build_command/completion.rs b/crates/sqlbuild-python/src/bindings/_helpers/build_command/completion.rs index be58089b78..96930b3e0d 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/build_command/completion.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/build_command/completion.rs @@ -17,18 +17,24 @@ use crate::bindings::_helpers::build_command::invocation::BuildContext; use crate::bindings::_helpers::build_command::support::{ boolean, emit, joined, none, text, utc_now, }; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; +use crate::bindings::_helpers::build_progress::footer::format_build_footer; +use crate::bindings::_helpers::compile_driver::pipeline_entries::plan_has_executable_work; +use crate::bindings::_helpers::compile_driver::python_calls::{call_with, keywords}; use crate::bindings::_helpers::compile_outputs::runtime_target::{ write_python_check_runtime_target, write_runtime_target, }; use crate::bindings::_helpers::cost_report::degradation::{ finalize_build_cost as finalize_cost, render_build_cost, }; +use crate::bindings::_helpers::cost_report::functions::cost_output_payload; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; +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::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::ids::RecordId; +use crate::bindings::_helpers::result_output::documents::format_build_execution_json; +use crate::bindings::_helpers::result_output::documents::format_degradable_build_execution_json; +use crate::bindings::_helpers::result_output::documents::write_execution_json_output; const SUCCESS_STATUS: &str = "success"; const PENDING_MESSAGE: &str = "Build cost collection has not completed."; @@ -78,10 +84,8 @@ pub(crate) fn finalize_build_cost<'py>( ("use_color", boolean(py, context.use_color)), ]; fields.extend(cost.overrides); - let finalization: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::BuildCostFinalization)?, - &fields, - )?; + let finalization: Bound<'py, PyAny> = + new_record_with(py, RecordId::BuildCostFinalization, &fields)?; finalize_cost(&finalization) } @@ -137,7 +141,8 @@ fn write_timings(context: &BuildContext<'_>, timings: &PhaseTimings) -> PyResult /// Write the phases the tracker measured before an exceptional exit, to stderr without colour. pub(crate) fn write_partial_timings(py: Python<'_>, tracker: &Bound<'_, PyAny>) -> PyResult<()> { let snapshot: Bound<'_, PyAny> = tracker.call_method0("snapshot")?; - let seconds = |name: &str| -> PyResult> { snapshot.getattr(name)?.extract() }; + let seconds = + |name: &str| -> PyResult> { native_field(&snapshot, name)?.extract() }; let timings: PhaseTimings = PhaseTimings { compile_seconds: seconds("compile_seconds")?, planning_seconds: seconds("planning_seconds")?, @@ -157,10 +162,8 @@ fn cost_payload<'py>(py: Python<'py>, record: &Bound<'py, PyAny>) -> PyResult( @@ -174,37 +177,32 @@ fn run_id_for<'py>( } fn write_json(context: &BuildContext<'_>, payload: Bound<'_, PyAny>) -> PyResult<()> { - call_with( - ®istry::native(context.py(), NativeAttr::WriteExecutionJsonOutput)?, - &[ - ("payload", payload), - ( - "json_output", - native_field(&context.request, "json_output")?, - ), - ( - "json_output_path", - native_field(&context.request, "json_output_path")?, - ), - ], - )?; - Ok(()) + write_execution_json_output( + context.py(), + payload, + native_field(&context.request, "json_output")?.extract()?, + native_field(&context.request, "json_output_path")?, + ) } /// Persist and emit a completed zero-cost record when the plan has no work. fn finalize_no_work(context: &BuildContext<'_>, pipeline: &Bound<'_, PyAny>) -> PyResult { let py: Python<'_> = context.py(); let plan: Bound<'_, PyAny> = native_field(pipeline, "plan_output")?; - if call_with( - ®istry::native(py, NativeAttr::PlanHasExecutableWork)?, - &[ - ("plan", plan.clone()), - ( - "python_plan_entries", - native_field(pipeline, "python_plan_entries")?, - ), - ], - )? + if plan_has_executable_work( + py, + Some(keywords( + py, + &[ + ("plan", plan.clone()), + ( + "python_plan_entries", + native_field(pipeline, "python_plan_entries")?, + ), + ], + )?), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))? .is_truthy()? { return Ok(false); @@ -233,15 +231,19 @@ fn finalize_no_work(context: &BuildContext<'_>, pipeline: &Bound<'_, PyAny>) -> host::entry(py, ExecutorHost::BuildStatus)?.getattr("SUCCESS")?, )], )?; - let payload: Bound<'_, PyAny> = call_with( - ®istry::native(py, NativeAttr::FormatBuildExecutionJson)?, - &[ - ("result", result), - ("plan", plan), - ("run_id", run_id_for(pipeline, &record)?), - ("cost", cost_payload(py, &record)?), - ], - )?; + let payload: Bound<'_, PyAny> = format_build_execution_json( + py, + Some(keywords( + py, + &[ + ("result", result), + ("plan", plan), + ("run_id", run_id_for(pipeline, &record)?), + ("cost", cost_payload(py, &record)?), + ], + )?), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; write_json(context, payload)?; Ok(true) } @@ -354,35 +356,38 @@ pub(crate) fn complete_direct_build<'py>( ); let cost_seconds: f64 = finish_cost_timing(context, cost_started); let record: Bound<'py, PyAny> = record?; - let footer: String = call_with( - ®istry::native(py, NativeAttr::FormatBuildFooter)?, - &[ - ("result", result.clone()), - ("elapsed", native_field(&preparation.callbacks, "elapsed")?), - ("use_color", boolean(py, context.use_color)), - ("python_node_results", outcome.python_results.clone()), - ], - )? + let footer: String = format_build_footer( + &result.clone(), + native_field(&preparation.callbacks, "elapsed")?.extract::()?, + boolean(py, context.use_color).extract::()?, + Some(&outcome.python_results.clone()).filter(|value| !value.is_none()), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))? .extract()?; emit(&context.stream, &format!("\n{footer}\n"))?; - let payload: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::FormatDegradableBuildExecutionJson)?, - &[ - ("result", result.clone()), - ("plan", plan), - ("python_node_results", outcome.python_results.clone()), - ("python_check_results", check_results.clone()), - ("command", text(py, "build")), - ("run_id", run_id_for(pipeline, &record)?), - ("cost", cost_payload(py, &record)?), - ("execution_succeeded", boolean(py, exit_code == 0)), - ], - )?; + let payload: Bound<'py, PyAny> = format_degradable_build_execution_json( + py, + Some(keywords( + py, + &[ + ("result", result.clone()), + ("plan", plan), + ("python_node_results", outcome.python_results.clone()), + ("python_check_results", check_results.clone()), + ("command", text(py, "build")), + ("run_id", run_id_for(pipeline, &record)?), + ("cost", cost_payload(py, &record)?), + ("execution_succeeded", boolean(py, exit_code == 0)), + ], + )?), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; write_json(context, payload)?; render_build_cost(&record, &context.stream, context.use_color)?; if context.verbose { let timings: Bound<'py, PyAny> = native_field(result, "timings")?; - let seconds = |name: &str| -> PyResult> { timings.getattr(name)?.extract() }; + let seconds = + |name: &str| -> PyResult> { native_field(&timings, name)?.extract() }; write_timings( context, &PhaseTimings { 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 147b8f8056..9ec6a15e48 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 @@ -13,13 +13,23 @@ use sqlbuild_core::build_command::errors::BuildCommandError; use crate::bindings::_helpers::build_command::invocation::BuildContext; use crate::bindings::_helpers::build_command::policies::build_command_error; +use crate::bindings::_helpers::build_command::policies::enforce_model_execution_limit_binding; +use crate::bindings::_helpers::clone::clone_pipeline::run_clone_pipeline_with_options; use crate::bindings::_helpers::compile_driver::project::build_compiled_project; -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported, keywords}; +use crate::bindings::_helpers::compile_driver::python_calls::{call_with, keywords}; +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::executor::ExecutorHost; 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::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::ids::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::fields::replaced; +use crate::bindings::_helpers::records::ids::RecordId; const VIEW_MATERIALIZATION: &str = "view"; const RECREATED_TYPES: [&str; 2] = ["udf", "table_fn"]; @@ -101,20 +111,27 @@ fn membership<'py>( 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: NativeAttr, + function: TraversalFunction, (is_clonable, is_view): (&Bound<'py, PyAny>, &Bound<'py, PyAny>), ) -> PyResult> { let py: Python<'py> = scope.py(); - let keys: Bound<'py, PyAny> = call_with( - ®istry::native(py, function)?, - &[ - ("selected", native_field(scope, "selected_keys")?), - ("upstream", native_field(scope, "upstream_deps")?), - ("is_clonable", is_clonable.clone()), - ("is_view", is_view.clone()), - ], + 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()? { @@ -158,8 +175,8 @@ fn defer_clone_selection(scope: &Bound<'_, PyAny>) -> PyResult<(Vec, Vec let is_view: Bound<'_, PyAny> = membership(py, &keys, |key| recreated_over_clones(scope, key))?; let callbacks: (&Bound<'_, PyAny>, &Bound<'_, PyAny>) = (&is_clonable, &is_view); Ok(( - traversed_names(scope, NativeAttr::ResolveCloneBoundary, callbacks)?, - traversed_names(scope, NativeAttr::ResolveSkippedViewChain, callbacks)?, + traversed_names(scope, resolve_clone_boundary, callbacks)?, + traversed_names(scope, resolve_skipped_view_chain, callbacks)?, selected_executable_model_count(scope)?, )) } @@ -173,26 +190,11 @@ fn resolve_defer_clone_selection( defer_clone_selection(scope) } -fn native<'py>( - py: Python<'py>, - name: NativeAttr, - fields: &[(&str, Bound<'py, PyAny>)], -) -> PyResult> { - call_with(®istry::native(py, name)?, fields) -} - fn external_resolver<'py>( project_dir: &Bound<'py, PyAny>, discovered: &Bound<'py, PyAny>, ) -> PyResult> { - native( - project_dir.py(), - NativeAttr::ResolveExternalSqlReferenceResolver, - &[ - ("project_dir", project_dir.clone()), - ("discovered_inputs", discovered.clone()), - ], - ) + resolve_external_sql_reference_resolver(project_dir.clone(), discovered.clone()) } /// The resolved defer-clone prephase: what to clone, from where, into which target. @@ -223,20 +225,11 @@ fn clone_input<'py>( let project: Bound<'py, PyAny> = native_field(pipeline, "destination_project")?; let destination_models: Bound<'py, PyAny> = native_field(pipeline, "destination_model_entries")?; - let target: Bound<'py, PyAny> = native( + let target: Bound<'py, PyAny> = resolve_target_config( py, - NativeAttr::ResolveTargetConfig, - &[ - ( - "project_config", - native_field(&prephase.discovered, "project_config")?, - ), - ( - "local_config", - native_field(&prephase.discovered, "local_config")?, - ), - ("target_name", prephase.destination.clone()), - ], + &native_field(&prephase.discovered, "project_config")?, + &native_field(&prephase.discovered, "local_config")?, + &prephase.destination.clone(), )?; let mut selected: Vec> = Vec::new(); for entry in destination_models.try_iter()? { @@ -262,7 +255,7 @@ fn clone_input<'py>( )?; let allow_decrease: bool = target .getattr("time_travel_retention_decrease")? - .eq(registry::class(py, RecordId::RetentionDecreasePolicy)?.getattr("ALLOW")?)?; + .eq(enum_member(py, RecordId::RetentionDecreasePolicy, "ALLOW")?)?; let sources: Bound<'py, PyAny> = call_with( &host::entry(py, ExecutorHost::CloneSourceEntries)?, &[ @@ -361,10 +354,8 @@ fn executed_clone<'py>( _: Option<&Bound<'_, PyDict>>| -> PyResult> { let py: Python<'_> = arguments.py(); - let inputs: Bound<'_, PyAny> = imported(py, "dataclasses", "replace")?.call( - (prepared.0.bind(py),), - Some(&keywords(py, &[("on_item", arguments.get_item(0)?)])?), - )?; + let inputs: Bound<'_, PyAny> = + replaced(prepared.0.bind(py), &[("on_item", arguments.get_item(0)?)])?; Ok(prepared .1 .bind(py) @@ -426,9 +417,8 @@ fn clone_boundaries<'py>( .getattr("on_progress")?; let start: Instant = Instant::now(); let request: &Bound<'py, PyAny> = &context.request; - let pipeline: Bound<'py, PyAny> = native( + let pipeline: Bound<'py, PyAny> = run_clone_pipeline_with_options(Some(&keywords( py, - NativeAttr::RunClonePipelineWithOptions, &[ ("discovered_inputs", prephase.discovered.clone()), ("adapter", prephase.adapter.clone()), @@ -436,8 +426,9 @@ fn clone_boundaries<'py>( ("destination_target_name", prephase.destination.clone()), ( "destination_connection", - call_with( - ®istry::class(py, RecordId::ClonePipelineConnection)?, + new_record_with( + py, + RecordId::ClonePipelineConnection, &[ ( "config", @@ -449,8 +440,9 @@ fn clone_boundaries<'py>( ), ( "options", - call_with( - ®istry::class(py, RecordId::ClonePipelineOptions)?, + new_record_with( + py, + RecordId::ClonePipelineOptions, &[ ( "no_sql_validation", @@ -467,7 +459,7 @@ fn clone_boundaries<'py>( external_resolver(&prephase.project_dir, &prephase.discovered)?, ), ], - )?; + )?))?; progress( &on_progress, &format!( @@ -569,14 +561,10 @@ pub(crate) fn run_defer_clone_boundary_prephase( let py: Python<'_> = context.py(); let (request, invocation) = (&context.request, &context.invocation); let discovered: Bound<'_, PyAny> = native_field(invocation, "discovered_inputs")?; - let split: Bound<'_, PyAny> = native( - py, - NativeAttr::SplitUnitTestSelectors, - &[ - ("select", native_field(request, "select")?), - ("exclude", native_field(request, "exclude")?), - ("discovered_inputs", discovered.clone()), - ], + let split: Bound<'_, PyAny> = split_unit_test_selectors( + &native_field(request, "select")?, + &native_field(request, "exclude")?, + &discovered.clone(), )?; if !split .getattr("sql_test_selection")? @@ -601,8 +589,9 @@ pub(crate) fn run_defer_clone_boundary_prephase( ), ( "analysis_selection", - call_with( - ®istry::class(py, RecordId::CompileAnalysisSelection)?, + new_record_with( + py, + RecordId::CompileAnalysisSelection, &[("no_cache", native_field(request, "no_cache")?)], )?, ), @@ -614,37 +603,42 @@ pub(crate) fn run_defer_clone_boundary_prephase( ], )?), )?; - let scope: Bound<'_, PyAny> = native( + let scope: Bound<'_, PyAny> = build_planner_scope( py, - NativeAttr::BuildPlannerScope, - &[ - ("project", project.clone()), - ("select", native_field(&split, "select")?), - ("exclude", native_field(&split, "exclude")?), - ( - "auto_load_sources", - native_field(invocation, "should_load_sources")?, - ), - ], + Some(keywords( + py, + &[ + ("project", project.clone()), + ("select", native_field(&split, "select")?), + ("exclude", native_field(&split, "exclude")?), + ( + "auto_load_sources", + native_field(invocation, "should_load_sources")?, + ), + ], + )?), )?; let (boundary, view_chain, model_count): (Vec, Vec, usize) = defer_clone_selection(&scope)?; - native( + enforce_model_execution_limit_binding( py, - NativeAttr::EnforceModelExecutionLimit, - &[ - ("model_count", model_count.into_pyobject(py)?.into_any()), - ( - "target_name", - native_field(invocation, "effective_target_name")?, - ), - ("limits", native_field(invocation, "execution_limits")?), - ( - "refusal_note", - EXECUTION_LIMIT_BUILD_NOTE.into_bound_py_any(py)?, - ), - ], - )?; + Some(keywords( + py, + &[ + ("model_count", model_count.into_pyobject(py)?.into_any()), + ( + "target_name", + native_field(invocation, "effective_target_name")?, + ), + ("limits", native_field(invocation, "execution_limits")?), + ( + "refusal_note", + EXECUTION_LIMIT_BUILD_NOTE.into_bound_py_any(py)?, + ), + ], + )?), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; let mut select: Vec = boundary; select.extend(view_chain); run_prephase( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/build_command/execution.rs b/crates/sqlbuild-python/src/bindings/_helpers/build_command/execution.rs index a3a257648c..3ae7608e0c 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/build_command/execution.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/build_command/execution.rs @@ -14,19 +14,27 @@ use crate::bindings::_helpers::build_command::lifecycle::{ use crate::bindings::_helpers::build_command::support::{ boolean, display_text, emit, frozenset, joined, keyword_relay, line_writer, none, text, tuple, }; +use crate::bindings::_helpers::build_progress::build_callbacks::BuildProgressCallbacks; use crate::bindings::_helpers::check::check_core::{ build_check_relation_targets, load_results_by_loader_name, record_python_run_state_results, relevant_check_functions, }; -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, within}; +use crate::bindings::_helpers::check::check_results::write_check_results; +use crate::bindings::_helpers::cli::progress_reporters::ConnectionProgressReporter; +use crate::bindings::_helpers::cli::progress_text::write_execution_header; +use crate::bindings::_helpers::command_runtime::warehouses::describe_connection_warehouse; +use crate::bindings::_helpers::compile_driver::python_calls::{call_with, keywords, within}; +use crate::bindings::_helpers::compile_driver::relation_targets::build_project_relation_targets; +use crate::bindings::_helpers::cost_telemetry::context::CostContext; use crate::bindings::_helpers::cursor::overrides::{cursor_integer, cursor_timestamp}; use crate::bindings::_helpers::freshness::publication::append_freshness_records; use crate::bindings::_helpers::graph::python_node_graph::python_node_graph; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; +use crate::bindings::_helpers::planner_resolution::cursor_overrides::type_cursor_overrides; +use crate::bindings::_helpers::records::construct::new_record; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::ids::RecordId; const EXECUTION_COMMAND: &str = "sqb build"; const SUCCESS_STATUS: &str = "success"; @@ -49,7 +57,7 @@ pub(crate) struct Outcome<'py> { } fn count(value: &Bound<'_, PyAny>, name: &str) -> PyResult { - value.getattr(name)?.len() + native_field(value, name)?.len() } fn validated_date(py: Python<'_>, value: Option>) -> PyResult> { @@ -58,10 +66,7 @@ fn validated_date(py: Python<'_>, value: Option>) -> PyResult = py.import("datetime")?.into_any(); for kind in ["date", "datetime"] { - match datetime - .getattr(kind)? - .call_method1("fromisoformat", (&value,)) - { + match native_field(&datetime, kind)?.call_method1("fromisoformat", (&value,)) { Ok(_) => return Ok(Some(value.extract()?)), Err(error) if error.is_instance_of::(py) => {} Err(error) => return Err(error), @@ -100,16 +105,11 @@ fn run_context_facts( + loaded_sources + count(&discovered, "task_functions")? + count(&discovered, "asset_functions")?; - let resolved: Bound<'_, PyAny> = call_with( - ®istry::native(py, NativeAttr::DescribeConnectionWarehouse)?, - &[ - ("discovered_inputs", discovered), - ( - "selected_target", - native_field(&project, "effective_target_name")?, - ), - ("connection_config", context.get("connection_config")?), - ], + let resolved: Bound<'_, PyAny> = describe_connection_warehouse( + py, + discovered, + native_field(&project, "effective_target_name")?, + context.get("connection_config")?, )?; let warehouse: Bound<'_, PyAny> = native_field(&resolved, "warehouse")?; let warehouse: Option<(String, String)> = if warehouse.is_none() { @@ -177,16 +177,11 @@ fn cursors<'py>( let py: Python<'py> = context.py(); let mut overrides: Bound<'py, PyAny> = native_field(plan, "cursor_overrides")?; if !overrides.is_truthy()? { - overrides = call_with( - ®istry::native(py, NativeAttr::TypeCursorOverrides)?, - &[( - "cursor_overrides", - native_field(&context.request, "cursor_overrides")?, - )], - )?; + overrides = + type_cursor_overrides(&native_field(&context.request, "cursor_overrides")?, &[])?; } if !overrides.is_truthy()? { - overrides = registry::class(py, RecordId::CursorOverrides)?.call0()?; + overrides = new_record(py, RecordId::CursorOverrides, (), None)?; } Ok([ cursor_timestamp(&native_field(&overrides, "start_ts")?)?, @@ -205,48 +200,62 @@ pub(crate) fn prepare_build_execution<'py>( let py: Python<'py> = context.py(); let plan: Bound<'py, PyAny> = native_field(pipeline, "plan_output")?; let debug: bool = context.request_flag("debug")? || context.request_flag("json_output")?; - let callbacks: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::BuildProgressCallbacks)?, - &[ - ("plan", plan.clone()), - ("use_color", boolean(py, context.use_color)), - ("verbose", native_field(&context.request, "verbose")?), - ("debug", boolean(py, debug)), - ( - "event_output_path", - native_field(&context.request, "event_output_path")?, - ), - ], - )?; + let callbacks: Bound<'py, PyAny> = BuildProgressCallbacks::new( + py, + Some(keywords( + py, + &[ + ("plan", plan.clone()), + ("use_color", boolean(py, context.use_color)), + ("verbose", native_field(&context.request, "verbose")?), + ("debug", boolean(py, debug)), + ( + "event_output_path", + native_field(&context.request, "event_output_path")?, + ), + ], + )?), + ) + .and_then(|value| pyo3::Bound::new(py, value)) + .map(pyo3::Bound::into_any)?; let concurrency: Bound<'py, PyAny> = context.concurrency(&native_field(pipeline, "project")?)?; if context.verbose { write_run_context(context, pipeline)?; } else { - call_with( - ®istry::native(py, NativeAttr::WriteExecutionHeader)?, + write_execution_header( + py, + Some(keywords( + py, + &[ + ("stream", context.stream.clone()), + ("command", text(py, EXECUTION_COMMAND)), + ("target", none(py)), + ("concurrency", concurrency.clone()), + ("use_color", boolean(py, context.use_color)), + ], + )?), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; + } + let connection_progress: Bound<'py, PyAny> = ConnectionProgressReporter::new( + py, + Some(keywords( + py, &[ + ("adapter_name", context.get("adapter_name")?), ("stream", context.stream.clone()), - ("command", text(py, EXECUTION_COMMAND)), - ("target", none(py)), - ("concurrency", concurrency.clone()), + ( + "blank_line_before_start", + boolean(py, native_field(&plan, "source_load_entries")?.is_truthy()?), + ), + ("blank_line_after_complete", boolean(py, true)), ("use_color", boolean(py, context.use_color)), ], - )?; - } - let connection_progress: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ConnectionProgressReporter)?, - &[ - ("adapter_name", context.get("adapter_name")?), - ("stream", context.stream.clone()), - ( - "blank_line_before_start", - boolean(py, native_field(&plan, "source_load_entries")?.is_truthy()?), - ), - ("blank_line_after_complete", boolean(py, true)), - ("use_color", boolean(py, context.use_color)), - ], - )?; + )?), + ) + .and_then(|value| pyo3::Bound::new(py, value)) + .map(pyo3::Bound::into_any)?; let cursors: [Bound<'py, PyAny>; 4] = cursors(context, &plan)?; let inputs: LifecycleInputs<'py> = LifecycleInputs { cursors: cursors.clone(), @@ -271,7 +280,7 @@ fn verbose_callback<'py>( name: &str, ) -> PyResult> { if context.verbose { - return callbacks.getattr(name); + return native_field(callbacks, name); } Ok(none(context.py())) } @@ -291,7 +300,7 @@ pub(crate) fn execute_build_plan<'py>( let callbacks: &Bound<'py, PyAny> = &preparation.callbacks; let lifecycle: &DirectPythonLifecycle = preparation.lifecycle.get(); let [start_ts, end_ts, start_int, end_int] = &preparation.cursors; - let request = |name: &str| context.request.getattr(name); + let request = |name: &str| native_field(&context.request, name); let runtime: Bound<'py, PyAny> = call_with( &host::entry(py, ExecutorHost::BuildRuntimeParams)?, &[ @@ -324,10 +333,14 @@ pub(crate) fn execute_build_plan<'py>( ("providers", providers.clone()), ( "project_relations", - call_with( - ®istry::native(py, NativeAttr::BuildProjectRelationTargets)?, - &[("adapter", adapter.clone()), ("plan_output", plan.clone())], - )?, + build_project_relation_targets( + py, + Some(keywords( + py, + &[("adapter", adapter.clone()), ("plan_output", plan.clone())], + )?), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?, ), ], )?; @@ -457,13 +470,17 @@ fn check_runtime<'py>( ), ( "project_relations", - call_with( - ®istry::native(py, NativeAttr::BuildProjectRelationTargets)?, - &[ - ("adapter", adapter), - ("plan_output", native_field(pipeline, "plan_output")?), - ], - )?, + build_project_relation_targets( + py, + Some(keywords( + py, + &[ + ("adapter", adapter), + ("plan_output", native_field(pipeline, "plan_output")?), + ], + )?), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?, ), ("providers", providers.clone()), ], @@ -473,7 +490,7 @@ fn check_runtime<'py>( fn names_of<'py>(values: &Bound<'py, PyAny>, attribute: &str) -> PyResult>> { values .try_iter()? - .map(|value| value?.getattr(attribute)) + .map(|value| native_field(&value?, attribute)) .collect() } @@ -558,16 +575,20 @@ pub(crate) fn run_post_build_python_checks<'py>( let closed: PyResult> = adapter.call_method1("close", (connection,)); let check_results: Bound<'py, PyAny> = executed?; closed?; - call_with( - ®istry::native(py, NativeAttr::WriteCheckResults)?, - &[ - ("stream", context.stream.clone()), - ("results", check_results.clone()), - ("use_color", boolean(py, context.use_color)), - ("check_functions", tuple(py, &checks)?), - ("python_graph", graph), - ], - )?; + write_check_results( + py, + Some(keywords( + py, + &[ + ("stream", context.stream.clone()), + ("results", check_results.clone()), + ("use_color", boolean(py, context.use_color)), + ("check_functions", tuple(py, &checks)?), + ("python_graph", graph), + ], + )?), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; Ok(check_results) } @@ -586,7 +607,6 @@ pub(crate) fn execute_direct_build<'py>( } else { context.get("adapter_name")? }; - let cost_context: Bound<'py, PyAny> = registry::native(py, NativeAttr::CostContext)?; let options: Bound<'py, PyDict> = PyDict::new(py); options.set_item("run_id", &run_id)?; options.set_item("resource_type", "run")?; @@ -604,20 +624,26 @@ pub(crate) fn execute_direct_build<'py>( )?, )?; options.set_item("phase", "build")?; - let scope: Bound<'py, PyAny> = cost_context.call_method("scope", (), Some(&options))?; + let scope: Bound<'py, PyAny> = + Bound::new(py, CostContext::scope(py, Some(options))?)?.into_any(); within(&scope, |_| { let preparation: Preparation<'py> = prepare_build_execution(context, pipeline, providers)?; let ran: PyResult<(Outcome<'py>, Bound<'py, PyAny>)> = (|| { let outcome: Outcome<'py> = execute_build_plan(context, pipeline, &preparation, providers)?; - let checks_scope: Bound<'py, PyAny> = call_with( - &native_field(&cost_context, "resource_scope")?, - &[ - ("resource_type", text(py, "run")), - ("resource_name", resource_name.clone()), - ("phase", text(py, "post_build_checks")), - ], - )?; + let checks_scope: Bound<'py, PyAny> = CostContext::resource_scope( + py, + Some(keywords( + py, + &[ + ("resource_type", text(py, "run")), + ("resource_name", resource_name.clone()), + ("phase", text(py, "post_build_checks")), + ], + )?), + ) + .and_then(|scope| Bound::new(py, scope))? + .into_any(); let checks: Bound<'py, PyAny> = within(&checks_scope, |_| { run_post_build_python_checks(context, pipeline, (&outcome, &preparation), providers) })?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/build_command/invocation.rs b/crates/sqlbuild-python/src/bindings/_helpers/build_command/invocation.rs index d7756831c6..000e83c016 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/build_command/invocation.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/build_command/invocation.rs @@ -9,14 +9,21 @@ use sqlbuild_core::build_command::models::DurationLimit; use crate::bindings::_helpers::build_command::policies::build_command_error; use crate::bindings::_helpers::build_command::support::{boolean, or_none, text}; +use crate::bindings::_helpers::cli::progress_reporters::ConnectionProgressReporter; +use crate::bindings::_helpers::cli::progress_reporters::PlanningProgressReporter; use crate::bindings::_helpers::command_runtime::adapters::resolve_cli_adapter; +use crate::bindings::_helpers::command_runtime::connection::resolve_project_connection_config; +use crate::bindings::_helpers::compile_attachment::effective_config::build_effective_settings_config; use crate::bindings::_helpers::compile_driver::discovery::discover_project_inputs; +use crate::bindings::_helpers::compile_driver::python_calls::imported; use crate::bindings::_helpers::compile_driver::python_calls::keywords; -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; +use crate::bindings::_helpers::contracts::spec_resolution::resolve_effective_adapter_name; +use crate::bindings::_helpers::contracts::spec_resolution::resolve_target_config; +use crate::bindings::_helpers::contracts::spec_resolution::resolve_target_name; use crate::bindings::_helpers::presentation_text::terminal::stdout_supports_color; +use crate::bindings::_helpers::records::construct::{new_record, new_record_with}; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::ids::RecordId; const DEFER_CONFLICT_CODE: &str = "C408"; @@ -37,11 +44,11 @@ impl<'py> BuildContext<'py> { } pub(crate) fn request_flag(&self, name: &str) -> PyResult { - self.request.getattr(name)?.is_truthy() + native_field(&self.request, name)?.is_truthy() } pub(crate) fn get(&self, name: &str) -> PyResult> { - self.invocation.getattr(name) + native_field(&self.invocation, name) } /// The requested concurrency, or the project setting. @@ -57,16 +64,11 @@ fn target_config<'py>( target_name: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = discovered.py(); - call_with( - ®istry::native(py, NativeAttr::ResolveTargetConfig)?, - &[ - ( - "project_config", - native_field(discovered, "project_config")?, - ), - ("local_config", native_field(discovered, "local_config")?), - ("target_name", target_name.clone()), - ], + resolve_target_config( + py, + &native_field(discovered, "project_config")?, + &native_field(discovered, "local_config")?, + &target_name.clone(), ) } @@ -77,7 +79,7 @@ fn execution_limits<'py>( ) -> PyResult> { let py: Python<'py> = discovered.py(); let limits: Bound<'py, PyAny> = if target_name.is_none() { - registry::class(py, RecordId::ExecutionLimitsConfig)?.call0()? + new_record(py, RecordId::ExecutionLimitsConfig, (), None)? } else { native_field(&target_config(discovered, target_name)?, "execution_limits")? }; @@ -118,17 +120,13 @@ pub(crate) fn resolve_build_context<'py>( )?), )?; let selected_target: Bound<'py, PyAny> = native_field(request, "selected_target")?; - let target_name: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ResolveTargetName)?, - &[ - ( - "project_config", - native_field(&discovered, "project_config")?, - ), - ("local_config", native_field(&discovered, "local_config")?), - ("selected_target", selected_target.clone()), - ], - )?; + let target_name: Bound<'py, PyAny> = resolve_target_name( + py, + &native_field(&discovered, "project_config")?, + &native_field(&discovered, "local_config")?, + &selected_target.clone(), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; let defer_clone_from: Bound<'py, PyAny> = or_none(native_field(request, "defer_clone_from")?, || { if target_name.is_none() { @@ -149,28 +147,25 @@ pub(crate) fn resolve_build_context<'py>( ), )); } - let adapter_name: String = call_with( - ®istry::native(py, NativeAttr::ResolveEffectiveAdapterName)?, - &[ - ( - "project_config", - native_field(&discovered, "project_config")?, - ), - ("local_config", native_field(&discovered, "local_config")?), - ], + let adapter_name: String = resolve_effective_adapter_name( + &native_field(&discovered, "project_config")?, + &native_field(&discovered, "local_config")?, )? .extract()?; let adapter: Bound<'py, PyAny> = resolve_cli_adapter(&text(py, &adapter_name), &project_dir)?; let limits: Bound<'py, PyAny> = execution_limits(&discovered, &target_name, (&adapter_name, &adapter))?; - let connection_config: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ResolveProjectConnectionConfig)?, - &[ - ("discovered_inputs", discovered.clone()), - ("project_dir", project_dir.clone()), - ("selected_target", selected_target), - ("cli_vars", native_field(request, "cli_vars")?), - ], + let connection_config: Bound<'py, PyAny> = resolve_project_connection_config( + py, + Some(keywords( + py, + &[ + ("discovered_inputs", discovered.clone()), + ("project_dir", project_dir.clone()), + ("selected_target", selected_target), + ("cli_vars", native_field(request, "cli_vars")?), + ], + )?), )?; let machine_output: bool = native_field(request, "json_output")?.is_truthy()?; let debug: bool = native_field(request, "debug")?.is_truthy()?; @@ -182,35 +177,38 @@ pub(crate) fn resolve_build_context<'py>( } else { "stdout" })?; - let connection_progress: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ConnectionProgressReporter)?, - &[ - ("adapter_name", text(py, &adapter_name)), - ("stream", stream.clone()), - ("use_color", boolean(py, use_color)), - ], - )?; - let planning_progress: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::PlanningProgressReporter)?, - &[ - ("stream", stream.clone()), - ("use_color", boolean(py, use_color)), - ], - )?; + let connection_progress: Bound<'py, PyAny> = ConnectionProgressReporter::new( + py, + Some(keywords( + py, + &[ + ("adapter_name", text(py, &adapter_name)), + ("stream", stream.clone()), + ("use_color", boolean(py, use_color)), + ], + )?), + ) + .and_then(|value| pyo3::Bound::new(py, value)) + .map(pyo3::Bound::into_any)?; + let planning_progress: Bound<'py, PyAny> = PlanningProgressReporter::new( + py, + &stream.clone(), + boolean(py, use_color).extract::()?, + ) + .and_then(|value| pyo3::Bound::new(py, value)) + .map(pyo3::Bound::into_any)?; let load_sources: Bound<'py, PyAny> = native_field(request, "load_sources")?; let should_load_sources: bool = native_field(request, "reload_sources")?.is_truthy()? || if load_sources.is_none() { - call_with( - ®istry::native(py, NativeAttr::BuildEffectiveSettingsConfig)?, - &[("discovered_inputs", discovered.clone())], - )? - .getattr("auto_load_sources")? - .is_truthy()? + build_effective_settings_config(&discovered.clone())? + .getattr("auto_load_sources")? + .is_truthy()? } else { load_sources.is_truthy()? }; - let invocation: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::BuildInvocation)?, + let invocation: Bound<'py, PyAny> = new_record_with( + py, + RecordId::BuildInvocation, &[ ("effective_project_dir", project_dir), ("discovered_inputs", discovered), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/build_command/lifecycle.rs b/crates/sqlbuild-python/src/bindings/_helpers/build_command/lifecycle.rs index 5637fc9624..f4ed49db10 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/build_command/lifecycle.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/build_command/lifecycle.rs @@ -14,13 +14,18 @@ use sqlbuild_core::cli_text::models::CliStyle; use crate::bindings::_helpers::build_command::support::{ boolean, emit, frozenset, or_none, text, tuple, }; -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; +use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported, keywords}; +use crate::bindings::_helpers::compile_driver::relation_targets::build_project_relation_targets; +use crate::bindings::_helpers::compile_driver::relation_targets::build_python_relation_targets; +use crate::bindings::_helpers::graph::python_lifecycle::build_python_sql_run_lifecycle; use crate::bindings::_helpers::graph::python_node_graph::python_node_graph; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; +use crate::bindings::_helpers::planner_scope::loaders::build_intermediate_source_map; +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::ids::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::fields::replaced_keywords; +use crate::bindings::_helpers::records::ids::RecordId; const SUCCESS_STATUS: &str = "success"; @@ -148,7 +153,6 @@ impl DirectPythonLifecycle { .collect::>()?; let functions: Bound<'py, PyAny> = frozenset(py, &functions)?; let ingress: &Bound<'py, PyAny> = self.ingress_loader_names.bind(py); - let replace: Bound<'py, PyAny> = imported(py, "dataclasses", "replace")?; let mut handoff: Vec> = Vec::new(); for loader in loaders { let keeps_all: bool = !ingress.contains(native_field(&loader, "name")?)?; @@ -161,7 +165,7 @@ impl DirectPythonLifecycle { } let changes: Bound<'py, PyDict> = PyDict::new(py); changes.set_item("depends_on", tuple(py, &dependencies)?)?; - handoff.push(replace.call((loader,), Some(&changes))?); + handoff.push(replaced_keywords(&loader, &changes)?); } tuple(py, &handoff) } @@ -237,7 +241,7 @@ fn scheduled_sql_names(plan: &Bound<'_, PyAny>) -> PyResult> { .call1((native_field(plan, "execution_order")?,))?; let mut names: HashSet = HashSet::new(); for kind in ["model_entries", "seed_entries", "source_load_entries"] { - for entry in plan.getattr(kind)?.try_iter()? { + for entry in native_field(plan, kind)?.try_iter()? { let entry: Bound<'_, PyAny> = entry?; if scheduled.contains(native_field(&entry, "key")?)? { names.insert(native_field(&entry, "name")?.extract()?); @@ -303,8 +307,9 @@ fn lifecycle_plan<'py>( let py: Python<'py> = discovered.py(); let plan_output: Bound<'py, PyAny> = native_field(pipeline, "plan_output")?; let graph: Bound<'py, PyAny> = python_node_graph(discovered)?; - let selection: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::PythonSqlRunSelection)?, + let selection: Bound<'py, PyAny> = new_record_with( + py, + RecordId::PythonSqlRunSelection, &[ ( "sql_keys", @@ -317,17 +322,13 @@ fn lifecycle_plan<'py>( ), ], )?; - let plan: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::BuildPythonSqlRunLifecycle)?, - &[("selection", selection), ("python_graph", graph.clone())], - )?; - let source_type: Bound<'py, PyAny> = - registry::class(py, RecordId::CompiledResourceType)?.getattr("SOURCE")?; - let key_class: Bound<'py, PyAny> = registry::class(py, RecordId::CompiledObjectKey)?; + let plan: Bound<'py, PyAny> = build_python_sql_run_lifecycle(&selection, &graph.clone())?; + let source_type: Bound<'py, PyAny> = enum_member(py, RecordId::CompiledResourceType, "SOURCE")?; let mut keys: Vec> = Vec::new(); for name in native_field(&plan, "ingress_loader_names")?.try_iter()? { - keys.push(call_with( - &key_class, + keys.push(new_record_with( + py, + RecordId::CompiledObjectKey, &[("name", name?), ("resource_type", source_type.clone())], )?); } @@ -335,13 +336,11 @@ fn lifecycle_plan<'py>( imported(py, "builtins", "dict")?.call1((native_field(&plan_output, "source_map")?,))?; source_map.call_method1( "update", - (call_with( - ®istry::native(py, NativeAttr::BuildIntermediateSourceMap)?, - &[ - ("project", native_field(pipeline, "project")?), - ("selected_keys", frozenset(py, &keys)?), - ], - )?,), + (build_intermediate_source_map( + &native_field(pipeline, "project")?, + &frozenset(py, &keys)?, + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?,), )?; Ok(LifecyclePlan { graph, @@ -356,7 +355,7 @@ fn task_asset_names( ) -> PyResult> { let mut known: HashSet = HashSet::new(); for kind in ["task_functions", "asset_functions"] { - for node in discovered.getattr(kind)?.try_iter()? { + for node in native_field(discovered, kind)?.try_iter()? { known.insert(native_field(&node?, "name")?.extract()?); } } @@ -403,22 +402,30 @@ pub(crate) fn prepare_direct_python_lifecycle<'py>( )?, )], )?; - let relation_targets: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::BuildPythonRelationTargets)?, - &[ - ("adapter", adapter.clone()), - ("project", project.clone()), - ("plan_output", plan_output.clone()), - ("required_refs", required), - ], - )?; - let project_relations: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::BuildProjectRelationTargets)?, - &[ - ("adapter", adapter.clone()), - ("plan_output", plan_output.clone()), - ], - )?; + let relation_targets: Bound<'py, PyAny> = build_python_relation_targets( + py, + Some(keywords( + py, + &[ + ("adapter", adapter.clone()), + ("project", project.clone()), + ("plan_output", plan_output.clone()), + ("required_refs", required), + ], + )?), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; + let project_relations: Bound<'py, PyAny> = build_project_relation_targets( + py, + Some(keywords( + py, + &[ + ("adapter", adapter.clone()), + ("plan_output", plan_output.clone()), + ], + )?), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; let parts: RuntimeParts<'_, 'py> = RuntimeParts { adapter: &adapter, connection_config: &connection_config, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/build_command/planning.rs b/crates/sqlbuild-python/src/bindings/_helpers/build_command/planning.rs index 94469a8dc2..0e9d5993c4 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/build_command/planning.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/build_command/planning.rs @@ -14,12 +14,16 @@ use crate::bindings::_helpers::build_command::support::{ boolean, emit, joined, keyword_relay, none, }; use crate::bindings::_helpers::cli::progress_text::with_verbose_inspection; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; +use crate::bindings::_helpers::compile_driver::pipeline::run_compile_pipeline; +use crate::bindings::_helpers::compile_driver::python_calls::{call_with, keywords}; +use crate::bindings::_helpers::dbt_interop::reference_resolver::resolve_external_sql_reference_resolver; +use crate::bindings::_helpers::manifest::builder::build_manifest; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; +use crate::bindings::_helpers::plan_output::plan_text::format_plan; +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::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::ids::RecordId; const CONNECTION_TIMING: [&str; 2] = ["connection_count", "elapsed_seconds"]; @@ -35,19 +39,17 @@ pub(crate) fn run_defer_clone_prephase(context: &BuildContext<'_>) -> PyResult<( fn request_options<'py>(context: &BuildContext<'py>) -> PyResult> { let py: Python<'py> = context.py(); let request: &Bound<'py, PyAny> = &context.request; - let field = |name: &str| request.getattr(name); + let field = |name: &str| native_field(request, name); let should_load_sources: Bound<'py, PyAny> = context.get("should_load_sources")?; let python_selectors: bool = context.request_flag("include_python")? || should_load_sources.is_truthy()?; - let resolver: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ResolveExternalSqlReferenceResolver)?, - &[ - ("project_dir", context.get("effective_project_dir")?), - ("discovered_inputs", context.get("discovered_inputs")?), - ], + let resolver: Bound<'py, PyAny> = resolve_external_sql_reference_resolver( + context.get("effective_project_dir")?, + context.get("discovered_inputs")?, )?; - call_with( - ®istry::class(py, RecordId::CompilePipelineOptions)?, + new_record_with( + py, + RecordId::CompilePipelineOptions, &[ ("no_sql_validation", field("no_sql_validation")?), ("no_cache", field("no_cache")?), @@ -109,14 +111,17 @@ pub(crate) fn compile_build_plan<'py>(context: &BuildContext<'py>) -> PyResult = py.import("sys")? .getattr(if machine { "stderr" } else { "stdout" })?; - let plan_text: String = call_with( - ®istry::native(py, NativeAttr::FormatPlan)?, - &[ - ("plan", native_field(pipeline, "plan_output")?), - ( - "full_refresh", - native_field(&context.request, "full_refresh")?, - ), - ("use_color", boolean(py, context.use_color)), - ( - "python_plan_entries", - native_field(pipeline, "python_plan_entries")?, - ), - ("include_direct_freshness_diagnostics", boolean(py, false)), - ], - )? + let plan_text: String = format_plan( + py, + Some(keywords( + py, + &[ + ("plan", native_field(pipeline, "plan_output")?), + ( + "full_refresh", + native_field(&context.request, "full_refresh")?, + ), + ("use_color", boolean(py, context.use_color)), + ( + "python_plan_entries", + native_field(pipeline, "python_plan_entries")?, + ), + ("include_direct_freshness_diagnostics", boolean(py, false)), + ], + )?), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))? .extract()?; emit(&stream, &format!("\n{plan_text}\n\n")) } @@ -197,8 +206,8 @@ pub(crate) fn write_compile_target( let py: Python<'_> = context.py(); let plan: Bound<'_, PyAny> = native_field(pipeline, "plan_output")?; let manifest: Bound<'_, PyAny> = if context.request_flag("manifest")? { - call_with( - ®istry::native(py, NativeAttr::BuildManifest)?, + build_manifest(Some(&keywords( + py, &[ ("project", native_field(pipeline, "project")?), ("plan_output", plan.clone()), @@ -213,21 +222,25 @@ pub(crate) fn write_compile_target( ("upstream_deps", native_field(&plan, "upstream_deps")?), ("downstream_deps", native_field(&plan, "downstream_deps")?), ], - )? + )?)) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))? } else { none(py) }; - call_with( - ®istry::native(py, NativeAttr::WriteCompileTarget)?, - &[ - ( - "target_dir", - joined(&context.get("effective_project_dir")?, &["target"])?, - ), - ("adapter", context.get("adapter")?), - ("plan_output", plan), - ("manifest", manifest), - ], + crate::bindings::_helpers::compile_command::target_writer::write_compile_target( + py, + Some(keywords( + py, + &[ + ( + "target_dir", + joined(&context.get("effective_project_dir")?, &["target"])?, + ), + ("adapter", context.get("adapter")?), + ("plan_output", plan), + ("manifest", manifest), + ], + )?), )?; Ok(()) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/build_command/policies.rs b/crates/sqlbuild-python/src/bindings/_helpers/build_command/policies.rs index 393b729b8a..599fc01b40 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/build_command/policies.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/build_command/policies.rs @@ -56,11 +56,11 @@ pub(crate) fn build_command_error(py: Python<'_>, error: BuildCommandError) -> P } fn text(value: &Bound<'_, PyAny>, name: &str) -> PyResult { - value.getattr(name)?.str()?.extract() + native_field(value, name)?.str()?.extract() } fn optional_text(value: &Bound<'_, PyAny>, name: &str) -> PyResult> { - let attribute: Bound<'_, PyAny> = value.getattr(name)?; + let attribute: Bound<'_, PyAny> = native_field(value, name)?; if attribute.is_none() { return Ok(None); } @@ -68,7 +68,7 @@ fn optional_text(value: &Bound<'_, PyAny>, name: &str) -> PyResult, name: &str) -> PyResult { - value.getattr(name)?.is_truthy() + native_field(value, name)?.is_truthy() } fn model_migration(entry: &Bound<'_, PyAny>) -> PyResult { @@ -107,7 +107,7 @@ fn column_migration(entry: &Bound<'_, PyAny>) -> PyResult { } fn entries<'py>(plan: &Bound<'py, PyAny>, name: &str) -> PyResult>> { - plan.getattr(name)?.try_iter()?.collect() + native_field(plan, name)?.try_iter()?.collect() } /// The models a plan executes: every model entry that is not skipped. @@ -240,7 +240,7 @@ pub(crate) fn enforce_plan_policies( /// Apply every plan safety gate before a build executes anything. #[pyfunction] #[pyo3(name = "enforce_plan_safety_policies", signature = (**options))] -fn enforce_plan_safety_policies_binding<'py>( +pub(crate) fn enforce_plan_safety_policies_binding<'py>( py: Python<'py>, options: Option>, ) -> Result<(), PyErr> { @@ -264,7 +264,7 @@ fn enforce_plan_safety_policies_binding<'py>( /// Reject a build whose expanded model count exceeds its target policy. #[pyfunction] #[pyo3(name = "enforce_model_execution_limit", signature = (**options))] -fn enforce_model_execution_limit_binding<'py>( +pub(crate) fn enforce_model_execution_limit_binding<'py>( py: Python<'py>, options: Option>, ) -> Result<(), PyErr> { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/build_progress/audit_progress.rs b/crates/sqlbuild-python/src/bindings/_helpers/build_progress/audit_progress.rs index b98d01ab05..2fde1ae5fe 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/build_progress/audit_progress.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/build_progress/audit_progress.rs @@ -175,7 +175,7 @@ impl AuditProgressReporter { impl AuditProgressReporter { #[new] #[pyo3(signature = (**options))] - fn new(py: Python<'_>, options: Option>) -> PyResult { + pub(crate) fn new(py: Python<'_>, options: Option>) -> PyResult { let arguments = KeywordArguments::read( py, "AuditProgressReporter", diff --git a/crates/sqlbuild-python/src/bindings/_helpers/build_progress/build_callbacks.rs b/crates/sqlbuild-python/src/bindings/_helpers/build_progress/build_callbacks.rs index 6f5f280900..58cafc25bb 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/build_progress/build_callbacks.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/build_progress/build_callbacks.rs @@ -31,8 +31,8 @@ use crate::bindings::_helpers::build_progress::terminal::{ }; use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, imported, within}; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::NativeAttr; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::fields::replaced_keywords; +use crate::bindings::_helpers::result_output::event_writer::ExecutionEventWriter; const RUNTIME_LOGGER: &str = "sqlbuild.executor.runtime"; const PASS_OUTCOME: &str = "pass"; @@ -379,7 +379,7 @@ fn statement(telemetry: &Bound<'_, PyAny>) -> PyResult { impl BuildProgressCallbacks { #[new] #[pyo3(signature = (**options))] - fn new(py: Python<'_>, options: Option>) -> PyResult { + pub(crate) fn new(py: Python<'_>, options: Option>) -> PyResult { let arguments = KeywordArguments::read( py, "BuildProgressCallbacks", @@ -406,11 +406,11 @@ impl BuildProgressCallbacks { } else { py.None().into_bound(py) }; - let writer_options: Bound<'_, PyDict> = PyDict::new(py); - writer_options.set_item("path", path)?; - let event_writer: Bound<'_, PyAny> = - registry::native(py, NativeAttr::ExecutionEventWriter)? - .call((), Some(&writer_options))?; + let event_writer: Bound<'_, PyAny> = Bound::new( + py, + ExecutionEventWriter::new(py, Some(path).filter(|value| !value.is_none()))?, + )? + .into_any(); let facts: BuildPlanFacts = build_plan_facts(&plan)?; let projector: Option> = projector(py)?; if let Some(projector) = &projector { @@ -563,8 +563,7 @@ impl BuildProgressCallbacks { ) { let options: Bound<'_, PyDict> = PyDict::new(py); options.set_item("duration_ms", duration_ms.trunc() as i64)?; - result = - imported(py, "dataclasses", "replace")?.call((node_result,), Some(&options))?; + result = replaced_keywords(node_result, &options)?; } } let written: PyResult<()> = Self::write_node_result(slf, &result, kind); diff --git a/crates/sqlbuild-python/src/bindings/_helpers/build_progress/footer.rs b/crates/sqlbuild-python/src/bindings/_helpers/build_progress/footer.rs index 15b9afca2f..72ba57bd60 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/build_progress/footer.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/build_progress/footer.rs @@ -11,7 +11,7 @@ use crate::bindings::_helpers::build_progress::result_text::{build_text, python_ /// Render the build completion line, warnings, failures and skipped rollup. #[pyfunction] #[pyo3(signature = (*, result, elapsed, use_color, python_node_results = None))] -fn format_build_footer( +pub(crate) fn format_build_footer( result: &Bound<'_, PyAny>, elapsed: f64, use_color: bool, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/build_progress/nested_progress.rs b/crates/sqlbuild-python/src/bindings/_helpers/build_progress/nested_progress.rs index a5c341cf17..89cd2617dd 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/build_progress/nested_progress.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/build_progress/nested_progress.rs @@ -190,7 +190,7 @@ impl NestedCommandProgressCallbacks { impl NestedCommandProgressCallbacks { #[new] #[pyo3(signature = (**options))] - fn new(py: Python<'_>, options: Option>) -> PyResult { + pub(crate) fn new(py: Python<'_>, options: Option>) -> PyResult { let arguments = KeywordArguments::read( py, "NestedCommandProgressCallbacks", diff --git a/crates/sqlbuild-python/src/bindings/_helpers/build_progress/plan_facts.rs b/crates/sqlbuild-python/src/bindings/_helpers/build_progress/plan_facts.rs index 39fbc20cf0..cc4f34a535 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/build_progress/plan_facts.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/build_progress/plan_facts.rs @@ -37,7 +37,7 @@ fn row_facts(facts: Option<&ModelLabelFacts>, logical_ddl: Option) -> Mo } fn names(plan: &Bound<'_, PyAny>, field: &str) -> PyResult> { - plan.getattr(field)? + native_field(plan, field)? .try_iter()? .map(|entry| text(&entry?, "name")) .collect() diff --git a/crates/sqlbuild-python/src/bindings/_helpers/build_progress/result_text.rs b/crates/sqlbuild-python/src/bindings/_helpers/build_progress/result_text.rs index 3a2e3db12c..96bf33a0d5 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/build_progress/result_text.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/build_progress/result_text.rs @@ -18,11 +18,11 @@ const MEASUREMENT: &str = "measurement"; const FAILED: &str = "failed"; pub(crate) fn text(value: &Bound<'_, PyAny>, name: &str) -> PyResult { - Ok(value.getattr(name)?.str()?.to_string()) + Ok(native_field(value, name)?.str()?.to_string()) } pub(crate) fn optional(value: &Bound<'_, PyAny>, name: &str) -> PyResult> { - let field: Bound<'_, PyAny> = value.getattr(name)?; + let field: Bound<'_, PyAny> = native_field(value, name)?; if field.is_none() { return Ok(None); } @@ -30,15 +30,14 @@ pub(crate) fn optional(value: &Bound<'_, PyAny>, name: &str) -> PyResult, name: &str) -> PyResult> { - value - .getattr(name)? + native_field(value, name)? .try_iter()? .map(|item| Ok(item?.str()?.to_string())) .collect() } fn items<'py>(value: &Bound<'py, PyAny>, name: &str) -> PyResult>> { - value.getattr(name)?.try_iter()?.collect() + native_field(value, name)?.try_iter()?.collect() } fn duration(value: &Bound<'_, PyAny>) -> PyResult> { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/build_progress/terminal.rs b/crates/sqlbuild-python/src/bindings/_helpers/build_progress/terminal.rs index 63aaf8c68f..69a035167b 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/build_progress/terminal.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/build_progress/terminal.rs @@ -5,10 +5,9 @@ use pyo3::types::{PyBool, PyDict, PyDictMethods, PyTuple}; use pyo3::{IntoPyObjectExt, pyfunction, wrap_pyfunction}; use crate::bindings::_helpers::build_progress::transient_lines::shared_coordinator; +use crate::bindings::_helpers::cli::progress::current_native_progress_projector; use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::NativeAttr; -use crate::bindings::_helpers::records::registry; const DEFAULT_TERMINAL_SIZE: (i64, i64) = (120, 24); const SPINNER_JOIN_SECONDS: f64 = 0.2; @@ -50,8 +49,7 @@ pub(crate) fn tty_flag(stream: &Bound<'_, PyAny>) -> PyResult { /// The current native progress projector, if a projection is active. pub(crate) fn projector(py: Python<'_>) -> PyResult>> { - let projector: Bound<'_, PyAny> = - registry::native(py, NativeAttr::CurrentNativeProgressProjector)?.call0()?; + let projector: Bound<'_, PyAny> = current_native_progress_projector(py)?; Ok((!projector.is_none()).then(|| projector.unbind())) } @@ -111,7 +109,7 @@ fn stop_progress_spinners(py: Python<'_>) -> PyResult<()> { if native_field(&thread, "name")?.eq(SPINNER_THREAD_NAME)? && thread.hasattr(STOP_EVENT_ATTRIBUTE)? { - thread.getattr(STOP_EVENT_ATTRIBUTE)?.call_method0("set")?; + native_field(&thread, STOP_EVENT_ATTRIBUTE)?.call_method0("set")?; spinners.push(thread); } } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/build_progress/transient_lines.rs b/crates/sqlbuild-python/src/bindings/_helpers/build_progress/transient_lines.rs index e746a615ab..72ffe392e2 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/build_progress/transient_lines.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/build_progress/transient_lines.rs @@ -129,7 +129,7 @@ pub(crate) fn shared_coordinator(py: Python<'_>) -> PyResult, ) -> PyResult> { shared_coordinator(py) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/build_progress/transient_status.rs b/crates/sqlbuild-python/src/bindings/_helpers/build_progress/transient_status.rs index 702e84608b..2d34cf8a75 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/build_progress/transient_status.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/build_progress/transient_status.rs @@ -144,7 +144,7 @@ impl TransientStatusReporter { impl TransientStatusReporter { #[new] #[pyo3(signature = (*, stream, use_color = false, enabled = true))] - fn new( + pub(crate) fn new( py: Python<'_>, stream: &Bound<'_, PyAny>, use_color: bool, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cache/sql_test_scan_store.rs b/crates/sqlbuild-python/src/bindings/_helpers/cache/sql_test_scan_store.rs index a5d7a6f9b5..8842001e16 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cache/sql_test_scan_store.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cache/sql_test_scan_store.rs @@ -126,7 +126,7 @@ pub(crate) struct SqlTestScanCache { impl SqlTestScanCache { #[new] #[pyo3(signature = (*, cache_dir))] - fn new(cache_dir: Option) -> Self { + pub(crate) fn new(cache_dir: Option) -> Self { Self { state: Mutex::new((cache_dir, None)), } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/check/check_command.rs b/crates/sqlbuild-python/src/bindings/_helpers/check/check_command.rs index 51707dada5..5752f51a3c 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/check/check_command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/check/check_command.rs @@ -15,6 +15,7 @@ use crate::bindings::_helpers::planner_entries::command_plans::build_relation_co use crate::bindings::_helpers::planner_phases::objects::object; use crate::bindings::_helpers::planner_selection::selectors::parse_project_selector; use crate::bindings::_helpers::planner_selection::unit_tests::reject_unit_test_selectors; +use crate::bindings::_helpers::records::construct::{enum_member, new_record_with, record_class}; use crate::bindings::_helpers::result_output::documents::write_execution_json_output; use pyo3::prelude::{ Bound, Py, PyAny, PyAnyMethods, PyErr, PyModule, PyModuleMethods, PyResult, Python, @@ -87,7 +88,7 @@ fn empty_tuple(py: Python<'_>) -> Bound<'_, PyAny> { /// Refuse `+`, `~` and path selectors, splitting selectors as Python's `split` does. fn refuse_graph_selectors(py: Python<'_>, selectors: &[Bound<'_, PyAny>]) -> PyResult<()> { - let path_selector: Bound<'_, PyAny> = registry::class(py, RecordId::PathSelector)?; + let path_selector: Bound<'_, PyAny> = record_class(py, RecordId::PathSelector)?; for selector in selectors { for token in selector.call_method0("split")?.try_iter()? { for part in token?.call_method1("split", (",",))?.try_iter()? { @@ -199,9 +200,7 @@ fn compile_pipeline<'py>( } } let loaders: Bound<'py, PyFrozenSet> = PyFrozenSet::new(py, loaders)?; - let key_class: Bound<'py, PyAny> = registry::class(py, RecordId::CompiledObjectKey)?; - let source_type: Bound<'py, PyAny> = - registry::class(py, RecordId::CompiledResourceType)?.getattr("SOURCE")?; + let source_type: Bound<'py, PyAny> = enum_member(py, RecordId::CompiledResourceType, "SOURCE")?; let mut keys: Vec> = Vec::new(); for reference in required_refs.try_iter()? { keys.push(sql_ref_key(py, &reference?)?); @@ -210,8 +209,9 @@ fn compile_pipeline<'py>( for source in native_field(&source_file?, "source_entries")?.try_iter()? { let source: Bound<'py, PyAny> = source?; if loaders.contains(native_field(&source, "loader")?)? { - keys.push(call_with( - &key_class, + keys.push(new_record_with( + py, + RecordId::CompiledObjectKey, &[ ("resource_type", source_type.clone()), ("name", native_field(&source, "name")?), @@ -225,8 +225,9 @@ fn compile_pipeline<'py>( native_field(invocation, "effective_project_dir")?, discovered.clone(), )?; - let options: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::CompilePipelineOptions)?, + let options: Bound<'py, PyAny> = new_record_with( + py, + RecordId::CompilePipelineOptions, &[ ("selected_target", native_field(request, "selected_target")?), ( @@ -276,7 +277,7 @@ fn target_default<'py>( adapter: &Bound<'py, PyAny>, (field, fallback): (&str, &str), ) -> PyResult> { - let value: Bound<'py, PyAny> = project.getattr(field)?; + let value: Bound<'py, PyAny> = native_field(project, field)?; if value.is_none() { adapter.call_method0(fallback) } else { @@ -318,8 +319,9 @@ fn prepare<'py>( check_functions.push(check); } } - let selection: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::PythonSqlRunSelection)?, + let selection: Bound<'py, PyAny> = new_record_with( + py, + RecordId::PythonSqlRunSelection, &[ ("sql_keys", PyFrozenSet::empty(py)?.into_any()), ("python_node_names", PyFrozenSet::empty(py)?.into_any()), @@ -649,10 +651,10 @@ fn complete<'py>( /// Run one `sqb check` request. #[pyfunction] #[pyo3(signature = (*, request))] -fn run_check_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { +pub(crate) fn run_check_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { let invocation: Bound<'_, PyAny> = resolve_reported_command_invocation( request, - ®istry::class(py, RecordId::CheckInvocation)?, + &record_class(py, RecordId::CheckInvocation)?, true, )?; let stream: Bound<'_, PyAny> = native_field(&invocation, "progress_stream")?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/check/check_core.rs b/crates/sqlbuild-python/src/bindings/_helpers/check/check_core.rs index f0bac785b1..94e3060651 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/check/check_core.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/check/check_core.rs @@ -128,7 +128,7 @@ pub(crate) fn record_python_run_state_results<'py>( } let node_by_name: Bound<'py, PyDict> = PyDict::new(py); for attribute in ["task_functions", "asset_functions"] { - for node in discovered_inputs.getattr(attribute)?.try_iter()? { + for node in native_field(discovered_inputs, attribute)?.try_iter()? { let node: Bound<'py, PyAny> = node?; node_by_name.set_item( native_field(&node, "name")?, @@ -292,8 +292,7 @@ fn located<'py>( SEED_KIND => "seed_locations", _ => return Ok(None), }; - let location: Bound<'py, PyAny> = plan_output - .getattr(attribute)? + let location: Bound<'py, PyAny> = native_field(plan_output, attribute)? .call_method1("get", (native_field(reference, "name")?,))?; Ok((!location.is_none()).then_some(location)) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/check/check_results.rs b/crates/sqlbuild-python/src/bindings/_helpers/check/check_results.rs index c1a1622a0c..ea94881054 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/check/check_results.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/check/check_results.rs @@ -197,7 +197,10 @@ pub(crate) fn results_json(results: &[Bound<'_, PyAny>]) -> PyResult { /// Write human-readable check result rows. #[pyfunction] #[pyo3(signature = (**options))] -fn write_check_results(py: Python<'_>, options: Option>) -> PyResult<()> { +pub(crate) fn write_check_results( + py: Python<'_>, + options: Option>, +) -> PyResult<()> { let arguments: KeywordArguments<'_> = KeywordArguments::read( py, "write_check_results", diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cli/argv.rs b/crates/sqlbuild-python/src/bindings/_helpers/cli/argv.rs index 2d50199dca..d1b06b4437 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cli/argv.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cli/argv.rs @@ -13,10 +13,9 @@ use sqlbuild_core::text::main::python_str_repr::python_str_repr; use crate::bindings::_helpers::cli::namespace::new_cli_namespace; use crate::bindings::_helpers::cli::surrogates::{python_text, rust_text, uses_placeholders}; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; +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; -use crate::bindings::_helpers::records::registry; const INT_CONVERTER: &str = "int"; const POSITIVE_INTEGER_CONVERTER: &str = "_positive_integer"; @@ -215,7 +214,7 @@ fn parse_cli_arguments( Stream::Stdout => "stdout", Stream::Stderr => "stderr", }; - let file: Bound<'_, PyAny> = sys.getattr(name)?; + let file: Bound<'_, PyAny> = native_field(&sys, name)?; if file.is_none() { continue; } @@ -258,10 +257,10 @@ pub(crate) fn parse_cli_invocation<'py>( .extract()?; let settings: ArgvSettings = ArgvSettings { columns, use_color }; let (rows, exit_code): ParsedRows = parse_cli_arguments(&argv, &defaults, settings)?; - let parsed: Bound<'py, PyAny> = registry::class(py, RecordId::ParsedCliInvocation)?; let Some(rows) = rows else { - return call_with( - &parsed, + return new_record_with( + py, + RecordId::ParsedCliInvocation, &[ ("args", py.None().into_bound(py)), ("exit_code", exit_code.into_bound_py_any(py)?), @@ -271,8 +270,9 @@ pub(crate) fn parse_cli_invocation<'py>( for (name, value) in rows { args.setattr(name.as_str(), value)?; } - call_with( - &parsed, + new_record_with( + py, + RecordId::ParsedCliInvocation, &[("args", args), ("exit_code", py.None().into_bound(py))], ) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cli/dispatch.rs b/crates/sqlbuild-python/src/bindings/_helpers/cli/dispatch.rs index 33e2a625d5..af30bf6975 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cli/dispatch.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cli/dispatch.rs @@ -2,10 +2,43 @@ use std::path::PathBuf; +use crate::bindings::_helpers::audit_command::command::run_audit_command; +use crate::bindings::_helpers::build_command::command::run_build_command; +use crate::bindings::_helpers::check::check_command::run_check_command; use crate::bindings::_helpers::cli::launch_state::withhold_launch_record; +use crate::bindings::_helpers::clone::clone_command::run_clone_command; use crate::bindings::_helpers::command_runtime::warehouse_scope::command_warehouse_scope; +use crate::bindings::_helpers::command_runtime::warehouses::validate_cli_warehouse; +use crate::bindings::_helpers::compile_command::reuse_attempt::run_compile; +use crate::bindings::_helpers::contract_command::command::run_contract_command; +use crate::bindings::_helpers::cost_command::command::run_cost_command; +use crate::bindings::_helpers::dbt_command::command::run_dbt_command; +use crate::bindings::_helpers::dbt_command::init::run_dbt_init_command; +use crate::bindings::_helpers::debug_command::run_debug::run_debug_command; +use crate::bindings::_helpers::diff_report::command::run_diff_command; use crate::bindings::_helpers::diff_report::support::cli_error; +use crate::bindings::_helpers::format_command::command::run_format_command; +use crate::bindings::_helpers::freshness::freshness_command::run_freshness_command; +use crate::bindings::_helpers::init_command::command::run_init_command; +use crate::bindings::_helpers::janitor::janitor_command::run_janitor_command; +use crate::bindings::_helpers::lineage::lineage_command::run_lineage_command; +use crate::bindings::_helpers::load::load_command::run_load_command; +use crate::bindings::_helpers::plan_command::plan::run_plan_command; +use crate::bindings::_helpers::plan_command::rules::run_rules_command; +use crate::bindings::_helpers::query_command::dag::run_dag_command; +use crate::bindings::_helpers::query_command::query::run_query_command; +use crate::bindings::_helpers::records::construct::{new_record_with, record_class}; +use crate::bindings::_helpers::refactor::refactor_command::run_refactor_command; +use crate::bindings::_helpers::scenario_command::capture_command::run_scenario_capture_command; +use crate::bindings::_helpers::scenario_command::test_command::run_scenario_test_command; +use crate::bindings::_helpers::scopes::scope_command::run_scope_command; +use crate::bindings::_helpers::seed_command::command::run_seed_command; +use crate::bindings::_helpers::sql_test_command::test_command::run_test_command; +use crate::bindings::_helpers::workspace_commands::commands::{ + run_playground_command, run_skills_command, +}; use pyo3::IntoPyObjectExt; +use pyo3::exceptions::{PyAttributeError, PyTypeError}; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyErr, PyResult, Python}; use pyo3::types::{PyDict, PyDictMethods, PyString, PyTuple}; use sqlbuild_cli::diff_report::errors::DiffUserError; @@ -18,9 +51,9 @@ use sqlbuild_cli::dispatch::models::{ use sqlbuild_core::format_command::main::format_path_selectors::format_path_selectors; use sqlbuild_core::text::main::python_strip::python_strip; -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported, within}; +use crate::bindings::_helpers::compile_driver::python_calls::{imported, within}; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr, RecordId}; +use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; const PATHLIB_MODULE: &str = "pathlib"; @@ -176,7 +209,7 @@ fn path_class(py: Python<'_>) -> PyResult> { } fn class_object<'py>(py: Python<'py>, class: ClassRef) -> PyResult> { - registry::class(py, registry::record_named(class.name)?) + record_class(py, registry::record_named(class.name)?) } fn tuple_of<'py>(value: &Bound<'py, PyAny>) -> PyResult> { @@ -217,7 +250,6 @@ fn selector_inputs<'py>( .getattr("select")? .try_iter()? .collect::>()?; - let summary_class: Bound<'py, PyAny> = registry::class(py, RecordId::SelectorFileSummary)?; let mut files: Vec> = Vec::new(); for raw_path in native_field(args, "select_file")?.try_iter()? { let path: Bound<'py, PyAny> = path_class(py)?.call1((raw_path?,))?; @@ -236,8 +268,9 @@ fn selector_inputs<'py>( selectors.push(PyString::new(py, stripped).into_any()); count += 1; } - files.push(call_with( - &summary_class, + files.push(new_record_with( + py, + RecordId::SelectorFileSummary, &[ ("path", path), ("selector_count", count.into_bound_py_any(py)?), @@ -256,19 +289,17 @@ fn call_route<'py>( let py: Python<'py> = invocation.py(); let mut positional: Vec> = Vec::new(); let keywords: Bound<'py, PyDict> = PyDict::new(py); - let (callable, positional_count) = match handler { - Some(handler) => (handler, route.shape.slot_positional), + let positional_count: usize = match &handler { + Some(_) => route.shape.slot_positional, None => { - let function: NativeAttr = registry::attribute_named(route.implementation)?; if let Some(extra) = route.implementation_extra { keywords.set_item(extra.name, invocation.value(extra.source)?)?; } - let count: usize = if route.shape.implementation_keywords { + if route.shape.implementation_keywords { 0 } else { route.shape.slot_positional - }; - (registry::native(py, function)?, count) + } } }; for (index, field) in route.arguments.iter().enumerate() { @@ -279,7 +310,79 @@ fn call_route<'py>( keywords.set_item(field.name, value)?; } } - callable.call(PyTuple::new(py, positional)?, Some(&keywords)) + match handler { + Some(handler) => handler.call(PyTuple::new(py, positional)?, Some(&keywords)), + None => run_implementation(route.implementation, (positional, keywords)), + } +} + +/// The argument passed first by position or under `name`, or `None`. +fn argument<'py>( + (positional, keywords): &(Vec>, Bound<'py, PyDict>), + name: &str, +) -> PyResult>> { + match positional.first() { + Some(value) => Ok(Some(value.clone())), + None => keywords.get_item(name), + } +} + +/// Run the native command implementation `name` with the route's arguments. +fn run_implementation<'py>( + name: &str, + arguments: (Vec>, Bound<'py, PyDict>), +) -> PyResult> { + let py: Python<'py> = arguments.1.py(); + let request = || -> PyResult> { + argument(&arguments, "request")? + .ok_or_else(|| PyTypeError::new_err(format!("{name}() missing argument 'request'"))) + }; + let options = || Some(arguments.1.clone()); + let code: i32 = match name { + "run_audit_command" => run_audit_command(py, &request()?)?, + "run_build_command" => run_build_command(py, &request()?)?, + "run_check_command" => run_check_command(py, &request()?)?, + "run_clone_command" => run_clone_command(py, &request()?)?, + "run_compile" => return run_compile(&request()?), + "run_contract_command" => run_contract_command(py, &request()?)?, + "run_cost_command" => run_cost_command(py, &request()?)?, + "run_dag_command" => run_dag_command(py, options())?, + "run_dbt_command" => run_dbt_command(py, options())?, + "run_dbt_init_command" => run_dbt_init_command(py, &request()?)?, + "run_debug_command" => run_debug_command(py, options())?, + "run_diff_command" => run_diff_command(py, &request()?)?, + "run_format_command" => run_format_command(py, options())?, + "run_freshness_command" => run_freshness_command(py, &request()?)?, + "run_init_command" => run_init_command( + py, + argument(&arguments, "project_dir")?.filter(|value| !value.is_none()), + )?, + "run_janitor_command" => run_janitor_command(py, &request()?)?, + "run_lineage_command" => run_lineage_command(py, &request()?)?, + "run_load_command" => run_load_command(py, &request()?)?, + "run_plan_command" => run_plan_command(&request()?)?, + "run_playground_command" => run_playground_command(py, &request()?)?, + "run_query_command" => run_query_command(py, options())?, + "run_refactor_command" => run_refactor_command(py, &request()?)?, + "run_rules_command" => run_rules_command(&request()?)?, + "run_scenario_capture_command" => run_scenario_capture_command(py, &request()?)?, + "run_scenario_test_command" => run_scenario_test_command(py, &request()?)?, + "run_scope_command" => run_scope_command( + py, + &request()?, + arguments.1.get_item("load_scope_index")?, + arguments.1.get_item("output_stream")?, + )?, + "run_seed_command" => run_seed_command(py, &request()?)?, + "run_skills_command" => run_skills_command(py, options())?, + "run_test_command" => run_test_command(py, &request()?)?, + _ => { + return Err(PyAttributeError::new_err(format!( + "module 'sqlbuild._native' has no attribute '{name}'" + ))); + } + }; + code.into_bound_py_any(py) } fn routed_command<'py>( @@ -345,13 +448,12 @@ pub(crate) fn dispatch_command<'py>( .call1((raw_project_dir,))? .call_method0("resolve")? }; - call_with( - ®istry::native(py, NativeAttr::ValidateCliWarehouse)?, - &[ - ("cli_warehouse", warehouse.clone()), - ("project_dir", project_dir), - ], - )?; + validate_cli_warehouse( + py, + warehouse.clone().extract::>()?, + project_dir, + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; } let scope: Bound<'py, PyAny> = Bound::new( py, @@ -367,7 +469,7 @@ pub(crate) fn dispatch_command<'py>( /// The parsed values that select a route, read from the namespace. pub(crate) fn command_facts(args: &Bound<'_, PyAny>) -> PyResult { - let attr = |name: &str| args.getattr(name); + let attr = |name: &str| native_field(args, name); let flag = |name: &str| -> PyResult { attr(name)?.is_truthy() }; let present = |name: &str| -> PyResult { Ok(!attr(name)?.is_none()) }; let command: Option = optional_text(&attr("command")?)?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cli/entry.rs b/crates/sqlbuild-python/src/bindings/_helpers/cli/entry.rs index e6a518878e..c1d76e4726 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cli/entry.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cli/entry.rs @@ -12,19 +12,26 @@ use sqlbuild_cli::dispatch::main::failure_report::failure_report; use sqlbuild_cli::dispatch::main::invocation_policy::invocation_policy; use sqlbuild_cli::dispatch::models::{CommandFacts, CommandFailure, InvocationPolicy}; +use crate::bindings::_helpers::build_progress::transient_lines::shared_transient_line_coordinator; use crate::bindings::_helpers::cli::argv::parse_cli_invocation; use crate::bindings::_helpers::cli::dispatch::{command_facts, dispatch_command}; use crate::bindings::_helpers::cli::launch_state::offer_launch_record; use crate::bindings::_helpers::cli::observability::cli_observability_scope; -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported, within}; +use crate::bindings::_helpers::compile_command::report::format_compile_error_json; +use crate::bindings::_helpers::compile_driver::python_calls::{ + call_with, imported, keywords, within, +}; +use crate::bindings::_helpers::diff_report::registration::render_diff_error_json; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::facade::FacadeHost; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::output_capture::records::invocation_context; use crate::bindings::_helpers::presentation_text::terminal::stdout_supports_color; +use crate::bindings::_helpers::records::construct::record_class; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr, RecordId}; +use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::workspace_commands::commands::maintain_sqlbuild_skills; use sqlbuild_cli::launcher::constants::LAUNCH_RECORD_OPTION; use sqlbuild_core::cli_text::main::format_coded_error::format_coded_error; use sqlbuild_core::cli_text::models::CliStyle; @@ -164,20 +171,18 @@ fn write_compile_machine_error( .getattr_opt("location")? .unwrap_or_else(|| py.None().into_bound(py)); let location: Bound<'_, PyAny> = - if location.is_instance(®istry::class(py, RecordId::SourceLocation)?)? { + if location.is_instance(&record_class(py, RecordId::SourceLocation)?)? { location } else { py.None().into_bound(py) }; - let document: Bound<'_, PyAny> = call_with( - ®istry::native(py, NativeAttr::FormatCompileErrorJson)?, - &[ - ("code", code), - ("message", message), - ("help_text", help), - ("location", location), - ], - )?; + let document: Bound<'_, PyAny> = format_compile_error_json( + code, + message, + help, + Some(location).filter(|value| !value.is_none()), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; print_object(&document, None, None) } @@ -185,10 +190,8 @@ fn render_diff_error<'py>( py: Python<'py>, fields: &[(&str, Bound<'py, PyAny>)], ) -> PyResult> { - call_with( - ®istry::native(py, NativeAttr::RenderDiffErrorJson)?, - fields, - ) + render_diff_error_json(py, Some(keywords(py, fields)?)) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py)) } /// The diff JSON error document, and the publication error that replaces `error` if any. @@ -352,22 +355,20 @@ fn report_skill_freshness(invocation: &Invocation<'_>, policy: InvocationPolicy) } let py: Python<'_> = invocation.py(); let project_dir: Bound<'_, PyAny> = invocation.project_dir()?; - let result: Bound<'_, PyAny> = match call_with( - ®istry::native(py, NativeAttr::MaintainSqlbuildSkills)?, - &[("project_dir", project_dir)], - ) { - Ok(result) => result, - Err(error) => { - let value: Bound<'_, PyAny> = error.value(py).clone().into_any(); - if value.is_instance(®istry::error(py, ErrorId::CliUser)?)? - || value.is_instance_of::() - || value.is_instance_of::() - { - return Ok(()); + let result: Bound<'_, PyAny> = + match maintain_sqlbuild_skills(py, project_dir.extract::()?) { + Ok(result) => result, + Err(error) => { + let value: Bound<'_, PyAny> = error.value(py).clone().into_any(); + if value.is_instance(®istry::error(py, ErrorId::CliUser)?)? + || value.is_instance_of::() + || value.is_instance_of::() + { + return Ok(()); + } + return Err(error); } - return Err(error); - } - }; + }; let message: Bound<'_, PyAny> = native_field(&result, "message")?; if message.is_truthy()? { print_object(&message, Some("stderr"), Some(""))?; @@ -453,8 +454,8 @@ fn handled_dispatch<'py>(invocation: &Invocation<'py>) -> PyResult> = observed_dispatch(invocation, policy); - registry::native(py, NativeAttr::SharedTransientLineCoordinator)? - .call0()? + shared_transient_line_coordinator(py) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))? .call_method0("clear_active")?; let error: PyErr = match dispatched { Ok(exit_code) => return Ok(exit_code), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cli/launch_state.rs b/crates/sqlbuild-python/src/bindings/_helpers/cli/launch_state.rs index 78d618c392..5f17fdc86d 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cli/launch_state.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cli/launch_state.rs @@ -3,8 +3,8 @@ use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; use pyo3::types::{PyBool, PyDict, PyDictMethods}; -use crate::bindings::_helpers::records::ids::NativeAttr; -use crate::bindings::_helpers::records::registry; +use pyo3::Py; +use pyo3::sync::PyOnceLock; /// The module attributes holding the offered path's and the withheld flag's `ContextVar`s. const OFFERED_ATTRIBUTE: &str = "_launch_record_offered"; @@ -12,29 +12,30 @@ const WITHHELD_ATTRIBUTE: &str = "_launch_record_withheld"; const OFFERED_NAME: &str = "sqlbuild_launch_record"; const WITHHELD_NAME: &str = "sqlbuild_launch_record_withheld"; +/// The offered path's and the withheld flag's `ContextVar`s. +static OFFERED: PyOnceLock> = PyOnceLock::new(); +static WITHHELD: PyOnceLock> = PyOnceLock::new(); + /// Remember where `sqb` looks for this invocation's launch record, or that it offered none. pub(crate) fn offer_launch_record(py: Python<'_>, path: Option>) -> PyResult<()> { let path: Bound<'_, PyAny> = path.unwrap_or_else(|| py.None().into_bound(py)); - registry::native(py, NativeAttr::LaunchRecordOffered)?.call_method1("set", (path,))?; - registry::native(py, NativeAttr::LaunchRecordWithheld)?.call_method1("set", (false,))?; + offered(py)?.call_method1("set", (path,))?; + withheld(py)?.call_method1("set", (false,))?; Ok(()) } /// Withhold the launch record: `sqb` cannot replay what this invocation also depends on. pub(crate) fn withhold_launch_record(py: Python<'_>) -> PyResult<()> { - registry::native(py, NativeAttr::LaunchRecordWithheld)?.call_method1("set", (true,))?; + withheld(py)?.call_method1("set", (true,))?; Ok(()) } /// The offered launch record path, unless an input outside its key was read. pub(crate) fn launch_record_path(py: Python<'_>) -> PyResult> { - if registry::native(py, NativeAttr::LaunchRecordWithheld)? - .call_method0("get")? - .is_truthy()? - { + if withheld(py)?.call_method0("get")?.is_truthy()? { return Ok(py.None().into_bound(py)); } - registry::native(py, NativeAttr::LaunchRecordOffered)?.call_method0("get") + offered(py)?.call_method0("get") } fn context_variable<'py>( @@ -49,18 +50,31 @@ fn context_variable<'py>( .call((name,), Some(&keywords)) } +fn offered(py: Python<'_>) -> PyResult> { + Ok(OFFERED + .get_or_try_init(py, || { + context_variable(py, OFFERED_NAME, py.None().into_bound(py)).map(Bound::unbind) + })? + .bind(py) + .clone()) +} + +fn withheld(py: Python<'_>) -> PyResult> { + Ok(WITHHELD + .get_or_try_init(py, || { + context_variable( + py, + WITHHELD_NAME, + PyBool::new(py, false).to_owned().into_any(), + ) + .map(Bound::unbind) + })? + .bind(py) + .clone()) +} + pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> { let py: Python<'_> = module.py(); - module.add( - OFFERED_ATTRIBUTE, - context_variable(py, OFFERED_NAME, py.None().into_bound(py))?, - )?; - module.add( - WITHHELD_ATTRIBUTE, - context_variable( - py, - WITHHELD_NAME, - PyBool::new(py, false).to_owned().into_any(), - )?, - ) + module.add(OFFERED_ATTRIBUTE, offered(py)?)?; + module.add(WITHHELD_ATTRIBUTE, withheld(py)?) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cli/namespace.rs b/crates/sqlbuild-python/src/bindings/_helpers/cli/namespace.rs index 777b09895a..ef49a74deb 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cli/namespace.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cli/namespace.rs @@ -7,8 +7,8 @@ use pyo3::{IntoPyObjectExt, pyclass, pymethods}; use sqlbuild_cli::argv::main::cli_namespace_defaults::cli_namespace_defaults; use sqlbuild_cli::argv::models::NamespaceDefault; +use crate::bindings::_helpers::records::construct::enum_member; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; /// Typed namespace for all CLI arguments across all commands. #[pyclass(module = "sqlbuild._native", dict, frozen)] @@ -23,11 +23,9 @@ fn default_object(py: Python<'_>, default: NamespaceDefault) -> PyResult Ok(PyList::empty(py).into_any()), NamespaceDefault::EmptyDict => Ok(PyDict::new(py).into_any()), NamespaceDefault::CompileLineageFast => { - registry::class(py, RecordId::CompileLineageMode)?.getattr("FAST") - } - NamespaceDefault::ColumnLineageRich => { - registry::class(py, RecordId::ColumnLineageMode)?.getattr("RICH") + enum_member(py, RecordId::CompileLineageMode, "FAST") } + NamespaceDefault::ColumnLineageRich => enum_member(py, RecordId::ColumnLineageMode, "RICH"), } } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cli/observability.rs b/crates/sqlbuild-python/src/bindings/_helpers/cli/observability.rs index bfeebfe9b0..b6b8066a02 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cli/observability.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cli/observability.rs @@ -14,14 +14,19 @@ use sqlbuild_cli::dispatch::models::{ExportSummary, ExporterLoss}; use sqlbuild_core::event_export::constants::DEFAULT_EVENT_EXPORT_SHUTDOWN_TIMEOUT_SECONDS; use crate::bindings::_helpers::cli::launch_state::withhold_launch_record; -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; +use crate::bindings::_helpers::cli::progress::NativeProgressProjector; +use crate::bindings::_helpers::cli::progress::machine_output_mode; +use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported, keywords}; +use crate::bindings::_helpers::event_exporting::dispatcher::EventExporterDispatcher; +use crate::bindings::_helpers::event_exporting::scope::EventExporterCommandScope; +use crate::bindings::_helpers::event_exporting::scope::event_exporter_command_scope; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::native_host::user_code::UserCodeHost; use crate::bindings::_helpers::presentation_text::terminal::stdout_supports_color; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::NativeAttr; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::result_output::terminal_index::TerminalEventIndex; +use crate::bindings::_helpers::result_output::terminal_index::terminal_event_index_scope; const OBSERVABILITY_LOGGER: &str = "sqlbuild.cli.observability"; const DEBUG_COMMAND: &str = "debug"; @@ -97,7 +102,7 @@ fn log_exporter_summary(summary: &Bound<'_, PyAny>) -> PyResult<()> { let py: Python<'_> = summary.py(); let mut fields: Vec<(&str, Bound<'_, PyAny>)> = Vec::new(); for name in COUNT_FIELDS.into_iter().chain(SUMMARY_FIELDS) { - fields.push((name, summary.getattr(name)?)); + fields.push((name, native_field(summary, name)?)); } log_debug(py, "Event exporter delivery summary", &fields)?; for exporter in native_field(summary, "per_exporter")?.try_iter()? { @@ -106,10 +111,10 @@ fn log_exporter_summary(summary: &Bound<'_, PyAny>) -> PyResult<()> { let mut fields: Vec<(&str, Bound<'_, PyAny>)> = vec![("exporter_name", native_field(&exporter, "exporter_name")?)]; for name in COUNT_FIELDS { - fields.push((name, counts.getattr(name)?)); + fields.push((name, native_field(&counts, name)?)); } for name in SUMMARY_FIELDS { - fields.push((name, summary.getattr(name)?)); + fields.push((name, native_field(summary, name)?)); } log_debug(py, "Event exporter accounting", &fields)?; } @@ -340,24 +345,30 @@ impl Observed { timeout.extract::()? }; let timeout: Bound<'_, PyAny> = timeout.into_bound_py_any(py)?; - let delivery: Bound<'_, PyAny> = call_with( - ®istry::native(py, NativeAttr::EventExporterDispatcher)?, - &[ - ("shutdown_timeout_seconds", timeout), - ( - "failure_callback", - wrap_pyfunction!(log_exporter_failure, py)?.into_any(), - ), - ( - "summary_callback", - wrap_pyfunction!(log_exporter_summary, py)?.into_any(), - ), - ], - )?; - let scope: Bound<'_, PyAny> = call_with( - ®istry::native(py, NativeAttr::EventExporterCommandScope)?, - &[("dispatcher", delivery.clone())], - )?; + let delivery: Bound<'_, PyAny> = EventExporterDispatcher::new( + py, + Some(keywords( + py, + &[ + ("shutdown_timeout_seconds", timeout), + ( + "failure_callback", + wrap_pyfunction!(log_exporter_failure, py)?.into_any(), + ), + ( + "summary_callback", + wrap_pyfunction!(log_exporter_summary, py)?.into_any(), + ), + ], + )?), + ) + .and_then(|value| pyo3::Bound::new(py, value)) + .map(pyo3::Bound::into_any)?; + let scope: Bound<'_, PyAny> = pyo3::Bound::new( + py, + EventExporterCommandScope::new(delivery.clone().unbind()), + ) + .map(pyo3::Bound::into_any)?; self.exporter_scope = Some(scope.clone().unbind()); call_with( &native_field(&scope, "configure_extensions")?, @@ -378,10 +389,10 @@ impl Observed { self.enter( host::entry(py, ExecutorHost::DispatcherScope)?.call1((self.dispatcher.bind(py),))?, )?; - self.enter( - registry::native(py, NativeAttr::TerminalEventIndexScopeFunction)? - .call1((terminal_index,))?, - )?; + self.enter(pyo3::IntoPyObjectExt::into_bound_py_any( + terminal_event_index_scope(terminal_index.extract::>()?), + py, + )?)?; let Some(scope) = self .exporter_scope .as_ref() @@ -389,10 +400,10 @@ impl Observed { else { return Ok(()); }; - self.enter( - registry::native(py, NativeAttr::EventExporterCommandScopeFunction)? - .call1((&scope,))?, - )?; + self.enter(pyo3::IntoPyObjectExt::into_bound_py_any( + event_exporter_command_scope(scope.clone().unbind()), + py, + )?)?; let identity: Bound<'_, PyAny> = host::entry(py, ExecutorHost::CurrentExecutionIdentity)?.call0()?; if !identity.is_none() && command_output_sinks { @@ -439,8 +450,9 @@ impl CliObservabilityScope { wrap_pyfunction!(log_dispatch_failure, py)?.into_any(), )], )?; - let terminal_index: Bound<'_, PyAny> = - registry::native(py, NativeAttr::TerminalEventIndex)?.call0()?; + let terminal_index: Bound<'_, PyAny> = TerminalEventIndex::new(py) + .and_then(|value| pyo3::Bound::new(py, value)) + .map(pyo3::Bound::into_any)?; let subscribe = |subscriber: Bound<'_, PyAny>| -> PyResult> { Ok(call_with( &native_field(&dispatcher, "subscribe_lifecycle")?, @@ -456,8 +468,8 @@ impl CliObservabilityScope { }; let unsubscribe_terminal_index: Py = subscribe(native_field(&terminal_index, "consume")?)?; - let machine_output: bool = registry::native(py, NativeAttr::MachineOutputMode)? - .call1((args,))? + let machine_output: bool = machine_output_mode(args) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))? .is_truthy()?; let debug: Bound<'_, PyAny> = native_field(args, "debug")?; let progress_on_stderr: bool = machine_output @@ -466,24 +478,21 @@ impl CliObservabilityScope { let use_color: bool = !machine_output && !native_field(args, "no_color")?.is_truthy()? && stdout_supports_color(py)?; - let projector: Bound<'_, PyAny> = call_with( - ®istry::native(py, NativeAttr::NativeProgressProjector)?, - &[ - ( - "stream", - py.import("sys")?.getattr(if progress_on_stderr { - "stderr" - } else { - "stdout" - })?, - ), - ( - "use_color", - PyBool::new(py, use_color).to_owned().into_any(), - ), - ("debug", debug), - ], - )?; + let projector: Bound<'_, PyAny> = NativeProgressProjector::new( + py, + py.import("sys")?.getattr(if progress_on_stderr { + "stderr" + } else { + "stdout" + })?, + PyBool::new(py, use_color) + .to_owned() + .into_any() + .extract::()?, + debug.extract::()?, + ) + .and_then(|value| pyo3::Bound::new(py, value)) + .map(pyo3::Bound::into_any)?; let unsubscribe_progress: Py = subscribe(native_field(&projector, "consume")?)?; let projector_token: Bound<'_, PyAny> = projector.call_method0("install")?; let mut observed: Observed = Observed { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cli/progress.rs b/crates/sqlbuild-python/src/bindings/_helpers/cli/progress.rs index 5e9a2722f7..bbebc961d0 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cli/progress.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cli/progress.rs @@ -4,6 +4,7 @@ use std::sync::{Mutex, MutexGuard}; use pyo3::exceptions::PyRuntimeError; use pyo3::prelude::{Bound, Py, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; +use pyo3::sync::PyOnceLock; use pyo3::types::{PyBool, PyDict, PyDictMethods, PyFloat, PyInt, PyMapping, PyString}; use pyo3::{pyclass, pyfunction, pymethods, wrap_pyfunction}; use sqlbuild_core::progress::main::claim_progress_terminal::claim_progress_terminal; @@ -16,14 +17,16 @@ use sqlbuild_core::progress::models::{ }; use sqlbuild_core::terminal_events::models::TerminalRequest; +use crate::bindings::_helpers::build_progress::transient_lines::shared_transient_line_coordinator; use crate::bindings::_helpers::compile_driver::python_calls::KeywordArguments; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::NativeAttr; -use crate::bindings::_helpers::records::registry; /// The module attribute holding the `ContextVar` of the installed projector. const CONTEXT_ATTRIBUTE: &str = "_native_progress_projector_context"; const CONTEXT_NAME: &str = "sqlbuild_native_progress_projector"; + +/// The `ContextVar` holding the installed projector. +static CONTEXT: PyOnceLock> = PyOnceLock::new(); const CLAIM_OPTIONS: (&[&str], &[&str]) = ( &["resource_name"], &["resource_id", "resource_attempt_id", "attempt_number"], @@ -116,7 +119,7 @@ fn progress_payload(payload: &Bound<'_, PyAny>) -> PyResult { } fn optional_text(event: &Bound<'_, PyAny>, name: &str) -> PyResult> { - event.getattr(name)?.extract() + native_field(event, name)?.extract() } /// The facts of one lifecycle event that progress projection reads. @@ -134,7 +137,8 @@ fn progress_event(event: &Bound<'_, PyAny>) -> PyResult { /// The coordinator shared by every terminal writer in this process. pub(crate) fn shared_coordinator(py: Python<'_>) -> PyResult> { - registry::native(py, NativeAttr::SharedTransientLineCoordinator)?.call0() + shared_transient_line_coordinator(py) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py)) } fn persist(coordinator: &Bound<'_, PyAny>, stream: &Bound<'_, PyAny>, text: &str) -> PyResult<()> { @@ -214,7 +218,7 @@ fn terminal_request( impl NativeProgressProjector { #[new] #[pyo3(signature = (*, stream, use_color, debug = false))] - fn new( + pub(crate) fn new( py: Python<'_>, stream: Bound<'_, PyAny>, use_color: bool, @@ -310,7 +314,19 @@ impl NativeProgressProjector { } fn context_variable(py: Python<'_>) -> PyResult> { - registry::native(py, NativeAttr::NativeProgressProjectorContext) + Ok(CONTEXT + .get_or_try_init(py, || { + let keywords: Bound<'_, PyDict> = PyDict::new(py); + keywords.set_item("default", py.None())?; + Ok::<_, pyo3::PyErr>( + py.import("contextvars")? + .getattr("ContextVar")? + .call((CONTEXT_NAME,), Some(&keywords))? + .unbind(), + ) + })? + .bind(py) + .clone()) } /// Return the projector installed for the current CLI invocation. @@ -321,7 +337,7 @@ pub(crate) fn current_native_progress_projector(py: Python<'_>) -> PyResult) -> PyResult { +pub(crate) fn machine_output_mode(args: &Bound<'_, PyAny>) -> PyResult { let mut dbt_args: Vec = Vec::new(); for arg in native_field(args, "dbt_args")?.try_iter()? { dbt_args.push(arg?.str()?.extract()?); @@ -337,14 +353,7 @@ fn machine_output_mode(args: &Bound<'_, PyAny>) -> PyResult { pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> { let py: Python<'_> = module.py(); - let keywords: Bound<'_, PyDict> = PyDict::new(py); - keywords.set_item("default", py.None())?; - module.add( - CONTEXT_ATTRIBUTE, - py.import("contextvars")? - .getattr("ContextVar")? - .call((CONTEXT_NAME,), Some(&keywords))?, - )?; + module.add(CONTEXT_ATTRIBUTE, context_variable(py)?)?; module.add_class::()?; module.add_function(wrap_pyfunction!(machine_output_mode, module)?)?; module.add_function(wrap_pyfunction!(current_native_progress_projector, module)?) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cli/progress_reporters.rs b/crates/sqlbuild-python/src/bindings/_helpers/cli/progress_reporters.rs index ac240e891f..036656306a 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cli/progress_reporters.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cli/progress_reporters.rs @@ -218,7 +218,7 @@ pub(crate) struct DbtInitProgressReporter { impl DbtInitProgressReporter { #[new] #[pyo3(signature = (*, stream, use_color))] - fn new(stream: &Bound<'_, PyAny>, use_color: bool) -> PyResult { + pub(crate) fn new(stream: &Bound<'_, PyAny>, use_color: bool) -> PyResult { Ok(Self { stream: stream.clone().unbind(), status: status_reporter(stream, use_color)?.unbind(), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cli/progress_text.rs b/crates/sqlbuild-python/src/bindings/_helpers/cli/progress_text.rs index 33f9dc3f11..29c1214b2f 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cli/progress_text.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cli/progress_text.rs @@ -29,7 +29,11 @@ const HEADER_OPTIONS: (&[&str], &[&str]) = ( /// Format command and execution context for a progress header. #[pyfunction] #[pyo3(signature = (*, command, target, concurrency))] -fn format_execution_header(command: &str, target: Option<&str>, concurrency: i64) -> String { +pub(crate) fn format_execution_header( + command: &str, + target: Option<&str>, + concurrency: i64, +) -> String { format_header(command, target, concurrency) } @@ -57,7 +61,7 @@ pub(crate) fn write_execution_header( /// Format a comparison result name as a user-facing expectation name. #[pyfunction] -fn format_expectation_name(model_name: &str) -> String { +pub(crate) fn format_expectation_name(model_name: &str) -> String { format_name(model_name) } @@ -95,7 +99,7 @@ pub(crate) fn expectation_step(step: &Bound<'_, PyAny>) -> PyResult) -> PyResult { +pub(crate) fn format_expectation_detail(step_result: &Bound<'_, PyAny>) -> PyResult { Ok(format_detail(&expectation_step(step_result)?)) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/clone/clone_command.rs b/crates/sqlbuild-python/src/bindings/_helpers/clone/clone_command.rs index 0861ce3202..162fa825b8 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/clone/clone_command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/clone/clone_command.rs @@ -18,21 +18,31 @@ use sqlbuild_core::clone_text::main::validate_clone_policy::validate_clone_polic use sqlbuild_core::clone_text::main::validate_clone_target_names::validate_clone_target_names; use sqlbuild_core::clone_text::models::CloneItemText; +use crate::bindings::_helpers::cli::progress_reporters::PlanningProgressReporter; use crate::bindings::_helpers::clone::clone_execution::{ destination_entry_count, execute_clone_plan, resource_types_by_name, }; use crate::bindings::_helpers::clone::clone_pipeline::{ClonePipeline, prepared_clone_pipeline}; use crate::bindings::_helpers::clone::clone_progress::{clone_item_text, write_line}; use crate::bindings::_helpers::command_runtime::adapters::resolve_cli_adapter; +use crate::bindings::_helpers::command_runtime::connection::resolve_target_connection_config; use crate::bindings::_helpers::compile_driver::discovery::discover_project_inputs; use crate::bindings::_helpers::compile_driver::python_calls::keywords; use crate::bindings::_helpers::compile_driver::python_calls::{call_with, within}; +use crate::bindings::_helpers::contracts::spec_resolution::resolve_effective_adapter_name; +use crate::bindings::_helpers::contracts::spec_resolution::resolve_target_config; +use crate::bindings::_helpers::contracts::spec_resolution::resolve_target_name; +use crate::bindings::_helpers::dbt_interop::reference_resolver::resolve_external_sql_reference_resolver; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; +use crate::bindings::_helpers::planner_selection::unit_tests::reject_unit_test_selectors; use crate::bindings::_helpers::presentation_text::terminal::stdout_supports_color; +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::{ErrorId, NativeAttr, RecordId}; +use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::result_output::documents::format_clone_execution_json; +use crate::bindings::_helpers::result_output::documents::write_execution_json_output; const NO_DESTINATION: (&str, &str) = ( "C406", @@ -78,10 +88,11 @@ fn progress_reporter<'py>( style: CliStyle, ) -> PyResult> { let py: Python<'py> = stream.py(); - let options: Bound<'py, PyDict> = PyDict::new(py); - options.set_item("stream", stream)?; - options.set_item("use_color", style.use_color)?; - registry::native(py, NativeAttr::PlanningProgressReporter)?.call((), Some(&options)) + Ok(Bound::new( + py, + PlanningProgressReporter::new(py, stream, style.use_color)?, + )? + .into_any()) } fn lifecycle<'py>(py: Python<'py>, (kind, name): (&str, &str)) -> PyResult> { @@ -97,7 +108,7 @@ fn lifecycle<'py>(py: Python<'py>, (kind, name): (&str, &str)) -> PyResult) -> PyResult> { let mut names: BTreeSet = BTreeSet::new(); for config in ["project_config", "local_config"] { - for name in native_field(&discovered.getattr(config)?, "targets")?.try_iter()? { + for name in native_field(&native_field(discovered, config)?, "targets")?.try_iter()? { names.insert(name?.extract()?); } } @@ -106,20 +117,13 @@ fn target_names(discovered: &Bound<'_, PyAny>) -> PyResult> { fn clone_policy(discovered: &Bound<'_, PyAny>, (target, flag): (&str, &str)) -> PyResult { let py: Python<'_> = discovered.py(); - call_with( - ®istry::native(py, NativeAttr::ResolveTargetConfig)?, - &[ - ( - "project_config", - native_field(discovered, "project_config")?, - ), - ("local_config", native_field(discovered, "local_config")?), - ("target_name", text(py, target)), - ], - )? - .getattr("clone")? - .getattr(flag)? - .is_truthy() + let config = resolve_target_config( + py, + &native_field(discovered, "project_config")?, + &native_field(discovered, "local_config")?, + &text(py, target), + )?; + native_field(&native_field(&config, "clone")?, flag)?.is_truthy() } /// Discover the project, resolve and validate the targets, and resolve the adapter. @@ -139,27 +143,20 @@ fn invocation<'py>(py: Python<'py>, request: &Bound<'py, PyAny>) -> PyResult = native_field(request, "destination_target_name")?; if destination.is_none() { - destination = call_with( - ®istry::native(py, NativeAttr::ResolveTargetName)?, - &[ - ( - "project_config", - native_field(&discovered, "project_config")?, - ), - ("local_config", native_field(&discovered, "local_config")?), - ("selected_target", py.None().into_bound(py)), - ], - )?; + destination = resolve_target_name( + py, + &native_field(&discovered, "project_config")?, + &native_field(&discovered, "local_config")?, + &py.None().into_bound(py), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; } if destination.is_none() { return Err(user_error(py, NO_DESTINATION)); @@ -174,15 +171,9 @@ fn invocation<'py>(py: Python<'py>, request: &Bound<'py, PyAny>) -> PyResult = call_with( - ®istry::native(py, NativeAttr::ResolveEffectiveAdapterName)?, - &[ - ( - "project_config", - native_field(&discovered, "project_config")?, - ), - ("local_config", native_field(&discovered, "local_config")?), - ], + let adapter_name: Bound<'py, PyAny> = resolve_effective_adapter_name( + &native_field(&discovered, "project_config")?, + &native_field(&discovered, "local_config")?, )?; let adapter: Bound<'py, PyAny> = resolve_cli_adapter(&adapter_name, &project_dir)?; Ok(Invocation { @@ -202,14 +193,17 @@ fn connect<'py>( invocation: &Invocation<'py>, ) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> { let py: Python<'py> = request.py(); - let config: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ResolveTargetConnectionConfig)?, - &[ - ("discovered_inputs", invocation.discovered.clone()), - ("project_dir", invocation.project_dir.clone()), - ("target_name", text(py, &invocation.destination)), - ("cli_vars", native_field(request, "cli_vars")?), - ], + let config: Bound<'py, PyAny> = resolve_target_connection_config( + py, + Some(keywords( + py, + &[ + ("discovered_inputs", invocation.discovered.clone()), + ("project_dir", invocation.project_dir.clone()), + ("target_name", text(py, &invocation.destination)), + ("cli_vars", native_field(request, "cli_vars")?), + ], + )?), )?; let progress: Bound<'py, PyAny> = progress_reporter(&invocation.stream, invocation.style)?; progress.call_method1( @@ -249,19 +243,18 @@ fn prepare<'py>( let pipeline: Bound<'py, PyAny> = within(&lifecycle(py, ("project", "project_compile"))?, |_| { progress.call_method1("on_progress", ("Preparing clone plan...",))?; - let resolver: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ResolveExternalSqlReferenceResolver)?, - &[ - ("project_dir", invocation.project_dir.clone()), - ("discovered_inputs", invocation.discovered.clone()), - ], + let resolver: Bound<'py, PyAny> = resolve_external_sql_reference_resolver( + invocation.project_dir.clone(), + invocation.discovered.clone(), )?; - let destination_connection: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::ClonePipelineConnection)?, + let destination_connection: Bound<'py, PyAny> = new_record_with( + py, + RecordId::ClonePipelineConnection, &[("config", config.clone()), ("handle", connection.clone())], )?; - let options: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::ClonePipelineOptions)?, + let options: Bound<'py, PyAny> = new_record_with( + py, + RecordId::ClonePipelineOptions, &[ ( "no_sql_validation", @@ -323,7 +316,7 @@ fn clone<'py>( /// Run one `sqb clone` request. #[pyfunction] #[pyo3(signature = (*, request))] -fn run_clone_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { +pub(crate) fn run_clone_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { let invocation: Invocation<'_> = invocation(py, request)?; let (config, connection) = connect(request, &invocation)?; let outcome = clone(request, &invocation, (&config, &connection)); @@ -347,24 +340,19 @@ fn run_clone_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult = call_with( - ®istry::native(py, NativeAttr::FormatCloneExecutionJson)?, - &[ - ("result", result.clone()), - ("resource_types_by_name", resource_types.into_any()), - ], - )?; - call_with( - ®istry::native(py, NativeAttr::WriteExecutionJsonOutput)?, - &[ - ("payload", payload), - ("json_output", PyBool::new(py, false).to_owned().into_any()), - ( - "json_output_path", - native_field(request, "json_output_path")?, - ), - ], - )?; + let payload: Bound<'_, PyAny> = + format_clone_execution_json(py, result.clone(), resource_types.into_any()) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; + write_execution_json_output( + py, + payload, + PyBool::new(py, false) + .to_owned() + .into_any() + .extract::()?, + native_field(request, "json_output_path")?, + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; Ok(clone_exit_code(&items)) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/clone/clone_execution.rs b/crates/sqlbuild-python/src/bindings/_helpers/clone/clone_execution.rs index 5b3bb5f04e..f19ce6d00e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/clone/clone_execution.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/clone/clone_execution.rs @@ -13,11 +13,13 @@ use crate::bindings::_helpers::clone::clone_progress::{ CloneFingerprintReporter, CloneItemPrinter, }; use crate::bindings::_helpers::compile_driver::python_calls::{call_with, within}; +use crate::bindings::_helpers::contracts::spec_resolution::resolve_target_config; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; +use crate::bindings::_helpers::records::construct::{enum_member, new_record, new_record_with}; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::ids::RecordId; +use crate::bindings::_helpers::result_output::event_writer::ExecutionEventWriter; const DESTINATION_ENTRY_GROUPS: [&str; 4] = [ "destination_source_entries", @@ -35,7 +37,7 @@ const ORIGIN_ENTRY_GROUPS: [&str; 3] = [ fn entries<'py>(pipeline: &Bound<'py, PyAny>, groups: &[&str]) -> PyResult>> { let mut found: Vec> = Vec::new(); for group in groups { - for entry in pipeline.getattr(*group)?.try_iter()? { + for entry in native_field(pipeline, group)?.try_iter()? { found.push(entry?); } } @@ -120,11 +122,7 @@ fn dependency_locations<'py>( } else { native_field(&plan, "source_map")? }; - let key_class: Bound<'py, PyAny> = registry::class(py, RecordId::CompiledObjectKey)?; - let location_class: Bound<'py, PyAny> = - registry::class(py, RecordId::CompiledRelationLocation)?; - let source_type: Bound<'py, PyAny> = - registry::class(py, RecordId::CompiledResourceType)?.getattr("SOURCE")?; + let source_type: Bound<'py, PyAny> = enum_member(py, RecordId::CompiledResourceType, "SOURCE")?; for pair in source_map.call_method0("items")?.try_iter()? { let (name, source): (Bound<'py, PyAny>, Bound<'py, PyAny>) = pair?.extract()?; let table: Bound<'py, PyAny> = native_field(&source, "table")?; @@ -144,33 +142,26 @@ fn dependency_locations<'py>( table }; let [database, schema, table] = place; - let location: Bound<'py, PyAny> = call_with( - &location_class, + let location: Bound<'py, PyAny> = new_record_with( + py, + RecordId::CompiledRelationLocation, &[database, schema, table, ("qualified_name", qualified)], )?; - locations.set_item(key_class.call1((&source_type, name))?, location)?; + locations.set_item( + new_record(py, RecordId::CompiledObjectKey, (&source_type, name), None)?, + location, + )?; } Ok(locations) } fn target_config<'py>(invocation: &Invocation<'py>) -> PyResult> { let py: Python<'py> = invocation.adapter.py(); - call_with( - ®istry::native(py, NativeAttr::ResolveTargetConfig)?, - &[ - ( - "project_config", - native_field(&invocation.discovered, "project_config")?, - ), - ( - "local_config", - native_field(&invocation.discovered, "local_config")?, - ), - ( - "target_name", - PyString::new(py, &invocation.destination).into_any(), - ), - ], + resolve_target_config( + py, + &native_field(&invocation.discovered, "project_config")?, + &native_field(&invocation.discovered, "local_config")?, + &PyString::new(py, &invocation.destination).into_any(), ) } @@ -209,7 +200,7 @@ fn execution_input<'py>( )?; let allow_decrease: bool = config .getattr("time_travel_retention_decrease")? - .eq(registry::class(py, RecordId::RetentionDecreasePolicy)?.getattr("ALLOW")?)?; + .eq(enum_member(py, RecordId::RetentionDecreasePolicy, "ALLOW")?)?; let source_entries: Bound<'py, PyAny> = call_with( &host::entry(py, ExecutorHost::CloneSourceEntries)?, &[ @@ -232,7 +223,7 @@ fn execution_input<'py>( "destination_seed_entries", "destination_function_entries", ] { - fields.push((group, pipeline.getattr(group)?)); + fields.push((group, native_field(pipeline, group)?)); } fields.extend([ ("execution_order", native_field(&plan, "execution_order")?), @@ -288,7 +279,7 @@ fn copy_fingerprints<'py>( "origin_seed_entries", "destination_seed_entries", ] { - fields.push((group, pipeline.getattr(group)?)); + fields.push((group, native_field(pipeline, group)?)); } fields.extend([ ("adapter", invocation.adapter.clone()), @@ -333,10 +324,12 @@ pub(crate) fn execute_clone_plan<'py>( ) -> PyResult<(Bound<'py, PyAny>, f64)> { let py: Python<'py> = pipeline.py(); let started: Instant = Instant::now(); - let event_writer: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ExecutionEventWriter)?, - &[("path", native_field(request, "event_output_path")?)], - )?; + let event_writer: Bound<'py, PyAny> = ExecutionEventWriter::new( + py, + Some(native_field(request, "event_output_path")?).filter(|value| !value.is_none()), + ) + .and_then(|value| pyo3::Bound::new(py, value)) + .map(pyo3::Bound::into_any)?; let printer: Py = Py::new( py, CloneItemPrinter { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/clone/clone_pipeline.rs b/crates/sqlbuild-python/src/bindings/_helpers/clone/clone_pipeline.rs index 9522e6af79..36e5c150f1 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/clone/clone_pipeline.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/clone/clone_pipeline.rs @@ -6,12 +6,12 @@ use pyo3::{pyfunction, wrap_pyfunction}; use crate::bindings::_helpers::boundary::keyword_request::Request; use crate::bindings::_helpers::compile_driver::project::build_compiled_project; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; use crate::bindings::_helpers::compile_driver::python_calls::keywords; use crate::bindings::_helpers::contracts::spec_resolution::resolve_target_config; +use crate::bindings::_helpers::planner_entries::clone_plans::run_clone_planning; +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::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::ids::RecordId; /// The `ClonePipelineResult` fields after the two projects, in `run_clone_planning` order. const PLANNED_FIELDS: [&str; 8] = [ @@ -59,8 +59,9 @@ fn compiled_environment<'py>( ), ( "analysis_selection", - call_with( - ®istry::class(py, RecordId::CompileAnalysisSelection)?, + new_record_with( + py, + RecordId::CompileAnalysisSelection, &[("no_cache", native_field(options, "no_cache")?)], )?, ), @@ -116,19 +117,23 @@ pub(crate) fn prepared_clone_pipeline<'py>( source_project(pipeline, &origin_project, origin_target)?; let destination_source_project: Bound<'py, PyAny> = source_project(pipeline, &destination_project, destination_target)?; - let planned: Bound<'py, PyTuple> = call_with( - ®istry::native(py, NativeAttr::RunClonePlanning)?, - &[ - ("project", destination_project.clone()), - ("select", native_field(&pipeline.options, "select")?), - ("exclude", native_field(&pipeline.options, "exclude")?), - ("adapter", pipeline.adapter.clone()), - ("connection", native_field(&pipeline.connection, "handle")?), - ("origin_project", origin_project.clone()), - ("origin_source_project", origin_source_project), - ("destination_source_project", destination_source_project), - ], - )? + let planned: Bound<'py, PyTuple> = run_clone_planning( + py, + Some(keywords( + py, + &[ + ("project", destination_project.clone()), + ("select", native_field(&pipeline.options, "select")?), + ("exclude", native_field(&pipeline.options, "exclude")?), + ("adapter", pipeline.adapter.clone()), + ("connection", native_field(&pipeline.connection, "handle")?), + ("origin_project", origin_project.clone()), + ("origin_source_project", origin_source_project), + ("destination_source_project", destination_source_project), + ], + )?), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))? .downcast_into()?; let mut fields: Vec<(&str, Bound<'py, PyAny>)> = vec![ ("origin_project", origin_project), @@ -137,16 +142,13 @@ pub(crate) fn prepared_clone_pipeline<'py>( for (index, name) in PLANNED_FIELDS.iter().enumerate() { fields.push((*name, planned.get_item(index)?)); } - call_with( - ®istry::class(py, RecordId::ClonePipelineResult)?, - &fields, - ) + new_record_with(py, RecordId::ClonePipelineResult, &fields) } /// Run a clone pipeline with explicit compilation controls. #[pyfunction] #[pyo3(signature = (**request))] -fn run_clone_pipeline_with_options<'py>( +pub(crate) fn run_clone_pipeline_with_options<'py>( request: Option<&Bound<'py, PyDict>>, ) -> PyResult> { let request: Request<'_, 'py> = Request::new("run_clone_pipeline_with_options", request)?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/connection.rs b/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/connection.rs index f2b6c612c4..3d69658261 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/connection.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/connection.rs @@ -15,14 +15,17 @@ use sqlbuild_core::constants::PROJECT_CONFIG_FILENAME; use crate::bindings::_helpers::command_runtime::runtime_errors::raised; use crate::bindings::_helpers::command_runtime::warehouses::{WarehouseInputs, apply_warehouse}; +use crate::bindings::_helpers::compile_attachment::effective_config::build_effective_connection_config; use crate::bindings::_helpers::compile_driver::python_calls::{ - KeywordArguments, call_with, imported, + KeywordArguments, imported, keywords, }; +use crate::bindings::_helpers::contracts::spec_resolution::resolve_effective_adapter_name; +use crate::bindings::_helpers::contracts::spec_resolution::resolve_target_config; +use crate::bindings::_helpers::contracts::spec_resolution::resolve_target_name; +use crate::bindings::_helpers::dbt_interop::profile_connection::resolve_raw_dbt_profile_connection; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::native_host::third_party::ThirdPartyHost; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::NativeAttr; -use crate::bindings::_helpers::records::registry; const CONNECTION_OPTIONS: (&[&str], &[&str]) = ( &["raw_config", "project_dir", "adapter_name"], @@ -67,18 +70,21 @@ pub(crate) fn with_dbt_profile<'py>( (discovered_inputs, cli_vars): (&Bound<'py, PyAny>, &Bound<'py, PyAny>), ) -> PyResult> { let py: Python<'py> = raw_config.py(); - let profile: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ResolveRawDbtProfileConnection)?, - &[ - ("raw_config", config.clone().into_any()), - ("project_dir", project_dir.clone()), - ("adapter_name", adapter_name.clone()), - ( - "project_config", - native_field(discovered_inputs, "project_config")?, - ), - ("cli_vars", cli_vars.clone()), - ], + let profile: Bound<'py, PyAny> = resolve_raw_dbt_profile_connection( + py, + Some(keywords( + py, + &[ + ("raw_config", config.clone().into_any()), + ("project_dir", project_dir.clone()), + ("adapter_name", adapter_name.clone()), + ( + "project_config", + native_field(discovered_inputs, "project_config")?, + ), + ("cli_vars", cli_vars.clone()), + ], + )?), )?; if profile.is_none() { return Ok(config); @@ -206,14 +212,13 @@ fn require_configured_connection( { return Ok(()); } - let target_name: Bound<'_, PyAny> = call_with( - ®istry::native(py, NativeAttr::ResolveTargetName)?, - &[ - ("project_config", project_config.clone()), - ("local_config", local_config.clone()), - ("selected_target", selected_target.clone()), - ], - )?; + let target_name: Bound<'_, PyAny> = resolve_target_name( + py, + &project_config.clone(), + &local_config.clone(), + &selected_target.clone(), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; let project_file: String = path(py, project_dir)? .div(PROJECT_CONFIG_FILENAME)? .str()? @@ -221,14 +226,8 @@ fn require_configured_connection( if target_name.is_none() { return Err(raised(py, missing_connection_error(&project_file, None))); } - let target_config: Bound<'_, PyAny> = call_with( - ®istry::native(py, NativeAttr::ResolveTargetConfig)?, - &[ - ("project_config", project_config), - ("local_config", local_config), - ("target_name", target_name.clone()), - ], - )?; + let target_config: Bound<'_, PyAny> = + resolve_target_config(py, &project_config, &local_config, &target_name.clone())?; if !native_field(&target_config, "connection_name")?.is_none() || native_field(&target_config, "connection")?.is_truthy()? { @@ -246,31 +245,18 @@ pub(crate) fn project_connection_config<'py>( (project_dir, selected_target): (&Bound<'py, PyAny>, &Bound<'py, PyAny>), (cli_vars, require_connection): (&Bound<'py, PyAny>, bool), ) -> PyResult> { - let py: Python<'py> = project_dir.py(); if require_connection { require_configured_connection(discovered_inputs, project_dir, selected_target)?; } - let raw_config: Bound<'py, PyDict> = call_with( - ®istry::native(py, NativeAttr::BuildEffectiveConnectionConfig)?, - &[ - ("discovered_inputs", discovered_inputs.clone()), - ("selected_target", selected_target.clone()), - ("cli_vars", cli_vars.clone()), - ], + let raw_config: Bound<'py, PyDict> = build_effective_connection_config( + &discovered_inputs.clone(), + Some(selected_target.clone()).filter(|value| !value.is_none()), + Some(cli_vars.clone()).filter(|value| !value.is_none()), )? .cast_into()?; - let adapter_name: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ResolveEffectiveAdapterName)?, - &[ - ( - "project_config", - native_field(discovered_inputs, "project_config")?, - ), - ( - "local_config", - native_field(discovered_inputs, "local_config")?, - ), - ], + let adapter_name: Bound<'py, PyAny> = resolve_effective_adapter_name( + &native_field(discovered_inputs, "project_config")?, + &native_field(discovered_inputs, "local_config")?, )?; connection_config( &raw_config, @@ -308,7 +294,7 @@ pub(crate) fn resolve_project_connection_config<'py>( /// Resolve the effective connection config for one named target. #[pyfunction] #[pyo3(signature = (**options))] -fn resolve_target_connection_config<'py>( +pub(crate) fn resolve_target_connection_config<'py>( py: Python<'py>, options: Option>, ) -> PyResult> { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/invocation.rs b/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/invocation.rs index a43dcb0451..e14811cbdd 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/invocation.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/invocation.rs @@ -3,14 +3,18 @@ use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; use pyo3::types::PyBool; +use crate::bindings::_helpers::cli::progress_reporters::ConnectionProgressReporter; +use crate::bindings::_helpers::cli::progress_reporters::PlanningProgressReporter; use crate::bindings::_helpers::command_runtime::adapters::resolve_cli_adapter; +use crate::bindings::_helpers::command_runtime::connection::resolve_project_connection_config; use crate::bindings::_helpers::compile_driver::discovery::discover_project_inputs; use crate::bindings::_helpers::compile_driver::python_calls::keywords; use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; +use crate::bindings::_helpers::contracts::spec_resolution::resolve_effective_adapter_name; use crate::bindings::_helpers::presentation_text::terminal::stdout_supports_color; +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::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::ids::RecordId; /// The project directory, discovered inputs, adapter context, colour and progress stream. type InvocationContext<'py> = ( @@ -33,29 +37,27 @@ pub(crate) fn resolve_adapter_connection_context<'py>( require_connection: bool, ) -> PyResult> { let py: Python<'py> = discovered.py(); - let adapter_name: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ResolveEffectiveAdapterName)?, - &[ - ( - "project_config", - native_field(discovered, "project_config")?, - ), - ("local_config", native_field(discovered, "local_config")?), - ], + let adapter_name: Bound<'py, PyAny> = resolve_effective_adapter_name( + &native_field(discovered, "project_config")?, + &native_field(discovered, "local_config")?, )?; let adapter: Bound<'py, PyAny> = resolve_cli_adapter(&adapter_name, project_dir)?; - let connection_config: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ResolveProjectConnectionConfig)?, - &[ - ("discovered_inputs", discovered.clone()), - ("project_dir", project_dir.clone()), - ("selected_target", selected_target), - ("cli_vars", cli_vars), - ("require_connection", boolean(py, require_connection)), - ], + let connection_config: Bound<'py, PyAny> = resolve_project_connection_config( + py, + Some(keywords( + py, + &[ + ("discovered_inputs", discovered.clone()), + ("project_dir", project_dir.clone()), + ("selected_target", selected_target), + ("cli_vars", cli_vars), + ("require_connection", boolean(py, require_connection)), + ], + )?), )?; - call_with( - ®istry::class(py, RecordId::AdapterConnectionContext)?, + new_record_with( + py, + RecordId::AdapterConnectionContext, &[ ("adapter_name", adapter_name), ("adapter", adapter), @@ -141,27 +143,32 @@ pub(crate) fn resolve_reported_command_invocation<'py>( let mut fields: Vec<(&str, Bound<'py, PyAny>)> = context_fields(&context)?; fields.push(( "connection_progress", - call_with( - ®istry::native(py, NativeAttr::ConnectionProgressReporter)?, - &[ - ( - "adapter_name", - native_field(adapter_context, "adapter_name")?, - ), - ("stream", stream.clone()), - ("use_color", boolean(py, *use_color)), - ], - )?, + ConnectionProgressReporter::new( + py, + Some(keywords( + py, + &[ + ( + "adapter_name", + native_field(adapter_context, "adapter_name")?, + ), + ("stream", stream.clone()), + ("use_color", boolean(py, *use_color)), + ], + )?), + ) + .and_then(|value| pyo3::Bound::new(py, value)) + .map(pyo3::Bound::into_any)?, )); fields.push(( "planning_progress", - call_with( - ®istry::native(py, NativeAttr::PlanningProgressReporter)?, - &[ - ("stream", stream.clone()), - ("use_color", boolean(py, *use_color)), - ], - )?, + PlanningProgressReporter::new( + py, + &stream.clone(), + boolean(py, *use_color).extract::()?, + ) + .and_then(|value| pyo3::Bound::new(py, value)) + .map(pyo3::Bound::into_any)?, )); call_with(invocation_type, &fields) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/warehouse_scope.rs b/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/warehouse_scope.rs index 4806e45ac7..7b9a63b922 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/warehouse_scope.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/warehouse_scope.rs @@ -4,20 +4,35 @@ use std::sync::Mutex; use pyo3::exceptions::PyRuntimeError; use pyo3::prelude::{Bound, Py, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; +use pyo3::sync::PyOnceLock; use pyo3::types::{PyDict, PyDictMethods, PyTuple}; use pyo3::{pyclass, pyfunction, pymethods, wrap_pyfunction}; use sqlbuild_config::command_connection::constants::COMMAND_WAREHOUSE_GROUPS; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; -use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::construct::{enum_from_value, new_record_with}; +use crate::bindings::_helpers::records::ids::RecordId; const CONTEXT_ATTRIBUTE: &str = "_command_warehouse_scope_context"; const CONTEXT_NAME: &str = "sqlbuild_command_warehouse_scope"; +/// The `ContextVar` holding the active command's warehouse scope. +static CONTEXT: PyOnceLock> = PyOnceLock::new(); + fn context_variable(py: Python<'_>) -> PyResult> { - registry::native(py, NativeAttr::CommandWarehouseScopeContext) + Ok(CONTEXT + .get_or_try_init(py, || { + let keywords: Bound<'_, PyDict> = PyDict::new(py); + keywords.set_item("default", py.None())?; + Ok::<_, pyo3::PyErr>( + py.import("contextvars")? + .getattr("ContextVar")? + .call((CONTEXT_NAME,), Some(&keywords))? + .unbind(), + ) + })? + .bind(py) + .clone()) } /// Installs one command's warehouse scope for the duration of a `with` block. @@ -65,8 +80,7 @@ pub(crate) fn command_warehouse_scope<'py>( let group: Bound<'py, PyAny> = if command.is_none() { py.None().into_bound(py) } else { - let name: String = registry::class(py, RecordId::CliCommand)? - .call1((command,))? + let name: String = enum_from_value(py, RecordId::CliCommand, command)? .str()? .to_string(); match COMMAND_WAREHOUSE_GROUPS @@ -74,7 +88,7 @@ pub(crate) fn command_warehouse_scope<'py>( .find(|(candidate, _)| *candidate == name) .and_then(|(_, group)| *group) { - Some(group) => registry::class(py, RecordId::WarehouseGroup)?.call1((group,))?, + Some(group) => enum_from_value(py, RecordId::WarehouseGroup, group)?, None => py.None().into_bound(py), } }; @@ -82,8 +96,9 @@ pub(crate) fn command_warehouse_scope<'py>( Some(cli_vars) => py.get_type::().call1((cli_vars,))?, None => PyDict::new(py).into_any(), }; - let scope: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::CommandWarehouseScope)?, + let scope: Bound<'py, PyAny> = new_record_with( + py, + RecordId::CommandWarehouseScope, &[ ("group", group), ("cli_warehouse", cli_warehouse.clone()), @@ -103,22 +118,16 @@ pub(crate) fn current_command_warehouse_scope(py: Python<'_>) -> PyResult) -> PyResult<()> { let py: Python<'_> = module.py(); - let keywords: Bound<'_, PyDict> = PyDict::new(py); - keywords.set_item("default", py.None())?; - module.add( - CONTEXT_ATTRIBUTE, - py.import("contextvars")? - .getattr("ContextVar")? - .call((CONTEXT_NAME,), Some(&keywords))?, - )?; + module.add(CONTEXT_ATTRIBUTE, context_variable(py)?)?; module.add_class::()?; module.add_function(wrap_pyfunction!(command_warehouse_scope, module)?)?; module.add_function(wrap_pyfunction!(current_command_warehouse_scope, module)?) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/warehouses.rs b/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/warehouses.rs index 4a5d80d177..2f6767aa0d 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/warehouses.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/warehouses.rs @@ -22,14 +22,17 @@ use crate::bindings::_helpers::command_runtime::runtime_errors::{ expected_resolution_error, raised, }; use crate::bindings::_helpers::command_runtime::warehouse_scope::current_command_warehouse_scope; +use crate::bindings::_helpers::compile_attachment::effective_config::build_effective_runtime_config; use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; +use crate::bindings::_helpers::contracts::spec_resolution::resolve_effective_adapter_name; +use crate::bindings::_helpers::contracts::spec_resolution::resolve_target_name; use crate::bindings::_helpers::discovery::configuration::discover_project_configuration; use crate::bindings::_helpers::model_config::template_data::expanded_template_data; use crate::bindings::_helpers::native_host::adapter::AdapterHost; use crate::bindings::_helpers::native_host::host; +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::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::ids::RecordId; /// The `WarehouseGroup` whose warehouse is a target's `warehouses.build`; any other reads `query`. const BUILD_GROUP: &str = "build"; @@ -102,8 +105,9 @@ fn resolved_warehouse<'py>( warehouse: Option<&str>, source: &str, ) -> PyResult> { - call_with( - ®istry::class(py, RecordId::ResolvedWarehouse)?, + new_record_with( + py, + RecordId::ResolvedWarehouse, &[ ( "warehouse", @@ -126,7 +130,10 @@ fn group_value(target: &Bound<'_, PyAny>, group: &Bound<'_, PyAny>) -> PyResult< } else { QUERY_GROUP }; - optional_text(&native_field(target, "warehouses")?.getattr(attribute)?) + optional_text(&native_field( + &native_field(target, "warehouses")?, + attribute, + )?) } /// The active target's authored, unexpanded warehouse for one command group. @@ -138,14 +145,13 @@ pub(crate) fn find_authored<'py>( let project_config: Bound<'py, PyAny> = native_field(inputs.discovered_inputs, "project_config")?; let local_config: Bound<'py, PyAny> = native_field(inputs.discovered_inputs, "local_config")?; - let target_name: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ResolveTargetName)?, - &[ - ("project_config", project_config.clone()), - ("local_config", local_config.clone()), - ("selected_target", inputs.selected_target.clone()), - ], - )?; + let target_name: Bound<'py, PyAny> = resolve_target_name( + py, + &project_config.clone(), + &local_config.clone(), + &inputs.selected_target.clone(), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; if target_name.is_none() { return Ok(None); } @@ -175,14 +181,12 @@ fn expand_authored( cli_vars: &Bound<'_, PyAny>, ) -> PyResult { let py: Python<'_> = found.group.py(); - let runtime: Bound<'_, PyAny> = call_with( - ®istry::native(py, NativeAttr::BuildEffectiveRuntimeConfig)?, - &[ - ("discovered_inputs", inputs.discovered_inputs.clone()), - ("selected_target", inputs.selected_target.clone()), - ("cli_vars", cli_vars.clone()), - ], - )?; + let runtime: Bound<'_, PyAny> = build_effective_runtime_config( + &inputs.discovered_inputs.clone(), + Some(inputs.selected_target.clone()).filter(|value| !value.is_none()), + Some(cli_vars.clone()).filter(|value| !value.is_none()), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; let expanded: Bound<'_, PyAny> = expanded_template_data( PyString::new(py, &found.authored.warehouse).as_any(), (runtime.get_item(1)?.downcast()?, &PyDict::new(py)), @@ -276,7 +280,7 @@ fn optional_inputs<'a, 'py>( /// Reject an empty, malformed, or unsupported `--warehouse` before the command runs. #[pyfunction] #[pyo3(signature = (*, cli_warehouse, project_dir))] -fn validate_cli_warehouse( +pub(crate) fn validate_cli_warehouse( py: Python<'_>, cli_warehouse: Option, project_dir: Bound<'_, PyAny>, @@ -286,15 +290,9 @@ fn validate_cli_warehouse( }; checked_cli_warehouse(py, &cli_warehouse)?; let discovered: Bound<'_, PyAny> = discover_project_configuration(py, &project_dir)?; - let adapter_name: Bound<'_, PyAny> = call_with( - ®istry::native(py, NativeAttr::ResolveEffectiveAdapterName)?, - &[ - ( - "project_config", - native_field(&discovered, "project_config")?, - ), - ("local_config", native_field(&discovered, "local_config")?), - ], + let adapter_name: Bound<'_, PyAny> = resolve_effective_adapter_name( + &native_field(&discovered, "project_config")?, + &native_field(&discovered, "local_config")?, )?; if !session_warehouse_support(py, &adapter_name, &project_dir)? { return Err(unsupported(py, &cli_warehouse, &adapter_name)); @@ -363,7 +361,7 @@ pub(crate) fn apply_warehouse<'py>( /// Return connection config whose warehouse follows the active command's group. #[pyfunction] #[pyo3(signature = (**options))] -fn apply_command_warehouse<'py>( +pub(crate) fn apply_command_warehouse<'py>( py: Python<'py>, options: Option>, ) -> PyResult> { @@ -384,7 +382,7 @@ fn apply_command_warehouse<'py>( /// Describe the warehouse an already resolved connection uses and where it came from. #[pyfunction] #[pyo3(signature = (*, discovered_inputs, selected_target, connection_config))] -fn describe_connection_warehouse<'py>( +pub(crate) fn describe_connection_warehouse<'py>( py: Python<'py>, discovered_inputs: Bound<'py, PyAny>, selected_target: Bound<'py, PyAny>, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_attachment/cursor_roles.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_attachment/cursor_roles.rs index 1e664b7d8f..1df15a5b7a 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_attachment/cursor_roles.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_attachment/cursor_roles.rs @@ -4,11 +4,10 @@ use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; use pyo3::pyfunction; use pyo3::types::{PyDict, PyDictMethods, PyList, PyListMethods, PyString}; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; use crate::bindings::_helpers::compiled_project::model_input_view::input_reference_rows; +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; -use crate::bindings::_helpers::records::registry; const CURSOR_INPUTS_KEY: &str = "cursor_inputs"; const FILTER_ROLE: &str = "filter"; @@ -95,8 +94,9 @@ pub(crate) fn resolve_cursor_input_roles<'py>( } } } - call_with( - ®istry::class(py, RecordId::CursorInputRoles)?, + new_record_with( + py, + RecordId::CursorInputRoles, &[ ("filter_inputs", filter.into_any()), ("watermark_inputs", watermark.into_any()), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_attachment/declaration_scope.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_attachment/declaration_scope.rs index 227025419c..ac7a4d5791 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_attachment/declaration_scope.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_attachment/declaration_scope.rs @@ -4,16 +4,20 @@ use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyErr, PyResult, Python}; use pyo3::pyfunction; use pyo3::types::PyTuple; +use crate::bindings::_helpers::cache::sql_test_scan_store::SqlTestScanCache; use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; use crate::bindings::_helpers::compile_driver::stage_work::{DECLARATION_SCOPES, count_stage}; +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::{ErrorId, NativeAttr, RecordId}; +use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::render::declaration_values::declaration_scope_resolver; use crate::bindings::_helpers::scopes::scope_index::open_native_scope_index; use crate::bindings::_helpers::scopes::scope_lookup::build_scope_lookup; use crate::bindings::_helpers::scopes::scope_validation::validate_scope_index; +use crate::bindings::_helpers::sql_test_glue::authored_ctes::validate_authored_sql_test_ctes; use crate::bindings::_helpers::sql_test_glue::constants::SQL_TEST_EXPECTED_MODELS_SCAN_ALGORITHM; +use crate::bindings::_helpers::sqltext::lexical_syntax::LexicalSyntaxInput; fn compile_error(py: Python<'_>, message: Bound<'_, PyAny>) -> PyResult { Ok(PyErr::from_value( @@ -30,8 +34,9 @@ fn scope_build<'py>( let py: Python<'py> = discovered.py(); let resolver: Bound<'py, PyAny> = declaration_scope_resolver(discovered, &index, (loaded_macros, &lookup))?; - call_with( - ®istry::class(py, RecordId::DeclarationScopeBuild)?, + new_record_with( + py, + RecordId::DeclarationScopeBuild, &[ ("loaded_macros", loaded_macros.clone()), ("index", index), @@ -52,20 +57,24 @@ pub(crate) fn build_declaration_scope<'py>( ) -> PyResult> { let py: Python<'py> = discovered_inputs.py(); count_stage(py, DECLARATION_SCOPES, 1)?; - let scan_cache: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::SqlTestScanCache)?, - &[( - "cache_dir", - compile_cache_dir.unwrap_or_else(|| py.None().into_bound(py)), - )], - )?; + let scan_cache: Bound<'py, PyAny> = pyo3::Bound::new( + py, + SqlTestScanCache::new( + compile_cache_dir + .unwrap_or_else(|| py.None().into_bound(py)) + .extract::>()?, + ), + ) + .map(pyo3::Bound::into_any)?; let test_files: Bound<'py, PyAny> = native_field(discovered_inputs, "test_files")?; let scenario_files: Bound<'py, PyAny> = native_field(discovered_inputs, "scenario_files")?; - registry::native(py, NativeAttr::ValidateAuthoredSqlTestCtes)?.call1(( - &test_files, - &scenario_files, - native_field(sql_lexical_syntax, "native_mapping")?, - ))?; + validate_authored_sql_test_ctes( + py, + test_files.clone(), + scenario_files.clone(), + native_field(sql_lexical_syntax, "native_mapping")?.extract::()?, + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; let native: Bound<'py, PyAny> = Bound::new( py, open_native_scope_index(py, discovered_inputs, loaded_macros, None)?, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_attachment/effective_config.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_attachment/effective_config.rs index 464ffc2611..73b307a1f3 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_attachment/effective_config.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_attachment/effective_config.rs @@ -2,19 +2,23 @@ use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyErr, PyResult, Python}; use pyo3::pyfunction; -use pyo3::types::{PyDict, PyList, PyTuple}; +use pyo3::types::{PyDict, PyString, PyTuple}; use sqlbuild_model_config::templates::models::TemplateOptions; use crate::bindings::_helpers::compile_driver::python_calls::imported; +use crate::bindings::_helpers::contracts::effective_config::effective_config_connection; +use crate::bindings::_helpers::contracts::effective_config::effective_config_settings; +use crate::bindings::_helpers::contracts::effective_config::effective_config_vars; use crate::bindings::_helpers::contracts::spec_resolution::{ resolve_target_config, resolve_target_name, }; use crate::bindings::_helpers::model_config::template_data::{ expanded_effective_vars, expanded_template_data, run_id, }; +use crate::bindings::_helpers::records::construct::{new_record, record_class}; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr, RecordId}; +use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; const CONNECTION_LABEL: &str = "effective connection"; @@ -30,8 +34,8 @@ pub(crate) fn build_effective_connection<'py>( ) -> PyResult> { let py: Python<'py> = project_config.py(); let (connection, error): (Bound<'py, PyAny>, Option) = - registry::native(py, NativeAttr::EffectiveConfigConnection)? - .call1((project_config, local_config, target_config))? + effective_config_connection(py, project_config, local_config, target_config) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))? .extract()?; if connection.is_none() { let raised: Bound<'py, PyAny> = @@ -60,23 +64,27 @@ pub(crate) fn build_effective_settings<'py>( local_config: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = project_config.py(); - let settings_class: Bound<'py, PyAny> = registry::class(py, RecordId::SettingsConfig)?; + let settings_class: Bound<'py, PyAny> = record_class(py, RecordId::SettingsConfig)?; let names: Vec> = imported(py, "dataclasses", "fields")? .call1((&settings_class,))? .try_iter()? .map(|field| native_field(&field?, "name")) .collect::>()?; - let values: Bound<'py, PyDict> = registry::native(py, NativeAttr::EffectiveConfigSettings)? - .call1(( - ( - native_field(project_config, "settings")?, - native_field(local_config, "settings")?, - ), - native_field(local_config, "setting_overrides")?, - PyList::new(py, names)?, - ))? - .downcast_into()?; - settings_class.call((), Some(&values)) + let values: Bound<'py, PyDict> = effective_config_settings( + py, + ( + native_field(project_config, "settings")?, + native_field(local_config, "settings")?, + ), + &native_field(local_config, "setting_overrides")?, + names + .into_iter() + .map(|name| Ok(name.cast_into::()?)) + .collect::>()?, + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))? + .downcast_into()?; + new_record(py, RecordId::SettingsConfig, (), Some(&values)) } /// Merge effective vars using the locked precedence order. @@ -89,8 +97,13 @@ pub(crate) fn build_effective_vars<'py>( cli_vars: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = project_config.py(); - let merged: Bound<'py, PyAny> = registry::native(py, NativeAttr::EffectiveConfigVars)? - .call1(((project_config, local_config), target_config, cli_vars))?; + let merged: Bound<'py, PyAny> = effective_config_vars( + py, + (project_config.clone(), local_config.clone()), + target_config, + cli_vars, + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; expanded_effective_vars(merged.downcast()?) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_attachment/model_config.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_attachment/model_config.rs index f8c3245c49..3195beb87c 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_attachment/model_config.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_attachment/model_config.rs @@ -5,13 +5,12 @@ use pyo3::pyfunction; use pyo3::types::{PyDict, PyDictMethods, PyTuple}; use crate::bindings::_helpers::compile_attachment::storage_policies::inherited_storage; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; use crate::bindings::_helpers::compiled_project::loop_classes::table_backed; use crate::bindings::_helpers::model_config::model_config_builder::NativeModelConfigBuilder; use crate::bindings::_helpers::model_config::unicode_environment::unicode_environment; +use crate::bindings::_helpers::records::construct::{new_record, new_record_with, record_class}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; /// Capture the project inputs every model's native config build and validation read. #[pyfunction] @@ -27,7 +26,7 @@ pub(crate) fn native_model_config_session<'py>( let materialization_defaults: Bound<'py, PyAny> = if declared.is_truthy()? { declared } else { - registry::class(py, RecordId::MaterializationDefaultsConfig)?.call0()? + new_record(py, RecordId::MaterializationDefaultsConfig, (), None)? }; let hook_entries: Bound<'py, PyTuple> = PyTuple::new( py, @@ -37,7 +36,7 @@ pub(crate) fn native_model_config_session<'py>( RecordId::PythonHookEntry, ] .iter() - .map(|class| registry::class(py, *class)) + .map(|class| record_class(py, *class)) .collect::>>()?, )?; let target_namespace: Option<(Bound<'py, PyAny>, Bound<'py, PyAny>)> = if target.is_none() { @@ -78,8 +77,9 @@ pub(crate) fn native_model_config_session<'py>( inherited_storage(&materialized, &materialization_defaults, &target)?; inherited.set_item(&materialized, storage)?; } - call_with( - ®istry::class(py, RecordId::NativeModelConfigSession)?, + new_record_with( + py, + RecordId::NativeModelConfigSession, &[ ( "path_defaults", diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_attachment/model_inputs.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_attachment/model_inputs.rs index f50de01af2..564ae1f492 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_attachment/model_inputs.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_attachment/model_inputs.rs @@ -16,14 +16,18 @@ use crate::bindings::_helpers::compiled_project::model_loop::NativeModelLoop; use crate::bindings::_helpers::compiled_project::project::NativeCompiledProject; use crate::bindings::_helpers::discovery::model_file_record::NativeModelFile; use crate::bindings::_helpers::model_config::contract_classes::contract_classes; +use crate::bindings::_helpers::model_config::model_validator::NativeModelValidator; use crate::bindings::_helpers::model_config::unicode_environment::unicode_environment; +use crate::bindings::_helpers::records::construct::{ + enum_member, new_record, new_record_with, record_class, +}; use crate::bindings::_helpers::render::declaration_values::ConstantRendering; use crate::bindings::_helpers::render::macro_expansion_result::macro_expansion_classes; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::native_host::user_code::UserCodeHost; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr, RecordId}; +use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; use sqlbuild_core::constants::PROJECT_CONFIG_FILENAME; /// The explicit reference call kinds and the `SqlResourceRefKind` member each resolves to. @@ -35,7 +39,7 @@ fn explicit_reference_kinds(py: Python<'_>) -> PyResult> { let kinds: Bound<'_, PyAny> = host::entry(py, UserCodeHost::SqlResourceRefKind)?; let built: Bound<'_, PyDict> = PyDict::new(py); for (call_kind, member) in EXPLICIT_REFERENCE_KINDS { - built.set_item(call_kind, kinds.getattr(member)?)?; + built.set_item(call_kind, native_field(&kinds, member)?)?; } Ok(built) } @@ -89,7 +93,7 @@ fn macro_inputs<'py>(context: &Bound<'py, PyAny>) -> PyResult ), ( "declaration_context", - registry::class(py, RecordId::DeclarationResolutionContext)?, + record_class(py, RecordId::DeclarationResolutionContext)?, ), ( "macro_argument_error", @@ -98,9 +102,7 @@ fn macro_inputs<'py>(context: &Bound<'py, PyAny>) -> PyResult ("expansion", macro_expansion_classes(py)?.into_any()), ( "generic_syntax", - registry::class(py, RecordId::SqlLexicalSyntax)? - .call0()? - .getattr("native_mapping")?, + new_record(py, RecordId::SqlLexicalSyntax, (), None)?.getattr("native_mapping")?, ), ], ) @@ -116,18 +118,18 @@ fn sql_hook_inputs<'py>( vec![ ( "named_sql_hook_entry", - registry::class(py, RecordId::NamedSqlHookEntry)?, + record_class(py, RecordId::NamedSqlHookEntry)?, ), ( "python_hook_entry", - registry::class(py, RecordId::PythonHookEntry)?, + record_class(py, RecordId::PythonHookEntry)?, ), ("hook_files", native_field(discovered, "sql_hook_files")?), ("effective_vars", native_field(context, "effective_vars")?), ("environment", unicode_environment(py)?), ( "render_project_var", - registry::native(py, NativeAttr::RenderProjectVarText)?, + pyo3::wrap_pyfunction!(crate::bindings::_helpers::authored_values::project_vars::render_project_var_text, py)?.into_any(), ), ( "sql_resource_ref", @@ -136,11 +138,11 @@ fn sql_hook_inputs<'py>( ("explicit_kinds", explicit_reference_kinds(py)?.into_any()), ( "declaration_identity", - registry::class(py, RecordId::DeclarationIdentity)?, + record_class(py, RecordId::DeclarationIdentity)?, ), ( "sql_hook_kind", - registry::class(py, RecordId::DeclarationKind)?.getattr("SQL_HOOK")?, + enum_member(py, RecordId::DeclarationKind, "SQL_HOOK")?, ), ], ) @@ -173,14 +175,14 @@ pub(crate) fn build_model_inputs<'py>( let defer_model_sql_validation: bool = arguments.flag("defer_model_sql_validation")?; let external: Bound<'py, PyAny> = arguments.value("external_sql_reference_resolver")?; let settings: Bound<'py, PyAny> = native_field(context, "effective_settings")?; - let validator: Bound<'py, PyAny> = registry::native(py, NativeAttr::NativeModelValidator)? - .call_method1( - "from_inputs", - ( - discovered_inputs, - native_field(&settings, "microbatch_concurrency")?, - ), - )?; + let validator: Bound<'py, PyAny> = Bound::new( + py, + NativeModelValidator::from_inputs( + discovered_inputs.clone(), + native_field(&settings, "microbatch_concurrency")?.extract()?, + )?, + )? + .into_any(); if !external.is_none() { call_with( &native_field(&external, "validate_model_names")?, @@ -194,8 +196,9 @@ pub(crate) fn build_model_inputs<'py>( imported(py, "pathlib", "Path")?.call0()? }; let project_config_path: Bound<'py, PyAny> = root.div(PROJECT_CONFIG_FILENAME)?; - let config_inputs: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::NativeModelConfigInputs)?, + let config_inputs: Bound<'py, PyAny> = new_record_with( + py, + RecordId::NativeModelConfigInputs, &[ ( "project_config", @@ -225,15 +228,15 @@ pub(crate) fn build_model_inputs<'py>( vec![ ( "report_diagnostic", - registry::native(py, NativeAttr::ReportCompileDiagnostic)?, + pyo3::wrap_pyfunction!(crate::bindings::_helpers::compile_driver::diagnostic_collector::report_compile_diagnostic, py)?.into_any(), ), ( "record_template_reads", - registry::native(py, NativeAttr::RecordTemplateReads)?, + pyo3::wrap_pyfunction!(crate::bindings::_helpers::model_config::template_data::record_template_reads, py)?.into_any(), ), ( "visible_declarations", - registry::class(py, RecordId::VisibleModelDeclarations)?, + record_class(py, RecordId::VisibleModelDeclarations)?, ), ("config_session", session), ("syntax_help", opt_out_help(py, true)?), @@ -261,20 +264,26 @@ pub(crate) fn build_model_inputs<'py>( )?; let syntax: Bound<'py, PyAny> = native_field(context, "sql_lexical_syntax")?; let native_loop: Bound<'py, PyAny> = - registry::native(py, NativeAttr::NativeModelLoop)?.call1(( - (validator, contract_classes(py)?.into_any(), classes), - native_field(&syntax, "native_mapping")?, + crate::bindings::_helpers::compiled_project::model_loop::NativeModelLoop::new( + ( + validator.unbind(), + contract_classes(py)?.unbind(), + classes.unbind(), + ), + native_field(&syntax, "native_mapping")?.extract()?, reserved_cursor_markers(), ( ( - native_field(&settings, "sql_analysis")?, + native_field(&settings, "sql_analysis")?.extract()?, no_sql_validation, defer_model_sql_validation, - native_field(&settings, "require_sql_analysis")?, + native_field(&settings, "require_sql_analysis")?.extract()?, ), - project_config_path.str()?, + project_config_path.str()?.extract()?, ), - ))?; + ) + .and_then(|value| pyo3::Bound::new(py, value)) + .map(pyo3::Bound::into_any)?; let files: Vec> = discovered_inputs .getattr("model_files")? .try_iter()? diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_attachment/sql_expansion_context.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_attachment/sql_expansion_context.rs index 0788d4989d..bb19a08ff2 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_attachment/sql_expansion_context.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_attachment/sql_expansion_context.rs @@ -4,18 +4,20 @@ use pyo3::prelude::{Bound, Py, PyAny, PyAnyMethods, PyResult, Python}; use pyo3::pyfunction; use pyo3::types::{PyBool, PyDict, PyDictMethods}; +use crate::bindings::_helpers::compile_attachment::declaration_scope::build_declaration_scope; use crate::bindings::_helpers::compile_attachment::declaration_scope::rebind_declaration_scope; use crate::bindings::_helpers::compile_attachment::effective_config::build_effective_vars; use crate::bindings::_helpers::compile_driver::discovery::discover_project_inputs; +use crate::bindings::_helpers::compile_driver::python_calls::KeywordArguments; use crate::bindings::_helpers::compile_driver::python_calls::keywords; -use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; use crate::bindings::_helpers::contracts::spec_resolution::resolve_effective_adapter_name; use crate::bindings::_helpers::contracts::spec_resolution::resolve_effective_collection_rendering; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::native_host::user_code::UserCodeHost; +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::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::ids::RecordId; +use crate::bindings::_helpers::render::declaration_indexes::public_declaration_indexes; use pyo3::IntoPyObjectExt; /// Build the declaration scope of `discovered`; a static scope lends only its index to macros. @@ -35,20 +37,17 @@ fn declaration_scope<'py>( return Ok(rebound); } } - call_with( - ®istry::native(py, NativeAttr::BuildDeclarationScope)?, - &[ - ("discovered_inputs", discovered.clone()), - ("loaded_macros", loaded_macros), - ("sql_lexical_syntax", sql_lexical_syntax), - ], + build_declaration_scope( + &discovered.clone(), + &loaded_macros, + &sql_lexical_syntax, + None, ) } /// Each model file's own enum and constant declarations, by file path. fn local_declarations<'py>(discovered: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = discovered.py(); - let context: Bound<'py, PyAny> = registry::class(py, RecordId::DeclarationResolutionContext)?; let by_name = |declarations: Bound<'py, PyAny>| -> PyResult> { let named: Bound<'py, PyDict> = PyDict::new(py); for declaration in declarations.try_iter()? { @@ -62,8 +61,9 @@ fn local_declarations<'py>(discovered: &Bound<'py, PyAny>) -> PyResult = model_file?; local.set_item( native_field(&model_file, "file_path")?, - call_with( - &context, + new_record_with( + py, + RecordId::DeclarationResolutionContext, &[ ( "enums", @@ -97,7 +97,8 @@ pub(crate) struct SqlExpansionInputs { impl SqlExpansionInputs { /// The inputs a `SqlExpansionContext` record holds. pub(crate) fn from_record(record: &Bound<'_, PyAny>) -> PyResult { - let field = |name: &str| -> PyResult> { Ok(record.getattr(name)?.unbind()) }; + let field = + |name: &str| -> PyResult> { Ok(native_field(record, name)?.unbind()) }; Ok(Self { effective_vars: field("effective_vars")?, loaded_macros: field("loaded_macros")?, @@ -116,8 +117,9 @@ impl SqlExpansionInputs { /// These inputs as a `SqlExpansionContext` record. pub(crate) fn record<'py>(&self, py: Python<'py>) -> PyResult> { - call_with( - ®istry::class(py, RecordId::SqlExpansionContext)?, + new_record_with( + py, + RecordId::SqlExpansionContext, &[ ("effective_vars", self.effective_vars.bind(py).clone()), ("loaded_macros", self.loaded_macros.bind(py).clone()), @@ -195,14 +197,15 @@ pub(crate) fn sql_expansion_inputs(sources: ExpansionSources<'_>) -> PyResult, Bound<'_, PyAny>) = - registry::native(py, NativeAttr::PublicDeclarationIndexes)? - .call1((&discovered,))? + public_declaration_indexes(discovered.clone()) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))? .extract()?; let collection_rendering: Bound<'_, PyAny> = resolve_effective_collection_rendering(&project_config.clone(), &py.None().into_bound(py)) .and_then(|value| value.into_bound_py_any(py))?; - let macro_context: Bound<'_, PyAny> = call_with( - ®istry::class(py, RecordId::MacroContext)?, + let macro_context: Bound<'_, PyAny> = new_record_with( + py, + RecordId::MacroContext, &[ ( "adapter_name", diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_attachment/storage_policies.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_attachment/storage_policies.rs index 97e21f5ad0..00a44c0b1a 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_attachment/storage_policies.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_attachment/storage_policies.rs @@ -1,12 +1,11 @@ //! The retention and table type a model inherits from its target and materialization defaults. use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; -use pyo3::types::{PyBool, PyString, PyTuple}; +use pyo3::types::{PyBool, PyString, PyStringMethods, PyTuple}; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; +use crate::bindings::_helpers::records::construct::{enum_member, new_record, new_record_with}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; /// `value`, unless it is `None`. fn present(value: Bound<'_, PyAny>) -> Option> { @@ -14,7 +13,7 @@ fn present(value: Bound<'_, PyAny>) -> Option> { } fn source<'py>(py: Python<'py>, class: RecordId, member: &str) -> PyResult> { - registry::member(py, class, member) + enum_member(py, class, member) } /// The `materialized` defaults entry, or `None` for a model without a table-backed materialization. @@ -26,7 +25,7 @@ fn defaults_entry<'py>( return Ok(None); } let name: &Bound<'py, PyString> = materialized.downcast()?; - materialization_defaults.getattr(name).map(Some) + native_field(materialization_defaults, &name.to_cow()?).map(Some) } fn retention<'py>( @@ -35,7 +34,6 @@ fn retention<'py>( target: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = materialized.py(); - let resolved: Bound<'py, PyAny> = registry::class(py, RecordId::ResolvedTimeTravelRetention)?; let class = RecordId::TimeTravelRetentionSource; let mut policy: Option<(Bound<'py, PyAny>, &str)> = None; if let Some(entry) = entry { @@ -56,10 +54,11 @@ fn retention<'py>( } } let Some((policy, member)) = policy else { - return resolved.call0(); + return new_record(py, RecordId::ResolvedTimeTravelRetention, (), None); }; - call_with( - &resolved, + new_record_with( + py, + RecordId::ResolvedTimeTravelRetention, &[ ("desired_days", native_field(&policy, "desired_days")?), ("unmanaged", native_field(&policy, "unmanaged")?), @@ -73,7 +72,6 @@ fn table_type<'py>( target: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = target.py(); - let resolved: Bound<'py, PyAny> = registry::class(py, RecordId::ResolvedTableType)?; let class = RecordId::TableTypeSource; let mut declared: Option<(Bound<'py, PyAny>, &str)> = None; if let Some(entry) = entry { @@ -85,10 +83,11 @@ fn table_type<'py>( } } let Some((value, member)) = declared else { - return resolved.call0(); + return new_record(py, RecordId::ResolvedTableType, (), None); }; - call_with( - &resolved, + new_record_with( + py, + RecordId::ResolvedTableType, &[ ("value", value), ("source", source(py, class, member)?), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_attachment/target.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_attachment/target.rs index e1aa18f7a2..9a3d602dae 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_attachment/target.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_attachment/target.rs @@ -170,7 +170,7 @@ pub(crate) fn build_effective_target_namespace<'py>( if target_config.is_none() { return Ok(py.None().into_bound(py)); } - target_config.getattr(key) + native_field(&target_config, key) }; let values: Bound<'py, PyDict> = PyDict::new(py); values.set_item( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_command/report.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_command/report.rs index 79af87bdf9..2248a6f9fa 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_command/report.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_command/report.rs @@ -12,9 +12,7 @@ use sqlbuild_core::cli_text::models::{ }; use crate::bindings::_helpers::compile_command::surrogates::SurrogateText; -use crate::bindings::_helpers::compile_driver::python_calls::{ - KeywordArguments, call_with, imported, -}; +use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, imported}; use crate::bindings::_helpers::compile_outputs::json_report::{ emit_json_report, emit_orjson_report, }; @@ -22,9 +20,9 @@ use crate::bindings::_helpers::discovery::constants::SQL_HOOK_OUTPUT_FIELDS; use crate::bindings::_helpers::discovery::hook_entries::hook_entries; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::native_host::user_code::UserCodeHost; +use crate::bindings::_helpers::records::construct::{new_record_with, record_class}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::semantic_shapes::coverage::semantic_coverage; const METADATA_MODULE: &str = "importlib.metadata"; @@ -230,8 +228,8 @@ fn source_texts<'py>(project: &Bound<'py, PyAny>) -> PyResult } } for (collection, file) in [("sources", "source_file"), ("sql_tests", "test_file")] { - for resource in project.getattr(collection)?.try_iter()? { - let file: Bound<'py, PyAny> = resource?.getattr(file)?; + for resource in native_field(project, collection)?.try_iter()? { + let file: Bound<'py, PyAny> = native_field(&resource?, file)?; texts.set_item( native_field(&file, "relative_path")?, native_field(&file, "contents")?, @@ -466,7 +464,7 @@ fn location_json<'py>(location: &Bound<'py, PyAny>) -> PyResult = location.getattr(key)?; + let value: Bound<'py, PyAny> = native_field(location, key)?; if !value.is_none() { payload.set_item(key, value)?; } @@ -497,7 +495,7 @@ fn diagnostic_json<'py>(diagnostic: &Bound<'py, PyAny>) -> PyResult = diagnostic.getattr(key)?; + let value: Bound<'py, PyAny> = native_field(diagnostic, key)?; if !value.is_none() { payload.set_item(key, value)?; } @@ -507,7 +505,7 @@ fn diagnostic_json<'py>(diagnostic: &Bound<'py, PyAny>) -> PyResult = diagnostic.getattr(key)?; + let value: Bound<'py, PyAny> = native_field(diagnostic, key)?; if !value.is_none() { payload.set_item(key, value)?; } @@ -544,7 +542,7 @@ fn diagnostic_json<'py>(diagnostic: &Bound<'py, PyAny>) -> PyResult = diagnostic.getattr(key)?; + let value: Bound<'py, PyAny> = native_field(diagnostic, key)?; if value.is_truthy()? { payload.set_item( key, @@ -782,7 +780,7 @@ fn test_resource<'py>(test: &Bound<'py, PyAny>) -> PyResult> let py: Python<'py> = test.py(); let payload: Bound<'py, PyAny> = native_field(test, "payload")?; let expected_models: Bound<'py, PyList> = - if payload.is_instance(®istry::class(py, RecordId::CompiledModelSqlTestPayload)?)? { + if payload.is_instance(&record_class(py, RecordId::CompiledModelSqlTestPayload)?)? { PyList::new( py, payload @@ -875,8 +873,7 @@ fn resources<'py>( collection: &str, resource: impl Fn(&Bound<'py, PyAny>) -> PyResult>, ) -> PyResult> { - let items: Vec> = project - .getattr(collection)? + let items: Vec> = native_field(project, collection)? .try_iter()? .map(|item| resource(&item?)) .collect::>()?; @@ -955,7 +952,7 @@ pub(crate) fn format_compile_json( has_errors = has_errors || native_field(diagnostic, "is_error")?.is_truthy()?; diagnostics.push(diagnostic_json(diagnostic)?); } - let length = |collection: &str| -> PyResult { project.getattr(collection)?.len() }; + let length = |collection: &str| -> PyResult { native_field(project, collection)?.len() }; let summary: Bound<'_, PyDict> = dict( py, vec![ @@ -1069,8 +1066,9 @@ pub(crate) fn format_compile_error_json<'py>( native_field(&location, "column")?, ) }; - let diagnostic: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::CompilerDiagnostic)?, + let diagnostic: Bound<'py, PyAny> = new_record_with( + py, + RecordId::CompilerDiagnostic, &[ ("phase", any(py, "compile")?), ("severity", any(py, ERROR_SEVERITY)?), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_command/reuse_attempt.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_command/reuse_attempt.rs index 10966ebdcc..0f23297cd8 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_command/reuse_attempt.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_command/reuse_attempt.rs @@ -30,9 +30,11 @@ use crate::bindings::_helpers::compile_command::models::CompileReuseAttempt; use crate::bindings::_helpers::compile_command::runtime_identity::{ invocation_identity, redirected_output_files, runtime_identity, }; +use crate::bindings::_helpers::compile_driver::command::run_compile_command; use crate::bindings::_helpers::compile_driver::phases::{elapsed_ms, monotonic}; use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported, keywords}; use crate::bindings::_helpers::compile_outputs::json_report::plain_json; +use crate::bindings::_helpers::compile_outputs::reuse::{ReuseCheckPy, check_compile_reuse}; use crate::bindings::_helpers::frontier::cache_directory::cache_directory; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::facade::FacadeHost; @@ -261,7 +263,7 @@ fn reuse_check<'py>( project_dir: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = request.py(); - let flag = |name: &str| -> PyResult { request.getattr(name)?.is_truthy() }; + let flag = |name: &str| -> PyResult { native_field(request, name)?.is_truthy() }; let use_color: bool = !flag("no_color")? && stdout_supports_color(py)?; let check: Bound<'py, PyDict> = PyDict::new(py); check.set_item("project_dir", project_dir)?; @@ -382,8 +384,11 @@ fn attempt_compile_reuse<'py>(request: &Bound<'py, PyAny>) -> PyResult = registry::native(py, NativeAttr::CheckCompileReuse)? - .call1((reuse_check(request, &project_dir)?,))?; + let native: Bound<'py, PyAny> = check_compile_reuse( + py, + reuse_check(request, &project_dir)?.extract::()?, + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; let checked: String = native.call_method0("outcome")?.extract()?; let json_output: Bound<'py, PyAny> = native_field(request, "json_output")?; let mut base: Vec<(&'static str, Bound<'py, PyAny>)> = vec![ @@ -441,7 +446,7 @@ fn reuse_request<'py>(request: &Bound<'py, PyAny>) -> PyResult "skip_contracts", "skip_write", ] { - profiling = profiling || flags.getattr(name)?.is_truthy()?; + profiling = profiling || native_field(&flags, name)?.is_truthy()?; } let mut fields: Vec<(&str, Bound<'py, PyAny>)> = Vec::new(); for name in [ @@ -455,7 +460,7 @@ fn reuse_request<'py>(request: &Bound<'py, PyAny>) -> PyResult "no_cache", "no_sql_validation", ] { - fields.push((name, request.getattr(name)?)); + fields.push((name, native_field(request, name)?)); } fields.push(( "lineage_mode", @@ -466,7 +471,7 @@ fn reuse_request<'py>(request: &Bound<'py, PyAny>) -> PyResult .into_any(), )); for name in ["select", "exclude", "cli_vars", "debug"] { - fields.push((name, request.getattr(name)?)); + fields.push((name, native_field(request, name)?)); } fields.push(("profiling", profiling.into_bound_py_any(py)?)); call_with(&host::entry(py, REUSE_REQUEST)?, &fields) @@ -474,14 +479,19 @@ fn reuse_request<'py>(request: &Bound<'py, PyAny>) -> PyResult /// Run one compile command request, replaying the previous compile when nothing changed. #[pyfunction] -fn run_compile<'py>(request: &Bound<'py, PyAny>) -> PyResult> { +pub(crate) fn run_compile<'py>(request: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = request.py(); let attempt: Bound<'py, PyAny> = attempt_compile_reuse(&reuse_request(request)?)?; let exit_code: Bound<'py, PyAny> = native_field(&attempt, "exit_code")?; if native_field(&attempt, "outcome")?.eq(outcome(py, HIT)?)? && !exit_code.is_none() { return Ok(exit_code); } - registry::native(py, NativeAttr::RunCompileCommand)?.call1((request, attempt)) + run_compile_command( + py, + request.clone(), + Some(attempt).filter(|value| !value.is_none()), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py)) } pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_command/runtime_identity.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_command/runtime_identity.rs index bc53cdfc21..1e3fd55db4 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_command/runtime_identity.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_command/runtime_identity.rs @@ -82,7 +82,7 @@ pub(crate) fn invocation_identity<'py>( }; payload.set_item("requested_project_dir", requested)?; for field in REQUEST_CHOICES { - payload.set_item(field, request.getattr(field)?)?; + payload.set_item(field, native_field(request, field)?)?; } payload.set_item("cwd", py.import("os")?.getattr("getcwd")?.call0()?)?; payload.set_item("use_color", use_color)?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_command/target_writer.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_command/target_writer.rs index 9bd1f35ce2..9e86cc4dec 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_command/target_writer.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_command/target_writer.rs @@ -34,6 +34,7 @@ use crate::bindings::_helpers::compile_outputs::runtime_target::{ }; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; +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::{ErrorId, NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; @@ -136,7 +137,7 @@ fn function_sql(adapter: &Bound<'_, PyAny>, function: &Bound<'_, PyAny>) -> PyRe "entry_point", "packages", ] { - arguments.push((field, function.getattr(field)?)); + arguments.push((field, native_field(function, field)?)); } let statements: Vec = adapter .getattr("render_create_function")? @@ -201,8 +202,9 @@ fn written_target<'py>( ) -> PyResult> { let py: Python<'py> = target_dir.py(); let [models, seeds, functions, audits, tests] = counts; - call_with( - ®istry::class(py, RecordId::WrittenTarget)?, + new_record_with( + py, + RecordId::WrittenTarget, &[ ("model_count", models.into_bound_py_any(py)?), ("seed_count", seeds.into_bound_py_any(py)?), @@ -216,7 +218,7 @@ fn written_target<'py>( } fn length(value: &Bound<'_, PyAny>, field: &str) -> PyResult { - value.getattr(field)?.len() + native_field(value, field)?.len() } /// Write the planner's compiled output under `target_dir`. @@ -482,8 +484,9 @@ fn artifact_diagnostics<'py>( let py: Python<'py> = test.py(); let mut diagnostics: Vec> = Vec::new(); for message in native_field(artifact, "error_messages")?.try_iter()? { - diagnostics.push(call_with( - ®istry::class(py, RecordId::CompilerDiagnostic)?, + diagnostics.push(new_record_with( + py, + RecordId::CompilerDiagnostic, &[ ("phase", "test".into_bound_py_any(py)?), ("severity", "error".into_bound_py_any(py)?), @@ -729,7 +732,7 @@ fn write_static_compile_target<'py>( /// Write the planner's compiled output files under `target_dir`. #[pyfunction] #[pyo3(signature = (**options))] -fn write_compile_target<'py>( +pub(crate) fn write_compile_target<'py>( py: Python<'py>, options: Option>, ) -> PyResult> { @@ -750,7 +753,7 @@ fn write_compile_target<'py>( /// Plan uncached SQL tests and report their errors without writing artifacts or cache. #[pyfunction] #[pyo3(signature = (**options))] -fn static_sql_test_planning_diagnostics<'py>( +pub(crate) fn static_sql_test_planning_diagnostics<'py>( py: Python<'py>, options: Option>, ) -> PyResult> { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_command/test_artifact_cache.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_command/test_artifact_cache.rs index 045eab9cf4..254504c00f 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_command/test_artifact_cache.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_command/test_artifact_cache.rs @@ -12,8 +12,8 @@ use sqlbuild_cache::digest::main::sha256_hex::sha256_hex; use crate::bindings::_helpers::compile_driver::python_calls::imported; use crate::bindings::_helpers::compile_outputs::json_report::{compact_sorted_json, orjson_sorted}; +use crate::bindings::_helpers::records::construct::record_class; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; const CACHE_VERSION: i64 = 4; const ALGORITHM_FINGERPRINT: &str = "sql-test-artifact-v4-orjson-identity"; @@ -216,7 +216,7 @@ fn test_payload<'py>(test: &Bound<'py, PyAny>) -> PyResult> { ], )?; let test_payload: Bound<'py, PyAny> = test.getattr("payload")?; - if test_payload.is_instance(®istry::class(py, RecordId::CompiledModelSqlTestPayload)?)? { + if test_payload.is_instance(&record_class(py, RecordId::CompiledModelSqlTestPayload)?)? { let block: Bound<'py, PyAny> = test.getattr("test_block")?; payload.set_item( "model", @@ -251,7 +251,7 @@ fn test_payload<'py>(test: &Bound<'py, PyAny>) -> PyResult> { ], )?, )?; - } else if test_payload.is_instance(®istry::class( + } else if test_payload.is_instance(&record_class( py, RecordId::CompiledDirectLogicSqlTestPayload, )?)? { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/analysis.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/analysis.rs index bafd15f163..712cbf6f59 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/analysis.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/analysis.rs @@ -22,9 +22,10 @@ use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::planner_selection::selectors::select_project_graph; use crate::bindings::_helpers::planner_selection::unit_tests::reject_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::replaced; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::rules::constants::RULE_FIX_AVAILABLE_NOTE; use crate::bindings::_helpers::rules::early_sql_lint::EarlySqlLint; use crate::bindings::_helpers::rules::results::rules_progress; @@ -41,7 +42,7 @@ const TARGET_DIRECTORY: &str = "target"; /// `ColumnLineageMode.`. fn column_lineage_mode<'py>(py: Python<'py>, member: &str) -> PyResult> { - registry::class(py, RecordId::ColumnLineageMode)?.getattr(member) + enum_member(py, RecordId::ColumnLineageMode, member) } /// Column lineage in the compile's mode; `none` skips the analyzer. @@ -132,7 +133,7 @@ fn any_error(diagnostics: &Bound<'_, PyAny>) -> PyResult { /// Discover, compile and validate the project into one `CompileAnalysis`. #[pyfunction] #[pyo3(signature = (**options))] -fn analyze_compile_project<'py>( +pub(crate) fn analyze_compile_project<'py>( py: Python<'py>, options: Option>, ) -> PyResult> { @@ -226,8 +227,9 @@ fn analyzed<'py>( }; let diagnostics: Bound<'py, PyTuple> = with_rule_diagnostics(py, &core_diagnostics, &rules_result)?; - call_with( - ®istry::class(py, RecordId::CompileAnalysis)?, + new_record_with( + py, + RecordId::CompileAnalysis, &[ ("discovered_inputs", discovered), ("adapter", adapter), @@ -275,8 +277,9 @@ fn analyzed<'py>( /// `RulesRunResult` of a compile whose rules did not run. fn empty_rules_result(py: Python<'_>) -> PyResult> { let zero: Bound<'_, PyAny> = 0_i64.into_pyobject(py)?.into_any(); - call_with( - ®istry::class(py, RecordId::RulesRunResult)?, + new_record_with( + py, + RecordId::RulesRunResult, &[ ("findings", PyTuple::empty(py).into_any()), ("evaluated_models", zero.clone()), @@ -354,9 +357,9 @@ impl<'py> Driver<'_, 'py> { (self.early_lint.getattr("start")?,), Some(&keywords(py, &[("dialect", lint_dialect.clone())])?), )?; - let analysis_selection: Bound<'py, PyAny> = imported(py, "dataclasses", "replace")?.call( - (options.analysis_selection.clone(),), - Some(&keywords(py, &[("on_inputs_ready", on_inputs_ready)])?), + let analysis_selection: Bound<'py, PyAny> = replaced( + &options.analysis_selection.clone(), + &[("on_inputs_ready", on_inputs_ready)], )?; let lineage_mode: Bound<'py, PyAny> = column_lineage_mode( py, @@ -427,8 +430,9 @@ impl<'py> Driver<'_, 'py> { .getattr("skip_contracts")? .is_truthy()?; let result: Bound<'py, PyAny> = if skip { - call_with( - ®istry::class(py, RecordId::ContractValidationResult)?, + new_record_with( + py, + RecordId::ContractValidationResult, &[("diagnostics", PyTuple::empty(py).into_any())], )? } else { @@ -519,11 +523,8 @@ fn with_rule_diagnostics<'py>( core: &Bound<'py, PyTuple>, rules_result: &Bound<'py, PyAny>, ) -> PyResult> { - let diagnostic_class: Bound<'py, PyAny> = registry::class(py, RecordId::CompilerDiagnostic)?; - let phase: Bound<'py, PyAny> = - registry::class(py, RecordId::DiagnosticPhase)?.getattr("RULE")?; - let severity: Bound<'py, PyAny> = - registry::class(py, RecordId::DiagnosticSeverity)?.getattr("ERROR")?; + let phase: Bound<'py, PyAny> = enum_member(py, RecordId::DiagnosticPhase, "RULE")?; + let severity: Bound<'py, PyAny> = enum_member(py, RecordId::DiagnosticSeverity, "ERROR")?; let fix_note: Bound<'py, PyAny> = PyString::new(py, RULE_FIX_AVAILABLE_NOTE).into_any(); let mut diagnostics: Vec> = core.iter().collect(); for fault in native_field(rules_result, "findings")?.try_iter()? { @@ -533,8 +534,9 @@ fn with_rule_diagnostics<'py>( } else { PyTuple::empty(py) }; - diagnostics.push(call_with( - &diagnostic_class, + diagnostics.push(new_record_with( + py, + RecordId::CompilerDiagnostic, &[ ("phase", phase.clone()), ("severity", severity.clone()), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/analysis_cache.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/analysis_cache.rs index 410eb0dec9..c18fa7f098 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/analysis_cache.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/analysis_cache.rs @@ -9,12 +9,11 @@ use pyo3::{pyfunction, wrap_pyfunction}; use sqlbuild_cache::digest::main::sha256_hex::sha256_hex; use crate::bindings::_helpers::compile_driver::compile_timings::record_compile_metric; -use crate::bindings::_helpers::compile_driver::python_calls::{ - KeywordArguments, call_with, imported, -}; +use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, imported}; use crate::bindings::_helpers::planner_identity::json_values::{COMPACT, unicode_sorted_dumps}; +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; -use crate::bindings::_helpers::records::registry; const ANALYSIS_CACHE_VERSION: i64 = 13; const ANALYSIS_ALGORITHM_FINGERPRINT: &str = "model-sql-analysis-v21-completed-star-shapes"; @@ -73,8 +72,9 @@ pub(crate) fn analysis_cache_context<'py>( Err(error) if json_failure(py, &error) => return Ok(None), Err(error) => return Err(error), }; - let context = call_with( - ®istry::class(py, RecordId::AnalysisCacheContext)?, + let context = new_record_with( + py, + RecordId::AnalysisCacheContext, &[ ("root", root.clone()), ( @@ -194,7 +194,7 @@ fn inference_profile_payload<'py>( "semantic_known_functions", "semantic_known_types", ] { - payload.set_item(field, profile.getattr(field)?)?; + payload.set_item(field, native_field(profile, field)?)?; } payload.set_item("function_nullability_rules", rules)?; payload.set_item( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/assembly_analysis.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/assembly_analysis.rs index 29ba31d849..b58d9e7358 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/assembly_analysis.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/assembly_analysis.rs @@ -15,7 +15,7 @@ use crate::bindings::_helpers::compile_driver::assembly::{AssemblyRequest, Assem use crate::bindings::_helpers::compile_driver::compile_timings::{ record_compile_cpu_timing, record_compile_timing, }; -use crate::bindings::_helpers::compile_driver::python_calls::{imported, keywords}; +use crate::bindings::_helpers::compile_driver::python_calls::keywords; use crate::bindings::_helpers::compile_driver::stage_work::{ EXPRESSION_SHAPES, MODEL_ANALYSIS, count_stage, }; @@ -25,9 +25,10 @@ use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::project_assembly::declared_relations::{ DeclaredRelations, declared_relations, }; +use crate::bindings::_helpers::records::construct::{enum_member, record_class}; use crate::bindings::_helpers::records::fields::field as native_field; +use crate::bindings::_helpers::records::fields::replaced; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::semantic_shapes::declarations::{declared_types, function_names}; const GENERIC_DIALECT: &str = "generic"; @@ -127,8 +128,7 @@ pub(crate) fn analysis_context<'py>( /// `_build_dynamic_families_by_table`: enforced contract models' dynamic column families. fn dynamic_families_by_table<'py>(inputs: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = inputs.py(); - let enforced: Bound<'py, PyAny> = - registry::class(py, RecordId::ContractPolicy)?.getattr("ENFORCED")?; + let enforced: Bound<'py, PyAny> = enum_member(py, RecordId::ContractPolicy, "ENFORCED")?; let families: Bound<'py, PyDict> = PyDict::new(py); for model_input in native_field(inputs, "model_inputs")?.try_iter()? { let model_input: Bound<'py, PyAny> = model_input?; @@ -169,7 +169,6 @@ fn profile_with_catalog<'py>( column_types: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = inputs.py(); - let replace: Bound<'py, PyAny> = imported(py, "dataclasses", "replace")?; let profile: Bound<'py, PyAny> = if inference_profile.is_truthy()? { inference_profile.clone() } else { @@ -193,21 +192,18 @@ fn profile_with_catalog<'py>( Some(native) if Some(native) != name.as_deref() => PyString::new(py, native).into_any(), _ => dialect, }; - let profile: Bound<'py, PyAny> = replace.call( - (profile.clone(),), - Some(&keywords( - py, - &[ - ("semantic_known_functions", known_functions), - ("semantic_known_types", known_types), - ("quoted_identifiers_ignore_case", ignore_case), - ("sql_analysis_dialect", native), - ( - "function_return_types", - function_return_types(inputs, &profile)?.into_any(), - ), - ], - )?), + let profile: Bound<'py, PyAny> = replaced( + &profile.clone(), + &[ + ("semantic_known_functions", known_functions), + ("semantic_known_types", known_types), + ("quoted_identifiers_ignore_case", ignore_case), + ("sql_analysis_dialect", native), + ( + "function_return_types", + function_return_types(inputs, &profile)?.into_any(), + ), + ], )?; let dialect: Bound<'py, PyAny> = native_field(&profile, "sql_analysis_dialect")?; let dialect: String = if dialect.is_truthy()? { @@ -231,10 +227,7 @@ fn profile_with_catalog<'py>( )?, )? .into_any(); - replace.call( - (profile,), - Some(&keywords(py, &[("binding_catalog", catalog)])?), - ) + replaced(&profile, &[("binding_catalog", catalog)]) } /// The profile's function return types with each scalar project function's analysis name. @@ -305,8 +298,7 @@ fn record_expression_shapes<'py>( "zip() argument 2 is {relation} than argument 1" ))); } - let unknown: Bound<'py, PyAny> = - registry::class(py, RecordId::InferredNullability)?.getattr("UNKNOWN")?; + let unknown: Bound<'py, PyAny> = enum_member(py, RecordId::InferredNullability, "UNKNOWN")?; for (source_input, shape) in sources.iter().zip(shapes) { if shape.is_none() { continue; @@ -351,7 +343,7 @@ fn model_analyses<'py>( let inputs: &Bound<'py, PyAny> = request.inputs.bind(py); let names: &Bound<'py, PyAny> = request.analysis_model_names.bind(py); let mode: &Bound<'py, PyAny> = request.column_lineage_mode.bind(py); - let lineage_mode: Bound<'py, PyAny> = registry::class(py, RecordId::ColumnLineageMode)?; + let lineage_mode: Bound<'py, PyAny> = record_class(py, RecordId::ColumnLineageMode)?; let rich: bool = mode.eq(native_field(&lineage_mode, "RICH")?)?; let allow_compact: bool = mode.eq(native_field(&lineage_mode, "FAST")?)? || rich; let flags: (Bound<'py, PyAny>, Bound<'py, PyAny>) = ( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/assembly_checks.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/assembly_checks.rs index 621e9178cc..74c1228db6 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/assembly_checks.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/assembly_checks.rs @@ -4,11 +4,14 @@ use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; use pyo3::types::{PyBool, PyDict, PyDictMethods, PyTuple}; use crate::bindings::_helpers::attachments::cursor_intrinsics::validated_model_cursor_sql; -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported, keywords}; +use crate::bindings::_helpers::compile_driver::python_calls::keywords; use crate::bindings::_helpers::contracts::spec_resolution::resolve_effective_adapter_name; +use crate::bindings::_helpers::records::construct::{ + enum_member, new_record, new_record_with, record_class, +}; use crate::bindings::_helpers::records::fields::field as native_field; +use crate::bindings::_helpers::records::fields::replaced; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::render::declaration_values::declaration_scope_resolver; use crate::bindings::_helpers::render::macro_expansion_result::{ ExpansionRequest, expansion_result, @@ -39,7 +42,7 @@ pub(crate) fn test_model_query_overrides<'py>( let py: Python<'py> = inputs.py(); let overrides: Bound<'py, PyDict> = PyDict::new(py); let payload: Bound<'py, PyAny> = native_field(test_input, "payload")?; - if !payload.is_instance(®istry::class( + if !payload.is_instance(&record_class( py, RecordId::CompileModelSqlTestInputPayload, )?)? { @@ -49,15 +52,12 @@ pub(crate) fn test_model_query_overrides<'py>( if !macro_mocks.is_truthy()? { return Ok(overrides); } - let macro_context: Bound<'py, PyAny> = imported(py, "dataclasses", "replace")?.call( - (model_macro_context(inputs)?,), - Some(&keywords( - py, - &[( - "_enforce_explicit_references", - PyBool::new(py, false).to_owned().into_any(), - )], - )?), + let macro_context: Bound<'py, PyAny> = replaced( + &model_macro_context(inputs)?, + &[( + "_enforce_explicit_references", + PyBool::new(py, false).to_owned().into_any(), + )], )?; let declaration_scope: Bound<'py, PyAny> = native_field(inputs, "declaration_scope")?; let lookup: Bound<'py, PyAny> = if declaration_scope.is_none() { @@ -119,8 +119,9 @@ fn model_macro_context<'py>(inputs: &Bound<'py, PyAny>) -> PyResult( inputs: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = inputs.py(); - let scenario_class: Bound<'py, PyAny> = registry::class(py, RecordId::CompiledSqlScenario)?; - let key_class: Bound<'py, PyAny> = registry::class(py, RecordId::CompiledObjectKey)?; let resource_type: Bound<'py, PyAny> = - registry::class(py, RecordId::CompiledResourceType)?.getattr("SQL_SCENARIO")?; + enum_member(py, RecordId::CompiledResourceType, "SQL_SCENARIO")?; let mut scenarios: Vec> = Vec::new(); for scenario_input in native_field(inputs, "scenario_inputs")?.try_iter()? { let scenario_input: Bound<'py, PyAny> = scenario_input?; let scenario_file: Bound<'py, PyAny> = native_field(&scenario_input, "scenario_file")?; let name: Bound<'py, PyAny> = native_field(&scenario_file, "name")?; - let key: Bound<'py, PyAny> = call_with( - &key_class, + let key: Bound<'py, PyAny> = new_record_with( + py, + RecordId::CompiledObjectKey, &[ ("resource_type", resource_type.clone()), ("name", name.clone()), @@ -168,7 +168,7 @@ pub(crate) fn compiled_sql_scenarios<'py>( ], )?; for field in SCENARIO_FIELDS { - fields.set_item(field, scenario_input.getattr(field)?)?; + fields.set_item(field, native_field(&scenario_input, field)?)?; } fields.set_item( "source_path", @@ -178,7 +178,12 @@ pub(crate) fn compiled_sql_scenarios<'py>( "ownership_root", native_field(&scenario_file, "ownership_root")?, )?; - scenarios.push(scenario_class.call((), Some(&fields))?); + scenarios.push(new_record( + py, + RecordId::CompiledSqlScenario, + (), + Some(&fields), + )?); } PyTuple::new(py, scenarios) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/assembly_facts.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/assembly_facts.rs index 098a62cf58..60ae289112 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/assembly_facts.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/assembly_facts.rs @@ -6,7 +6,6 @@ use pyo3::types::{PyDict, PyDictMethods, PyFrozenSet, PyListMethods, PyTuple, Py use crate::bindings::_helpers::analysis_session::stage_functions::dynamic_contract_proofs; use crate::bindings::_helpers::compile_driver::assembly_analysis::AnalysisContext; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; use crate::bindings::_helpers::compile_driver::stage_work::{ PIVOT_PROOFS, SESSION_PIVOT_PROOFS, count_stage, }; @@ -14,9 +13,9 @@ use crate::bindings::_helpers::project_assembly::models::{NativeModelFacts, Nati use crate::bindings::_helpers::project_assembly::native_resources::assemble_native_project_resources; use crate::bindings::_helpers::project_assembly::syntax_checks::model_syntax_checks; use crate::bindings::_helpers::project_assembly::syntax_checks::standalone_pivot_models; +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; -use crate::bindings::_helpers::records::registry; use pyo3::IntoPyObjectExt; /// `NativeProjectFacts` of `inputs`: resources, syntax validity, deps and standalone pivot proofs. @@ -88,8 +87,9 @@ pub(crate) fn native_project_facts<'py>( /// The declared relation facts standalone pivot proofs read. pub(crate) fn pivot_tables<'py>(context: &AnalysisContext<'py>) -> PyResult> { let py: Python<'py> = context.profile.py(); - call_with( - ®istry::class(py, RecordId::NativePivotTables)?, + new_record_with( + py, + RecordId::NativePivotTables, &[ ( "dialect", diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/assembly_models.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/assembly_models.rs index 51f6e19b4d..53f77e3ee1 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/assembly_models.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/assembly_models.rs @@ -13,16 +13,15 @@ use crate::bindings::_helpers::boundary::panics::NativeCompilerError; use crate::bindings::_helpers::compile_driver::assembly_analysis::{ AnalysisContext, model_stem, selected_for_analysis, }; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; use crate::bindings::_helpers::compiled_project::model_input_view::NativeModelInput; use crate::bindings::_helpers::hand_built::compiled_model::compiled_model as hand_built_model; use crate::bindings::_helpers::project_assembly::syntax_checks::model_placeholders; use crate::bindings::_helpers::project_assembly::syntax_validation::{ ParseOptions, validate_hook_sql_syntax, validate_sql_syntax, }; +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; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::render::macro_calls::top_level_macro_call_names; const NATIVE_FAILURE_PREFIX: &str = "NativeCompilerError: "; @@ -183,8 +182,9 @@ fn compiled_model<'py>( ),), ); } - let analysis: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::HandBuiltAnalysis)?, + let analysis: Bound<'py, PyAny> = new_record_with( + py, + RecordId::HandBuiltAnalysis, &[ ("session", session_analysis), ("fallback", fallback), @@ -209,8 +209,9 @@ fn fallback_columns<'py>( placeholders: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = analysis_query_sql.py(); - let query: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::NativeColumnQuery)?, + let query: Bound<'py, PyAny> = new_record_with( + py, + RecordId::NativeColumnQuery, &[ ("sql", analysis_query_sql.clone()), ("mode", PyString::new(py, "parse").into_any()), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/assembly_project.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/assembly_project.rs index 6e01d7a4de..45aedec4b9 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/assembly_project.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/assembly_project.rs @@ -26,9 +26,9 @@ use crate::bindings::_helpers::contracts::spec_resolution::{ use crate::bindings::_helpers::model_config::template_data::early_model_templates; use crate::bindings::_helpers::native_host::adapter::AdapterHost; use crate::bindings::_helpers::native_host::host; +use crate::bindings::_helpers::records::construct::new_record; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::scopes::scope_errors::report_scope_index_errors; use crate::bindings::_helpers::scopes::scope_validation::scope_index_with_compile_usages; use crate::bindings::_helpers::semantic_checks::stage::{ @@ -162,7 +162,7 @@ fn target_field<'py>(inputs: &Bound<'py, PyAny>, field: &str) -> PyResult( fields.set_item("sql_tests", assembled_sql_tests(inputs)?)?; fields.set_item("sql_scenarios", compiled_sql_scenarios(inputs)?)?; for name in ["loader_functions", "hook_functions"] { - fields.set_item(name, discovered.getattr(name)?)?; + fields.set_item(name, native_field(&discovered, name)?)?; } fields.set_item( "enforce_explicit_references", @@ -290,10 +290,10 @@ fn project_fields<'py>( .getattr("enforce_explicit")?, )?; for name in ["sql_hook_files", "materialization_files"] { - fields.set_item(name, discovered.getattr(name)?)?; + fields.set_item(name, native_field(&discovered, name)?)?; } for name in ["public_enums", "public_constants", "loaded_macros"] { - fields.set_item(name, inputs.getattr(name)?)?; + fields.set_item(name, native_field(inputs, name)?)?; } let mut diagnostics: Vec> = inputs .getattr("diagnostics")? @@ -319,12 +319,12 @@ fn compiled_resources<'py>( kind: &ResourceKind<'_>, ) -> PyResult> { let py: Python<'py> = inputs.py(); - let count: usize = inputs.getattr(kind.inputs)?.len()?; + let count: usize = native_field(inputs, kind.inputs)?.len()?; if count == 0 { return Ok(PyTuple::empty(py)); } let owner: Bound<'py, PyAny> = native_field(inputs, "native_project")?; - let facts: Bound<'py, PyAny> = resources.getattr(kind.facts)?; + let facts: Bound<'py, PyAny> = native_field(resources, kind.facts)?; owner.call_method1( kind.record, (PyList::new( @@ -392,5 +392,5 @@ fn project_view<'py>( } let arguments: Bound<'py, PyDict> = fields.copy()?; arguments.set_item("sql_expansions", sql_expansions)?; - registry::class(py, RecordId::CompiledProject)?.call((), Some(&arguments)) + new_record(py, RecordId::CompiledProject, (), Some(&arguments)) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/assembly_sql_tests.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/assembly_sql_tests.rs index 0fcb82dafc..ad639f770d 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/assembly_sql_tests.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/assembly_sql_tests.rs @@ -8,6 +8,7 @@ use crate::bindings::_helpers::compile_driver::assembly_checks::test_model_query use crate::bindings::_helpers::compile_driver::diagnostic_collector::report_compile_diagnostic; use crate::bindings::_helpers::compile_driver::python_calls::{imported, keywords}; use crate::bindings::_helpers::compile_driver::stage_work::{SQL_TEST_ASSEMBLY, count_stage}; +use crate::bindings::_helpers::records::construct::record_class; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -36,7 +37,7 @@ pub(crate) fn assembled_sql_tests<'py>( )); } let mocked_input: Bound<'py, PyAny> = - registry::class(py, RecordId::CompileModelSqlTestInputPayload)?; + record_class(py, RecordId::CompileModelSqlTestInputPayload)?; let mut tests: Vec> = Vec::with_capacity(assemblies.len()); for (test_input, assembly) in test_inputs.iter().zip(assemblies) { let failure: Bound<'py, PyAny> = native_field(&assembly, "failure")?; @@ -98,7 +99,7 @@ fn with_model_queries<'py>( ) -> PyResult> { let py: Python<'py> = test.py(); let payload: Bound<'py, PyAny> = native_field(test, "payload")?; - if !payload.is_instance(®istry::class(py, RecordId::CompiledModelSqlTestPayload)?)? { + if !payload.is_instance(&record_class(py, RecordId::CompiledModelSqlTestPayload)?)? { return Ok(test.clone()); } let replace: Bound<'py, PyAny> = imported(py, DATACLASSES_MODULE, "replace")?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/command.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/command.rs index 985f4ae027..6f06f8d174 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/command.rs @@ -27,7 +27,9 @@ use crate::bindings::_helpers::compile_driver::python_calls::{ Status, call_with, imported, keywords, within, }; use crate::bindings::_helpers::presentation_text::terminal::stdout_supports_color; +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::replaced; use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::rules::results::format_skipped_type_proof_rules; @@ -53,7 +55,7 @@ struct Recorders<'py> { /// Run one full compile, recording it for reuse; returns the exit code. #[pyfunction] #[pyo3(signature = (request, reuse_attempt=None))] -fn run_compile_command<'py>( +pub(crate) fn run_compile_command<'py>( py: Python<'py>, request: Bound<'py, PyAny>, reuse_attempt: Option>, @@ -64,12 +66,9 @@ fn run_compile_command<'py>( }; let path_class: Bound<'py, PyAny> = imported(py, "pathlib", "Path")?; let effective_request: Bound<'py, PyAny> = if native_field(&request, "project_dir")?.is_none() { - imported(py, "dataclasses", "replace")?.call( - (request.clone(),), - Some(&keywords( - py, - &[("project_dir", path_class.call_method0("cwd")?)], - )?), + replaced( + &request.clone(), + &[("project_dir", path_class.call_method0("cwd")?)], )? } else { request.clone() @@ -335,8 +334,9 @@ fn analysis_options<'py>( status: &Status<'py>, ) -> PyResult> { let py: Python<'py> = request.py(); - let selection: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::CompileAnalysisSelection)?, + let selection: Bound<'py, PyAny> = new_record_with( + py, + RecordId::CompileAnalysisSelection, &[ ("select", native_field(request, "select")?), ("exclude", native_field(request, "exclude")?), @@ -365,8 +365,7 @@ fn analysis_options<'py>( /// Whether a rule finding failed the compile. fn rules_failed(analysis: &Bound<'_, PyAny>) -> PyResult { let py: Python<'_> = analysis.py(); - let rule_phase: Bound<'_, PyAny> = - registry::class(py, RecordId::DiagnosticPhase)?.getattr("RULE")?; + let rule_phase: Bound<'_, PyAny> = enum_member(py, RecordId::DiagnosticPhase, "RULE")?; for diagnostic in native_field(analysis, "diagnostics")?.try_iter()? { let diagnostic: Bound<'_, PyAny> = diagnostic?; if native_field(&diagnostic, "is_error")?.is_truthy()? @@ -399,7 +398,7 @@ fn timings_ms<'py>( ("rule_cache_hits", "rule_cache_hits"), ("rule_cache_misses", "rule_cache_misses"), ] { - timings.set_item(key, analysis.getattr(attribute)?)?; + timings.set_item(key, native_field(analysis, attribute)?)?; } timings.set_item("write_ms", write_ms)?; for (key, value) in timing_milliseconds(detailed_timings) { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/config_validation.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/config_validation.rs index 641a7c4d1e..c39a14c5bd 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/config_validation.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/config_validation.rs @@ -68,7 +68,7 @@ fn reject_local_sql_analysis_opt_out(config: &ProjectConfiguration<'_, '_>) -> P .getattr("setting_overrides")? .contains(SQL_ANALYSIS_KEY) .map(|overridden| PyBool::new(py, overridden).to_owned().into_any())?, - local_settings.getattr(SQL_ANALYSIS_KEY)?, + native_field(&local_settings, SQL_ANALYSIS_KEY)?, ], )?; validate_local_sql_analysis(py, config.project_dir, flags.extract()?) @@ -82,7 +82,7 @@ fn first_configured_group(config: &Bound<'_, PyAny>) -> PyResult = native_field(&targets.get_item(&name)?, "warehouses")?; for group in WAREHOUSE_GROUPS { - let value: Bound<'_, PyAny> = warehouses.getattr(group)?; + let value: Bound<'_, PyAny> = native_field(&warehouses, group)?; if !value.is_none() { return Ok(Some((name, group.to_owned(), value.extract()?))); } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/deferral.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/deferral.rs index b71ccba1b0..220cab538d 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/deferral.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/deferral.rs @@ -11,6 +11,7 @@ use sqlbuild_model_config::templates::models::DeferredSegment; use crate::bindings::_helpers::authored_values::project_vars::render_project_var_text; use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; +use crate::bindings::_helpers::records::construct::{new_record, new_record_with}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -70,7 +71,7 @@ fn deferred_locations<'py>( ) -> PyResult> { let locations: Bound<'py, PyDict> = PyDict::new(project.py()); for attribute in ["models", "seeds"] { - for resource in project.getattr(attribute)?.try_iter()? { + for resource in native_field(project, attribute)?.try_iter()? { let resource: Bound<'py, PyAny> = resource?; let location: Bound<'py, PyAny> = deferred_location(&native_field(&resource, "destination")?, naming)?; @@ -113,8 +114,9 @@ fn deferred_location<'py>( ("name", name.clone()), ], )?; - call_with( - ®istry::class(py, RecordId::CompiledRelationLocation)?, + new_record_with( + py, + RecordId::CompiledRelationLocation, &[ ("database", database), ("schema", schema), @@ -290,7 +292,7 @@ fn deferred_relations<'py>( /// Resolve native target locations and warehouse relations for model deferral. #[pyfunction] #[pyo3(signature = (**options))] -fn resolve_model_deferral_inputs<'py>( +pub(crate) fn resolve_model_deferral_inputs<'py>( py: Python<'py>, options: Option>, ) -> PyResult> { @@ -309,10 +311,9 @@ fn resolve_model_deferral_inputs<'py>( &[], ), )?; - let inputs: Bound<'py, PyAny> = registry::class(py, RecordId::DeferralInputs)?; let defer_to: Bound<'py, PyAny> = arguments.value("defer_to")?; if defer_to.is_none() { - return inputs.call0(); + return new_record(py, RecordId::DeferralInputs, (), None); } let project: Bound<'py, PyAny> = arguments.value("project")?; let adapter: Bound<'py, PyAny> = arguments.value("adapter")?; @@ -336,8 +337,9 @@ fn resolve_model_deferral_inputs<'py>( &arguments.value("connection")?, locations.as_any(), )?; - call_with( - &inputs, + new_record_with( + py, + RecordId::DeferralInputs, &[ ("deferred_locations", locations.into_any()), ("deferred_relations", relations.into_any()), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/diff.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/diff.rs index 533040b9f5..234caaeea8 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/diff.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/diff.rs @@ -16,6 +16,7 @@ use crate::bindings::_helpers::analysis::model_reads; use crate::bindings::_helpers::compile_driver::project::build_compiled_project; use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, keywords}; use crate::bindings::_helpers::graph::views::project_lineage_views; +use crate::bindings::_helpers::records::construct::enum_member; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::ErrorId; use crate::bindings::_helpers::records::ids::RecordId; @@ -63,7 +64,7 @@ fn diff_graph<'py>(project: &Bound<'py, PyAny>, keys: &KeyIndexes<'py>) -> PyRes let lineage: Bound<'py, PyAny> = project_lineage_views(py, project)?; let mut graph: DiffGraph = DiffGraph::default(); for attribute in ["models", "sources", "seeds"] { - for resource in project.getattr(attribute)?.try_iter()? { + for resource in native_field(project, attribute)?.try_iter()? { let resource: Bound<'py, PyAny> = resource?; let name: String = native_field(&resource, "name")?.extract()?; graph @@ -161,8 +162,7 @@ fn diff_model_names<'py>( let exclude: Vec = exclude.extract()?; let selected: HashSet = resolve_diff_selection(&graph, (&select, &exclude)) .map_err(|error| selector_error(py, error))?; - let model_type: Bound<'py, PyAny> = - registry::class(py, RecordId::CompiledResourceType)?.getattr("MODEL")?; + let model_type: Bound<'py, PyAny> = enum_member(py, RecordId::CompiledResourceType, "MODEL")?; let mut names: Vec> = Vec::new(); for model in native_field(project, "models")?.try_iter()? { let model: Bound<'py, PyAny> = model?; @@ -181,7 +181,7 @@ fn diff_model_names<'py>( /// Compile both environments and resolve selected diffable model names. #[pyfunction] #[pyo3(signature = (**options))] -fn run_diff_pipeline<'py>( +pub(crate) fn run_diff_pipeline<'py>( py: Python<'py>, options: Option>, ) -> PyResult> { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/discovery.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/discovery.rs index 92e7aeb24d..e67763d24c 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/discovery.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/discovery.rs @@ -43,9 +43,9 @@ use crate::bindings::_helpers::frontier::compile_frontier::{FrontierStage, produ use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::native_host::user_code::UserCodeHost; +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; -use crate::bindings::_helpers::records::registry; use pyo3::IntoPyObjectExt; const SQL_ANALYSIS_SETTING_KEY: &str = "sql_analysis"; @@ -398,7 +398,7 @@ fn imported_python_paths(python: &Bound<'_, PyDict>) -> PyResult { let mut groups: Vec> = vec![python.get_item("providers")?]; let nodes: Bound<'_, PyAny> = python.get_item("python_nodes")?; for group in PYTHON_NODE_GROUPS { - groups.push(nodes.getattr(group)?); + groups.push(native_field(&nodes, group)?); } for field in [ "materialization_files", @@ -433,8 +433,7 @@ fn project_inputs<'py>( let tuple_of = |group: &str| -> PyResult> { Ok(PyTuple::new( py, - nodes - .getattr(group)? + native_field(&nodes, group)? .try_iter()? .collect::>>()?, )? @@ -485,10 +484,7 @@ fn project_inputs<'py>( } fields.push(("adapter_file", declarations.get_item("adapter_file")?)); fields.push(("native_session", declarations.get_item("native_session")?)); - call_with( - ®istry::class(py, RecordId::DiscoveredProjectInputs)?, - &fields, - ) + new_record_with(py, RecordId::DiscoveredProjectInputs, &fields) } pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/inputs.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/inputs.rs index 562e243f7a..818cce8ad6 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/inputs.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/inputs.rs @@ -11,7 +11,7 @@ use crate::bindings::_helpers::compile_attachment::effective_config::{ use crate::bindings::_helpers::compile_attachment::model_inputs::build_model_inputs; use crate::bindings::_helpers::compile_driver::diagnostic_collector::collect_compile_diagnostics; use crate::bindings::_helpers::compile_driver::python_calls::{ - KeywordArguments, call_with, imported, keywords, within, + KeywordArguments, call_with, keywords, within, }; use crate::bindings::_helpers::compiled_project::attachment::{attach_checks, attach_resources}; use crate::bindings::_helpers::compiled_project::project::NativeCompiledProject; @@ -20,9 +20,10 @@ use crate::bindings::_helpers::contracts::spec_resolution::resolve_effective_ada use crate::bindings::_helpers::model_config::template_data::run_id; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::native_host::user_code::UserCodeHost; +use crate::bindings::_helpers::records::construct::new_record_with; use crate::bindings::_helpers::records::fields::field as native_field; +use crate::bindings::_helpers::records::fields::replaced; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::render::declaration_indexes::{ public_declaration_indexes, public_model_schema_index, }; @@ -158,8 +159,9 @@ fn compile_inputs<'py>(py: Python<'py>, request: &InputsRequest) -> PyResult = native_field(&adapter_context, "value_renderer")?; - let model_context: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::ModelInputBuildContext)?, + let model_context: Bound<'py, PyAny> = new_record_with( + py, + RecordId::ModelInputBuildContext, &[ ("effective_vars", target.vars.clone()), ("effective_settings", target.settings.clone()), @@ -275,8 +277,9 @@ fn target_context<'py>( build_effective_settings(&project_config.clone(), &local_config.clone())?; let adapter_name: Bound<'py, PyAny> = resolve_effective_adapter_name(&project_config.clone(), &local_config)?; - let macro_context: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::MacroContext)?, + let macro_context: Bound<'py, PyAny> = new_record_with( + py, + RecordId::MacroContext, &[ ("adapter_name", adapter_name), ( @@ -324,17 +327,13 @@ fn model_build<'py>( let schemas: Bound<'py, PyAny> = public_model_schema_index(discovered.clone())?.into_any(); let native_project: Bound<'py, PyAny> = Bound::new(py, NativeCompiledProject::default())?.into_any(); - let replace: Bound<'py, PyAny> = imported(py, "dataclasses", "replace")?; - let model_context: Bound<'py, PyAny> = replace.call( - (context,), - Some(&keywords( - py, - &[ - ("public_enums", enums.clone()), - ("public_constants", constants.clone()), - ("public_model_schemas", schemas), - ], - )?), + let model_context: Bound<'py, PyAny> = replaced( + context, + &[ + ("public_enums", enums.clone()), + ("public_constants", constants.clone()), + ("public_model_schemas", schemas), + ], )?; let inputs: Bound<'py, PyAny> = build_model_inputs( py, @@ -360,15 +359,12 @@ fn model_build<'py>( )?), )? .into_any(); - let context: Bound<'py, PyAny> = replace.call( - (context,), - Some(&keywords( - py, - &[ - ("public_enums", enums.clone()), - ("public_constants", constants.clone()), - ], - )?), + let context: Bound<'py, PyAny> = replaced( + context, + &[ + ("public_enums", enums.clone()), + ("public_constants", constants.clone()), + ], )?; Ok(ModelBuild { inputs, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/integration_loaders.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/integration_loaders.rs index 07eb7e45d2..4169bc2566 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/integration_loaders.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/integration_loaders.rs @@ -8,9 +8,9 @@ use sqlbuild_discovery::yaml_entries::main::integration_loader_name::integration use crate::bindings::_helpers::compile_driver::python_calls::call_with; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::native_host::third_party::ThirdPartyHost; +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::ids::RecordId; -use crate::bindings::_helpers::records::registry; /// One integration: its kind constant and the runtime module building its loader functions. struct Integration { @@ -35,9 +35,7 @@ pub(crate) fn integration_loader_functions<'py>( py: Python<'py>, source_files: &Bound<'py, PyAny>, ) -> PyResult> { - let record_class: Bound<'py, PyAny> = registry::class(py, RecordId::DiscoveredLoaderFunction)?; - let external: Bound<'py, PyAny> = - registry::class(py, RecordId::LoaderConnectionMode)?.getattr("EXTERNAL")?; + let external: Bound<'py, PyAny> = enum_member(py, RecordId::LoaderConnectionMode, "EXTERNAL")?; let mut loaders: Vec> = Vec::new(); for integration in &INTEGRATIONS { let kind: String = host::entry(py, integration.kind_constant)?.extract()?; @@ -64,8 +62,9 @@ pub(crate) fn integration_loader_functions<'py>( ("name", name.clone().into_pyobject(py)?.into_any()), ], )?; - loaders.push(call_with( - &record_class, + loaders.push(new_record_with( + py, + RecordId::DiscoveredLoaderFunction, &[ ("file_path", native_field(&source_file, "file_path")?), ( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/phases.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/phases.rs index 0c8d7bb8db..0e61ba02be 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/phases.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/phases.rs @@ -15,6 +15,7 @@ use crate::bindings::_helpers::contracts::spec_resolution::resolve_effective_ada use crate::bindings::_helpers::dag_artifact::artifact::build_dag_json; use crate::bindings::_helpers::graph::python_node_graph::python_node_graph; use crate::bindings::_helpers::manifest::builder::build_manifest; +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::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -175,16 +176,14 @@ pub(crate) fn sql_test_planning_diagnostics<'py>( } else { py.None().into_bound(py) }; - let diagnostic: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::CompilerDiagnostic)?, + let diagnostic: Bound<'py, PyAny> = new_record_with( + py, + RecordId::CompilerDiagnostic, &[ - ( - "phase", - registry::class(py, RecordId::DiagnosticPhase)?.getattr("TEST")?, - ), + ("phase", enum_member(py, RecordId::DiagnosticPhase, "TEST")?), ( "severity", - registry::class(py, RecordId::DiagnosticSeverity)?.getattr("ERROR")?, + enum_member(py, RecordId::DiagnosticSeverity, "ERROR")?, ), ("code", native_field(&value, "code")?), ("message", value.str()?.into_any()), @@ -207,8 +206,9 @@ pub(crate) fn write_artifacts<'py>( let target: Bound<'py, PyAny> = target_dir(project_dir)?; let written: Bound<'py, PyAny> = if skip_write { let zero: Bound<'py, PyAny> = 0_i64.into_pyobject(py)?.into_any(); - call_with( - ®istry::class(py, RecordId::WrittenTarget)?, + new_record_with( + py, + RecordId::WrittenTarget, &[ ("model_count", zero.clone()), ("seed_count", zero.clone()), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/pipeline.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/pipeline.rs index b5de764392..bddb0345ed 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/pipeline.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/pipeline.rs @@ -13,9 +13,7 @@ use crate::bindings::_helpers::compile_driver::plan_result::{ PlanningInputs, connection_with_hooks, pipeline_result, }; use crate::bindings::_helpers::compile_driver::project::build_compiled_project; -use crate::bindings::_helpers::compile_driver::python_calls::{ - KeywordArguments, call_with, imported, keywords, -}; +use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, keywords}; use crate::bindings::_helpers::compile_driver::selection_phases::{ apply_unit_test_selectors, resolve_configured_rule_selection, }; @@ -23,7 +21,9 @@ use crate::bindings::_helpers::diagnostics::trackers::current_tracker; use crate::bindings::_helpers::frontier::stage_capture::write_plan_capture; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; +use crate::bindings::_helpers::records::construct::{enum_member, new_record, new_record_with}; use crate::bindings::_helpers::records::fields::field as native_field; +use crate::bindings::_helpers::records::fields::replaced; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::rules::results::{format_rules_result, rules_progress}; @@ -81,7 +81,7 @@ fn compiled_phase<'py>( let options: Bound<'py, PyAny> = if options.is_truthy()? { options.clone() } else { - registry::class(py, RecordId::CompilePipelineOptions)?.call0()? + new_record(py, RecordId::CompilePipelineOptions, (), None)? }; let configured: Bound<'py, PyAny> = native_field(&options, "connection_config")?; let connection_config: Bound<'py, PyAny> = if configured.is_none() { @@ -134,8 +134,9 @@ fn compiled_phase<'py>( on_progress, &format!("Compiled project. ({compile_seconds:.2}s)"), )?; - call_with( - ®istry::class(py, RecordId::CompiledProjectPhaseResult)?, + new_record_with( + py, + RecordId::CompiledProjectPhaseResult, &[ ("project", project), ("connection_config", connection_config), @@ -155,8 +156,7 @@ fn reject_compile_errors( ) -> PyResult<()> { let py: Python<'_> = project.py(); let plan_sql_tests: bool = native_field(options, "plan_sql_tests")?.is_truthy()?; - let sql_test: Bound<'_, PyAny> = - registry::class(py, RecordId::CompiledResourceType)?.getattr("SQL_TEST")?; + let sql_test: Bound<'_, PyAny> = enum_member(py, RecordId::CompiledResourceType, "SQL_TEST")?; let mut errors: Vec> = Vec::new(); for diagnostic in native_field(project, "diagnostics")?.try_iter()? { let diagnostic: Bound<'_, PyAny> = diagnostic?; @@ -174,7 +174,7 @@ fn reject_compile_errors( let helps: Bound<'_, PySet> = PySet::empty(py)?; for diagnostic in &errors { let or_one = |name: &str| -> PyResult { - let value: Bound<'_, PyAny> = diagnostic.getattr(name)?; + let value: Bound<'_, PyAny> = native_field(diagnostic, name)?; Ok(if value.is_truthy()? { value.str()?.to_string() } else { @@ -209,7 +209,7 @@ fn reject_compile_errors( /// Run compile inputs, assembly, planning, and manifest generation. #[pyfunction] #[pyo3(signature = (**options))] -fn run_compile_pipeline<'py>( +pub(crate) fn run_compile_pipeline<'py>( py: Python<'py>, options: Option>, ) -> PyResult> { @@ -259,12 +259,12 @@ fn run_rules_gate<'py>( let mut config: Bound<'py, PyAny> = load_rules_config(py, project_dir.extract()?)?.into_bound(py); if native_field(options, "no_cache")?.is_truthy()? { - let cache: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::RulesCacheConfig)?, + let cache: Bound<'py, PyAny> = new_record_with( + py, + RecordId::RulesCacheConfig, &[("enabled", false.into_pyobject(py)?.to_owned().into_any())], )?; - config = imported(py, "dataclasses", "replace")? - .call((config,), Some(&keywords(py, &[("cache", cache)])?))?; + config = replaced(&config, &[("cache", cache)])?; } progress(on_progress, "Evaluating configured rules...")?; let started: f64 = monotonic(py)?; @@ -303,8 +303,9 @@ fn run_rules_gate<'py>( if !findings.is_truthy()? { return Ok(()); } - let summary: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::RulesResult)?, + let summary: Bound<'py, PyAny> = new_record_with( + py, + RecordId::RulesResult, &[ ("findings", findings), ( @@ -362,21 +363,18 @@ fn planned<'py>( } let result: Bound<'py, PyAny> = result?; write_plan_capture(&native_field(&result, "plan_output")?)?; - imported(py, "dataclasses", "replace")?.call( - (result,), - Some(&keywords( - py, - &[ - ( - "compile_seconds", - native_field(compiled, "compile_seconds")?, - ), - ( - "planning_seconds", - planning_seconds.into_pyobject(py)?.into_any(), - ), - ], - )?), + replaced( + &result, + &[ + ( + "compile_seconds", + native_field(compiled, "compile_seconds")?, + ), + ( + "planning_seconds", + planning_seconds.into_pyobject(py)?.into_any(), + ), + ], ) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/pipeline_entries.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/pipeline_entries.rs index 4a3a3da6c6..b320ca6221 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/pipeline_entries.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/pipeline_entries.rs @@ -5,13 +5,11 @@ use pyo3::types::{PyDict, PyFloat}; use pyo3::{pyfunction, wrap_pyfunction}; use crate::bindings::_helpers::compile_driver::pipeline::compile_project_phase; -use crate::bindings::_helpers::compile_driver::python_calls::{ - KeywordArguments, call_with, keywords, -}; +use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, keywords}; use crate::bindings::_helpers::graph::project_graph::{CompiledProjectGraph, NativeProjectGraph}; +use crate::bindings::_helpers::records::construct::{new_record, new_record_with}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; /// Discovered Python node functions whose selectors need the Python graph. const PYTHON_NODE_FUNCTIONS: [&str; 4] = [ @@ -57,12 +55,11 @@ pub(crate) fn compile_analysis_selection<'py>( let py: Python<'py> = options.py(); let mut has_python_nodes: bool = false; for name in PYTHON_NODE_FUNCTIONS { - if discovered.getattr(name)?.is_truthy()? { + if native_field(discovered, name)?.is_truthy()? { has_python_nodes = true; break; } } - let selection: Bound<'py, PyAny> = registry::class(py, RecordId::CompileAnalysisSelection)?; let no_cache: Bound<'py, PyAny> = native_field(options, "no_cache")?; if (options .getattr("resolve_python_run_selectors")? @@ -73,10 +70,15 @@ pub(crate) fn compile_analysis_selection<'py>( .getattr("names")? .is_truthy()? { - return call_with(&selection, &[("no_cache", no_cache)]); + return new_record_with( + py, + RecordId::CompileAnalysisSelection, + &[("no_cache", no_cache)], + ); } - call_with( - &selection, + new_record_with( + py, + RecordId::CompileAnalysisSelection, &[ ("select", native_field(options, "select")?), ("exclude", native_field(options, "exclude")?), @@ -119,7 +121,7 @@ pub(crate) fn project_graph<'py>(project: &Bound<'py, PyAny>) -> PyResult( +pub(crate) fn plan_has_executable_work<'py>( py: Python<'py>, options: Option>, ) -> PyResult { @@ -131,7 +133,7 @@ fn plan_has_executable_work<'py>( )?; let plan: Bound<'py, PyAny> = arguments.value("plan")?; for name in PLAN_WORK_ENTRIES { - if plan.getattr(name)?.is_truthy()? { + if native_field(&plan, name)?.is_truthy()? { return Ok(true); } } @@ -155,8 +157,9 @@ pub(crate) fn build_static_pipeline_result<'py>( (&["context", "plan_output"], &[]), )?; let context: Bound<'py, PyAny> = arguments.value("context")?; - call_with( - ®istry::class(py, RecordId::CompilePipelineResult)?, + new_record_with( + py, + RecordId::CompilePipelineResult, &[ ("project", native_field(&context, "project")?), ("plan_output", arguments.value("plan_output")?), @@ -172,7 +175,7 @@ pub(crate) fn build_static_pipeline_result<'py>( /// Compile a project for consumers that construct their own focused plans. #[pyfunction] #[pyo3(signature = (**options))] -fn run_compile_only_pipeline<'py>( +pub(crate) fn run_compile_only_pipeline<'py>( py: Python<'py>, options: Option>, ) -> PyResult> { @@ -197,13 +200,14 @@ fn run_compile_only_pipeline<'py>( ], )?), )?; - call_with( - ®istry::class(py, RecordId::CompilePipelineResult)?, + new_record_with( + py, + RecordId::CompilePipelineResult, &[ ("project", native_field(&compiled, "project")?), ( "plan_output", - registry::class(py, RecordId::PlanOutput)?.call0()?, + new_record(py, RecordId::PlanOutput, (), None)?, ), ( "compile_seconds", diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/plan_result.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/plan_result.rs index 12ced32ae3..97309d6123 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/plan_result.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/plan_result.rs @@ -10,19 +10,19 @@ use crate::bindings::_helpers::compile_driver::python_plan_entries::{ }; use crate::bindings::_helpers::planner_entries::execution_plan::build_execution_plan; use crate::bindings::_helpers::planner_phases::objects::object; +use crate::bindings::_helpers::records::construct::new_record_with; use pyo3::exceptions::PyException; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; use pyo3::types::{PyDict, PyDictMethods, PyFrozenSet}; use pyo3::{pyfunction, wrap_pyfunction}; use crate::bindings::_helpers::compile_driver::phases::monotonic; -use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; +use crate::bindings::_helpers::compile_driver::python_calls::KeywordArguments; use crate::bindings::_helpers::graph::python_node_graph::run_selection_from_inputs; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::native_host::user_code::UserCodeHost; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; const SINGLE_CONNECTION: i64 = 1; @@ -80,8 +80,9 @@ pub(crate) fn pipeline_result<'py>( )?), )?; } - let selection: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::PlannerSelection)?, + let selection: Bound<'py, PyAny> = new_record_with( + py, + RecordId::PlannerSelection, &[ ("select", select), ("exclude", exclude), @@ -90,8 +91,9 @@ pub(crate) fn pipeline_result<'py>( ("sql_test_selection", sql_test_selection), ], )?; - let overrides: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::PlannerOverrides)?, + let overrides: Bound<'py, PyAny> = new_record_with( + py, + RecordId::PlannerOverrides, &option_fields( options, &[ @@ -110,12 +112,11 @@ pub(crate) fn pipeline_result<'py>( options, &["defer_sources_to", "source_deferral_enabled"], )?); - let deferral: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::DeferralInputs)?, - &deferral_fields, - )?; - let policies: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::PlannerPolicies)?, + let deferral: Bound<'py, PyAny> = + new_record_with(py, RecordId::DeferralInputs, &deferral_fields)?; + let policies: Bound<'py, PyAny> = new_record_with( + py, + RecordId::PlannerPolicies, &option_fields( options, &[ @@ -162,8 +163,9 @@ pub(crate) fn pipeline_result<'py>( ], )?), )?; - call_with( - ®istry::class(py, RecordId::CompilePipelineResult)?, + new_record_with( + py, + RecordId::CompilePipelineResult, &[ ("project", project.clone()), ("plan_output", native_field(&python_outputs, "plan_output")?), @@ -187,7 +189,7 @@ fn option_fields<'a, 'py>( ) -> PyResult)>> { names .iter() - .map(|name| Ok((*name, options.getattr(*name)?))) + .map(|name| Ok((*name, native_field(options, name)?))) .collect() } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/project.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/project.rs index 60a2019f3a..f2b67b6c55 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/project.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/project.rs @@ -17,18 +17,20 @@ use crate::bindings::_helpers::compile_driver::targets::{ project_targets, }; use crate::bindings::_helpers::frontier::compile_frontier::{FrontierStage, bridged, produced}; +use crate::bindings::_helpers::records::construct::{enum_member, new_record_with}; use crate::bindings::_helpers::compile_driver::assembly::assemble_project; use crate::bindings::_helpers::compile_driver::python_calls::keywords; use crate::bindings::_helpers::compile_driver::selection_phases::resolve_analysis_model_names; -use crate::bindings::_helpers::compile_driver::stage_work::{PROMOTION_CONFLICTS, count_stage}; +use crate::bindings::_helpers::compile_driver::stage_work::{ + PROJECT_GRAPHS, PROMOTION_CONFLICTS, count_stage, +}; use crate::bindings::_helpers::compiled_project::records::project_with; use crate::bindings::_helpers::contracts::contract_diagnostics::promotion_conflict_diagnostics; use crate::bindings::_helpers::contracts::spec_resolution::resolve_effective_adapter_name; use crate::bindings::_helpers::contracts::spec_resolution::resolve_effective_collection_rendering; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use pyo3::IntoPyObjectExt; use sqlbuild_core::constants::PROJECT_CONFIG_FILENAME; use sqlbuild_core::constants::TABLE_PROMOTION_MODE_SETTING_KEY; @@ -64,7 +66,7 @@ pub(crate) fn lineage_mode<'py>(arguments: &KeywordArguments<'py>) -> PyResult( &py.None().into_bound(py), ) .and_then(|value| value.into_bound_py_any(py))?; - let adapter_context: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::CompileAdapterContext)?, + let adapter_context: Bound<'py, PyAny> = new_record_with( + py, + RecordId::CompileAdapterContext, &[ ("value_renderer", adapter.clone()), ("collection_rendering", collection_rendering), @@ -321,6 +324,7 @@ pub(crate) fn build_project_graph<'py>( py: Python<'py>, options: Option>, ) -> PyResult> { + count_stage(py, PROJECT_GRAPHS, 1)?; let mut optional: Vec<&str> = GRAPH_OPTIONAL.to_vec(); optional.push("no_cache"); let arguments: KeywordArguments<'py> = KeywordArguments::read( @@ -329,8 +333,9 @@ pub(crate) fn build_project_graph<'py>( options, (&["discovered_inputs", "adapter"], &optional), )?; - let selection: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::CompileAnalysisSelection)?, + let selection: Bound<'py, PyAny> = new_record_with( + py, + RecordId::CompileAnalysisSelection, &[("no_cache", py_bool(py, arguments.flag("no_cache")?))], )?; selected_graph(&arguments, selection) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/python_plan_entries.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/python_plan_entries.rs index dd794cb26c..af3a749e85 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/python_plan_entries.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/python_plan_entries.rs @@ -1,6 +1,8 @@ //! Display-ready Python plan entries and Python-aware selection warnings of a plan. use crate::bindings::_helpers::graph::python_lifecycle::build_python_sql_run_lifecycle; +use crate::bindings::_helpers::records::construct::{enum_member, new_record_with, record_class}; +use crate::bindings::_helpers::records::fields::replaced; use std::collections::HashMap; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; @@ -11,14 +13,13 @@ use sqlbuild_analysis::python_plan::main::ordered_python_names::ordered_python_n use sqlbuild_analysis::python_plan::main::upstream_closure::upstream_closure; use crate::bindings::_helpers::compile_driver::python_calls::{ - KeywordArguments, call_with, imported, keywords, + KeywordArguments, call_with, imported, }; use crate::bindings::_helpers::graph::python_node_graph::python_node_graph; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::native_host::user_code::UserCodeHost; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; const SKIPPED_UPSTREAM_CODE: &str = "P501"; @@ -124,13 +125,13 @@ pub(crate) fn build_python_run_plan_outputs<'py>( &[], ), )?; - let outputs: Bound<'py, PyAny> = registry::class(py, RecordId::PythonRunPlanOutputs)?; let plan_output: Bound<'py, PyAny> = arguments.value("plan_output")?; let run_selection: Bound<'py, PyAny> = arguments.value("run_selection")?; let selected: Bound<'py, PyAny> = arguments.value("selected_python_node_names")?; if run_selection.is_none() { - return call_with( - &outputs, + return new_record_with( + py, + RecordId::PythonRunPlanOutputs, &[ ("plan_output", plan_output), ("python_plan_entries", PyTuple::empty(py).into_any()), @@ -149,15 +150,13 @@ pub(crate) fn build_python_run_plan_outputs<'py>( &run_selection, (&graph, &kinds), )?); - let plan_output: Bound<'py, PyAny> = imported(py, "dataclasses", "replace")?.call( - (plan_output,), - Some(&keywords( - py, - &[("warnings", PyTuple::new(py, warnings)?.into_any())], - )?), + let plan_output: Bound<'py, PyAny> = replaced( + &plan_output, + &[("warnings", PyTuple::new(py, warnings)?.into_any())], )?; - let selection: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::PythonSqlRunSelection)?, + let selection: Bound<'py, PyAny> = new_record_with( + py, + RecordId::PythonSqlRunSelection, &[ ("sql_keys", native_field(&plan_output, "selected_keys")?), ("python_node_names", selected.clone()), @@ -169,8 +168,9 @@ pub(crate) fn build_python_run_plan_outputs<'py>( (&graph, &kinds), &native_field(&plan_output, "python_identity_fingerprints")?, )?; - call_with( - &outputs, + new_record_with( + py, + RecordId::PythonRunPlanOutputs, &[ ("plan_output", plan_output), ("python_plan_entries", entries.into_any()), @@ -215,8 +215,6 @@ fn plan_entries<'py>( previous_identities: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = lifecycle.py(); - let phases: Bound<'py, PyAny> = registry::class(py, RecordId::PythonRunPhase)?; - let entry_class: Bound<'py, PyAny> = registry::class(py, RecordId::PythonPlanEntry)?; let upstream: HashMap> = upstream_names(graph)?; let nodes: Bound<'py, PyAny> = native_field(graph, "nodes_by_name")?; let mut entries: Vec> = Vec::new(); @@ -224,8 +222,7 @@ fn plan_entries<'py>( ("ingress_python_node_names", "PRE_SQL_INGRESS"), ("read_side_python_node_names", "READ_SIDE"), ] { - let selected: Vec = lifecycle - .getattr(attribute)? + let selected: Vec = native_field(lifecycle, attribute)? .try_iter()? .map(|name| name?.extract()) .collect::>()?; @@ -241,15 +238,16 @@ fn plan_entries<'py>( if value.is_none() { Ok(py.None().into_bound(py)) } else { - value.getattr(name) + native_field(value, name) } }; - entries.push(call_with( - &entry_class, + entries.push(new_record_with( + py, + RecordId::PythonPlanEntry, &[ ("name", native_field(&node, "name")?), ("kind", kind), - ("phase", phases.getattr(phase)?), + ("phase", enum_member(py, RecordId::PythonRunPhase, phase)?), ( "identity_status", identity_status(&identity, previous_identities)?, @@ -301,8 +299,7 @@ fn identity_status<'py>( identity: &Bound<'py, PyAny>, previous_identities: &Bound<'py, PyAny>, ) -> PyResult> { - let statuses: Bound<'py, PyAny> = - registry::class(identity.py(), RecordId::PythonIdentityStatus)?; + let statuses: Bound<'py, PyAny> = record_class(identity.py(), RecordId::PythonIdentityStatus)?; if identity.is_none() { return native_field(&statuses, "UNKNOWN"); } @@ -337,9 +334,7 @@ fn skipped_ingress_warnings<'py>( let nodes: Bound<'py, PyAny> = native_field(graph, "nodes_by_name")?; let selected: Bound<'py, PyAny> = native_field(run_selection, "python_node_names")?; let upstream: HashMap> = upstream_names(graph)?; - let warning_class: Bound<'py, PyAny> = registry::class(py, RecordId::PlanWarning)?; - let severity: Bound<'py, PyAny> = - registry::class(py, RecordId::WarningSeverity)?.getattr("WARNING")?; + let severity: Bound<'py, PyAny> = enum_member(py, RecordId::WarningSeverity, "WARNING")?; let mut warnings: Vec> = Vec::new(); for loader in loaders { if !nodes.contains(loader.as_str())? { @@ -360,8 +355,9 @@ fn skipped_ingress_warnings<'py>( +source:{loader} to refresh upstream ingress dependencies.", native_field(&kind, "value")?.str()? ); - warnings.push(call_with( - &warning_class, + warnings.push(new_record_with( + py, + RecordId::PlanWarning, &[ ("model_name", py.None().into_bound(py)), ("severity", severity.clone()), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/relation_targets.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/relation_targets.rs index 92cadbfb21..8a4da6a5da 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/relation_targets.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/relation_targets.rs @@ -125,7 +125,7 @@ fn planned_relation_refs<'py>( ("seed_locations", &kinds.seed), ("python_source_entries", &kinds.source), ] { - for name in plan_output.getattr(attribute)?.try_iter()? { + for name in native_field(plan_output, attribute)?.try_iter()? { refs.add(call_with( &reference, &[("kind", kind.clone()), ("name", name?)], diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/selection_phases.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/selection_phases.rs index edde609702..8a1bb51bcc 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/selection_phases.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/selection_phases.rs @@ -11,15 +11,15 @@ use crate::bindings::_helpers::compile_driver::deferral::{ }; use crate::bindings::_helpers::compile_driver::pipeline::compile_project_phase; use crate::bindings::_helpers::compile_driver::pipeline_entries::compile_analysis_selection; -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported, keywords}; +use crate::bindings::_helpers::compile_driver::python_calls::{imported, keywords}; use crate::bindings::_helpers::planner_entries::command_plans::{ resolve_static_command_scope, resolve_static_relation_context, }; use crate::bindings::_helpers::planner_phases::scopes::resolve_planner_scopes; use crate::bindings::_helpers::planner_selection::unit_tests::split_project_unit_test_selectors; +use crate::bindings::_helpers::records::construct::{new_record, new_record_with}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; const DATACLASSES_MODULE: &str = "dataclasses"; @@ -36,7 +36,7 @@ fn unit_test_selectors_applied<'py>( ) -> PyResult> { let py: Python<'py> = discovered.py(); let resolved: Bound<'py, PyAny> = if options.is_none() { - registry::class(py, RecordId::CompilePipelineOptions)?.call0()? + new_record(py, RecordId::CompilePipelineOptions, (), None)? } else { options.clone() }; @@ -73,15 +73,17 @@ fn planner_scopes<'py>( let py: Python<'py> = project.py(); resolve_planner_scopes( project, - &call_with( - ®istry::class(py, RecordId::PlannerSelection)?, + &new_record_with( + py, + RecordId::PlannerSelection, &[ ("select", native_field(selection, "select")?), ("exclude", native_field(selection, "exclude")?), ], )?, - &call_with( - ®istry::class(py, RecordId::PlannerPolicies)?, + &new_record_with( + py, + RecordId::PlannerPolicies, &[( "auto_load_sources", native_field(selection, "auto_load_sources")?, @@ -259,8 +261,9 @@ pub(crate) fn compile_static_command_context<'py>( deferred_locations((&discovered, &adapter), &options, &project)?; let scope: Bound<'py, PyAny> = resolve_static_command_scope( &project, - &call_with( - ®istry::class(py, RecordId::PlannerSelection)?, + &new_record_with( + py, + RecordId::PlannerSelection, &[ ("select", native_field(&options, "select")?), ("exclude", native_field(&options, "exclude")?), @@ -283,8 +286,9 @@ pub(crate) fn compile_static_command_context<'py>( ("scope", scope.clone()), ( "deferral", - call_with( - ®istry::class(py, RecordId::DeferralInputs)?, + new_record_with( + py, + RecordId::DeferralInputs, &[ ("deferred_locations", deferred), ( @@ -310,8 +314,9 @@ pub(crate) fn compile_static_command_context<'py>( ], )?), )?; - call_with( - ®istry::class(py, RecordId::StaticCommandContext)?, + new_record_with( + py, + RecordId::StaticCommandContext, &[ ("project", project), ("scope", scope), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/stage_work.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/stage_work.rs index 2537090419..e57cb18e64 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/stage_work.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/stage_work.rs @@ -20,6 +20,9 @@ pub(crate) const PIVOT_PROOFS: &str = "pivot_proofs"; pub(crate) const SESSION_PIVOT_PROOFS: &str = "session_pivot_proofs"; pub(crate) const DECLARATION_SCOPES: &str = "build_declaration_scope"; pub(crate) const LINEAGE_GRAPH_BUILDS: &str = "lineage_graph_builds"; +pub(crate) const PROJECT_GRAPHS: &str = "build_project_graph"; +pub(crate) const FORMAT_LINT_BATCHES: &str = "format_lint_batches"; +pub(crate) const FORMAT_LINT_FILES: &str = "format_lint_files"; /// Compile stage runs since the last reset, by stage. #[pyclass] diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/target_defaults.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/target_defaults.rs index e15fb7b9c8..a09a843ed4 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/target_defaults.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/target_defaults.rs @@ -3,14 +3,15 @@ use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; use pyo3::types::PyTuple; -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported, keywords}; +use crate::bindings::_helpers::compile_driver::python_calls::{call_with, keywords}; use crate::bindings::_helpers::compiled_project::compiled_model_view::NativeCompiledModel; use crate::bindings::_helpers::compiled_project::function_views::NativeCompiledFunction; use crate::bindings::_helpers::compiled_project::records::project_with; use crate::bindings::_helpers::compiled_project::seed_views::NativeCompiledSeed; +use crate::bindings::_helpers::records::construct::new_record_with; use crate::bindings::_helpers::records::fields::field as native_field; +use crate::bindings::_helpers::records::fields::replaced; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; /// The adapter defaults every compiled location falls back to. pub(crate) struct TargetDefaults<'py> { @@ -45,7 +46,6 @@ pub(crate) fn apply_target_defaults<'py>( defaults: &TargetDefaults<'py>, ) -> PyResult> { let py: Python<'py> = project.py(); - let replace: Bound<'py, PyAny> = imported(py, "dataclasses", "replace")?; let mut models: Vec> = Vec::new(); for model in native_field(project, "models")?.try_iter()? { let model: Bound<'py, PyAny> = model?; @@ -60,15 +60,12 @@ pub(crate) fn apply_target_defaults<'py>( )? .into_any() } else { - replace.call( - (&model,), - Some(&keywords( - py, - &[( - "destination", - resolved_target(&native_field(&model, "destination")?, defaults)?, - )], - )?), + replaced( + &model, + &[( + "destination", + resolved_target(&native_field(&model, "destination")?, defaults)?, + )], )? }); } @@ -86,15 +83,12 @@ pub(crate) fn apply_target_defaults<'py>( )? .into_any() } else { - replace.call( - (&seed,), - Some(&keywords( - py, - &[( - "destination", - resolved_target(&native_field(&seed, "destination")?, defaults)?, - )], - )?), + replaced( + &seed, + &[( + "destination", + resolved_target(&native_field(&seed, "destination")?, defaults)?, + )], )? }); } @@ -177,8 +171,9 @@ fn resolved_target<'py>( { return Ok(target.clone()); } - call_with( - ®istry::class(py, RecordId::CompiledRelationLocation)?, + new_record_with( + py, + RecordId::CompiledRelationLocation, &[ ("database", database), ("schema", schema), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/targets.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/targets.rs index 26249342fd..dde70832c8 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/targets.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/targets.rs @@ -404,7 +404,7 @@ pub(crate) fn project_targets( let adapter_label: String = adapter_name.str()?.to_cow()?.into_owned(); for (resource_kind, attribute) in [("model", "models"), ("seed", "seeds")] { let mut targets: Vec<(String, Bound<'_, PyAny>)> = Vec::new(); - for resource in project.getattr(attribute)?.try_iter()? { + for resource in native_field(project, attribute)?.try_iter()? { let resource: Bound<'_, PyAny> = resource?; let resource_name: String = native_field(&resource, "name")? .str()? @@ -517,7 +517,7 @@ pub(crate) fn managed_write_schemas( ("Seed", "seeds"), ("Function", "functions"), ] { - for resource in project.getattr(attribute)?.try_iter()? { + for resource in native_field(project, attribute)?.try_iter()? { let resource: Bound<'_, PyAny> = resource?; managed_write_schema( &format!( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_outputs/reuse.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_outputs/reuse.rs index 2eec1a8b5b..ee68ca5a9e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_outputs/reuse.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_outputs/reuse.rs @@ -82,7 +82,7 @@ impl From for ReuseRules { /// One reuse check: where the project and its store slot are, and the host's identity facts. #[derive(FromPyObject)] #[pyo3(from_item_all)] -struct ReuseCheckPy { +pub(crate) struct ReuseCheckPy { project_dir: PathBuf, store_directory: PathBuf, selected_target: Option, @@ -115,7 +115,7 @@ struct CompileRecordPy { /// One invocation's native reuse state, from the check to the record. #[pyclass(module = "sqlbuild._native", frozen)] -struct NativeReuseAttempt { +pub(crate) struct NativeReuseAttempt { attempt: Mutex, } @@ -246,7 +246,10 @@ fn recorded_artifact(artifact: ArtifactRow) -> RecordedArtifact { /// Check the stored compile for this invocation, replaying it on a hit. #[pyfunction] -fn check_compile_reuse(py: Python<'_>, check: ReuseCheckPy) -> PyResult { +pub(crate) fn check_compile_reuse( + py: Python<'_>, + check: ReuseCheckPy, +) -> PyResult { compiler_guard(|| { let rules: ReuseRules = check.rules.into(); let request: ReuseCheck<'_> = ReuseCheck { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_outputs/runtime_target.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_outputs/runtime_target.rs index 2c83c893ca..2060ede683 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_outputs/runtime_target.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_outputs/runtime_target.rs @@ -43,7 +43,7 @@ fn path_object<'py>(py: Python<'py>, text: &str) -> PyResult> fn test_key(value: &Bound<'_, PyAny>, name_field: &str) -> PyResult { Ok(( - value.getattr(name_field)?.extract()?, + native_field(value, name_field)?.extract()?, posix(&native_field(value, "source_path")?)?, native_field(value, "block_index")?.extract()?, native_field(value, "case_name")?.extract()?, @@ -153,13 +153,13 @@ pub(crate) fn write_runtime_target( ("model_results", "model_name", &models), ("function_results", "function_name", &functions), ] { - for item in result.getattr(results)?.try_iter()? { + for item in native_field(result, results)?.try_iter()? { let item: Bound<'_, PyAny> = item?; let statements: Vec = sql_statements(&item)?; if statements.is_empty() { continue; } - let name: String = item.getattr(name_field)?.extract()?; + let name: String = native_field(&item, name_field)?.extract()?; if let Some(path) = paths.get(&name) { write_text(&run_dir.div(path)?, &runtime_sql_text(&statements))?; } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/attachment.rs b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/attachment.rs index 85c03a5899..b53e51251e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/attachment.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/attachment.rs @@ -26,6 +26,7 @@ use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::facade::FacadeHost; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::native_host::user_code::UserCodeHost; +use crate::bindings::_helpers::records::construct::{enum_member, new_record_with, record_class}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; @@ -39,27 +40,27 @@ const MEASUREMENT_MODE: &str = "MEASUREMENT"; /// `getattr(value, name)` extracted. fn attr<'py, T: pyo3::FromPyObject<'py>>(value: &Bound<'py, PyAny>, name: &str) -> PyResult { - value.getattr(name)?.extract() + native_field(value, name)?.extract() } /// `str(value.)`. fn text(value: &Bound<'_, PyAny>, name: &str) -> PyResult { - Ok(value.getattr(name)?.str()?.to_string()) + Ok(native_field(value, name)?.str()?.to_string()) } /// `value..stem`. fn stem(value: &Bound<'_, PyAny>, name: &str) -> PyResult { - attr(&value.getattr(name)?, "stem") + attr(&native_field(value, name)?, "stem") } /// Each item of the iterable `value.`. fn items<'py>(value: &Bound<'py, PyAny>, name: &str) -> PyResult>> { - value.getattr(name)?.try_iter()?.collect() + native_field(value, name)?.try_iter()?.collect() } /// `{member.value: member for member in enum_class}` of a native string enum. fn native_by_value(py: Python<'_>, class: RecordId) -> PyResult> { - members_by_value(®istry::class(py, class)?) + members_by_value(&record_class(py, class)?) } fn members_by_value<'py>(enum_class: &Bound<'py, PyAny>) -> PyResult> { @@ -112,7 +113,7 @@ fn either<'py>( defaults: &Bound<'py, PyAny>, (preferred, fallback): (&str, &str), ) -> PyResult> { - let value: Bound<'py, PyAny> = defaults.getattr(preferred)?; + let value: Bound<'py, PyAny> = native_field(defaults, preferred)?; if value.is_none() { return attr(defaults, fallback); } @@ -175,8 +176,7 @@ pub(crate) fn attach_resources<'py>( let defaults: Bound<'py, PyAny> = discovered_inputs .getattr("project_config")? .getattr("defaults")?; - let function_kind: Bound<'py, PyAny> = - registry::member(py, RecordId::ResourceKind, "FUNCTION")?; + let function_kind: Bound<'py, PyAny> = enum_member(py, RecordId::ResourceKind, "FUNCTION")?; let functions = NativeCompiledProject::attach_functions( native_project, ( @@ -216,7 +216,7 @@ pub(crate) fn attach_resources<'py>( ( attr(&defaults, "contract")?, validate, - registry::member(py, RecordId::ResourceKind, "SOURCE")?.unbind(), + enum_member(py, RecordId::ResourceKind, "SOURCE")?.unbind(), ), )?; PyTuple::new( @@ -336,7 +336,7 @@ fn sql_test_settings<'py>( ("scenario_input", RecordId::CompileSqlScenarioInput), ("scenario_cte", RecordId::CompileSqlScenarioCte), ] { - settings.set_item(key, registry::class(py, class)?)?; + settings.set_item(key, record_class(py, class)?)?; } settings.set_item( "modes", @@ -344,15 +344,15 @@ fn sql_test_settings<'py>( )?; settings.set_item( "test_kind", - registry::member(py, RecordId::ResourceKind, "TEST")?, + enum_member(py, RecordId::ResourceKind, "TEST")?, )?; settings.set_item( "scenario_kind", - registry::member(py, RecordId::ResourceKind, "SCENARIO")?, + enum_member(py, RecordId::ResourceKind, "SCENARIO")?, )?; settings.set_item( "runtime_usage", - registry::member(py, RecordId::UsageKind, "RUNTIME")?, + enum_member(py, RecordId::UsageKind, "RUNTIME")?, )?; settings.set_item("render_parameter", parameter_renderer(value_renderer)?)?; settings.set_item( @@ -361,7 +361,7 @@ fn sql_test_settings<'py>( )?; settings.set_item( "source_location", - registry::class(py, RecordId::SourceLocation)?, + record_class(py, RecordId::SourceLocation)?, )?; settings.set_item("scan_store", scan_cache.call_method0("opened")?)?; settings.set_item( @@ -390,7 +390,7 @@ fn audit_block_row(block: Bound<'_, PyAny>) -> PyResult { fn audit_file_row(file: Bound<'_, PyAny>) -> PyResult { let py: Python<'_> = file.py(); - let generic: Bound<'_, PyAny> = registry::member(py, RecordId::DeclarationKind, "AUDIT")?; + let generic: Bound<'_, PyAny> = enum_member(py, RecordId::DeclarationKind, "AUDIT")?; Ok(( text(&file, "file_path")?, text(&file, "relative_path")?, @@ -413,16 +413,18 @@ fn builtin_audit_file<'py>( let path_class: Bound<'py, PyAny> = imported(py, PATHLIB_MODULE, "Path")?; let relative_path: Bound<'py, PyAny> = path_class.call1((format!("audits/generic/{name}.sql"),))?; - let block: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::DiscoveredAuditBlock)?, + let block: Bound<'py, PyAny> = new_record_with( + py, + RecordId::DiscoveredAuditBlock, &[ ("audit_index", 0.into_bound_py_any(py)?), ("header_values", PyDict::new(py).into_any()), ("sql_body", sql_body.into_bound_py_any(py)?), ], )?; - call_with( - ®istry::class(py, RecordId::DiscoveredAuditFile)?, + new_record_with( + py, + RecordId::DiscoveredAuditFile, &[ ( "file_path", @@ -463,15 +465,15 @@ fn audit_settings<'py>( settings.set_item("builtins", builtins)?; settings.set_item( "declaration_identity", - registry::class(py, RecordId::DeclarationIdentity)?, + record_class(py, RecordId::DeclarationIdentity)?, )?; settings.set_item( "singular_kind", - registry::member(py, RecordId::DeclarationKind, "SINGULAR_AUDIT")?, + enum_member(py, RecordId::DeclarationKind, "SINGULAR_AUDIT")?, )?; settings.set_item( "audit_kind", - registry::member(py, RecordId::DeclarationKind, "AUDIT")?, + enum_member(py, RecordId::DeclarationKind, "AUDIT")?, )?; settings.set_item( "resource_kinds", @@ -510,12 +512,12 @@ fn diagnostic_settings<'py>( let settings: Bound<'py, PyDict> = PyDict::new(py); settings.set_item( "compiler_diagnostic", - registry::class(py, RecordId::CompilerDiagnostic)?, + record_class(py, RecordId::CompilerDiagnostic)?, )?; settings.set_item("compile_error", registry::error(py, ErrorId::CompileInput)?)?; settings.set_item( "source_location", - registry::class(py, RecordId::SourceLocation)?, + record_class(py, RecordId::SourceLocation)?, )?; settings.set_item("path_class", imported(py, PATHLIB_MODULE, "Path")?)?; settings.set_item("model_files", items(discovered, "model_files")?)?; @@ -556,7 +558,7 @@ fn diagnostic_settings<'py>( native_field(hook, "name")?, text(hook, "relative_path")?.into_bound_py_any(py)?, native_field(hook, "contents")?, - hook.getattr(DESCRIPTION_KEY)?, + native_field(hook, DESCRIPTION_KEY)?, ], ) }) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/audit_loop.rs b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/audit_loop.rs index acca204868..d02cd2cdc6 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/audit_loop.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/audit_loop.rs @@ -472,7 +472,7 @@ impl<'py> AuditScope<'_, 'py> { }), severity, run_scope, - always_run: instance.getattr(ALWAYS_RUN_KEY)?.is_truthy()?, + always_run: native_field(instance, ALWAYS_RUN_KEY)?.is_truthy()?, usages: PyTuple::new(py, usages)?.unbind(), name: native_field(instance, "name")?.extract()?, description: native_field(instance, "description")?.extract()?, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/compiled_model_view.rs b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/compiled_model_view.rs index a06b43dfc4..feba71426f 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/compiled_model_view.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/compiled_model_view.rs @@ -178,7 +178,7 @@ pub(crate) fn compiled_record( let analysis: AnalysisRecord = analysis_record(py, input, session, (fallback, standalone_proof))?; let config: &Bound<'_, PyAny> = input.config.bind(py); - let values: Bound<'_, PyDict> = config.getattr(VALUES_ATTRIBUTE)?.downcast_into()?; + let values: Bound<'_, PyDict> = native_field(config, VALUES_ATTRIBUTE)?.downcast_into()?; Ok(CompiledRecord { deps, destination: DestinationFact { @@ -186,8 +186,8 @@ pub(crate) fn compiled_record( schema: text_value(&values, SCHEMA_KEY)?, name: text_value(&values, ALIAS_KEY)?.unwrap_or_else(|| name.to_owned()), qualified_name: None, - logical_schema: config.getattr(LOGICAL_SCHEMA_ATTRIBUTE)?.extract()?, - logical_database: config.getattr(LOGICAL_DATABASE_ATTRIBUTE)?.extract()?, + logical_schema: native_field(config, LOGICAL_SCHEMA_ATTRIBUTE)?.extract()?, + logical_database: native_field(config, LOGICAL_DATABASE_ATTRIBUTE)?.extract()?, }, analysis, macro_deps, @@ -434,7 +434,7 @@ impl NativeCompiledModel { fn model_file_attribute(&self, py: Python<'_>, name: &str) -> PyResult> { self.read(py, |record, py| { - Ok(record.model_file.bind(py).getattr(name)?.unbind()) + Ok(native_field(record.model_file.bind(py), name)?.unbind()) }) } } @@ -831,7 +831,7 @@ pub(crate) fn model_graph_row( ) -> PyResult { model.read(py, |record, py| { let compiled: &CompiledRecord = compiled_of(record)?; - let values: Bound<'_, PyAny> = record.config.bind(py).getattr(VALUES_ATTRIBUTE)?; + let values: Bound<'_, PyAny> = native_field(record.config.bind(py), VALUES_ATTRIBUTE)?; let mut tags: Vec = Vec::new(); if let Some(value) = values.downcast::()?.get_item(TAGS_KEY)? && (value.is_instance_of::() diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/expanded_offsets.rs b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/expanded_offsets.rs index 6208bb7ec8..b46ae941b8 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/expanded_offsets.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/expanded_offsets.rs @@ -1,8 +1,8 @@ //! Map offsets in expanded SQL back onto the authored text that produced them. +use crate::bindings::_helpers::records::construct::new_record; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; use pyo3::types::PyDict; use pyo3::{pyfunction, wrap_pyfunction}; @@ -35,7 +35,7 @@ fn map_expanded_offset<'py>( let arguments: Bound<'py, PyDict> = PyDict::new(py); arguments.set_item("offset", offset)?; arguments.set_item("generated", generated)?; - registry::class(py, RecordId::MappedOffset)?.call((), Some(&arguments)) + new_record(py, RecordId::MappedOffset, (), Some(&arguments)) } pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/function_views.rs b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/function_views.rs index 7c265a45e9..9049e3ccfa 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/function_views.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/function_views.rs @@ -15,6 +15,7 @@ use crate::bindings::_helpers::compiled_project::model_input_view::{ use crate::bindings::_helpers::compiled_project::model_loop_host::ReferenceRow; use crate::bindings::_helpers::compiled_project::project::NativeCompiledProject; use crate::bindings::_helpers::graph::project_graph::ResourceRow; +use crate::bindings::_helpers::records::fields::field as native_field; const TABLE_FUNCTION_TYPE: &str = "table_fn"; const SCALAR_FUNCTION_TYPE: &str = "udf"; @@ -511,24 +512,22 @@ function_getters!(NativeCompiledFunction, { #[getter] fn relative_path(&self, py: Python<'_>) -> PyResult> { read_function(&self.owner, self.index, py, |function, py| { - Ok(function - .attached - .function_file - .bind(py) - .getattr(FUNCTION_FILE_ATTRIBUTE)? - .unbind()) + Ok(native_field( + function.attached.function_file.bind(py), + FUNCTION_FILE_ATTRIBUTE, + )? + .unbind()) }) } #[getter] fn source_file_path(&self, py: Python<'_>) -> PyResult> { read_function(&self.owner, self.index, py, |function, py| { - Ok(function - .attached - .function_file - .bind(py) - .getattr(FILE_PATH_ATTRIBUTE)? - .unbind()) + Ok(native_field( + function.attached.function_file.bind(py), + FILE_PATH_ATTRIBUTE, + )? + .unbind()) }) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/loop_classes.rs b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/loop_classes.rs index acb029a4bb..89e4f36fa5 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/loop_classes.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/loop_classes.rs @@ -7,8 +7,9 @@ use pyo3::{IntoPyObjectExt, pyfunction, wrap_pyfunction}; use crate::bindings::_helpers::compile_driver::python_calls::imported; use crate::bindings::_helpers::native_host::facade::FacadeHost; use crate::bindings::_helpers::native_host::host; +use crate::bindings::_helpers::records::construct::{enum_member, record_class}; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr, RecordId}; +use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; /// The materializations a model's storage policies are inherited for, besides none. @@ -31,7 +32,7 @@ fn dict<'py>( pub(crate) fn table_backed<'py>(py: Python<'py>) -> PyResult>> { TABLE_BACKED_MATERIALIZATIONS .iter() - .map(|name| registry::member(py, RecordId::MaterializationType, name)) + .map(|name| enum_member(py, RecordId::MaterializationType, name)) .collect() } @@ -56,24 +57,24 @@ pub(crate) fn native_config_classes<'py>( ("table_backed", PyTuple::new(py, table_backed)?.into_any()), ( "compile_model_config", - registry::class(py, RecordId::CompileModelConfig)?, + record_class(py, RecordId::CompileModelConfig)?, ), ( "resolved_retention", - registry::class(py, RecordId::ResolvedTimeTravelRetention)?, + record_class(py, RecordId::ResolvedTimeTravelRetention)?, ), ( "retention_source", - registry::member(py, RecordId::TimeTravelRetentionSource, "MODEL")?, + enum_member(py, RecordId::TimeTravelRetentionSource, "MODEL")?, ), ( "resolved_table_type", - registry::class(py, RecordId::ResolvedTableType)?, + record_class(py, RecordId::ResolvedTableType)?, ), - ("table_type", registry::class(py, RecordId::TableType)?), + ("table_type", record_class(py, RecordId::TableType)?), ( "table_type_source", - registry::member(py, RecordId::TableTypeSource, "MODEL")?, + enum_member(py, RecordId::TableTypeSource, "MODEL")?, ), ("record_template_reads", record_template_reads.clone()), ], @@ -89,7 +90,7 @@ fn named_records<'py>(resolver: &Bound<'py, PyAny>) -> PyResult> = NAMED_KINDS .iter() - .map(|name| registry::member(py, RecordId::DeclarationKind, name)) + .map(|name| enum_member(py, RecordId::DeclarationKind, name)) .collect::>()?; let declarations: Bound<'py, PyAny> = native_field(resolver, "lookup")?.getattr("declarations")?; @@ -119,8 +120,8 @@ fn schema_enums<'py>(resolver: &Bound<'py, PyAny>) -> PyResult = registry::member(py, RecordId::DeclarationKind, "ENUM")?; - let enum_class: Bound<'py, PyAny> = registry::class(py, RecordId::EnumDeclaration)?; + let enum_kind: Bound<'py, PyAny> = enum_member(py, RecordId::DeclarationKind, "ENUM")?; + let enum_class: Bound<'py, PyAny> = record_class(py, RecordId::EnumDeclaration)?; let declarations: Bound<'py, PyAny> = native_field(resolver, "projection")?.getattr("declarations")?; for pair in declarations.call_method0("items")?.try_iter()? { @@ -174,7 +175,7 @@ fn visibility_classes<'py>( let lookup: Bound<'py, PyAny> = native_field(&resolver, "lookup")?; let declarations: Bound<'py, PyAny> = native_field(&lookup, "index")?.getattr("declarations")?; - let private: Bound<'py, PyAny> = registry::member(py, RecordId::ScopeKind, "PRIVATE")?; + let private: Bound<'py, PyAny> = enum_member(py, RecordId::ScopeKind, "PRIVATE")?; let mut owners: Vec> = Vec::new(); for declaration in declarations.try_iter()? { let declaration: Bound<'py, PyAny> = declaration?; @@ -203,31 +204,31 @@ fn visibility_classes<'py>( ("visible_declarations", visible_declarations.clone()), ( "resource_identity", - registry::class(py, RecordId::ResourceIdentity)?, + record_class(py, RecordId::ResourceIdentity)?, ), ( "declaration_identity", - registry::class(py, RecordId::DeclarationIdentity)?, + record_class(py, RecordId::DeclarationIdentity)?, ), ( "model_kind", - registry::member(py, RecordId::ResourceKind, "MODEL")?, + enum_member(py, RecordId::ResourceKind, "MODEL")?, ), ( "enum_kind", - registry::member(py, RecordId::DeclarationKind, "ENUM")?, + enum_member(py, RecordId::DeclarationKind, "ENUM")?, ), ( "constant_kind", - registry::member(py, RecordId::DeclarationKind, "CONSTANT")?, + enum_member(py, RecordId::DeclarationKind, "CONSTANT")?, ), ( "private_owner", - registry::member(py, RecordId::VisibilityReason, "PRIVATE_OWNER")?, + enum_member(py, RecordId::VisibilityReason, "PRIVATE_OWNER")?, ), ( "visibility_record", - registry::class(py, RecordId::VisibilityRecord)?, + record_class(py, RecordId::VisibilityRecord)?, ), ], ) @@ -235,49 +236,46 @@ fn visibility_classes<'py>( fn input_view_classes<'py>(py: Python<'py>) -> PyResult> { let compiled: Vec<(&str, Bound<'py, PyAny>)> = vec![ - ( - "object_key", - registry::class(py, RecordId::CompiledObjectKey)?, - ), + ("object_key", record_class(py, RecordId::CompiledObjectKey)?), ( "relation_location", - registry::class(py, RecordId::CompiledRelationLocation)?, + record_class(py, RecordId::CompiledRelationLocation)?, ), ( "inferred_column", - registry::class(py, RecordId::InferredColumn)?, + record_class(py, RecordId::InferredColumn)?, ), ( "nullability", - registry::class(py, RecordId::InferredNullability)?, + record_class(py, RecordId::InferredNullability)?, ), ( "compiler_diagnostic", - registry::class(py, RecordId::CompilerDiagnostic)?, + record_class(py, RecordId::CompilerDiagnostic)?, ), ( "source_location", - registry::class(py, RecordId::SourceLocation)?, + record_class(py, RecordId::SourceLocation)?, ), ( "compact_lineage_facts", - registry::class(py, RecordId::CompactLineageFacts)?, + record_class(py, RecordId::CompactLineageFacts)?, ), ( "lineage_column_fact", - registry::class(py, RecordId::CompiledLineageColumnFact)?, + record_class(py, RecordId::CompiledLineageColumnFact)?, ), ( "lineage_source_fact", - registry::class(py, RecordId::CompiledLineageSourceFact)?, + record_class(py, RecordId::CompiledLineageSourceFact)?, ), ( "contract_proof", - registry::class(py, RecordId::DynamicColumnContractProof)?, + record_class(py, RecordId::DynamicColumnContractProof)?, ), ( "family_proof", - registry::class(py, RecordId::DynamicColumnFamilyProof)?, + record_class(py, RecordId::DynamicColumnFamilyProof)?, ), ]; dict( @@ -285,24 +283,21 @@ fn input_view_classes<'py>(py: Python<'py>) -> PyResult> { vec![ ( "sql_reference", - registry::class(py, RecordId::CompileSqlReference)?, + record_class(py, RecordId::CompileSqlReference)?, ), ( "reference_kind", - registry::class(py, RecordId::SqlReferenceKind)?, + record_class(py, RecordId::SqlReferenceKind)?, ), ( "schema_model_entry", - registry::class(py, RecordId::SchemaModelEntry)?, + record_class(py, RecordId::SchemaModelEntry)?, ), ( "sql_expansion", - registry::class(py, RecordId::CompiledSqlExpansion)?, - ), - ( - "expansion_span", - registry::class(py, RecordId::ExpansionSpan)?, + record_class(py, RecordId::CompiledSqlExpansion)?, ), + ("expansion_span", record_class(py, RecordId::ExpansionSpan)?), ( "audits", dict( @@ -327,11 +322,11 @@ fn input_view_classes<'py>(py: Python<'py>) -> PyResult> { vec![ ( "function_argument", - registry::class(py, RecordId::FunctionArgument)?, + record_class(py, RecordId::FunctionArgument)?, ), ( "function_return_column", - registry::class(py, RecordId::FunctionReturnColumn)?, + record_class(py, RecordId::FunctionReturnColumn)?, ), ( "function_language", @@ -354,7 +349,7 @@ pub(crate) fn by_attribute<'py>( let keyed: Bound<'py, PyDict> = PyDict::new(items.py()); for item in items.try_iter()? { let item: Bound<'py, PyAny> = item?; - keyed.set_item(item.getattr(attribute)?, item)?; + keyed.set_item(native_field(&item, attribute)?, item)?; } Ok(keyed) } @@ -384,7 +379,7 @@ pub(crate) fn model_loop_classes<'py>( ("provider_names", provider_names.into_bound_py_any(py)?), ( "python_hook_entry", - registry::class(py, RecordId::PythonHookEntry)?, + record_class(py, RecordId::PythonHookEntry)?, ), ( "signature", @@ -398,14 +393,18 @@ pub(crate) fn model_loop_classes<'py>( ("replace", imported(py, "dataclasses", "replace")?), ( "declaration_rows", - registry::native(py, NativeAttr::DeclarationRows)?, + pyo3::wrap_pyfunction!( + crate::bindings::_helpers::compiled_project::records::declaration_rows, + py + )? + .into_any(), ), ], )?; dict( py, vec![ - ("usage_record", registry::class(py, RecordId::UsageRecord)?), + ("usage_record", record_class(py, RecordId::UsageRecord)?), ("compile_error", registry::error(py, ErrorId::CompileInput)?), ("syntax_help", host("syntax_help")?), ("source_syntax_help", host("source_syntax_help")?), @@ -415,21 +414,18 @@ pub(crate) fn model_loop_classes<'py>( "scope_path_error", registry::error(py, ErrorId::InvalidScopePath)?, ), - ( - "sql_hook_entry", - registry::class(py, RecordId::SqlHookEntry)?, - ), + ("sql_hook_entry", record_class(py, RecordId::SqlHookEntry)?), ( "audit_factories", by_attribute(audit_factories, "name")?.into_any(), ), ( "generated_usage", - registry::member(py, RecordId::UsageKind, "GENERATED")?, + enum_member(py, RecordId::UsageKind, "GENERATED")?, ), ( "contract_enforced", - registry::member(py, RecordId::ContractPolicy, "ENFORCED")?, + enum_member(py, RecordId::ContractPolicy, "ENFORCED")?, ), ( "model_schemas", @@ -439,14 +435,14 @@ pub(crate) fn model_loop_classes<'py>( ("schema_enums", schema_enums(&resolver)?.into_any()), ( "compiler_diagnostic", - registry::class(py, RecordId::CompilerDiagnostic)?, + record_class(py, RecordId::CompilerDiagnostic)?, ), ("report_diagnostic", host("report_diagnostic")?), ("path_class", imported(py, "pathlib", "Path")?), ("external_resolver", external_resolver.clone()), ( "reference_kind", - registry::class(py, RecordId::SqlReferenceKind)?, + record_class(py, RecordId::SqlReferenceKind)?, ), ( "config", diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop.rs b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop.rs index 92bc8e9392..01f51cf0ee 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop.rs @@ -201,7 +201,7 @@ struct LoopModel<'a, 'py> { #[pymethods] impl NativeModelLoop { #[new] - fn new( + pub(crate) fn new( checks: LoopChecks, syntax: LexicalSyntaxInput, reserved_markers: Vec, @@ -543,7 +543,8 @@ impl NativeModelLoop { interpolation, model_file, } = row; - let values: Bound<'py, PyDict> = config.getattr(VALUES_ATTRIBUTE)?.downcast_into()?; + let values: Bound<'py, PyDict> = + native_field(&config, VALUES_ATTRIBUTE)?.downcast_into()?; let schema: Option> = model_schema(context, &values, name)?; let hooked: bool = hook_presence(&values)?; let python_hooks: Vec> = if hooked { @@ -788,12 +789,14 @@ impl NativeModelLoop { .zip(&external) .map(|((kind, name, _, _), error)| (kind.as_str(), name.as_str(), error.is_some())) .collect(); - let unmanaged: Bound<'_, PyAny> = config - .getattr(RETENTION_ATTRIBUTE)? - .getattr(UNMANAGED_ATTRIBUTE)?; - let declared: Bound<'_, PyAny> = config - .getattr(TABLE_TYPE_ATTRIBUTE)? - .getattr(DECLARED_ATTRIBUTE)?; + let unmanaged: Bound<'_, PyAny> = native_field( + &native_field(config, RETENTION_ATTRIBUTE)?, + UNMANAGED_ATTRIBUTE, + )?; + let declared: Bound<'_, PyAny> = native_field( + &native_field(config, TABLE_TYPE_ATTRIBUTE)?, + DECLARED_ATTRIBUTE, + )?; let outcome: Bound<'_, PyAny> = self.validator.bind(py).call_method1( VALIDATE_METHOD, ( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_contracts.rs b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_contracts.rs index 14a928a6a5..b82fe8f7bf 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_contracts.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_contracts.rs @@ -8,6 +8,7 @@ use sqlbuild_model_config::header_metadata::models::DynamicFamily; use crate::bindings::_helpers::compiled_project::model_loop::LoopContext; use crate::bindings::_helpers::compiled_project::model_loop_schema::DeclaredColumns; +use crate::bindings::_helpers::records::fields::field as native_field; const CASES_ATTRIBUTE: &str = "cases"; const RELATIVE_PATH_ATTRIBUTE: &str = "relative_path"; @@ -81,22 +82,25 @@ pub(crate) fn factory_audits<'py>( let location: Bound<'py, PyAny> = keywords( &context.contracts.source_location, &[ - (PATH_FIELD, factory.getattr(RELATIVE_PATH_ATTRIBUTE)?), - (LINE_ATTRIBUTE, factory.getattr(LINE_ATTRIBUTE)?), + (PATH_FIELD, native_field(&factory, RELATIVE_PATH_ATTRIBUTE)?), + (LINE_ATTRIBUTE, native_field(&factory, LINE_ATTRIBUTE)?), (COLUMN_FIELD, 1_u8.into_pyobject_any(py)?), ], )?; - for case in factory.getattr(CASES_ATTRIBUTE)?.try_iter()? { + for case in native_field(&factory, CASES_ATTRIBUTE)?.try_iter()? { let case: Bound<'py, PyAny> = case?; let fields: Bound<'py, PyDict> = PyDict::new(py); - fields.set_item(DEFINITION_NAME_FIELD, case.getattr(DEFINITION_ATTRIBUTE)?)?; + fields.set_item( + DEFINITION_NAME_FIELD, + native_field(&case, DEFINITION_ATTRIBUTE)?, + )?; fields.set_item( ARGUMENTS_FIELD, py.get_type::() - .call1((case.getattr(ARGUMENTS_FIELD)?,))?, + .call1((native_field(&case, ARGUMENTS_FIELD)?,))?, )?; for field in COPIED_CASE_FIELDS { - fields.set_item(field, case.getattr(field)?)?; + fields.set_item(field, native_field(&case, field)?)?; } fields.set_item(LOCATION_FIELD, &location)?; let audit: Bound<'py, PyAny> = context @@ -112,11 +116,11 @@ pub(crate) fn factory_audits<'py>( /// `audit.name or audit.definition_name`. pub(crate) fn audit_identity(audit: &Bound<'_, PyAny>) -> PyResult { - let name: Bound<'_, PyAny> = audit.getattr(NAME_FIELD)?; + let name: Bound<'_, PyAny> = native_field(audit, NAME_FIELD)?; if name.is_truthy()? { return name.extract(); } - audit.getattr(DEFINITION_NAME_FIELD)?.extract() + native_field(audit, DEFINITION_NAME_FIELD)?.extract() } /// The parsed families as `SchemaDynamicColumnFamily` objects. @@ -182,8 +186,7 @@ pub(crate) fn enum_columns<'py>( continue; }; resolved.set_item(&fact.name, &declaration)?; - let mut audits: Vec> = column - .getattr(AUDITS_FIELD)? + let mut audits: Vec> = native_field(&column, AUDITS_FIELD)? .try_iter()? .collect::>()?; if enforced { @@ -201,11 +204,11 @@ pub(crate) fn enum_columns<'py>( .push(ACCEPTED_VALUES_AUDIT.to_owned()); } } - let scalar_type: Bound<'py, PyAny> = declaration.getattr(SCALAR_TYPE_ATTRIBUTE)?; + let scalar_type: Bound<'py, PyAny> = native_field(&declaration, SCALAR_TYPE_ATTRIBUTE)?; fact.data_type = scalar_type.extract()?; let fields: Bound<'py, PyDict> = PyDict::new(py); for field in KEPT_COLUMN_FIELDS { - fields.set_item(field, column.getattr(field)?)?; + fields.set_item(field, native_field(&column, field)?)?; } fields.set_item(TYPE_FIELD, scalar_type)?; fields.set_item(AUDITS_FIELD, PyTuple::new(py, audits)?)?; @@ -228,8 +231,8 @@ fn accepted_values<'py>( ) -> PyResult> { let py: Python<'py> = declaration.py(); let mut members: Vec> = Vec::new(); - for member in declaration.getattr(MEMBERS_FIELD)?.try_iter()? { - members.push(member?.getattr(VALUE_FIELD)?); + for member in native_field(declaration, MEMBERS_FIELD)?.try_iter()? { + members.push(native_field(&member?, VALUE_FIELD)?); } let arguments: Bound<'py, PyDict> = PyDict::new(py); arguments.set_item(VALUES_ARGUMENT, PyTuple::new(py, members)?)?; @@ -251,12 +254,11 @@ fn generated_usages<'py>( consumer: &Bound<'py, PyAny>, resolved: &Bound<'py, PyDict>, ) -> PyResult>> { - let visibility: Bound<'py, PyDict> = declarations - .getattr(ENUM_VISIBILITY_ATTRIBUTE)? - .downcast_into()?; + let visibility: Bound<'py, PyDict> = + native_field(declarations, ENUM_VISIBILITY_ATTRIBUTE)?.downcast_into()?; let mut usages: Vec> = Vec::new(); for declaration in resolved.values() { - let Some(records) = visibility.get_item(declaration.getattr(NAME_FIELD)?)? else { + let Some(records) = visibility.get_item(native_field(&declaration, NAME_FIELD)?)? else { continue; }; for record in records.try_iter()? { @@ -265,9 +267,9 @@ fn generated_usages<'py>( &context.classes.usage_record, &[ (CONSUMER_FIELD, consumer.clone()), - (DECLARATION_FIELD, record.getattr(DECLARATION_FIELD)?), + (DECLARATION_FIELD, native_field(&record, DECLARATION_FIELD)?), (KIND_FIELD, context.classes.generated_usage.clone()), - (THROUGH_FIELD, record.getattr(THROUGH_FIELD)?), + (THROUGH_FIELD, native_field(&record, THROUGH_FIELD)?), ], )?); } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_header.rs b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_header.rs index 57fad6509a..75fbcd9740 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_header.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_header.rs @@ -28,6 +28,7 @@ use crate::bindings::_helpers::model_config::authored_nodes::PyNode; use crate::bindings::_helpers::model_config::column_migrations::column_migration_error; use crate::bindings::_helpers::model_config::config_errors::native_config_error; use crate::bindings::_helpers::model_config::header_metadata::{audit_tuple, column_tuple}; +use crate::bindings::_helpers::records::fields::field as native_field; const DESCRIPTION_KEY: &str = "description"; const COLUMNS_KEY: &str = "columns"; @@ -73,7 +74,7 @@ pub(crate) fn header_schema<'py>( source: &HeaderSource<'_, 'py>, ) -> PyResult> { let description: Option = match (description(context, source)?, source.schema) { - (None, Some(schema)) => schema.declaration.getattr(DESCRIPTION_KEY)?.extract()?, + (None, Some(schema)) => native_field(&schema.declaration, DESCRIPTION_KEY)?.extract()?, (description, _) => description, }; let columns: Option> = present(source.values, COLUMNS_KEY)?; @@ -154,7 +155,7 @@ pub(crate) fn header_schema<'py>( direct_audits.iter().chain(generated).collect::>(), )?; let families: Vec> = dynamic_family_objects(py, context, &families)?; - let enums: Bound<'py, PyDict> = source.visible.0.getattr(ENUMS_FIELD)?.downcast_into()?; + let enums: Bound<'py, PyDict> = native_field(source.visible.0, ENUMS_FIELD)?.downcast_into()?; let enums: Bound<'py, PyDict> = match source.schema { Some(schema) => { let merged: Bound<'py, PyDict> = enums.copy()?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_hooks.rs b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_hooks.rs index 8fdd710adf..2f27b2dd4e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_hooks.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_hooks.rs @@ -36,6 +36,7 @@ use crate::bindings::_helpers::compiled_project::model_loop_named::{ SQL_HOOK_DECLARATION, named_usages, }; use crate::bindings::_helpers::compiled_project::model_loop_visibility::ModelVisibility; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::render::declaration_expansion::{ DeclarationExpansion, ExpandedDeclarations, ExpansionClasses, ExpansionConsumer, expand_declarations, @@ -229,7 +230,7 @@ impl<'py> HookRun<'_, '_, 'py> { ) -> PyResult> { let start: usize = self.references.borrow().len(); let statement: String = self - .value(&entry.getattr(STATEMENT_ATTRIBUTE)?, scope, position)? + .value(&native_field(entry, STATEMENT_ATTRIBUTE)?, scope, position)? .str()? .to_str()? .to_owned(); @@ -243,7 +244,7 @@ impl<'py> HookRun<'_, '_, 'py> { KWARGS_ATTRIBUTE, DESCRIPTION_ATTRIBUTE, ] { - fields.set_item(field, entry.getattr(field)?)?; + fields.set_item(field, native_field(entry, field)?)?; } fields.set_item(READS_ATTRIBUTE, self.reads_since(start)?)?; self.context.classes.sql_hook_entry.call((), Some(&fields)) @@ -256,7 +257,7 @@ impl<'py> HookRun<'_, '_, 'py> { position: HookPosition<'_>, ) -> PyResult> { let py: Python<'py> = entry.py(); - let name: String = entry.getattr(NAME_ATTRIBUTE)?.extract()?; + let name: String = native_field(entry, NAME_ATTRIBUTE)?.extract()?; let label: String = named_label(&name, position); let Some(definition) = self.classes().definitions.get_item(&name)? else { let mut known: Vec = self @@ -285,9 +286,10 @@ impl<'py> HookRun<'_, '_, 'py> { &scope.consumer, (self.model.name, self.model.relative_path), )?); - let kwargs: Bound<'py, PyDict> = entry.getattr(KWARGS_ATTRIBUTE)?.downcast_into()?; - let body: String = definition.getattr(SQL_BODY_ATTRIBUTE)?.extract()?; - let definition_path: Bound<'py, PyAny> = definition.getattr(RELATIVE_PATH_ATTRIBUTE)?; + let kwargs: Bound<'py, PyDict> = native_field(entry, KWARGS_ATTRIBUTE)?.downcast_into()?; + let body: String = native_field(&definition, SQL_BODY_ATTRIBUTE)?.extract()?; + let definition_path: Bound<'py, PyAny> = + native_field(&definition, RELATIVE_PATH_ATTRIBUTE)?; let owner: String = format!("{} {label}", scope.file_path); let definition_label: String = format!("SQL hook '{name}' in {}", definition_path.str()?.to_str()?); @@ -308,8 +310,7 @@ impl<'py> HookRun<'_, '_, 'py> { compile_error(self.context.classes, &message, None) })?; let start: usize = self.references.borrow().len(); - let hook_path: String = definition - .getattr(FILE_PATH_ATTRIBUTE)? + let hook_path: String = native_field(&definition, FILE_PATH_ATTRIBUTE)? .str()? .to_str()? .to_owned(); @@ -330,7 +331,7 @@ impl<'py> HookRun<'_, '_, 'py> { fields.set_item(KWARGS_ATTRIBUTE, kwargs.copy()?)?; fields.set_item( DESCRIPTION_ATTRIBUTE, - definition.getattr(DESCRIPTION_ATTRIBUTE)?, + native_field(&definition, DESCRIPTION_ATTRIBUTE)?, )?; fields.set_item(READS_ATTRIBUTE, self.reads_since(start)?)?; self.context.classes.sql_hook_entry.call((), Some(&fields)) @@ -396,7 +397,10 @@ impl<'py> HookRun<'_, '_, 'py> { } else { let fields: Bound<'py, PyDict> = PyDict::new(py); for field in UNSCOPED_FIELDS { - fields.set_item(field, self.model.visible.declarations.getattr(field)?)?; + fields.set_item( + field, + native_field(&self.model.visible.declarations, field)?, + )?; } self.context .classes @@ -558,7 +562,7 @@ fn reject_hook_intrinsics( if !entry.is_instance(&context.classes.sql_hook_entry)? { continue; } - let statement: String = entry.getattr(STATEMENT_ATTRIBUTE)?.extract()?; + let statement: String = native_field(&entry, STATEMENT_ATTRIBUTE)?.extract()?; if let IntrinsicCheck::Rejected(message) = intrinsic_free(&statement, &model_loop.reserved_markers, &hook_context) { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_host.rs b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_host.rs index cb761c9cee..478af8e829 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_host.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_host.rs @@ -22,6 +22,7 @@ use crate::bindings::_helpers::compiled_project::project::{ DeclarationPy, config_values, declarations, }; use crate::bindings::_helpers::discovery::model_file_record::NativeModelFile; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::render::declaration_expansion::{ ExpandedDeclarations, ScannedReference, span_rows, }; @@ -76,7 +77,7 @@ impl ModelLoopRow { let (query_sql, contents) = record.texts(py); Ok(Self { paths: ModelPaths { - name: file_path.getattr(STEM_ATTRIBUTE)?.extract()?, + name: native_field(file_path, STEM_ATTRIBUTE)?.extract()?, posix_path: relative_path.call_method0(AS_POSIX_METHOD)?.extract()?, relative_path: relative_path.str()?.to_str()?.to_owned(), file_path: file_path.str()?.to_str()?.to_owned(), @@ -158,9 +159,9 @@ pub(crate) fn finished_record<'py>( None => Vec::new(), }; let local_enums: Bound<'py, PyTuple> = - dict_values(&inputs.visible.getattr(LOCAL_ENUMS_ATTRIBUTE)?)?; + dict_values(&native_field(inputs.visible, LOCAL_ENUMS_ATTRIBUTE)?)?; let local_constants: Bound<'py, PyTuple> = - dict_values(&inputs.visible.getattr(LOCAL_CONSTANTS_ATTRIBUTE)?)?; + dict_values(&native_field(inputs.visible, LOCAL_CONSTANTS_ATTRIBUTE)?)?; let rows_arguments: Bound<'py, PyDict> = PyDict::new(py); rows_arguments.set_item(ENUMS_ARGUMENT, &local_enums)?; rows_arguments.set_item(CONSTANTS_ARGUMENT, &local_constants)?; @@ -188,7 +189,7 @@ pub(crate) fn finished_record<'py>( .map(|item| item.into_bound(py)) .collect(), )? { - names.push(dependency.getattr(NAME_ATTRIBUTE)?.extract()?); + names.push(native_field(&dependency, NAME_ATTRIBUTE)?.extract()?); } ( macros.sql, @@ -275,8 +276,8 @@ pub(crate) fn finished_record<'py>( config: ModelConfigFacts { values, encoding_error, - header_keys: finished.config.getattr(HEADER_KEYS_ATTRIBUTE)?.extract()?, - layer_schema: finished.config.getattr(LAYER_SCHEMA_ATTRIBUTE)?.extract()?, + header_keys: native_field(&finished.config, HEADER_KEYS_ATTRIBUTE)?.extract()?, + layer_schema: native_field(&finished.config, LAYER_SCHEMA_ATTRIBUTE)?.extract()?, contract: text_value(&stripped, CONTRACT_KEY)?, materialized: materialization(&stripped)?, incremental_mode: text_value(&stripped, INCREMENTAL_MODE_KEY)?, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_macros.rs b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_macros.rs index 1c48783a98..64a85b3c40 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_macros.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_macros.rs @@ -80,7 +80,7 @@ pub(crate) fn expanded_macros<'py>( let declarations: Bound<'py, PyAny> = if classes.scoped { let fields: Bound<'py, PyDict> = PyDict::new(py); for field in CONTEXT_FIELDS { - fields.set_item(field, visible.declarations.getattr(field)?)?; + fields.set_item(field, native_field(&visible.declarations, field)?)?; } fields.set_item("consumer", &visible.consumer)?; classes.declaration_context.call((), Some(&fields))? @@ -119,10 +119,7 @@ pub(crate) fn expanded_with<'py>( declarations.clone().unbind(), consumer.clone().unbind(), ), - classes - .macro_context - .getattr(ENFORCE_ATTRIBUTE)? - .is_truthy()?, + native_field(&classes.macro_context, ENFORCE_ATTRIBUTE)?.is_truthy()?, classes.expansion.clone_ref(py), )?; let expansion: Bound<'py, NativeMacroExpansion> = Bound::new(py, expansion)?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_named.rs b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_named.rs index 6a237a47f2..88fdb63008 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_named.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_named.rs @@ -11,6 +11,7 @@ use sqlbuild_scopes::scope_index::models::{ }; use crate::bindings::_helpers::compiled_project::model_loop::LoopContext; +use crate::bindings::_helpers::records::fields::field as native_field; const NAME_FIELD: &str = "name"; const PATH_FIELD: &str = "path"; @@ -105,7 +106,7 @@ pub(crate) fn resource_named_usages<'py>( } let fields: Bound<'py, PyDict> = PyDict::new(py); fields.set_item(CONSUMER_FIELD, consumer)?; - fields.set_item(DECLARATION_FIELD, record.getattr(IDENTITY_FIELD)?)?; + fields.set_item(DECLARATION_FIELD, native_field(&record, IDENTITY_FIELD)?)?; Ok(vec![context.classes.usage_record.call((), Some(&fields))?]) } @@ -145,22 +146,22 @@ pub(crate) fn scope_path_message(declared: &DeclaredScope, label: &str) -> Strin /// The scope facts of a Python `DeclarationRecord`. fn declared_scope(record: &Bound<'_, PyAny>) -> PyResult { - let scope: String = record - .getattr(SCOPE_FIELD)? - .getattr(VALUE_FIELD)? - .extract()?; - let owner: Bound<'_, PyAny> = record.getattr(IDENTITY_FIELD)?.getattr(OWNER_FIELD)?; + let scope: String = + native_field(&native_field(record, SCOPE_FIELD)?, VALUE_FIELD)?.extract()?; + let owner: Bound<'_, PyAny> = + native_field(&native_field(record, IDENTITY_FIELD)?, OWNER_FIELD)?; let owner: Option = if owner.is_none() { None } else { - let kind: String = owner.getattr(KIND_FIELD)?.getattr(VALUE_FIELD)?.extract()?; - let name: String = owner.getattr(NAME_FIELD)?.extract()?; + let kind: String = + native_field(&native_field(&owner, KIND_FIELD)?, VALUE_FIELD)?.extract()?; + let name: String = native_field(&owner, NAME_FIELD)?.extract()?; ResourceKind::parse(&kind).map(|kind| ResourceIdentity { kind, name }) }; Ok(DeclaredScope { scope: ScopeKind::parse(&scope).unwrap_or(ScopeKind::Global), owner, - owning_path: record.getattr(OWNING_PATH_FIELD)?.extract()?, + owning_path: native_field(record, OWNING_PATH_FIELD)?.extract()?, }) } @@ -179,21 +180,17 @@ fn report_inaccessible( .and_then(Path::to_str) .filter(|parent| !parent.is_empty()) .unwrap_or(CURRENT_FOLDER); - let owning_path: Bound<'_, PyAny> = record.getattr(OWNING_PATH_FIELD)?; + let owning_path: Bound<'_, PyAny> = native_field(record, OWNING_PATH_FIELD)?; let owner: String = if owning_path.is_truthy()? { owning_path.str()?.to_string() } else { - record - .getattr(OWNERSHIP_ROOT_FIELD)? - .getattr(PATH_FIELD)? + native_field(&native_field(record, OWNERSHIP_ROOT_FIELD)?, PATH_FIELD)? .str()? .to_string() }; - let record_path: String = record.getattr(PATH_FIELD)?.str()?.to_string(); - let scope: String = record - .getattr(SCOPE_FIELD)? - .getattr(VALUE_FIELD)? - .extract()?; + let record_path: String = native_field(record, PATH_FIELD)?.str()?.to_string(); + let scope: String = + native_field(&native_field(record, SCOPE_FIELD)?, VALUE_FIELD)?.extract()?; let (label_text, role) = (kind.label, kind.role); let fields: Bound<'_, PyDict> = PyDict::new(py); fields.set_item("phase", "compile")?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_opt_out.rs b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_opt_out.rs index dcd90a2846..afde98b0d6 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_opt_out.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_opt_out.rs @@ -14,6 +14,7 @@ use crate::bindings::_helpers::compiled_project::model_loop::{ }; use crate::bindings::_helpers::compiled_project::model_loop_inputs::{HOOK_KEYS, present}; use crate::bindings::_helpers::project_assembly::resources::{syntax_error_message, syntax_valid}; +use crate::bindings::_helpers::records::fields::field as native_field; const OPT_OUT_KEYS: [&str; 2] = ["sql_analysis", "sql_validation"]; const CURSOR_TYPE_KEY: &str = "cursor_type"; @@ -150,7 +151,7 @@ fn hook_statements( return Ok(vec![text.extract()?]); } if value.is_instance(&context.classes.sql_hook_entry)? { - return hook_statements(context, &value.getattr(STATEMENT_ATTRIBUTE)?); + return hook_statements(context, &native_field(value, STATEMENT_ATTRIBUTE)?); } let mut statements: Vec = Vec::new(); if value.is_instance_of::() || value.is_instance_of::() { @@ -181,14 +182,15 @@ fn location<'py>( model: &OptOutModel<'_, 'py>, ) -> PyResult> { let py: Python<'py> = model.config.py(); - let header_keys: Vec = model.config.getattr(HEADER_KEYS_ATTRIBUTE)?.extract()?; + let header_keys: Vec = native_field(model.config, HEADER_KEYS_ATTRIBUTE)?.extract()?; let (path, position): (&str, TextPosition) = if header_keys .iter() .any(|key| OPT_OUT_KEYS.contains(&key.as_str())) { (model.relative_path, opt_out_position(model.contents, 0)) } else { - let path_key: Option = model.config.getattr(PATH_DEFAULT_ATTRIBUTE)?.extract()?; + let path_key: Option = + native_field(model.config, PATH_DEFAULT_ATTRIBUTE)?.extract()?; let config_path: &Path = Path::new(&model_loop.project_config_path); let config_text: String = if config_path.is_file() { std::fs::read_to_string(config_path)? diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_python_hooks.rs b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_python_hooks.rs index a3ccac90e9..d0fa86a4c2 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_python_hooks.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_python_hooks.rs @@ -11,6 +11,7 @@ use crate::bindings::_helpers::compiled_project::model_loop::{LoopContext, compi use crate::bindings::_helpers::compiled_project::model_loop_named::{ PYTHON_HOOK_DECLARATION, named_usages, }; +use crate::bindings::_helpers::records::fields::field as native_field; /// Hook keys in the sorted order `validate_python_hook_config` reads them. const SORTED_HOOK_KEYS: [&str; 2] = ["post_hooks", "pre_hooks"]; @@ -67,7 +68,7 @@ impl HookSignatures { let entries: Vec<(&str, usize, Bound<'py, PyAny>)> = python_entries(context, values)?; let functions: &Bound<'py, PyDict> = &context.classes.hooks.functions; for (hook_key, index, entry) in &entries { - let name: String = entry.getattr(NAME_FIELD)?.extract()?; + let name: String = native_field(entry, NAME_FIELD)?.extract()?; let site: HookSite<'_> = HookSite { model_name, hook_key, @@ -88,8 +89,7 @@ impl HookSignatures { ); return Err(compile_error(context.classes, &message, None)); }; - let kwargs: Vec = entry - .getattr(KWARGS_FIELD)? + let kwargs: Vec = native_field(entry, KWARGS_FIELD)? .downcast_into::()? .keys() .extract()?; @@ -102,7 +102,7 @@ impl HookSignatures { } let mut usages: Vec> = Vec::new(); for (_, _, entry) in &entries { - let name: String = entry.getattr(NAME_FIELD)?.extract()?; + let name: String = native_field(entry, NAME_FIELD)?.extract()?; usages.extend(named_usages( context, (&PYTHON_HOOK_DECLARATION, &name), @@ -127,7 +127,7 @@ impl HookSignatures { .classes .hooks .signature - .call1((function.getattr(FUNCTION_FIELD)?,))? + .call1((native_field(function, FUNCTION_FIELD)?,))? .extract()?; self.by_name .borrow_mut() diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_schema.rs b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_schema.rs index 7c6d83eb43..0bcfd48851 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_schema.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_schema.rs @@ -7,6 +7,7 @@ use sqlbuild_analysis::compiled_project::models::SchemaColumnFact; use crate::bindings::_helpers::compiled_project::model_loop::{LoopContext, compile_error}; use crate::bindings::_helpers::compiled_project::model_loop_inputs::present; use crate::bindings::_helpers::compiled_project::schema_facts::python_column_fact; +use crate::bindings::_helpers::records::fields::field as native_field; const MODEL_SCHEMA_KEY: &str = "model_schema"; const NAME_FIELD: &str = "name"; @@ -74,11 +75,9 @@ pub(crate) fn model_schema<'py>( /// The schema's column names, which validation reads as the declared columns. pub(crate) fn schema_column_names(schema: &ModelSchema<'_>) -> PyResult> { - schema - .declaration - .getattr(COLUMNS_FIELD)? + native_field(&schema.declaration, COLUMNS_FIELD)? .try_iter()? - .map(|column| column?.getattr(NAME_FIELD)?.extract()) + .map(|column| native_field(&column?, NAME_FIELD)?.extract()) .collect() } @@ -89,8 +88,8 @@ pub(crate) fn schema_enums<'py>( ) -> PyResult> { let py: Python<'py> = schema.declaration.py(); let enums: Bound<'py, PyDict> = PyDict::new(py); - for column in schema.declaration.getattr(COLUMNS_FIELD)?.try_iter()? { - let column_type: Bound<'py, PyAny> = column?.getattr(TYPE_FIELD)?; + for column in native_field(&schema.declaration, COLUMNS_FIELD)?.try_iter()? { + let column_type: Bound<'py, PyAny> = native_field(&column?, TYPE_FIELD)?; if column_type.is_none() { continue; } @@ -119,7 +118,7 @@ pub(crate) fn merged_columns<'py>( specs: Vec::new(), facts: Vec::new(), }; - for column in schema.declaration.getattr(COLUMNS_FIELD)?.try_iter()? { + for column in native_field(&schema.declaration, COLUMNS_FIELD)?.try_iter()? { let column: Bound<'py, PyAny> = column?; merged .facts @@ -167,13 +166,11 @@ fn augmented<'py>( (local, local_fact): (&Bound<'py, PyAny>, SchemaColumnFact), ) -> PyResult<(Bound<'py, PyAny>, SchemaColumnFact)> { let py: Python<'py> = named.py(); - let mut audits: Vec> = named - .getattr(AUDITS_FIELD)? + let mut audits: Vec> = native_field(named, AUDITS_FIELD)? .try_iter()? .collect::>()?; let mut fact: SchemaColumnFact = named_fact.clone(); - for (audit, definition) in local - .getattr(AUDITS_FIELD)? + for (audit, definition) in native_field(local, AUDITS_FIELD)? .try_iter()? .zip(local_fact.audit_definitions) { @@ -192,7 +189,7 @@ fn augmented<'py>( } let fields: Bound<'py, PyDict> = PyDict::new(py); for field in KEPT_NAMED_FIELDS { - fields.set_item(field, named.getattr(field)?)?; + fields.set_item(field, native_field(named, field)?)?; } fields.set_item(AUDITS_FIELD, PyTuple::new(py, audits)?)?; fields.set_item(MIGRATE_FROM_FIELD, &local_fact.migrate_from)?; @@ -245,14 +242,14 @@ fn check_augmentation( /// Where a named-schema column is declared, as its `path:line` or the named schema. fn origin(named: &Bound<'_, PyAny>) -> PyResult { - let location: Bound<'_, PyAny> = named.getattr(LOCATION_FIELD)?; + let location: Bound<'_, PyAny> = native_field(named, LOCATION_FIELD)?; if location.is_none() { return Ok("the named schema".to_owned()); } Ok(format!( "{}:{}", - location.getattr(PATH_FIELD)?.str()?, - location.getattr(LINE_FIELD)?.str()? + native_field(&location, PATH_FIELD)?.str()?, + native_field(&location, LINE_FIELD)?.str()? )) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_visibility.rs b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_visibility.rs index fb31778a67..3042c27beb 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_visibility.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_loop_visibility.rs @@ -125,21 +125,19 @@ impl<'py> ModelVisibility<'py> { let (enums, constants) = if classes.projects { let resolved: Bound<'py, PyAny> = self.resolved(context, model.file_path)?; for field in SHARED_FIELDS { - fields.set_item(field, resolved.getattr(field)?)?; + fields.set_item(field, native_field(&resolved, field)?)?; } fields.set_item( ENUM_VISIBILITY_FIELD, - resolved.getattr(ENUM_VISIBILITY_FIELD)?, + native_field(&resolved, ENUM_VISIBILITY_FIELD)?, )?; fields.set_item( CONSTANT_VISIBILITY_FIELD, - resolved.getattr(CONSTANT_VISIBILITY_FIELD)?, + native_field(&resolved, CONSTANT_VISIBILITY_FIELD)?, )?; ( - resolved.getattr(ENUMS_FIELD)?.downcast_into::()?, - resolved - .getattr(CONSTANTS_FIELD)? - .downcast_into::()?, + native_field(&resolved, ENUMS_FIELD)?.downcast_into::()?, + native_field(&resolved, CONSTANTS_FIELD)?.downcast_into::()?, ) } else { for field in [ @@ -204,9 +202,9 @@ fn rebound<'py>( ) -> PyResult> { let py: Python<'py> = cached.py(); let enum_visibility: Bound<'py, PyDict> = - cached.getattr(ENUM_VISIBILITY_FIELD)?.downcast_into()?; + native_field(cached, ENUM_VISIBILITY_FIELD)?.downcast_into()?; let constant_visibility: Bound<'py, PyDict> = - cached.getattr(CONSTANT_VISIBILITY_FIELD)?.downcast_into()?; + native_field(cached, CONSTANT_VISIBILITY_FIELD)?.downcast_into()?; if enum_visibility.is_empty() && constant_visibility.is_empty() { return Ok(cached.clone()); } @@ -215,7 +213,7 @@ fn rebound<'py>( native_field(resolver, "native_contexts")?.downcast_into()?; let fields: Bound<'py, PyDict> = PyDict::new(py); for field in VISIBLE_FIELDS { - fields.set_item(field, cached.getattr(field)?)?; + fields.set_item(field, native_field(cached, field)?)?; } fields.set_item( ENUM_VISIBILITY_FIELD, @@ -257,7 +255,7 @@ fn by_name<'py>(declarations: &Bound<'py, PyAny>) -> PyResult let named: Bound<'py, PyDict> = PyDict::new(declarations.py()); for declaration in declarations.try_iter()? { let declaration: Bound<'py, PyAny> = declaration?; - named.set_item(declaration.getattr(NAME_FIELD)?, declaration)?; + named.set_item(native_field(&declaration, NAME_FIELD)?, declaration)?; } Ok(named) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/project.rs b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/project.rs index a663b6fbd2..c0422ad9ca 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/project.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/project.rs @@ -162,10 +162,7 @@ impl NativeCompiledProject { }; owners.push(model_owner( record.name.clone(), - record - .model_file - .bind(py) - .getattr(RELATIVE_PATH_ATTRIBUTE)? + native_field(record.model_file.bind(py), RELATIVE_PATH_ATTRIBUTE)? .str()? .to_string(), ( @@ -176,12 +173,10 @@ impl NativeCompiledProject { } for source in &self.sources { let attached: &AttachedSource = source.attached(); - let path: String = attached - .source_file - .bind(py) - .getattr(RELATIVE_PATH_ATTRIBUTE)? - .str()? - .to_string(); + let path: String = + native_field(attached.source_file.bind(py), RELATIVE_PATH_ATTRIBUTE)? + .str()? + .to_string(); owners.push(resource_owner( SOURCE_OWNER, attached.name.clone(), @@ -199,14 +194,10 @@ impl NativeCompiledProject { SEED_OWNER, name.to_owned(), ( - seed_file - .bind(py) - .getattr(RELATIVE_PATH_ATTRIBUTE)? + native_field(seed_file.bind(py), RELATIVE_PATH_ATTRIBUTE)? .str()? .to_string(), - schema_file - .bind(py) - .getattr(RELATIVE_PATH_ATTRIBUTE)? + native_field(schema_file.bind(py), RELATIVE_PATH_ATTRIBUTE)? .str()? .to_string(), ), @@ -452,10 +443,7 @@ impl NativeCompiledProject { .iter() .map(|record| { Ok(( - record - .model_file - .bind(py) - .getattr(RELATIVE_PATH_ATTRIBUTE)? + native_field(record.model_file.bind(py), RELATIVE_PATH_ATTRIBUTE)? .str()? .to_string(), record.references.clone(), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/project_view.rs b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/project_view.rs index 59376c3924..53b1b9adb0 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/project_view.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/project_view.rs @@ -20,6 +20,7 @@ use crate::bindings::_helpers::compiled_project::source_views::NativeCompiledSou use crate::bindings::_helpers::graph::project_graph::{ NativeProjectGraph, ResourceRow, project_graph, }; +use crate::bindings::_helpers::records::fields::field as native_field; const MODELS_FIELD: &str = "models"; const RUN_ID_FIELD: &str = "run_id"; @@ -504,7 +505,7 @@ impl NativeProjectView { let expansion: Py = authored_expansion(&classes, record)?; if !expansion.is_none(py) { let path: Bound<'_, PyAny> = - record.model_file.bind(py).getattr(FILE_PATH_ATTRIBUTE)?; + native_field(record.model_file.bind(py), FILE_PATH_ATTRIBUTE)?; expansions.set_item(path, expansion)?; } } @@ -536,9 +537,10 @@ impl NativeProjectView { rows.push(match seed.cast::() { Ok(native) => native.get().graph_row(py)?, Err(_) => { - let tags: Bound<'_, PyAny> = seed - .getattr(SCHEMA_ENTRY_ATTRIBUTE)? - .getattr(TAGS_ATTRIBUTE)?; + let tags: Bound<'_, PyAny> = native_field( + &native_field(&seed, SCHEMA_ENTRY_ATTRIBUTE)?, + TAGS_ATTRIBUTE, + )?; resource_row(&seed, Some(&tags))? } }); @@ -548,7 +550,7 @@ impl NativeProjectView { rows.push(match function.cast::() { Ok(native) => native.get().graph_row(py)?, Err(_) => { - let tags: Bound<'_, PyAny> = function.getattr(TAGS_ATTRIBUTE)?; + let tags: Bound<'_, PyAny> = native_field(&function, TAGS_ATTRIBUTE)?; resource_row(&function, Some(&tags))? } }); @@ -567,7 +569,7 @@ fn resource_row( tags: Option<&Bound<'_, PyAny>>, ) -> PyResult { let mut deps: Vec<(String, String)> = Vec::new(); - for key in resource.getattr(DEPS_ATTRIBUTE)?.try_iter()? { + for key in native_field(resource, DEPS_ATTRIBUTE)?.try_iter()? { deps.push(key_row(&key?)?); } let mut tag_names: Vec = Vec::new(); @@ -577,7 +579,7 @@ fn resource_row( } } Ok(( - key_row(&resource.getattr(KEY_ATTRIBUTE)?)?, + key_row(&native_field(resource, KEY_ATTRIBUTE)?)?, deps, tag_names, None, @@ -587,7 +589,9 @@ fn resource_row( /// `(resource type, name)` of a `CompiledObjectKey`. pub(crate) fn key_row(key: &Bound<'_, PyAny>) -> PyResult<(String, String)> { Ok(( - key.getattr(RESOURCE_TYPE_ATTRIBUTE)?.str()?.extract()?, - key.getattr(NAME_ATTRIBUTE)?.extract()?, + native_field(key, RESOURCE_TYPE_ATTRIBUTE)? + .str()? + .extract()?, + native_field(key, NAME_ATTRIBUTE)?.extract()?, )) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/records.rs b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/records.rs index e751dac524..ebad43bfe1 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/records.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/records.rs @@ -14,9 +14,9 @@ use crate::bindings::_helpers::compiled_project::model_input_view::NativeModelIn use crate::bindings::_helpers::compiled_project::project::{NativeCompiledProject, model_facts}; use crate::bindings::_helpers::compiled_project::project_view::NativeProjectView; use crate::bindings::_helpers::compiled_project::schema_facts::entry_schema_fact; +use crate::bindings::_helpers::records::construct::{enum_member, new_record, record_class}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; const DATACLASSES_MODULE: &str = "dataclasses"; const DECIMAL_KIND: &str = "decimal"; @@ -39,7 +39,7 @@ pub(crate) fn replace<'py>( /// `value` when it already is a `class` record, else `class(**{field: getattr(value, field)})`. fn record<'py>(value: &Bound<'py, PyAny>, class: RecordId) -> PyResult> { let py: Python<'py> = value.py(); - let class: Bound<'py, PyAny> = registry::class(py, class)?; + let class: Bound<'py, PyAny> = record_class(py, class)?; if value.is_instance(&class)? { return Ok(value.clone()); } @@ -49,10 +49,7 @@ fn record<'py>(value: &Bound<'py, PyAny>, class: RecordId) -> PyResult = native_field(&field?, "name")?; - fields.set_item( - &name, - value.getattr(name.downcast::()?)?, - )?; + fields.set_item(&name, native_field(value, &name.extract::()?)?)?; } class.call((), Some(&fields)) } @@ -111,7 +108,7 @@ pub(crate) fn compiled_project_record<'py>( include_sql_expansions: bool, ) -> PyResult> { let py: Python<'py> = project.py(); - let class: Bound<'py, PyAny> = registry::class(py, RecordId::CompiledProject)?; + let class: Bound<'py, PyAny> = record_class(py, RecordId::CompiledProject)?; if project.is_instance(&class)? { if include_sql_expansions { return Ok(project.clone()); @@ -129,7 +126,7 @@ pub(crate) fn compiled_project_record<'py>( ] { records_of(&fields, pair)?; } - class.call((), Some(&fields)) + new_record(py, RecordId::CompiledProject, (), Some(&fields)) } /// The model with these final binding diagnostics. @@ -140,7 +137,7 @@ pub(crate) fn with_binding_diagnostics<'py>( diagnostics: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = model.py(); - if model.is_instance(®istry::class(py, RecordId::CompiledModel)?)? { + if model.is_instance(&record_class(py, RecordId::CompiledModel)?)? { return replace(model, &[("binding_diagnostics", diagnostics.clone())]); } model.call_method1("with_binding_diagnostics", (diagnostics,)) @@ -163,7 +160,7 @@ pub(crate) fn with_type_recovery<'py>( unchecked.push(column); } } - if !model.is_instance(®istry::class(py, RecordId::CompiledModel)?)? { + if !model.is_instance(&record_class(py, RecordId::CompiledModel)?)? { return model.call_method1("with_type_recovery", (diagnostics, unchecked)); } let inferred: Bound<'py, PyAny> = native_field(model, "inferred_columns")?; @@ -223,7 +220,7 @@ fn with_changes<'py>( ) -> PyResult> { let py: Python<'py> = value.py(); let changes: Bound<'py, PyDict> = changes.unwrap_or_else(|| PyDict::new(py)); - if value.is_instance(®istry::class(py, class)?)? { + if value.is_instance(&record_class(py, class)?)? { return imported(py, DATACLASSES_MODULE, "replace")?.call((value,), Some(&changes)); } value.call_method("with_changes", (), Some(&changes)) @@ -253,7 +250,7 @@ pub(crate) fn compile_inputs_view<'py>( } let keywords: Bound<'py, PyDict> = fields.copy()?; keywords.set_item("native_project", native_project)?; - registry::class(py, RecordId::CompileProjectInputs)?.call((), Some(&keywords)) + new_record(py, RecordId::CompileProjectInputs, (), Some(&keywords)) } /// A typed SQL value as the plain value native consumers read. @@ -391,8 +388,7 @@ pub(crate) fn record_model_analyses<'py>( .map(|model| model.extract::>()) .collect::>()?; let mut transferred: usize = NativeCompiledProject::record_view_analyses(project, views)?; - let nullable: Bound<'py, PyAny> = - registry::class(py, RecordId::InferredNullability)?.getattr("NULLABLE")?; + let nullable: Bound<'py, PyAny> = enum_member(py, RecordId::InferredNullability, "NULLABLE")?; for model in &models { if model.is_instance_of::() { continue; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/schema_facts.rs b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/schema_facts.rs index 1124532b51..d72ac55f30 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/schema_facts.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/schema_facts.rs @@ -1,5 +1,6 @@ //! Native facts of declared schema columns and families, read once from their Python specs. +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; use sqlbuild_analysis::compiled_project::models::{ DynamicFamilyFact, ModelSchemaFact, SchemaColumnFact, @@ -33,12 +34,12 @@ pub(crate) fn python_column_fact( let mut audit_definitions: Vec = Vec::new(); if let Some(audits) = present(column, AUDITS_FIELD)? { for audit in audits.try_iter()? { - audit_definitions.push(audit?.getattr(DEFINITION_NAME_FIELD)?.extract()?); + audit_definitions.push(native_field(&audit?, DEFINITION_NAME_FIELD)?.extract()?); } } let location_path: Option = match present(column, LOCATION_FIELD)? { Some(location) => { - let path: Bound<'_, PyAny> = location.getattr(PATH_FIELD)?; + let path: Bound<'_, PyAny> = native_field(&location, PATH_FIELD)?; let outside: bool = match model_path { Some(model_path) => !path.is(model_path) && path.ne(model_path)?, None => true, @@ -50,7 +51,7 @@ pub(crate) fn python_column_fact( None => None, }; Ok(SchemaColumnFact { - name: column.getattr(NAME_FIELD)?.str()?.extract()?, + name: native_field(column, NAME_FIELD)?.str()?.extract()?, data_type: optional(column, TYPE_FIELD)?, nullable: optional(column, NULLABLE_FIELD)?, described: present(column, DESCRIPTION_FIELD)?.is_some(), @@ -80,23 +81,23 @@ pub(crate) fn entry_schema_fact( return Ok(None); } let mut dynamic_columns: Vec = Vec::new(); - for family in entry.getattr(DYNAMIC_COLUMNS_FIELD)?.try_iter()? { + for family in native_field(entry, DYNAMIC_COLUMNS_FIELD)?.try_iter()? { let family: Bound<'_, PyAny> = family?; dynamic_columns.push(family_fact(&DynamicFamily { - name: family.getattr(NAME_FIELD)?.extract()?, - pivot_column: family.getattr(PIVOT_COLUMN_FIELD)?.extract()?, - value_column: family.getattr(VALUE_COLUMN_FIELD)?.extract()?, - aggregate: family.getattr(AGGREGATE_FIELD)?.extract()?, - data_type: family.getattr(TYPE_FIELD)?.extract()?, - name_pattern: family.getattr(NAME_PATTERN_FIELD)?.extract()?, + name: native_field(&family, NAME_FIELD)?.extract()?, + pivot_column: native_field(&family, PIVOT_COLUMN_FIELD)?.extract()?, + value_column: native_field(&family, VALUE_COLUMN_FIELD)?.extract()?, + aggregate: native_field(&family, AGGREGATE_FIELD)?.extract()?, + data_type: native_field(&family, TYPE_FIELD)?.extract()?, + name_pattern: native_field(&family, NAME_PATTERN_FIELD)?.extract()?, })); } Ok(Some(ModelSchemaFact { - audit_count: u32::try_from(entry.getattr(AUDITS_FIELD)?.len()?).unwrap_or(u32::MAX), - columns: python_column_facts(&entry.getattr(COLUMNS_FIELD)?, Some(model_path))?, + audit_count: u32::try_from(native_field(entry, AUDITS_FIELD)?.len()?).unwrap_or(u32::MAX), + columns: python_column_facts(&native_field(entry, COLUMNS_FIELD)?, Some(model_path))?, dynamic_columns, - type_enforcement: entry.getattr(TYPE_ENFORCEMENT_FIELD)?.is_truthy()?, - named_schema: !entry.getattr(MODEL_SCHEMA_FIELD)?.is_none(), + type_enforcement: native_field(entry, TYPE_ENFORCEMENT_FIELD)?.is_truthy()?, + named_schema: !native_field(entry, MODEL_SCHEMA_FIELD)?.is_none(), })) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/seed_views.rs b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/seed_views.rs index e270cd4b0e..197bacdc21 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/seed_views.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/seed_views.rs @@ -15,6 +15,7 @@ use crate::bindings::_helpers::compiled_project::model_input_view::{ }; use crate::bindings::_helpers::compiled_project::project::NativeCompiledProject; use crate::bindings::_helpers::graph::project_graph::ResourceRow; +use crate::bindings::_helpers::records::fields::field as native_field; const SEED_RESOURCE: &str = "seed"; const TAGS_ATTRIBUTE: &str = "tags"; @@ -54,27 +55,26 @@ pub(crate) fn paired_seeds( let py: Python<'_> = compile_error.py(); let mut names: Vec = Vec::with_capacity(declarations.len()); for (entry, _) in &declarations { - names.push(entry.bind(py).getattr(NAME_ATTRIBUTE)?.extract()?); + names.push(native_field(entry.bind(py), NAME_ATTRIBUTE)?.extract()?); } let mut stems: Vec = Vec::with_capacity(seed_files.len()); for seed_file in seed_files { stems.push( - seed_file - .bind(py) - .getattr(FILE_PATH_ATTRIBUTE)? - .getattr(STEM_ATTRIBUTE)? - .extract()?, + native_field( + &native_field(seed_file.bind(py), FILE_PATH_ATTRIBUTE)?, + STEM_ATTRIBUTE, + )? + .extract()?, ); } let pairs: Vec = match pair_seed_declarations(&names, &stems) { Ok(pairs) => pairs, Err(missing) => { let schema_file: &Py = &declarations[missing].1; - let relative_path: String = schema_file - .bind(py) - .getattr(RELATIVE_PATH_ATTRIBUTE)? - .str()? - .extract()?; + let relative_path: String = + native_field(schema_file.bind(py), RELATIVE_PATH_ATTRIBUTE)? + .str()? + .extract()?; let message: String = format!( "Seed declaration '{}' in {relative_path} has no matching CSV file under seeds/", names[missing] @@ -198,12 +198,7 @@ impl NativeCompiledSeed { pub(crate) fn graph_row(&self, py: Python<'_>) -> PyResult { read_seed(&self.owner, self.index, py, |seed, py| { let mut tags: Vec = Vec::new(); - for tag in seed - .schema_entry - .bind(py) - .getattr(TAGS_ATTRIBUTE)? - .try_iter()? - { + for tag in native_field(seed.schema_entry.bind(py), TAGS_ATTRIBUTE)?.try_iter()? { tags.push(tag?.str()?.extract()?); } Ok(( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/source_loop.rs b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/source_loop.rs index 61d293301b..62fa0f7cd4 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/source_loop.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/source_loop.rs @@ -135,10 +135,8 @@ pub(crate) fn scoped_declarations<'py>( ) -> PyResult> { let visibility = &context.classes.visibility; if visibility.projects { - let contexts: Bound<'py, NativeDeclarationContexts> = visibility - .resolver - .getattr(NATIVE_CONTEXTS_ATTRIBUTE)? - .downcast_into()?; + let contexts: Bound<'py, NativeDeclarationContexts> = + native_field(&visibility.resolver, NATIVE_CONTEXTS_ATTRIBUTE)?.downcast_into()?; let (resolved, _) = contexts .get() @@ -173,10 +171,8 @@ pub(crate) fn file_declarations<'py>( let fields: Bound<'py, PyDict> = PyDict::new(py); fields.set_item("consumer", identity)?; if visibility.projects { - let contexts: Bound<'py, NativeDeclarationContexts> = visibility - .resolver - .getattr(NATIVE_CONTEXTS_ATTRIBUTE)? - .downcast_into()?; + let contexts: Bound<'py, NativeDeclarationContexts> = + native_field(&visibility.resolver, NATIVE_CONTEXTS_ATTRIBUTE)?.downcast_into()?; let (resolved, _) = contexts .get() @@ -251,9 +247,8 @@ pub(crate) fn expanded_authored_sql<'py>( let macro_declarations: Bound<'py, PyAny> = if resolver.is_none() { py.None().into_bound(py) } else { - let contexts: Bound<'py, NativeDeclarationContexts> = resolver - .getattr(NATIVE_CONTEXTS_ATTRIBUTE)? - .downcast_into()?; + let contexts: Bound<'py, NativeDeclarationContexts> = + native_field(resolver, NATIVE_CONTEXTS_ATTRIBUTE)?.downcast_into()?; contexts .get() .resolved_context(resolver, file_path, None)? @@ -287,7 +282,7 @@ fn template_text( field: &str, label: &str, ) -> PyResult> { - let raw: Bound<'_, PyAny> = owner.getattr(field)?; + let raw: Bound<'_, PyAny> = native_field(owner, field)?; if raw.is_none() { return Ok(None); } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/source_views.rs b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/source_views.rs index 62c118242b..d90df591db 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/source_views.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/source_views.rs @@ -13,10 +13,9 @@ use crate::bindings::_helpers::compiled_project::model_input_view::{ use crate::bindings::_helpers::compiled_project::project::NativeCompiledProject; use crate::bindings::_helpers::graph::project_graph::ResourceRow; use crate::bindings::_helpers::records::fields::field as native_field; +use crate::bindings::_helpers::records::fields::replaced_keywords; const SOURCE_RESOURCE: &str = "source"; -const DATACLASSES_MODULE: &str = "dataclasses"; -const REPLACE_FUNCTION: &str = "replace"; /// `(database, schema)` native project assembly resolved for a source, or `None`. pub(crate) type SourceNamespace = Option<(Option, Option)>; @@ -114,15 +113,11 @@ impl SourceRecord { /// `dataclasses.replace(value, **changes)`. pub(crate) fn replaced( - py: Python<'_>, + _py: Python<'_>, value: &Bound<'_, PyAny>, changes: &Bound<'_, PyDict>, ) -> PyResult> { - Ok(py - .import(DATACLASSES_MODULE)? - .getattr(REPLACE_FUNCTION)? - .call((value,), Some(changes))? - .unbind()) + Ok(replaced_keywords(value, changes)?.unbind()) } fn read_source<'py, T>( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/sql_test_loop.rs b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/sql_test_loop.rs index c37367dc7e..c330a2897d 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/sql_test_loop.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/sql_test_loop.rs @@ -534,9 +534,9 @@ impl<'py> TestScope<'_, 'py> { ))); } let enum_visibility: Bound<'py, PyDict> = - declarations.getattr(ENUM_VISIBILITY)?.downcast_into()?; + native_field(declarations, ENUM_VISIBILITY)?.downcast_into()?; let constant_visibility: Bound<'py, PyDict> = - declarations.getattr(CONSTANT_VISIBILITY)?.downcast_into()?; + native_field(declarations, CONSTANT_VISIBILITY)?.downcast_into()?; for reference in scans.iter().flat_map(|scan| scan.references.iter()) { let visibility: &Bound<'py, PyDict> = match reference.kind { sqlbuild_sqltext::compiler::models::DeclarationReferenceKind::Enum => { @@ -561,9 +561,9 @@ impl<'py> TestScope<'_, 'py> { } } let macro_records: Bound<'py, PyDict> = - declarations.getattr(MACRO_RECORDS)?.downcast_into()?; + native_field(declarations, MACRO_RECORDS)?.downcast_into()?; let macro_visibility: Bound<'py, PyDict> = - declarations.getattr(MACRO_VISIBILITY)?.downcast_into()?; + native_field(declarations, MACRO_VISIBILITY)?.downcast_into()?; for name in top_level_macro_names(sql).map_err(|message| self.error(message))? { let Some(record) = macro_records.get_item(&name)? else { continue; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/values.rs b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/values.rs index d9e7695d33..90a9f140d2 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/values.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/values.rs @@ -8,6 +8,7 @@ use pyo3::types::{ use serde_json::{Map, Number, Value}; use crate::bindings::_helpers::boundary::panics::value_error; +use crate::bindings::_helpers::records::fields::field as native_field; const DATACLASS_FIELDS: &str = "__dataclass_fields__"; const INSTANCE_DICT: &str = "__dict__"; @@ -55,14 +56,14 @@ pub(crate) fn plain_value(value: &Bound<'_, PyAny>) -> PyResult { fn object_like_value(value: &Bound<'_, PyAny>) -> PyResult { let py = value.py(); if value.is_instance(&py.import(ENUM_MODULE)?.getattr(ENUM_TYPE)?)? { - return plain_value(&value.getattr(ENUM_VALUE)?); + return plain_value(&native_field(value, ENUM_VALUE)?); } if value.hasattr(DATACLASS_FIELDS)? && !value.is_instance_of::() { return dataclass_value(value); } let datetime = py.import(DATETIME_MODULE)?; - if value.is_instance(&datetime.getattr(DATE_TYPE)?)? - || value.is_instance(&datetime.getattr(TIME_TYPE)?)? + if value.is_instance(&native_field(&datetime, DATE_TYPE)?)? + || value.is_instance(&native_field(&datetime, TIME_TYPE)?)? { return Ok(Value::String( value.call_method0(ISOFORMAT)?.str()?.to_str()?.to_owned(), @@ -74,7 +75,7 @@ fn object_like_value(value: &Bound<'_, PyAny>) -> PyResult { /// A dataclass's public instance attributes, or its public fields when it has slots. fn dataclass_value(value: &Bound<'_, PyAny>) -> PyResult { let mut entries: Vec<(String, Value)> = Vec::new(); - if let Ok(attributes) = value.getattr(INSTANCE_DICT) { + if let Ok(attributes) = native_field(value, INSTANCE_DICT) { for (key, item) in attributes.downcast::()?.iter() { let name: String = key.str()?.to_str()?.to_owned(); if !name.starts_with(PRIVATE_PREFIX) { @@ -82,10 +83,10 @@ fn dataclass_value(value: &Bound<'_, PyAny>) -> PyResult { } } } else { - for key in value.getattr(DATACLASS_FIELDS)?.try_iter()? { + for key in native_field(value, DATACLASS_FIELDS)?.try_iter()? { let name: String = key?.str()?.to_str()?.to_owned(); if !name.starts_with(PRIVATE_PREFIX) { - let item = value.getattr(name.as_str())?; + let item = native_field(value, name.as_str())?; entries.push((name, plain_value(&item)?)); } } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/contract_command/command.rs b/crates/sqlbuild-python/src/bindings/_helpers/contract_command/command.rs index b6599f754c..b4de29494e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/contract_command/command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/contract_command/command.rs @@ -19,16 +19,22 @@ use sqlbuild_core::contract_command::models::{ }; use crate::bindings::_helpers::build_progress::transient_status::status_reporter; +use crate::bindings::_helpers::cli::progress_reporters::ConnectionProgressReporter; use crate::bindings::_helpers::command_runtime::invocation::resolve_adapter_connection_context; use crate::bindings::_helpers::compile_driver::discovery::discover_project_inputs; +use crate::bindings::_helpers::compile_driver::pipeline_entries::build_project_graph_from_compiled_project; use crate::bindings::_helpers::compile_driver::project::build_compiled_project; use crate::bindings::_helpers::compile_driver::python_calls::keywords; use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; use crate::bindings::_helpers::contract_command::write::{ContractWrite, write_contracts}; +use crate::bindings::_helpers::planner_selection::selectors::select_project_graph; +use crate::bindings::_helpers::planner_selection::unit_tests::reject_unit_test_selectors; use crate::bindings::_helpers::presentation_text::terminal::stdout_supports_color; +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::{ErrorId, NativeAttr, RecordId}; +use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::refactor::contract_compare::compare_contracts; const GENERATE_ACTION: &str = "generate"; @@ -79,8 +85,9 @@ fn select_resources<'py>( context: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = request.py(); - let analysis: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::CompileAnalysisSelection)?, + let analysis: Bound<'py, PyAny> = new_record_with( + py, + RecordId::CompileAnalysisSelection, &[("no_cache", PyBool::new(py, true).to_owned().into_any())], )?; let project: Bound<'py, PyAny> = build_compiled_project( @@ -100,18 +107,16 @@ fn select_resources<'py>( ], )?), )?; - let graph: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::BuildProjectGraphFromCompiledProject)?, - &[("project", project)], - )?; - let selected: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::SelectProjectGraph)?, - &[ - ("graph", graph.clone()), - ("select", native_field(request, "select")?), - ("exclude", native_field(request, "exclude")?), - ], + let graph: Bound<'py, PyAny> = build_project_graph_from_compiled_project( + py, + Some(keywords(py, &[("project", project)])?), )?; + let selected: Bound<'py, PyAny> = select_project_graph( + &graph.clone(), + &native_field(request, "select")?, + &native_field(request, "exclude")?, + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; let compiled: Bound<'py, PyAny> = native_field(&graph, "project")?; let models: Bound<'py, PyTuple> = selected_of(native_field(&compiled, "models")?, &selected)?; let sources: Bound<'py, PyTuple> = selected_of(native_field(&compiled, "sources")?, &selected)?; @@ -148,7 +153,7 @@ fn exception(py: Python<'_>, error: &PyErr) -> bool { fn connection_timing(reporter: &Bound<'_, PyAny>, method: &str, started: Instant) -> PyResult<()> { let py: Python<'_> = reporter.py(); call_with( - &reporter.getattr(method)?, + &native_field(reporter, method)?, &[ ("connection_count", PyInt::new(py, 1).into_any()), ( @@ -169,17 +174,22 @@ fn compare<'py>( ) -> PyResult> { let py: Python<'py> = request.py(); let adapter: Bound<'py, PyAny> = native_field(context, "adapter")?; - let reporter: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ConnectionProgressReporter)?, - &[ - ("adapter_name", native_field(context, "adapter_name")?), - ("stream", progress.stream.clone()), - ( - "use_color", - PyBool::new(py, progress.use_color).to_owned().into_any(), - ), - ], - )?; + let reporter: Bound<'py, PyAny> = ConnectionProgressReporter::new( + py, + Some(keywords( + py, + &[ + ("adapter_name", native_field(context, "adapter_name")?), + ("stream", progress.stream.clone()), + ( + "use_color", + PyBool::new(py, progress.use_color).to_owned().into_any(), + ), + ], + )?), + ) + .and_then(|value| pyo3::Bound::new(py, value)) + .map(pyo3::Bound::into_any)?; let connection_started: Instant = Instant::now(); reporter.call_method1("on_connection_start", (1,))?; let connection: Bound<'py, PyAny> = @@ -207,15 +217,17 @@ fn compare<'py>( }, ),), )?; - let evidence: Bound<'py, PyAny> = match call_with( - ®istry::native(py, NativeAttr::CompareContracts)?, + let evidence: Bound<'py, PyAny> = match compare_contracts(Some(&keywords( + py, &[ ("adapter", adapter.clone()), ("connection", connection.clone()), ("models", selection.models.clone().into_any()), ("sources", selection.sources.clone().into_any()), ], - ) { + )?)) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py)) + { Ok(evidence) => evidence, Err(error) => { if exception(py, &error) { @@ -307,7 +319,7 @@ fn finding_text(finding: Bound<'_, PyAny>) -> PyResult { fn resource_text(item: Bound<'_, PyAny>) -> PyResult { let mut parts: Vec = Vec::new(); for field in ["database", "schema", "relation"] { - let value: Bound<'_, PyAny> = item.getattr(field)?; + let value: Bound<'_, PyAny> = native_field(&item, field)?; if value.is_truthy()? { parts.push(value.str()?.to_string()); } @@ -355,7 +367,7 @@ fn progress_for<'py>(request: &Bound<'py, PyAny>) -> PyResult> { /// Compare or adopt selected contracts from one read-only target. #[pyfunction] #[pyo3(signature = (*, request))] -fn run_contract_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { +pub(crate) fn run_contract_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { let action: String = native_field(request, "action")?.str()?.to_string(); let generate_write: bool = action == GENERATE_ACTION && native_field(request, "write")?.is_truthy()?; @@ -376,13 +388,10 @@ fn run_contract_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult< py, Some(keywords(py, &[("project_dir", project_dir.clone())])?), )?; - call_with( - ®istry::native(py, NativeAttr::RejectUnitTestSelectors)?, - &[ - ("select", native_field(request, "select")?), - ("exclude", native_field(request, "exclude")?), - ("discovered_inputs", discovered.clone()), - ], + reject_unit_test_selectors( + &native_field(request, "select")?, + &native_field(request, "exclude")?, + &discovered.clone(), )?; let from_target: Bound<'_, PyAny> = native_field(request, "from_target")?; if !discovered @@ -405,8 +414,9 @@ fn run_contract_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult< let selection: Selection<'_> = select_resources(request, &discovered, &context)?; let progress: Progress<'_> = progress_for(request)?; let evidence: Bound<'_, PyAny> = compare(request, &context, &selection, &progress)?; - let mut result: Bound<'_, PyAny> = call_with( - ®istry::class(py, RecordId::ContractAdoptionResult)?, + let mut result: Bound<'_, PyAny> = new_record_with( + py, + RecordId::ContractAdoptionResult, &[("from_target", from_target), ("evidence", evidence)], )?; if generate_write { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/contract_command/write.rs b/crates/sqlbuild-python/src/bindings/_helpers/contract_command/write.rs index 5bc9f1632c..546dc0d42a 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/contract_command/write.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/contract_command/write.rs @@ -6,11 +6,16 @@ use pyo3::types::{PyDict, PyDictMethods, PyList, PyListMethods, PyTuple}; use crate::bindings::_helpers::compile_driver::discovery::discover_project_inputs; use crate::bindings::_helpers::compile_driver::project::build_compiled_project; +use crate::bindings::_helpers::compile_driver::python_calls::imported; use crate::bindings::_helpers::compile_driver::python_calls::keywords; -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; +use crate::bindings::_helpers::format_command::files::write_file_atomically; +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::ids::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::fields::replaced_keywords; +use crate::bindings::_helpers::records::ids::RecordId; +use crate::bindings::_helpers::refactor::contract_edits::edit_contract_model; +use crate::bindings::_helpers::refactor::contract_edits::edit_contract_source; +use crate::bindings::_helpers::refactor::contract_edits::remaining_contract_findings; const UTF8: &str = "utf-8"; @@ -33,8 +38,7 @@ pub(crate) fn write_contracts<'py>( let project: Bound<'py, PyAny> = native_field(graph, "project")?; let models: Bound<'py, PyDict> = by_name(&native_field(&project, "models")?)?; let sources: Bound<'py, PyDict> = by_name(&native_field(&project, "sources")?)?; - let model_type: Bound<'py, PyAny> = - registry::class(py, RecordId::CompiledResourceType)?.getattr("MODEL")?; + let model_type: Bound<'py, PyAny> = enum_member(py, RecordId::CompiledResourceType, "MODEL")?; let updates: Bound<'py, PyDict> = PyDict::new(py); let updated_evidence: Bound<'py, PyList> = PyList::empty(py); for evidence in native_field(result, "evidence")?.try_iter()? { @@ -49,14 +53,13 @@ pub(crate) fn write_contracts<'py>( let (path, edited): (Bound<'py, PyAny>, Bound<'py, PyAny>) = if native_field(&evidence, "resource_type")?.eq(&model_type)? { let model: Bound<'py, PyAny> = models.as_any().get_item(&name)?; - let edited: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::EditContractModel)?, - &[ - ("model", model.clone()), - ("physical_columns", physical_columns), - ("overwrite", write.overwrite.clone()), - ], - )?; + let edited: Bound<'py, PyAny> = edit_contract_model( + py, + &model.clone(), + &physical_columns, + write.overwrite.clone().extract::()?, + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; ( write .project_dir @@ -67,29 +70,29 @@ pub(crate) fn write_contracts<'py>( let source: Bound<'py, PyAny> = sources.as_any().get_item(&name)?; let path: Bound<'py, PyAny> = native_field(&source, "source_file")?.getattr("file_path")?; - let edited: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::EditContractSource)?, - &[ - ("source", source), - ("physical_columns", physical_columns), - ("overwrite", write.overwrite.clone()), - ("contents", updates.as_any().call_method1("get", (&path,))?), - ], - )?; + let edited: Bound<'py, PyAny> = edit_contract_source( + &source, + &physical_columns, + write.overwrite.clone().extract::()?, + updates + .as_any() + .call_method1("get", (&path,))? + .extract::>()?, + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; (path, edited) }; let (contents, conflict): (Bound<'py, PyAny>, Bound<'py, PyAny>) = edited.extract()?; if !contents.is_none() && contents.ne(read_text(&path)?)? { updates.set_item(&path, contents)?; } - let remaining: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::RemainingContractFindings)?, - &[ - ("findings", findings), - ("overwrite", write.overwrite.clone()), - ("conflict", conflict), - ], - )?; + let remaining: Bound<'py, PyAny> = remaining_contract_findings( + py, + &findings, + write.overwrite.clone().extract::()?, + conflict.extract::>()?, + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; updated_evidence.append(replaced(&evidence, &[("findings", remaining)])?)?; } let evidence: Bound<'py, PyAny> = PyTuple::new(py, updated_evidence)?.into_any(); @@ -141,8 +144,12 @@ fn applied<'py>( } let selection_keywords: Bound<'py, PyDict> = PyDict::new(py); selection_keywords.set_item("no_cache", true)?; - let selection: Bound<'py, PyAny> = registry::class(py, RecordId::CompileAnalysisSelection)? - .call((), Some(&selection_keywords))?; + let selection: Bound<'py, PyAny> = new_record( + py, + RecordId::CompileAnalysisSelection, + (), + Some(&selection_keywords), + )?; build_compiled_project( py, Some(keywords( @@ -162,10 +169,8 @@ fn applied<'py>( /// Write each `path: contents` pair atomically, in order. fn write_all(contents_by_path: &Bound<'_, PyDict>) -> PyResult<()> { - let py: Python<'_> = contents_by_path.py(); - let write_atomically: Bound<'_, PyAny> = registry::native(py, NativeAttr::WriteAtomically)?; for (path, contents) in contents_by_path.iter() { - call_with(&write_atomically, &[("path", path), ("contents", contents)])?; + write_file_atomically(path.extract()?, &contents.extract::()?)?; } Ok(()) } @@ -196,5 +201,5 @@ fn replaced<'py>( for (name, field) in fields { keywords.set_item(*name, field)?; } - imported(py, "dataclasses", "replace")?.call((value,), Some(&keywords)) + replaced_keywords(value, &keywords) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/contracts/contract_diagnostics.rs b/crates/sqlbuild-python/src/bindings/_helpers/contracts/contract_diagnostics.rs index 0a8084a1e8..1a4a8ff37c 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/contracts/contract_diagnostics.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/contracts/contract_diagnostics.rs @@ -8,12 +8,13 @@ use pyo3::{IntoPyObject, pyfunction, wrap_pyfunction}; use sqlbuild_sqltext::compiler::main::model_output_columns::model_output_columns; use crate::bindings::_helpers::compiled_project::project::NativeCompiledProject; +use crate::bindings::_helpers::compiled_project::records::compiled_project_facts; use crate::bindings::_helpers::contracts::model_contracts::{ ConflictRow, DiagnosticRow, evaluate_project_model_contracts, project_promotion_conflicts, }; +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::ids::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::sqltext::model_headers::{ColumnLocation, location_rows}; const PATHLIB_MODULE: &str = "pathlib"; @@ -36,7 +37,7 @@ fn keyword_call<'py>( /// The compile's retained native project, read through `_native.compiled_project_facts`. fn native_project<'py>(project: &Bound<'py, PyAny>) -> PyResult> { - registry::native(project.py(), NativeAttr::CompiledProjectFacts)?.call1((project,)) + Ok(compiled_project_facts(project)?.into_any()) } /// Where the model's SQL produces `column_name`, recovered from the authored SQL when needed. @@ -65,7 +66,7 @@ fn output_column_location<'py>( }; let py: Python<'py> = model.py(); let location: Bound<'py, PyAny> = keyword_call( - ®istry::class(py, RecordId::SourceLocation)?, + &record_class(py, RecordId::SourceLocation)?, vec![ ("path", native_field(model, "relative_path")?), ("line", line.into_pyobject(py)?.into_any()), @@ -112,7 +113,7 @@ fn related_locations<'py>( return Ok(PyTuple::empty(py)); }; let related: Bound<'py, PyAny> = keyword_call( - ®istry::class(py, RecordId::RelatedLocation)?, + &record_class(py, RecordId::RelatedLocation)?, vec![ ("label", OUTPUT_LABEL.into_pyobject(py)?.into_any()), ("location", location), @@ -130,7 +131,7 @@ fn contract_diagnostic<'py>( let (code, is_error, message, column_name, declared_index, output_column, related, help) = row; let severity: &str = if is_error { "error" } else { "warning" }; keyword_call( - ®istry::class(py, RecordId::CompilerDiagnostic)?, + &record_class(py, RecordId::CompilerDiagnostic)?, vec![ ("phase", CONTRACT_PHASE.into_pyobject(py)?.into_any()), ("severity", severity.into_pyobject(py)?.into_any()), @@ -154,7 +155,7 @@ fn contract_diagnostic<'py>( } fn model_resource_type(py: Python<'_>) -> PyResult> { - registry::class(py, RecordId::CompiledResourceType)?.call1((MODEL_RESOURCE,)) + enum_from_value(py, RecordId::CompiledResourceType, MODEL_RESOURCE) } /// Every model's column contract diagnostics, in model order. @@ -195,7 +196,7 @@ pub(crate) fn evaluate_model_contracts<'py>( } } keyword_call( - ®istry::class(py, RecordId::ContractValidationResult)?, + &record_class(py, RecordId::ContractValidationResult)?, vec![("diagnostics", PyTuple::new(py, diagnostics)?.into_any())], ) } @@ -224,7 +225,7 @@ pub(crate) fn promotion_conflict_diagnostics<'py>( ), &native, )?; - let diagnostic_type: Bound<'py, PyAny> = registry::class(py, RecordId::CompilerDiagnostic)?; + let diagnostic_type: Bound<'py, PyAny> = record_class(py, RecordId::CompilerDiagnostic)?; let path_type: Bound<'py, PyAny> = py.import(PATHLIB_MODULE)?.getattr("Path")?; let mut diagnostics: Vec> = Vec::with_capacity(conflicts.len()); for (name, relative_path, code, message, help) in conflicts { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/contracts/effective_config.rs b/crates/sqlbuild-python/src/bindings/_helpers/contracts/effective_config.rs index 72fff61518..68efb2f2f2 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/contracts/effective_config.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/contracts/effective_config.rs @@ -119,7 +119,7 @@ fn field_owner<'py>( field: &TargetField, ) -> PyResult> { match field.group { - Some(group) => target.getattr(group), + Some(group) => native_field(target, group), None => Ok(target.clone()), } } @@ -128,7 +128,7 @@ fn field_value<'py>( target: &Bound<'py, PyAny>, field: &TargetField, ) -> PyResult, Value<'py>>> { - let value: Bound<'py, PyAny> = field_owner(target, field)?.getattr(field.name)?; + let value: Bound<'py, PyAny> = native_field(&field_owner(target, field)?, field.name)?; match (field.rule, value.is_none()) { (MergeRule::Overlay { .. }, true) => Ok(FieldValue::Scalar(None)), (MergeRule::Overlay { .. }, false) => Ok(FieldValue::Scalar(Some(value))), @@ -255,7 +255,7 @@ pub(crate) fn effective_config_target_fields<'py>( /// `build_effective_vars` before template expansion. #[pyfunction] -fn effective_config_vars<'py>( +pub(crate) fn effective_config_vars<'py>( py: Python<'py>, configs: (Bound<'py, PyAny>, Bound<'py, PyAny>), target_config: &Bound<'py, PyAny>, @@ -283,7 +283,7 @@ fn named<'py>(mapping: &Bound<'py, PyAny>) -> PyResult, Pairs<' /// `build_effective_connection` before template expansion, or the unknown-connection message. #[pyfunction] -fn effective_config_connection<'py>( +pub(crate) fn effective_config_connection<'py>( py: Python<'py>, project_config: &Bound<'py, PyAny>, local_config: &Bound<'py, PyAny>, @@ -312,7 +312,7 @@ fn effective_config_connection<'py>( /// `build_effective_settings`: setting values by field name, local overrides applied. #[pyfunction] -fn effective_config_settings<'py>( +pub(crate) fn effective_config_settings<'py>( py: Python<'py>, settings: (Bound<'py, PyAny>, Bound<'py, PyAny>), overrides: &Bound<'py, PyAny>, @@ -321,7 +321,12 @@ fn effective_config_settings<'py>( let read = |settings: &Bound<'py, PyAny>| -> PyResult> { names .iter() - .map(|name| Ok((key(name.as_any())?, settings.getattr(name)?))) + .map(|name| { + Ok(( + key(name.as_any())?, + native_field(settings, &name.to_cow()?)?, + )) + }) .collect() }; let overridden: Vec> = overrides diff --git a/crates/sqlbuild-python/src/bindings/_helpers/contracts/project_config.rs b/crates/sqlbuild-python/src/bindings/_helpers/contracts/project_config.rs index 9d5236873c..c370bfe172 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/contracts/project_config.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/contracts/project_config.rs @@ -16,6 +16,7 @@ use sqlbuild_config::project::main::resolve_config_file::resolve_config_file; use sqlbuild_config::project::models::{Built, ConfigFile, ConfigPaths}; use crate::bindings::_helpers::boundary::config_values::config_value_to_python; +use crate::bindings::_helpers::records::construct::{new_record, record_class}; use crate::bindings::_helpers::records::ids::ErrorId; use crate::bindings::_helpers::records::registry; use sqlbuild_core::constants::PROJECT_CONFIG_FILENAME; @@ -115,7 +116,7 @@ fn enum_member<'py>(py: Python<'py>, class: &str, value: &str) -> PyResult = match PYTHON_ENUM_MODULES.iter().find(|(name, _)| *name == class) { Some((_, module)) => py.import(*module)?.getattr(class)?, - None => registry::class(py, registry::record_named(class)?)?, + None => record_class(py, registry::record_named(class)?)?, }; class_object.call1((value,)) } @@ -129,7 +130,7 @@ fn object<'py>( for (name, value) in fields { keywords.set_item(name, built(py, value)?)?; } - registry::class(py, registry::record_named(class)?)?.call((), Some(&keywords)) + new_record(py, registry::record_named(class)?, (), Some(&keywords)) } fn built(py: Python<'_>, value: Built) -> PyResult> { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/contracts/spec_resolution.rs b/crates/sqlbuild-python/src/bindings/_helpers/contracts/spec_resolution.rs index a262bdf308..46f14fc301 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/contracts/spec_resolution.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/contracts/spec_resolution.rs @@ -12,10 +12,10 @@ use pyo3::{pyfunction, wrap_pyfunction}; use sqlbuild_model_config::name_patterns::main::compile_name_pattern::compile_name_pattern; use sqlbuild_model_config::name_patterns::models::NamePattern; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; use crate::bindings::_helpers::contracts::effective_config::{ effective_config_target_fields, effective_config_target_name, }; +use crate::bindings::_helpers::records::construct::{new_record, new_record_with}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -73,12 +73,11 @@ pub(crate) fn resolve_target_config<'py>( local_config: &Bound<'py, PyAny>, target_name: &Bound<'py, PyAny>, ) -> PyResult> { - let target_class: Bound<'py, PyAny> = registry::class(py, RecordId::TargetConfig)?; let (rows, error) = effective_config_target_fields( py, (project_config.clone(), local_config.clone()), target_name, - &target_class.call0()?, + &new_record(py, RecordId::TargetConfig, (), None)?, )?; let Some(rows) = rows else { let message: Bound<'py, PyString> = error.unwrap_or_else(|| PyString::new(py, "")); @@ -100,10 +99,10 @@ pub(crate) fn resolve_target_config<'py>( } } for ((group, class_name), (_, fields)) in TARGET_GROUPS.iter().zip(&grouped) { - let nested: Bound<'py, PyAny> = registry::class(py, *class_name)?.call((), Some(fields))?; + let nested: Bound<'py, PyAny> = new_record(py, *class_name, (), Some(fields))?; values.set_item(*group, nested)?; } - target_class.call((), Some(&values)) + new_record(py, RecordId::TargetConfig, (), Some(&values)) } /// Resolve the effective adapter name, allowing local override. @@ -177,14 +176,15 @@ pub(crate) fn resolve_effective_scenario_config<'py>( Ok(( *field, override_or( - local_limits.getattr(*field)?, - project_limits.getattr(*field)?, + native_field(&local_limits, field)?, + native_field(&project_limits, field)?, ), )) }) .collect::>()?; - call_with( - ®istry::class(py, RecordId::ScenarioConfig)?, + new_record_with( + py, + RecordId::ScenarioConfig, &[ ( "run_namespace", @@ -196,10 +196,7 @@ pub(crate) fn resolve_effective_scenario_config<'py>( ("local_type_overrides", overrides.into_any()), ( "snapshot_limits", - call_with( - ®istry::class(py, RecordId::ScenarioSnapshotLimitsConfig)?, - &limits, - )?, + new_record_with(py, RecordId::ScenarioSnapshotLimitsConfig, &limits)?, ), ], ) @@ -237,7 +234,7 @@ fn blank(text: &str) -> bool { /// Parse a one-, two-, or three-part loader destination. #[pyfunction] #[pyo3(signature = (*, destination, default_database, default_schema))] -fn loader_destination_parts<'py>( +pub(crate) fn loader_destination_parts<'py>( py: Python<'py>, destination: &str, default_database: Option>, @@ -282,7 +279,12 @@ pub(crate) fn loader_parts<'py>( ), _ => (text(parts[0]), text(parts[1]), text(parts[2])), }; - registry::class(py, RecordId::LoaderDestinationParts)?.call1((database, schema, table)) + new_record( + py, + RecordId::LoaderDestinationParts, + (database, schema, table), + None, + ) } /// Whether a family's optional `name_pattern` accepts `column_name`. diff --git a/crates/sqlbuild-python/src/bindings/_helpers/contracts/sql_values.rs b/crates/sqlbuild-python/src/bindings/_helpers/contracts/sql_values.rs index e424f1c74e..dc47daa3e4 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/contracts/sql_values.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/contracts/sql_values.rs @@ -1,5 +1,6 @@ //! Typed SQL value normalization between Python authored values and native `SqlValue`s. +use crate::bindings::_helpers::records::construct::{new_record, record_class}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; @@ -30,11 +31,11 @@ impl<'py> SqlValueClasses<'py> { pub(crate) fn load(py: Python<'py>) -> PyResult { Ok(Self { decimal: py.import("decimal")?.getattr("Decimal")?, - authored_set: registry::class(py, RecordId::AuthoredSqlSet)?, - value_call: registry::class(py, RecordId::AuthoredSqlValueCall)?, - sql_value: registry::class(py, RecordId::SqlValue)?, - logical_type: registry::class(py, RecordId::SqlLogicalType)?, - kind: registry::class(py, RecordId::SqlValueKind)?, + authored_set: record_class(py, RecordId::AuthoredSqlSet)?, + value_call: record_class(py, RecordId::AuthoredSqlValueCall)?, + sql_value: record_class(py, RecordId::SqlValue)?, + logical_type: record_class(py, RecordId::SqlLogicalType)?, + kind: record_class(py, RecordId::SqlValueKind)?, }) } } @@ -199,7 +200,7 @@ fn normalize_sql_value<'py>( let raw = self::raw_value(raw_value, &classes)?; let applied: Bound<'py, PyAny> = match limits { Some(limits) if !limits.is_none() => limits.clone(), - _ => registry::class(py, RecordId::SqlValueLimits)?.call0()?, + _ => new_record(py, RecordId::SqlValueLimits, (), None)?, }; let limits = SqlValueLimits { max_depth: native_field(&applied, "max_depth")?.extract()?, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cost_command/bounds.rs b/crates/sqlbuild-python/src/bindings/_helpers/cost_command/bounds.rs index 86b1ce0cac..1ce23b0463 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cost_command/bounds.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cost_command/bounds.rs @@ -12,9 +12,9 @@ use sqlbuild_core::cost_command::main::is_zero_cost_duration::is_zero_cost_durat use crate::bindings::_helpers::compile_driver::python_calls::imported; use crate::bindings::_helpers::cost_command::refusal::cost_error; +use crate::bindings::_helpers::records::construct::record_class; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; const DATETIME_MODULE: &str = "datetime"; const MINUTE_TO_DAY_DURATION_UNITS: [&str; 3] = ["m", "h", "d"]; @@ -103,7 +103,7 @@ pub(crate) fn parse_since<'py>(py: Python<'py>, value: &str) -> PyResult = - registry::class(py, RecordId::Duration)?.call_method1("parse", (value,))?; + record_class(py, RecordId::Duration)?.call_method1("parse", (value,))?; if duration.is_none() { return parse_bound(py, value, "--since", false); } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cost_command/command.rs b/crates/sqlbuild-python/src/bindings/_helpers/cost_command/command.rs index f2955d695b..e73c488da8 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cost_command/command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cost_command/command.rs @@ -12,15 +12,20 @@ use sqlbuild_core::cost_command::main::cost_history_limit::cost_history_limit; use sqlbuild_core::cost_command::main::select_cost_run::select_cost_run; use sqlbuild_core::cost_command::main::validate_cost_request::validate_cost_request; -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; +use crate::bindings::_helpers::compile_driver::python_calls::imported; use crate::bindings::_helpers::cost_command::bounds::{parse_bound, parse_since}; use crate::bindings::_helpers::cost_command::refresh::refresh_pending_cost_run; use crate::bindings::_helpers::cost_command::refusal::cost_error; use crate::bindings::_helpers::cost_command::sorting::{sort_history, sort_resources}; +use crate::bindings::_helpers::cost_report::functions::format_cost_breakdown; +use crate::bindings::_helpers::cost_report::functions::format_cost_history; +use crate::bindings::_helpers::cost_report::functions::format_cost_history_json; +use crate::bindings::_helpers::cost_report::functions::format_cost_json; +use crate::bindings::_helpers::cost_report::functions::list_cost_runs; +use crate::bindings::_helpers::cost_report::functions::resolve_cost_run; use crate::bindings::_helpers::presentation_text::terminal::stdout_supports_color; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::NativeAttr; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::fields::replaced_keywords; /// The request fields every cost view reads. struct CostRequest<'py> { @@ -102,9 +107,12 @@ fn refused(py: Python<'_>, result: Result<(), CostCommandError>) -> PyResult<()> fn cost_runs<'py>(request: &CostRequest<'py>) -> PyResult>> { let py: Python<'py> = request.request.py(); - call_with( - ®istry::native(py, NativeAttr::ListCostRuns)?, - &[("project_dir", request.project_dir.clone())], + list_cost_runs( + py, + request + .project_dir + .clone() + .extract::()?, )? .try_iter()? .collect() @@ -177,26 +185,20 @@ fn run_history(request: &CostRequest<'_>) -> PyResult { } let records: Bound<'_, PyAny> = PyTuple::new(py, records)?.into_any(); let count: Bound<'_, PyAny> = PyInt::new(py, matching_count).into_any(); - let payload: String = call_with( - ®istry::native(py, NativeAttr::FormatCostHistoryJson)?, - &[ - ("records", records.clone()), - ("matching_count", count.clone()), - ], - )? - .extract()?; + let payload: String = + format_cost_history_json(&records.clone(), count.clone().extract::>()?) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))? + .extract()?; request.emit(&payload, || { - call_with( - ®istry::native(py, NativeAttr::FormatCostHistory)?, - &[ - ("records", records), - ("matching_count", count), - ( - "use_color", - PyBool::new(py, request.use_color()?).to_owned().into_any(), - ), - ], - )? + format_cost_history( + &records, + count.extract::()?, + PyBool::new(py, request.use_color()?) + .to_owned() + .into_any() + .extract::()?, + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))? .extract() })?; Ok(0) @@ -207,13 +209,17 @@ fn resolve_run<'py>(request: &CostRequest<'py>) -> PyResult> { let py: Python<'py> = request.request.py(); let records: Vec> = cost_runs(request)?; if request.selector == LATEST_SELECTOR { - let latest: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ResolveCostRun)?, - &[ - ("project_dir", request.project_dir.clone()), - ("selector", PyString::new(py, &request.selector).into_any()), - ], - )?; + let latest: Bound<'py, PyAny> = resolve_cost_run( + py, + request + .project_dir + .clone() + .extract::()?, + &PyString::new(py, &request.selector) + .into_any() + .extract::()?, + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; if latest.is_none() { return Err(cost_error( py, @@ -237,14 +243,12 @@ fn with_resources<'py>( resources: &[Bound<'py, PyAny>], ) -> PyResult> { let py: Python<'py> = record.py(); - let replace: Bound<'py, PyAny> = imported(py, "dataclasses", "replace")?; let cost_changes: Bound<'py, PyDict> = PyDict::new(py); cost_changes.set_item("resources", PyTuple::new(py, resources)?)?; - let cost: Bound<'py, PyAny> = - replace.call((native_field(record, "cost")?,), Some(&cost_changes))?; + let cost: Bound<'py, PyAny> = replaced_keywords(&native_field(record, "cost")?, &cost_changes)?; let changes: Bound<'py, PyDict> = PyDict::new(py); changes.set_item("cost", cost)?; - replace.call((record,), Some(&changes)) + replaced_keywords(record, &changes) } fn run_detail(request: &CostRequest<'_>) -> PyResult { @@ -280,16 +284,16 @@ fn run_detail(request: &CostRequest<'_>) -> PyResult { } None => record.clone(), }; - let payload: String = registry::native(py, NativeAttr::FormatCostJson)? - .call1((json_record,))? + let payload: String = format_cost_json(&json_record) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))? .extract()?; request.emit(&payload, || { let options: Bound<'_, PyDict> = PyDict::new(py); options.set_item("record", &record)?; options.set_item("use_color", request.use_color()?)?; options.set_item("limit", limit)?; - registry::native(py, NativeAttr::FormatCostBreakdown)? - .call((), Some(&options))? + format_cost_breakdown(py, Some(options.clone())) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))? .extract() })?; Ok(0) @@ -306,7 +310,7 @@ fn report(request: &Bound<'_, PyAny>) -> PyResult { /// Refresh a delayed run, then render persisted cost history or one run breakdown. #[pyfunction] -fn run_cost_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { +pub(crate) fn run_cost_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { refresh_pending_cost_run(py, request); report(request) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cost_command/refresh.rs b/crates/sqlbuild-python/src/bindings/_helpers/cost_command/refresh.rs index 6911b73225..fade4b27b9 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cost_command/refresh.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cost_command/refresh.rs @@ -10,15 +10,20 @@ use sqlbuild_core::cost_command::main::accepts_cost_refresh::accepts_cost_refres use sqlbuild_core::cost_command::models::CostCoverage; use crate::bindings::_helpers::command_runtime::adapters::resolve_cli_adapter; +use crate::bindings::_helpers::command_runtime::connection::resolve_project_connection_config; +use crate::bindings::_helpers::compile_attachment::effective_config::build_effective_target_config; use crate::bindings::_helpers::compile_driver::discovery::discover_project_inputs; use crate::bindings::_helpers::compile_driver::python_calls::keywords; use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; +use crate::bindings::_helpers::contracts::spec_resolution::resolve_effective_adapter_name; +use crate::bindings::_helpers::cost_report::functions::read_cost_run; +use crate::bindings::_helpers::cost_report::functions::resolve_cost_run; +use crate::bindings::_helpers::cost_report::functions::write_cost_run; use crate::bindings::_helpers::cost_telemetry::values::cost_aware; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::NativeAttr; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::fields::replaced_keywords; fn coverage(summary: &Bound<'_, PyAny>) -> PyResult { Ok(CostCoverage { @@ -43,24 +48,22 @@ fn refresh_record<'py>( ) -> PyResult> { let py: Python<'py> = project_dir.py(); let resolve = || { - call_with( - ®istry::native(py, NativeAttr::ResolveCostRun)?, - &[ - ("project_dir", project_dir.clone()), - ("selector", selector.clone()), - ], + resolve_cost_run( + py, + project_dir.clone().extract::()?, + &selector.clone().extract::()?, ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py)) }; if selector.extract::()? == LATEST_SELECTOR { return resolve(); } - let exact: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ReadCostRun)?, - &[ - ("project_dir", project_dir.clone()), - ("run_id", selector.clone()), - ], - )?; + let exact: Bound<'py, PyAny> = read_cost_run( + py, + project_dir.clone().extract::()?, + &selector.clone().extract::()?, + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; if !exact.is_none() { return Ok(exact); } @@ -113,22 +116,13 @@ fn refresh(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult<()> { py, Some(keywords(py, &[("project_dir", project_dir.clone())])?), )?; - let target_config: Bound<'_, PyAny> = call_with( - ®istry::native(py, NativeAttr::BuildEffectiveTargetConfig)?, - &[ - ("discovered_inputs", discovered.clone()), - ("selected_target", target_name.clone()), - ], + let target_config: Bound<'_, PyAny> = build_effective_target_config( + &discovered.clone(), + Some(target_name.clone()).filter(|value| !value.is_none()), )?; - let adapter_name: Bound<'_, PyAny> = call_with( - ®istry::native(py, NativeAttr::ResolveEffectiveAdapterName)?, - &[ - ( - "project_config", - native_field(&discovered, "project_config")?, - ), - ("local_config", native_field(&discovered, "local_config")?), - ], + let adapter_name: Bound<'_, PyAny> = resolve_effective_adapter_name( + &native_field(&discovered, "project_config")?, + &native_field(&discovered, "local_config")?, )?; let adapter: Bound<'_, PyAny> = resolve_cli_adapter(&adapter_name, &project_dir)?; if !cost_aware(&adapter)? { @@ -142,13 +136,16 @@ fn refresh(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult<()> { { return Ok(()); } - let connection_config: Bound<'_, PyAny> = call_with( - ®istry::native(py, NativeAttr::ResolveProjectConnectionConfig)?, - &[ - ("discovered_inputs", discovered), - ("project_dir", project_dir.clone()), - ("selected_target", target_name), - ], + let connection_config: Bound<'_, PyAny> = resolve_project_connection_config( + py, + Some(keywords( + py, + &[ + ("discovered_inputs", discovered), + ("project_dir", project_dir.clone()), + ("selected_target", target_name), + ], + )?), )?; let run_id: Bound<'_, PyAny> = native_field(&record, "run_id")?; let ledger: Bound<'_, PyAny> = project_dir @@ -177,12 +174,9 @@ fn refresh(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult<()> { } let changes: Bound<'_, PyDict> = PyDict::new(py); changes.set_item("cost", summary)?; - let refreshed: Bound<'_, PyAny> = - imported(py, "dataclasses", "replace")?.call((record,), Some(&changes))?; - call_with( - ®istry::native(py, NativeAttr::WriteCostRun)?, - &[("project_dir", project_dir), ("record", refreshed)], - )?; + let refreshed: Bound<'_, PyAny> = replaced_keywords(&record, &changes)?; + write_cost_run(py, project_dir.extract::()?, &refreshed) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; Ok(()) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cost_command/sorting.rs b/crates/sqlbuild-python/src/bindings/_helpers/cost_command/sorting.rs index 0727f3360e..91bb50c231 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cost_command/sorting.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cost_command/sorting.rs @@ -33,7 +33,7 @@ fn sorted_with<'py>( fn attribute(item: &Bound<'_, PyAny>, path: &str) -> PyResult> { let mut value: Bound<'_, PyAny> = item.clone(); for name in path.split('.') { - value = value.getattr(name)?; + value = native_field(&value, name)?; } Ok(value.unbind()) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cost_report/degradation.rs b/crates/sqlbuild-python/src/bindings/_helpers/cost_report/degradation.rs index cd5c5e490b..754cfae34f 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cost_report/degradation.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cost_report/degradation.rs @@ -17,9 +17,9 @@ use crate::bindings::_helpers::cost_report::store; use crate::bindings::_helpers::cost_telemetry::values::cost_aware; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; +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; -use crate::bindings::_helpers::records::registry; const LOGGER_NAME: &str = "sqlbuild.cli.commands._helpers.cost.collection"; const COLLECTION_LOGGER_NAME: &str = "sqlbuild.cost"; @@ -157,14 +157,14 @@ fn cost_record<'py>( "started_at", "completed_at", ] { - fields.push((name, finalization.getattr(name)?)); + fields.push((name, native_field(finalization, name)?)); } fields.push(("cost", summary)); fields.push(( "had_executable_work", native_field(finalization, "had_executable_work")?, )); - call_with(®istry::class(py, RecordId::CostRunRecord)?, &fields) + new_record_with(py, RecordId::CostRunRecord, &fields) } fn summary<'py>( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cost_report/functions.rs b/crates/sqlbuild-python/src/bindings/_helpers/cost_report/functions.rs index 3e818f3b23..f1288d7a3e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cost_report/functions.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cost_report/functions.rs @@ -36,7 +36,7 @@ fn optional_record<'py>( /// Atomically persist one run's cost artifact and return its path. #[pyfunction] #[pyo3(signature = (*, project_dir, record))] -fn write_cost_run( +pub(crate) fn write_cost_run( py: Python<'_>, project_dir: PathBuf, record: &Bound<'_, PyAny>, @@ -47,7 +47,7 @@ fn write_cost_run( /// Read the run stored under an exact run ID, or `None`. #[pyfunction] #[pyo3(signature = (*, project_dir, run_id))] -fn read_cost_run<'py>( +pub(crate) fn read_cost_run<'py>( py: Python<'py>, project_dir: PathBuf, run_id: &str, @@ -59,14 +59,17 @@ fn read_cost_run<'py>( /// Every readable persisted run, newest first. #[pyfunction] #[pyo3(signature = (*, project_dir))] -fn list_cost_runs<'py>(py: Python<'py>, project_dir: PathBuf) -> PyResult> { +pub(crate) fn list_cost_runs<'py>( + py: Python<'py>, + project_dir: PathBuf, +) -> PyResult> { Ok(record_tuple(py, &store::list(py, &project_dir)?)?.into_any()) } /// The latest finished run, or the only run whose ID starts with the selector. #[pyfunction] #[pyo3(signature = (*, project_dir, selector))] -fn resolve_cost_run<'py>( +pub(crate) fn resolve_cost_run<'py>( py: Python<'py>, project_dir: PathBuf, selector: &str, @@ -77,14 +80,14 @@ fn resolve_cost_run<'py>( /// `sqb cost --json` text of one run. #[pyfunction] -fn format_cost_json(record: &Bound<'_, PyAny>) -> PyResult { +pub(crate) fn format_cost_json(record: &Bound<'_, PyAny>) -> PyResult { Ok(cost_report_json(&cost_run(record)?)) } /// `sqb cost history --json` text of the shown runs. #[pyfunction] #[pyo3(signature = (*, records, matching_count = None))] -fn format_cost_history_json( +pub(crate) fn format_cost_history_json( records: &Bound<'_, PyAny>, matching_count: Option, ) -> PyResult { @@ -94,14 +97,20 @@ fn format_cost_history_json( /// The `sqb cost --json` document of one run, as a dictionary. #[pyfunction] #[pyo3(signature = (*, record))] -fn cost_output_payload(py: Python<'_>, record: &Bound<'_, PyAny>) -> PyResult> { +pub(crate) fn cost_output_payload( + py: Python<'_>, + record: &Bound<'_, PyAny>, +) -> PyResult> { json_to_python(py, output_payload(&cost_run(record)?), false) } /// The aligned human breakdown of one run. #[pyfunction] #[pyo3(signature = (**options))] -fn format_cost_breakdown(py: Python<'_>, options: Option>) -> PyResult { +pub(crate) fn format_cost_breakdown( + py: Python<'_>, + options: Option>, +) -> PyResult { let arguments: KeywordArguments<'_> = KeywordArguments::read(py, "format_cost_breakdown", options, BREAKDOWN_OPTIONS)?; let limit: Bound<'_, PyAny> = arguments.value("limit")?; @@ -134,7 +143,7 @@ fn format_cost_breakdown(py: Python<'_>, options: Option>) -> /// The aligned human history of the shown runs. #[pyfunction] #[pyo3(signature = (*, records, matching_count, use_color))] -fn format_cost_history( +pub(crate) fn format_cost_history( records: &Bound<'_, PyAny>, matching_count: usize, use_color: bool, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cost_report/records.rs b/crates/sqlbuild-python/src/bindings/_helpers/cost_report/records.rs index f1358d228e..407315978e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cost_report/records.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cost_report/records.rs @@ -8,6 +8,7 @@ use sqlbuild_core::cost_report::models::{CostResource, CostRun, CostSummary}; use crate::bindings::_helpers::compile_driver::python_calls::imported; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; +use crate::bindings::_helpers::records::construct::new_record; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; @@ -23,24 +24,26 @@ pub(crate) fn artifact_error(py: Python<'_>, error: &CostArtifactError) -> PyErr } fn text(value: &Bound<'_, PyAny>, field: &str) -> PyResult { - value.getattr(field)?.extract() + native_field(value, field)?.extract() } fn optional_text(value: &Bound<'_, PyAny>, field: &str) -> PyResult> { - value.getattr(field)?.extract() + native_field(value, field)?.extract() } /// `str(value.)`, the text a `Decimal` field is stored as. fn decimal(value: &Bound<'_, PyAny>, field: &str) -> PyResult { - Ok(value.getattr(field)?.str()?.to_string()) + Ok(native_field(value, field)?.str()?.to_string()) } fn integer(value: &Bound<'_, PyAny>, field: &str) -> PyResult { - value.getattr(field)?.extract() + native_field(value, field)?.extract() } fn timestamp(value: &Bound<'_, PyAny>, field: &str) -> PyResult { - value.getattr(field)?.call_method0("isoformat")?.extract() + native_field(value, field)? + .call_method0("isoformat")? + .extract() } fn resource(value: &Bound<'_, PyAny>) -> PyResult { @@ -155,7 +158,7 @@ fn resource_object<'py>( builders.decimal(&resource.estimated_compute_credits)?, )?; fields.set_item("estimated_usd", builders.decimal(&resource.estimated_usd)?)?; - registry::class(py, RecordId::ResourceCost)?.call((), Some(&fields)) + new_record(py, RecordId::ResourceCost, (), Some(&fields)) } fn summary_object<'py>( @@ -216,7 +219,7 @@ pub(crate) fn record_object<'py>(py: Python<'py>, run: &CostRun) -> PyResult, run_id: &str) -> PyResult { - let text = |name: &str| -> PyResult> { observation.getattr(name)?.extract() }; + let text = + |name: &str| -> PyResult> { native_field(observation, name)?.extract() }; Ok(CostQuery { query_id: native_field(observation, "query_id")?.extract()?, warehouse: WarehouseKey { @@ -64,8 +65,9 @@ fn resource_cost<'py>( resource: &ResourceAllocation, ) -> PyResult> { let decimal = |value: &PyDecimal| python_decimal(py, value); - call_with( - ®istry::class(py, RecordId::ResourceCost)?, + new_record_with( + py, + RecordId::ResourceCost, &[ ( "resource_type", diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cost_telemetry/context.rs b/crates/sqlbuild-python/src/bindings/_helpers/cost_telemetry/context.rs index 42aede93ad..0d80fbf9af 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cost_telemetry/context.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cost_telemetry/context.rs @@ -69,7 +69,7 @@ fn or_current<'py>( if !value.is_none() || current.is_none() { return Ok(value); } - current.getattr(field) + native_field(current, field) } fn scope_of(context: Bound<'_, PyAny>) -> CostScope { @@ -92,7 +92,7 @@ impl CostContext { #[staticmethod] #[pyo3(signature = (**options))] - fn scope(py: Python<'_>, options: Option>) -> PyResult { + pub(crate) fn scope(py: Python<'_>, options: Option>) -> PyResult { let arguments: KeywordArguments<'_> = KeywordArguments::read( py, "scope", diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cost_telemetry/ledger.rs b/crates/sqlbuild-python/src/bindings/_helpers/cost_telemetry/ledger.rs index 45d4cd2a1a..ccbd1af6bc 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cost_telemetry/ledger.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cost_telemetry/ledger.rs @@ -15,12 +15,11 @@ use sqlbuild_cache::digest::main::sha256_hex::sha256_hex; use sqlbuild_core::json::main::dumps::dumps; use sqlbuild_core::json::models::{JsonDialect, JsonInteger, JsonValue, StdlibJsonOptions}; -use crate::bindings::_helpers::compile_driver::python_calls::{ - KeywordArguments, call_with, imported, within, -}; +use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, imported, within}; use crate::bindings::_helpers::cost_telemetry::health::mark_ledger_failure; use crate::bindings::_helpers::cost_telemetry::values::process_object; use crate::bindings::_helpers::cursor::values::DATETIME_MODULE; +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::{NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; @@ -200,10 +199,7 @@ fn ledger_entry<'py>(line: &Bound<'py, PyAny>) -> PyResult> { ("started_at", parsed("started_at")?), ("completed_at", parsed("completed_at")?), ]; - call_with( - ®istry::class(py, RecordId::StatementLedgerEntry)?, - &entry_fields, - ) + new_record_with(py, RecordId::StatementLedgerEntry, &entry_fields) } fn entry_error(py: Python<'_>, error: &PyErr) -> bool { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cost_telemetry/snowflake.rs b/crates/sqlbuild-python/src/bindings/_helpers/cost_telemetry/snowflake.rs index c08efd5777..405562e494 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cost_telemetry/snowflake.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cost_telemetry/snowflake.rs @@ -13,18 +13,17 @@ use sqlbuild_core::cost_allocation::constants::QUERY_HISTORY_RESULT_LIMIT; use sqlbuild_core::cost_allocation::main::query_history_sql::query_history_sql; use sqlbuild_core::cost_allocation::models::{QueryHistoryWindow, RunAllocation}; -use crate::bindings::_helpers::compile_driver::python_calls::{ - KeywordArguments, call_with, imported, -}; +use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, imported}; use crate::bindings::_helpers::cost_telemetry::allocation::{ SummaryState, allocate_observations, priced, summary_object, }; use crate::bindings::_helpers::cost_telemetry::health::consume_ledger_failure; use crate::bindings::_helpers::cost_telemetry::ledger::read_ledger; use crate::bindings::_helpers::cursor::values::DATETIME_MODULE; +use crate::bindings::_helpers::records::construct::new_record_with; use crate::bindings::_helpers::records::fields::field as native_field; +use crate::bindings::_helpers::records::fields::replaced_keywords; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; const DEADLINE_SECONDS: f64 = 10.0; const QUERY_TIMEOUT_SECONDS: &str = "5"; @@ -259,8 +258,9 @@ fn row_observation<'py>( return Ok(Err(NO_WAREHOUSE_COMPUTE)); } let tag: QueryTag = tag.unwrap_or_default(); - let observation: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::QueryCostObservation)?, + let observation: Bound<'py, PyAny> = new_record_with( + py, + RecordId::QueryCostObservation, &[ ("query_id", query_id.str()?.into_any()), ("warehouse_name", warehouse_name.str()?.into_any()), @@ -445,7 +445,7 @@ fn ledger_attributed<'py>( pyo3::types::PyDictMethods::get_item(ledger, native_field(observation, "query_id")?)?; let field = |name: &str| -> PyResult> { match &entry { - Some(entry) => entry.getattr(name), + Some(entry) => native_field(entry, name), None => Ok(py.None().into_bound(py)), } }; @@ -453,7 +453,7 @@ fn ledger_attributed<'py>( changes.set_item("run_id", field("run_id")?)?; changes.set_item("resource_type", field("resource_type")?)?; changes.set_item("resource_name", field("resource_name")?)?; - imported(py, "dataclasses", "replace")?.call((observation,), Some(&changes)) + replaced_keywords(observation, &changes) } /// Collection inputs read from the keyword arguments. diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cost_telemetry/values.rs b/crates/sqlbuild-python/src/bindings/_helpers/cost_telemetry/values.rs index b615bace40..ab41216edd 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cost_telemetry/values.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cost_telemetry/values.rs @@ -9,6 +9,7 @@ use sqlbuild_core::cost_allocation::models::PyDecimal; use crate::bindings::_helpers::compile_driver::python_calls::imported; use crate::bindings::_helpers::cursor::values::DATETIME_MODULE; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::state_rows::timestamps::offset_aware; const DECIMAL_MODULE: &str = "decimal"; @@ -18,7 +19,7 @@ const COST_AWARE_METHODS: [&str; 2] = ["cost_capability", "collect_run_cost"]; /// Whether `adapter` implements the cost telemetry methods. pub(crate) fn cost_aware(adapter: &Bound<'_, PyAny>) -> PyResult { for name in COST_AWARE_METHODS { - if !adapter.hasattr(name)? || adapter.getattr(name)?.is_none() { + if !adapter.hasattr(name)? || native_field(adapter, name)?.is_none() { return Ok(false); } } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cursor/conversion.rs b/crates/sqlbuild-python/src/bindings/_helpers/cursor/conversion.rs index 51349afb9f..a67df9d222 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cursor/conversion.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cursor/conversion.rs @@ -20,9 +20,11 @@ use crate::bindings::_helpers::cursor::values::{ }; use crate::bindings::_helpers::native_host::facade::FacadeHost; use crate::bindings::_helpers::native_host::host; +use crate::bindings::_helpers::records::construct::{ + enum_from_value, new_record_with, record_class, +}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; /// Cursor grain names, finest first. pub(crate) const GRAIN_NAMES: [(&str, Grain); 6] = [ @@ -59,9 +61,9 @@ impl<'py> CursorSession<'py> { pub(crate) fn new(py: Python<'py>) -> PyResult { Ok(Self { py, - timestamp_class: registry::class(py, RecordId::TimestampValue)?, - date_class: registry::class(py, RecordId::DateValue)?, - integer_class: registry::class(py, RecordId::IntegerValue)?, + timestamp_class: record_class(py, RecordId::TimestampValue)?, + date_class: record_class(py, RecordId::DateValue)?, + integer_class: record_class(py, RecordId::IntegerValue)?, inputs: RefCell::new(Vec::new()), failure: RefCell::new(None), }) @@ -135,7 +137,7 @@ impl<'py> CursorSession<'py> { return Ok(Some(*known)); } } - registry::class(self.py, RecordId::CursorGrain)?.call1((grain,))?; + enum_from_value(self.py, RecordId::CursorGrain, grain)?; Err(PyTypeError::new_err("cursor grain must be a CursorGrain")) } @@ -198,13 +200,14 @@ impl<'py> CursorSession<'py> { .iter() .find(|(_, known)| *known == grain) .map_or("", |(name, _)| name); - registry::class(self.py, RecordId::CursorGrain)?.call1((name,)) + enum_from_value(self.py, RecordId::CursorGrain, name) } /// A new `AlignedInterval`, validated as its constructor validates. pub(crate) fn python_interval(&self, interval: &Interval) -> PyResult> { - call_with( - ®istry::class(self.py, RecordId::AlignedInterval)?, + new_record_with( + self.py, + RecordId::AlignedInterval, &[ ("start", self.python_scalar(&interval.start)?), ("end", self.python_scalar(&interval.end)?), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cursor/durations.rs b/crates/sqlbuild-python/src/bindings/_helpers/cursor/durations.rs index ef510ee59f..748ca20656 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cursor/durations.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cursor/durations.rs @@ -43,7 +43,7 @@ pub(crate) fn parse_cursor_duration<'py>( } fn duration_span(duration: &Bound<'_, PyAny>) -> PyResult { - let field = |name: &str| -> PyResult { duration.getattr(name)?.extract() }; + let field = |name: &str| -> PyResult { native_field(duration, name)?.extract() }; Ok(DurationSpan { months: field("years")? * MONTHS_PER_YEAR + field("months")?, days: field("days")?, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cursor/overrides.rs b/crates/sqlbuild-python/src/bindings/_helpers/cursor/overrides.rs index d3178e8364..92b95542c1 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cursor/overrides.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cursor/overrides.rs @@ -8,16 +8,16 @@ use sqlbuild_cursor::algebra::main::parse_integer_text::parse_integer_text; use crate::bindings::_helpers::compile_driver::python_calls::imported; use crate::bindings::_helpers::cursor::conversion::algebra_error; use crate::bindings::_helpers::cursor::values::DATETIME_MODULE; +use crate::bindings::_helpers::records::construct::record_class; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; const DECIMAL_MODULE: &str = "decimal"; const INTEGER_AS_TIMESTAMP: &str = "an integer cursor cannot be used as a timestamp"; const TIMESTAMP_AS_INTEGER: &str = "a timestamp cursor cannot be used as an integer"; fn instance_of(value: &Bound<'_, PyAny>, class: RecordId) -> PyResult { - value.is_instance(®istry::class(value.py(), class)?) + value.is_instance(&record_class(value.py(), class)?) } /// A timestamp override as a `datetime`; dates start at midnight and text is ISO 8601. diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cursor/scalars.rs b/crates/sqlbuild-python/src/bindings/_helpers/cursor/scalars.rs index d3e9377179..8595a0fa79 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cursor/scalars.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cursor/scalars.rs @@ -24,14 +24,14 @@ use sqlbuild_cursor::algebra::main::parse_integer_text::parse_integer_text; use sqlbuild_cursor::algebra::main::render::render; use sqlbuild_cursor::algebra::models::{Extreme, Grain, Scalar, ScalarValue, SortKey, ZoneOffset}; -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; +use crate::bindings::_helpers::compile_driver::python_calls::imported; use crate::bindings::_helpers::cursor::conversion::{CursorSession, algebra_error}; use crate::bindings::_helpers::cursor::values::{DATETIME_MODULE, naive_date_time}; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; +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; -use crate::bindings::_helpers::records::registry; const DECIMAL_MODULE: &str = "decimal"; const INTEGER_CURSOR: &str = "integer"; @@ -49,9 +49,12 @@ fn chained(py: Python<'_>, message: String, cause: PyErr) -> PyErr { fn parse_integer<'py>(raw: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = raw.py(); let text: String = raw.str()?.to_str()?.to_owned(); - let class: Bound<'py, PyAny> = registry::class(py, RecordId::IntegerValue)?; if let Some(value) = parse_integer_text(&text) { - return call_with(&class, &[("value", value.into_pyobject(py)?.into_any())]); + return new_record_with( + py, + RecordId::IntegerValue, + &[("value", value.into_pyobject(py)?.into_any())], + ); } let parsed: PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> = imported(py, DECIMAL_MODULE, "Decimal")? @@ -78,7 +81,7 @@ fn parse_integer<'py>(raw: &Bound<'py, PyAny>) -> PyResult> { &format!("non-integral integer cursor value: {text}"), )); } - call_with(&class, &[("value", integer)]) + new_record_with(py, RecordId::IntegerValue, &[("value", integer)]) } /// Whether `int(Decimal(...))` failed with `InvalidOperation`, `ValueError` or `OverflowError`. @@ -108,10 +111,7 @@ fn parse_timestamp_text<'py>(raw: &Bound<'py, PyAny>, text: &str) -> PyResult { - return call_with( - ®istry::class(py, RecordId::DateValue)?, - &[("value", date)], - ); + return new_record_with(py, RecordId::DateValue, &[("value", date)]); } Err(error) if error.is_instance_of::(py) => {} Err(error) => return Err(error), @@ -120,8 +120,9 @@ fn parse_timestamp_text<'py>(raw: &Bound<'py, PyAny>, text: &str) -> PyResult call_with( - ®istry::class(py, RecordId::TimestampValue)?, + Ok(value) => new_record_with( + py, + RecordId::TimestampValue, &[ ("value", value), ("source_text", PyString::new(py, &normalized).into_any()), @@ -153,16 +154,10 @@ pub(crate) fn parse_value<'py>( } if raw.is_instance(&imported(py, DATETIME_MODULE, "datetime")?)? { let value: Bound<'py, PyAny> = utc_normalized(raw)?; - return call_with( - ®istry::class(py, RecordId::TimestampValue)?, - &[("value", value)], - ); + return new_record_with(py, RecordId::TimestampValue, &[("value", value)]); } if raw.is_instance(&imported(py, DATETIME_MODULE, "date")?)? { - return call_with( - ®istry::class(py, RecordId::DateValue)?, - &[("value", raw.clone())], - ); + return new_record_with(py, RecordId::DateValue, &[("value", raw.clone())]); } let Ok(text) = raw.downcast::() else { return Err(algebra_error( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/dag_artifact/rows.rs b/crates/sqlbuild-python/src/bindings/_helpers/dag_artifact/rows.rs index 3ef6e485fe..ba0b4e5305 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dag_artifact/rows.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dag_artifact/rows.rs @@ -23,7 +23,7 @@ const CHECK_KIND: &str = "check"; const DEFAULT_MATERIALIZATION: &str = "view"; fn attribute<'py>(value: &Bound<'py, PyAny>, name: &str) -> PyResult> { - value.getattr(name) + native_field(value, name) } fn text(value: &Bound<'_, PyAny>, name: &str) -> PyResult { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/dagster_invocation/results.rs b/crates/sqlbuild-python/src/bindings/_helpers/dagster_invocation/results.rs index 7c5293cce2..d609740a4d 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dagster_invocation/results.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dagster_invocation/results.rs @@ -470,7 +470,7 @@ fn envelope_fields<'py>( ) -> PyResult)>> { names .iter() - .map(|name| Ok(((*name).to_owned(), envelope.getattr(*name)?))) + .map(|name| Ok(((*name).to_owned(), native_field(envelope, name)?))) .collect() } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/dbt_command/auto_init.rs b/crates/sqlbuild-python/src/bindings/_helpers/dbt_command/auto_init.rs index 3710ca73c8..a6794f07de 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dbt_command/auto_init.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dbt_command/auto_init.rs @@ -11,12 +11,13 @@ use sqlbuild_core::dbt_command::main::forwarded_dbt_args::forwarded_dbt_args; use sqlbuild_core::dbt_command::main::render_dbt_auto_init::render_dbt_auto_init; use sqlbuild_core::dbt_command::models::ForwardedArg; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; +use crate::bindings::_helpers::cli::progress_reporters::DbtInitProgressReporter; +use crate::bindings::_helpers::dbt_command::init_project::build_dbt_init_project; use crate::bindings::_helpers::dbt_command::support::{color_enabled, flag, path, stream}; +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::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::ids::RecordId; use sqlbuild_core::constants::PROJECT_CONFIG_FILENAME; fn project_config_found(directory: &Bound<'_, PyAny>) -> PyResult { @@ -80,15 +81,15 @@ pub(crate) fn ensure_twin_project<'py>( let use_color: bool = !no_color && !args.iter().any(|arg| arg == JSON_OPTION) && color_enabled(py)?; let progress_stream: Bound<'py, PyAny> = stream(py, "stderr")?; - let progress: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::DbtInitProgressReporter)?, - &[ - ("stream", progress_stream.clone()), - ("use_color", flag(py, use_color)), - ], - )?; - let callbacks: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::DbtInitProgressCallbacks)?, + let progress: Bound<'py, PyAny> = DbtInitProgressReporter::new( + &progress_stream.clone(), + flag(py, use_color).extract::()?, + ) + .and_then(|value| pyo3::Bound::new(py, value)) + .map(pyo3::Bound::into_any)?; + let callbacks: Bound<'py, PyAny> = new_record_with( + py, + RecordId::DbtInitProgressCallbacks, &[ ("start", native_field(&progress, "start")?), ("complete", native_field(&progress, "complete")?), @@ -103,8 +104,9 @@ pub(crate) fn ensure_twin_project<'py>( Some(raw) => PyString::new(py, &raw).into_any(), None => none.clone(), }; - let request: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::DbtInitRequest)?, + let request: Bound<'py, PyAny> = new_record_with( + py, + RecordId::DbtInitRequest, &[ ("cwd", effective_dir), ("dbt_project_dir", dbt_project_dir), @@ -118,10 +120,7 @@ pub(crate) fn ensure_twin_project<'py>( ("progress_callbacks", callbacks), ], )?; - let result: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::BuildDbtInitProject)?, - &[("request", request)], - )?; + let result: Bound<'py, PyAny> = build_dbt_init_project(&request)?; let project_file: String = native_field(&result, "project_file")?.str()?.to_string(); progress_stream.call_method1( "write", diff --git a/crates/sqlbuild-python/src/bindings/_helpers/dbt_command/command.rs b/crates/sqlbuild-python/src/bindings/_helpers/dbt_command/command.rs index 3a35b84ba0..c015fa7d63 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dbt_command/command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dbt_command/command.rs @@ -6,11 +6,16 @@ use pyo3::{pyfunction, wrap_pyfunction}; use sqlbuild_core::dbt_command::main::dbt_output_options::dbt_output_options; use sqlbuild_core::dbt_command::models::DbtOutputOptions; -use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; +use crate::bindings::_helpers::cli::progress_reporters::PlanningProgressReporter; +use crate::bindings::_helpers::compile_driver::python_calls::{ + KeywordArguments, call_with, keywords, +}; use crate::bindings::_helpers::dbt_command::auto_init::{args_tuple, ensure_twin_project}; use crate::bindings::_helpers::dbt_command::debug::run_dbt_debug; use crate::bindings::_helpers::dbt_command::support::{color_enabled, effective_dir, flag, stream}; +use crate::bindings::_helpers::dbt_interop::plan::render_dbt_interop_plan; use crate::bindings::_helpers::dbt_interop::runtime_config::validate_dbt_execution_args; +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::{NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; @@ -37,13 +42,13 @@ impl<'py> Progress<'py> { "stdout" }, )?; - let reporter: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::PlanningProgressReporter)?, - &[ - ("stream", stream.clone()), - ("use_color", flag(py, use_color)), - ], - )?; + let reporter: Bound<'py, PyAny> = PlanningProgressReporter::new( + py, + &stream.clone(), + flag(py, use_color).extract::()?, + ) + .and_then(|value| pyo3::Bound::new(py, value)) + .map(pyo3::Bound::into_any)?; Ok(Self { stream, use_color, @@ -79,8 +84,9 @@ fn run_plan(project_dir: &Bound<'_, PyAny>, args: &[String], no_color: bool) -> ("use_color", flag(py, progress.use_color)), ], )?; - let display: Bound<'_, PyAny> = call_with( - ®istry::class(py, RecordId::DisplayOptions)?, + let display: Bound<'_, PyAny> = new_record_with( + py, + RecordId::DisplayOptions, &[( "max_entries_per_section", if options.verbose { @@ -90,15 +96,19 @@ fn run_plan(project_dir: &Bound<'_, PyAny>, args: &[String], no_color: bool) -> }, )], )?; - let rendered: String = call_with( - ®istry::native(py, NativeAttr::RenderDbtInteropPlan)?, - &[ - ("plan", plan), - ("json_output", flag(py, options.json_output)), - ("use_color", flag(py, progress.use_color)), - ("display_options", display), - ], - )? + let rendered: String = render_dbt_interop_plan( + py, + Some(keywords( + py, + &[ + ("plan", plan), + ("json_output", flag(py, options.json_output)), + ("use_color", flag(py, progress.use_color)), + ("display_options", display), + ], + )?), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))? .extract()?; stream(py, "stdout")?.call_method1("write", (format!("{rendered}\n"),))?; Ok(0) @@ -115,8 +125,9 @@ fn run_execution( let options: DbtOutputOptions = dbt_output_options(args); let progress: Progress<'_> = Progress::new(py, &options, no_color)?; progress.blank_line()?; - let request: Bound<'_, PyAny> = call_with( - ®istry::class(py, RecordId::DbtInteropExecutionRequest)?, + let request: Bound<'_, PyAny> = new_record_with( + py, + RecordId::DbtInteropExecutionRequest, &[ ("command", command.clone()), ("project_dir", project_dir.clone()), @@ -140,7 +151,7 @@ fn run_execution( /// Execute one `sqb dbt` interop command. #[pyfunction] #[pyo3(signature = (**options))] -fn run_dbt_command(py: Python<'_>, options: Option>) -> PyResult { +pub(crate) fn run_dbt_command(py: Python<'_>, options: Option>) -> PyResult { let arguments: KeywordArguments<'_> = KeywordArguments::read( py, "run_dbt_command", diff --git a/crates/sqlbuild-python/src/bindings/_helpers/dbt_command/debug.rs b/crates/sqlbuild-python/src/bindings/_helpers/dbt_command/debug.rs index 3c63ed14d7..a3158e1ed4 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dbt_command/debug.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dbt_command/debug.rs @@ -3,11 +3,11 @@ use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; use sqlbuild_core::dbt_command::constants::NO_CONNECTION_OPTION; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; +use crate::bindings::_helpers::compile_driver::python_calls::keywords; use crate::bindings::_helpers::dbt_command::auto_init::args_tuple; use crate::bindings::_helpers::dbt_command::support::{color_enabled, flag, status, stream}; -use crate::bindings::_helpers::records::ids::NativeAttr; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::dbt_interop::pipeline::debug_dbt_from_project; +use crate::bindings::_helpers::debug_command::run_debug::run_debug_command; /// Run `body` under a stderr status line that is always closed; returns its exit code. fn with_status( @@ -33,32 +33,40 @@ pub(crate) fn run_dbt_debug( let py: Python<'_> = project_dir.py(); let use_color: bool = !no_color && color_enabled(py)?; let dbt_code: i64 = with_status(py, ("Running dbt debug...", use_color), || { - call_with( - ®istry::native(py, NativeAttr::DebugDbtFromProject)?, - &[ - ("project_dir", project_dir.clone()), - ("args", args_tuple(py, args)?), - ("stdout_stream", stream(py, "stdout")?), - ("stderr_stream", stream(py, "stderr")?), - ], - )? + debug_dbt_from_project( + py, + Some(keywords( + py, + &[ + ("project_dir", project_dir.clone()), + ("args", args_tuple(py, args)?), + ("stdout_stream", stream(py, "stdout")?), + ("stderr_stream", stream(py, "stderr")?), + ], + )?), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))? .extract() })?; stream(py, "stdout")?.call_method1("write", ("\n",))?; let sqlbuild_code: i64 = with_status(py, ("Running SQLBuild diagnostics...", use_color), || { - call_with( - ®istry::native(py, NativeAttr::RunDebugCommand)?, - &[ - ("project_dir", project_dir.clone()), - ("no_color", flag(py, no_color)), - ( - "no_connection", - flag(py, args.iter().any(|arg| arg == NO_CONNECTION_OPTION)), - ), - ("json_output", flag(py, false)), - ], - )? + run_debug_command( + py, + Some(keywords( + py, + &[ + ("project_dir", project_dir.clone()), + ("no_color", flag(py, no_color)), + ( + "no_connection", + flag(py, args.iter().any(|arg| arg == NO_CONNECTION_OPTION)), + ), + ("json_output", flag(py, false)), + ], + )?), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))? .extract() })?; Ok(i32::from(dbt_code != 0 || sqlbuild_code != 0)) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/dbt_command/init.rs b/crates/sqlbuild-python/src/bindings/_helpers/dbt_command/init.rs index 6b2c6b68a7..d79bb6cbb3 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dbt_command/init.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dbt_command/init.rs @@ -11,10 +11,14 @@ use sqlbuild_core::dbt_command::main::render_dbt_init_preview::render_dbt_init_p use sqlbuild_core::dbt_command::main::render_dbt_init_summary::render_dbt_init_summary; use sqlbuild_core::dbt_command::models::DbtInitSummary; -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; +use crate::bindings::_helpers::cli::progress_reporters::DbtInitProgressReporter; +use crate::bindings::_helpers::compile_driver::python_calls::imported; +use crate::bindings::_helpers::dbt_command::init_project::build_dbt_init_project; +use crate::bindings::_helpers::dbt_command::init_project::validate_dbt_init_project; use crate::bindings::_helpers::dbt_command::support::{color_enabled, flag, path, stream}; +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::{ErrorId, NativeAttr, RecordId}; +use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; fn missing_project(py: Python<'_>) -> PyErr { @@ -59,7 +63,7 @@ fn dbt_project_dir<'py>(request: &Bound<'py, PyAny>) -> PyResult, name: &str) -> PyResult { - Ok(result.getattr(name)?.str()?.to_string()) + Ok(native_field(result, name)?.str()?.to_string()) } fn summary(result: &Bound<'_, PyAny>) -> PyResult { @@ -77,25 +81,26 @@ fn summary(result: &Bound<'_, PyAny>) -> PyResult { /// Execute SQLBuild-owned `sqb dbt init`. #[pyfunction] #[pyo3(signature = (request))] -fn run_dbt_init_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { +pub(crate) fn run_dbt_init_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { let project_dir: Bound<'_, PyAny> = dbt_project_dir(request)?; let use_color: bool = color_enabled(py)?; - let progress: Bound<'_, PyAny> = call_with( - ®istry::native(py, NativeAttr::DbtInitProgressReporter)?, - &[ - ("stream", stream(py, "stdout")?), - ("use_color", flag(py, use_color)), - ], - )?; - let callbacks: Bound<'_, PyAny> = call_with( - ®istry::class(py, RecordId::DbtInitProgressCallbacks)?, + let progress: Bound<'_, PyAny> = DbtInitProgressReporter::new( + &stream(py, "stdout")?, + flag(py, use_color).extract::()?, + ) + .and_then(|value| pyo3::Bound::new(py, value)) + .map(pyo3::Bound::into_any)?; + let callbacks: Bound<'_, PyAny> = new_record_with( + py, + RecordId::DbtInitProgressCallbacks, &[ ("start", native_field(&progress, "start")?), ("complete", native_field(&progress, "complete")?), ], )?; - let init_request: Bound<'_, PyAny> = call_with( - ®istry::class(py, RecordId::DbtInitRequest)?, + let init_request: Bound<'_, PyAny> = new_record_with( + py, + RecordId::DbtInitRequest, &[ ("cwd", native_field(request, "cwd")?), ("dbt_project_dir", project_dir), @@ -115,14 +120,9 @@ fn run_dbt_init_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult< ("progress_callbacks", callbacks), ], )?; - call_with( - ®istry::native(py, NativeAttr::ValidateDbtInitProject)?, - &[("request", init_request.clone())], - )?; - let result: Bound<'_, PyAny> = call_with( - ®istry::native(py, NativeAttr::BuildDbtInitProject)?, - &[("request", init_request)], - )?; + validate_dbt_init_project(&init_request.clone()) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; + let result: Bound<'_, PyAny> = build_dbt_init_project(&init_request)?; let style: CliStyle = CliStyle { use_color }; let stdout: Bound<'_, PyAny> = stream(py, "stdout")?; if native_field(request, "dry_run")?.is_truthy()? { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/dbt_command/init_project.rs b/crates/sqlbuild-python/src/bindings/_helpers/dbt_command/init_project.rs index fdb10dcd25..b1d3fd6abc 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dbt_command/init_project.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dbt_command/init_project.rs @@ -12,19 +12,21 @@ use sqlbuild_core::dbt_process::main::run_dbt_process::run_dbt_process; use sqlbuild_core::dbt_process::models::DbtProcessOutput; use sqlbuild_core::text::main::python_strip::python_strip; -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; +use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported, keywords}; +use crate::bindings::_helpers::dbt_interop::profile::build_dbt_init_project_toml; +use crate::bindings::_helpers::dbt_interop::profile::normalize_dbt_profile_connection; use crate::bindings::_helpers::dbt_interop::profile_load::{ load_dbt_project_metadata, load_selected_dbt_profile_output, }; use crate::bindings::_helpers::dbt_interop::runner::launch_os_error; +use crate::bindings::_helpers::records::construct::new_record_with; use crate::bindings::types::CompilerDetach; use sqlbuild_config::dbt_profile::models::DbtProjectMetadata; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::native_host::third_party::ThirdPartyHost; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::ids::RecordId; use sqlbuild_core::constants::PROJECT_CONFIG_FILENAME as PROJECT_FILE; const DEFAULT_OUTPUT_DIR: &str = "sqlbuild_project"; const PROFILES_FILE: &str = "profiles.yml"; @@ -42,7 +44,9 @@ struct InitPaths<'py> { /// Create or render a minimal SQLBuild project from a dbt project. #[pyfunction] #[pyo3(signature = (*, request))] -fn build_dbt_init_project<'py>(request: &Bound<'py, PyAny>) -> PyResult> { +pub(crate) fn build_dbt_init_project<'py>( + request: &Bound<'py, PyAny>, +) -> PyResult> { let py: Python<'py> = request.py(); let cwd: Bound<'py, PyAny> = native_field(request, "cwd")?; let paths: InitPaths<'py> = init_paths(request)?; @@ -89,39 +93,40 @@ fn build_dbt_init_project<'py>(request: &Bound<'py, PyAny>) -> PyResult = call_with( - ®istry::native(py, NativeAttr::NormalizeDbtProfileConnection)?, - &[("resolved", resolved_output)], - )?; + let normalized: Bound<'py, PyAny> = normalize_dbt_profile_connection(py, resolved_output)?; let target_path: Bound<'py, PyAny> = resolved( &paths.dbt_project_dir, &path_of(py, &PyString::new(py, &metadata.target_path).into_any())?, )?; let project_name: String = metadata.project_name.replace('-', "_"); let target_name: Bound<'py, PyAny> = native_field(&selected, "target_name")?; - let toml: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::BuildDbtInitProjectToml)?, - &[ - ("project_name", project_name.clone().into_bound_py_any(py)?), - ("adapter", native_field(&normalized, "adapter")?), - ("target_name", target_name.clone()), - ("profile_name", profile_name.clone()), - ( - "dbt_project_dir", - display_path(&paths.dbt_project_dir, &output_dir)?, - ), - ( - "profiles_dir", - display_path(&paths.profiles_dir, &output_dir)?, - ), - ("target_path", display_path(&target_path, &output_dir)?), - ("target_schema", native_field(&normalized, "target_schema")?), - ( - "target_database", - native_field(&normalized, "target_database")?, - ), - ], - )?; + let toml: Bound<'py, PyAny> = build_dbt_init_project_toml( + py, + Some(keywords( + py, + &[ + ("project_name", project_name.clone().into_bound_py_any(py)?), + ("adapter", native_field(&normalized, "adapter")?), + ("target_name", target_name.clone()), + ("profile_name", profile_name.clone()), + ( + "dbt_project_dir", + display_path(&paths.dbt_project_dir, &output_dir)?, + ), + ( + "profiles_dir", + display_path(&paths.profiles_dir, &output_dir)?, + ), + ("target_path", display_path(&target_path, &output_dir)?), + ("target_schema", native_field(&normalized, "target_schema")?), + ( + "target_database", + native_field(&normalized, "target_database")?, + ), + ], + )?), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; progress(&callbacks, "complete", "Rendered dbt profile connection.")?; let mut warnings: Vec> = normalized .getattr("warnings")? @@ -146,8 +151,9 @@ fn build_dbt_init_project<'py>(request: &Bound<'py, PyAny>) -> PyResult(request: &Bound<'py, PyAny>) -> PyResult) -> Result<(), PyErr> { +pub(crate) fn validate_dbt_init_project(request: &Bound<'_, PyAny>) -> Result<(), PyErr> { let paths: InitPaths<'_> = init_paths(request)?; let metadata: DbtProjectMetadata = load_dbt_project_metadata(&paths.dbt_project_dir)?; let profile_name: Bound<'_, PyAny> = profile_name(request, &metadata)?; @@ -317,7 +323,7 @@ fn dbt_debug_warning(paths: &InitPaths<'_>, target_name: &str) -> PyResult, name: &str, message: &str) -> PyResult<()> { - let callback: Bound<'_, PyAny> = callbacks.getattr(name)?; + let callback: Bound<'_, PyAny> = native_field(callbacks, name)?; if !callback.is_none() { callback.call1((message,))?; } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/dbt_events/results.rs b/crates/sqlbuild-python/src/bindings/_helpers/dbt_events/results.rs index 537bf6b09d..f759e483c5 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dbt_events/results.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dbt_events/results.rs @@ -5,9 +5,9 @@ use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; use pyo3::types::{PyString, PyTuple}; use sqlbuild_core::dbt_events::models::{DbtNodeMessage, DbtNodeResult}; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; +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; -use crate::bindings::_helpers::records::registry; fn optional<'py, T: pyo3::IntoPyObject<'py>>( py: Python<'py>, @@ -32,13 +32,13 @@ pub(crate) fn python_result<'py>( py: Python<'py>, result: &DbtNodeResult, ) -> PyResult> { - let message_class: Bound<'py, PyAny> = registry::class(py, RecordId::DbtNodeMessage)?; let messages: Vec> = result .messages .iter() .map(|message| { - call_with( - &message_class, + new_record_with( + py, + RecordId::DbtNodeMessage, &[ ("level", text(py, &message.level)), ("message", text(py, &message.message)), @@ -46,8 +46,9 @@ pub(crate) fn python_result<'py>( ) }) .collect::>()?; - call_with( - ®istry::class(py, RecordId::DbtNodeExecutionResult)?, + new_record_with( + py, + RecordId::DbtNodeExecutionResult, &[ ("unique_id", text(py, &result.unique_id)), ("resource_type", text(py, &result.resource_type)), @@ -76,7 +77,7 @@ pub(crate) fn python_result<'py>( } fn field pyo3::FromPyObject<'a>>(value: &Bound<'_, PyAny>, name: &str) -> PyResult { - value.getattr(name)?.extract() + native_field(value, name)?.extract() } /// A Python `DbtNodeExecutionResult` as plain data. diff --git a/crates/sqlbuild-python/src/bindings/_helpers/dbt_events/stream.rs b/crates/sqlbuild-python/src/bindings/_helpers/dbt_events/stream.rs index 7ccd60218c..c1c472ddc7 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dbt_events/stream.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dbt_events/stream.rs @@ -88,7 +88,7 @@ impl DbtEventStream { impl DbtEventStream { #[new] #[pyo3(signature = (**options))] - fn new(py: Python<'_>, options: Option>) -> PyResult { + pub(crate) fn new(py: Python<'_>, options: Option>) -> PyResult { let arguments: KeywordArguments<'_> = KeywordArguments::read(py, "DbtEventStream", options, STREAM_OPTIONS)?; Ok(Self { @@ -121,7 +121,10 @@ impl DbtEventStream { /// One dbt node row and its messages. #[pyfunction] #[pyo3(signature = (**options))] -fn render_dbt_node_result(py: Python<'_>, options: Option>) -> PyResult { +pub(crate) fn render_dbt_node_result( + py: Python<'_>, + options: Option>, +) -> PyResult { let arguments: KeywordArguments<'_> = KeywordArguments::read(py, "render_dbt_node_result", options, RENDER_OPTIONS)?; let position: DbtRowPosition = DbtRowPosition { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/cli.rs b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/cli.rs index d38960d760..eb2dcac4de 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/cli.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/cli.rs @@ -21,6 +21,9 @@ use sqlbuild_core::text::main::python_strip::python_strip; use crate::bindings::_helpers::compile_driver::python_calls::{ KeywordArguments, call_with, imported, }; +use crate::bindings::_helpers::records::construct::{ + enum_from_value, new_record_with, record_class, +}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -45,7 +48,7 @@ fn optional_text(value: &Bound<'_, PyAny>) -> PyResult> { } fn attribute_text(object: &Bound<'_, PyAny>, name: &str) -> PyResult> { - optional_text(&object.getattr(name)?) + optional_text(&native_field(object, name)?) } fn texts(values: &Bound<'_, PyAny>) -> PyResult> { @@ -218,13 +221,16 @@ fn ls_node<'py>( /// Nodes from `dbt ls` JSON lines, skipping log lines, non-objects and nodes without an ID. #[pyfunction] #[pyo3(signature = (*, stdout))] -fn parse_dbt_ls_json_lines<'py>(py: Python<'py>, stdout: &str) -> PyResult> { +pub(crate) fn parse_dbt_ls_json_lines<'py>( + py: Python<'py>, + stdout: &str, +) -> PyResult> { ls_nodes(py, stdout) } /// The `DbtLsNode`s of `dbt ls --output json` lines. pub(crate) fn ls_nodes<'py>(py: Python<'py>, stdout: &str) -> PyResult> { - let node_class: Bound<'py, PyAny> = registry::class(py, RecordId::DbtLsNode)?; + let node_class: Bound<'py, PyAny> = record_class(py, RecordId::DbtLsNode)?; let mut nodes: Vec> = Vec::new(); for line in python_splitlines(stdout) { let stripped: &str = python_strip(line); @@ -259,7 +265,7 @@ pub(crate) fn raised(py: Python<'_>, class_name: ErrorId, error: DbtCliError) -> fn parsed_flags(parsed: &Bound<'_, PyAny>) -> PyResult { let value = |name: &str| attribute_text(parsed, name); - let flag = |name: &str| -> PyResult { parsed.getattr(name)?.is_truthy() }; + let flag = |name: &str| -> PyResult { native_field(parsed, name)?.is_truthy() }; Ok(DbtParsedFlags { select: texts(&native_field(parsed, "select")?)?, exclude: texts(&native_field(parsed, "exclude")?)?, @@ -290,7 +296,7 @@ fn parsed_flags(parsed: &Bound<'_, PyAny>) -> PyResult { /// Split parsed `sqb dbt ` flags into dbt and SQLBuild buckets. #[pyfunction] #[pyo3(signature = (*, command, parsed))] -fn route_dbt_interop_args<'py>( +pub(crate) fn route_dbt_interop_args<'py>( py: Python<'py>, command: &Bound<'py, PyAny>, parsed: &Bound<'py, PyAny>, @@ -299,10 +305,10 @@ fn route_dbt_interop_args<'py>( let flags: DbtParsedFlags = parsed_flags(parsed)?; let (kind, routed): (DbtInteropCommandKind, DbtRoutedArgs) = route(&command, &flags).map_err(|error| raised(py, ErrorId::DbtInteropArgument, error))?; - let command: Bound<'py, PyAny> = - registry::class(py, RecordId::DbtInteropCommand)?.call1((kind.name(),))?; - call_with( - ®istry::class(py, RecordId::DbtInteropRoutedArgs)?, + let command: Bound<'py, PyAny> = enum_from_value(py, RecordId::DbtInteropCommand, kind.name())?; + new_record_with( + py, + RecordId::DbtInteropRoutedArgs, &[ ("command", command), ("select", PyTuple::new(py, flags.select)?.into_any()), @@ -363,7 +369,7 @@ fn config_sources( /// Resolve dbt config from CLI overrides, local config and project config. #[pyfunction] #[pyo3(signature = (**options))] -fn resolve_dbt_config<'py>( +pub(crate) fn resolve_dbt_config<'py>( py: Python<'py>, options: Option>, ) -> PyResult> { @@ -382,8 +388,9 @@ fn resolve_dbt_config<'py>( || py.None().into_bound(py), |target| PyString::new(py, &target).into_any(), ); - call_with( - ®istry::class(py, RecordId::ResolvedDbtConfig)?, + new_record_with( + py, + RecordId::ResolvedDbtConfig, &[ ( "project_dir", diff --git a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/event_stream.rs b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/event_stream.rs index efd43a63a3..3360637d7d 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/event_stream.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/event_stream.rs @@ -24,12 +24,11 @@ use sqlbuild_core::dbt_process::models::DbtJsonProcess; use crate::bindings::_helpers::build_progress::transient_status::status_reporter; use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, imported, within}; +use crate::bindings::_helpers::dbt_events::stream::DbtEventStream; use crate::bindings::_helpers::dbt_interop::runner::{ dbt_command_lifecycle, launch_failure, launch_os_error, report_exit, }; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::NativeAttr; -use crate::bindings::_helpers::records::registry; use crate::bindings::types::CompilerDetach; const STREAM_OPTIONS: (&[&str], &[&str]) = ( @@ -77,7 +76,7 @@ struct Output<'py> { /// Run dbt and render SQLBuild-styled rows from JSON events; returns the exit code and results. #[pyfunction] #[pyo3(signature = (**options))] -fn execute_dbt_json_event_stream<'py>( +pub(crate) fn execute_dbt_json_event_stream<'py>( py: Python<'py>, options: Option>, ) -> PyResult> { @@ -98,7 +97,9 @@ fn execute_dbt_json_event_stream<'py>( arguments.value("detail_by_unique_id")?, )?; keywords.set_item("use_color", output.use_color)?; - registry::native(py, NativeAttr::DbtEventStream)?.call((), Some(&keywords))? + DbtEventStream::new(py, Some(keywords.clone())) + .and_then(|value| pyo3::Bound::new(py, value)) + .map(pyo3::Bound::into_any)? }; let argv: Vec = arguments.value("argv")?.extract()?; let cwd: Option = arguments.value("cwd")?.extract()?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/execution.rs b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/execution.rs index eac5dec3c8..4d4e9d7df4 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/execution.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/execution.rs @@ -25,7 +25,7 @@ const MERGED_OPTIONS: (&[&str], &[&str]) = (&["command", "options", "routed_args /// Merge the original dbt selection with dbt models SQLBuild work requires; `None` without dbt work. #[pyfunction] #[pyo3(signature = (**options))] -fn build_merged_dbt_execution_argv<'py>( +pub(crate) fn build_merged_dbt_execution_argv<'py>( py: Python<'py>, options: Option>, ) -> PyResult>> { @@ -59,7 +59,7 @@ fn build_merged_dbt_execution_argv<'py>( /// The `dbt ls --output json` that counts an execution argv's selection, when it has a command. #[pyfunction] #[pyo3(signature = (*, argv))] -fn dbt_execution_ls_argv(argv: Vec) -> Option> { +pub(crate) fn dbt_execution_ls_argv(argv: Vec) -> Option> { ls_argv(&argv) } @@ -80,7 +80,7 @@ fn executed_nodes(node_results: &Bound<'_, PyAny>) -> PyResult( +pub(crate) fn build_failed_sqlbuild_model_names<'py>( graph: &Bound<'py, DbtCombinedGraph>, manifest: &Bound<'py, PyAny>, node_results: &Bound<'py, PyAny>, @@ -102,7 +102,7 @@ fn build_failed_sqlbuild_model_names<'py>( /// Render a PASS/WARN/FAIL/SKIP/TOTAL footer for executed dbt nodes. #[pyfunction] #[pyo3(signature = (*, node_results, use_color))] -fn render_dbt_execution_summary_footer( +pub(crate) fn render_dbt_execution_summary_footer( node_results: &Bound<'_, PyAny>, use_color: bool, ) -> PyResult> { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/graph.rs b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/graph.rs index c7cf0daf34..040dab6785 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/graph.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/graph.rs @@ -19,10 +19,14 @@ use sqlbuild_core::dbt_graph::models::{ }; use sqlbuild_core::dbt_graph::types::DbtSelectorResolver; -use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; +use crate::bindings::_helpers::compile_driver::python_calls::KeywordArguments; use crate::bindings::_helpers::dbt_interop::manifest::resolve_sqlbuild_model_dbt_refs; +use crate::bindings::_helpers::planner_selection::model_selectors::{ + resolve_sqlbuild_model_selector_names, split_selector_expansion, +}; +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::{ErrorId, NativeAttr, RecordId}; +use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; const MODEL_RESOURCE_TYPE: &str = "model"; @@ -164,7 +168,7 @@ fn project_models<'py>( /// Build the combined graph from a dbt manifest index and a compiled SQLBuild project. #[pyfunction] #[pyo3(signature = (*, manifest, project))] -fn build_dbt_combined_graph<'py>( +pub(crate) fn build_dbt_combined_graph<'py>( py: Python<'py>, manifest: Bound<'py, PyAny>, project: Bound<'py, PyAny>, @@ -177,25 +181,19 @@ fn build_dbt_combined_graph<'py>( pub(crate) struct PlannerResolver<'py> { project: Bound<'py, PyAny>, - split: Bound<'py, PyAny>, - resolve: Bound<'py, PyAny>, } /// The planner's selector parsing and SQLBuild model selector resolution over `project`. pub(crate) fn planner_resolver(project: Bound<'_, PyAny>) -> PyResult> { - let py: Python<'_> = project.py(); - Ok(PlannerResolver { - split: registry::native(py, NativeAttr::SplitSelectorExpansion)?, - resolve: registry::native(py, NativeAttr::ResolveSqlbuildModelSelectorNames)?, - project, - }) + Ok(PlannerResolver { project }) } impl DbtSelectorResolver for PlannerResolver<'_> { type Error = PyErr; fn split(&self, term: &str) -> PyResult { - let parsed: Bound<'_, PyAny> = self.split.call1((term,))?; + let parsed: Bound<'_, PyAny> = + split_selector_expansion(&PyString::new(self.project.py(), term))?; Ok(DbtSelectorExpansion { core: native_field(&parsed, "core")?.extract()?, upstream: native_field(&parsed, "upstream")?.is_truthy()?, @@ -205,14 +203,9 @@ impl DbtSelectorResolver for PlannerResolver<'_> { fn model_names(&self, core: &str) -> PyResult<(Vec, Option)> { let py: Python<'_> = self.project.py(); - let (names, translated): (Bound<'_, PyAny>, Option) = call_with( - &self.resolve, - &[ - ("project", self.project.clone()), - ("term", PyString::new(py, core).into_any()), - ], - )? - .extract()?; + let (names, translated): (Bound<'_, PyAny>, Option) = + resolve_sqlbuild_model_selector_names(&self.project, &PyString::new(py, core))? + .extract()?; Ok((texts(&names)?, translated)) } } @@ -267,8 +260,9 @@ fn selection_result<'py>(py: Python<'py>, selection: DbtSelection) -> PyResult>()?; - call_with( - ®istry::class(py, RecordId::DbtInteropSelectionResult)?, + new_record_with( + py, + RecordId::DbtInteropSelectionResult, &[ ( "sqlbuild_model_names", diff --git a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/manifest.rs b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/manifest.rs index cea8e6a43b..36b7047a13 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/manifest.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/manifest.rs @@ -19,9 +19,8 @@ use sqlbuild_config::dbt_manifest::models::{ ManifestNode, NodeChecksum, ParsedModel, ParsedSeed, ParsedSource, }; -use crate::bindings::_helpers::compile_driver::python_calls::{ - KeywordArguments, call_with, imported, -}; +use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, imported}; +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::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -139,8 +138,9 @@ fn model_object<'py>( raw: &Bound<'py, PyDict>, ) -> PyResult> { let py: Python<'py> = raw.py(); - call_with( - ®istry::class(py, RecordId::DbtManifestModel)?, + new_record_with( + py, + RecordId::DbtManifestModel, &[ ("unique_id", unique_id.clone()), ("package_name", text(py, &model.package_name)), @@ -170,8 +170,9 @@ fn seed_object<'py>( raw: &Bound<'py, PyDict>, ) -> PyResult> { let py: Python<'py> = raw.py(); - call_with( - ®istry::class(py, RecordId::DbtManifestSeed)?, + new_record_with( + py, + RecordId::DbtManifestSeed, &[ ("unique_id", unique_id.clone()), ("package_name", text(py, &seed.package_name)), @@ -191,8 +192,9 @@ fn source_object<'py>( raw: &Bound<'py, PyDict>, ) -> PyResult> { let py: Python<'py> = raw.py(); - call_with( - ®istry::class(py, RecordId::DbtManifestSource)?, + new_record_with( + py, + RecordId::DbtManifestSource, &[ ("unique_id", unique_id.clone()), ("package_name", text(py, &source.package_name)), @@ -337,8 +339,9 @@ fn manifest_index<'py>(raw_data: &Bound<'py, PyAny>) -> PyResult(contents: &Bound<'py, PyAny>) -> PyResult> { /// Load and index dbt model nodes from a manifest.json file. #[pyfunction] #[pyo3(signature = (*, manifest_path))] -fn load_dbt_manifest_index<'py>( +pub(crate) fn load_dbt_manifest_index<'py>( py: Python<'py>, manifest_path: Bound<'py, PyAny>, ) -> PyResult> { @@ -393,7 +396,7 @@ fn load_dbt_manifest_index<'py>( /// Build a dbt manifest index from discovered manifest contents. #[pyfunction] #[pyo3(signature = (*, manifest_contents))] -fn build_compile_dbt_manifest_index<'py>( +pub(crate) fn build_compile_dbt_manifest_index<'py>( py: Python<'py>, manifest_contents: Bound<'py, PyAny>, ) -> PyResult> { @@ -460,7 +463,7 @@ fn resolve_dbt_manifest_model<'py>( /// Reject ambiguous ownership between dbt and SQLBuild models. #[pyfunction] #[pyo3(signature = (*, known_model_names, dbt_manifest))] -fn validate_compile_dbt_model_names( +pub(crate) fn validate_compile_dbt_model_names( py: Python<'_>, known_model_names: Bound<'_, PyAny>, dbt_manifest: Bound<'_, PyAny>, @@ -496,7 +499,7 @@ fn optional_text(value: &Bound<'_, PyAny>) -> PyResult> { /// Validate one compile-time dbt ref against a dbt manifest. #[pyfunction] #[pyo3(signature = (*, reference, model_relative_path, dbt_manifest))] -fn validate_compile_dbt_model_reference( +pub(crate) fn validate_compile_dbt_model_reference( py: Python<'_>, reference: Bound<'_, PyAny>, model_relative_path: Bound<'_, PyAny>, @@ -561,7 +564,7 @@ pub(crate) fn resolve_sqlbuild_model_dbt_refs<'py>( /// Resolve one external dbt reference to its manifest relation. #[pyfunction] #[pyo3(signature = (**options))] -fn resolve_dbt_reference_relation<'py>( +pub(crate) fn resolve_dbt_reference_relation<'py>( py: Python<'py>, options: Option>, ) -> PyResult> { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/orchestration.rs b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/orchestration.rs index 8489295842..79de03a7da 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/orchestration.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/orchestration.rs @@ -22,6 +22,7 @@ use crate::bindings::_helpers::dbt_interop::graph::{ }; use crate::bindings::_helpers::dbt_interop::manifest::resolve_sqlbuild_model_dbt_refs; use crate::bindings::_helpers::dbt_interop::plan::build_dbt_interop_plan; +use crate::bindings::_helpers::records::construct::enum_from_value; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -136,14 +137,14 @@ fn argv_tuples<'py>(py: Python<'py>, argvs: Vec>) -> PyResult( +pub(crate) fn plan_dbt_interop_command<'py>( py: Python<'py>, options: Option>, ) -> PyResult> { let arguments: KeywordArguments<'py> = KeywordArguments::read(py, "plan_dbt_interop_command", options, PLAN_OPTIONS)?; let command: Bound<'py, PyAny> = - registry::class(py, RecordId::DbtInteropCommand)?.call1((arguments.value("command")?,))?; + enum_from_value(py, RecordId::DbtInteropCommand, arguments.value("command")?)?; let command_args: Bound<'py, PyAny> = arguments.value("command_args")?; let (select, exclude) = ( native_field(&command_args, "select")?, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/pipeline.rs b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/pipeline.rs index 5a66b8c059..ccf2edf5de 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/pipeline.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/pipeline.rs @@ -9,11 +9,20 @@ use sqlbuild_core::dbt_cli::main::dbt_debug_args::dbt_debug_args; use std::time::Instant; use crate::bindings::_helpers::build_command::support::{boolean, none, text}; +use crate::bindings::_helpers::cli::progress_reporters::ConnectionProgressReporter; use crate::bindings::_helpers::compile_driver::discovery::discover_project_inputs; +use crate::bindings::_helpers::compile_driver::pipeline_entries::plan_has_executable_work; use crate::bindings::_helpers::compile_driver::python_calls::keywords; use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; +use crate::bindings::_helpers::dbt_interop::cli::resolve_dbt_config; +use crate::bindings::_helpers::dbt_interop::execution::build_failed_sqlbuild_model_names; +use crate::bindings::_helpers::dbt_interop::execution::build_merged_dbt_execution_argv; +use crate::bindings::_helpers::dbt_interop::pipeline_phases::resolve_dbt_connection_config; +use crate::bindings::_helpers::dbt_interop::pipeline_phases::run_dbt_sqlbuild_work; use crate::bindings::_helpers::dbt_interop::pipeline_prologue::progress; +use crate::bindings::_helpers::dbt_interop::runner::DbtRunner; use crate::bindings::_helpers::dbt_interop::runtime_config::config_overrides; +use crate::bindings::_helpers::records::construct::{enum_member, new_record_with, record_class}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; @@ -49,9 +58,8 @@ fn prepare<'py>( let py = request.py(); let invocation = registry::native(py, NativeAttr::ResolveDbtExecutionInvocation)?.call1((request,))?; - let manifest = native( - py, - NativeAttr::LoadCompiledDbtManifest, + let manifest = call_with( + ®istry::native(py, NativeAttr::LoadCompiledDbtManifest)?, &[ ("runner", native_field(&invocation, "runner")?), ("dbt_options", native_field(&invocation, "dbt_options")?), @@ -59,9 +67,8 @@ fn prepare<'py>( ("on_progress", native_field(request, "on_progress")?), ], )?; - let compiled = native( - py, - NativeAttr::CompileDbtInteropProject, + let compiled = call_with( + ®istry::native(py, NativeAttr::CompileDbtInteropProject)?, &[ ("project_dir", native_field(request, "project_dir")?), ( @@ -108,7 +115,7 @@ fn resolve_plan<'py>( fn execute_dbt_interop_from_project(request: &Bound<'_, PyAny>) -> PyResult { let py = request.py(); let command = native_field(request, "command")?; - let kind = registry::class(py, RecordId::DbtInteropCommand)?; + let kind = record_class(py, RecordId::DbtInteropCommand)?; if !command.eq(native_field(&kind, "RUN")?)? && !command.eq(native_field(&kind, "BUILD")?)? { return Err(PyErr::from_value( registry::error(py, ErrorId::DbtInteropArgument)?.call1((format!( @@ -120,22 +127,24 @@ fn execute_dbt_interop_from_project(request: &Bound<'_, PyAny>) -> PyResult let (invocation, manifest, compiled) = prepare(request)?; let resolution = resolve_plan(request, &invocation, &compiled, &manifest)?; let plan = native_field(&resolution, "plan")?; - let merged = native( - py, - NativeAttr::BuildMergedDbtExecutionArgv, - &[ - ("command", command), - ("options", native_field(&invocation, "dbt_options")?), - ( - "routed_args", - native_field(&invocation, "routed")?.getattr("dbt_args")?, - ), - ("plan", plan.clone()), - ], - )?; - native( + let merged = build_merged_dbt_execution_argv( py, - NativeAttr::WriteDbtExecutionPlanText, + Some(keywords( + py, + &[ + ("command", command), + ("options", native_field(&invocation, "dbt_options")?), + ( + "routed_args", + native_field(&invocation, "routed")?.getattr("dbt_args")?, + ), + ("plan", plan.clone()), + ], + )?), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; + call_with( + ®istry::native(py, NativeAttr::WriteDbtExecutionPlanText)?, &[ ("request", request.clone()), ("invocation", invocation.clone()), @@ -143,40 +152,35 @@ fn execute_dbt_interop_from_project(request: &Bound<'_, PyAny>) -> PyResult ("merged_dbt_argv", merged.clone()), ], )?; - let execution = native( - py, - NativeAttr::ExecuteDbtWithoutStateTracking, + let execution = call_with( + ®istry::native(py, NativeAttr::ExecuteDbtWithoutStateTracking)?, &[ ("request", request.clone()), ("invocation", invocation.clone()), ("merged_dbt_argv", merged), ], )?; - native( - py, - NativeAttr::WriteDbtExecutionSummary, + call_with( + ®istry::native(py, NativeAttr::WriteDbtExecutionSummary)?, &[ ("request", request.clone()), ("invocation", invocation.clone()), ("execution", execution.clone()), ], )?; - let failed = native( - py, - NativeAttr::BuildFailedSqlbuildModelNames, - &[ - ("graph", native_field(&resolution, "graph")?), - ("manifest", manifest.clone()), - ("node_results", native_field(&execution, "node_results")?), - ], - )?; + let failed = build_failed_sqlbuild_model_names( + &native_field(&resolution, "graph")? + .cast_into::()?, + &manifest.clone(), + &native_field(&execution, "node_results")?, + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; let code: i64 = native_field(&execution, "returncode")?.extract()?; if code != 0 && !native_field(&execution, "node_results")?.is_truthy()? { return Ok(code); } - let output = native( - py, - NativeAttr::ResolveSqlbuildExecutionPlanOutput, + let output = call_with( + ®istry::native(py, NativeAttr::ResolveSqlbuildExecutionPlanOutput)?, &[ ("request", request.clone()), ("invocation", invocation.clone()), @@ -191,21 +195,17 @@ fn execute_dbt_interop_from_project(request: &Bound<'_, PyAny>) -> PyResult return Ok(code); } Some("No SQLBuild work selected.") - } else if !native( - py, - NativeAttr::PlanHasExecutableWork, - &[("plan", output.clone())], - )? - .is_truthy()? + } else if !plan_has_executable_work(py, Some(keywords(py, &[("plan", output.clone())])?)) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))? + .is_truthy()? { Some("Skipping SQLBuild: no executable work selected.") } else { None }; if let Some(message) = skip { - native( - py, - NativeAttr::WriteSqlbuildSkipNotice, + call_with( + ®istry::native(py, NativeAttr::WriteSqlbuildSkipNotice)?, &[ ("request", request.clone()), ("invocation", invocation), @@ -214,25 +214,29 @@ fn execute_dbt_interop_from_project(request: &Bound<'_, PyAny>) -> PyResult )?; return Ok(code); } - let connection = native( + let connection = resolve_dbt_connection_config( py, - NativeAttr::ResolveDbtConnectionConfig, - &[ - ("request", request.clone()), - ("invocation", invocation.clone()), - ("compiled", compiled.clone()), - ], + Some(keywords( + py, + &[ + ("request", request.clone()), + ("invocation", invocation.clone()), + ("compiled", compiled.clone()), + ], + )?), )?; - let result = native( + let result = run_dbt_sqlbuild_work( py, - NativeAttr::RunDbtSqlbuildWork, - &[ - ("request", request.clone()), - ("invocation", invocation), - ("compiled", compiled), - ("plan_output", output), - ("connection_config", connection), - ], + Some(keywords( + py, + &[ + ("request", request.clone()), + ("invocation", invocation), + ("compiled", compiled), + ("plan_output", output), + ("connection_config", connection), + ], + )?), )?; Ok(code.max(result.extract()?)) } @@ -240,17 +244,18 @@ fn execute_dbt_interop_from_project(request: &Bound<'_, PyAny>) -> PyResult /// Build an interop plan from actual project files, dbt artifacts and warehouse facts. #[pyfunction] #[pyo3(signature = (**options))] -fn plan_dbt_interop_from_project<'py>( +pub(crate) fn plan_dbt_interop_from_project<'py>( py: Python<'py>, options: Option>, ) -> PyResult> { let args = KeywordArguments::read(py, "plan_dbt_interop_from_project", options, PLAN_OPTIONS)?; - let request = call_with( - ®istry::class(py, RecordId::DbtInteropExecutionRequest)?, + let request = new_record_with( + py, + RecordId::DbtInteropExecutionRequest, &[ ( "command", - registry::class(py, RecordId::DbtInteropCommand)?.getattr("PLAN")?, + enum_member(py, RecordId::DbtInteropCommand, "PLAN")?, ), ("project_dir", args.value("project_dir")?), ("args", args.value("args")?), @@ -278,21 +283,24 @@ fn plan_dbt_interop_from_project<'py>( let connection = if stream.is_none() { none(py) } else { - native( + ConnectionProgressReporter::new( py, - NativeAttr::ConnectionProgressReporter, - &[ - ("adapter_name", native_field(&compiled, "adapter_name")?), - ("stream", stream), - ("use_color", args.value_or_false("use_color")?), - ], - )? + Some(keywords( + py, + &[ + ("adapter_name", native_field(&compiled, "adapter_name")?), + ("stream", stream), + ("use_color", args.value_or_false("use_color")?), + ], + )?), + ) + .and_then(|value| pyo3::Bound::new(py, value)) + .map(pyo3::Bound::into_any)? }; let start = Instant::now(); let resolution = resolve_plan(&request, &invocation, &compiled, &manifest)?; - let plan = native( - py, - NativeAttr::AttachSqlbuildPlanOutput, + let plan = call_with( + ®istry::native(py, NativeAttr::AttachSqlbuildPlanOutput)?, &[ ("plan", native_field(&resolution, "plan")?), ("project_dir", args.value("project_dir")?), @@ -322,7 +330,10 @@ fn plan_dbt_interop_from_project<'py>( /// Run dbt debug using SQLBuild's resolved dbt configuration. #[pyfunction] #[pyo3(signature = (**options))] -fn debug_dbt_from_project(py: Python<'_>, options: Option>) -> PyResult { +pub(crate) fn debug_dbt_from_project( + py: Python<'_>, + options: Option>, +) -> PyResult { let args = KeywordArguments::read(py, "debug_dbt_from_project", options, DEBUG_OPTIONS)?; let discovered = discover_project_inputs( py, @@ -337,25 +348,28 @@ fn debug_dbt_from_project(py: Python<'_>, options: Option>) -> .map(|arg| arg?.extract()) .collect::>()?; let overrides = config_overrides(py, &dbt_args)?; - let resolved = native( + let resolved = resolve_dbt_config( py, - NativeAttr::ResolveDbtConfig, - &[ - ("project_root", args.value("project_dir")?), - ( - "config", - native_field(&discovered, "project_config")?.getattr("dbt")?, - ), - ( - "local_config", - native_field(&discovered, "local_config")?.getattr("dbt")?, - ), - ("overrides", overrides), - ("require_project_dir", boolean(py, true)), - ], + Some(keywords( + py, + &[ + ("project_root", args.value("project_dir")?), + ( + "config", + native_field(&discovered, "project_config")?.getattr("dbt")?, + ), + ( + "local_config", + native_field(&discovered, "local_config")?.getattr("dbt")?, + ), + ("overrides", overrides), + ("require_project_dir", boolean(py, true)), + ], + )?), )?; - let dbt_options = call_with( - ®istry::class(py, RecordId::DbtCliOptions)?, + let dbt_options = new_record_with( + py, + RecordId::DbtCliOptions, &[ ("project_dir", native_field(&resolved, "project_dir")?), ("profiles_dir", native_field(&resolved, "profiles_dir")?), @@ -364,10 +378,14 @@ fn debug_dbt_from_project(py: Python<'_>, options: Option>) -> )?; let mut runner = args.value("dbt_runner")?; if !runner.is_truthy()? { - runner = call_with( - ®istry::native(py, NativeAttr::DbtRunner)?, - &[("dbt_executable", args.value("dbt_executable")?)], - )?; + runner = pyo3::Bound::new( + py, + DbtRunner::new( + args.value("dbt_executable")?.extract::>()?, + None, + ), + ) + .map(pyo3::Bound::into_any)?; } let argv: Vec = args.value("args")?.extract()?; let result = call_with( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/pipeline_phases.rs b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/pipeline_phases.rs index 4bf18bb67a..4450fc5cda 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/pipeline_phases.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/pipeline_phases.rs @@ -5,14 +5,20 @@ use pyo3::types::PyDict; use pyo3::{pyfunction, wrap_pyfunction}; use crate::bindings::_helpers::build_command::support::{boolean, emit, none, text, tuple}; -use crate::bindings::_helpers::compile_driver::python_calls::{ - KeywordArguments, call_with, imported, keywords, -}; +use crate::bindings::_helpers::cli::progress_reporters::ConnectionProgressReporter; +use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, keywords}; +use crate::bindings::_helpers::contracts::spec_resolution::resolve_target_config; +use crate::bindings::_helpers::dbt_interop::execution::render_dbt_execution_summary_footer; use crate::bindings::_helpers::dbt_interop::pipeline_prologue::progress; +use crate::bindings::_helpers::dbt_interop::plan::render_dbt_interop_plan; +use crate::bindings::_helpers::dbt_interop::plan_output::build_sqlbuild_plan_output; use crate::bindings::_helpers::dbt_interop::plan_output::{connection_config, connection_hooks}; +use crate::bindings::_helpers::dbt_interop::runtime_execution::execute_dbt_commands; +use crate::bindings::_helpers::dbt_interop::sqlbuild_work::execute_dbt_sqlbuild_work; +use crate::bindings::_helpers::records::construct::{new_record, new_record_with}; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::fields::replaced; +use crate::bindings::_helpers::records::ids::RecordId; const OPTIONS: (&[&str], &[&str]) = ( &["request", "invocation"], @@ -32,8 +38,9 @@ const OPTIONS: (&[&str], &[&str]) = ( fn environment<'py>(args: &KeywordArguments<'py>) -> PyResult> { let request = args.value("request")?; let compiled = args.value("compiled")?; - call_with( - ®istry::class(request.py(), RecordId::DbtPlanEnvironment)?, + new_record_with( + request.py(), + RecordId::DbtPlanEnvironment, &[ ("project_dir", native_field(&request, "project_dir")?), ( @@ -50,7 +57,7 @@ fn environment<'py>(args: &KeywordArguments<'py>) -> PyResult> /// Resolve the selected SQLBuild work's effective connection configuration. #[pyfunction] #[pyo3(signature = (**options))] -fn resolve_dbt_connection_config<'py>( +pub(crate) fn resolve_dbt_connection_config<'py>( py: Python<'py>, options: Option>, ) -> PyResult> { @@ -61,7 +68,7 @@ fn resolve_dbt_connection_config<'py>( /// Render the merged selection unless the command is producing JSON. #[pyfunction] #[pyo3(signature = (**options))] -fn write_dbt_execution_plan_text( +pub(crate) fn write_dbt_execution_plan_text( py: Python<'_>, options: Option>, ) -> PyResult<()> { @@ -75,15 +82,12 @@ fn write_dbt_execution_plan_text( let display = if merged.is_none() { plan } else { - imported(py, "dataclasses", "replace")?.call( - (plan,), - Some(&keywords( - py, - &[ - ("dbt_command_argv", merged), - ("supplemental_dbt_command_argvs", tuple(py, &[])?), - ], - )?), + replaced( + &plan, + &[ + ("dbt_command_argv", merged), + ("supplemental_dbt_command_argvs", tuple(py, &[])?), + ], )? }; let limit = if native_field(&request, "verbose")?.is_truthy()? { @@ -91,21 +95,26 @@ fn write_dbt_execution_plan_text( } else { pyo3::types::PyInt::new(py, super::constants::PLAN_DISPLAY_LIMIT).into_any() }; - let rendered = call_with( - ®istry::native(py, NativeAttr::RenderDbtInteropPlan)?, - &[ - ("plan", display), - ("json_output", boolean(py, false)), - ("use_color", native_field(&request, "use_color")?), - ( - "display_options", - call_with( - ®istry::class(py, RecordId::DisplayOptions)?, - &[("max_entries_per_section", limit)], - )?, - ), - ], - )?; + let rendered = render_dbt_interop_plan( + py, + Some(keywords( + py, + &[ + ("plan", display), + ("json_output", boolean(py, false)), + ("use_color", native_field(&request, "use_color")?), + ( + "display_options", + new_record_with( + py, + RecordId::DisplayOptions, + &[("max_entries_per_section", limit)], + )?, + ), + ], + )?), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; emit( &args.value("invocation")?.getattr("output_stream")?, &format!("{}\n\n", rendered.extract::()?), @@ -115,52 +124,55 @@ fn write_dbt_execution_plan_text( /// Execute dbt without introducing any SQLBuild state callbacks. #[pyfunction] #[pyo3(signature = (**options))] -fn execute_dbt_without_state_tracking<'py>( +pub(crate) fn execute_dbt_without_state_tracking<'py>( py: Python<'py>, options: Option>, ) -> PyResult> { let args = KeywordArguments::read(py, "execute_dbt_without_state_tracking", options, OPTIONS)?; let request = args.value("request")?; let invocation = args.value("invocation")?; - call_with( - ®istry::native(py, NativeAttr::ExecuteDbtCommands)?, - &[ - ("runner", native_field(&invocation, "runner")?), - ("options", native_field(&invocation, "dbt_options")?), - ("merged_argv", args.value("merged_dbt_argv")?), - ( - "progress_stream", - native_field(&invocation, "output_stream")?, - ), - ( - "stdout_stream", - native_field(&invocation, "dbt_output_stream")?, - ), - ("use_color", native_field(&request, "use_color")?), - ( - "skip_message", - text(py, "Skipping dbt: no dbt work selected."), - ), - ("on_progress", native_field(&request, "on_progress")?), - ], + execute_dbt_commands( + py, + Some(keywords( + py, + &[ + ("runner", native_field(&invocation, "runner")?), + ("options", native_field(&invocation, "dbt_options")?), + ("merged_argv", args.value("merged_dbt_argv")?), + ( + "progress_stream", + native_field(&invocation, "output_stream")?, + ), + ( + "stdout_stream", + native_field(&invocation, "dbt_output_stream")?, + ), + ("use_color", native_field(&request, "use_color")?), + ( + "skip_message", + text(py, "Skipping dbt: no dbt work selected."), + ), + ("on_progress", native_field(&request, "on_progress")?), + ], + )?), ) } /// Write the summary of actual dbt execution events. #[pyfunction] #[pyo3(signature = (**options))] -fn write_dbt_execution_summary(py: Python<'_>, options: Option>) -> PyResult<()> { +pub(crate) fn write_dbt_execution_summary( + py: Python<'_>, + options: Option>, +) -> PyResult<()> { let args = KeywordArguments::read(py, "write_dbt_execution_summary", options, OPTIONS)?; - let footer = call_with( - ®istry::native(py, NativeAttr::RenderDbtExecutionSummaryFooter)?, - &[ - ( - "node_results", - args.value("execution")?.getattr("node_results")?, - ), - ("use_color", args.value("request")?.getattr("use_color")?), - ], - )?; + let footer = render_dbt_execution_summary_footer( + &args.value("execution")?.getattr("node_results")?, + args.value("request")? + .getattr("use_color")? + .extract::()?, + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; if !footer.is_none() { emit( &args.value("invocation")?.getattr("output_stream")?, @@ -173,7 +185,7 @@ fn write_dbt_execution_summary(py: Python<'_>, options: Option /// Plan SQLBuild work after dbt using actual dbt failures as external blockers. #[pyfunction] #[pyo3(signature = (**options))] -fn resolve_sqlbuild_execution_plan_output<'py>( +pub(crate) fn resolve_sqlbuild_execution_plan_output<'py>( py: Python<'py>, options: Option>, ) -> PyResult> { @@ -194,20 +206,26 @@ fn resolve_sqlbuild_execution_plan_output<'py>( let invocation = args.value("invocation")?; let stream = native_field(&invocation, "output_stream")?; emit(&stream, "\n")?; - let reporter = call_with( - ®istry::native(py, NativeAttr::ConnectionProgressReporter)?, - &[ - ( - "adapter_name", - args.value("compiled")?.getattr("adapter_name")?, - ), - ("stream", stream), - ("blank_line_after_complete", boolean(py, true)), - ("use_color", args.value("request")?.getattr("use_color")?), - ], - )?; - let request = call_with( - ®istry::class(py, RecordId::DbtSqlbuildPlanRequest)?, + let reporter = ConnectionProgressReporter::new( + py, + Some(keywords( + py, + &[ + ( + "adapter_name", + args.value("compiled")?.getattr("adapter_name")?, + ), + ("stream", stream), + ("blank_line_after_complete", boolean(py, true)), + ("use_color", args.value("request")?.getattr("use_color")?), + ], + )?), + ) + .and_then(|value| pyo3::Bound::new(py, value)) + .map(pyo3::Bound::into_any)?; + let request = new_record_with( + py, + RecordId::DbtSqlbuildPlanRequest, &[ ("selected_model_names", selected), ( @@ -220,13 +238,16 @@ fn resolve_sqlbuild_execution_plan_output<'py>( ), ], )?; - call_with( - ®istry::native(py, NativeAttr::BuildSqlbuildPlanOutput)?, - &[ - ("environment", environment(&args)?), - ("request", request), - ("hooks", connection_hooks(&reporter, none(py))?), - ], + build_sqlbuild_plan_output( + py, + Some(keywords( + py, + &[ + ("environment", environment(&args)?), + ("request", request), + ("hooks", connection_hooks(&reporter, none(py))?), + ], + )?), ) } @@ -236,18 +257,13 @@ fn execution_limits<'py>( ) -> PyResult> { let py = discovered.py(); if target.is_none() { - return registry::class(py, RecordId::ExecutionLimitsConfig)?.call0(); + return new_record(py, RecordId::ExecutionLimitsConfig, (), None); } - call_with( - ®istry::native(py, NativeAttr::ResolveTargetConfig)?, - &[ - ( - "project_config", - native_field(discovered, "project_config")?, - ), - ("local_config", native_field(discovered, "local_config")?), - ("target_name", target), - ], + resolve_target_config( + py, + &native_field(discovered, "project_config")?, + &native_field(discovered, "local_config")?, + &target, )? .getattr("execution_limits") } @@ -255,7 +271,7 @@ fn execution_limits<'py>( /// Execute the planned SQLBuild phase with the selected target's execution limits. #[pyfunction] #[pyo3(signature = (**options))] -fn run_dbt_sqlbuild_work<'py>( +pub(crate) fn run_dbt_sqlbuild_work<'py>( py: Python<'py>, options: Option>, ) -> PyResult> { @@ -265,8 +281,9 @@ fn run_dbt_sqlbuild_work<'py>( let compiled = args.value("compiled")?; let project = native_field(&compiled, "project")?; let discovered = native_field(&invocation, "discovered_inputs")?; - let context = call_with( - ®istry::class(py, RecordId::DbtSqlbuildWorkContext)?, + let context = new_record_with( + py, + RecordId::DbtSqlbuildWorkContext, &[ ("plan_output", args.value("plan_output")?), ("connection_config", args.value("connection_config")?), @@ -287,23 +304,30 @@ fn run_dbt_sqlbuild_work<'py>( ), ], )?; - call_with( - ®istry::native(py, NativeAttr::ExecuteDbtSqlbuildWork)?, - &[ - ("context", context), - ("command", native_field(&request, "command")?), - ("project", project), - ("project_dir", native_field(&request, "project_dir")?), - ("fail_fast", native_field(&request, "fail_fast")?), - ("verbose", native_field(&request, "verbose")?), - ], + execute_dbt_sqlbuild_work( + py, + Some(keywords( + py, + &[ + ("context", context), + ("command", native_field(&request, "command")?), + ("project", project), + ("project_dir", native_field(&request, "project_dir")?), + ("fail_fast", native_field(&request, "fail_fast")?), + ("verbose", native_field(&request, "verbose")?), + ], + )?), ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py)) } /// Report a SQLBuild skip transition without writing machine output to stdout. #[pyfunction] #[pyo3(signature = (**options))] -fn write_sqlbuild_skip_notice(py: Python<'_>, options: Option>) -> PyResult<()> { +pub(crate) fn write_sqlbuild_skip_notice( + py: Python<'_>, + options: Option>, +) -> PyResult<()> { let args = KeywordArguments::read(py, "write_sqlbuild_skip_notice", options, OPTIONS)?; emit(&args.value("invocation")?.getattr("output_stream")?, "\n")?; progress( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/pipeline_prologue.rs b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/pipeline_prologue.rs index 207cf71e76..7e9690b162 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/pipeline_prologue.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/pipeline_prologue.rs @@ -4,6 +4,15 @@ use crate::bindings::_helpers::compile_driver::discovery::discover_project_input use crate::bindings::_helpers::compile_driver::project::build_compiled_project; use crate::bindings::_helpers::compile_driver::python_calls::keywords; use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; +use crate::bindings::_helpers::contracts::spec_resolution::resolve_effective_adapter_name; +use crate::bindings::_helpers::dbt_interop::cli::resolve_dbt_executable; +use crate::bindings::_helpers::dbt_interop::cli::route_dbt_interop_args; +use crate::bindings::_helpers::dbt_interop::graph::build_dbt_combined_graph; +use crate::bindings::_helpers::dbt_interop::manifest::load_dbt_manifest_index; +use crate::bindings::_helpers::dbt_interop::orchestration::plan_dbt_interop_command; +use crate::bindings::_helpers::dbt_interop::plan_output::dbt_failure_detail; +use crate::bindings::_helpers::dbt_interop::runner::DbtRunner; +use crate::bindings::_helpers::records::construct::new_record_with; use pyo3::prelude::{ Bound, PyAny, PyAnyMethods, PyErr, PyModule, PyModuleMethods, PyResult, Python, }; @@ -14,7 +23,7 @@ use crate::bindings::_helpers::dbt_interop::runtime_config::{ interop_adapter, manifest_path, parse_dbt_execution_args, plan_options, vars_mapping, }; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr, RecordId}; +use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; use pyo3::{pyfunction, wrap_pyfunction}; use std::time::Instant; @@ -68,7 +77,7 @@ pub(crate) fn resolve_dbt_execution_invocation<'py>( let py = request.py(); let mut executable = native_field(request, "dbt_executable")?; if !executable.is_truthy()? { - executable = registry::native(py, NativeAttr::ResolveDbtExecutable)?.call0()?; + executable = pyo3::IntoPyObjectExt::into_bound_py_any(resolve_dbt_executable(), py)?; } let mut stream = native_field(request, "progress_stream")?; if !stream.is_truthy()? { @@ -76,10 +85,7 @@ pub(crate) fn resolve_dbt_execution_invocation<'py>( } let command = native_field(request, "command")?; let parsed = parse_dbt_execution_args(&command, &native_field(request, "args")?)?; - let routed = call_with( - ®istry::native(py, NativeAttr::RouteDbtInteropArgs)?, - &[("command", command), ("parsed", parsed)], - )?; + let routed = route_dbt_interop_args(py, &command, &parsed)?; let project_dir = native_field(request, "project_dir")?; let discovered = discover_project_inputs( py, @@ -99,14 +105,16 @@ pub(crate) fn resolve_dbt_execution_invocation<'py>( .into_any(); let mut runner = native_field(request, "dbt_runner")?; if !runner.is_truthy()? { - runner = call_with( - ®istry::native(py, NativeAttr::DbtRunner)?, - &[("dbt_executable", executable.clone())], - )?; + runner = pyo3::Bound::new( + py, + DbtRunner::new(executable.clone().extract::>()?, None), + ) + .map(pyo3::Bound::into_any)?; } let dbt_stream = native_field(request, "dbt_stdout_stream")?; - call_with( - ®istry::class(py, RecordId::DbtInteropInvocation)?, + new_record_with( + py, + RecordId::DbtInteropInvocation, &[ ("dbt_executable", executable), ("output_stream", stream.clone()), @@ -134,7 +142,7 @@ pub(crate) fn resolve_dbt_execution_invocation<'py>( /// Compile dbt and load the manifest after a successful subprocess result. #[pyfunction] #[pyo3(signature = (**options))] -fn load_compiled_dbt_manifest<'py>( +pub(crate) fn load_compiled_dbt_manifest<'py>( py: Python<'py>, options: Option>, ) -> PyResult> { @@ -150,7 +158,7 @@ fn load_compiled_dbt_manifest<'py>( ], )?; if native_field(&result, "returncode")?.extract::()? != 0 { - let detail = registry::native(py, NativeAttr::DbtFailureDetail)?.call1((result,))?; + let detail = dbt_failure_detail(&result)?; let error = registry::error(py, ErrorId::DbtInteropRuntime)?.call( ("dbt compile failed",), Some(&keywords(py, &[("help", detail)])?), @@ -161,10 +169,7 @@ fn load_compiled_dbt_manifest<'py>( let start = Instant::now(); progress(&callback, "Loading dbt manifest...")?; let path = manifest_path(&args.value("dbt_options")?)?; - let manifest = call_with( - ®istry::native(py, NativeAttr::LoadDbtManifestIndex)?, - &[("manifest_path", path)], - )?; + let manifest = load_dbt_manifest_index(py, path)?; completed(&callback, "Loaded dbt manifest.", start)?; Ok(manifest) } @@ -172,7 +177,7 @@ fn load_compiled_dbt_manifest<'py>( /// Compile SQLBuild models against dbt references using the existing adapter boundary. #[pyfunction] #[pyo3(signature = (**options))] -fn compile_dbt_interop_project<'py>( +pub(crate) fn compile_dbt_interop_project<'py>( py: Python<'py>, options: Option>, ) -> PyResult> { @@ -181,15 +186,9 @@ fn compile_dbt_interop_project<'py>( let start = Instant::now(); progress(&callback, "Compiling SQLBuild project...")?; let discovered = args.value("discovered_inputs")?; - let name = call_with( - ®istry::native(py, NativeAttr::ResolveEffectiveAdapterName)?, - &[ - ( - "project_config", - native_field(&discovered, "project_config")?, - ), - ("local_config", native_field(&discovered, "local_config")?), - ], + let name = resolve_effective_adapter_name( + &native_field(&discovered, "project_config")?, + &native_field(&discovered, "local_config")?, )?; let adapter = interop_adapter(&name, &args.value("project_dir")?)?; let resolver = Bound::new( @@ -211,8 +210,9 @@ fn compile_dbt_interop_project<'py>( )?), )?; completed(&callback, "Compiled SQLBuild project.", start)?; - call_with( - ®istry::class(py, RecordId::DbtInteropCompiledProject)?, + new_record_with( + py, + RecordId::DbtInteropCompiledProject, &[ ("adapter_name", name), ("adapter", adapter), @@ -224,7 +224,7 @@ fn compile_dbt_interop_project<'py>( /// Build the combined graph and resolve its dbt and SQLBuild selections. #[pyfunction] #[pyo3(signature = (**options))] -fn resolve_dbt_interop_plan<'py>( +pub(crate) fn resolve_dbt_interop_plan<'py>( py: Python<'py>, options: Option>, ) -> PyResult> { @@ -233,21 +233,17 @@ fn resolve_dbt_interop_plan<'py>( let start = Instant::now(); progress(&callback, "Building dbt interop graph...")?; let project = args.value("compiled")?.getattr("project")?; - let graph = call_with( - ®istry::native(py, NativeAttr::BuildDbtCombinedGraph)?, - &[ - ("manifest", args.value("manifest")?), - ("project", project.clone()), - ], - )?; + let graph = build_dbt_combined_graph(py, args.value("manifest")?, project.clone()) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; completed(&callback, "Built dbt interop graph.", start)?; let start = Instant::now(); progress(&callback, "Resolving dbt and SQLBuild selection...")?; let invocation = args.value("invocation")?; let routed = native_field(&invocation, "routed")?; let tuple = py.import("builtins")?.getattr("tuple")?; - let command_args = call_with( - ®istry::class(py, RecordId::DbtInteropCommandArgs)?, + let command_args = new_record_with( + py, + RecordId::DbtInteropCommandArgs, &[ ("select", tuple.call1((native_field(&routed, "select")?,))?), ( @@ -269,21 +265,25 @@ fn resolve_dbt_interop_plan<'py>( ("sqlbuild_executable", args.value("sqlbuild_executable")?), ], )?; - let plan = call_with( - ®istry::native(py, NativeAttr::PlanDbtInteropCommand)?, - &[ - ("command", args.value("command")?), - ("project", project), - ("manifest", args.value("manifest")?), - ("graph", graph.clone()), - ("dbt_runner", native_field(&invocation, "runner")?), - ("dbt_options", native_field(&invocation, "dbt_options")?), - ("command_args", command_args), - ], + let plan = plan_dbt_interop_command( + py, + Some(keywords( + py, + &[ + ("command", args.value("command")?), + ("project", project), + ("manifest", args.value("manifest")?), + ("graph", graph.clone()), + ("dbt_runner", native_field(&invocation, "runner")?), + ("dbt_options", native_field(&invocation, "dbt_options")?), + ("command_args", command_args), + ], + )?), )?; completed(&callback, "Resolved dbt and SQLBuild selection.", start)?; - call_with( - ®istry::class(py, RecordId::DbtInteropPlanResolution)?, + new_record_with( + py, + RecordId::DbtInteropPlanResolution, &[("graph", graph), ("plan", plan)], ) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/plan.rs b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/plan.rs index 07d12ad906..31e7cce43b 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/plan.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/plan.rs @@ -8,10 +8,13 @@ use sqlbuild_core::cli_text::main::format_dbt_interop_plan::format_dbt_interop_p use sqlbuild_core::cli_text::main::format_dbt_interop_plan_json::format_dbt_interop_plan_json; use sqlbuild_core::cli_text::models::{DbtInteropPlanText, DbtLsNodeText, DisplayLimit}; -use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; +use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, keywords}; +use crate::bindings::_helpers::plan_output::plan_text::format_plan; +use crate::bindings::_helpers::records::construct::{ + enum_from_value, enum_member, new_record, new_record_with, +}; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::ids::RecordId; const BUILD_OPTIONS: (&[&str], &[&str]) = ( &[ @@ -121,7 +124,7 @@ fn skip_reason<'py>(py: Python<'py>, has_work: bool, member: &str) -> PyResult( let arguments: KeywordArguments<'py> = KeywordArguments::read(py, "build_dbt_interop_plan", options, BUILD_OPTIONS)?; let command: Bound<'py, PyAny> = - registry::class(py, RecordId::DbtInteropCommand)?.call1((arguments.value("command")?,))?; + enum_from_value(py, RecordId::DbtInteropCommand, arguments.value("command")?)?; let nodes: Bound<'py, PyAny> = tuple_of(arguments.value("dbt_ls_nodes")?)?; let mut selected: Vec = Vec::new(); for node in nodes.try_iter()? { @@ -149,8 +152,9 @@ pub(crate) fn build_dbt_interop_plan<'py>( strings(&empty_or(arguments.value("warnings")?))?, dbt_has_work || sqlbuild_has_work, ); - call_with( - ®istry::class(py, RecordId::DbtInteropPlan)?, + new_record_with( + py, + RecordId::DbtInteropPlan, &[ ("command", command), ( @@ -202,7 +206,10 @@ fn display_limit(options: &Bound<'_, PyAny>) -> PyResult { /// Render a dbt interop plan in the requested CLI output format. #[pyfunction] #[pyo3(signature = (**options))] -fn render_dbt_interop_plan(py: Python<'_>, options: Option>) -> PyResult { +pub(crate) fn render_dbt_interop_plan( + py: Python<'_>, + options: Option>, +) -> PyResult { let arguments: KeywordArguments<'_> = KeywordArguments::read(py, "render_dbt_interop_plan", options, RENDER_OPTIONS)?; let plan: Bound<'_, PyAny> = arguments.value("plan")?; @@ -212,7 +219,7 @@ fn render_dbt_interop_plan(py: Python<'_>, options: Option>) - } let mut display: Bound<'_, PyAny> = arguments.value("display_options")?; if !display.is_truthy()? { - display = registry::class(py, RecordId::DisplayOptions)?.call0()?; + display = new_record(py, RecordId::DisplayOptions, (), None)?; } let use_color: bool = arguments.flag("use_color")?; let plan_output: Bound<'_, PyAny> = native_field(&plan, "sqlbuild_plan_output")?; @@ -221,17 +228,21 @@ fn render_dbt_interop_plan(py: Python<'_>, options: Option>) - None } else { Some( - call_with( - ®istry::native(py, NativeAttr::FormatPlan)?, - &[ - ("plan", plan_output), - ( - "use_color", - PyBool::new(py, use_color).to_owned().into_any(), - ), - ("display_options", display.clone()), - ], - )? + format_plan( + py, + Some(keywords( + py, + &[ + ("plan", plan_output), + ( + "use_color", + PyBool::new(py, use_color).to_owned().into_any(), + ), + ("display_options", display.clone()), + ], + )?), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))? .extract()?, ) }; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/plan_output.rs b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/plan_output.rs index 7af4501e51..ce896e3a80 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/plan_output.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/plan_output.rs @@ -8,14 +8,21 @@ use crate::bindings::_helpers::build_command::support::{boolean, keyword_relay, use crate::bindings::_helpers::command_runtime::connection::{ anchor_duckdb_database, with_dbt_profile, }; +use crate::bindings::_helpers::command_runtime::warehouses::apply_command_warehouse; +use crate::bindings::_helpers::compile_attachment::effective_config::build_effective_connection_config; +use crate::bindings::_helpers::compile_driver::deferral::resolve_model_deferral_inputs; use crate::bindings::_helpers::compile_driver::plan_result::connection_with_hooks; use crate::bindings::_helpers::compile_driver::python_calls::{ - KeywordArguments, call_with, imported, keywords, + KeywordArguments, call_with, keywords, }; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; +use crate::bindings::_helpers::planner_entries::display_plan::build_display_only_sqlbuild_plan; +use crate::bindings::_helpers::planner_entries::execution_plan::build_execution_plan; +use crate::bindings::_helpers::records::construct::{new_record, new_record_with}; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr, RecordId}; +use crate::bindings::_helpers::records::fields::replaced; +use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; const BUILD_OPTIONS: (&[&str], &[&str]) = (&["environment", "request", "hooks"], &[]); @@ -37,10 +44,7 @@ pub(crate) fn connection_config<'py>( ) -> PyResult> { let py = environment.py(); let discovered = native_field(environment, "discovered_inputs")?; - let raw = call_with( - ®istry::native(py, NativeAttr::BuildEffectiveConnectionConfig)?, - &[("discovered_inputs", discovered.clone())], - )?; + let raw = build_effective_connection_config(&discovered.clone(), None, None)?; let raw: Bound<'_, PyDict> = raw.cast_into()?; let project_dir = native_field(environment, "project_dir")?; let adapter_name = native_field(environment, "adapter_name")?; @@ -55,15 +59,18 @@ pub(crate) fn connection_config<'py>( (&project_dir, &adapter_name.str()?.extract::()?), )?; let config = config.into_any(); - call_with( - ®istry::native(py, NativeAttr::ApplyCommandWarehouse)?, - &[ - ("config", config), - ("adapter_name", native_field(environment, "adapter_name")?), - ("discovered_inputs", discovered), - ("project_dir", native_field(environment, "project_dir")?), - ("selected_target", none(py)), - ], + apply_command_warehouse( + py, + Some(keywords( + py, + &[ + ("config", config), + ("adapter_name", native_field(environment, "adapter_name")?), + ("discovered_inputs", discovered), + ("project_dir", native_field(environment, "project_dir")?), + ("selected_target", none(py)), + ], + )?), ) } @@ -94,7 +101,7 @@ pub(crate) fn connection_hooks<'py>( pairs.push(( name, keyword_relay( - reporter.getattr(name)?, + native_field(reporter, name)?, &["connection_count", "elapsed_seconds"], )?, )); @@ -115,7 +122,7 @@ fn open_connection<'py>( native_field(hooks, "on_connection_start")?, )]; for name in ["on_connection_complete", "on_connection_error"] { - let callback = hooks.getattr(name)?; + let callback = native_field(hooks, name)?; pairs.push(( name, if callback.is_none() { @@ -148,21 +155,25 @@ pub(crate) fn build_sqlbuild_plan_output<'py>( let connection = open_connection(&environment, connection_config(&environment)?, &hooks)?; let planned = (|| { let args = native_field(&request, "sqlbuild_args")?; - let deferral = call_with( - ®istry::native(py, NativeAttr::ResolveModelDeferralInputs)?, - &[ - ("project", native_field(&environment, "project")?), - ( - "discovered_inputs", - native_field(&environment, "discovered_inputs")?, - ), - ("adapter", native_field(&environment, "adapter")?), - ("connection", connection.clone()), - ("defer_to", flag_value(&args, "--defer-to")?), - ], + let deferral = resolve_model_deferral_inputs( + py, + Some(keywords( + py, + &[ + ("project", native_field(&environment, "project")?), + ( + "discovered_inputs", + native_field(&environment, "discovered_inputs")?, + ), + ("adapter", native_field(&environment, "adapter")?), + ("connection", connection.clone()), + ("defer_to", flag_value(&args, "--defer-to")?), + ], + )?), )?; - let cursor = call_with( - ®istry::class(py, RecordId::CursorOverrides)?, + let cursor = new_record_with( + py, + RecordId::CursorOverrides, &[ ("start_ts", flag_value(&args, "--start-cursor-ts")?), ("end_ts", flag_value(&args, "--end-cursor-ts")?), @@ -171,12 +182,14 @@ pub(crate) fn build_sqlbuild_plan_output<'py>( ], )?; let full_refresh = boolean(py, args.contains("--full-refresh")?); - let selection = call_with( - ®istry::class(py, RecordId::PlannerSelection)?, + let selection = new_record_with( + py, + RecordId::PlannerSelection, &[("select", selected.clone())], )?; - let overrides = call_with( - ®istry::class(py, RecordId::PlannerOverrides)?, + let overrides = new_record_with( + py, + RecordId::PlannerOverrides, &[ ("cursor_overrides", cursor), ("full_refresh", full_refresh.clone()), @@ -187,36 +200,36 @@ pub(crate) fn build_sqlbuild_plan_output<'py>( ], )?; let discovered = native_field(&environment, "discovered_inputs")?; - let result = call_with( - ®istry::native(py, NativeAttr::BuildExecutionPlan)?, - &[ - ("project", native_field(&environment, "project")?), - ("adapter", native_field(&environment, "adapter")?), - ("connection", connection.clone()), - ("selection", selection), - ("overrides", overrides), - ("deferral", deferral), - ( - "policies", - registry::class(py, RecordId::PlannerPolicies)?.call0()?, - ), - ("on_progress", native_field(&hooks, "on_progress")?), - ( - "project_config", - native_field(&discovered, "project_config")?, - ), - ("local_config", native_field(&discovered, "local_config")?), - ], + let result = build_execution_plan( + py, + Some(keywords( + py, + &[ + ("project", native_field(&environment, "project")?), + ("adapter", native_field(&environment, "adapter")?), + ("connection", connection.clone()), + ("selection", selection), + ("overrides", overrides), + ("deferral", deferral), + ( + "policies", + new_record(py, RecordId::PlannerPolicies, (), None)?, + ), + ("on_progress", native_field(&hooks, "on_progress")?), + ( + "project_config", + native_field(&discovered, "project_config")?, + ), + ("local_config", native_field(&discovered, "local_config")?), + ], + )?), ); match result { Err(error) if error.is_instance(py, ®istry::error(py, ErrorId::PlannerInput)?) => { - call_with( - ®istry::native(py, NativeAttr::BuildDisplayOnlySqlbuildPlan)?, - &[ - ("project", native_field(&environment, "project")?), - ("selected_model_names", selected), - ("full_refresh", full_refresh), - ], + build_display_only_sqlbuild_plan( + &native_field(&environment, "project")?, + &selected, + full_refresh.extract::()?, ) } result => result, @@ -231,15 +244,16 @@ pub(crate) fn build_sqlbuild_plan_output<'py>( /// Attach live SQLBuild planning when the selection contains SQLBuild models. #[pyfunction] #[pyo3(signature = (**options))] -fn attach_sqlbuild_plan_output<'py>( +pub(crate) fn attach_sqlbuild_plan_output<'py>( py: Python<'py>, options: Option>, ) -> PyResult> { let args = KeywordArguments::read(py, "attach_sqlbuild_plan_output", options, ATTACH_OPTIONS)?; let compiled = args.value("compiled")?; let plan = args.value("plan")?; - let environment = call_with( - ®istry::class(py, RecordId::DbtPlanEnvironment)?, + let environment = new_record_with( + py, + RecordId::DbtPlanEnvironment, &[ ("project_dir", args.value("project_dir")?), ("discovered_inputs", args.value("discovered_inputs")?), @@ -248,8 +262,9 @@ fn attach_sqlbuild_plan_output<'py>( ("adapter_name", native_field(&compiled, "adapter_name")?), ], )?; - let request = call_with( - ®istry::class(py, RecordId::DbtSqlbuildPlanRequest)?, + let request = new_record_with( + py, + RecordId::DbtSqlbuildPlanRequest, &[ ( "selected_model_names", @@ -278,15 +293,12 @@ fn attach_sqlbuild_plan_output<'py>( if output.is_none() { return Ok(plan); } - imported(py, "dataclasses", "replace")?.call( - (plan,), - Some(&keywords(py, &[("sqlbuild_plan_output", output)])?), - ) + replaced(&plan, &[("sqlbuild_plan_output", output)]) } /// Prefer stderr for dbt subprocess failure details, retaining Python whitespace semantics. #[pyfunction] -fn dbt_failure_detail<'py>(result: &Bound<'py, PyAny>) -> PyResult> { +pub(crate) fn dbt_failure_detail<'py>(result: &Bound<'py, PyAny>) -> PyResult> { let stderr = native_field(result, "stderr")?; let detail = if stderr.is_truthy()? { stderr diff --git a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/profile.rs b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/profile.rs index 1bb253c083..fd9599abdf 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/profile.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/profile.rs @@ -12,12 +12,12 @@ use sqlbuild_config::dbt_profile::models::{ ConnectionKey, ConnectionSource, DbtInitConfig, NormalizedProfile, ProfileEntry, ProfileValue, }; -use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; +use crate::bindings::_helpers::compile_driver::python_calls::KeywordArguments; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::native_host::third_party::ThirdPartyHost; +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; -use crate::bindings::_helpers::records::registry; const INIT_TOML_OPTIONS: (&[&str], &[&str]) = ( &[ @@ -81,8 +81,9 @@ fn normalized_connection<'py>( }; connection.set_item(key, value)?; } - call_with( - ®istry::class(py, RecordId::NormalizedDbtProfileConnection)?, + new_record_with( + py, + RecordId::NormalizedDbtProfileConnection, &[ ("adapter", PyString::new(py, profile.adapter).into_any()), ("connection", connection.into_any()), @@ -102,7 +103,7 @@ fn normalized_connection<'py>( /// Normalize rendered dbt profile output for SQLBuild. #[pyfunction] #[pyo3(signature = (*, resolved))] -fn normalize_dbt_profile_connection<'py>( +pub(crate) fn normalize_dbt_profile_connection<'py>( py: Python<'py>, resolved: Bound<'py, PyAny>, ) -> PyResult> { @@ -131,7 +132,7 @@ fn text_argument(arguments: &KeywordArguments<'_>, name: &str) -> PyResult, options: Option>, ) -> PyResult { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/profile_connection.rs b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/profile_connection.rs index 8ba22b6097..7971187131 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/profile_connection.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/profile_connection.rs @@ -9,14 +9,13 @@ use sqlbuild_core::text::main::python_strip::python_strip; use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; use crate::bindings::_helpers::dbt_interop::cli::resolved_path; +use crate::bindings::_helpers::dbt_interop::profile::normalize_dbt_profile_connection; use crate::bindings::_helpers::dbt_interop::profile_load::{ default_profiles_dir, load_dbt_project_metadata, load_selected_dbt_profile_output, }; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::native_host::third_party::ThirdPartyHost; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::NativeAttr; -use crate::bindings::_helpers::records::registry; const DBT_PROFILE_SOURCE: &str = "dbt_profile"; const PROFILE_CONNECTION_OPTIONS: (&[&str], &[&str]) = ( @@ -98,16 +97,13 @@ fn profile_connection<'py>( ("cli_vars", cli_vars), ], )?; - call_with( - ®istry::native(py, NativeAttr::NormalizeDbtProfileConnection)?, - &[("resolved", resolved)], - ) + normalize_dbt_profile_connection(py, resolved) } /// Resolve a raw connection config if it references a dbt profile. #[pyfunction] #[pyo3(signature = (**options))] -fn resolve_raw_dbt_profile_connection<'py>( +pub(crate) fn resolve_raw_dbt_profile_connection<'py>( py: Python<'py>, options: Option>, ) -> PyResult> { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/profile_load.rs b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/profile_load.rs index 60090e0373..fd8e338baa 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/profile_load.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/profile_load.rs @@ -15,8 +15,8 @@ use crate::bindings::_helpers::dbt_interop::profile::raised; use crate::bindings::_helpers::dbt_interop::profile_connection::profile_error; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::native_host::third_party::ThirdPartyHost; +use crate::bindings::_helpers::records::construct::new_record_with; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; const DBT_PROJECT_FILE: &str = "dbt_project.yml"; const PROFILES_FILE: &str = "profiles.yml"; @@ -131,8 +131,9 @@ pub(crate) fn load_selected_dbt_profile_output<'py>( .map_err(|error| raised(py, error))?; let (target, index): (String, usize) = select_dbt_profile_target(&validated, target_name).map_err(|error| raised(py, error))?; - call_with( - ®istry::class(py, RecordId::SelectedDbtProfileOutput)?, + new_record_with( + py, + RecordId::SelectedDbtProfileOutput, &[ ("profile_name", PyString::new(py, profile_name).into_any()), ("target_name", PyString::new(py, &target).into_any()), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/reference_resolver.rs b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/reference_resolver.rs index f965dcd379..60b5e714f9 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/reference_resolver.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/reference_resolver.rs @@ -10,10 +10,15 @@ use std::collections::HashSet; use crate::bindings::_helpers::compile_driver::python_calls::{ KeywordArguments, call_with, imported, keywords, within, }; +use crate::bindings::_helpers::dbt_interop::manifest::build_compile_dbt_manifest_index; +use crate::bindings::_helpers::dbt_interop::manifest::resolve_dbt_reference_relation; +use crate::bindings::_helpers::dbt_interop::manifest::validate_compile_dbt_model_names; +use crate::bindings::_helpers::dbt_interop::manifest::validate_compile_dbt_model_reference; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; +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::{ErrorId, NativeAttr, RecordId}; +use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; const VALIDATE_OPTIONS: (&[&str], &[&str]) = ( @@ -45,7 +50,7 @@ fn compile_input_error(py: Python<'_>, message: String, code: &str) -> PyErr { } fn text(value: &Bound<'_, PyAny>, name: &str) -> PyResult { - value.getattr(name)?.extract() + native_field(value, name)?.extract() } /// External reference resolver backed by a dbt manifest index. @@ -106,13 +111,8 @@ impl DbtCompileReferenceResolver { py: Python<'_>, known_model_names: Bound<'_, PyAny>, ) -> Result<(), PyErr> { - call_with( - ®istry::native(py, NativeAttr::ValidateCompileDbtModelNames)?, - &[ - ("known_model_names", known_model_names), - ("dbt_manifest", self.manifest_or_none(py)), - ], - )?; + validate_compile_dbt_model_names(py, known_model_names, self.manifest_or_none(py)) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; Ok(()) } @@ -257,25 +257,22 @@ impl DbtCompileReferenceResolver { ) -> Result<(), PyErr> { let arguments: KeywordArguments<'_> = KeywordArguments::read(py, "validate_reference", options, VALIDATE_OPTIONS)?; - let reference: Bound<'_, PyAny> = call_with( - ®istry::class(py, RecordId::CompileSqlReference)?, + let reference: Bound<'_, PyAny> = new_record_with( + py, + RecordId::CompileSqlReference, &[ ("ref_kind", arguments.value("ref_kind")?), ("ref_name", arguments.value("ref_name")?), ("ref_package", arguments.value("ref_package")?), ], )?; - call_with( - ®istry::native(py, NativeAttr::ValidateCompileDbtModelReference)?, - &[ - ("reference", reference), - ( - "model_relative_path", - arguments.value("owner_relative_sql_path")?, - ), - ("dbt_manifest", self.manifest_or_none(py)), - ], - )?; + validate_compile_dbt_model_reference( + py, + reference, + arguments.value("owner_relative_sql_path")?, + self.manifest_or_none(py), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; Ok(()) } @@ -287,14 +284,17 @@ impl DbtCompileReferenceResolver { ref_name: Bound<'py, PyAny>, ref_package: Bound<'py, PyAny>, ) -> PyResult> { - call_with( - ®istry::native(py, NativeAttr::ResolveDbtReferenceRelation)?, - &[ - ("manifest", self.manifest_or_none(py)), - ("ref_kind", ref_kind), - ("ref_name", ref_name), - ("ref_package", ref_package), - ], + resolve_dbt_reference_relation( + py, + Some(keywords( + py, + &[ + ("manifest", self.manifest_or_none(py)), + ("ref_kind", ref_kind), + ("ref_name", ref_name), + ("ref_package", ref_package), + ], + )?), ) } } @@ -309,10 +309,7 @@ pub(crate) fn build_compile_reference_resolver<'py>( if manifest_contents.is_none() { return Ok(py.None().into_bound(py)); } - let manifest: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::BuildCompileDbtManifestIndex)?, - &[("manifest_contents", manifest_contents)], - )?; + let manifest: Bound<'py, PyAny> = build_compile_dbt_manifest_index(py, manifest_contents)?; Ok(Bound::new( py, DbtCompileReferenceResolver::new(Some(manifest.unbind())), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/runner.rs b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/runner.rs index d620f39fcc..59581f6a0d 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/runner.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/runner.rs @@ -22,6 +22,7 @@ use crate::bindings::_helpers::dbt_interop::cli::{ }; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; +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::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -44,7 +45,7 @@ pub(crate) struct DbtRunner { impl DbtRunner { #[new] #[pyo3(signature = (*, dbt_executable = None, invoker = None))] - fn new(dbt_executable: Option, invoker: Option>) -> Self { + pub(crate) fn new(dbt_executable: Option, invoker: Option>) -> Self { Self { dbt_executable: dbt_executable .filter(|value| !value.is_empty()) @@ -125,8 +126,9 @@ impl DbtRunner { let result: Bound<'py, PyAny> = self.run(command, &native_field(options, "project_dir")?)?; let stdout: String = native_field(&result, "stdout")?.extract()?; - let listing: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::DbtLsResult)?, + let listing: Bound<'py, PyAny> = new_record_with( + py, + RecordId::DbtLsResult, &[ ("nodes", ls_nodes(py, &stdout)?.into_any()), ("command", result), @@ -193,8 +195,9 @@ impl DbtRunner { Ok(output) }); let output: DbtProcessOutput = output.map_err(|error| launch_failure(py, error))?; - call_with( - ®istry::class(py, RecordId::DbtCommandResult)?, + new_record_with( + py, + RecordId::DbtCommandResult, &[ ("argv", command.into_any()), ("returncode", output.returncode.into_pyobject_any(py)?), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/runtime_config.rs b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/runtime_config.rs index 50cb28f9f8..1a2e165ada 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/runtime_config.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/runtime_config.rs @@ -12,13 +12,14 @@ use sqlbuild_core::dbt_cli::errors::DbtCliError; use sqlbuild_core::dbt_cli::main::dbt_flag_value::dbt_flag_value; use sqlbuild_core::dbt_cli::models::DbtParsedFlags; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; +use crate::bindings::_helpers::compile_driver::python_calls::{call_with, keywords}; +use crate::bindings::_helpers::dbt_interop::cli::resolve_dbt_config; use crate::bindings::_helpers::dbt_interop::cli::{raised, resolved_path}; use crate::bindings::_helpers::native_host::adapter::AdapterHost; use crate::bindings::_helpers::native_host::host; +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::{ErrorId, NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; const ARGUMENT_ERROR: ErrorId = ErrorId::DbtInteropArgument; const CONFIG_ERROR: ErrorId = ErrorId::DbtInteropConfig; @@ -64,7 +65,7 @@ fn write_outputs(py: Python<'_>, outcome: &DbtExecutionOutcome) -> PyResult<()> Stream::Stdout => "stdout", Stream::Stderr => "stderr", }; - sys.getattr(name)?.call_method1("write", (text,))?; + native_field(&sys, name)?.call_method1("write", (text,))?; } Ok(()) } @@ -104,8 +105,9 @@ pub(crate) fn parsed_flags( } fn parsed_args_model<'py>(py: Python<'py>, flags: DbtParsedFlags) -> PyResult> { - call_with( - ®istry::class(py, RecordId::DbtInteropParsedArgs)?, + new_record_with( + py, + RecordId::DbtInteropParsedArgs, &[ ("select", PyTuple::new(py, flags.select)?.into_any()), ("exclude", PyTuple::new(py, flags.exclude)?.into_any()), @@ -184,8 +186,9 @@ pub(crate) fn config_overrides<'py>( args: &[String], ) -> PyResult> { let value = |name: &str| optional_text(py, dbt_flag_value(args, name)); - call_with( - ®istry::class(py, RecordId::DbtCliConfigOverrides)?, + new_record_with( + py, + RecordId::DbtCliConfigOverrides, &[ ("project_dir", value("--project-dir")), ("profiles_dir", value("--profiles-dir")), @@ -265,15 +268,18 @@ pub(crate) fn plan_options<'py>( .getattr("dbt")?; let local_config: Bound<'py, PyAny> = native_field(discovered_inputs, "local_config")?.getattr("dbt")?; - let resolved: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ResolveDbtConfig)?, - &[ - ("project_root", project_dir.clone()), - ("config", project_config.clone()), - ("local_config", local_config.clone()), - ("overrides", config_overrides(py, args)?), - ("require_project_dir", flag(py, true)), - ], + let resolved: Bound<'py, PyAny> = resolve_dbt_config( + py, + Some(keywords( + py, + &[ + ("project_root", project_dir.clone()), + ("config", project_config.clone()), + ("local_config", local_config.clone()), + ("overrides", config_overrides(py, args)?), + ("require_project_dir", flag(py, true)), + ], + )?), )?; let values: Bound<'py, PyDict> = vars_mapping(&project_config, &local_config, args)?; let vars: Bound<'py, PyAny> = if values.is_empty() { @@ -284,8 +290,9 @@ pub(crate) fn plan_options<'py>( &[("obj", values.into_any()), ("sort_keys", flag(py, true))], )? }; - call_with( - ®istry::class(py, RecordId::DbtCliOptions)?, + new_record_with( + py, + RecordId::DbtCliOptions, &[ ("project_dir", native_field(&resolved, "project_dir")?), ("profiles_dir", native_field(&resolved, "profiles_dir")?), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/runtime_execution.rs b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/runtime_execution.rs index cc27de688d..ecca3e3a25 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/runtime_execution.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/runtime_execution.rs @@ -1,10 +1,18 @@ //! Execute dbt through the subprocess boundary and finalize results in artifact order. -use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; +use crate::bindings::_helpers::build_progress::transient_status::TransientStatusReporter; +use crate::bindings::_helpers::compile_driver::python_calls::{ + KeywordArguments, call_with, keywords, +}; +use crate::bindings::_helpers::dbt_events::stream::render_dbt_node_result; +use crate::bindings::_helpers::dbt_interop::cli::parse_dbt_ls_json_lines; +use crate::bindings::_helpers::dbt_interop::event_stream::execute_dbt_json_event_stream; +use crate::bindings::_helpers::dbt_interop::execution::dbt_execution_ls_argv; use crate::bindings::_helpers::dbt_interop::runtime_results::{append_missing, result_ids}; +use crate::bindings::_helpers::presentation_text::style::PythonCliStyle; +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::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::ids::RecordId; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; use pyo3::types::{PyDict, PyInt, PyString, PyTuple}; use pyo3::{pyfunction, wrap_pyfunction}; @@ -39,20 +47,21 @@ fn expected_total<'py>( argv: &Bound<'py, PyAny>, ) -> PyResult> { let py = argv.py(); - let ls = call_with( - ®istry::native(py, NativeAttr::DbtExecutionLsArgv)?, - &[("argv", argv.clone())], + let ls = pyo3::IntoPyObjectExt::into_bound_py_any( + dbt_execution_ls_argv(argv.clone().extract::>()?), + py, )?; if ls.is_none() { return Ok(py.None().into_bound(py)); } - let status = call_with( - ®istry::native(py, NativeAttr::TransientStatusReporter)?, - &[ - ("stream", arguments.value("progress_stream")?), - ("use_color", arguments.value("use_color")?), - ], - )?; + let status = TransientStatusReporter::new( + py, + &arguments.value("progress_stream")?, + arguments.value("use_color")?.extract::()?, + true, + ) + .and_then(|value| pyo3::Bound::new(py, value)) + .map(pyo3::Bound::into_any)?; status.call_method1("start", ("Resolving dbt execution selection...",))?; let start = Instant::now(); let result = call_with( @@ -83,10 +92,8 @@ fn expected_total<'py>( if native_field(&result, "returncode")?.extract::()? != 0 { return Ok(py.None().into_bound(py)); } - let nodes = call_with( - ®istry::native(py, NativeAttr::ParseDbtLsJsonLines)?, - &[("stdout", native_field(&result, "stdout")?)], - )?; + let nodes = parse_dbt_ls_json_lines(py, &native_field(&result, "stdout")?.extract::()?) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; Ok(PyInt::new(py, nodes.len()?).into_any()) } @@ -101,18 +108,15 @@ fn progress(arguments: &KeywordArguments<'_>, message: &str) -> PyResult<()> { /// Execute selected dbt work and backfill results absent from its JSON event stream. #[pyfunction] #[pyo3(signature = (**options))] -fn execute_dbt_commands<'py>( +pub(crate) fn execute_dbt_commands<'py>( py: Python<'py>, options: Option>, ) -> PyResult> { let arguments = KeywordArguments::read(py, "execute_dbt_commands", options, OPTIONS)?; let argv = arguments.value("merged_argv")?; let output = arguments.value("progress_stream")?; - let style = call_with( - ®istry::native(py, NativeAttr::CliStyle)?, - &[("use_color", arguments.value("use_color")?)], - )?; - let result_type = registry::class(py, RecordId::DbtCommandExecutionResult)?; + let style = Bound::new(py, PythonCliStyle::new(&arguments.value("use_color")?)?) + .map(Bound::into_any)?; if argv.is_none() { let message = arguments.value("skip_message")?; let message = if message.is_none() { @@ -129,8 +133,9 @@ fn execute_dbt_commands<'py>( .extract::()? ), )?; - return call_with( - &result_type, + return new_record_with( + py, + RecordId::DbtCommandExecutionResult, &[("returncode", PyInt::new(py, 0).into_any())], ); } @@ -174,19 +179,24 @@ fn execute_dbt_commands<'py>( if target.is_none() && !project_dir.is_none() { target = project_dir.call_method1("__truediv__", ("target",))?; } - let (returncode, streamed): (Bound<'py, PyAny>, Bound<'py, PyAny>) = call_with( - ®istry::native(py, NativeAttr::ExecuteDbtJsonEventStream)?, - &[ - ("argv", argv), - ("cwd", project_dir), - ("stream", arguments.value("stdout_stream")?), - ("use_color", arguments.value("use_color")?), - ("target_path", target.clone()), - ("display_total", total.clone()), - ("on_node_result", arguments.value("on_node_result")?), - ], - )? - .extract()?; + let (returncode, streamed): (Bound<'py, PyAny>, Bound<'py, PyAny>) = + execute_dbt_json_event_stream( + py, + Some(keywords( + py, + &[ + ("argv", argv), + ("cwd", project_dir), + ("stream", arguments.value("stdout_stream")?), + ("use_color", arguments.value("use_color")?), + ("target_path", target.clone()), + ("display_total", total.clone()), + ("on_node_result", arguments.value("on_node_result")?), + ], + )?), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))? + .extract()?; progress(&arguments, "Finalizing dbt run...")?; let streamed_ids = result_ids(&streamed)?; let mut filtered: Vec> = Vec::new(); @@ -211,25 +221,29 @@ fn execute_dbt_commands<'py>( } let count = results.len()? - appended.len(); for (offset, result) in appended.into_iter().enumerate() { - let rendered = call_with( - ®istry::native(py, NativeAttr::RenderDbtNodeResult)?, - &[ - ("result", result.clone()), - ("use_color", native_field(&style, "use_color")?), - ( - "display_index", - PyInt::new(py, count + offset + 1).into_any(), - ), - ( - "display_total", - if total.is_none() { - PyInt::new(py, results.len()?).into_any() - } else { - total.clone() - }, - ), - ], - )?; + let rendered = render_dbt_node_result( + py, + Some(keywords( + py, + &[ + ("result", result.clone()), + ("use_color", native_field(&style, "use_color")?), + ( + "display_index", + PyInt::new(py, count + offset + 1).into_any(), + ), + ( + "display_total", + if total.is_none() { + PyInt::new(py, results.len()?).into_any() + } else { + total.clone() + }, + ), + ], + )?), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; write( &arguments.value("stdout_stream")?, &rendered.extract::()?, @@ -240,8 +254,9 @@ fn execute_dbt_commands<'py>( } } progress(&arguments, "Finalized dbt run.")?; - call_with( - &result_type, + new_record_with( + py, + RecordId::DbtCommandExecutionResult, &[("returncode", returncode), ("node_results", results)], ) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/runtime_results.rs b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/runtime_results.rs index 75fdc61f63..64f90fa032 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/runtime_results.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/runtime_results.rs @@ -1,9 +1,9 @@ //! Backfill streamed dbt results using Python's JSON and UTF-8 artifact contract. use crate::bindings::_helpers::compile_driver::python_calls::call_with; +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; -use crate::bindings::_helpers::records::registry; use pyo3::exceptions::PyOSError; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; use pyo3::types::{ @@ -48,8 +48,9 @@ fn messages<'py>(raw: &Bound<'py, PyDict>) -> PyResult> { }; PyTuple::new( py, - [call_with( - ®istry::class(py, RecordId::DbtNodeMessage)?, + [new_record_with( + py, + RecordId::DbtNodeMessage, &[ ("level", PyString::new(py, "error").into_any()), ("message", text), @@ -128,8 +129,9 @@ pub(crate) fn append_missing<'py>( } else { py.None().into_bound(py) }; - appended.push(call_with( - ®istry::class(py, RecordId::DbtNodeExecutionResult)?, + appended.push(new_record_with( + py, + RecordId::DbtNodeExecutionResult, &[ ("unique_id", unique_id), ("resource_type", PyString::new(py, resource_type).into_any()), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/sqlbuild_work.rs b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/sqlbuild_work.rs index 8c25401c45..9c6b9873ad 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/sqlbuild_work.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/sqlbuild_work.rs @@ -8,15 +8,24 @@ use sqlbuild_core::build_command::constants::{ SNAPSHOT_FULL_REFRESH_DBT_HELP, TABLE_TYPE_DOWNGRADE_DBT_HELP, }; +use crate::bindings::_helpers::build_command::policies::enforce_plan_safety_policies_binding; use crate::bindings::_helpers::build_command::support::{ boolean, emit, joined, keyword_relay, none, text, tuple, }; -use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; +use crate::bindings::_helpers::build_progress::build_callbacks::BuildProgressCallbacks; +use crate::bindings::_helpers::build_progress::footer::format_build_footer; +use crate::bindings::_helpers::cli::progress_reporters::ConnectionProgressReporter; +use crate::bindings::_helpers::cli::progress_text::format_execution_header; +use crate::bindings::_helpers::compile_driver::python_calls::{ + KeywordArguments, call_with, keywords, +}; +use crate::bindings::_helpers::compile_outputs::runtime_target::write_runtime_target; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; +use crate::bindings::_helpers::presentation_text::style::PythonCliStyle; +use crate::bindings::_helpers::records::construct::{new_record_with, record_class}; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::ids::RecordId; const WORK_OPTIONS: (&[&str], &[&str]) = ( &[ @@ -33,8 +42,9 @@ const WORK_OPTIONS: (&[&str], &[&str]) = ( /// Interop has no allow flags; destructive work uses the ordinary interactive safety gates. #[pyfunction] fn dbt_interop_plan_safety_gates(py: Python<'_>) -> PyResult> { - call_with( - ®istry::class(py, RecordId::PlanSafetyGates)?, + new_record_with( + py, + RecordId::PlanSafetyGates, &[ ("allow_missing_migration_origin", boolean(py, false)), ("allow_snapshot_full_refresh", boolean(py, false)), @@ -67,7 +77,7 @@ fn dbt_interop_plan_safety_gates(py: Python<'_>) -> PyResult> { /// Execute SQLBuild work only after its plan safety policies succeed. #[pyfunction] #[pyo3(signature = (**options))] -fn execute_dbt_sqlbuild_work<'py>( +pub(crate) fn execute_dbt_sqlbuild_work<'py>( py: Python<'py>, options: Option>, ) -> PyResult { @@ -77,40 +87,49 @@ fn execute_dbt_sqlbuild_work<'py>( let plan = native_field(&context, "plan_output")?; let output = native_field(&context, "output_stream")?; let color = native_field(&context, "use_color")?; - call_with( - ®istry::native(py, NativeAttr::EnforcePlanSafetyPolicies)?, - &[ - ("plan", plan.clone()), - ("gates", dbt_interop_plan_safety_gates(py)?), - ( - "snapshots_config", - native_field(&context, "snapshots_config")?, - ), - ( - "target_name", - native_field(&project, "effective_target_name")?, - ), - ( - "execution_limits", - native_field(&context, "execution_limits")?, - ), - ( - "streams", - tuple(py, &[py.import("sys")?.getattr("stdin")?, output.clone()])?, - ), - ], - )?; - let callbacks = call_with( - ®istry::native(py, NativeAttr::BuildProgressCallbacks)?, - &[ - ("plan", plan.clone()), - ("use_color", color.clone()), - ("verbose", args.value("verbose")?), - ("debug", boolean(py, false)), - ], - )?; + enforce_plan_safety_policies_binding( + py, + Some(keywords( + py, + &[ + ("plan", plan.clone()), + ("gates", dbt_interop_plan_safety_gates(py)?), + ( + "snapshots_config", + native_field(&context, "snapshots_config")?, + ), + ( + "target_name", + native_field(&project, "effective_target_name")?, + ), + ( + "execution_limits", + native_field(&context, "execution_limits")?, + ), + ( + "streams", + tuple(py, &[py.import("sys")?.getattr("stdin")?, output.clone()])?, + ), + ], + )?), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; + let callbacks = BuildProgressCallbacks::new( + py, + Some(keywords( + py, + &[ + ("plan", plan.clone()), + ("use_color", color.clone()), + ("verbose", args.value("verbose")?), + ("debug", boolean(py, false)), + ], + )?), + ) + .and_then(|value| pyo3::Bound::new(py, value)) + .map(pyo3::Bound::into_any)?; let command = args.value("command")?; - let kinds = registry::class(py, RecordId::DbtInteropCommand)?; + let kinds = record_class(py, RecordId::DbtInteropCommand)?; let display = if command.eq(native_field(&kinds, "RUN")?)? { "sqb build --no-tests --no-audits".to_owned() } else { @@ -120,18 +139,15 @@ fn execute_dbt_sqlbuild_work<'py>( ) }; let concurrency = native_field(&project, "settings")?.getattr("concurrency")?; - let header = call_with( - ®istry::native(py, NativeAttr::FormatExecutionHeader)?, - &[ - ("command", text(py, &display)), - ("target", none(py)), - ("concurrency", concurrency.clone()), - ], - )?; - let style = call_with( - ®istry::native(py, NativeAttr::CliStyle)?, - &[("use_color", color.clone())], + let header = pyo3::IntoPyObjectExt::into_bound_py_any( + format_execution_header( + &text(py, &display).extract::()?, + none(py).extract::>()?.as_deref(), + concurrency.clone().extract::()?, + ), + py, )?; + let style = Bound::new(py, PythonCliStyle::new(&color.clone())?).map(Bound::into_any)?; emit( &output, &format!( @@ -144,15 +160,20 @@ fn execute_dbt_sqlbuild_work<'py>( .extract::()? ), )?; - let connection = call_with( - ®istry::native(py, NativeAttr::ConnectionProgressReporter)?, - &[ - ("adapter_name", native_field(&context, "adapter_name")?), - ("stream", output.clone()), - ("blank_line_after_complete", boolean(py, true)), - ("use_color", color.clone()), - ], - )?; + let connection = ConnectionProgressReporter::new( + py, + Some(keywords( + py, + &[ + ("adapter_name", native_field(&context, "adapter_name")?), + ("stream", output.clone()), + ("blank_line_after_complete", boolean(py, true)), + ("use_color", color.clone()), + ], + )?), + ) + .and_then(|value| pyo3::Bound::new(py, value)) + .map(pyo3::Bound::into_any)?; let build = command.eq(native_field(&kinds, "BUILD")?)?; let runtime = call_with( &host::entry(py, ExecutorHost::BuildRuntimeParams)?, @@ -217,25 +238,18 @@ fn execute_dbt_sqlbuild_work<'py>( ("callbacks", hooks), ], )?; - call_with( - ®istry::native(py, NativeAttr::WriteRuntimeTarget)?, - &[ - ( - "target_dir", - joined(&args.value("project_dir")?, &["target"])?, - ), - ("plan_output", plan), - ("result", result.clone()), - ], - )?; - let footer = call_with( - ®istry::native(py, NativeAttr::FormatBuildFooter)?, - &[ - ("result", result.clone()), - ("elapsed", native_field(&callbacks, "elapsed")?), - ("use_color", color), - ], + write_runtime_target( + &joined(&args.value("project_dir")?, &["target"])?, + &plan, + &result, )?; + let footer = format_build_footer( + &result.clone(), + native_field(&callbacks, "elapsed")?.extract::()?, + color.extract::()?, + None, + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; emit(&output, &format!("\n{}\n", footer.extract::()?))?; Ok(i64::from(!native_field(&result, "status")?.eq( host::entry(py, ExecutorHost::BuildStatus)?.getattr("SUCCESS")?, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/debug_command/run_debug.rs b/crates/sqlbuild-python/src/bindings/_helpers/debug_command/run_debug.rs index c5c6fb0c02..869bc16d7b 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/debug_command/run_debug.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/debug_command/run_debug.rs @@ -30,15 +30,17 @@ use crate::bindings::_helpers::command_runtime::warehouse_scope::command_warehou use crate::bindings::_helpers::command_runtime::warehouses::{ CommandScope, WarehouseInputs, apply_warehouse, command_scope, find_authored, resolve_warehouse, }; +use crate::bindings::_helpers::compile_attachment::target::build_effective_target_namespace; use crate::bindings::_helpers::compile_driver::discovery::discover_project_inputs; use crate::bindings::_helpers::compile_driver::python_calls::keywords; use crate::bindings::_helpers::compile_driver::python_calls::{ KeywordArguments, call_with, imported, within, }; +use crate::bindings::_helpers::contracts::spec_resolution::resolve_effective_adapter_name; use crate::bindings::_helpers::presentation_text::terminal::stdout_supports_color; +use crate::bindings::_helpers::records::construct::record_class; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::ids::RecordId; const DEBUG_OPTIONS: (&[&str], &[&str]) = ( &[], @@ -123,7 +125,7 @@ fn warehouse_checks( selected_target: &context.selected_target, }; let mut checks: Vec = Vec::new(); - for group in registry::class(py, RecordId::WarehouseGroup)?.try_iter()? { + for group in record_class(py, RecordId::WarehouseGroup)?.try_iter()? { let group: Bound<'_, PyAny> = group?; let group_name: String = native_field(&group, "value")?.str()?.extract()?; if !supports_session_warehouse { @@ -269,18 +271,9 @@ fn build_report( )?), )?; let files: DebugConfigFiles = debug_config_files(project_dir); - let adapter_name: Bound<'_, PyAny> = call_with( - ®istry::native(py, NativeAttr::ResolveEffectiveAdapterName)?, - &[ - ( - "project_config", - native_field(&discovered_inputs, "project_config")?, - ), - ( - "local_config", - native_field(&discovered_inputs, "local_config")?, - ), - ], + let adapter_name: Bound<'_, PyAny> = resolve_effective_adapter_name( + &native_field(&discovered_inputs, "project_config")?, + &native_field(&discovered_inputs, "local_config")?, )?; let adapter: Bound<'_, PyAny> = resolve_cli_adapter(&adapter_name, &project_dir_object)?; let context: DebugContext<'_> = DebugContext { @@ -302,33 +295,37 @@ fn build_report( })?; let connection_config: Bound<'_, PyDict> = effective_connection(&context, (&adapter_name, &base_connection))?.cast_into()?; - let namespace: Bound<'_, PyTuple> = call_with( - ®istry::native(py, NativeAttr::BuildEffectiveTargetNamespace)?, - &[ - ("discovered_inputs", context.discovered_inputs.clone()), - ("selected_target", context.selected_target.clone()), - ( - "connection_database", - connection_config - .get_item("database")? - .unwrap_or_else(|| py.None().into_bound(py)), - ), - ( - "connection_schema", - connection_config - .get_item("schema")? - .unwrap_or_else(|| py.None().into_bound(py)), - ), - ( - "default_database", - context.adapter.call_method0("default_database")?, - ), - ( - "default_schema", - context.adapter.call_method0("default_schema")?, - ), - ], - )? + let namespace: Bound<'_, PyTuple> = build_effective_target_namespace( + py, + Some(keywords( + py, + &[ + ("discovered_inputs", context.discovered_inputs.clone()), + ("selected_target", context.selected_target.clone()), + ( + "connection_database", + connection_config + .get_item("database")? + .unwrap_or_else(|| py.None().into_bound(py)), + ), + ( + "connection_schema", + connection_config + .get_item("schema")? + .unwrap_or_else(|| py.None().into_bound(py)), + ), + ( + "default_database", + context.adapter.call_method0("default_database")?, + ), + ( + "default_schema", + context.adapter.call_method0("default_schema")?, + ), + ], + )?), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))? .cast_into()?; let target_config: Bound<'_, PyAny> = namespace.get_item(1)?; let connection_name: Option = if target_config.is_none() { @@ -373,7 +370,10 @@ fn build_report( /// `sqb debug`: validate project config and the active connection; `1` when a check failed. #[pyfunction] #[pyo3(signature = (**options))] -fn run_debug_command(py: Python<'_>, options: Option>) -> PyResult { +pub(crate) fn run_debug_command( + py: Python<'_>, + options: Option>, +) -> PyResult { let arguments: KeywordArguments<'_> = KeywordArguments::read(py, "run_debug_command", options, DEBUG_OPTIONS)?; let project_dir: Bound<'_, PyAny> = arguments.value("project_dir")?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/diagnostics/console_records.rs b/crates/sqlbuild-python/src/bindings/_helpers/diagnostics/console_records.rs index 9b958d2487..05bd5f97e3 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/diagnostics/console_records.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/diagnostics/console_records.rs @@ -30,7 +30,7 @@ fn text_field(record: &Bound<'_, PyAny>, field: &str) -> PyResult if !record.hasattr(field)? { return Ok(None); } - let value: Bound<'_, PyAny> = record.getattr(field)?; + let value: Bound<'_, PyAny> = native_field(record, field)?; if value.is_instance_of::() { return Ok(Some(value.extract()?)); } @@ -42,7 +42,7 @@ fn duration_seconds(record: &Bound<'_, PyAny>) -> PyResult> { if !record.hasattr(DURATION_FIELD)? { return Ok(None); } - let value: Bound<'_, PyAny> = record.getattr(DURATION_FIELD)?; + let value: Bound<'_, PyAny> = native_field(record, DURATION_FIELD)?; if !value.is_instance_of::() { return Ok(None); } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/diagnostics/trackers.rs b/crates/sqlbuild-python/src/bindings/_helpers/diagnostics/trackers.rs index 4dab1204b6..1171a8dcb2 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/diagnostics/trackers.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/diagnostics/trackers.rs @@ -10,8 +10,8 @@ use pyo3::{pyclass, pymethods}; use crate::bindings::_helpers::compile_driver::python_calls::{context_variable, imported}; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; +use crate::bindings::_helpers::records::construct::new_record; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; const TRACKER_VARIABLE: &str = "sqlbuild_build_phase_timing_tracker"; const PHASES: [&str; 6] = [ @@ -75,7 +75,7 @@ pub(crate) struct BuildPhaseTimingTracker { impl BuildPhaseTimingTracker { #[new] #[pyo3(signature = (*, monotonic = None))] - fn new(py: Python<'_>, monotonic: Option>) -> PyResult { + pub(crate) fn new(py: Python<'_>, monotonic: Option>) -> PyResult { let monotonic: Bound<'_, PyAny> = clock(py, monotonic)?; let started_at: f64 = monotonic.call0()?.extract()?; Ok(Self { @@ -107,7 +107,7 @@ impl BuildPhaseTimingTracker { keywords.set_item(phase, value)?; } keywords.set_item("total_seconds", non_negative(now, self.started_at))?; - registry::class(py, RecordId::PartialBuildPhaseTimings)?.call((), Some(&keywords)) + new_record(py, RecordId::PartialBuildPhaseTimings, (), Some(&keywords)) } /// Return the active build timing tracker. diff --git a/crates/sqlbuild-python/src/bindings/_helpers/diff_report/command.rs b/crates/sqlbuild-python/src/bindings/_helpers/diff_report/command.rs index 27dc07328f..5d3313059e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/diff_report/command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/diff_report/command.rs @@ -17,9 +17,14 @@ use sqlbuild_cli::diff_report::types::QueryDiffPhase; use sqlbuild_core::json::main::dumps::dumps; use sqlbuild_core::json::models::{JsonDialect, StdlibJsonOptions}; +use crate::bindings::_helpers::command_runtime::connection::resolve_target_connection_config; +use crate::bindings::_helpers::compile_driver::diff::run_diff_pipeline; use crate::bindings::_helpers::compile_driver::discovery::discover_project_inputs; use crate::bindings::_helpers::compile_driver::python_calls::imported; use crate::bindings::_helpers::compile_driver::python_calls::keywords; +use crate::bindings::_helpers::contracts::spec_resolution::resolve_target_config; +use crate::bindings::_helpers::contracts::spec_resolution::resolve_target_name; +use crate::bindings::_helpers::dbt_interop::reference_resolver::resolve_external_sql_reference_resolver; use crate::bindings::_helpers::diff_report::facts::diff_facts; use crate::bindings::_helpers::diff_report::query::{ execute_query_diff, execution_options, prepare_query_diff, project_adapter, @@ -34,8 +39,9 @@ use crate::bindings::_helpers::frontier::encoder::emit_error; use crate::bindings::_helpers::native_host::adapter::AdapterHost; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; +use crate::bindings::_helpers::planner_selection::unit_tests::reject_unit_test_selectors; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr}; +use crate::bindings::_helpers::records::ids::ErrorId; use crate::bindings::_helpers::records::registry; const DEFAULT_DIFF_MAX_FULL_ROWS: i64 = 10_000_000; @@ -90,13 +96,10 @@ fn resolve_invocation<'py>( Some(keywords(py, &[("project_dir", project_dir.clone())])?), )? }; - call( - ®istry::native(py, NativeAttr::RejectUnitTestSelectors)?, - &[ - ("select", &native_field(request, "select")?), - ("exclude", &native_field(request, "exclude")?), - ("discovered_inputs", &discovered), - ], + reject_unit_test_selectors( + &native_field(request, "select")?, + &native_field(request, "exclude")?, + &discovered.clone(), )?; if facts.select.is_empty() && !query_mode { return Err(rejected( @@ -226,16 +229,11 @@ fn target_max_full_rows<'py>( target: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = discovered.py(); - let configured: Bound<'py, PyAny> = call( - ®istry::native(py, NativeAttr::ResolveTargetConfig)?, - &[ - ( - "project_config", - &native_field(discovered, "project_config")?, - ), - ("local_config", &native_field(discovered, "local_config")?), - ("target_name", target), - ], + let configured: Bound<'py, PyAny> = resolve_target_config( + py, + &native_field(discovered, "project_config")?, + &native_field(discovered, "local_config")?, + target, )? .getattr("diff")? .getattr("max_full_rows")?; @@ -259,17 +257,13 @@ fn diff_to_target<'py>( if !to_name.is_none() { return Ok(to_name); } - let active: Bound<'py, PyAny> = call( - ®istry::native(py, NativeAttr::ResolveTargetName)?, - &[ - ( - "project_config", - &native_field(discovered, "project_config")?, - ), - ("local_config", &native_field(discovered, "local_config")?), - ("selected_target", &none(py)), - ], - )?; + let active: Bound<'py, PyAny> = resolve_target_name( + py, + &native_field(discovered, "project_config")?, + &native_field(discovered, "local_config")?, + &none(py), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; if active.is_none() { return Err(rejected( py, @@ -381,40 +375,42 @@ fn run_model_diff( let from_target: Bound<'_, PyAny> = object(py, from_name)?; let (_, adapter) = project_adapter(discovered, project_dir)?; let cli_vars: Bound<'_, PyAny> = native_field(request, "cli_vars")?; - let connection_config: Bound<'_, PyAny> = call( - ®istry::native(py, NativeAttr::ResolveTargetConnectionConfig)?, - &[ - ("discovered_inputs", discovered), - ("project_dir", project_dir), - ("target_name", &to_target), - ("cli_vars", &cli_vars), - ], - )?; - let resolver: Bound<'_, PyAny> = call( - ®istry::native(py, NativeAttr::ResolveExternalSqlReferenceResolver)?, - &[ - ("project_dir", project_dir), - ("discovered_inputs", discovered), - ], + let connection_config: Bound<'_, PyAny> = resolve_target_connection_config( + py, + Some(keywords( + py, + &[ + ("discovered_inputs", discovered.clone()), + ("project_dir", project_dir.clone()), + ("target_name", to_target.clone()), + ("cli_vars", cli_vars.clone()), + ], + )?), )?; - let pipeline: Bound<'_, PyTuple> = call( - ®istry::native(py, NativeAttr::RunDiffPipeline)?, - &[ - ("discovered_inputs", discovered), - ("adapter", &adapter), - ("from_target", &from_target), - ("to_target", &to_target), - ("resolved_connection", &connection_config), - ( - "no_sql_validation", - &native_field(request, "no_sql_validation")?, - ), - ("select", &native_field(request, "select")?), - ("exclude", &native_field(request, "exclude")?), - ("cli_vars", &cli_vars), - ("external_sql_reference_resolver", &resolver), - ], - )? + let resolver: Bound<'_, PyAny> = + resolve_external_sql_reference_resolver(project_dir.clone(), discovered.clone())?; + let pipeline: Bound<'_, PyTuple> = run_diff_pipeline( + py, + Some(keywords( + py, + &[ + ("discovered_inputs", discovered.clone()), + ("adapter", adapter.clone()), + ("from_target", from_target.clone()), + ("to_target", to_target.clone()), + ("resolved_connection", connection_config.clone()), + ( + "no_sql_validation", + native_field(request, "no_sql_validation")?, + ), + ("select", native_field(request, "select")?), + ("exclude", native_field(request, "exclude")?), + ("cli_vars", cli_vars.clone()), + ("external_sql_reference_resolver", resolver.clone()), + ], + )?), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))? .downcast_into()?; let selected: Bound<'_, PyAny> = pipeline.get_item(2)?; if !selected.is_truthy()? { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/diff_report/facts.rs b/crates/sqlbuild-python/src/bindings/_helpers/diff_report/facts.rs index aa39b587ca..c5ad6e3502 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/diff_report/facts.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/diff_report/facts.rs @@ -172,7 +172,7 @@ fn row_facts(rows: Bound<'_, PyAny>) -> PyResult> { if rows.is_none() { return Ok(None); } - let count = |name: &str| -> PyResult { rows.getattr(name)?.extract() }; + let count = |name: &str| -> PyResult { native_field(&rows, name)?.extract() }; let mut columns: Vec = Vec::new(); for item in native_field(&rows, "column_results")?.try_iter()? { columns.push(column(&item?)?); @@ -227,7 +227,7 @@ fn model_facts(model: &Bound<'_, PyAny>) -> PyResult { let schema: Bound<'_, PyAny> = native_field(model, "schema_result")?; let names = |attribute: &str| -> PyResult> { let mut names: Vec = Vec::new(); - for column in schema.getattr(attribute)?.try_iter()? { + for column in native_field(&schema, attribute)?.try_iter()? { names.push(text_of(&native_field(&column?, "name")?)?); } Ok(names) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/diff_report/query.rs b/crates/sqlbuild-python/src/bindings/_helpers/diff_report/query.rs index 71d42879e0..336d7d50aa 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/diff_report/query.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/diff_report/query.rs @@ -14,7 +14,11 @@ use sqlbuild_cli::diff_report::types::PhaseSeconds; use crate::bindings::_helpers::boundary::panics::value_error; use crate::bindings::_helpers::command_runtime::adapters::resolve_cli_adapter; -use crate::bindings::_helpers::compile_driver::python_calls::imported; +use crate::bindings::_helpers::command_runtime::connection::resolve_project_connection_config; +use crate::bindings::_helpers::compile_attachment::effective_config::build_effective_runtime_config; +use crate::bindings::_helpers::compile_attachment::target::build_effective_target_namespace; +use crate::bindings::_helpers::compile_driver::python_calls::{imported, keywords}; +use crate::bindings::_helpers::contracts::spec_resolution::resolve_effective_adapter_name; use crate::bindings::_helpers::diff_report::support::{ call, cli_error, error_text, none, object, progress_line, }; @@ -22,9 +26,9 @@ use crate::bindings::_helpers::diff_report::tolerances::cli_tolerance_overrides; use crate::bindings::_helpers::native_host::adapter::AdapterHost; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; +use crate::bindings::_helpers::records::construct::record_class; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::types::CompilerDetach; use sqlbuild_analysis::diff_selection::main::raw_query_shape::raw_query_shape; use sqlbuild_analysis::diff_selection::models::RawQueryShape; @@ -53,16 +57,9 @@ pub(crate) fn project_adapter<'py>( discovered: &Bound<'py, PyAny>, project_dir: &Bound<'py, PyAny>, ) -> PyResult<(String, Bound<'py, PyAny>)> { - let py: Python<'py> = discovered.py(); - let name: Bound<'py, PyAny> = call( - ®istry::native(py, NativeAttr::ResolveEffectiveAdapterName)?, - &[ - ( - "project_config", - &native_field(discovered, "project_config")?, - ), - ("local_config", &native_field(discovered, "local_config")?), - ], + let name: Bound<'py, PyAny> = resolve_effective_adapter_name( + &native_field(discovered, "project_config")?, + &native_field(discovered, "local_config")?, )?; let adapter: Bound<'py, PyAny> = resolve_cli_adapter(&name, project_dir)?; Ok((name.extract()?, adapter)) @@ -173,37 +170,44 @@ pub(crate) fn prepare_query_diff<'py>( let (adapter_name, adapter) = project_adapter(discovered, project_dir)?; let selected_target: Bound<'py, PyAny> = native_field(request, "selected_target")?; let cli_vars: Bound<'py, PyAny> = native_field(request, "cli_vars")?; - let config: Bound<'py, PyAny> = call( - ®istry::native(py, NativeAttr::ResolveProjectConnectionConfig)?, - &[ - ("discovered_inputs", discovered), - ("project_dir", project_dir), - ("selected_target", &selected_target), - ("cli_vars", &cli_vars), - ], + let config: Bound<'py, PyAny> = resolve_project_connection_config( + py, + Some(keywords( + py, + &[ + ("discovered_inputs", discovered.clone()), + ("project_dir", project_dir.clone()), + ("selected_target", selected_target.clone()), + ("cli_vars", cli_vars.clone()), + ], + )?), )?; let database: Bound<'py, PyAny> = if NAMESPACELESS_ADAPTERS.contains(&adapter_name.as_str()) { none(py) } else { config.call_method1("get", ("database",))? }; - let namespace: Bound<'py, PyAny> = call( - ®istry::native(py, NativeAttr::BuildEffectiveTargetNamespace)?, - &[ - ("discovered_inputs", discovered), - ("selected_target", &selected_target), - ("connection_database", &database), - ( - "connection_schema", - &config.call_method1("get", ("schema",))?, - ), - ( - "default_database", - &adapter.call_method0("default_database")?, - ), - ("default_schema", &adapter.call_method0("default_schema")?), - ], - )?; + let namespace: Bound<'py, PyAny> = build_effective_target_namespace( + py, + Some(keywords( + py, + &[ + ("discovered_inputs", discovered.clone()), + ("selected_target", selected_target.clone()), + ("connection_database", database.clone()), + ( + "connection_schema", + config.call_method1("get", ("schema",))?, + ), + ( + "default_database", + adapter.call_method0("default_database")?, + ), + ("default_schema", adapter.call_method0("default_schema")?), + ], + )?), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; let (target_name, _target, database, schema): ( Bound<'py, PyAny>, Bound<'py, PyAny>, @@ -232,17 +236,15 @@ pub(crate) fn prepare_query_diff<'py>( )?; let left_sql: String = read_only_query(py, &left_sql, "left", &adapter)?; let right_sql: String = read_only_query(py, &right_sql, "right", &adapter)?; - let runtime: Bound<'py, PyTuple> = call( - ®istry::native(py, NativeAttr::BuildEffectiveRuntimeConfig)?, - &[ - ("discovered_inputs", discovered), - ("selected_target", &selected_target), - ("cli_vars", &cli_vars), - ], - )? + let runtime: Bound<'py, PyTuple> = build_effective_runtime_config( + discovered, + Some(selected_target.clone()).filter(|value| !value.is_none()), + Some(cli_vars.clone()).filter(|value| !value.is_none()), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))? .downcast_into()?; let label = |name: &str, default: &str| -> PyResult { - let value: Bound<'py, PyAny> = request.getattr(name)?; + let value: Bound<'py, PyAny> = native_field(request, name)?; let text: String = if value.is_truthy()? { value.extract()? } else { @@ -254,11 +256,11 @@ pub(crate) fn prepare_query_diff<'py>( let examples = |name: &str| -> PyResult> { object( py, - effective_max_examples(request.getattr(name)?.extract()?, verbose), + effective_max_examples(native_field(request, name)?.extract()?, verbose), ) }; call( - ®istry::class(py, RecordId::QueryDiffPreparation)?, + &record_class(py, RecordId::QueryDiffPreparation)?, &[ ("adapter", &adapter), ("connection_config", &config), @@ -305,7 +307,7 @@ pub(crate) fn execution_options<'py>( extra: &[(&str, &Bound<'py, PyAny>)], ) -> PyResult> { let py: Python<'py> = request.py(); - let attribute = |name: &str| request.getattr(name); + let attribute = |name: &str| native_field(request, name); let sampling: Bound<'py, PyAny> = call( &host::entry(py, ExecutorHost::RowDiffSamplingOverride)?, &[ @@ -490,7 +492,7 @@ fn compare_artifacts<'py>( ("adapter", &adapter), ("connection", connection), ("artifact", artifact), - ("sql", &preparation.getattr(sql)?), + ("sql", &native_field(preparation, sql)?), ("expires_at", &expires_at), ], ) { @@ -698,7 +700,7 @@ fn inspect_inputs<'py>( )); } } - let ttl: Bound<'py, PyAny> = registry::class(py, RecordId::Duration)? + let ttl: Bound<'py, PyAny> = record_class(py, RecordId::Duration)? .call_method1("parse", (native_field(preparation, "artifact_ttl")?,))?; if ttl.is_none() { return Err(failure( @@ -714,11 +716,11 @@ fn inspect_inputs<'py>( &native_field(&adapter, "query_column_names")?, &[ ("connection", connection), - ("sql", &preparation.getattr(sql)?), + ("sql", &native_field(preparation, sql)?), ], )? .extract()?; - let label: String = preparation.getattr(label)?.extract()?; + let label: String = native_field(preparation, label)?.extract()?; query_column_problem(&label, &columns, max_columns) .map_err(|error| user_failure(py, error))?; } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/diff_report/registration.rs b/crates/sqlbuild-python/src/bindings/_helpers/diff_report/registration.rs index 088a0352a2..af9c4d14b3 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/diff_report/registration.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/diff_report/registration.rs @@ -119,7 +119,10 @@ fn render_diff_output(py: Python<'_>, options: Option>) -> PyR /// Render a machine-readable incomplete or execution-failure outcome. #[pyfunction] #[pyo3(signature = (**options))] -fn render_diff_error_json(py: Python<'_>, options: Option>) -> PyResult { +pub(crate) fn render_diff_error_json( + py: Python<'_>, + options: Option>, +) -> PyResult { let arguments: KeywordArguments<'_> = KeywordArguments::read(py, "render_diff_error_json", options, ERROR_OPTIONS)?; let size_guard: Bound<'_, PyAny> = arguments.value("size_guard")?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/diff_report/support.rs b/crates/sqlbuild-python/src/bindings/_helpers/diff_report/support.rs index 103090db23..636bde0e65 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/diff_report/support.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/diff_report/support.rs @@ -117,7 +117,7 @@ fn texts(value: Bound<'_, PyAny>) -> PyResult> { /// The facts of one `DiffCommandRequest` that validation and the size guard read. pub(crate) fn request_facts(request: &Bound<'_, PyAny>) -> PyResult { let py: Python<'_> = request.py(); - let attribute = |name: &str| request.getattr(name); + let attribute = |name: &str| native_field(request, name); let present = |name: &str| -> PyResult { Ok(!attribute(name)?.is_none()) }; let number = |name: &str| -> PyResult> { attribute(name)?.extract() }; let flag = |name: &str| -> PyResult { attribute(name)?.is_truthy() }; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/diff_report/tolerances.rs b/crates/sqlbuild-python/src/bindings/_helpers/diff_report/tolerances.rs index e84bc01802..2700d8b6dc 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/diff_report/tolerances.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/diff_report/tolerances.rs @@ -281,7 +281,7 @@ fn resolve_row_diff_sampling<'py>( } let label: String = request.get("label")?.extract()?; let overridden = |field: &str, raw: &str| -> PyResult> { - let value: Bound<'py, PyAny> = override_values.getattr(field)?; + let value: Bound<'py, PyAny> = native_field(&override_values, field)?; if value.is_none() { request.get(raw) } else { @@ -330,8 +330,8 @@ fn merge_row_diff_tolerances<'py>( } let merged = |field: &str| -> PyResult> { let combined: Bound<'py, PyDict> = PyDict::new(py); - combined.update(base.getattr(field)?.downcast()?.as_mapping())?; - combined.update(overlay.getattr(field)?.downcast()?.as_mapping())?; + combined.update(native_field(&base, field)?.downcast()?.as_mapping())?; + combined.update(native_field(&overlay, field)?.downcast()?.as_mapping())?; Ok(combined.into_any()) }; call_with( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/discovery/configuration.rs b/crates/sqlbuild-python/src/bindings/_helpers/discovery/configuration.rs index 3b6b868ec7..b7ff43f7eb 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/discovery/configuration.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/discovery/configuration.rs @@ -9,8 +9,8 @@ use crate::bindings::_helpers::contracts::project_config::{ }; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; +use crate::bindings::_helpers::records::construct::new_record_with; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; /// `DiscoveredProjectInputs` with the loaded configuration, inside the project discovery lifecycle. pub(crate) fn discover_project_configuration<'py>( @@ -28,8 +28,9 @@ pub(crate) fn discover_project_configuration<'py>( ], )?; within(&lifecycle, |_| { - call_with( - ®istry::class(py, RecordId::DiscoveredProjectInputs)?, + new_record_with( + py, + RecordId::DiscoveredProjectInputs, &[ ("project_dir", project_dir.clone()), ( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/discovery/declaration_records.rs b/crates/sqlbuild-python/src/bindings/_helpers/discovery/declaration_records.rs index 3f427818eb..06e03d02e1 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/discovery/declaration_records.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/discovery/declaration_records.rs @@ -16,7 +16,7 @@ use sqlbuild_discovery::tree::models::ProjectTree; use sqlbuild_sqltext::compiler::models::AuthoredValue; use std::sync::Arc; -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported, keywords}; +use crate::bindings::_helpers::compile_driver::python_calls::{imported, keywords}; use crate::bindings::_helpers::discovery::constants; use crate::bindings::_helpers::discovery::faults::{FileFaults, record_tuple}; use crate::bindings::_helpers::discovery::header_declarations::typed_constant_record; @@ -29,8 +29,8 @@ use crate::bindings::_helpers::discovery::sorted_keys::sorted_keys; use crate::bindings::_helpers::model_config::header_columns::schema_columns; use crate::bindings::_helpers::native_host::facade::FacadeHost; use crate::bindings::_helpers::native_host::host; +use crate::bindings::_helpers::records::construct::{enum_from_value, new_record, new_record_with}; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::sqltext::authored_values::map_to_python; use crate::bindings::models::{NativeDiscoverySession, NativeProjectTree}; @@ -72,7 +72,7 @@ impl<'py> FileContext<'_, 'py> { } fn record(&self, class: RecordId, fields: &Bound<'py, PyDict>) -> PyResult> { - registry::class(self.py(), class)?.call((), Some(fields)) + new_record(self.py(), class, (), Some(fields)) } } @@ -85,7 +85,7 @@ pub(crate) fn add_scope_fields( let py: Python<'_> = fields.py(); fields.set_item( "scope_kind", - registry::class(py, RecordId::ScopeKind)?.call1((scope.scope_kind,))?, + enum_from_value(py, RecordId::ScopeKind, scope.scope_kind)?, )?; for (name, path) in [ ("ownership_root", &scope.ownership_root), @@ -128,16 +128,21 @@ fn single_value<'py>( fn enum_file<'py>(context: &FileContext<'_, 'py>, file: &EnumFile) -> PyResult> { let py: Python<'py> = context.py(); - let member_class: Bound<'py, PyAny> = registry::class(py, RecordId::EnumMember)?; - let declaration_class: Bound<'py, PyAny> = registry::class(py, RecordId::EnumDeclaration)?; let mut declarations: Vec> = Vec::with_capacity(file.declarations.len()); for declaration in &file.declarations { let members: Vec> = authored(py, &declaration.members)? .iter() - .map(|(name, value)| call_with(&member_class, &[("name", name), ("value", value)])) + .map(|(name, value)| { + new_record_with( + py, + RecordId::EnumMember, + &[("name", name), ("value", value)], + ) + }) .collect::>()?; - declarations.push(call_with( - &declaration_class, + declarations.push(new_record_with( + py, + RecordId::EnumDeclaration, &[ ("name", declaration.name.as_str().into_bound_py_any(py)?), ("members", PyTuple::new(py, members)?.into_any()), @@ -189,13 +194,13 @@ pub(crate) fn source_locations<'py>( relative_path: &Bound<'py, PyAny>, rows: &[(String, LineColumnSpan)], ) -> PyResult> { - let class: Bound<'py, PyAny> = registry::class(py, RecordId::SourceLocation)?; let located: Bound<'py, PyDict> = PyDict::new(py); for (name, span) in rows { located.set_item( name, - call_with( - &class, + new_record_with( + py, + RecordId::SourceLocation, &[ ("path", relative_path.clone()), ("line", span.line.into_bound_py_any(py)?), @@ -215,8 +220,6 @@ fn schema_file<'py>( ) -> PyResult> { let py: Python<'py> = context.py(); let file_path: String = context.file_path.str()?.to_string(); - let declaration_class: Bound<'py, PyAny> = - registry::class(py, RecordId::ModelSchemaDeclaration)?; let mut declarations: Vec> = Vec::with_capacity(file.declarations.len()); for declaration in &file.declarations { let raw_columns: Bound<'py, PyAny> = @@ -230,8 +233,9 @@ fn schema_file<'py>( &source_locations(py, &context.relative_path, &declaration.column_locations)?, )? .into_any(); - declarations.push(call_with( - &declaration_class, + declarations.push(new_record_with( + py, + RecordId::ModelSchemaDeclaration, &[ ("name", declaration.name.as_str().into_bound_py_any(py)?), ( @@ -288,8 +292,9 @@ fn audit_block<'py>(py: Python<'py>, block: &AuditBlock) -> PyResult = if mode.is(&host::entry(py, FacadeHost::AuditEvaluationMode)?.getattr(MEASUREMENT_MODE)?) { - call_with( - ®istry::class(py, RecordId::MeasurementContract)?, + new_record_with( + py, + RecordId::MeasurementContract, &[ ("value_column", header_values.as_any().get_item("value")?), ( @@ -309,8 +314,9 @@ fn audit_block<'py>(py: Python<'py>, block: &AuditBlock) -> PyResult( let declaration_kind: &str = context.scope.map_or("", |scope| scope.declaration_kind); fields.set_item( "declaration_kind", - registry::class(py, RecordId::DeclarationKind)?.call1((declaration_kind,))?, + enum_from_value(py, RecordId::DeclarationKind, declaration_kind)?, )?; context.add_scope(&fields)?; context.record(RecordId::DiscoveredAuditFile, &fields) @@ -518,13 +524,13 @@ fn seed_records<'py, T>( files: &[ScopedFile], ) -> PyResult> { let py: Python<'py> = faults.project_dir.py(); - let class: Bound<'py, PyAny> = registry::class(py, RecordId::DiscoveredSeedFile)?; let seeds: Vec> = files .iter() .map(|scoped| { let relative: Py = path_object(py, tree, scoped.file.relative_path.clone())?; - call_with( - &class, + new_record_with( + py, + RecordId::DiscoveredSeedFile, &[ ("file_path", faults.project_dir.div(relative.bind(py))?), ("relative_path", path_class(py)?.call1((relative,))?), @@ -665,8 +671,9 @@ pub(crate) fn discover_adapter_file<'py>( if !file_path.call_method0("is_file")?.is_truthy()? { return Ok(py.None().into_bound(py)); } - call_with( - ®istry::class(py, RecordId::DiscoveredAdapterFile)?, + new_record_with( + py, + RecordId::DiscoveredAdapterFile, &[ ("file_path", file_path), ("relative_path", path_class(py)?.call1(("adapter.py",))?), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/discovery/description_inputs.rs b/crates/sqlbuild-python/src/bindings/_helpers/discovery/description_inputs.rs index dc659ce42d..eaf8f24422 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/discovery/description_inputs.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/discovery/description_inputs.rs @@ -1,10 +1,9 @@ //! Model description resolution for header-only tooling, without importing project Python. -use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; +use pyo3::prelude::{Bound, PyAny, PyModule, PyModuleMethods, PyResult, Python}; use pyo3::types::{PyCFunction, PyDict, PyFrozenSet}; use pyo3::{pyfunction, wrap_pyfunction}; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; use crate::bindings::_helpers::contracts::project_config::load_project_config_file; use crate::bindings::_helpers::discovery::declaration_records::discover_model_schema_files; use crate::bindings::_helpers::discovery::model_records::{ @@ -12,8 +11,8 @@ use crate::bindings::_helpers::discovery::model_records::{ }; use crate::bindings::_helpers::discovery::snapshot::in_snapshot; use crate::bindings::_helpers::model_config::described_models::externally_described_models as described_models; +use crate::bindings::_helpers::records::construct::{new_record, new_record_with}; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::render::declaration_indexes::public_model_schema_index; /// Models and model schemas, skipping files that cannot be read. @@ -31,13 +30,14 @@ pub(crate) fn discover_model_description_inputs<'py>( })? .into_any(); in_snapshot(project_dir, || { - call_with( - ®istry::class(py, RecordId::DiscoveredProjectInputs)?, + new_record_with( + py, + RecordId::DiscoveredProjectInputs, &[ ("project_config", project_config), ( "local_config", - registry::class(py, RecordId::LocalConfig)?.call0()?, + new_record(py, RecordId::LocalConfig, (), None)?, ), ( "model_files", diff --git a/crates/sqlbuild-python/src/bindings/_helpers/discovery/faults.rs b/crates/sqlbuild-python/src/bindings/_helpers/discovery/faults.rs index dcecb736de..62ab716dc5 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/discovery/faults.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/discovery/faults.rs @@ -4,9 +4,8 @@ use pyo3::exceptions::{PyOSError, PySyntaxError, PyUnicodeError, PyValueError}; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyErr, PyResult, Python}; use pyo3::types::PyTuple; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; +use crate::bindings::_helpers::records::construct::new_record_with; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; const PROJECT_DIR_MARKER: &str = "."; @@ -45,8 +44,9 @@ impl<'py> FileFaults<'py> { .value(py) .str()? .call_method1("replace", (self.project_dir.str()?, PROJECT_DIR_MARKER))?; - on_fault.call1((call_with( - ®istry::class(py, RecordId::DiscoveryFileFault)?, + on_fault.call1((new_record_with( + py, + RecordId::DiscoveryFileFault, &[("path", relative_path.clone()), ("message", message)], )?,))?; Ok(None) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/discovery/guidance.rs b/crates/sqlbuild-python/src/bindings/_helpers/discovery/guidance.rs index 7343bc751e..5537bccfc0 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/discovery/guidance.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/discovery/guidance.rs @@ -29,11 +29,7 @@ pub(crate) fn sql_analysis_guidance( } else { native_field(discovered_inputs, "project_config")? }; - if owner - .getattr("settings")? - .getattr(SQL_ANALYSIS_KEY)? - .is_truthy()? - { + if native_field(&native_field(&owner, "settings")?, SQL_ANALYSIS_KEY)?.is_truthy()? { return Ok(None); } Ok(Some(analysis_off(local_override, purpose))) @@ -55,7 +51,7 @@ pub(crate) fn microbatch_guidance() -> (String, String) { #[pyfunction] #[pyo3(signature = (*, allowed))] -fn explicit_references_help(allowed: &str) -> String { +pub(crate) fn explicit_references_help(allowed: &str) -> String { references_help(allowed) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/discovery/header_declarations.rs b/crates/sqlbuild-python/src/bindings/_helpers/discovery/header_declarations.rs index d8223255b9..6db76378e9 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/discovery/header_declarations.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/discovery/header_declarations.rs @@ -18,6 +18,7 @@ use sqlbuild_model_config::sql_values::models::RawSqlValue; use crate::bindings::_helpers::contracts::sql_values::{ SqlValueClasses, raw_value, sql_value, sql_value_error, }; +use crate::bindings::_helpers::records::construct::{enum_from_value, new_record}; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -51,7 +52,6 @@ fn enum_record<'py>( facts: EnumFacts, owner: &Owner<'py>, ) -> PyResult> { - let member_class = registry::class(py, RecordId::EnumMember)?; let int_class = py.import("builtins")?.getattr("int")?; let members = facts .members @@ -61,16 +61,21 @@ fn enum_record<'py>( EnumValue::Text(text) => text.into_bound_py_any(py)?, EnumValue::Integer(digits) => int_class.call1((digits,))?, }; - member_class.call1((name, value)) + new_record(py, RecordId::EnumMember, (name, value), None) }) .collect::>>()?; - registry::class(py, RecordId::EnumDeclaration)?.call1(( - facts.name, - PyTuple::new(py, members)?, - facts.scalar_type, - &owner.relative_path, - owner.model_name.as_deref(), - )) + new_record( + py, + RecordId::EnumDeclaration, + ( + facts.name, + PyTuple::new(py, members)?, + facts.scalar_type, + &owner.relative_path, + owner.model_name.as_deref(), + ), + None, + ) } fn constant_record<'py>( @@ -80,18 +85,21 @@ fn constant_record<'py>( classes: &SqlValueClasses<'py>, ) -> PyResult> { let render_as = match facts.render_as { - Some(rendering) => { - registry::class(py, RecordId::CollectionRendering)?.call1((rendering,))? - } + Some(rendering) => enum_from_value(py, RecordId::CollectionRendering, rendering)?, None => py.None().into_bound(py), }; - registry::class(py, RecordId::ConstantDeclaration)?.call1(( - facts.name, - sql_value(&facts.value, classes)?, - &owner.relative_path, - owner.model_name.as_deref(), - render_as, - )) + new_record( + py, + RecordId::ConstantDeclaration, + ( + facts.name, + sql_value(&facts.value, classes)?, + &owner.relative_path, + owner.model_name.as_deref(), + render_as, + ), + None, + ) } fn optional_raw( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/discovery/header_values.rs b/crates/sqlbuild-python/src/bindings/_helpers/discovery/header_values.rs index ff016dd6db..602b483475 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/discovery/header_values.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/discovery/header_values.rs @@ -6,6 +6,7 @@ use pyo3::types::{PyDict, PyDictMethods, PyList, PyListMethods, PyTuple}; use pyo3::{pyfunction, wrap_pyfunction}; use crate::bindings::_helpers::boundary::keyword_request::Request; +use crate::bindings::_helpers::records::construct::new_record; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::sqltext::model_headers::{ @@ -101,7 +102,6 @@ fn source_locations<'py>( relative_path: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = relative_path.py(); - let location_class: Bound<'py, PyAny> = registry::class(py, RecordId::SourceLocation)?; let locations: Bound<'py, PyDict> = PyDict::new(py); for (name, line, column, end_line, end_column) in rows { let arguments: Bound<'py, PyDict> = PyDict::new(py); @@ -110,7 +110,10 @@ fn source_locations<'py>( arguments.set_item("column", column)?; arguments.set_item("end_line", end_line)?; arguments.set_item("end_column", end_column)?; - locations.set_item(name, location_class.call((), Some(&arguments))?)?; + locations.set_item( + name, + new_record(py, RecordId::SourceLocation, (), Some(&arguments))?, + )?; } Ok(locations) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/discovery/hook_entries.rs b/crates/sqlbuild-python/src/bindings/_helpers/discovery/hook_entries.rs index db3314521e..785b0f78a6 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/discovery/hook_entries.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/discovery/hook_entries.rs @@ -4,9 +4,9 @@ use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyRes use pyo3::types::{PyDict, PyDictMethods, PyList, PyListMethods, PyTuple}; use pyo3::{pyfunction, wrap_pyfunction}; +use crate::bindings::_helpers::records::construct::record_class; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; const RELATIVE_PATH_FIELD: &str = "relative_path"; const SQL_HOOK_FIELDS: [&str; 6] = [ @@ -25,7 +25,7 @@ fn sql_hook<'py>(entry: &Bound<'py, PyAny>, sql_fields: &[String]) -> PyResult = entry.getattr(*name)?; + let mut value: Bound<'py, PyAny> = native_field(entry, name)?; if value.is_none() { continue; } @@ -48,8 +48,8 @@ pub(crate) fn hook_entries<'py>( if !value.is_instance_of::() && !value.is_instance_of::() { return Ok(hooks); } - let sql_class: Bound<'py, PyAny> = registry::class(py, RecordId::SqlHookEntry)?; - let python_class: Bound<'py, PyAny> = registry::class(py, RecordId::PythonHookEntry)?; + let sql_class: Bound<'py, PyAny> = record_class(py, RecordId::SqlHookEntry)?; + let python_class: Bound<'py, PyAny> = record_class(py, RecordId::PythonHookEntry)?; for entry in value.try_iter()? { let entry: Bound<'py, PyAny> = entry?; if entry.is_instance(&sql_class)? { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/discovery/imported_modules.rs b/crates/sqlbuild-python/src/bindings/_helpers/discovery/imported_modules.rs index ba8affd828..400c8c0c51 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/discovery/imported_modules.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/discovery/imported_modules.rs @@ -10,6 +10,7 @@ use crate::bindings::_helpers::discovery::python_module_files::discover_python_m use crate::bindings::_helpers::event_exporting::scope::current_scope; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::native_host::user_code::UserCodeHost; +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::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -105,7 +106,7 @@ pub(crate) fn discovered<'py>( class_name: RecordId, pairs: &[(&str, Bound<'py, PyAny>)], ) -> PyResult> { - call_with(®istry::class(py, class_name)?, pairs) + new_record_with(py, class_name, pairs) } /// `sqlbuild._native.(message)`. diff --git a/crates/sqlbuild-python/src/bindings/_helpers/discovery/imported_providers.rs b/crates/sqlbuild-python/src/bindings/_helpers/discovery/imported_providers.rs index 4653475d06..a9e33da9d4 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/discovery/imported_providers.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/discovery/imported_providers.rs @@ -336,7 +336,7 @@ pub(crate) fn bind_event_exporter_declarations<'py>( "event_kinds", "min_severity", ] { - pairs.push((field, declaration.getattr(field)?)); + pairs.push((field, native_field(&declaration, field)?)); } pairs.push(("provider_usages", usages)); bound.push(discovered(py, RecordId::DiscoveredEventExporter, &pairs)?); diff --git a/crates/sqlbuild-python/src/bindings/_helpers/discovery/imported_python_nodes.rs b/crates/sqlbuild-python/src/bindings/_helpers/discovery/imported_python_nodes.rs index 22de979c8b..4f1ee4a910 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/discovery/imported_python_nodes.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/discovery/imported_python_nodes.rs @@ -12,6 +12,7 @@ use crate::bindings::_helpers::discovery::imported_modules::{ module_functions, provider_by_name, provider_usages, shim, tuple_of, }; use crate::bindings::_helpers::discovery::imported_providers; +use crate::bindings::_helpers::records::construct::new_record; use crate::bindings::_helpers::discovery::python_hook_files::discover_python_hook_files; use crate::bindings::_helpers::graph::python_node_graph::node_identity; @@ -19,7 +20,6 @@ use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::native_host::user_code::UserCodeHost; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; -use crate::bindings::_helpers::records::registry; use pyo3::IntoPyObjectExt; use sqlbuild_core::constants::PYTHON_NODE_ROOT; const NODE_ERROR: ErrorId = ErrorId::PythonNodeDiscovery; @@ -135,7 +135,7 @@ impl<'py> Buckets<'py> { ("function", function.clone()), ]; for field in kind.fields { - pairs.push((field, read.getattr(*field)?)); + pairs.push((field, native_field(&read, field)?)); } pairs.push(("provider_usages", provider_usages(function, providers)?)); let get = |name: &str| -> PyResult> { @@ -503,10 +503,12 @@ pub(crate) fn discover_hook_functions<'py>( ] { arguments.set_item(field, value)?; } - hooks.push( - registry::class(py, RecordId::DiscoveredHookFunction)? - .call((), Some(&arguments))?, - ); + hooks.push(new_record( + py, + RecordId::DiscoveredHookFunction, + (), + Some(&arguments), + )?); } } tuple_of(py, hooks) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/discovery/model_file_record.rs b/crates/sqlbuild-python/src/bindings/_helpers/discovery/model_file_record.rs index cfaaca1bb7..5f2b073cee 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/discovery/model_file_record.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/discovery/model_file_record.rs @@ -1,7 +1,7 @@ //! A discovered SQL model file as a native record whose Python attributes are built on demand. +use crate::bindings::_helpers::records::construct::new_record; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use pyo3::exceptions::PyTypeError; use pyo3::prelude::{Bound, Py, PyAny, PyAnyMethods, PyResult, Python}; use pyo3::sync::PyOnceLock; @@ -62,16 +62,20 @@ impl Locations { Ok(self .projected .get_or_try_init(py, || { - let class: Bound<'py, PyAny> = registry::class(py, RecordId::SourceLocation)?; let dict: Bound<'py, PyDict> = PyDict::new(py); for (name, span) in &self.rows { - let location: Bound<'py, PyAny> = class.call1(( - path.bind(py), - span.line, - span.column, - span.end_line, - span.end_column, - ))?; + let location: Bound<'py, PyAny> = new_record( + py, + RecordId::SourceLocation, + ( + path.bind(py), + span.line, + span.column, + span.end_line, + span.end_column, + ), + None, + )?; dict.set_item(name, location)?; } Ok::<_, pyo3::PyErr>(dict.unbind()) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/discovery/model_records.rs b/crates/sqlbuild-python/src/bindings/_helpers/discovery/model_records.rs index e7bba18f30..3b11b2612d 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/discovery/model_records.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/discovery/model_records.rs @@ -14,6 +14,7 @@ use crate::bindings::_helpers::discovery::project_files::{model_record, path_obj use crate::bindings::_helpers::discovery::raised::{failure_error, read_error, stage_error}; use crate::bindings::_helpers::discovery::snapshot::{in_snapshot, project_root, project_tree}; use crate::bindings::_helpers::discovery::sorted_keys::sorted_keys; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::models::NativeProjectTree; const PATHLIB_MODULE: &str = "pathlib"; @@ -91,7 +92,7 @@ pub(crate) fn discovered_model_files<'py>( let mut records: Vec> = Vec::with_capacity(items.len()); for (relative_path, built) in items { if let Some(selected) = &selected - && !selected.contains(relative_path.getattr(STEM_ATTRIBUTE)?)? + && !selected.contains(native_field(&relative_path, STEM_ATTRIBUTE)?)? { continue; } @@ -103,7 +104,7 @@ pub(crate) fn discovered_model_files<'py>( /// Discover the (selected) model files natively, reporting failing files to `on_fault`. #[pyfunction] #[pyo3(signature = (**options))] -fn discover_model_files<'py>( +pub(crate) fn discover_model_files<'py>( py: Python<'py>, options: Option>, ) -> PyResult> { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/discovery/project_facts.rs b/crates/sqlbuild-python/src/bindings/_helpers/discovery/project_facts.rs index 0bc9b73ea8..c894f0bac0 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/discovery/project_facts.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/discovery/project_facts.rs @@ -80,23 +80,23 @@ pub(crate) fn each( read: impl Fn(&Bound<'_, PyAny>) -> PyResult, ) -> PyResult> { let mut items: Vec = Vec::new(); - for item in owner.getattr(name)?.try_iter()? { + for item in native_field(owner, name)?.try_iter()? { items.push(read(&item?)?); } Ok(items) } fn text(owner: &Bound<'_, PyAny>, name: &str) -> PyResult { - Ok(owner.getattr(name)?.str()?.to_string()) + Ok(native_field(owner, name)?.str()?.to_string()) } fn optional_text(owner: &Bound<'_, PyAny>, name: &str) -> PyResult> { - owner.getattr(name)?.extract() + native_field(owner, name)?.extract() } /// `str(Path(...).stem)` of the path attribute `name`. pub(crate) fn stem(owner: &Bound<'_, PyAny>, name: &str) -> PyResult { - native_field(&owner.getattr(name)?, "stem")?.extract() + native_field(&native_field(owner, name)?, "stem")?.extract() } /// `(resource.name, str(resource.relative_path))`. @@ -124,7 +124,7 @@ fn relative_stem_named(file: &Bound<'_, PyAny>) -> PyResult { } fn enum_value(owner: &Bound<'_, PyAny>, name: &str) -> PyResult { - native_field(&owner.getattr(name)?, "value")?.extract() + native_field(&native_field(owner, name)?, "value")?.extract() } fn model_fact(model: &Bound<'_, PyAny>) -> PyResult { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/discovery/project_files.rs b/crates/sqlbuild-python/src/bindings/_helpers/discovery/project_files.rs index 405ac70b26..2042228633 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/discovery/project_files.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/discovery/project_files.rs @@ -17,6 +17,7 @@ use crate::bindings::_helpers::discovery::header_declarations::{ }; use crate::bindings::_helpers::discovery::model_file_record::{ModelFileParts, NativeModelFile}; use crate::bindings::_helpers::discovery::raised::failure_error; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::sqltext::authored_values::map_to_python; use crate::bindings::models::{ModelTextRequest, NativeProjectTree}; use crate::bindings::types::CompilerDetach; @@ -60,7 +61,7 @@ pub(crate) fn model_record<'py>( let header_values: Bound<'py, PyDict> = map_to_python(py, model.header_values)? .into_bound(py) .downcast_into::()?; - let model_name: String = file_path.getattr(STEM_ATTRIBUTE)?.extract()?; + let model_name: String = native_field(&file_path, STEM_ATTRIBUTE)?.extract()?; let declarations: HeaderDeclarations<'py> = match model_header_declarations( py, ( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/discovery/project_validation.rs b/crates/sqlbuild-python/src/bindings/_helpers/discovery/project_validation.rs index f2246ac55a..92d7a38846 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/discovery/project_validation.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/discovery/project_validation.rs @@ -60,7 +60,7 @@ fn validate_discovered_project_inputs( } raised( py.import(BUILTINS_MODULE) - .and_then(|module| module.getattr(error.class.as_str())) + .and_then(|module| native_field(&module, error.class.as_str())) .and_then(|class| class.call1((error.message,))), ) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/discovery/python_function_files.rs b/crates/sqlbuild-python/src/bindings/_helpers/discovery/python_function_files.rs index 0e218d3d75..b28b116fa7 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/discovery/python_function_files.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/discovery/python_function_files.rs @@ -17,11 +17,11 @@ use sqlbuild_pysource::udf_functions::main::udf_function::udf_function; use sqlbuild_pysource::udf_functions::models::{UdfFunction, UdfShape}; use crate::bindings::_helpers::boundary::panics::compiler_error; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; use crate::bindings::_helpers::discovery::declaration_records::path_class; use crate::bindings::_helpers::discovery::project_files::path_object; use crate::bindings::_helpers::discovery::raised::{failure_error, read_error, stage_error}; use crate::bindings::_helpers::discovery::snapshot::{in_snapshot, project_root, project_tree}; +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::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -250,8 +250,9 @@ fn function_record<'py>( contents: PyString::new(py, &contents), }; let (header_values, entry_point, body_python) = parsed_function(&source)?; - call_with( - ®istry::class(py, RecordId::DiscoveredPythonFunctionFile)?, + new_record_with( + py, + RecordId::DiscoveredPythonFunctionFile, &[ ("file_path", source.file_path.clone()), ("relative_path", source.relative_path.clone()), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/discovery/scope_snapshot.rs b/crates/sqlbuild-python/src/bindings/_helpers/discovery/scope_snapshot.rs index 63f9f7ee77..c73a2bfbae 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/discovery/scope_snapshot.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/discovery/scope_snapshot.rs @@ -7,7 +7,6 @@ use pyo3::prelude::{ use pyo3::types::{PyDict, PyDictMethods, PyList, PyListMethods, PyString, PyTuple}; use pyo3::{pyfunction, wrap_pyfunction}; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; use crate::bindings::_helpers::contracts::project_config::{ load_local_config_file, load_project_config_file, }; @@ -23,10 +22,10 @@ use crate::bindings::_helpers::discovery::sql_test_records::{ discover_scenario_files, discover_test_files, }; use crate::bindings::_helpers::discovery::yaml_records::discover_source_files; +use crate::bindings::_helpers::records::construct::{new_record, new_record_with}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use sqlbuild_core::constants::PROJECT_CONFIG_FILENAME; const LOCAL_CONFIG_FILENAME: &str = "sqlbuild_local.toml"; const FALLBACK_PROJECT_NAME: &str = "project"; @@ -66,8 +65,9 @@ impl<'py> Faults<'py> { Some(path) => path_class(py)?.call1((path,))?, None => py.None().into_bound(py), }; - call_with( - ®istry::class(py, RecordId::DiscoveryFileFault)?, + new_record_with( + py, + RecordId::DiscoveryFileFault, &[ ("path", path), ("message", PyString::new(py, &message).into_any()), @@ -120,8 +120,9 @@ fn configs<'py>( } else { PyString::new(py, FALLBACK_PROJECT_NAME).into_any() }; - call_with( - ®istry::class(py, RecordId::ProjectConfig)?, + new_record_with( + py, + RecordId::ProjectConfig, &[ ("name", name), ("adapter", PyString::new(py, FALLBACK_ADAPTER).into_any()), @@ -133,7 +134,7 @@ fn configs<'py>( faults, (&list, Some(LOCAL_CONFIG_FILENAME)), load_local_config_file(py, project_dir).map(|config| config.into_bound(py)), - || registry::class(py, RecordId::LocalConfig)?.call0(), + || new_record(py, RecordId::LocalConfig, (), None), )?; Ok((project_config, local_config, list)) } @@ -228,9 +229,10 @@ fn tolerant_discovery<'py>(project_dir: &Bound<'py, PyAny>) -> PyResult = - registry::class(py, RecordId::DiscoveredProjectInputs)?.call((), Some(&inputs))?; - call_with( - ®istry::class(py, RecordId::TolerantScopeDiscovery)?, + new_record(py, RecordId::DiscoveredProjectInputs, (), Some(&inputs))?; + new_record_with( + py, + RecordId::TolerantScopeDiscovery, &[ ("discovered_inputs", discovered_inputs), ( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/discovery/snapshot.rs b/crates/sqlbuild-python/src/bindings/_helpers/discovery/snapshot.rs index 6aa4fcd003..54668afd05 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/discovery/snapshot.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/discovery/snapshot.rs @@ -153,7 +153,9 @@ impl NativeDirectorySnapshotScope { /// Share one directory snapshot across every discovery call for `project_dir` inside the scope. #[pyfunction] #[pyo3(signature = (*, project_dir))] -fn directory_snapshot_scope(project_dir: Bound<'_, PyAny>) -> NativeDirectorySnapshotScope { +pub(crate) fn directory_snapshot_scope( + project_dir: Bound<'_, PyAny>, +) -> NativeDirectorySnapshotScope { NativeDirectorySnapshotScope { project_dir: project_dir.unbind(), token: None, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/discovery/sql_test_blocks.rs b/crates/sqlbuild-python/src/bindings/_helpers/discovery/sql_test_blocks.rs index 0c7ff037c9..7b28d86fc3 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/discovery/sql_test_blocks.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/discovery/sql_test_blocks.rs @@ -11,6 +11,7 @@ use sqlbuild_model_config::sql_values::models::{RawKey, RawSqlValue}; use crate::bindings::_helpers::contracts::sql_values::{SqlValueClasses, raw_value, sql_value}; use crate::bindings::_helpers::native_host::facade::FacadeHost; use crate::bindings::_helpers::native_host::host; +use crate::bindings::_helpers::records::construct::{enum_from_value, new_record}; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -44,11 +45,12 @@ fn case_record<'py>( [name.into_bound_py_any(py)?, sql_value(value, classes)?], )?); } - registry::class(py, RecordId::DiscoveredSqlTestCase)?.call1(( - case.name.as_str(), - PyTuple::new(py, values)?, - case_index, - )) + new_record( + py, + RecordId::DiscoveredSqlTestCase, + (case.name.as_str(), PyTuple::new(py, values)?, case_index), + None, + ) } fn block_record<'py>( @@ -57,16 +59,20 @@ fn block_record<'py>( block: SqlTestBlock, classes: &SqlValueClasses<'py>, ) -> PyResult> { - let kind = registry::class(py, RecordId::SqlValueKind)?; let parameters = block .parameters .iter() .map(|parameter| { - registry::class(py, RecordId::SqlTestParameterDeclaration)?.call1(( - parameter.name.as_str(), - kind.call1((parameter.value_type,))?, - parameter.nullable, - )) + new_record( + py, + RecordId::SqlTestParameterDeclaration, + ( + parameter.name.as_str(), + enum_from_value(py, RecordId::SqlValueKind, parameter.value_type)?, + parameter.nullable, + ), + None, + ) }) .collect::>>()?; let cases = block @@ -89,7 +95,7 @@ fn block_record<'py>( keywords.set_item("cursor_start", block.cursor_start)?; keywords.set_item("cursor_end", block.cursor_end)?; keywords.set_item("sql_body_span", &native.2)?; - registry::class(py, RecordId::DiscoveredSqlTestBlock)?.call((), Some(&keywords)) + new_record(py, RecordId::DiscoveredSqlTestBlock, (), Some(&keywords)) } /// Each `(header, sql_body, span)` block of a SQL test file as a `DiscoveredSqlTestBlock`. diff --git a/crates/sqlbuild-python/src/bindings/_helpers/discovery/sql_test_records.rs b/crates/sqlbuild-python/src/bindings/_helpers/discovery/sql_test_records.rs index bc691b6d4f..7fcbca43fa 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/discovery/sql_test_records.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/discovery/sql_test_records.rs @@ -15,7 +15,7 @@ use sqlbuild_discovery::sql_tests::models::{ }; use sqlbuild_discovery::tree::models::ProjectTree; -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; +use crate::bindings::_helpers::compile_driver::python_calls::imported; use crate::bindings::_helpers::discovery::constants; use crate::bindings::_helpers::discovery::faults::{FileFaults, record_tuple}; use crate::bindings::_helpers::discovery::project_files::path_object; @@ -27,9 +27,9 @@ use crate::bindings::_helpers::discovery::sorted_keys::sorted_keys; use crate::bindings::_helpers::discovery::sql_test_blocks::{ sql_test_block_records, validate_block_names, }; +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; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::sqltext::authored_values::map_to_python; use crate::bindings::models::NativeProjectTree; @@ -128,8 +128,9 @@ fn test_file<'py>( ) -> PyResult> { let (file_path, relative_path) = paths; let py: Python<'py> = file_path.py(); - call_with( - ®istry::class(py, RecordId::DiscoveredSqlTestFile)?, + new_record_with( + py, + RecordId::DiscoveredSqlTestFile, &[ ("file_path", file_path.clone()), ("relative_path", relative_path.clone()), @@ -181,8 +182,9 @@ fn scenario_file<'py>( "SQL scenario '{shown}' must define SQL after SCENARIO(...)" )); } - call_with( - ®istry::class(py, RecordId::DiscoveredSqlScenarioFile)?, + new_record_with( + py, + RecordId::DiscoveredSqlScenarioFile, &[ ("file_path", file_path.clone()), ("relative_path", relative_path.clone()), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/discovery/yaml_records.rs b/crates/sqlbuild-python/src/bindings/_helpers/discovery/yaml_records.rs index a70c58da43..6349270505 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/discovery/yaml_records.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/discovery/yaml_records.rs @@ -17,7 +17,7 @@ use sqlbuild_discovery::yaml_files::models::{YamlFileOutcome, YamlPathFailure}; use std::path::PathBuf; use crate::bindings::_helpers::boundary::config_values::config_value_to_python; -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; +use crate::bindings::_helpers::compile_driver::python_calls::imported; use crate::bindings::_helpers::discovery::faults::{FileFaults, record_tuple}; use crate::bindings::_helpers::discovery::project_files::path_object; use crate::bindings::_helpers::discovery::raised::{ @@ -29,8 +29,8 @@ use crate::bindings::_helpers::discovery::snapshot::{ use crate::bindings::_helpers::discovery::yaml_entries::{ parse_schema_document, parse_sources_document, }; +use crate::bindings::_helpers::records::construct::new_record_with; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use crate::bindings::models::{NativeProjectTree, YamlDiscoveryRequest}; /// The most YAML bytes one native load reads before its files are built and released. @@ -223,7 +223,6 @@ pub(crate) fn discover_source_files<'py>( project_dir: project_dir.clone(), on_fault, }; - let class: Bound<'py, PyAny> = registry::class(py, RecordId::DiscoveredSourceFile)?; let mut records: Vec> = Vec::new(); load_files( project_dir, @@ -231,8 +230,9 @@ pub(crate) fn discover_source_files<'py>( |(relative_path, file_path), built| { let record = built.and_then(|(contents, loaded)| { let shown: String = file_path.str()?.to_str()?.to_owned(); - call_with( - &class, + new_record_with( + py, + RecordId::DiscoveredSourceFile, &[ ("file_path", file_path.clone()), ("relative_path", relative_path.clone()), @@ -283,7 +283,6 @@ pub(crate) fn discover_schema_files<'py>( if paths.is_empty() { return record_tuple(py, Vec::new()); } - let class: Bound<'py, PyAny> = registry::class(py, RecordId::DiscoveredSchemaFile)?; let mut records: Vec> = Vec::new(); load_files( project_dir, @@ -291,8 +290,9 @@ pub(crate) fn discover_schema_files<'py>( |(relative_path, file_path), built| { let (contents, loaded) = built?; let shown: String = file_path.str()?.to_str()?.to_owned(); - records.push(call_with( - &class, + records.push(new_record_with( + py, + RecordId::DiscoveredSchemaFile, &[ ("file_path", file_path.clone()), ("relative_path", relative_path), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/event_exporting/dispatcher.rs b/crates/sqlbuild-python/src/bindings/_helpers/event_exporting/dispatcher.rs index 80863e67d3..87e2c5175d 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/event_exporting/dispatcher.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/event_exporting/dispatcher.rs @@ -326,7 +326,7 @@ pub(crate) fn stop_event_exporter_threads(py: Python<'_>) -> PyResult<()> { .try_iter()? { let thread: Bound<'_, PyAny> = thread?; - let Ok(worker) = thread.getattr(WORKER_ATTRIBUTE) else { + let Ok(worker) = native_field(&thread, WORKER_ATTRIBUTE) else { continue; }; if let Ok(worker) = worker.downcast::() { @@ -610,7 +610,7 @@ fn start_worker<'py>( impl EventExporterDispatcher { #[new] #[pyo3(signature = (**options))] - fn new(py: Python<'_>, options: Option>) -> PyResult { + pub(crate) fn new(py: Python<'_>, options: Option>) -> PyResult { let options: KeywordArguments<'_> = KeywordArguments::read( py, "EventExporterDispatcher", diff --git a/crates/sqlbuild-python/src/bindings/_helpers/event_exporting/scope.rs b/crates/sqlbuild-python/src/bindings/_helpers/event_exporting/scope.rs index 9179f5318a..be96283bf9 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/event_exporting/scope.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/event_exporting/scope.rs @@ -202,7 +202,7 @@ impl EventExporterCommandScope { impl EventExporterCommandScope { #[new] #[pyo3(signature = (*, dispatcher))] - fn new(dispatcher: Py) -> Self { + pub(crate) fn new(dispatcher: Py) -> Self { Self { dispatcher, state: Mutex::new(ScopeState { @@ -411,7 +411,7 @@ impl EventExporterScopeContext { /// Install one command-owned exporter scope. #[pyfunction] -fn event_exporter_command_scope(scope: Py) -> EventExporterScopeContext { +pub(crate) fn event_exporter_command_scope(scope: Py) -> EventExporterScopeContext { EventExporterScopeContext { scope, token: Mutex::new(None), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/event_exporting/values.rs b/crates/sqlbuild-python/src/bindings/_helpers/event_exporting/values.rs index 8b99a62e64..80ee687a90 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/event_exporting/values.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/event_exporting/values.rs @@ -14,8 +14,8 @@ use sqlbuild_core::lifecycle_events::main::catalog::catalog; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::native_host::user_code::UserCodeHost; +use crate::bindings::_helpers::records::construct::enum_from_value; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; /// `BoundEventExporter` parameters, in positional order. const BOUND_EXPORTER_PARAMETERS: [&str; 5] = [ @@ -52,7 +52,7 @@ pub(crate) fn severity_object( py: Python<'_>, severity: ExportSeverity, ) -> PyResult> { - registry::class(py, RecordId::EventExportSeverity)?.call1((severity.value(),)) + enum_from_value(py, RecordId::EventExportSeverity, severity.value()) } /// An `EventExportSeverity` or its string value, as `EventExportSeverity(value)` accepts it. diff --git a/crates/sqlbuild-python/src/bindings/_helpers/execution_history/conversion.rs b/crates/sqlbuild-python/src/bindings/_helpers/execution_history/conversion.rs index f56f2678ba..6e015d3b3c 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/execution_history/conversion.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/execution_history/conversion.rs @@ -260,7 +260,7 @@ pub(crate) fn envelope_of(event: &Bound<'_, PyAny>) -> PyResult { if known_event(event)? { envelope.schema_version = native_field(event, "schema_version")?.extract()?; for (slot, name) in envelope.text_fields.iter_mut().zip(ENVELOPE_TEXT_FIELDS) { - *slot = optional_text(&event.getattr(name)?)?; + *slot = optional_text(&native_field(event, name)?)?; } envelope.occurred_at = Some(utc_time(&native_field(event, "occurred_at")?)?); return Ok(envelope); @@ -300,7 +300,7 @@ pub(crate) fn projection_fact(stored: &Bound<'_, PyAny>) -> PyResult) -> PyResult { let optional_time = |name: &str| -> PyResult> { - let value: Bound<'_, PyAny> = record.getattr(name)?; + let value: Bound<'_, PyAny> = native_field(record, name)?; if value.is_none() { return Ok(None); } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/fingerprints/latest_rows.rs b/crates/sqlbuild-python/src/bindings/_helpers/fingerprints/latest_rows.rs index 3987efa485..407544c18f 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/fingerprints/latest_rows.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/fingerprints/latest_rows.rs @@ -2,6 +2,7 @@ use std::ffi::CString; +use crate::bindings::_helpers::records::construct::{new_record, record_class}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -37,7 +38,7 @@ pub(crate) fn fingerprint_set_from_rows<'py>( qualified_name: &str, rows: &Bound<'py, PyAny>, ) -> PyResult> { - let fingerprint_class: Bound<'py, PyAny> = registry::class(py, RecordId::Fingerprint)?; + let fingerprint_class: Bound<'py, PyAny> = record_class(py, RecordId::Fingerprint)?; let from_iso: Bound<'py, PyAny> = py .import(DATETIME_MODULE)? .getattr("datetime")? @@ -109,7 +110,7 @@ fn indexed_set<'py>( kwargs.set_item("schema", schema)?; kwargs.set_item("fingerprints", by_name)?; kwargs.set_item("fingerprints_by_identity", by_identity)?; - registry::class(py, RecordId::FingerprintSet)?.call((), Some(&kwargs)) + new_record(py, RecordId::FingerprintSet, (), Some(&kwargs)) } fn identity_of(fingerprint: &Bound<'_, PyAny>) -> PyResult<(String, String)> { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/fingerprints/storage_io.rs b/crates/sqlbuild-python/src/bindings/_helpers/fingerprints/storage_io.rs index f722831ae8..d2beed1e6a 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/fingerprints/storage_io.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/fingerprints/storage_io.rs @@ -19,12 +19,12 @@ use crate::bindings::_helpers::fingerprints::latest_rows::{ use crate::bindings::_helpers::fingerprints::storage_sql::{ fingerprint_insert_batches, fingerprint_insert_sql, }; +use crate::bindings::_helpers::records::construct::new_record; use crate::bindings::_helpers::native_host::adapter::AdapterHost; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use sqlbuild_core::constants::FINGERPRINT_TABLE_NAME; const TIME_MODULE: &str = "time"; const LOGGING_MODULE: &str = "logging"; @@ -272,7 +272,7 @@ fn empty_set<'py>(py: Python<'py>, schema: &str) -> PyResult> let keywords: Bound<'py, PyDict> = PyDict::new(py); keywords.set_item("schema", schema)?; keywords.set_item("fingerprints", PyDict::new(py))?; - registry::class(py, RecordId::FingerprintSet)?.call((), Some(&keywords)) + new_record(py, RecordId::FingerprintSet, (), Some(&keywords)) } /// `FingerprintInputError` for an unreadable table, caused by the warehouse error. diff --git a/crates/sqlbuild-python/src/bindings/_helpers/fingerprints/storage_sql.rs b/crates/sqlbuild-python/src/bindings/_helpers/fingerprints/storage_sql.rs index 2ebc2de696..15da3c0292 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/fingerprints/storage_sql.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/fingerprints/storage_sql.rs @@ -66,7 +66,7 @@ fn stored_fingerprint(fingerprint: &Bound<'_, PyAny>) -> PyResult, name: &str) -> PyResult { - fingerprint.getattr(name)?.str()?.extract() + native_field(fingerprint, name)?.str()?.extract() } fn storage_error(py: Python<'_>, error: FingerprintStorageError) -> PyErr { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/format_command/command.rs b/crates/sqlbuild-python/src/bindings/_helpers/format_command/command.rs index 67f7efd85d..9ad208bc0e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/format_command/command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/format_command/command.rs @@ -142,10 +142,11 @@ fn fixture_inputs<'py>(resolved: &Resolved<'py>) -> PyResult(py: Python<'py>, options: Option>) -> PyResult { +pub(crate) fn run_format_command<'py>( + py: Python<'py>, + options: Option>, +) -> PyResult { let arguments: KeywordArguments<'py> = KeywordArguments::read( py, "run_format_command", diff --git a/crates/sqlbuild-python/src/bindings/_helpers/format_command/config.rs b/crates/sqlbuild-python/src/bindings/_helpers/format_command/config.rs index a2b573c76b..64a98f3881 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/format_command/config.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/format_command/config.rs @@ -9,9 +9,9 @@ use pyo3::types::PyString; use sqlbuild_rules::lint_text::main::resolve_format_settings::resolve_format_settings; use sqlbuild_rules::lint_text::models::FormatSettings; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; -use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::construct::new_record_with; +use crate::bindings::_helpers::records::ids::RecordId; +use crate::bindings::_helpers::rules::rules_config::load_rules_config; /// Format settings and whether lint suppressions are allowed. pub(crate) struct FormatConfig { @@ -28,10 +28,13 @@ pub(crate) fn format_config( let settings: FormatSettings = resolve_format_settings(Path::new(&directory)).map_err(PyValueError::new_err)?; let allow_suppressions: bool = if settings.has_project_config { - call_with( - ®istry::native(py, NativeAttr::LoadRulesConfig)?, - &[("project_dir", project_dir.call_method0("resolve")?)], - )? + load_rules_config( + py, + project_dir + .call_method0("resolve")? + .extract::()?, + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))? .getattr("allow_exceptions")? .extract()? } else { @@ -50,8 +53,9 @@ pub(crate) fn lint_config<'py>( dialect: &str, ) -> PyResult> { let settings: &FormatSettings = &config.settings; - call_with( - ®istry::class(py, RecordId::LintConfig)?, + new_record_with( + py, + RecordId::LintConfig, &[ ( "line_width", diff --git a/crates/sqlbuild-python/src/bindings/_helpers/format_command/files.rs b/crates/sqlbuild-python/src/bindings/_helpers/format_command/files.rs index 7f607c3774..0b4e817f02 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/format_command/files.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/format_command/files.rs @@ -91,7 +91,7 @@ fn write_atomically(path: &Path, contents: &str) -> PyResult<()> { /// `write_atomically(*, path, contents)`: replace a text file atomically, keeping its mode. #[pyfunction] #[pyo3(name = "write_atomically", signature = (*, path, contents))] -fn write_file_atomically(path: PathBuf, contents: &str) -> PyResult<()> { +pub(crate) fn write_file_atomically(path: PathBuf, contents: &str) -> PyResult<()> { write_atomically(&path, contents) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/format_command/run.rs b/crates/sqlbuild-python/src/bindings/_helpers/format_command/run.rs index 9677ca951b..341895c43e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/format_command/run.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/format_command/run.rs @@ -4,21 +4,29 @@ use std::collections::BTreeSet; use std::path::PathBuf; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; -use pyo3::types::{PyBool, PyDict, PyDictMethods, PyList, PyListMethods, PyTuple}; +use pyo3::types::{PyBool, PyDict, PyDictMethods, PyList, PyListMethods}; use sqlbuild_core::format_command::main::collect_lint_files::collect_lint_files; use crate::bindings::_helpers::boundary::panics::value_error; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; +use crate::bindings::_helpers::compile_driver::python_calls::keywords; +use crate::bindings::_helpers::compile_driver::stage_work::{ + FORMAT_LINT_BATCHES, FORMAT_LINT_FILES, count_stage, +}; use crate::bindings::_helpers::discovery::description_inputs::externally_described_models; use crate::bindings::_helpers::format_command::files::{ FormatChange, file_dictionary, overlaid, persist_changes, sorted_items, }; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr}; +use crate::bindings::_helpers::records::ids::ErrorId; use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::rules::dsl_headers::format_native_headers; +use crate::bindings::_helpers::rules::dsl_headers::reject_unparseable_header_rewrites; +use crate::bindings::_helpers::rules::dsl_headers::safe_format_files; use crate::bindings::_helpers::rules::dsl_headers::{header_violations, rules_config}; +use crate::bindings::_helpers::rules::fixture_formatting::format_redundant_fixture_nulls; use crate::bindings::_helpers::rules::lint_pipeline::{LintViolationRow, ViolationClasses}; use crate::bindings::_helpers::rules::lint_text::apply_lint_suppressions; +use crate::bindings::_helpers::rules::lint_text::format_lint_sql_bodies; use sqlbuild_rules::dsl_headers::main::scan_headers::scan_headers; use sqlbuild_rules::dsl_headers::models::{HeaderRulesConfig, HeaderViolation}; @@ -47,10 +55,6 @@ pub(crate) struct FormatOutcome<'py> { pub(crate) final_contents: Bound<'py, PyDict>, } -fn native<'py>(py: Python<'py>, name: NativeAttr) -> PyResult> { - registry::native(py, name) -} - /// Fixture-null rewrites proven safe by the contracts, when fixture inputs are known. fn fixture_autofix<'py>( request: &FormatRequest<'_, 'py>, @@ -61,15 +65,13 @@ fn fixture_autofix<'py>( let Some((discovered, syntax)) = fixture_inputs else { return Ok(PyDict::new(py)); }; - Ok(call_with( - ®istry::native(py, NativeAttr::FormatRedundantFixtureNulls)?, - &[ - ("files", files.clone().into_any()), - ("project_dir", request.project_dir.clone()), - ("discovered_inputs", discovered.clone()), - ("sql_lexical_syntax", syntax.clone()), - ], - )? + Ok(format_redundant_fixture_nulls( + &files.clone().into_any().cast_into::()?, + &request.project_dir.clone(), + &discovered.clone(), + &syntax.clone(), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))? .downcast_into::()?) } @@ -79,15 +81,19 @@ fn format_bodies<'py>( files: &Bound<'py, PyDict>, ) -> PyResult<(Bound<'py, PyDict>, Bound<'py, PyAny>)> { let py: Python<'py> = files.py(); - let items: Vec> = sorted_items(files)? + let items: Vec<(Bound<'py, PyAny>, String)> = sorted_items(files)? .into_iter() - .map(|(path, contents)| Ok(PyTuple::new(py, [path, contents])?.into_any())) + .map(|(path, contents)| Ok((path, contents.extract::()?))) .collect::>()?; - let result: Bound<'py, PyAny> = native(py, NativeAttr::FormatLintSqlBodies)?.call1(( - PyList::new(py, items)?, - request.project_dir, + count_stage(py, FORMAT_LINT_BATCHES, 1)?; + count_stage(py, FORMAT_LINT_FILES, items.len() as u64)?; + let result: Bound<'py, PyAny> = format_lint_sql_bodies( + py, + items, + request.project_dir.extract::()?, request.config, - ))?; + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; Ok(( result.get_item(0)?.downcast_into::()?, result.get_item(1)?, @@ -108,14 +114,9 @@ fn apply_fixes<'py>( } let current: Bound<'py, PyDict> = overlaid(files, &updated)?; for (path, contents) in sorted_items(¤t)? { - let result: Bound<'py, PyAny> = call_with( - &native(py, NativeAttr::FormatNativeHeaders)?, - &[ - ("contents", contents.clone()), - ("file_path", path.clone()), - ("config", request.config.clone()), - ], - )?; + let result: Bound<'py, PyAny> = + format_native_headers(&contents.clone(), &path.clone(), &request.config.clone()) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; for fault in result.get_item(1)?.try_iter()? { faults.append(fault?)?; } @@ -139,14 +140,12 @@ fn apply_fixes<'py>( updated.update(fixture_formatted.as_mapping())?; updated.update(final_native.as_mapping())?; } - let result: Bound<'py, PyAny> = call_with( - &native(py, NativeAttr::RejectUnparseableHeaderRewrites)?, - &[ - ("updated", updated.into_any()), - ("config", request.config.clone()), - ("faults", faults.into_any()), - ], - )?; + let result: Bound<'py, PyAny> = reject_unparseable_header_rewrites( + &updated.into_any().cast_into::()?, + &request.config.clone(), + &faults.into_any().cast_into::()?, + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; Ok(( result.get_item(0)?.downcast_into::()?, result.get_item(1)?.downcast_into::()?, @@ -215,29 +214,32 @@ fn plan_rule_fixes<'py>( files: &Bound<'py, PyDict>, ) -> PyResult<(Bound<'py, PyDict>, Bound<'py, PyAny>, Bound<'py, PyAny>)> { let py: Python<'py> = files.py(); - let result: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::PlanRuleFixes)?, - &[ - ("project_dir", request.project_dir.clone()), - ("files", files.clone().into_any()), - ("config", request.config.clone()), - ( - "adapter", - request - .adapter - .map_or_else(|| py.None().into_bound(py), Clone::clone), - ), - ( - "discovered_inputs", - request - .discovered - .map_or_else(|| py.None().into_bound(py), Clone::clone), - ), - ( - "enabled", - PyBool::new(py, request.fix).to_owned().into_any(), - ), - ], + let result: Bound<'py, PyAny> = crate::bindings::_helpers::rules::fix_session::plan_rule_fixes( + py, + Some(keywords( + py, + &[ + ("project_dir", request.project_dir.clone()), + ("files", files.clone().into_any()), + ("config", request.config.clone()), + ( + "adapter", + request + .adapter + .map_or_else(|| py.None().into_bound(py), Clone::clone), + ), + ( + "discovered_inputs", + request + .discovered + .map_or_else(|| py.None().into_bound(py), Clone::clone), + ), + ( + "enabled", + PyBool::new(py, request.fix).to_owned().into_any(), + ), + ], + )?), )?; Ok(( result.get_item(0)?.downcast_into::()?, @@ -267,13 +269,11 @@ pub(crate) fn run_format<'py>(request: &FormatRequest<'_, 'py>) -> PyResult
= file_dictionary(py, collected)?; - let safe: Bound<'py, PyAny> = call_with( - &native(py, NativeAttr::SafeFormatFiles)?, - &[ - ("files", files.clone().into_any()), - ("config", request.config.clone()), - ], - )?; + let safe: Bound<'py, PyAny> = safe_format_files( + &files.clone().into_any().cast_into::()?, + &request.config.clone(), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; let newlines: Bound<'py, PyDict> = safe.get_item(1)?.downcast_into::()?; let (safe_files, rule_fixes, rule_faults) = plan_rule_fixes(request, &safe.get_item(0)?.downcast_into::()?)?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/format_command/selection.rs b/crates/sqlbuild-python/src/bindings/_helpers/format_command/selection.rs index 631ccf7ebc..dc6ccc1413 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/format_command/selection.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/format_command/selection.rs @@ -16,10 +16,24 @@ use sqlbuild_rules::lint_text::main::scan_fixture_null_candidates::scan_fixture_ use crate::bindings::_helpers::boundary::panics::value_error; use crate::bindings::_helpers::command_runtime::adapters::resolve_cli_adapter; use crate::bindings::_helpers::compile_driver::discovery::discover_project_inputs; +use crate::bindings::_helpers::compile_driver::project::build_project_graph; use crate::bindings::_helpers::compile_driver::python_calls::keywords; -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, within}; +use crate::bindings::_helpers::compile_driver::python_calls::within; +use crate::bindings::_helpers::contracts::project_config::load_local_config_file; +use crate::bindings::_helpers::contracts::project_config::load_project_config_file; +use crate::bindings::_helpers::contracts::spec_resolution::resolve_effective_adapter_name; +use crate::bindings::_helpers::discovery::declaration_records::discover_model_schema_files; +use crate::bindings::_helpers::discovery::model_records::discover_model_files; +use crate::bindings::_helpers::discovery::snapshot::directory_snapshot_scope; +use crate::bindings::_helpers::discovery::sql_test_records::discover_test_files; +use crate::bindings::_helpers::discovery::yaml_records::{ + discover_schema_files, discover_source_files, +}; +use crate::bindings::_helpers::planner_selection::selectors::select_project_graph; +use crate::bindings::_helpers::planner_selection::unit_tests::reject_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::ids::{ErrorId, NativeAttr, RecordId}; +use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; const SELECTION_CODE: &str = "S007"; @@ -73,16 +87,9 @@ fn discovered_adapter<'py>( project_dir: &Bound<'py, PyAny>, discovered: &Bound<'py, PyAny>, ) -> PyResult> { - let py: Python<'py> = project_dir.py(); - let adapter_name: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ResolveEffectiveAdapterName)?, - &[ - ( - "project_config", - native_field(discovered, "project_config")?, - ), - ("local_config", native_field(discovered, "local_config")?), - ], + let adapter_name: Bound<'py, PyAny> = resolve_effective_adapter_name( + &native_field(discovered, "project_config")?, + &native_field(discovered, "local_config")?, )?; resolve_cli_adapter(&adapter_name, project_dir) } @@ -156,23 +163,23 @@ fn model_paths( if let Some(paths) = exact_model_paths(py, scope, (select, exclude))? { return Ok(paths); } - let graph: Bound<'_, PyAny> = call_with( - ®istry::native(py, NativeAttr::BuildProjectGraph)?, - &[ - ("discovered_inputs", scope.discovered.clone()), - ("adapter", scope.adapter.clone()), - ], - )?; - let keys: Bound<'_, PyAny> = call_with( - ®istry::native(py, NativeAttr::SelectProjectGraph)?, - &[ - ("graph", graph.clone()), - ("select", selectors(py, select)?), - ("exclude", selectors(py, exclude)?), - ], + let graph: Bound<'_, PyAny> = build_project_graph( + py, + Some(keywords( + py, + &[ + ("discovered_inputs", scope.discovered.clone()), + ("adapter", scope.adapter.clone()), + ], + )?), )?; - let model_type: Bound<'_, PyAny> = - registry::class(py, RecordId::CompiledResourceType)?.getattr("MODEL")?; + let keys: Bound<'_, PyAny> = select_project_graph( + &graph.clone(), + &selectors(py, select)?, + &selectors(py, exclude)?, + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; + let model_type: Bound<'_, PyAny> = enum_member(py, RecordId::CompiledResourceType, "MODEL")?; let mut names: BTreeSet = BTreeSet::new(); for key in keys.try_iter()? { let key: Bound<'_, PyAny> = key?; @@ -243,59 +250,59 @@ fn selected_contract_inputs<'py>( referenced_model_names: Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = project_dir.py(); - let native = |name: NativeAttr| registry::native(py, name); - let project_config: Bound<'py, PyAny> = - native(NativeAttr::LoadProjectConfigFile)?.call1((project_dir,))?; - let local_config: Bound<'py, PyAny> = - native(NativeAttr::LoadLocalConfigFile)?.call1((project_dir,))?; + let project_config: Bound<'py, PyAny> = load_project_config_file(py, project_dir) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; + let local_config: Bound<'py, PyAny> = load_local_config_file(py, project_dir) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; let disabled: Bound<'py, PyAny> = PyBool::new(py, false).to_owned().into_any(); - let scope: Bound<'py, PyAny> = call_with( - &native(NativeAttr::DirectorySnapshotScope)?, - &[("project_dir", project_dir.clone())], + let scope: Bound<'py, PyAny> = pyo3::IntoPyObjectExt::into_bound_py_any( + directory_snapshot_scope(project_dir.clone()), + py, )?; within(&scope, |_| { - let project_only = |name: NativeAttr| -> PyResult> { - call_with(&native(name)?, &[("project_dir", project_dir.clone())]) - }; - call_with( - ®istry::class(py, RecordId::DiscoveredProjectInputs)?, + new_record_with( + py, + RecordId::DiscoveredProjectInputs, &[ ("project_config", project_config), ("local_config", local_config), ("project_dir", project_dir.clone()), ( "model_files", - call_with( - &native(NativeAttr::DiscoverModelFiles)?, - &[ - ("project_dir", project_dir.clone()), - ("extract_implicit_alias_columns", disabled.clone()), - ("extract_output_column_locations", disabled.clone()), - ("selected_model_names", referenced_model_names), - ], - )?, + discover_model_files( + py, + Some(keywords( + py, + &[ + ("project_dir", project_dir.clone()), + ("extract_implicit_alias_columns", disabled.clone()), + ("extract_output_column_locations", disabled.clone()), + ("selected_model_names", referenced_model_names), + ], + )?), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?, ), ( "model_schema_files", - project_only(NativeAttr::DiscoverModelSchemaFiles)?, + discover_model_schema_files(project_dir, None)?.into_any(), ), ( "schema_files", - project_only(NativeAttr::DiscoverSchemaFiles)?, + discover_schema_files(project_dir)?.into_any(), ), ( "source_files", - project_only(NativeAttr::DiscoverSourceFiles)?, + discover_source_files(project_dir, None)?.into_any(), ), ( "test_files", - call_with( - &native(NativeAttr::DiscoverTestFiles)?, - &[ - ("project_dir", project_dir.clone()), - ("selected_paths", selected_test_paths), - ], - )?, + discover_test_files( + &project_dir.clone(), + Some(selected_test_paths).filter(|value| !value.is_none()), + None, + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?, ), ], ) @@ -346,13 +353,10 @@ pub(crate) fn resolve_lint_inputs<'py>( ], )?), )?; - call_with( - ®istry::native(py, NativeAttr::RejectUnitTestSelectors)?, - &[ - ("select", selectors(py, select)?), - ("exclude", selectors(py, exclude)?), - ("discovered_inputs", discovered.clone()), - ], + reject_unit_test_selectors( + &selectors(py, select)?, + &selectors(py, exclude)?, + &discovered.clone(), )?; let adapter: Bound<'py, PyAny> = discovered_adapter(project_dir, &discovered)?; if select.is_empty() && exclude.is_empty() { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/freshness/freshness_command.rs b/crates/sqlbuild-python/src/bindings/_helpers/freshness/freshness_command.rs index 3a493440b1..4f79813aa6 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/freshness/freshness_command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/freshness/freshness_command.rs @@ -27,6 +27,7 @@ use crate::bindings::_helpers::planner_selection::selectors::{ }; use crate::bindings::_helpers::planner_selection::unit_tests::reject_unit_test_selectors; use crate::bindings::_helpers::presentation_text::terminal::stdout_supports_color; +use crate::bindings::_helpers::records::construct::enum_member; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::result_output::documents::write_execution_json_output; @@ -80,8 +81,7 @@ fn source_names_for_keys( ) -> PyResult> { let mut names: BTreeSet = BTreeSet::new(); let py: Python<'_> = graph.py(); - let source_type: Bound<'_, PyAny> = - registry::class(py, RecordId::CompiledResourceType)?.getattr("SOURCE")?; + let source_type: Bound<'_, PyAny> = enum_member(py, RecordId::CompiledResourceType, "SOURCE")?; let upstream_deps: Bound<'_, PyAny> = native_field(graph, "upstream_deps")?; for key in keys.try_iter()? { let key: Bound<'_, PyAny> = key?; @@ -135,7 +135,7 @@ fn previous_records<'py>( let mut database: Option> = None; let mut schemas: BTreeSet = BTreeSet::new(); for collection in ["models", "seeds", "functions"] { - for item in project.getattr(collection)?.try_iter()? { + for item in native_field(project, collection)?.try_iter()? { let destination: Bound<'py, PyAny> = native_field(&item?, "destination")?; let destination_database: Bound<'py, PyAny> = native_field(&destination, "database")?; if database.is_none() && !destination_database.is_none() { @@ -269,7 +269,7 @@ fn write_json_output( /// Run one `sqb freshness` request. #[pyfunction] #[pyo3(signature = (*, request))] -fn run_freshness_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { +pub(crate) fn run_freshness_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { let fail_on_stale: bool = native_field(request, "fail_on_stale")?.extract()?; let fail_on_error: bool = native_field(request, "fail_on_error")?.extract()?; if fail_on_stale && !native_field(request, "compare_state")?.extract::()? { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/freshness/observation.rs b/crates/sqlbuild-python/src/bindings/_helpers/freshness/observation.rs index d287a8768b..22e7a621a1 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/freshness/observation.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/freshness/observation.rs @@ -6,7 +6,7 @@ use pyo3::{pyfunction, wrap_pyfunction}; use sqlbuild_state::freshness::main::single_value_problem::single_value_problem; use sqlbuild_state::freshness::models::QueryShape; -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported, within}; +use crate::bindings::_helpers::compile_driver::python_calls::{call_with, within}; use crate::bindings::_helpers::freshness::support::{ OBSERVATION_ERROR, exception_text, freshness_error, presumed_utc_datetime, presumed_utc_value, text, @@ -15,9 +15,12 @@ use crate::bindings::_helpers::native_host::adapter::AdapterHost; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::records::constants::{DATA_VERSION_FIELD, OBSERVED_AT_FIELD}; +use crate::bindings::_helpers::records::construct::{ + enum_from_value, enum_member, new_record, new_record_with, +}; use crate::bindings::_helpers::records::fields::field as native_field; +use crate::bindings::_helpers::records::fields::replaced_keywords; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::references::source_relations::render_source_relation; use crate::bindings::_helpers::state_rows::freshness::{normalize, version_hash}; use crate::bindings::_helpers::state_rows::timestamps::{in_utc, offset_aware}; @@ -67,8 +70,9 @@ pub(crate) fn observation_source<'py>( { return Ok(None); } - let config: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::SourceFreshnessConfig)?, + let config: Bound<'py, PyAny> = new_record_with( + py, + RecordId::SourceFreshnessConfig, &[("strategy", adapter_strategy(py)?)], )?; replaced_source(source, config).map(Some) @@ -80,11 +84,11 @@ fn replaced_source<'py>( ) -> PyResult> { let changes: Bound<'py, PyDict> = PyDict::new(source.py()); changes.set_item("freshness", config)?; - imported(source.py(), "dataclasses", "replace")?.call((source,), Some(&changes)) + replaced_keywords(source, &changes) } fn adapter_strategy(py: Python<'_>) -> PyResult> { - registry::class(py, RecordId::SourceFreshnessStrategy)?.getattr("ADAPTER") + enum_member(py, RecordId::SourceFreshnessStrategy, "ADAPTER") } /// `OperationLifecycle(operation_kind="freshness", ...)` for one observation phase. @@ -122,8 +126,9 @@ fn observation<'py>( (strategy, value_kind): (Bound<'py, PyAny>, Bound<'py, PyAny>), (data_version, observed_at): (Bound<'py, PyAny>, Bound<'py, PyAny>), ) -> PyResult> { - call_with( - ®istry::class(source_name.py(), RecordId::SourceFreshnessObservation)?, + new_record_with( + source_name.py(), + RecordId::SourceFreshnessObservation, &[ ("source_name", source_name.clone()), ("strategy", strategy), @@ -310,13 +315,14 @@ pub(crate) fn unknown<'py>( (reason, message): (&str, Bound<'py, PyAny>), ) -> PyResult> { let py: Python<'py> = source_name.py(); - call_with( - ®istry::class(py, RecordId::SourceFreshnessUnknown)?, + new_record_with( + py, + RecordId::SourceFreshnessUnknown, &[ ("source_name", source_name.clone()), ( "reason", - registry::class(py, RecordId::SourceFreshnessUnknownReason)?.getattr(reason)?, + enum_member(py, RecordId::SourceFreshnessUnknownReason, reason)?, ), ("message", message), ], @@ -364,8 +370,11 @@ fn metadata_observation<'py>( source_name, ( adapter_strategy(py)?, - registry::class(py, RecordId::SourceFreshnessValueKind)? - .call1((native_field(metadata, "value_kind")?,))?, + enum_from_value( + py, + RecordId::SourceFreshnessValueKind, + native_field(metadata, "value_kind")?, + )?, ), ( presumed_utc_value(&native_field(metadata, "data_version")?)?, @@ -409,10 +418,8 @@ pub(crate) fn observe_adapter_batch<'py>( sources: &[Bound<'py, PyAny>], ) -> PyResult> { let py: Python<'py> = observer.adapter.py(); - let batch_class: Bound<'py, PyAny> = - registry::class(py, RecordId::AdapterSourceFreshnessBatch)?; if sources.is_empty() { - return batch_class.call0(); + return new_record(py, RecordId::AdapterSourceFreshnessBatch, (), None); } let manager: Bound<'py, PyAny> = lifecycle( py, @@ -437,8 +444,6 @@ fn observe_adapter_batch_inner<'py>( sources: &[Bound<'py, PyAny>], ) -> PyResult> { let py: Python<'py> = observer.adapter.py(); - let batch_class: Bound<'py, PyAny> = - registry::class(py, RecordId::AdapterSourceFreshnessBatch)?; let unknown_sources: Bound<'py, PyDict> = PyDict::new(py); let requests: Bound<'py, PyDict> = PyDict::new(py); let request_class: Bound<'py, PyAny> = host::entry(py, AdapterHost::TableFreshnessRequest)?; @@ -462,7 +467,11 @@ fn observe_adapter_batch_inner<'py>( )?; } if requests.is_empty() { - return call_with(&batch_class, &[("unknown", unknown_sources.into_any())]); + return new_record_with( + py, + RecordId::AdapterSourceFreshnessBatch, + &[("unknown", unknown_sources.into_any())], + ); } let unique: Bound<'py, PyDict> = PyDict::new(py); for request in requests.values() { @@ -502,8 +511,9 @@ fn observe_adapter_batch_inner<'py>( metadata_observation(&name, &metadata, &observer.observed_at)?, )?; } - call_with( - &batch_class, + new_record_with( + py, + RecordId::AdapterSourceFreshnessBatch, &[ ("observations", observations.into_any()), ("unknown", unknown_sources.into_any()), @@ -529,8 +539,9 @@ pub(crate) fn observation_record<'py>( &value_kind.extract::()?, &data_version.extract::()?, )?; - call_with( - ®istry::class(py, RecordId::SourceFreshnessRecord)?, + new_record_with( + py, + RecordId::SourceFreshnessRecord, &[ ("source_name", source_name), ("target_database", native_field(source, "database")?), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/freshness/planning.rs b/crates/sqlbuild-python/src/bindings/_helpers/freshness/planning.rs index 7f3fb613a0..8eaaf668f6 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/freshness/planning.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/freshness/planning.rs @@ -7,7 +7,7 @@ use pyo3::types::{PyDict, PyDictMethods, PyFrozenSet, PySet, PySetMethods, PyTup use pyo3::{pyfunction, wrap_pyfunction}; use sqlbuild_state::freshness::main::configuration_error::configuration_error; -use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; +use crate::bindings::_helpers::compile_driver::python_calls::KeywordArguments; use crate::bindings::_helpers::freshness::exception_types::SourceFreshnessObservationError; use crate::bindings::_helpers::freshness::observation::{ Observer, observation_record, observation_source, observe_adapter_batch, observe_configured, @@ -18,9 +18,9 @@ use crate::bindings::_helpers::freshness::support::text; use crate::bindings::_helpers::native_host::adapter::AdapterHost; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::planner_identity::graph::build_downstream_model_name_closure; +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::ids::RecordId; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::state_rows::freshness::{age_policy, equivalent}; const ADAPTER_STRATEGY: &str = "adapter"; @@ -125,8 +125,9 @@ fn adapter_batch<'py>( let name: Bound<'py, PyAny> = native_field(source, "name")?; unknown_sources.set_item(&name, unknown(&name, ("ERROR", message.clone()))?)?; } - call_with( - ®istry::class(py, RecordId::AdapterSourceFreshnessBatch)?, + new_record_with( + py, + RecordId::AdapterSourceFreshnessBatch, &[("unknown", unknown_sources.into_any())], ) } @@ -268,8 +269,9 @@ pub(crate) fn build_direct_source_freshness_planning_result<'py>( } } let unknown_names: Bound<'py, PyDict> = sorted_dict(py, &unknown_sources)?; - call_with( - ®istry::class(py, RecordId::DirectSourceFreshnessPlanningResult)?, + new_record_with( + py, + RecordId::DirectSourceFreshnessPlanningResult, &[ ( "observed_records", @@ -301,12 +303,13 @@ fn downstream_models<'py>( downstream_deps: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = source_name.py(); - let key: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::CompiledObjectKey)?, + let key: Bound<'py, PyAny> = new_record_with( + py, + RecordId::CompiledObjectKey, &[ ( "resource_type", - registry::class(py, RecordId::CompiledResourceType)?.getattr("SOURCE")?, + enum_member(py, RecordId::CompiledResourceType, "SOURCE")?, ), ("name", source_name.clone()), ], @@ -345,7 +348,7 @@ pub(crate) fn build_direct_source_freshness_propagation_result<'py>( unknown_sources.set_item(name, models)?; } let error_status: Bound<'py, PyAny> = - registry::class(py, RecordId::SourceFreshnessAgeStatus)?.getattr("ERROR")?; + enum_member(py, RecordId::SourceFreshnessAgeStatus, "ERROR")?; for (identity, status) in source_freshness .getattr("age_statuses")? .downcast::()? @@ -359,8 +362,9 @@ pub(crate) fn build_direct_source_freshness_propagation_result<'py>( blocked.call_method1("update", (&models,))?; errors.set_item(identity, models)?; } - call_with( - ®istry::class(py, RecordId::DirectSourceFreshnessPropagationResult)?, + new_record_with( + py, + RecordId::DirectSourceFreshnessPropagationResult, &[ ("changed_source_model_names", changed.into_any()), ("unknown_source_model_names", unknown_sources.into_any()), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/freshness/publication.rs b/crates/sqlbuild-python/src/bindings/_helpers/freshness/publication.rs index 5606688024..d1d5fe591c 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/freshness/publication.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/freshness/publication.rs @@ -12,9 +12,10 @@ use crate::bindings::_helpers::cost_telemetry::context::CostContext; use crate::bindings::_helpers::freshness::state_io::write_source_freshness_records; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; +use crate::bindings::_helpers::records::construct::new_record_with; use crate::bindings::_helpers::records::fields::field as native_field; +use crate::bindings::_helpers::records::fields::replaced_keywords; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; /// Observed records grouped by `(database, schema)` in first-seen order. type RecordsByLocation<'py> = Vec<( @@ -126,7 +127,6 @@ fn eligible_records<'py>( let record: Bound<'py, PyAny> = record?; observed.set_item(native_field(&record, "identity")?, record)?; } - let replace: Bound<'py, PyAny> = imported(py, "dataclasses", "replace")?; let changes: Bound<'py, PyDict> = PyDict::new(py); changes.set_item("run_id", run_id)?; let mut grouped: RecordsByLocation<'py> = Vec::new(); @@ -146,7 +146,7 @@ fn eligible_records<'py>( let Some(record) = observed.get_item(identity)? else { continue; }; - let replaced: Bound<'py, PyAny> = replace.call((record,), Some(&changes))?; + let replaced: Bound<'py, PyAny> = replaced_keywords(&record, &changes)?; grouped = collect_record(grouped, &replaced, (&affected, &entries_by_name))?; } Ok(grouped) @@ -154,8 +154,9 @@ fn eligible_records<'py>( /// `SourceFreshnessRenderers` bound to `adapter`. fn renderers<'py>(adapter: &Bound<'py, PyAny>) -> PyResult> { - call_with( - ®istry::class(adapter.py(), RecordId::SourceFreshnessRenderers)?, + new_record_with( + adapter.py(), + RecordId::SourceFreshnessRenderers, &[ ( "render_qualified_name", diff --git a/crates/sqlbuild-python/src/bindings/_helpers/freshness/state_io.rs b/crates/sqlbuild-python/src/bindings/_helpers/freshness/state_io.rs index d01d1d9175..df9f1f03fd 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/freshness/state_io.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/freshness/state_io.rs @@ -15,9 +15,9 @@ use crate::bindings::_helpers::cursor::values::DATETIME_MODULE; use crate::bindings::_helpers::freshness::support::{ INPUT_ERROR, exception_text, freshness_error, optional_str, presumed_utc_datetime, text, }; +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; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::state_rows::state_tables::state_table_create_sql; use crate::bindings::_helpers::state_rows::timestamps::date_time_typed; @@ -110,8 +110,9 @@ pub(crate) struct StateRead<'py> { fn empty_set<'py>(schema: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = schema.py(); - call_with( - ®istry::class(py, RecordId::SourceFreshnessSet)?, + new_record_with( + py, + RecordId::SourceFreshnessSet, &[ ("schema", schema.clone()), ("records", PyDict::new(py).into_any()), @@ -174,8 +175,9 @@ freshness table to regenerate source freshness state." let record: Bound<'py, PyAny> = row_record(&row?)?; records.set_item(native_field(&record, "identity")?, record)?; } - call_with( - ®istry::class(py, RecordId::SourceFreshnessSet)?, + new_record_with( + py, + RecordId::SourceFreshnessSet, &[("schema", schema.clone()), ("records", records.into_any())], ) } @@ -195,8 +197,9 @@ fn row_record<'py>(row: &Bound<'py, PyAny>) -> PyResult> { }; let optional = |index: usize| -> PyResult> { optional_str(&row.get_item(index)?) }; - call_with( - ®istry::class(py, RecordId::SourceFreshnessRecord)?, + new_record_with( + py, + RecordId::SourceFreshnessRecord, &[ ("source_name", field(0)?), ("target_database", optional(1)?), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/frontier/compile_frontier.rs b/crates/sqlbuild-python/src/bindings/_helpers/frontier/compile_frontier.rs index 9c05ed2d96..9be48b4ae4 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/frontier/compile_frontier.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/frontier/compile_frontier.rs @@ -8,8 +8,8 @@ use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, use crate::bindings::_helpers::frontier::stage_capture::write_stage_capture; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::native_host::user_code::UserCodeHost; +use crate::bindings::_helpers::records::construct::enum_member; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; /// Produce the frontier object for `until`, capturing it on request. #[pyfunction] @@ -65,7 +65,7 @@ pub(crate) fn frontier_stage(py: Python<'_>, stage: FrontierStage) -> PyResult "COMPILE_PROJECT_INPUTS", FrontierStage::CompiledProject => "COMPILED_PROJECT", }; - registry::class(py, RecordId::CompilerStage)?.getattr(member) + enum_member(py, RecordId::CompilerStage, member) } /// `stage()` run inside the macro bridge user macros render through. diff --git a/crates/sqlbuild-python/src/bindings/_helpers/frontier/encoder.rs b/crates/sqlbuild-python/src/bindings/_helpers/frontier/encoder.rs index ddb868ca9f..d7dd0cabb1 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/frontier/encoder.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/frontier/encoder.rs @@ -90,7 +90,7 @@ pub(crate) fn qualified_name(value: &Bound<'_, PyAny>) -> PyResult { /// `getattr(value, name, None)`. fn attribute_or_none<'py>(value: &Bound<'py, PyAny>, name: &str) -> PyResult> { let py: Python<'py> = value.py(); - match value.getattr(name) { + match native_field(value, name) { Ok(attribute) => Ok(attribute), Err(error) if error.is_instance_of::(py) => { Ok(py.None().into_bound(py)) @@ -369,7 +369,7 @@ impl<'py> CaptureEncoder<'py> { Some(names) => Some( names .iter() - .map(|name| Ok(((*name).to_owned(), value.getattr(*name)?))) + .map(|name| Ok(((*name).to_owned(), native_field(value, name)?))) .collect::>()?, ), None => instance_attributes(value)?, @@ -442,7 +442,7 @@ fn instance_attributes<'py>(value: &Bound<'py, PyAny>) -> PyResult = value.getattr(text.as_str())?; + let item: Bound<'py, PyAny> = native_field(value, text.as_str())?; attributes = with_entry(attributes, text, item); } } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/graph/execution_gates.rs b/crates/sqlbuild-python/src/bindings/_helpers/graph/execution_gates.rs index 319aeb6f8a..d6c43b6c66 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/graph/execution_gates.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/graph/execution_gates.rs @@ -8,9 +8,9 @@ use crate::bindings::_helpers::analysis::explicit_references::{ GateRow, HookRow, native_execution_gate_edges, }; use crate::bindings::_helpers::graph::keys::KeyCache; +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::ids::RecordId; -use crate::bindings::_helpers::records::registry; const PATHLIB_MODULE: &str = "pathlib"; /// The model config keys whose entries are hooks, in lifecycle order. @@ -36,7 +36,7 @@ fn attached_audit_gate_edges<'py>( ) -> PyResult> { let (gates, _): (Vec, Vec) = native_execution_gate_edges(project)?; let cache: KeyCache<'py> = KeyCache::new(py)?; - let edge_type: Bound<'py, PyAny> = registry::class(py, RecordId::AttachedAuditGateEdge)?; + let edge_type: Bound<'py, PyAny> = record_class(py, RecordId::AttachedAuditGateEdge)?; let mut edges: Vec> = Vec::with_capacity(gates.len()); for (audit_name, target, gated, read) in gates { edges.push(keyword_call( @@ -61,8 +61,7 @@ fn hook_read_edges<'py>( ) -> PyResult> { let (_, hooks): (Vec, Vec) = native_execution_gate_edges(project)?; let cache: KeyCache<'py> = KeyCache::new(py)?; - let edge_type: Bound<'py, PyAny> = registry::class(py, RecordId::HookReadEdge)?; - let hook_type: Bound<'py, PyAny> = registry::class(py, RecordId::HookReadType)?; + let edge_type: Bound<'py, PyAny> = record_class(py, RecordId::HookReadEdge)?; let path_type: Bound<'py, PyAny> = py.import(PATHLIB_MODULE)?.getattr("Path")?; let mut edges: Vec> = Vec::with_capacity(hooks.len()); for (hook_name, gated, read, kind, path) in hooks { @@ -76,7 +75,10 @@ fn hook_read_edges<'py>( ("hook_name", PyString::new(py, &hook_name).into_any()), ("gated", cache.key(&gated)?), ("read", cache.key(&read)?), - ("hook_type", hook_type.call1((kind,))?), + ( + "hook_type", + enum_from_value(py, RecordId::HookReadType, kind)?, + ), ("hook_path", hook_path), ], )?); @@ -87,11 +89,11 @@ fn hook_read_edges<'py>( /// The Python hooks attached to one model, in lifecycle order without repeats. #[pyfunction] #[pyo3(signature = (*, model))] -fn model_python_hook_names<'py>( +pub(crate) fn model_python_hook_names<'py>( py: Python<'py>, model: &Bound<'py, PyAny>, ) -> PyResult> { - let entry_type: Bound<'_, PyAny> = registry::class(py, RecordId::PythonHookEntry)?; + let entry_type: Bound<'_, PyAny> = record_class(py, RecordId::PythonHookEntry)?; let values: Bound<'_, PyAny> = native_field(model, "config")?.getattr("values")?; let mut names: Vec = Vec::new(); for hook_key in MODEL_HOOK_KEYS { @@ -122,10 +124,13 @@ pub(crate) fn sql_ref_key<'py>( py: Python<'py>, reference: &Bound<'py, PyAny>, ) -> PyResult> { - let resource_type: Bound<'py, PyAny> = registry::class(py, RecordId::CompiledResourceType)? - .call1((native_field(reference, "kind")?.getattr("value")?,))?; + let resource_type: Bound<'py, PyAny> = enum_from_value( + py, + RecordId::CompiledResourceType, + native_field(reference, "kind")?.getattr("value")?, + )?; keyword_call( - ®istry::class(py, RecordId::CompiledObjectKey)?, + &record_class(py, RecordId::CompiledObjectKey)?, &[ ("resource_type", resource_type), ("name", native_field(reference, "name")?), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/graph/keys.rs b/crates/sqlbuild-python/src/bindings/_helpers/graph/keys.rs index a5d71ca98f..b2e2363db6 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/graph/keys.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/graph/keys.rs @@ -1,8 +1,8 @@ //! `CompiledObjectKey`s for native `(resource type, name)` pairs, converted once per pair. +use crate::bindings::_helpers::records::construct::record_class; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; use pyo3::types::{PyDict, PyDictMethods}; @@ -16,8 +16,8 @@ pub(crate) struct KeyCache<'py> { impl<'py> KeyCache<'py> { pub(crate) fn new(py: Python<'py>) -> PyResult { Ok(Self { - key_type: registry::class(py, RecordId::CompiledObjectKey)?, - resource_type: registry::class(py, RecordId::CompiledResourceType)?, + key_type: record_class(py, RecordId::CompiledObjectKey)?, + resource_type: record_class(py, RecordId::CompiledResourceType)?, keys: PyDict::new(py), }) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/graph/project_graph.rs b/crates/sqlbuild-python/src/bindings/_helpers/graph/project_graph.rs index 7ab69a9101..80be21806a 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/graph/project_graph.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/graph/project_graph.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; -use pyo3::prelude::{Bound, Py, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; +use pyo3::prelude::{Bound, Py, PyAny, PyModule, PyModuleMethods, PyResult, Python}; use pyo3::sync::PyOnceLock; use pyo3::{pyclass, pymethods}; use sqlbuild_analysis::assembly::project::types::ObjectKey; @@ -22,6 +22,7 @@ use sqlbuild_analysis::graph::models::{ use crate::bindings::_helpers::boundary::panics::compiler_guard; use crate::bindings::_helpers::graph::views::lineage_graph_views; +use crate::bindings::_helpers::records::fields::field as native_field; /// `(key, deps, tags, model folder)`. pub(crate) type ResourceRow = (ObjectKey, Vec, Vec, Option); @@ -103,7 +104,7 @@ impl CompiledProjectGraph { } fn view(&self, py: Python<'_>, name: &str) -> PyResult> { - Ok(self.lineage_views(py)?.bind(py).getattr(name)?.unbind()) + Ok(native_field(self.lineage_views(py)?.bind(py), name)?.unbind()) } } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/graph/python_lifecycle.rs b/crates/sqlbuild-python/src/bindings/_helpers/graph/python_lifecycle.rs index 3127c88639..5cc6ca2556 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/graph/python_lifecycle.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/graph/python_lifecycle.rs @@ -2,9 +2,9 @@ use std::collections::{BTreeSet, HashMap}; +use crate::bindings::_helpers::records::construct::new_record; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; use pyo3::types::{PyDict, PyFrozenSet}; use pyo3::{pyfunction, wrap_pyfunction}; @@ -62,7 +62,12 @@ pub(crate) fn build_python_sql_run_lifecycle<'py>( "read_side_python_node_names", PyFrozenSet::new(py, lifecycle.read_side_names)?, )?; - registry::class(py, RecordId::PythonSqlRunLifecyclePlan)?.call((), Some(&arguments)) + new_record( + py, + RecordId::PythonSqlRunLifecyclePlan, + (), + Some(&arguments), + ) } fn lifecycle_node(node: &Bound<'_, PyAny>) -> PyResult { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/graph/python_node_graph.rs b/crates/sqlbuild-python/src/bindings/_helpers/graph/python_node_graph.rs index 0eb247bf0d..d8794c5432 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/graph/python_node_graph.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/graph/python_node_graph.rs @@ -13,9 +13,9 @@ use crate::bindings::_helpers::graph::python_selection::resolve_python_sql_run_s use crate::bindings::_helpers::graph::views::compiled_graph_keys; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::native_host::user_code::UserCodeHost; +use crate::bindings::_helpers::records::construct::{new_record_with, record_class}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; const NAME_KEY: &str = "name"; @@ -36,7 +36,7 @@ struct Vocabulary<'py> { } fn attribute_value<'py>(value: &Bound<'py, PyAny>, name: &str) -> PyResult> { - value.getattr(name) + native_field(value, name) } fn optional_value<'py>(value: &Bound<'py, PyAny>) -> PyResult> { @@ -115,7 +115,7 @@ fn kind_metadata<'py>( .iter() .map(|field| Ok((*field, get(field)?))) .collect::>()?; - call_with(®istry::class(py, class)?, &pairs) + new_record_with(py, class, &pairs) }; Ok(match kind { "LOADER" => ( @@ -160,7 +160,7 @@ fn python_node<'py>( let py: Python<'py> = discovered.py(); let get = |name: &str| attribute_value(discovered, name); let (metadata_field, metadata) = kind_metadata(py, kind, discovered)?; - let kind_member: Bound<'py, PyAny> = vocabulary.kinds.getattr(kind)?; + let kind_member: Bound<'py, PyAny> = native_field(&vocabulary.kinds, kind)?; let usages: Bound<'py, PyAny> = provider_usages(&get("function")?, &vocabulary.providers)?; let mut identity: Bound<'py, PyAny> = get("identity")?; if identity.is_none() { @@ -255,14 +255,14 @@ pub(crate) fn python_node_graph<'py>( providers.set_item(native_field(&provider, "name")?, provider)?; } let vocabulary: Vocabulary<'py> = Vocabulary { - node: registry::class(py, RecordId::DiscoveredPythonNode)?, + node: record_class(py, RecordId::DiscoveredPythonNode)?, kinds: host::entry(py, UserCodeHost::PythonNodeKind)?, sql_ref: host::entry(py, UserCodeHost::SqlResourceRef)?, providers, }; let mut nodes: Vec> = Vec::new(); for (collection, kind, documented) in KINDS { - for discovered in discovered_inputs.getattr(collection)?.try_iter()? { + for discovered in native_field(discovered_inputs, collection)?.try_iter()? { nodes.push(python_node(&vocabulary, (kind, documented), &discovered?)?); } } @@ -288,12 +288,12 @@ fn graph_record<'py>( tags, paths, } = indexes; - let edge_class: Bound<'py, PyAny> = registry::class(py, RecordId::PythonNodeDependencyEdge)?; let dependency_edges: Vec> = edges .into_iter() .map(|(upstream_name, downstream_name)| { - call_with( - &edge_class, + new_record_with( + py, + RecordId::PythonNodeDependencyEdge, &[ ( "upstream_name", @@ -330,8 +330,9 @@ fn graph_record<'py>( by_name.set_item(&name, node)?; by_selector.set_item(format!("{kind}:{}", name.str()?), node)?; } - call_with( - ®istry::class(py, RecordId::PythonNodeGraph)?, + new_record_with( + py, + RecordId::PythonNodeGraph, &[ ("nodes", PyTuple::new(py, nodes)?.into_any()), ( @@ -358,8 +359,9 @@ pub(crate) fn run_selection_from_inputs<'py>( let python_graph: Bound<'py, PyAny> = python_node_graph(discovered_inputs)?; let (sql_keys, python_node_names) = resolve_python_sql_run_selection(project_graph, &python_graph, select, exclude)?; - call_with( - ®istry::class(py, RecordId::PythonSqlRunSelection)?, + new_record_with( + py, + RecordId::PythonSqlRunSelection, &[ ("sql_keys", compiled_graph_keys(py, sql_keys)?.into_any()), ( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/graph/views.rs b/crates/sqlbuild-python/src/bindings/_helpers/graph/views.rs index 74cbc632aa..9567961880 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/graph/views.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/graph/views.rs @@ -12,9 +12,9 @@ use sqlbuild_analysis::graph::models::{GraphIndexes, ProjectGraph}; use crate::bindings::_helpers::boundary::panics::compiler_guard; use crate::bindings::_helpers::graph::keys::{KeyCache, native_key}; use crate::bindings::_helpers::graph::project_graph::NativeProjectGraph; +use crate::bindings::_helpers::records::construct::new_record; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; /// The native graph's `(resource type, name)` pair for `key`. #[pyfunction] @@ -85,7 +85,7 @@ fn graph_views<'py>(py: Python<'py>, graph: &ProjectGraph) -> PyResult = Bound<'py, PyAny>; @@ -25,8 +29,8 @@ pub(crate) fn compiled_model<'py>( let mut star_resolved: Object<'py> = false_object(py); let mut diagnostics: Object<'py> = PyTuple::empty(py).into_any(); let mut validated: Object<'py> = false_object(py); - let mut proof: Object<'py> = registry::native(py, NativeAttr::DynamicContractProof)? - .call1((native_field(analysis, "standalone_proof")?,))?; + let mut proof: Object<'py> = + dynamic_contract_proof(&native_field(analysis, "standalone_proof")?)?; let session: Object<'py> = native_field(analysis, "session")?; if !session.is_none() { let (session, position): (Object<'py>, Object<'py>) = session.extract()?; @@ -34,8 +38,7 @@ pub(crate) fn compiled_model<'py>( arguments.set_item("session", session)?; arguments.set_item("position", position)?; arguments.set_item("placeholders", &none)?; - let outcome: Object<'py> = - registry::native(py, NativeAttr::SessionOutcomeAnalysis)?.call((), Some(&arguments))?; + let outcome: Object<'py> = session_outcome_analysis(Some(&arguments))?; let result: Object<'py> = native_field(&outcome, "polyglot_analysis")?; if native_field(&result, "analysis_succeeded")?.is_truthy()? { inferred = native_field(&result, "columns")?; @@ -66,15 +69,18 @@ pub(crate) fn compiled_model<'py>( inferred = native_field(&proof, "fixed_columns")?; has_star = true_object(py); } - let resource_type: Object<'py> = registry::class(py, RecordId::CompiledResourceType)?; - let key_class: Object<'py> = registry::class(py, RecordId::CompiledObjectKey)?; + let resource_type: Object<'py> = record_class(py, RecordId::CompiledResourceType)?; + let key_class: Object<'py> = record_class(py, RecordId::CompiledObjectKey)?; let mut dependency_keys: Vec> = Vec::new(); for dependency in deps.try_iter()? { let (kind, name): (Object<'py>, Object<'py>) = dependency?.extract()?; dependency_keys.push(call( &key_class, &[ - ("resource_type", resource_type.call1((kind,))?), + ( + "resource_type", + enum_from_value(py, RecordId::CompiledResourceType, kind)?, + ), ("name", name), ], )?); @@ -88,7 +94,7 @@ pub(crate) fn compiled_model<'py>( ], )?; call( - ®istry::class(py, RecordId::CompiledModel)?, + &record_class(py, RecordId::CompiledModel)?, &[ ("key", key), ("deps", PyTuple::new(py, dependency_keys)?.into_any()), @@ -155,7 +161,7 @@ fn destination<'py>(config: &Object<'py>, model_name: &Object<'py>) -> PyResult< }; let none: Object<'py> = py.None().into_bound(py); call( - ®istry::class(py, RecordId::CompiledRelationLocation)?, + &record_class(py, RecordId::CompiledRelationLocation)?, &[ ( "database", @@ -179,8 +185,7 @@ fn fallback_columns<'py>(rows: &Object<'py>) -> PyResult> { if rows.is_none() { return Ok(rows.clone()); } - let column_class: Object<'py> = registry::class(py, RecordId::InferredColumn)?; - let nullability: Object<'py> = registry::class(py, RecordId::InferredNullability)?; + let column_class: Object<'py> = record_class(py, RecordId::InferredColumn)?; let mut columns: Vec> = Vec::new(); for row in rows.try_iter()? { let (name, data_type, value): (Object<'py>, Object<'py>, Object<'py>) = row?.extract()?; @@ -189,7 +194,10 @@ fn fallback_columns<'py>(rows: &Object<'py>) -> PyResult> { &[ ("name", name), ("type", data_type), - ("nullability", nullability.call1((value,))?), + ( + "nullability", + enum_from_value(py, RecordId::InferredNullability, value)?, + ), ], )?); } @@ -215,7 +223,7 @@ fn located<'py>( "code", "message", "line", "column", "start", "end", "severity", ] .into_iter() - .map(|name| item.getattr(name)) + .map(|name| native_field(&item, name)) .collect::>()?; rows.push(PyTuple::new(py, fields)?.into_any()); } @@ -240,7 +248,7 @@ fn located<'py>( let fields: Vec> = ["source_start", "source_end", "output_start", "output_end"] .into_iter() - .map(|name| span.getattr(name)) + .map(|name| native_field(&span, name)) .collect::>()?; rows.push(PyTuple::new(py, fields)?.into_any()); } @@ -255,13 +263,16 @@ fn located<'py>( )? .into_any() }; - let located: Object<'py> = registry::native(py, NativeAttr::LocateModelBindingDiagnostics)? - .call1((PyList::new(py, rows)?, texts, expansion_row))?; - let diagnostic_class: Object<'py> = registry::class(py, RecordId::CompilerDiagnostic)?; - let location_class: Object<'py> = registry::class(py, RecordId::SourceLocation)?; - let phase: Object<'py> = registry::class(py, RecordId::DiagnosticPhase)?.getattr("COMPILE")?; - let model: Object<'py> = - registry::class(py, RecordId::CompiledResourceType)?.getattr("MODEL")?; + let located: Object<'py> = locate_model_binding_diagnostics( + PyList::new(py, rows)?.extract::>()?, + texts.extract::<(String, String, String)>()?, + expansion_row.extract::>)>>()?, + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; + let diagnostic_class: Object<'py> = record_class(py, RecordId::CompilerDiagnostic)?; + let location_class: Object<'py> = record_class(py, RecordId::SourceLocation)?; + let phase: Object<'py> = enum_member(py, RecordId::DiagnosticPhase, "COMPILE")?; + let model: Object<'py> = enum_member(py, RecordId::CompiledResourceType, "MODEL")?; let name: Object<'py> = native_field(&model_file, "file_path")?.getattr("stem")?; let mut built: Vec> = Vec::new(); for row in located.try_iter()? { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/init_command/command.rs b/crates/sqlbuild-python/src/bindings/_helpers/init_command/command.rs index 3323d9c927..a434d5a90d 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/init_command/command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/init_command/command.rs @@ -13,12 +13,13 @@ use sqlbuild_core::init_command::main::init_project_name::init_project_name; use sqlbuild_core::init_command::main::init_project_toml::init_project_toml; use sqlbuild_core::init_command::main::render_init_summary::render_init_summary; -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; +use crate::bindings::_helpers::compile_driver::python_calls::{imported, keywords}; use crate::bindings::_helpers::init_command::scaffold::scaffold_blank_project; use crate::bindings::_helpers::presentation_text::terminal::stdout_supports_color; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr}; +use crate::bindings::_helpers::records::ids::ErrorId; use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::workspace_commands::commands::update_sqlbuild_skills; const EXISTS_MESSAGE: &str = "sqlbuild_project.toml already exists in this directory"; @@ -37,7 +38,10 @@ fn exists_error(py: Python<'_>) -> PyErr { /// Create a minimal SQLBuild project in the project directory. #[pyfunction] #[pyo3(signature = (project_dir = None))] -fn run_init_command(py: Python<'_>, project_dir: Option>) -> PyResult { +pub(crate) fn run_init_command( + py: Python<'_>, + project_dir: Option>, +) -> PyResult { let base_dir: Bound<'_, PyAny> = match project_dir.filter(|path| !path.is_none()) { Some(path) => path, None => imported(py, "pathlib", "Path")?.call_method0("cwd")?, @@ -53,10 +57,7 @@ fn run_init_command(py: Python<'_>, project_dir: Option>) -> Py init_project_name(&native_field(&base_dir, "name")?.extract::()?); let base_path: PathBuf = base_dir.extract()?; scaffold_blank_project(py, &base_path, &init_project_toml(&project_name))?; - call_with( - ®istry::native(py, NativeAttr::UpdateSqlbuildSkills)?, - &[("project_dir", base_dir)], - )?; + update_sqlbuild_skills(py, Some(keywords(py, &[("project_dir", base_dir)])?))?; let style: CliStyle = CliStyle { use_color: stdout_supports_color(py)?, }; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_command.rs b/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_command.rs index 35c5a7f9f0..f53d9c36ec 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_command.rs @@ -31,6 +31,7 @@ use crate::bindings::_helpers::janitor::janitor_phases::{ use crate::bindings::_helpers::janitor::janitor_plan::{janitor_plan_text, janitor_result_counts}; use crate::bindings::_helpers::planner_phases::objects::object; use crate::bindings::_helpers::presentation_text::terminal::stdout_supports_color; +use crate::bindings::_helpers::records::construct::new_record; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -114,7 +115,7 @@ fn invocation<'py>(py: Python<'py>, request: &Bound<'py, PyAny>) -> PyResult( ), ], )?; - registry::class(py, RecordId::JanitorSettings)?.call((), Some(&options)) + new_record(py, RecordId::JanitorSettings, (), Some(&options)) } /// Ask for the exact confirmation phrase; an interrupt cancels. @@ -215,7 +216,7 @@ fn clean( /// Run one `sqb janitor` request. #[pyfunction] #[pyo3(signature = (*, request))] -fn run_janitor_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { +pub(crate) fn run_janitor_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { let invocation: Bound<'_, PyAny> = invocation(py, request)?; let style: CliStyle = CliStyle { use_color: native_field(&invocation, "use_color")?.extract()?, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_facts.rs b/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_facts.rs index 12feaa347e..2955fc5c7b 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_facts.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_facts.rs @@ -13,6 +13,7 @@ use crate::bindings::_helpers::janitor::janitor_keys::{ Folded, Key, SchemaKey, dedupe_schema_keys, folded_schema, integer, optional, record, sorted_schemas, text, tuple, }; +use crate::bindings::_helpers::records::fields::replaced; const QUERY_ARTIFACT_LIFECYCLE: ExecutorHost = ExecutorHost::QueryDiffArtifactLifecycle; const RELATION_LOOKUP: AdapterHost = AdapterHost::BuildRelationLookup; @@ -45,7 +46,7 @@ impl<'py> Warehouse<'py> { let mut arguments: Vec<(&str, Bound<'py, PyAny>)> = vec![("connection", self.connection.clone())]; arguments.extend(pairs.iter().cloned()); - call_with(&self.adapter.getattr(method)?, &arguments) + call_with(&native_field(&self.adapter, method)?, &arguments) } pub(crate) fn age_metadata_support(&self) -> PyResult { @@ -91,7 +92,7 @@ fn destination_keys<'py>( field: &str, ) -> PyResult)>> { let mut keys: Vec<(Key, Bound<'py, PyAny>)> = Vec::new(); - for node in project.getattr(field)?.try_iter()? { + for node in native_field(project, field)?.try_iter()? { let node: Bound<'py, PyAny> = node?; keys.push((Key::of(&native_field(&node, "destination")?)?, node)); } @@ -163,7 +164,6 @@ fn list_target_schema_relations<'py>( target_schemas: &[SchemaKey], ) -> PyResult> { let py: Python<'py> = warehouse.py(); - let replace: Bound<'py, PyAny> = imported(py, "dataclasses", "replace")?; let mut databases: Vec> = Vec::new(); for (database, _) in sorted_schemas(target_schemas) { if !databases.contains(&database) { @@ -203,15 +203,12 @@ fn list_target_schema_relations<'py>( else { continue; }; - let respelled: Bound<'py, PyAny> = replace.call( - (relation,), - Some(&keywords( - py, - &[ - ("database", optional(py, target.0.as_deref())?), - ("schema", optional(py, target.1.as_deref())?), - ], - )?), + let respelled: Bound<'py, PyAny> = replaced( + &relation, + &[ + ("database", optional(py, target.0.as_deref())?), + ("schema", optional(py, target.1.as_deref())?), + ], )?; match result.iter_mut().find(|(listed, _)| listed == target) { Some((_, relations)) => relations.push(respelled), @@ -436,7 +433,7 @@ pub(crate) fn direct_state_prune_candidates<'py>( continue; } let prune_sql: Bound<'py, PyAny> = call_with( - &warehouse.adapter.getattr(render)?, + &native_field(&warehouse.adapter, render)?, &[ ("database", optional(py, database.as_deref())?), ("schema", text(py, schema)), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_keys.rs b/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_keys.rs index 31d2ba4bfd..0f582cee79 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_keys.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_keys.rs @@ -179,7 +179,7 @@ pub(crate) fn relation_age_timestamp<'py>( ) -> PyResult>> { let mut latest: Option> = None; for field in ["created_at", "last_altered_at"] { - let value: Bound<'py, PyAny> = relation.getattr(field)?; + let value: Bound<'py, PyAny> = native_field(relation, field)?; if value.is_none() { continue; } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_old_names.rs b/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_old_names.rs index d2110ed1d0..77ae7ba7d2 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_old_names.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_old_names.rs @@ -16,6 +16,7 @@ use crate::bindings::_helpers::janitor::janitor_facts::{ use crate::bindings::_helpers::janitor::janitor_keys::{ Folded, Key, SchemaKey, optional, record, sorted_schemas, text, tuple, }; +use crate::bindings::_helpers::records::construct::enum_member; use crate::bindings::_helpers::state_rows::migration_io::{ read_histories as read_view_histories, stored_columns_binding, }; @@ -25,7 +26,6 @@ use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use sqlbuild_core::constants::MIGRATION_TABLE_NAME; use sqlbuild_core::constants::OLD_NAME_VIEW_TABLE_NAME; const MIGRATE_FROM_CONFIG_KEY: &str = "migrate_from"; @@ -37,7 +37,7 @@ pub(crate) struct OldNamePlanning<'py> { } fn drop_reason<'py>(py: Python<'py>, member: &str) -> PyResult> { - registry::class(py, RecordId::OldNameViewDropReason)?.getattr(member) + enum_member(py, RecordId::OldNameViewDropReason, member) } fn status<'py>( @@ -48,7 +48,7 @@ fn status<'py>( } fn status_is(py: Python<'_>, value: &Bound<'_, PyAny>, member: &str) -> PyResult { - value.eq(registry::class(py, RecordId::OldNameViewStatus)?.getattr(member)?) + value.eq(enum_member(py, RecordId::OldNameViewStatus, member)?) } fn read_histories<'py>( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_phases.rs b/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_phases.rs index a16c115d78..021334fc12 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_phases.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_phases.rs @@ -191,7 +191,7 @@ fn candidate_count(plan: &Bound<'_, PyAny>) -> PyResult { .getattr("drops")? .len()?; for field in CANDIDATE_FIELDS { - count += plan.getattr(field)?.len()?; + count += native_field(plan, field)?.len()?; } Ok(count) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_plan.rs b/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_plan.rs index 7735ab251f..5120fa24c3 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_plan.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_plan.rs @@ -181,7 +181,7 @@ pub(crate) fn janitor_plan_text(plan: &Bound<'_, PyAny>) -> PyResult) -> PyResult { - let count = |name: &str| -> PyResult { result.getattr(name)?.len() }; + let count = |name: &str| -> PyResult { native_field(result, name)?.len() }; Ok(JanitorResultCounts { archived: count("archived")?, deleted: count("deleted")?, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/lifecycle_events/envelopes.rs b/crates/sqlbuild-python/src/bindings/_helpers/lifecycle_events/envelopes.rs index 1666b67c37..be7984ec7f 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/lifecycle_events/envelopes.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/lifecycle_events/envelopes.rs @@ -87,7 +87,7 @@ fn text(value: &Bound<'_, PyAny>) -> PyResult> { /// Freeze a JSON object field of a frozen dataclass in place. fn freeze_object_field(owner: &Bound<'_, PyAny>, field: &str, path: &str) -> PyResult<()> { let py: Python<'_> = owner.py(); - let frozen: Bound<'_, PyAny> = frozen_json(&owner.getattr(field)?, path)?; + let frozen: Bound<'_, PyAny> = frozen_json(&native_field(owner, field)?, path)?; if !frozen.is_instance(&imported(py, "collections.abc", "Mapping")?)? { return Err(validation_error( py, @@ -106,7 +106,7 @@ fn freeze_object_field(owner: &Bound<'_, PyAny>, field: &str, path: &str) -> PyR fn lifecycle_event_check(event: &Bound<'_, PyAny>) -> PyResult<()> { let py: Python<'_> = event.py(); for field in EVENT_REQUIRED_TEXT { - required_text_check(&event.getattr(field)?, field)?; + required_text_check(&native_field(event, field)?, field)?; } let schema_version: Bound<'_, PyAny> = native_field(event, "schema_version")?; schema_version_check(&schema_version)?; @@ -126,7 +126,7 @@ fn lifecycle_event_check(event: &Bound<'_, PyAny>) -> PyResult<()> { } timestamp_check(&native_field(event, "occurred_at")?)?; for field in EVENT_OPTIONAL_TEXT { - optional_text_check(&event.getattr(field)?, field)?; + optional_text_check(&native_field(event, field)?, field)?; } freeze_object_field(event, "external_context", "external_context")?; freeze_object_field(event, "payload", "payload")?; @@ -154,7 +154,7 @@ fn diagnostic_log_check(record: &Bound<'_, PyAny>) -> PyResult<()> { return Err(validation_error(py, &problem)); } for field in DIAGNOSTIC_REQUIRED_TEXT { - required_text_check(&record.getattr(field)?, field)?; + required_text_check(&native_field(record, field)?, field)?; } let severity: String = native_field(record, "severity")?.extract()?; if let Some(problem) = diagnostic_severity_problem(&severity) { @@ -162,7 +162,7 @@ fn diagnostic_log_check(record: &Bound<'_, PyAny>) -> PyResult<()> { } timestamp_check(&native_field(record, "occurred_at")?)?; for field in DIAGNOSTIC_OPTIONAL_TEXT { - optional_text_check(&record.getattr(field)?, field)?; + optional_text_check(&native_field(record, field)?, field)?; } freeze_object_field(record, "fields", "fields") } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/lifecycle_events/validation.rs b/crates/sqlbuild-python/src/bindings/_helpers/lifecycle_events/validation.rs index cbde594342..53f9c1f9ad 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/lifecycle_events/validation.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/lifecycle_events/validation.rs @@ -95,7 +95,7 @@ pub(crate) fn timestamp_check(value: &Bound<'_, PyAny>) -> PyResult<()> { pub(crate) fn known_event_check(event: &Bound<'_, PyAny>) -> PyResult<()> { let mut correlations: Vec<(&'static str, bool)> = Vec::new(); for field in CORRELATION_FIELDS { - correlations.push((field, !event.getattr(field)?.is_none())); + correlations.push((field, !native_field(event, field)?.is_none())); } let facts = KnownEventFacts { schema_version: native_field(event, "schema_version")?.extract()?, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/lineage/lineage_cache.rs b/crates/sqlbuild-python/src/bindings/_helpers/lineage/lineage_cache.rs index 70e23da755..a83d2f1dd9 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/lineage/lineage_cache.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/lineage/lineage_cache.rs @@ -4,9 +4,8 @@ use std::collections::{HashMap, HashSet}; use std::hash::Hash; use std::path::{Path, PathBuf}; +use crate::bindings::_helpers::lineage::relation_fingerprint::relation_lineage_fingerprint; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::NativeAttr; -use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyErr, PyModule, PyResult, Python}; use pyo3::types::{PyBytes, PyBytesMethods, PyDict, PyDictMethods, PyList, PyTuple}; use sqlbuild_analysis::lineage_command::models::{LineageIndex, LineageKey, LineageNode}; @@ -141,9 +140,7 @@ pub(crate) fn lineage_fingerprint( prefix.extend(sqlbuild_version(py)?.as_bytes()); prefix.extend(encoded.downcast::()?.as_bytes()); let uncacheable: bool = py.version_info() < FIRST_PYTHON_SKIPPING_INTERRUPTED_LISTINGS; - registry::native(py, NativeAttr::RelationLineageFingerprint)? - .call1((project_dir, PyBytes::new(py, &prefix), uncacheable))? - .extract() + relation_lineage_fingerprint(py, project_dir.extract()?, prefix, uncacheable) } fn rows<'py, T: pyo3::FromPyObject<'py>>( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/lineage/lineage_command.rs b/crates/sqlbuild-python/src/bindings/_helpers/lineage/lineage_command.rs index 463de3a49d..2d25b5cc21 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/lineage/lineage_command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/lineage/lineage_command.rs @@ -34,11 +34,22 @@ use sqlbuild_core::cli_text::models::CliStyle; use crate::bindings::_helpers::build_progress::transient_status::maybe_status; use crate::bindings::_helpers::command_runtime::adapters::resolve_cli_adapter; use crate::bindings::_helpers::compile_driver::discovery::discover_project_inputs; +use crate::bindings::_helpers::compile_driver::project::build_project_graph; +use crate::bindings::_helpers::contracts::spec_resolution::resolve_effective_adapter_name; use crate::bindings::_helpers::lineage::lineage_cache::{ lineage_fingerprint, read_lineage_cache, write_lineage_cache, }; use crate::bindings::_helpers::lineage::lineage_index::{graph_index, index_views, lineage_keys}; +use crate::bindings::_helpers::lineage::project_lineage::build_direct_semantic_uses; +use crate::bindings::_helpers::planner_selection::selectors::resolve_project_selector_tokens; +use crate::bindings::_helpers::planner_selection::selectors::resolve_project_selectors; +use crate::bindings::_helpers::planner_selection::selectors::select_project_graph; +use crate::bindings::_helpers::planner_selection::selectors::{ + match_project_selector, parse_project_selector, +}; +use crate::bindings::_helpers::planner_selection::unit_tests::reject_unit_test_selectors; use crate::bindings::_helpers::presentation_text::terminal::stdout_supports_color; +use crate::bindings::_helpers::records::construct::record_class; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; @@ -140,20 +151,12 @@ fn discover<'py>( discover_project_inputs(py, Some(options)) } -fn reject_unit_tests( - py: Python<'_>, - request: &Bound<'_, PyAny>, - discovered: &Bound<'_, PyAny>, -) -> PyResult<()> { - let options: Bound<'_, PyDict> = keywords( - py, - &[ - ("select", &native_field(request, "select")?), - ("exclude", &native_field(request, "exclude")?), - ("discovered_inputs", discovered), - ], +fn reject_unit_tests(request: &Bound<'_, PyAny>, discovered: &Bound<'_, PyAny>) -> PyResult<()> { + reject_unit_test_selectors( + &native_field(request, "select")?, + &native_field(request, "exclude")?, + discovered, )?; - registry::native(py, NativeAttr::RejectUnitTestSelectors)?.call((), Some(&options))?; Ok(()) } @@ -176,21 +179,11 @@ fn prepare_source<'py>( } let discovered: Bound<'py, PyAny> = discover(py, project_dir, if compiled { None } else { Some(false) })?; - reject_unit_tests(py, request, &discovered)?; - let adapter_name: Bound<'py, PyAny> = - registry::native(py, NativeAttr::ResolveEffectiveAdapterName)?.call( - (), - Some(&keywords( - py, - &[ - ( - "project_config", - &native_field(&discovered, "project_config")?, - ), - ("local_config", &native_field(&discovered, "local_config")?), - ], - )?), - )?; + reject_unit_tests(request, &discovered)?; + let adapter_name: Bound<'py, PyAny> = resolve_effective_adapter_name( + &native_field(&discovered, "project_config")?, + &native_field(&discovered, "local_config")?, + )?; let adapter: Bound<'py, PyAny> = resolve_cli_adapter(&adapter_name, project_dir)?; let no_validation: bool = native_field(request, "no_sql_validation")?.extract::()? || !compiled; @@ -204,8 +197,7 @@ fn prepare_source<'py>( )?; options.set_item("no_sql_validation", no_validation)?; options.set_item("skip_column_inference", !compiled)?; - let graph: Bound<'py, PyAny> = - registry::native(py, NativeAttr::BuildProjectGraph)?.call((), Some(&options))?; + let graph: Bound<'py, PyAny> = build_project_graph(py, Some(options.clone()))?; let index: LineageIndex = graph_index(&graph)?; if let Some(fingerprint) = &fingerprint { write_lineage_cache(py, &directory, fingerprint, &index)?; @@ -265,16 +257,13 @@ fn semantic_uses( }; let targets: Vec = strings(&native_field(request, "targets")?)?; let model_names: Vec = if targets.is_empty() { - let options: Bound<'_, PyDict> = keywords( - py, - &[ - ("graph", graph), - ("select", &native_field(request, "select")?), - ("exclude", &native_field(request, "exclude")?), - ], - )?; let keys: Vec = lineage_keys( - ®istry::native(py, NativeAttr::SelectProjectGraph)?.call((), Some(&options))?, + select_project_graph( + graph, + &native_field(request, "select")?, + &native_field(request, "exclude")?, + )? + .as_any(), )?; keys.into_iter() .filter(|key| key.resource_type == MODEL_RESOURCE_TYPE) @@ -290,12 +279,15 @@ fn semantic_uses( }) .collect() }; - let uses: Bound<'_, PyAny> = - registry::native(py, NativeAttr::BuildDirectSemanticUses)?.call1(( - native_field(graph, "project")?, - adapter.call_method0("sql_analysis_dialect")?, - PyFrozenSet::new(py, model_names)?, - ))?; + let uses: Bound<'_, PyAny> = build_direct_semantic_uses( + &native_field(graph, "project")?, + adapter + .call_method0("sql_analysis_dialect")? + .extract::>()? + .as_deref(), + PyFrozenSet::new(py, model_names)?.as_any(), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; uses.try_iter()? .map(|value| semantic_use(&value?)) .collect() @@ -464,7 +456,7 @@ fn selector_views<'py>( fn view_options<'py>(views: &Bound<'py, PyAny>, names: &[&str]) -> PyResult> { let options: Bound<'py, PyDict> = PyDict::new(views.py()); for name in names { - options.set_item(*name, views.getattr(*name)?)?; + options.set_item(*name, native_field(views, name)?)?; } Ok(options) } @@ -476,9 +468,7 @@ fn selector_anchors( select: &[String], require_clear_anchors: bool, ) -> PyResult { - let parse: Bound<'_, PyAny> = registry::native(py, NativeAttr::ParseProjectSelector)?; - let matcher: Bound<'_, PyAny> = registry::native(py, NativeAttr::MatchProjectSelector)?; - let path_class: Bound<'_, PyAny> = registry::class(py, RecordId::PathSelector)?; + let path_class: Bound<'_, PyAny> = record_class(py, RecordId::PathSelector)?; let names: HashMap<&str, &LineageKey> = name_lookup(index); let named = |name: String| -> PyResult { names @@ -499,7 +489,8 @@ fn selector_anchors( )); } for part in token.split(SELECTOR_INTERSECTION_MARKER) { - let parsed: Bound<'_, PyAny> = parse.call1((part,))?; + let parsed: Bound<'_, PyAny> = + parse_project_selector(&pyo3::types::PyString::new(py, part))?; if parsed.is_instance(&path_class)? { let start: LineageKey = named(native_field(&parsed, "start_name")?.extract()?)?; let end: LineageKey = named(native_field(&parsed, "end_name")?.extract()?)?; @@ -523,8 +514,15 @@ fn selector_anchors( } let options: Bound<'_, PyDict> = view_options(views, &["all_keys", "tag_index", "path_index"])?; - options.set_item("parsed", &parsed)?; - let matched: Vec = lineage_keys(&matcher.call((), Some(&options))?)?; + let matched: Vec = lineage_keys( + match_project_selector( + &parsed, + &options.as_any().get_item("all_keys")?, + &options.as_any().get_item("tag_index")?, + &options.as_any().get_item("path_index")?, + )? + .as_any(), + )?; let upstream: bool = native_field(&parsed, "upstream")?.extract()?; let downstream: bool = native_field(&parsed, "downstream")?.extract()?; if set_kind || upstream { @@ -556,7 +554,8 @@ fn selector_rendering( options.set_item("select", &select)?; options.set_item("exclude", &exclude)?; let selected: Vec = lineage_keys( - ®istry::native(py, NativeAttr::ResolveProjectSelectors)?.call((), Some(&options))?, + &resolve_project_selectors(py, Some(options.clone())) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?, )?; let index: &LineageIndex = source.index(); let direction: Option = native_field(request, "direction")?.extract()?; @@ -565,8 +564,8 @@ fn selector_rendering( let options: Bound<'_, PyDict> = view_options(&views, &GRAPH_VIEW_NAMES)?; options.set_item("selectors", &exclude)?; let excluded: Vec = lineage_keys( - ®istry::native(py, NativeAttr::ResolveProjectSelectorTokens)? - .call((), Some(&options))?, + &resolve_project_selector_tokens(py, Some(options.clone())) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?, )?; select_directional_lineage(index, (&selected, &excluded), (&direction, depth)) } else { @@ -593,7 +592,7 @@ fn selector_failure( { return Ok(()); } - reject_unit_tests(py, request, &discover(py, project_dir, Some(false))?) + reject_unit_tests(request, &discover(py, project_dir, Some(false))?) }; match check() { Ok(()) => error, @@ -644,7 +643,7 @@ fn request_shape(request: &Bound<'_, PyAny>) -> PyResult { /// Run one `sqb lineage` request and print its result. #[pyfunction] #[pyo3(signature = (*, request))] -fn run_lineage_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { +pub(crate) fn run_lineage_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { let shape: LineageRequestShape = request_shape(request)?; validate_lineage_request(&shape).map_err(|error| user_error(py, error))?; let depth: Option = diff --git a/crates/sqlbuild-python/src/bindings/_helpers/lineage/lineage_index.rs b/crates/sqlbuild-python/src/bindings/_helpers/lineage/lineage_index.rs index f6c960c321..7806212141 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/lineage/lineage_index.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/lineage/lineage_index.rs @@ -2,9 +2,9 @@ use std::collections::HashMap; +use crate::bindings::_helpers::records::construct::{new_record, record_class}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; use pyo3::types::{PyDict, PyDictMethods, PyFrozenSet, PyTuple}; use sqlbuild_analysis::lineage_command::models::{LineageIndex, LineageKey, LineageNode}; @@ -31,7 +31,7 @@ pub(crate) struct PythonKeys<'py> { impl<'py> PythonKeys<'py> { pub(crate) fn new(py: Python<'py>) -> PyResult { Ok(Self { - class: registry::class(py, RecordId::CompiledObjectKey)?, + class: record_class(py, RecordId::CompiledObjectKey)?, made: HashMap::new(), }) } @@ -75,7 +75,7 @@ fn optional_text(value: Bound<'_, PyAny>) -> PyResult> { /// A source entry's `database.schema.table`, as far as it is set. fn source_relation_name(entry: &Bound<'_, PyAny>) -> PyResult> { let part = |name: &str| -> PyResult> { - match entry.getattr(name) { + match native_field(entry, name) { Ok(value) if !value.is_none() => Ok(Some(value.extract()?)), _ => Ok(None), } @@ -219,5 +219,5 @@ pub(crate) fn index_views<'py>( options.set_item("tag_index", tags)?; options.set_item("path_index", paths)?; options.set_item("all_keys", names)?; - registry::class(py, RecordId::LineageGraphViews)?.call((), Some(&options)) + new_record(py, RecordId::LineageGraphViews, (), Some(&options)) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/lineage/lineage_objects.rs b/crates/sqlbuild-python/src/bindings/_helpers/lineage/lineage_objects.rs index e8bee6abb6..bb45d5359d 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/lineage/lineage_objects.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/lineage/lineage_objects.rs @@ -2,9 +2,9 @@ use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; +use crate::bindings::_helpers::records::construct::record_class; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; use pyo3::types::{PyDict, PyTuple}; use sqlbuild_analysis::lineage::models::{LineageColumn, LineageSource}; @@ -34,21 +34,21 @@ pub(crate) struct LineageClasses<'py> { impl<'py> LineageClasses<'py> { pub(crate) fn new(py: Python<'py>) -> PyResult { - let resource_type: Bound<'py, PyAny> = registry::class(py, RecordId::CompiledResourceType)?; - let nullability: Bound<'py, PyAny> = registry::class(py, RecordId::InferredNullability)?; + let resource_type: Bound<'py, PyAny> = record_class(py, RecordId::CompiledResourceType)?; + let nullability: Bound<'py, PyAny> = record_class(py, RecordId::InferredNullability)?; Ok(Self { - qualified_column: registry::class(py, RecordId::QualifiedLineageColumn)?, - lineage_source: registry::class(py, RecordId::ColumnLineageSource)?, - lineage_edge: registry::class(py, RecordId::ColumnLineageEdge)?, - column_lineage: registry::class(py, RecordId::ColumnLineage)?, - model_lineage: registry::class(py, RecordId::ModelColumnLineage)?, - semantic_use: registry::class(py, RecordId::DirectSemanticColumnUse)?, - column_fact: registry::class(py, RecordId::CompiledLineageColumnFact)?, - source_fact: registry::class(py, RecordId::CompiledLineageSourceFact)?, - compact_facts: registry::class(py, RecordId::CompactLineageFacts)?, - transform_kind: registry::class(py, RecordId::ColumnTransformKind)?, - confidence: registry::class(py, RecordId::ColumnLineageConfidence)?, - analysis_dialect: registry::class(py, RecordId::PolyglotAnalysisDialect)?, + qualified_column: record_class(py, RecordId::QualifiedLineageColumn)?, + lineage_source: record_class(py, RecordId::ColumnLineageSource)?, + lineage_edge: record_class(py, RecordId::ColumnLineageEdge)?, + column_lineage: record_class(py, RecordId::ColumnLineage)?, + model_lineage: record_class(py, RecordId::ModelColumnLineage)?, + semantic_use: record_class(py, RecordId::DirectSemanticColumnUse)?, + column_fact: record_class(py, RecordId::CompiledLineageColumnFact)?, + source_fact: record_class(py, RecordId::CompiledLineageSourceFact)?, + compact_facts: record_class(py, RecordId::CompactLineageFacts)?, + transform_kind: record_class(py, RecordId::ColumnTransformKind)?, + confidence: record_class(py, RecordId::ColumnLineageConfidence)?, + analysis_dialect: record_class(py, RecordId::PolyglotAnalysisDialect)?, model_resource: native_field(&resource_type, "MODEL")?, unknown_nullability: native_field(&nullability, "UNKNOWN")?, nullability, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/lineage/project_lineage.rs b/crates/sqlbuild-python/src/bindings/_helpers/lineage/project_lineage.rs index 6441bd888a..ea01075063 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/lineage/project_lineage.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/lineage/project_lineage.rs @@ -64,7 +64,7 @@ pub(crate) fn build_project_column_lineage<'py>( /// Direct non-projection semantic column uses of the requested models. #[pyfunction] -fn build_direct_semantic_uses<'py>( +pub(crate) fn build_direct_semantic_uses<'py>( project: &Bound<'py, PyAny>, dialect: Option<&str>, model_names: &Bound<'py, PyAny>, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/lineage/project_schema.rs b/crates/sqlbuild-python/src/bindings/_helpers/lineage/project_schema.rs index 623b015e46..9cb3beeafd 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/lineage/project_schema.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/lineage/project_schema.rs @@ -71,10 +71,10 @@ fn project_columns(project: &Bound<'_, PyAny>) -> PyResult> ("sources", "source_entry", LineageResourceType::Source), ("seeds", "schema_entry", LineageResourceType::Seed), ] { - for resource in project.getattr(attribute)?.try_iter()? { + for resource in native_field(project, attribute)?.try_iter()? { let resource: Bound<'_, PyAny> = resource?; let mut typed: TypedColumns = Vec::new(); - for column in native_field(&resource.getattr(entry)?, "columns")?.try_iter()? { + for column in native_field(&native_field(&resource, entry)?, "columns")?.try_iter()? { let column: Bound<'_, PyAny> = column?; typed.push(( native_field(&column, "name")?.extract()?, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/lineage/relation_fingerprint.rs b/crates/sqlbuild-python/src/bindings/_helpers/lineage/relation_fingerprint.rs index 275c8663ea..3d85c883bc 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/lineage/relation_fingerprint.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/lineage/relation_fingerprint.rs @@ -12,7 +12,7 @@ use crate::bindings::types::CompilerDetach; /// The authored files' digest, or `None`; the flag is true where `rglob` raises mid-listing. #[pyfunction] -fn relation_lineage_fingerprint( +pub(crate) fn relation_lineage_fingerprint( py: Python<'_>, project_dir: PathBuf, prefix: Vec, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/load/ingestr_command.rs b/crates/sqlbuild-python/src/bindings/_helpers/load/ingestr_command.rs index cd6d231201..fd8825e1c3 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/load/ingestr_command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/load/ingestr_command.rs @@ -98,7 +98,7 @@ fn resolved_source( let py: Python<'_> = config.py(); let value: Bound<'_, PyDict> = PyDict::new(py); for field in TEMPLATE_FIELDS { - let raw: Bound<'_, PyAny> = config.getattr(field)?; + let raw: Bound<'_, PyAny> = native_field(config, field)?; if LIST_FIELDS.contains(&field) { value.set_item( field, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/load/load_command.rs b/crates/sqlbuild-python/src/bindings/_helpers/load/load_command.rs index 5ef2ef5906..88ba6f7884 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/load/load_command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/load/load_command.rs @@ -16,10 +16,17 @@ use sqlbuild_core::load_command::main::render_load_plan::render_load_plan; use sqlbuild_core::load_command::main::render_load_ready::render_load_ready; use sqlbuild_core::load_command::models::{LoadCounts, LoadResultText}; +use crate::bindings::_helpers::cli::progress_reporters::ConnectionProgressReporter; +use crate::bindings::_helpers::cli::progress_text::write_execution_header; use crate::bindings::_helpers::command_runtime::adapters::resolve_cli_adapter; +use crate::bindings::_helpers::command_runtime::connection::resolve_project_connection_config; +use crate::bindings::_helpers::compile_attachment::effective_config::build_effective_runtime_config; +use crate::bindings::_helpers::compile_attachment::effective_config::build_effective_settings_config; +use crate::bindings::_helpers::compile_attachment::effective_config::build_effective_target_config; use crate::bindings::_helpers::compile_driver::discovery::discover_project_inputs; use crate::bindings::_helpers::compile_driver::python_calls::call_with; use crate::bindings::_helpers::compile_driver::python_calls::keywords; +use crate::bindings::_helpers::contracts::spec_resolution::resolve_effective_adapter_name; use crate::bindings::_helpers::cursor::overrides::{cursor_integer, cursor_timestamp}; use crate::bindings::_helpers::load::load_progress::{ LoadCallbackRelay, LoadProgressReporter, load_result_text, @@ -31,10 +38,17 @@ use crate::bindings::_helpers::native_host::adapter::AdapterHost; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::native_host::user_code::UserCodeHost; +use crate::bindings::_helpers::planner_resolution::cursor_overrides::type_cursor_overrides; +use crate::bindings::_helpers::planner_selection::unit_tests::reject_unit_test_selectors; use crate::bindings::_helpers::presentation_text::terminal::stdout_supports_color; +use crate::bindings::_helpers::records::construct::new_record; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr, RecordId}; +use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::references::source_relations::render_source_relation; +use crate::bindings::_helpers::result_output::documents::format_load_execution_json; +use crate::bindings::_helpers::result_output::documents::write_execution_json_output; +use crate::bindings::_helpers::result_output::event_writer::ExecutionEventWriter; const LOADER_NODE_KEY: &str = "sqlbuild_loader_node"; const EXECUTION_COMMAND: &str = "sqb load"; @@ -83,21 +97,11 @@ fn invocation<'py>(py: Python<'py>, request: &Bound<'py, PyAny>) -> PyResult = native_field(request, "select")?; let exclude: Bound<'py, PyAny> = native_field(request, "exclude")?; - call_with( - ®istry::native(py, NativeAttr::RejectUnitTestSelectors)?, - &[ - ("select", select.clone()), - ("exclude", exclude.clone()), - ("discovered_inputs", discovered.clone()), - ], - )?; + reject_unit_test_selectors(&select.clone(), &exclude.clone(), &discovered.clone())?; let selected_target: Bound<'py, PyAny> = native_field(request, "selected_target")?; - let target_config: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::BuildEffectiveTargetConfig)?, - &[ - ("discovered_inputs", discovered.clone()), - ("selected_target", selected_target.clone()), - ], + let target_config: Bound<'py, PyAny> = build_effective_target_config( + &discovered.clone(), + Some(selected_target.clone()).filter(|value| !value.is_none()), )?; let defaults: LoaderDefaults<'py> = loader_defaults( &discovered, @@ -250,15 +254,14 @@ fn project_relations<'py>( let reference_class: Bound<'py, PyAny> = host::entry(py, UserCodeHost::SqlResourceRef)?; let source_kind: Bound<'py, PyAny> = host::entry(py, UserCodeHost::SqlResourceRefKind)?.getattr("SOURCE")?; - let render: Bound<'py, PyAny> = registry::native(py, NativeAttr::RenderSourceRelation)?; let relations: Bound<'py, PyDict> = PyDict::new(py); for (name, source) in sources.iter() { let key: Bound<'py, PyAny> = call_with( &reference_class, &[("kind", source_kind.clone()), ("name", name)], )?; - let relation: Bound<'py, PyAny> = - call_with(&render, &[("entry", source), ("adapter", adapter.clone())])?; + let relation: String = + render_source_relation(&source, Some(adapter).filter(|value| !value.is_none()))?; relations.set_item(key, relation)?; } Ok(relations.into_any()) @@ -270,58 +273,46 @@ fn prepare<'py>( ) -> PyResult> { let py: Python<'py> = request.py(); let discovered: &Bound<'py, PyAny> = &invocation.discovered; - let adapter_name: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ResolveEffectiveAdapterName)?, - &[ - ( - "project_config", - native_field(discovered, "project_config")?, - ), - ("local_config", native_field(discovered, "local_config")?), - ], + let adapter_name: Bound<'py, PyAny> = resolve_effective_adapter_name( + &native_field(discovered, "project_config")?, + &native_field(discovered, "local_config")?, )?; let adapter: Bound<'py, PyAny> = resolve_cli_adapter(&adapter_name, &invocation.project_dir)?; - let connection_config: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ResolveProjectConnectionConfig)?, - &[ - ("discovered_inputs", discovered.clone()), - ("project_dir", invocation.project_dir.clone()), - ("selected_target", native_field(request, "selected_target")?), - ("cli_vars", native_field(request, "cli_vars")?), - ], + let connection_config: Bound<'py, PyAny> = resolve_project_connection_config( + py, + Some(keywords( + py, + &[ + ("discovered_inputs", discovered.clone()), + ("project_dir", invocation.project_dir.clone()), + ("selected_target", native_field(request, "selected_target")?), + ("cli_vars", native_field(request, "cli_vars")?), + ], + )?), )?; check_reference_targets(invocation, (&adapter, &connection_config))?; - let runtime: (Bound<'py, PyAny>, Bound<'py, PyAny>, Bound<'py, PyAny>) = call_with( - ®istry::native(py, NativeAttr::BuildEffectiveRuntimeConfig)?, - &[ - ("discovered_inputs", discovered.clone()), - ("selected_target", native_field(request, "selected_target")?), - ("cli_vars", native_field(request, "cli_vars")?), - ], - )? - .extract()?; + let runtime: (Bound<'py, PyAny>, Bound<'py, PyAny>, Bound<'py, PyAny>) = + build_effective_runtime_config( + &discovered.clone(), + Some(native_field(request, "selected_target")?).filter(|value| !value.is_none()), + Some(native_field(request, "cli_vars")?).filter(|value| !value.is_none()), + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))? + .extract()?; let requested: Bound<'py, PyAny> = native_field(request, "concurrency")?; let concurrency: i64 = if requested.is_none() { - call_with( - ®istry::native(py, NativeAttr::BuildEffectiveSettingsConfig)?, - &[("discovered_inputs", discovered.clone())], - )? - .getattr("concurrency")? - .extract()? + build_effective_settings_config(&discovered.clone())? + .getattr("concurrency")? + .extract()? } else { requested.extract()? }; - let typed: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::TypeCursorOverrides)?, - &[( - "cursor_overrides", - native_field(request, "cursor_overrides")?, - )], - )?; + let typed: Bound<'py, PyAny> = + type_cursor_overrides(&native_field(request, "cursor_overrides")?, &[])?; let cursor_overrides: Bound<'py, PyAny> = if typed.is_truthy()? { typed } else { - registry::class(py, RecordId::CursorOverrides)?.call0()? + new_record(py, RecordId::CursorOverrides, (), None)? }; let provider_session: Bound<'py, PyAny> = call_with( &host::entry(py, UserCodeHost::BuildProviderSession)?, @@ -403,43 +394,53 @@ fn execute<'py>( ) -> PyResult<(Vec>, f64)> { let py: Python<'py> = request.py(); let started: Instant = Instant::now(); - let event_writer: Bound<'py, PyAny> = - registry::native(py, NativeAttr::ExecutionEventWriter)?.call0()?; + let event_writer: Bound<'py, PyAny> = ExecutionEventWriter::new(py, None) + .and_then(|value| pyo3::Bound::new(py, value)) + .map(pyo3::Bound::into_any)?; let order: Bound<'py, PyDict> = PyDict::new(py); for (index, source) in invocation.selected.iter().enumerate() { order.set_item(native_field(source, "name")?, index + 1)?; } let reporter: Bound<'py, LoadProgressReporter> = - registry::native(py, NativeAttr::LoadProgressReporter)? - .call( - (), - Some(&{ + crate::bindings::_helpers::load::load_progress::LoadProgressReporter::new( + py, + Some( + { let options: Bound<'py, PyDict> = PyDict::new(py); options.set_item("stream", &invocation.stream)?; options.set_item("use_color", invocation.style.use_color)?; options.set_item("source_order", order)?; options.set_item("total_count", invocation.selected.len())?; options - }), - )? - .downcast_into()?; - let connection: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ConnectionProgressReporter)?, - &[ - ("adapter_name", text(py, &preparation.adapter_name)), - ("stream", invocation.stream.clone()), - ( - "blank_line_after_complete", - PyBool::new(py, true).to_owned().into_any(), + } + .clone(), ), - ( - "use_color", - PyBool::new(py, invocation.style.use_color) - .to_owned() - .into_any(), - ), - ], - )?; + ) + .and_then(|value| pyo3::Bound::new(py, value)) + .map(pyo3::Bound::into_any)? + .downcast_into()?; + let connection: Bound<'py, PyAny> = ConnectionProgressReporter::new( + py, + Some(keywords( + py, + &[ + ("adapter_name", text(py, &preparation.adapter_name)), + ("stream", invocation.stream.clone()), + ( + "blank_line_after_complete", + PyBool::new(py, true).to_owned().into_any(), + ), + ( + "use_color", + PyBool::new(py, invocation.style.use_color) + .to_owned() + .into_any(), + ), + ], + )?), + ) + .and_then(|value| pyo3::Bound::new(py, value)) + .map(pyo3::Bound::into_any)?; let relay: Bound<'py, LoadCallbackRelay> = Bound::new( py, LoadCallbackRelay { @@ -508,21 +509,16 @@ fn execute<'py>( fn write_json(request: &Bound<'_, PyAny>, results: &[Bound<'_, PyAny>]) -> PyResult<()> { let py: Python<'_> = request.py(); - let payload: Bound<'_, PyAny> = call_with( - ®istry::native(py, NativeAttr::FormatLoadExecutionJson)?, - &[("results", PyTuple::new(py, results)?.into_any())], - )?; - call_with( - ®istry::native(py, NativeAttr::WriteExecutionJsonOutput)?, - &[ - ("payload", payload), - ("json_output", native_field(request, "json_output")?), - ( - "json_output_path", - native_field(request, "json_output_path")?, - ), - ], - )?; + let payload: Bound<'_, PyAny> = + format_load_execution_json(py, PyTuple::new(py, results)?.into_any()) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; + write_execution_json_output( + py, + payload, + native_field(request, "json_output")?.extract::()?, + native_field(request, "json_output_path")?, + ) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; Ok(()) } @@ -537,7 +533,7 @@ fn loader_node_flag(source: &Bound<'_, PyAny>) -> PyResult { /// Run one `sqb load` request. #[pyfunction] #[pyo3(signature = (*, request))] -fn run_load_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { +pub(crate) fn run_load_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { let invocation: Invocation<'_> = invocation(py, request)?; write( &invocation.stream, @@ -570,7 +566,8 @@ fn run_load_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult options.set_item("target", py.None())?; options.set_item("concurrency", preparation.concurrency)?; options.set_item("use_color", invocation.style.use_color)?; - registry::native(py, NativeAttr::WriteExecutionHeader)?.call((), Some(&options))?; + write_execution_header(py, Some(options.clone())) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; execute(request, &invocation, &preparation) })(); let closed: PyResult> = preparation.provider_session.call_method0("close"); diff --git a/crates/sqlbuild-python/src/bindings/_helpers/load/load_progress.rs b/crates/sqlbuild-python/src/bindings/_helpers/load/load_progress.rs index ea751efde9..37bb5f7858 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/load/load_progress.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/load/load_progress.rs @@ -12,12 +12,12 @@ use sqlbuild_core::load_command::main::render_load_spinner_line::render_load_spi use sqlbuild_core::load_command::models::{LoadResultText, LoadSpinnerRow, LoadStatus}; use crate::bindings::_helpers::build_progress::terminal::start_spinner; +use crate::bindings::_helpers::build_progress::transient_lines::shared_transient_line_coordinator; +use crate::bindings::_helpers::cli::progress::current_native_progress_projector; use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, within}; use crate::bindings::_helpers::native_host::adapter::AdapterHost; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::NativeAttr; -use crate::bindings::_helpers::records::registry; const LOADER_NODE_KEY: &str = "sqlbuild_loader_node"; const SPINNER_TICK_SECONDS: f64 = 0.1; @@ -190,7 +190,7 @@ impl LoadProgressReporter { impl LoadProgressReporter { #[new] #[pyo3(signature = (**options))] - fn new(py: Python<'_>, options: Option>) -> PyResult { + pub(crate) fn new(py: Python<'_>, options: Option>) -> PyResult { let arguments: KeywordArguments<'_> = KeywordArguments::read( py, "LoadProgressReporter", @@ -200,10 +200,9 @@ impl LoadProgressReporter { let stream: Bound<'_, PyAny> = arguments.value("stream")?; let source_order: HashMap = arguments.value("source_order")?.extract()?; let total_count: i64 = arguments.value("total_count")?.extract()?; - let lines: Bound<'_, PyAny> = - registry::native(py, NativeAttr::SharedTransientLineCoordinator)?.call0()?; - let projector: Bound<'_, PyAny> = - registry::native(py, NativeAttr::CurrentNativeProgressProjector)?.call0()?; + let lines: Bound<'_, PyAny> = shared_transient_line_coordinator(py) + .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; + let projector: Bound<'_, PyAny> = current_native_progress_projector(py)?; if !projector.is_none() { let options: Bound<'_, PyDict> = PyDict::new(py); options.set_item("ordinals", source_order.clone())?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/load/load_selection.rs b/crates/sqlbuild-python/src/bindings/_helpers/load/load_selection.rs index 26feb7a965..52dd1441a9 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/load/load_selection.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/load/load_selection.rs @@ -12,13 +12,16 @@ use sqlbuild_core::load_command::main::load_reference_loaders::load_reference_lo use sqlbuild_core::load_command::main::select_load_entries::select_load_entries as select_entries; use sqlbuild_core::load_command::models::{LoadEntry, LoadGraph}; -use crate::bindings::_helpers::compile_driver::python_calls::{ - KeywordArguments, call_with, imported, -}; +use crate::bindings::_helpers::compile_attachment::effective_config::build_effective_connection_config; +use crate::bindings::_helpers::compile_driver::python_calls::KeywordArguments; +use crate::bindings::_helpers::contracts::spec_resolution::loader_destination_parts; +use crate::bindings::_helpers::contracts::spec_resolution::resolve_effective_adapter_name; use crate::bindings::_helpers::native_host::adapter::AdapterHost; use crate::bindings::_helpers::native_host::host; +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::{ErrorId, NativeAttr, RecordId}; +use crate::bindings::_helpers::records::fields::replaced_keywords; +use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; const LOADER_NODE_KEY: &str = "sqlbuild_loader_node"; @@ -45,22 +48,21 @@ pub(crate) fn environment_sources<'py>( target_config: &Bound<'py, PyAny>, ) -> PyResult>> { let py: Python<'py> = discovered.py(); - let replace: Bound<'py, PyAny> = imported(py, "dataclasses", "replace")?; let mut sources: Vec> = Vec::new(); for source_file in native_field(discovered, "source_files")?.try_iter()? { for source in native_field(&source_file?, "source_entries")?.try_iter()? { let source: Bound<'py, PyAny> = source?; let changes: Bound<'py, PyDict> = PyDict::new(py); for field in ["database", "schema"] { - let value: Bound<'py, PyAny> = source.getattr(field)?; + let value: Bound<'py, PyAny> = native_field(&source, field)?; let effective: Bound<'py, PyAny> = if !value.is_none() || target_config.is_none() { value } else { - target_config.getattr(field)? + native_field(target_config, field)? }; changes.set_item(field, effective)?; } - sources.push(replace.call((source,), Some(&changes))?); + sources.push(replaced_keywords(&source, &changes)?); } } Ok(sources) @@ -126,19 +128,18 @@ impl<'py> LoadProject<'py> { } else { destination }; - let parts: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::LoaderDestinationPartsFunction)?, - &[ - ("destination", destination), - ("default_database", defaults.database.clone()), - ("default_schema", defaults.schema.clone()), - ], + let parts: Bound<'py, PyAny> = loader_destination_parts( + py, + &destination.extract::()?, + Some(defaults.database.clone()).filter(|value| !value.is_none()), + Some(defaults.schema.clone()).filter(|value| !value.is_none()), )?; let meta: Bound<'py, PyDict> = PyDict::new(py); meta.set_item(LOADER_NODE_KEY, true)?; let name_value: Bound<'py, PyAny> = PyString::new(py, name).into_any(); - call_with( - ®istry::class(py, RecordId::SourceEntry)?, + new_record_with( + py, + RecordId::SourceEntry, &[ ("name", name_value.clone()), ("database", native_field(&parts, "database")?), @@ -238,7 +239,7 @@ fn first_truthy<'py>( ) -> PyResult> { let py: Python<'py> = target_config.py(); if !target_config.is_none() { - let value: Bound<'py, PyAny> = target_config.getattr(field)?; + let value: Bound<'py, PyAny> = native_field(target_config, field)?; if value.is_truthy()? { return Ok(value); } @@ -256,24 +257,15 @@ pub(crate) fn loader_defaults<'py>( ), ) -> PyResult> { let py: Python<'py> = discovered.py(); - let connection: Bound<'py, PyDict> = call_with( - ®istry::native(py, NativeAttr::BuildEffectiveConnectionConfig)?, - &[ - ("discovered_inputs", discovered.clone()), - ("selected_target", selected_target.clone()), - ("cli_vars", cli_vars.clone()), - ], + let connection: Bound<'py, PyDict> = build_effective_connection_config( + &discovered.clone(), + Some(selected_target.clone()).filter(|value| !value.is_none()), + Some(cli_vars.clone()).filter(|value| !value.is_none()), )? .downcast_into()?; - let adapter_name: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ResolveEffectiveAdapterName)?, - &[ - ( - "project_config", - native_field(discovered, "project_config")?, - ), - ("local_config", native_field(discovered, "local_config")?), - ], + let adapter_name: Bound<'py, PyAny> = resolve_effective_adapter_name( + &native_field(discovered, "project_config")?, + &native_field(discovered, "local_config")?, )?; let duckdb: Bound<'py, PyAny> = host::entry(py, AdapterHost::BuiltinAdapter)?.getattr("DUCKDB")?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/macro_bridge/store_keys.rs b/crates/sqlbuild-python/src/bindings/_helpers/macro_bridge/store_keys.rs index 3431f09483..6ff716cb59 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/macro_bridge/store_keys.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/macro_bridge/store_keys.rs @@ -150,7 +150,7 @@ impl<'py> CanonicalTypes<'py> { let mut items: Vec = vec![JsonValue::String("timedelta".to_owned())]; for part in ["days", "seconds", "microseconds"] { items.push(JsonValue::Integer(JsonInteger::from( - value.getattr(part)?.extract::()?, + native_field(value, part)?.extract::()?, ))); } return Ok(JsonValue::Array(items)); @@ -218,7 +218,7 @@ impl<'py> CanonicalTypes<'py> { /// Name a macro by its name, file, identity and exact source, or None when it has no key. #[pyfunction] #[pyo3(signature = (**options))] -fn macro_store_token<'py>( +pub(crate) fn macro_store_token<'py>( py: Python<'py>, options: Option>, ) -> PyResult> { @@ -233,8 +233,7 @@ fn macro_store_token<'py>( let types: CanonicalTypes<'py> = CanonicalTypes::load(py)?; let name: JsonValue = text(native_field(&loaded, "name")?.downcast::()?)?; let path: JsonValue = text( - loaded - .getattr("relative_path")? + native_field(&loaded, "relative_path")? .call_method0("as_posix")? .downcast::()?, )?; @@ -251,7 +250,7 @@ fn macro_store_token<'py>( /// Digest the whole context a macro may read, or None when a value has no stable text. #[pyfunction] #[pyo3(signature = (**options))] -fn context_store_token<'py>( +pub(crate) fn context_store_token<'py>( py: Python<'py>, options: Option>, ) -> PyResult> { @@ -275,7 +274,10 @@ fn context_store_token<'py>( ]; for name in ["inaccessible_constants", "inaccessible_enums"] { let mut items: Vec = Vec::new(); - for item in sorted.call1((declarations.getattr(name)?,))?.try_iter()? { + for item in sorted + .call1((native_field(&declarations, name)?,))? + .try_iter()? + { items.push(plain_json(&item?)?); } parts.push(JsonValue::Array(items)); diff --git a/crates/sqlbuild-python/src/bindings/_helpers/manifest/rows.rs b/crates/sqlbuild-python/src/bindings/_helpers/manifest/rows.rs index 5e352f28b4..fac146af2d 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/manifest/rows.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/manifest/rows.rs @@ -13,16 +13,16 @@ use sqlbuild_analysis::manifest::models::{ use sqlbuild_analysis::python_values::models::PythonValue; use crate::bindings::_helpers::python_values::values::python_value; +use crate::bindings::_helpers::records::construct::record_class; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; const DECIMAL_KIND: &str = "decimal"; const MACRO_KIND: &str = "macro"; const DEFAULT_MATERIALIZATION: &str = "view"; fn attribute<'py>(value: &Bound<'py, PyAny>, name: &str) -> PyResult> { - value.getattr(name) + native_field(value, name) } fn text(value: &Bound<'_, PyAny>, name: &str) -> PyResult { @@ -149,8 +149,8 @@ fn hooks(value: &Bound<'_, PyAny>) -> PyResult>> { if !(value.is_instance_of::() || value.is_instance_of::()) { return Ok(None); } - let sql_class: Bound<'_, PyAny> = registry::class(value.py(), RecordId::SqlHookEntry)?; - let python_class: Bound<'_, PyAny> = registry::class(value.py(), RecordId::PythonHookEntry)?; + let sql_class: Bound<'_, PyAny> = record_class(value.py(), RecordId::SqlHookEntry)?; + let python_class: Bound<'_, PyAny> = record_class(value.py(), RecordId::PythonHookEntry)?; value .try_iter()? .map(|entry| hook(&entry?, (&sql_class, &python_class))) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/microbatches/projection.rs b/crates/sqlbuild-python/src/bindings/_helpers/microbatches/projection.rs index fe5c70c1bd..1e904e48dd 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/microbatches/projection.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/microbatches/projection.rs @@ -187,7 +187,7 @@ fn replay<'py>(request: Option<&Bound<'py, PyDict>>) -> PyResult = piece?; pieces.push(CoveragePiece { interval: parser.interval(&piece)?, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/microbatches/statements.rs b/crates/sqlbuild-python/src/bindings/_helpers/microbatches/statements.rs index ea9060f12e..cd90df39ac 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/microbatches/statements.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/microbatches/statements.rs @@ -261,9 +261,9 @@ fn create_indexes<'py>(request: Option<&Bound<'py, PyDict>>) -> PyResult>) -> PyResult { let request = Request::new("deterministic_microbatch_event_id", request)?; let scope: Bound<'_, PyAny> = request.get("scope")?; - let field = |name: &str| -> PyResult { scope.getattr(name)?.extract() }; + let field = |name: &str| -> PyResult { native_field(&scope, name)?.extract() }; let optional = - |name: &str| -> PyResult> { optional_text(&scope.getattr(name)?) }; + |name: &str| -> PyResult> { optional_text(&native_field(&scope, name)?) }; let identity = MicrobatchEventIdentity { scope_kind: field("scope_kind")?, scope_key: field("scope_key")?, @@ -272,7 +272,7 @@ fn deterministic_event_id(request: Option<&Bound<'_, PyDict>>) -> PyResult) -> PyResult> fn built_contract_classes(py: Python<'_>) -> PyResult> { let classes: Bound<'_, PyDict> = PyDict::new(py); for (key, class) in CONTRACT_CLASSES { - classes.set_item(key, registry::class(py, class)?)?; + classes.set_item(key, record_class(py, class)?)?; } classes.set_item( "severities", @@ -92,7 +92,7 @@ fn built_yaml_entry_classes(py: Python<'_>) -> PyResult> { let classes: Bound<'_, PyDict> = PyDict::new(py); classes.set_item("contracts", contract_classes(py)?)?; for (key, class) in YAML_ENTRY_CLASSES { - classes.set_item(key, registry::class(py, class)?)?; + classes.set_item(key, record_class(py, class)?)?; } for (key, class) in INTEGRATION_ENTRY_CLASSES { classes.set_item(key, host::entry(py, class)?)?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/model_config/model_validator.rs b/crates/sqlbuild-python/src/bindings/_helpers/model_config/model_validator.rs index 090d0d39e2..ff64213160 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/model_config/model_validator.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/model_config/model_validator.rs @@ -97,7 +97,10 @@ fn returns_table(file: &Bound<'_, PyAny>) -> PyResult { impl NativeModelValidator { /// The validator for the resources and materializations `inputs` discovered. #[staticmethod] - fn from_inputs(inputs: Bound<'_, PyAny>, microbatch_concurrency: bool) -> PyResult { + pub(crate) fn from_inputs( + inputs: Bound<'_, PyAny>, + microbatch_concurrency: bool, + ) -> PyResult { let names: ResourceNames = discovered_names(&inputs)?; let custom_materializations: HashSet = each(&inputs, "materialization_files", |file| { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/model_config/project_defaults.rs b/crates/sqlbuild-python/src/bindings/_helpers/model_config/project_defaults.rs index 2d17e95531..1746e11126 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/model_config/project_defaults.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/model_config/project_defaults.rs @@ -1,5 +1,6 @@ //! The sparse mapping of a project's `[defaults]` that every model's config layers over. +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; use pyo3::types::{PyDict, PyDictMethods, PyList}; @@ -44,7 +45,7 @@ pub(crate) fn project_defaults_mapping<'py>( ) -> PyResult> { let values: Bound<'py, PyDict> = PyDict::new(defaults.py()); for (key, falsy_unset) in DEFAULT_KEYS { - let value: Bound<'py, PyAny> = defaults.getattr(key)?; + let value: Bound<'py, PyAny> = native_field(defaults, key)?; let unset: bool = if falsy_unset { !value.is_truthy()? } else { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/model_config/template_data.rs b/crates/sqlbuild-python/src/bindings/_helpers/model_config/template_data.rs index a3b1ae296a..b31a0e0bd2 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/model_config/template_data.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/model_config/template_data.rs @@ -186,7 +186,7 @@ fn expand_effective_vars<'py>(raw_values: &Bound<'py, PyDict>) -> PyResult, reads: Vec<(String, String)>) -> PyResult<()> { +pub(crate) fn record_template_reads(py: Python<'_>, reads: Vec<(String, String)>) -> PyResult<()> { record_reads( py, reads diff --git a/crates/sqlbuild-python/src/bindings/_helpers/output_capture/records.rs b/crates/sqlbuild-python/src/bindings/_helpers/output_capture/records.rs index 8249be09a4..e2edf0a83a 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/output_capture/records.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/output_capture/records.rs @@ -136,7 +136,7 @@ pub(crate) fn freeze_command_output_record<'py>( pub(crate) fn command_output_to_json(record: &Bound<'_, PyAny>) -> PyResult { let field = |name: &str| record.getattr(name); let occurred_at: String = call_with( - &field("occurred_at")?.getattr("isoformat")?, + &native_field(&field("occurred_at")?, "isoformat")?, &[("timespec", object(record.py(), "microseconds")?)], )? .extract()?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/plan_command/plan.rs b/crates/sqlbuild-python/src/bindings/_helpers/plan_command/plan.rs index 35517b405f..7653cd894b 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/plan_command/plan.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/plan_command/plan.rs @@ -7,17 +7,26 @@ use pyo3::types::{PyBool, PyDict, PyDictMethods}; use pyo3::wrap_pyfunction; use crate::bindings::_helpers::build_command::support::keyword_relay; +use crate::bindings::_helpers::cli::progress_reporters::{ + ConnectionProgressReporter, PlanningProgressReporter, +}; use crate::bindings::_helpers::cli::progress_text::with_verbose_inspection; use crate::bindings::_helpers::command_runtime::invocation::resolve_adapter_connection_context; +use crate::bindings::_helpers::command_runtime::preview_target::validate_preview_target; +use crate::bindings::_helpers::compile_attachment::effective_config::build_effective_settings_config; use crate::bindings::_helpers::compile_driver::discovery::discover_project_inputs; +use crate::bindings::_helpers::compile_driver::pipeline::run_compile_pipeline; use crate::bindings::_helpers::compile_driver::python_calls::keywords; use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; +use crate::bindings::_helpers::dbt_interop::reference_resolver::resolve_external_sql_reference_resolver; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; +use crate::bindings::_helpers::plan_output::plan_json::format_plan_json; +use crate::bindings::_helpers::plan_output::plan_text::format_plan; use crate::bindings::_helpers::presentation_text::terminal::stdout_supports_color; +use crate::bindings::_helpers::records::construct::new_record; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::ids::RecordId; const COMMAND_NAME: &str = "plan"; const DEFAULT_SECTION_ENTRIES: i64 = 50; @@ -76,11 +85,12 @@ fn resolve_invocation<'py>(request: &Bound<'py, PyAny>) -> PyResult = PyDict::new(py); - preview.set_item("discovered_inputs", &discovered)?; - preview.set_item("as_target", native_field(request, "as_target")?)?; - preview.set_item("command_name", COMMAND_NAME)?; - registry::native(py, NativeAttr::ValidatePreviewTarget)?.call((), Some(&preview))?; + validate_preview_target( + py, + discovered.clone(), + native_field(request, "as_target")?.extract()?, + COMMAND_NAME.to_owned(), + )?; let context: Bound<'py, PyAny> = resolve_adapter_connection_context( &discovered, &project_dir, @@ -97,28 +107,29 @@ fn resolve_invocation<'py>(request: &Bound<'py, PyAny>) -> PyResult = py.import("sys")? .getattr(if json_output { "stderr" } else { "stdout" })?; - let connection_progress: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ConnectionProgressReporter)?, - &[ - ("adapter_name", native_field(&context, "adapter_name")?), - ("stream", stream.clone()), - ("use_color", flag(py, use_color)), - ], - )?; - let planning_progress: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::PlanningProgressReporter)?, - &[ - ("stream", stream.clone()), - ("use_color", flag(py, use_color)), - ], - )?; + let connection_progress: Bound<'py, PyAny> = Bound::new( + py, + ConnectionProgressReporter::new( + py, + Some(keywords( + py, + &[ + ("adapter_name", native_field(&context, "adapter_name")?), + ("stream", stream.clone()), + ("use_color", flag(py, use_color)), + ], + )?), + )?, + )? + .into_any(); + let planning_progress: Bound<'py, PyAny> = + Bound::new(py, PlanningProgressReporter::new(py, &stream, use_color)?)?.into_any(); let requested_load: Bound<'py, PyAny> = native_field(request, "load_sources")?; let load_sources: Bound<'py, PyAny> = if requested_load.is_none() { - call_with( - ®istry::native(py, NativeAttr::BuildEffectiveSettingsConfig)?, - &[("discovered_inputs", discovered.clone())], + native_field( + &build_effective_settings_config(&discovered)?, + "auto_load_sources", )? - .getattr("auto_load_sources")? } else { requested_load }; @@ -172,12 +183,9 @@ fn compile_pipeline<'py>( invocation: &PlanInvocation<'py>, ) -> PyResult> { let py: Python<'py> = request.py(); - let resolver: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ResolveExternalSqlReferenceResolver)?, - &[ - ("project_dir", invocation.project_dir.clone()), - ("discovered_inputs", invocation.discovered.clone()), - ], + let resolver: Bound<'py, PyAny> = resolve_external_sql_reference_resolver( + invocation.project_dir.clone(), + invocation.discovered.clone(), )?; let as_target: Bound<'py, PyAny> = native_field(request, "as_target")?; if !as_target.is_none() { @@ -200,7 +208,7 @@ fn compile_pipeline<'py>( }, )?; for field in PIPELINE_FIELDS { - options.set_item(field, request.getattr(field)?)?; + options.set_item(field, native_field(request, field)?)?; } options.set_item("auto_load_sources", &invocation.load_sources)?; options.set_item( @@ -214,15 +222,18 @@ fn compile_pipeline<'py>( )?; options.set_item("record_migration_fingerprints", false)?; let pipeline_options: Bound<'py, PyAny> = - registry::class(py, RecordId::CompilePipelineOptions)?.call((), Some(&options))?; - call_with( - ®istry::native(py, NativeAttr::RunCompilePipeline)?, - &[ - ("discovered_inputs", invocation.discovered.clone()), - ("adapter", native_field(&invocation.context, "adapter")?), - ("options", pipeline_options), - ("hooks", connection_hooks(invocation)?), - ], + new_record(py, RecordId::CompilePipelineOptions, (), Some(&options))?; + run_compile_pipeline( + py, + Some(keywords( + py, + &[ + ("discovered_inputs", invocation.discovered.clone()), + ("adapter", native_field(&invocation.context, "adapter")?), + ("options", pipeline_options), + ("hooks", connection_hooks(invocation)?), + ], + )?), ) } @@ -235,9 +246,12 @@ fn write_output( let plan: Bound<'_, PyAny> = native_field(pipeline, "plan_output")?; let python_entries: Bound<'_, PyAny> = native_field(pipeline, "python_plan_entries")?; if native_field(request, "json_output")?.is_truthy()? { - let text: Bound<'_, PyAny> = call_with( - ®istry::native(py, NativeAttr::FormatPlanJson)?, - &[("plan", plan), ("python_plan_entries", python_entries)], + let text: String = format_plan_json( + py, + Some(keywords( + py, + &[("plan", plan), ("python_plan_entries", python_entries)], + )?), )?; imported(py, "builtins", "print")?.call1((text,))?; return Ok(()); @@ -251,29 +265,31 @@ fn write_output( Some(DEFAULT_SECTION_ENTRIES) }, )?; - let text: String = call_with( - ®istry::native(py, NativeAttr::FormatPlan)?, - &[ - ("plan", plan), - ("full_refresh", native_field(request, "full_refresh")?), - ("use_color", flag(py, invocation.use_color)), - ( - "display_options", - registry::class(py, RecordId::DisplayOptions)?.call((), Some(&display))?, - ), - ("python_plan_entries", python_entries), - ("include_direct_freshness_diagnostics", flag(py, false)), - ("include_completion", flag(py, true)), - ], - )? - .extract()?; + let text: String = format_plan( + py, + Some(keywords( + py, + &[ + ("plan", plan), + ("full_refresh", native_field(request, "full_refresh")?), + ("use_color", flag(py, invocation.use_color)), + ( + "display_options", + new_record(py, RecordId::DisplayOptions, (), Some(&display))?, + ), + ("python_plan_entries", python_entries), + ("include_direct_freshness_diagnostics", flag(py, false)), + ("include_completion", flag(py, true)), + ], + )?), + )?; print_line(py, &format!("\n{text}")) } /// Run one `sqb plan` request. #[pyfunction] #[pyo3(signature = (*, request))] -fn run_plan_command(request: &Bound<'_, PyAny>) -> PyResult { +pub(crate) fn run_plan_command(request: &Bound<'_, PyAny>) -> PyResult { let py: Python<'_> = request.py(); let invocation: PlanInvocation<'_> = resolve_invocation(request)?; if !native_field(request, "json_output")?.is_truthy()? { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/plan_command/rules.rs b/crates/sqlbuild-python/src/bindings/_helpers/plan_command/rules.rs index b5a3ecbde1..a3be4ae5df 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/plan_command/rules.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/plan_command/rules.rs @@ -8,11 +8,23 @@ use pyo3::wrap_pyfunction; use crate::bindings::_helpers::command_runtime::adapters::resolve_cli_adapter; use crate::bindings::_helpers::compile_driver::discovery::discover_project_inputs; +use crate::bindings::_helpers::compile_driver::project::build_project_graph; +use crate::bindings::_helpers::compile_driver::python_calls::imported; use crate::bindings::_helpers::compile_driver::python_calls::keywords; -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; +use crate::bindings::_helpers::contracts::spec_resolution::resolve_effective_adapter_name; +use crate::bindings::_helpers::planner_selection::selectors::select_project_graph; +use crate::bindings::_helpers::planner_selection::unit_tests::reject_unit_test_selectors; +use crate::bindings::_helpers::records::construct::new_record; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::fields::replaced_keywords; +use crate::bindings::_helpers::records::ids::RecordId; +use crate::bindings::_helpers::rules::catalogue::build_rule_catalogue; +use crate::bindings::_helpers::rules::guidance::{format_rule, install_rules_skills}; +use crate::bindings::_helpers::rules::results::{ + format_rules_result, format_skipped_type_proof_rules, +}; +use crate::bindings::_helpers::rules::rule_run::run_rules; +use crate::bindings::_helpers::rules::rules_config::load_rules_config; const LIST_ACTION: &str = "list"; const SHOW_ACTION: &str = "show"; @@ -52,15 +64,7 @@ fn run_skills( ) -> PyResult { let py: Python<'_> = request.py(); let check: bool = native_field(request, "skills_check")?.is_truthy()?; - let fresh: bool = call_with( - ®istry::native(py, NativeAttr::InstallRulesSkills)?, - &[ - ("config", config.clone()), - ("project_dir", project_dir.clone()), - ("check", native_field(request, "skills_check")?), - ], - )? - .is_truthy()?; + let fresh: bool = install_rules_skills(config, project_dir, check)?; if !check { print_line(py, "Installed Rules skills", false)?; return Ok(0); @@ -97,7 +101,7 @@ fn list_rules(catalogue: &Bound<'_, PyAny>, json_output: bool) -> PyResult let rule: Bound<'_, PyAny> = rule?; let item: Bound<'_, PyDict> = PyDict::new(py); for field in ["code", "family", "message", "custom"] { - item.set_item(field, rule.getattr(field)?)?; + item.set_item(field, native_field(&rule, field)?)?; } let subject: Bound<'_, PyAny> = native_field(&rule, "subject")?; item.set_item( @@ -130,11 +134,7 @@ fn show_rule( for rule in catalogue.try_iter()? { let rule: Bound<'_, PyAny> = rule?; if native_field(&rule, "code")?.extract::()? == selector { - let text: String = call_with( - ®istry::native(py, NativeAttr::FormatRule)?, - &[("rule", rule), ("config", config.clone())], - )? - .extract()?; + let text: String = format_rule(&rule, Some(config.clone()))?; print_line(py, &text, false)?; return Ok(0); } @@ -162,40 +162,23 @@ fn rule_inputs<'py>( )?; let select: Bound<'py, PyAny> = native_field(request, "select")?; let exclude: Bound<'py, PyAny> = native_field(request, "exclude")?; - call_with( - ®istry::native(py, NativeAttr::RejectUnitTestSelectors)?, - &[ - ("select", select.clone()), - ("exclude", exclude.clone()), - ("discovered_inputs", discovered.clone()), - ], - )?; - let adapter_name: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ResolveEffectiveAdapterName)?, - &[ - ( - "project_config", - native_field(&discovered, "project_config")?, - ), - ("local_config", native_field(&discovered, "local_config")?), - ], + reject_unit_test_selectors(&select, &exclude, &discovered)?; + let adapter_name: Bound<'py, PyAny> = resolve_effective_adapter_name( + &native_field(&discovered, "project_config")?, + &native_field(&discovered, "local_config")?, )?; let adapter: Bound<'py, PyAny> = resolve_cli_adapter(&adapter_name, project_dir)?; - let graph: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::BuildProjectGraph)?, - &[ - ("discovered_inputs", discovered.clone()), - ("adapter", adapter.clone()), - ], - )?; - let selected: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::SelectProjectGraph)?, - &[ - ("graph", graph.clone()), - ("select", select), - ("exclude", exclude), - ], + let graph: Bound<'py, PyAny> = build_project_graph( + py, + Some(keywords( + py, + &[ + ("discovered_inputs", discovered.clone()), + ("adapter", adapter.clone()), + ], + )?), )?; + let selected: Bound<'py, PyAny> = select_project_graph(&graph, &select, &exclude)?.into_any(); Ok(RuleInputs { discovered, adapter, @@ -235,56 +218,47 @@ fn run_rule_selection( replaced.set_item("select", PyTuple::new(py, [selector])?)?; replaced.set_item("ignore", PyTuple::empty(py))?; let dialect: Bound<'_, PyAny> = adapter.call_method0("sql_analysis_dialect")?; - let result: Bound<'_, PyAny> = call_with( - ®istry::native(py, NativeAttr::RunRules)?, - &[ - ("graph", graph), - ("discovered_inputs", discovered), - ( - "config", - imported(py, "dataclasses", "replace")?.call((config,), Some(&replaced))?, - ), - ("project_dir", project_dir.clone()), - ( - "dialect", - if dialect.is_truthy()? { - dialect - } else { - pyo3::types::PyString::new(py, GENERIC_DIALECT).into_any() - }, - ), - ( - "selected_keys", - if narrowed { - selected - } else { - py.None().into_bound(py) - }, - ), - ], - )?; - let note: Bound<'_, PyAny> = call_with( - ®istry::native(py, NativeAttr::FormatSkippedTypeProofRules)?, - &[("codes", native_field(&result, "skipped_type_proof_rules")?)], + let result: Bound<'_, PyAny> = run_rules( + py, + Some(keywords( + py, + &[ + ("graph", graph), + ("discovered_inputs", discovered), + ("config", replaced_keywords(config, &replaced)?), + ("project_dir", project_dir.clone()), + ( + "dialect", + if dialect.is_truthy()? { + dialect + } else { + pyo3::types::PyString::new(py, GENERIC_DIALECT).into_any() + }, + ), + ( + "selected_keys", + if narrowed { + selected + } else { + py.None().into_bound(py) + }, + ), + ], + )?), )?; - if !note.is_none() { - print_line(py, &format!("note: {}", note.str()?), true)?; + if let Some(note) = format_skipped_type_proof_rules( + native_field(&result, "skipped_type_proof_rules")?.extract()?, + ) { + print_line(py, &format!("note: {note}"), true)?; } let fields: Bound<'_, PyDict> = PyDict::new(py); for field in RESULT_FIELDS { - fields.set_item(field, result.getattr(field)?)?; + fields.set_item(field, native_field(&result, field)?)?; } - let text: String = call_with( - ®istry::native(py, NativeAttr::FormatRulesResult)?, - &[ - ( - "result", - registry::class(py, RecordId::RulesResult)?.call((), Some(&fields))?, - ), - ("json_output", native_field(request, "json_output")?), - ], - )? - .extract()?; + let text: String = format_rules_result( + &new_record(py, RecordId::RulesResult, (), Some(&fields))?, + native_field(request, "json_output")?.is_truthy()?, + )?; print_line(py, &text, false)?; let findings: usize = native_field(&result, "findings")?.len()?; if findings > 0 { @@ -302,7 +276,7 @@ fn run_rule_selection( /// List, inspect, execute, or generate guidance for compiler rules. #[pyfunction] #[pyo3(signature = (*, request))] -fn run_rules_command(request: &Bound<'_, PyAny>) -> PyResult { +pub(crate) fn run_rules_command(request: &Bound<'_, PyAny>) -> PyResult { let py: Python<'_> = request.py(); let requested_dir: Bound<'_, PyAny> = native_field(request, "project_dir")?; let project_dir: Bound<'_, PyAny> = if requested_dir.is_none() { @@ -311,21 +285,12 @@ fn run_rules_command(request: &Bound<'_, PyAny>) -> PyResult { requested_dir } .call_method0("resolve")?; - let config: Bound<'_, PyAny> = call_with( - ®istry::native(py, NativeAttr::LoadRulesConfig)?, - &[("project_dir", project_dir.clone())], - )?; + let config: Bound<'_, PyAny> = load_rules_config(py, project_dir.extract()?)?.into_bound(py); let action: String = native_field(request, "action")?.extract()?; if action == SKILLS_ACTION { return run_skills(request, &config, &project_dir); } - let catalogue: Bound<'_, PyAny> = call_with( - ®istry::native(py, NativeAttr::BuildRuleCatalogue)?, - &[ - ("config", config.clone()), - ("project_dir", project_dir.clone()), - ], - )?; + let catalogue: Bound<'_, PyAny> = build_rule_catalogue(&config, &project_dir, true)?.into_any(); if action == LIST_ACTION { return list_rules( &catalogue, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/plan_output/cursor_details.rs b/crates/sqlbuild-python/src/bindings/_helpers/plan_output/cursor_details.rs index 38561eada9..e66e8d319b 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/plan_output/cursor_details.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/plan_output/cursor_details.rs @@ -9,9 +9,9 @@ use crate::bindings::_helpers::plan_output::display::{ aligned_timestamp_bounds, resolve_effective_microbatch_batch_size, }; use crate::bindings::_helpers::plan_output::values::{optional_text, text}; +use crate::bindings::_helpers::records::construct::record_class; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; const MICROBATCH_MODE: &str = "microbatch"; const TIMESTAMP_CURSOR: &str = "timestamp"; @@ -60,10 +60,7 @@ fn effective_grain(entry: &Bound<'_, PyAny>) -> PyResult> { return Ok(declared); } let mut effective: String = declared.unwrap_or_else(|| SECOND_GRAIN.to_owned()); - if entry - .getattr("microbatch_strategy")? - .eq(WATERMARK_STRATEGY)? - { + if native_field(entry, "microbatch_strategy")?.eq(WATERMARK_STRATEGY)? { return Ok(Some(effective)); } for relation in native_field(entry, "cursor_input_relations")?.try_iter()? { @@ -86,15 +83,13 @@ fn count_batches( let (start, end) = (native_field(bounds, "start")?, native_field(bounds, "end")?); if cursor_type == TIMESTAMP_CURSOR { let duration: Bound<'_, PyAny> = - registry::class(py, RecordId::Duration)?.call_method1("parse", (batch_size,))?; + record_class(py, RecordId::Duration)?.call_method1("parse", (batch_size,))?; if duration.is_none() { return Ok(None); } let temporal = |value: &Bound<'_, PyAny>| -> PyResult { - Ok( - value.is_instance(®istry::class(py, RecordId::DateValue)?)? - || value.is_instance(®istry::class(py, RecordId::TimestampValue)?)?, - ) + Ok(value.is_instance(&record_class(py, RecordId::DateValue)?)? + || value.is_instance(&record_class(py, RecordId::TimestampValue)?)?) }; if !temporal(&start)? || !temporal(&end)? { return Ok(None); @@ -112,7 +107,7 @@ fn count_batches( if cursor_type != INTEGER_CURSOR { return Ok(None); } - let integer: Bound<'_, PyAny> = registry::class(py, RecordId::IntegerValue)?; + let integer: Bound<'_, PyAny> = record_class(py, RecordId::IntegerValue)?; if !start.is_instance(&integer)? || !end.is_instance(&integer)? { return Ok(None); } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/plan_output/display.rs b/crates/sqlbuild-python/src/bindings/_helpers/plan_output/display.rs index 29d39adac2..d6cccac33f 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/plan_output/display.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/plan_output/display.rs @@ -18,19 +18,22 @@ use sqlbuild_core::plan_display::main::model_materialization_label::model_materi use sqlbuild_core::plan_display::main::model_resource_type::model_resource_type as resource_type; use sqlbuild_core::plan_display::models::{ModelLabelFacts, ParsedBound}; -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; +use crate::bindings::_helpers::compile_driver::python_calls::imported; use crate::bindings::_helpers::cursor::scalars::{ render_value, to_exclusive, to_inclusive, try_parse, }; use crate::bindings::_helpers::plan_output::values::{optional_text, text}; +use crate::bindings::_helpers::records::construct::{ + enum_from_value, new_record_with, record_class, +}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; const DATETIME_MODULE: &str = "datetime"; fn label_facts(entry: &Bound<'_, PyAny>) -> PyResult { - let field = |name: &str| -> PyResult> { optional_text(&entry.getattr(name)?) }; + let field = + |name: &str| -> PyResult> { optional_text(&native_field(entry, name)?) }; Ok(ModelLabelFacts { materialization_type: text(&native_field(entry, "materialization_type")?)?, action: text(&native_field(entry, "action")?)?, @@ -70,7 +73,7 @@ fn cursor_typing<'py>( } else { PyString::new(py, DEFAULT_CURSOR_GRAIN).into_any() }; - let grain: Bound<'py, PyAny> = registry::class(py, RecordId::CursorGrain)?.call1((grain,))?; + let grain: Bound<'py, PyAny> = enum_from_value(py, RecordId::CursorGrain, grain)?; Ok((effective, grain)) } @@ -109,8 +112,9 @@ pub(crate) fn resolve_bounded_cursor_override<'py>( ) else { return Ok(None); }; - call_with( - ®istry::class(py, RecordId::CursorBounds)?, + new_record_with( + py, + RecordId::CursorBounds, &[("start", start), ("end", to_exclusive(&end, &grain)?)], ) .map(Some) @@ -126,7 +130,7 @@ fn parsed_bound(value: &Bound<'_, PyAny>) -> PyResult> { }; let mut midnight: bool = true; for part in ["hour", "minute", "second", "microsecond"] { - midnight = midnight && parsed.getattr(part)?.extract::()? == 0; + midnight = midnight && native_field(&parsed, part)?.extract::()? == 0; } Ok(Some(ParsedBound { date: parsed @@ -216,7 +220,7 @@ pub(crate) fn resolve_effective_microbatch_batch_size( effective_grain: &str, ) -> Result { effective_microbatch_batch_size(batch_size, effective_grain).map_err(|error| { - registry::class(py, RecordId::CursorGrain) + record_class(py, RecordId::CursorGrain) .and_then(|grain| grain.call1((error.0,))) .err() .unwrap_or_else(|| PyValueError::new_err("unknown cursor grain")) @@ -226,7 +230,7 @@ pub(crate) fn resolve_effective_microbatch_batch_size( fn as_datetime<'py>(value: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = value.py(); let inner: Bound<'py, PyAny> = native_field(value, "value")?; - if value.is_instance(®istry::class(py, RecordId::TimestampValue)?)? { + if value.is_instance(&record_class(py, RecordId::TimestampValue)?)? { return Ok(inner); } let datetime: Bound<'py, PyAny> = imported(py, DATETIME_MODULE, "datetime")?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/plan_output/migrations.rs b/crates/sqlbuild-python/src/bindings/_helpers/plan_output/migrations.rs index 9b5ec90e53..f1e6b2d95e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/plan_output/migrations.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/plan_output/migrations.rs @@ -46,8 +46,11 @@ fn migration(entry: &Bound<'_, PyAny>, materialization: Option) -> PyRes origin: text(&relation_name(&native_field(entry, "origin")?)?)?, destination: text(&relation_name(&native_field(entry, "destination")?)?)?, decision, - compatibility: text(&native_field(entry, "compatibility")?.getattr("value")?)?, - discovery: text(&native_field(entry, "discovery")?.getattr("value")?)?, + compatibility: text(&native_field( + &native_field(entry, "compatibility")?, + "value", + )?)?, + discovery: text(&native_field(&native_field(entry, "discovery")?, "value")?)?, completed_at: optional_text(&iso(&native_field(entry, "completed_at")?)?)?, target_label: text(&native_field(entry, "target_label")?)?, transfer: label(&native_field(entry, "transfer")?)?, @@ -85,7 +88,7 @@ fn column(entry: &Bound<'_, PyAny>) -> PyResult { origin_column: text(&native_field(entry, "origin_column")?)?, destination_column: text(&native_field(entry, "destination_column")?)?, decision: decision(entry)?, - discovery: text(&native_field(entry, "discovery")?.getattr("value")?)?, + discovery: text(&native_field(&native_field(entry, "discovery")?, "value")?)?, completed_at: optional_text(&iso(&native_field(entry, "completed_at")?)?)?, }) } @@ -161,7 +164,7 @@ pub(crate) fn pre_build_lines( let request = native_field(&entry, "request")?; let mut parts: Vec = Vec::new(); for field in ["database", "schema", "name"] { - let part = request.getattr(field)?; + let part = native_field(&request, field)?; if part.is_truthy()? { parts.push(text(&part)?); } @@ -172,8 +175,8 @@ pub(crate) fn pre_build_lines( actual_days: days(&native_field(&entry, "actual_days")?)?, effective_days: days(&native_field(&entry, "effective_days")?)?, source: text(&native_field(&entry, "source")?)?, - direction: text(&native_field(&entry, "direction")?.getattr("value")?)?, - phase: text(&native_field(&entry, "phase")?.getattr("value")?)?, + direction: text(&native_field(&native_field(&entry, "direction")?, "value")?)?, + phase: text(&native_field(&native_field(&entry, "phase")?, "value")?)?, irreversible_warning: optional_text(&native_field(&entry, "irreversible_warning")?)?, }); } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/plan_output/plan_json.rs b/crates/sqlbuild-python/src/bindings/_helpers/plan_output/plan_json.rs index f9144957f4..64a3add7ec 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/plan_output/plan_json.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/plan_output/plan_json.rs @@ -102,7 +102,7 @@ fn identity_status(entry: &Bound<'_, PyAny>) -> PyResult<&'static str> { fn evidence<'py>(entry: &Bound<'py, PyAny>, field: &str) -> PyResult> { let microbatch_range: Bound<'py, PyAny> = native_field(entry, "microbatch_range")?; if !microbatch_range.is_none() { - let found: Bound<'py, PyAny> = microbatch_range.getattr(field)?; + let found: Bound<'py, PyAny> = native_field(µbatch_range, field)?; if !found.is_none() { return Ok(found); } @@ -111,7 +111,7 @@ fn evidence<'py>(entry: &Bound<'py, PyAny>, field: &str) -> PyResult(entry: &Bound<'py, PyAny>) -> PyResult> { @@ -176,7 +176,7 @@ fn model_json<'py>(entry: &Bound<'py, PyAny>) -> PyResult> { ], )?; for key in ["incremental_strategy", "incremental_mode"] { - let value: Bound<'py, PyAny> = entry.getattr(key)?; + let value: Bound<'py, PyAny> = native_field(entry, key)?; if !value.is_none() { model.set_item(key, value)?; } @@ -185,7 +185,7 @@ fn model_json<'py>(entry: &Bound<'py, PyAny>) -> PyResult> { model.set_item("microbatch_state", microbatch_state(entry)?)?; } for key in ["cursor_column", "cursor_type"] { - let value: Bound<'py, PyAny> = entry.getattr(key)?; + let value: Bound<'py, PyAny> = native_field(entry, key)?; if !value.is_none() { model.set_item(key, value)?; } @@ -227,7 +227,7 @@ fn model_json<'py>(entry: &Bound<'py, PyAny>) -> PyResult> { model.set_item("changed_functions", list(&changed_functions)?)?; } let qualified: Bound<'py, PyAny> = - native_field(entry, "destination")?.getattr("qualified_name")?; + native_field(&native_field(entry, "destination")?, "qualified_name")?; if !qualified.is_none() { model.set_item("qualified_name", qualified)?; } @@ -260,7 +260,7 @@ fn with_qualified_name<'py>( entry: &Bound<'py, PyAny>, ) -> PyResult> { let qualified: Bound<'py, PyAny> = - native_field(entry, "destination")?.getattr("qualified_name")?; + native_field(&native_field(entry, "destination")?, "qualified_name")?; if !qualified.is_none() { item.set_item("qualified_name", qualified)?; } @@ -362,7 +362,7 @@ fn warning_json<'py>(warning: &Bound<'py, PyAny>) -> PyResult> ], )?; for key in ["model_name", "code"] { - let value: Bound<'py, PyAny> = warning.getattr(key)?; + let value: Bound<'py, PyAny> = native_field(warning, key)?; if !value.is_none() { item.set_item(key, value)?; } @@ -706,7 +706,10 @@ fn table_type_json<'py>(plan: &Bound<'py, PyAny>) -> PyResult> /// The plan as one JSON document, `json.dumps(indent=2)`. #[pyfunction] #[pyo3(signature = (**options))] -fn format_plan_json(py: Python<'_>, options: Option>) -> PyResult { +pub(crate) fn format_plan_json( + py: Python<'_>, + options: Option>, +) -> PyResult { let arguments: KeywordArguments<'_> = KeywordArguments::read( py, "format_plan_json", @@ -719,7 +722,7 @@ fn format_plan_json(py: Python<'_>, options: Option>) -> PyRes } else { Vec::new() }; - let length = |field: &str| -> PyResult { plan.getattr(field)?.len() }; + let length = |field: &str| -> PyResult { native_field(&plan, field)?.len() }; let python_nodes: Vec> = python_entries .iter() .map(python_entry_json) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/plan_output/plan_text.rs b/crates/sqlbuild-python/src/bindings/_helpers/plan_output/plan_text.rs index a18c46709f..282a8e1553 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/plan_output/plan_text.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/plan_output/plan_text.rs @@ -27,9 +27,9 @@ use crate::bindings::_helpers::plan_output::python_identity::{ definition_source_text, dependency_source_text, }; use crate::bindings::_helpers::plan_output::values::{items, optional_text, text, texts}; +use crate::bindings::_helpers::records::construct::new_record; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; const SKIP_ACTION: &str = "skip"; const QUERY_CHANGED: &str = "query_changed"; @@ -169,8 +169,8 @@ fn config_diff(entry: &Bound<'_, PyAny>, reason: &str) -> PyResult) -> PyResult { - let action: String = text(&native_field(entry, "action")?.getattr("value")?)?; - let reason: String = text(&native_field(entry, "reason")?.getattr("value")?)?; + let action: String = text(&native_field(&native_field(entry, "action")?, "value")?)?; + let reason: String = text(&native_field(&native_field(entry, "reason")?, "value")?)?; let name: String = text(&native_field(entry, "name")?)?; if action == SKIP_ACTION { return Ok(ModelPlanText { @@ -184,7 +184,7 @@ fn model_text(entry: &Bound<'_, PyAny>) -> PyResult { let mut schema_findings: Vec = Vec::new(); for finding in items(&native_field(entry, "schema_findings")?)? { schema_findings.push(SchemaFindingText { - kind: text(&native_field(&finding, "kind")?.getattr("value")?)?, + kind: text(&native_field(&native_field(&finding, "kind")?, "value")?)?, column_name: text(&native_field(&finding, "column_name")?)?, expected_type: optional_text(&native_field(&finding, "expected_type")?)?, actual_type: optional_text(&native_field(&finding, "actual_type")?)?, @@ -214,7 +214,7 @@ fn model_text(entry: &Bound<'_, PyAny>) -> PyResult { entry, "custom_materialization_name", )?)?, - backfill_action: text(&native_field(&backfill, "action")?.getattr("value")?)?, + backfill_action: text(&native_field(&native_field(&backfill, "action")?, "value")?)?, backfill_duration: optional_text(&native_field(&backfill, "duration")?)?, cursor: cursor_details(entry)? .map(|details| cursor_text(entry, details)) @@ -231,7 +231,7 @@ fn model_text(entry: &Bound<'_, PyAny>) -> PyResult { } fn function_text(entry: &Bound<'_, PyAny>) -> PyResult { - let reason: String = text(&native_field(entry, "reason")?.getattr("value")?)?; + let reason: String = text(&native_field(&native_field(entry, "reason")?, "value")?)?; let previous: Bound<'_, PyAny> = native_field(entry, "previous_query_sql")?; let query_diff_lines: Option> = if reason == QUERY_CHANGED && !previous.is_none() { Some(query_diff( @@ -248,7 +248,7 @@ fn function_text(entry: &Bound<'_, PyAny>) -> PyResult { } else { format!( "{} udf", - text(&native_field(entry, "language")?.getattr("value")?)? + text(&native_field(&native_field(entry, "language")?, "value")?)? ) }, reason, @@ -260,7 +260,10 @@ fn source_load_text(entry: &Bound<'_, PyAny>) -> PyResult { let strategy: Bound<'_, PyAny> = native_field(entry, "write_strategy")?; Ok(SourceLoadPlanText { name: text(&native_field(entry, "name")?)?, - resource_kind: text(&native_field(entry, "resource_kind")?.getattr("value")?)?, + resource_kind: text(&native_field( + &native_field(entry, "resource_kind")?, + "value", + )?)?, is_reload: native_field(entry, "is_reload")?.is_truthy()?, integration_kind: optional_text(&native_field(entry, "integration_kind")?)?, write_strategy: if strategy.is_none() { @@ -275,7 +278,10 @@ fn source_load_text(entry: &Bound<'_, PyAny>) -> PyResult { /// Both identity diffs of a changed Python entry, each as `format_query_diff` lines. fn python_text(entry: &Bound<'_, PyAny>) -> PyResult { - let identity_status: String = text(&native_field(entry, "identity_status")?.getattr("value")?)?; + let identity_status: String = text(&native_field( + &native_field(entry, "identity_status")?, + "value", + )?)?; let mut diffs: [Vec; 2] = [Vec::new(), Vec::new()]; if identity_status == CHANGED_IDENTITY { let sources = [ @@ -303,8 +309,8 @@ fn python_text(entry: &Bound<'_, PyAny>) -> PyResult { let [source_diff, dependency_diff] = diffs; Ok(PythonPlanText { name: text(&native_field(entry, "name")?)?, - kind: text(&native_field(entry, "kind")?.getattr("value")?)?, - phase: text(&native_field(entry, "phase")?.getattr("value")?)?, + kind: text(&native_field(&native_field(entry, "kind")?, "value")?)?, + phase: text(&native_field(&native_field(entry, "phase")?, "value")?)?, identity_status, source_diff, dependency_diff, @@ -327,7 +333,7 @@ fn provider_usages( } for entry in python_entries { let (kind, name) = ( - text(&native_field(entry, "kind")?.getattr("value")?)?, + text(&native_field(&native_field(entry, "kind")?, "value")?)?, text(&native_field(entry, "name")?)?, ); for usage in items(&native_field(entry, "provider_usages")?)? { @@ -416,7 +422,7 @@ fn report(arguments: &KeywordArguments<'_>) -> PyResult { let display_options: Bound<'_, PyAny> = { let given: Bound<'_, PyAny> = arguments.value("display_options")?; if given.is_none() { - registry::class(py, RecordId::DisplayOptions)?.call0()? + new_record(py, RecordId::DisplayOptions, (), None)? } else { given } @@ -437,7 +443,10 @@ fn report(arguments: &KeywordArguments<'_>) -> PyResult { let mut warnings: Vec = Vec::new(); for warning in items(&native_field(&plan, "warnings")?)? { warnings.push(PlanWarningText { - severity: text(&native_field(&warning, "severity")?.getattr("value")?)?, + severity: text(&native_field( + &native_field(&warning, "severity")?, + "value", + )?)?, message: text(&native_field(&warning, "message")?)?, model_name: optional_text(&native_field(&warning, "model_name")?)?, }); @@ -488,7 +497,7 @@ fn report(arguments: &KeywordArguments<'_>) -> PyResult { /// The text plan grouped by reason with inline detail, optionally closed by its completion line. #[pyfunction] #[pyo3(signature = (**options))] -fn format_plan(py: Python<'_>, options: Option>) -> PyResult { +pub(crate) fn format_plan(py: Python<'_>, options: Option>) -> PyResult { let arguments: KeywordArguments<'_> = KeywordArguments::read(py, "format_plan", options, PLAN_OPTIONS)?; let report: PlanTextReport = report(&arguments)?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/plan_output/safety.rs b/crates/sqlbuild-python/src/bindings/_helpers/plan_output/safety.rs index 733d559750..4a89d40637 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/plan_output/safety.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/plan_output/safety.rs @@ -28,8 +28,8 @@ fn bounds<'py>( Ok(dict( evidence.py(), vec![ - ("start", render(evidence.getattr(start)?)?), - ("end", render(evidence.getattr(end)?)?), + ("start", render(native_field(evidence, start)?)?), + ("end", render(native_field(evidence, end)?)?), ], )? .into_any()) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/plan_output/values.rs b/crates/sqlbuild-python/src/bindings/_helpers/plan_output/values.rs index 2c3b6725f5..67981b9d20 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/plan_output/values.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/plan_output/values.rs @@ -33,7 +33,7 @@ pub(crate) fn enum_value<'py>( value: &Bound<'py, PyAny>, name: &str, ) -> PyResult> { - native_field(&value.getattr(name)?, "value") + native_field(&native_field(value, name)?, "value") } /// `value.value` when `value` is set, else `None`. diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_changes/actions.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_changes/actions.rs index 5435b9a3fa..748e555e11 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_changes/actions.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_changes/actions.rs @@ -4,10 +4,9 @@ use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyRes use pyo3::types::{PyDict, PyDictMethods}; use pyo3::{pyfunction, wrap_pyfunction}; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; +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::ids::RecordId; -use crate::bindings::_helpers::records::registry; const INCREMENTAL_MATERIALIZATION: &str = "incremental"; const CREATE_TABLE_ACTION: &str = "create_table"; @@ -20,9 +19,7 @@ pub(crate) fn resolve_model_actions<'py>( changes: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = scope.py(); - let model_kind: Bound<'py, PyAny> = - registry::class(py, RecordId::CompiledResourceType)?.getattr("MODEL")?; - let action_class: Bound<'py, PyAny> = registry::class(py, RecordId::ResolvedModelAction)?; + let model_kind: Bound<'py, PyAny> = enum_member(py, RecordId::CompiledResourceType, "MODEL")?; let selected: Bound<'py, PyAny> = native_field(scope, "selected_keys")?; let models_by_name: Bound<'py, PyAny> = native_field(scope, "models_by_name")?; let model_changes: Bound<'py, PyAny> = native_field(changes, "models")?; @@ -40,11 +37,16 @@ pub(crate) fn resolve_model_actions<'py>( let backfill: Bound<'py, PyAny> = native_field(&change, "backfill")?; resolved.set_item( name, - call_with(&action_class, &[("change", change), ("backfill", backfill)])?, + new_record_with( + py, + RecordId::ResolvedModelAction, + &[("change", change), ("backfill", backfill)], + )?, )?; } - call_with( - ®istry::class(py, RecordId::PlannerResolvedActions)?, + new_record_with( + py, + RecordId::PlannerResolvedActions, &[("models", resolved.into_any())], ) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_changes/detect.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_changes/detect.rs index 60bf12cb77..8cccda9fca 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_changes/detect.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_changes/detect.rs @@ -4,6 +4,7 @@ use crate::bindings::_helpers::analysis::model_reads; use crate::bindings::_helpers::contracts::spec_resolution::matching_dynamic_families; use crate::bindings::_helpers::model_config::config_values::config_str as authored_config_str; use crate::bindings::_helpers::planner_changes::results::native_enum; +use crate::bindings::_helpers::records::construct::{enum_member, new_record_with}; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; use pyo3::types::{PyDict, PyDictMethods, PyString, PyTuple}; use pyo3::{IntoPyObject, pyfunction, wrap_pyfunction}; @@ -45,7 +46,6 @@ use crate::bindings::_helpers::planner_identity::metadata::{ }; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::type_system::equality::types_equal as native_types_equal; const CHANGES_LOGGER: &str = "sqlbuild.planner.changes"; @@ -100,14 +100,11 @@ pub(crate) fn detect_changes<'py>( let models: Bound<'py, PyDict> = scope_model_changes((project, scope, snapshot), None, expected)?; let functions: Bound<'py, PyDict> = PyDict::new(py); - let result_class: Bound<'py, PyAny> = registry::class(py, RecordId::FunctionChangeResult)?; let selected: Bound<'py, PyAny> = native_field(scope, "selected_keys")?; let recorded: Bound<'py, PyAny> = - native_field(snapshot, "fingerprints")?.getattr("functions")?; - let query_change_tracking: bool = project - .getattr("settings")? - .getattr("query_change_tracking")? - .is_truthy()?; + native_field(&native_field(snapshot, "fingerprints")?, "functions")?; + let query_change_tracking: bool = + native_field(&native_field(project, "settings")?, "query_change_tracking")?.is_truthy()?; let dialect: Option = native_field(snapshot, "column_dialect")?.extract()?; for function in native_field(project, "functions")?.try_iter()? { let function: Bound<'py, PyAny> = function?; @@ -135,9 +132,7 @@ pub(crate) fn detect_changes<'py>( .extract::()?, dialect.as_deref(), ), - )? != fingerprint - .getattr("definition_hash")? - .extract::()? + )? != native_field(&fingerprint, "definition_hash")?.extract::()? { "query_changed" } else { @@ -146,10 +141,14 @@ pub(crate) fn detect_changes<'py>( }; arguments.push(("reason", planner_enum(py, RecordId::PlanReason, reason)?)); } - functions.set_item(name, call_with(&result_class, &arguments)?)?; + functions.set_item( + name, + new_record_with(py, RecordId::FunctionChangeResult, &arguments)?, + )?; } - call_with( - ®istry::class(py, RecordId::PlannerChangeResults)?, + new_record_with( + py, + RecordId::PlannerChangeResults, &[ ("models", models.into_any()), ("functions", functions.into_any()), @@ -243,15 +242,13 @@ pub(crate) fn scope_model_changes<'py>( let py: Python<'py> = project.py(); let tracking: bool = match query_change_tracking { Some(value) => value, - None => project - .getattr("settings")? - .getattr("query_change_tracking")? - .extract()?, + None => { + native_field(&native_field(project, "settings")?, "query_change_tracking")?.extract()? + } }; let function_local_hashes: Bound<'py, PyAny> = function_local_hashes( &native_field(project, "functions")?, - project - .getattr("sql_analysis_dialect")? + native_field(project, "sql_analysis_dialect")? .extract::>()? .as_deref(), )? @@ -273,8 +270,7 @@ pub(crate) fn scope_model_changes<'py>( function_local_hashes, hook_version_hashes: hook_version_hashes.into_any(), }; - let model_kind: Bound<'py, PyAny> = - registry::class(py, RecordId::CompiledResourceType)?.getattr("MODEL")?; + let model_kind: Bound<'py, PyAny> = enum_member(py, RecordId::CompiledResourceType, "MODEL")?; let selected: Bound<'py, PyAny> = native_field(scope, "selected_keys")?; let models_by_name: Bound<'py, PyAny> = native_field(scope, "models_by_name")?; let changes: Bound<'py, PyDict> = PyDict::new(py); @@ -338,13 +334,11 @@ pub(crate) fn model_change<'py>( "no_change", ); } - let relation_exists: bool = snapshot - .getattr("existing_relations")? - .contains(&model_name)?; - let fingerprint: Bound<'py, PyAny> = snapshot - .getattr("fingerprints")? - .getattr("models")? - .call_method1("get", (&model_name,))?; + let relation_exists: bool = + native_field(snapshot, "existing_relations")?.contains(&model_name)?; + let fingerprint: Bound<'py, PyAny> = + native_field(&native_field(snapshot, "fingerprints")?, "models")? + .call_method1("get", (&model_name,))?; let recorded: Option = recorded_fingerprint(&fingerprint)?; let dialect: Bound<'py, PyAny> = native_field(snapshot, "column_dialect")?; let compiled_query_hash: Option = if pass.query_change_tracking && recorded.is_some() { @@ -444,7 +438,7 @@ pub(crate) fn model_change<'py>( fn replay_policy(model: &Bound<'_, PyAny>) -> PyResult { let py: Python<'_> = model.py(); let raw: Option = authored_config_str( - &native_field(model, "config")?.getattr("values")?, + &native_field(&native_field(model, "config")?, "values")?, REPLAY_CONFIG_KEY, )? .map(|value| value.extract()) @@ -531,9 +525,8 @@ fn model_schema_findings<'py>( model_name: &Bound<'py, PyAny>, dialect: &Bound<'py, PyAny>, ) -> PyResult> { - let warehouse: Bound<'py, PyAny> = snapshot - .getattr("existing_columns")? - .call_method1("get", (model_name,))?; + let warehouse: Bound<'py, PyAny> = + native_field(snapshot, "existing_columns")?.call_method1("get", (model_name,))?; if warehouse.is_none() { return Ok(Vec::new()); } @@ -548,8 +541,7 @@ fn model_schema_findings<'py>( yml.push((native_field(&column, "name")?.extract()?, kind)); } } - type_enforcement = schema_entry - .getattr("type_enforcement")? + type_enforcement = native_field(&schema_entry, "type_enforcement")? .extract::>()? .unwrap_or(false); let families: Bound<'py, PyAny> = native_field(&schema_entry, "dynamic_columns")?; @@ -746,8 +738,9 @@ impl<'py> ChangeResult<'py> { .into_pyobject(py) .map(|flag| flag.to_owned().into_any()) }; - call_with( - ®istry::class(py, RecordId::ChangeDetectionResult)?, + new_record_with( + py, + RecordId::ChangeDetectionResult, &[ ("model_name", self.model_name), ("change_kind", kind), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_changes/renames.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_changes/renames.rs index 2f7aa8f553..17d88ea590 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_changes/renames.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_changes/renames.rs @@ -1,6 +1,7 @@ //! Model renames that downstream `__ref` names may have been rewritten for. use crate::bindings::_helpers::fingerprints::query_hashes::compute_query_hash; +use crate::bindings::_helpers::records::construct::{enum_member, new_record_with}; use std::collections::BTreeSet; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; @@ -19,7 +20,6 @@ use crate::bindings::_helpers::compile_driver::python_calls::{ use crate::bindings::_helpers::planner_identity::hashing::model_query_hash; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; const GENERIC_DIALECT: &str = "generic"; const FINAL_NEWLINE: &str = "\n"; @@ -179,17 +179,16 @@ pub(crate) fn plan_reference_renames<'py>( &native_field(&state, "inspect_migration_events")?, &[("schemas", inspected.into_any())], )?; - let target_name: Bound<'py, PyAny> = runtime - .getattr("project")? - .getattr("effective_target_name")?; - let rename_class: Bound<'py, PyAny> = registry::class(py, RecordId::ReferenceRename)?; + let target_name: Bound<'py, PyAny> = + native_field(&native_field(runtime, "project")?, "effective_target_name")?; let mut renames: Vec> = Vec::new(); for item in planned.call_method0("items")?.try_iter()? { let item: Bound<'py, PyAny> = item?; let (new_name, origin_name) = (item.get_item(0)?, item.get_item(1)?); if new_name.ne(&origin_name)? { - renames.push(call_with( - &rename_class, + renames.push(new_record_with( + py, + RecordId::ReferenceRename, &[("new_name", new_name), ("origin_name", origin_name)], )?); } @@ -203,8 +202,9 @@ pub(crate) fn plan_reference_renames<'py>( && !origin.is_none() && origin.ne(&destination)? { - renames.push(call_with( - &rename_class, + renames.push(new_record_with( + py, + RecordId::ReferenceRename, &[ ("new_name", destination), ("origin_name", origin), @@ -231,9 +231,8 @@ fn changed_referencing_query( } } let recorded_models: Bound<'_, PyAny> = - native_field(snapshot, "fingerprints")?.getattr("models")?; - let ref_kind: Bound<'_, PyAny> = - registry::class(py, RecordId::SqlReferenceKind)?.getattr("REF")?; + native_field(&native_field(snapshot, "fingerprints")?, "models")?; + let ref_kind: Bound<'_, PyAny> = enum_member(py, RecordId::SqlReferenceKind, "REF")?; let dialect: Option = native_field(snapshot, "column_dialect")?.extract()?; for model in selected { let recorded: Bound<'_, PyAny> = diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_changes/results.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_changes/results.rs index bd2d0137ba..576dead32b 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_changes/results.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_changes/results.rs @@ -9,7 +9,7 @@ use sqlbuild_analysis::planner_changes::models::{ Backfill, BackfillAction, ChangeKind, SchemaChangeKind, SchemaColumnSource, SchemaFindingRow, }; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; +use crate::bindings::_helpers::records::construct::{new_record, new_record_with}; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -29,7 +29,7 @@ pub(crate) fn planner_enum<'py>( name: RecordId, value: &str, ) -> PyResult> { - registry::class(py, name)?.call1((value,)) + new_record(py, name, (value,), None) } /// A member of a natively declared planner enumeration, from its value. @@ -38,7 +38,7 @@ pub(crate) fn native_enum<'py>( name: RecordId, value: &str, ) -> PyResult> { - registry::class(py, name)?.call1((value,)) + new_record(py, name, (value,), None) } /// `BackfillResult(action=..., duration=...)`. @@ -51,8 +51,9 @@ pub(crate) fn backfill_result<'py>( BackfillAction::Bounded => "bounded", BackfillAction::ForwardOnly => "forward", }; - call_with( - ®istry::class(py, RecordId::BackfillResult)?, + new_record_with( + py, + RecordId::BackfillResult, &[ ( "action", @@ -83,7 +84,6 @@ pub(crate) fn schema_findings<'py>( py: Python<'py>, findings: &[SchemaFindingRow], ) -> PyResult> { - let class: Bound<'py, PyAny> = registry::class(py, RecordId::SchemaFinding)?; let mut items: Vec> = Vec::with_capacity(findings.len()); for finding in findings { let kind: &str = match finding.kind { @@ -95,8 +95,9 @@ pub(crate) fn schema_findings<'py>( SchemaColumnSource::Yml => "yml", SchemaColumnSource::SqlAnalysis => "sql_analysis", }; - items.push(call_with( - &class, + items.push(new_record_with( + py, + RecordId::SchemaFinding, &[ ("kind", native_enum(py, RecordId::SchemaChangeKind, kind)?), ( 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 fa549ebded..c956ad2184 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 @@ -23,9 +23,9 @@ use crate::bindings::_helpers::planner_resolution::orchestration::{Resolution, r use crate::bindings::_helpers::planner_selection::selectors::resolve_graph_selection; use crate::bindings::_helpers::planner_strategy::strategy::materialization_of; use crate::bindings::_helpers::planner_warehouse::source_columns::gather_source_columns; +use crate::bindings::_helpers::records::construct::new_record; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use pyo3::PyRef; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; use pyo3::types::{PyBool, PyDict, PyDictMethods, PyFrozenSet, PySet, PySetMethods, PyTuple}; @@ -36,9 +36,7 @@ const GRAPH_ARGUMENTS: [&str; 4] = ["selected", "upstream", "is_clonable", "is_v /// Whether a model is explicitly disabled with a boolean `enabled = false`. fn disabled_model(model: &Bound<'_, PyAny>) -> PyResult { - let raw = model - .getattr("config")? - .getattr("values")? + let raw = native_field(&native_field(model, "config")?, "values")? .call_method1("get", ("enabled",))?; Ok(raw.is_instance_of::() && !raw.is_truthy()?) } @@ -72,9 +70,7 @@ fn clone_plan_output<'py>( let selected = object( py, resolve_graph_selection( - project - .getattr("lineage_graph")? - .extract::>()?, + native_field(project, "lineage_graph")?.extract::>()?, select, exclude, )?, @@ -272,7 +268,9 @@ fn clone_source_entries<'py>( let qualified = adapter .getattr("render_qualified_name")? .call((), Some(&kwargs))?; - let destination = registry::class(py, RecordId::CompiledRelationLocation)?.call( + let destination = new_record( + py, + RecordId::CompiledRelationLocation, (), Some(&{ let location = PyDict::new(py); @@ -335,7 +333,7 @@ fn clone_function_entries<'py>( ], )?), )?; - let field = |name: &str| function.getattr(name); + let field = |name: &str| native_field(&function, name); let entry = planner_model( py, RecordId::FunctionPlanEntry, @@ -376,12 +374,12 @@ fn seed_entry<'py>(seed: &Bound<'py, PyAny>) -> PyResult> { ("destination", native_field(seed, "destination")?), ( "file_path", - native_field(seed, "seed_file")?.getattr("file_path")?, + native_field(&native_field(seed, "seed_file")?, "file_path")?, ), ("columns", PyTuple::empty(py).into_any()), ( "csv_settings", - native_field(seed, "schema_entry")?.getattr("csv_settings")?, + native_field(&native_field(seed, "schema_entry")?, "csv_settings")?, ), ], ) @@ -396,7 +394,7 @@ fn seed_entries<'py>( let mut entries: Vec> = Vec::new(); for seed in native_field(project, "seeds")?.try_iter()? { let seed = seed?; - if wanted.contains(seed.getattr(field)?)? { + if wanted.contains(native_field(&seed, field)?)? { entries.push(seed_entry(&seed)?); } } @@ -456,7 +454,7 @@ fn origin_source_entries<'py>( /// Prepare planner outputs needed for clone execution. #[pyfunction] #[pyo3(signature = (**kwargs))] -fn run_clone_planning<'py>( +pub(crate) fn run_clone_planning<'py>( py: Python<'py>, kwargs: Option>, ) -> PyResult> { @@ -495,8 +493,7 @@ fn run_clone_planning<'py>( &destination_sources, )?; let mut sources = PyTuple::empty(py).into_any(); - if destination_sources - .getattr("effective_target_name")? + if native_field(&destination_sources, "effective_target_name")? .eq(native_field(&project, "effective_target_name")?)? { sources = clone_source_entries( @@ -528,10 +525,7 @@ fn run_clone_planning<'py>( let Some(origin) = origin_by_name.get_item(native_field(&entry, "name")?)? else { continue; }; - if origin - .getattr("destination")? - .ne(native_field(&entry, "destination")?)? - { + if native_field(&origin, "destination")?.ne(native_field(&entry, "destination")?)? { kept_origins.push(origin); kept_sources.push(entry); } @@ -627,7 +621,7 @@ impl<'py> CloneGraph<'py> { /// Return non-view clonable ancestors anchoring selected nodes. #[pyfunction] #[pyo3(signature = (**kwargs))] -fn resolve_clone_boundary<'py>( +pub(crate) fn resolve_clone_boundary<'py>( py: Python<'py>, kwargs: Option>, ) -> PyResult> { @@ -643,7 +637,7 @@ fn resolve_clone_boundary<'py>( /// Return skipped view ancestors rebuilt over cloned boundaries. #[pyfunction] #[pyo3(signature = (**kwargs))] -fn resolve_skipped_view_chain<'py>( +pub(crate) fn resolve_skipped_view_chain<'py>( py: Python<'py>, kwargs: Option>, ) -> PyResult> { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/command_plans.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/command_plans.rs index 52f30a7ad8..daf5f0b783 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/command_plans.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/command_plans.rs @@ -147,7 +147,7 @@ pub(crate) fn resolve_static_relation_context<'py>( /// Project selected audits from canonical static state. #[pyfunction] #[pyo3(signature = (*, project, adapter, scope, relations))] -fn build_audit_command_plan<'py>( +pub(crate) fn build_audit_command_plan<'py>( project: &Bound<'py, PyAny>, adapter: &Bound<'py, PyAny>, scope: &Bound<'py, PyAny>, @@ -192,7 +192,7 @@ fn build_audit_command_plan<'py>( /// Project selected SQL tests from canonical static state. #[pyfunction] #[pyo3(signature = (**kwargs))] -fn build_test_command_plan<'py>( +pub(crate) fn build_test_command_plan<'py>( py: Python<'py>, kwargs: Option>, ) -> PyResult> { @@ -283,7 +283,7 @@ fn build_test_command_plan<'py>( /// Project selected direct seed work and canonical identities. #[pyfunction] #[pyo3(signature = (*, project, scope, relations, fingerprints = None))] -fn build_seed_command_plan<'py>( +pub(crate) fn build_seed_command_plan<'py>( project: &Bound<'py, PyAny>, scope: &Bound<'py, PyAny>, relations: &Bound<'py, PyAny>, @@ -320,12 +320,12 @@ fn build_seed_command_plan<'py>( ("destination", native_field(&seed, "destination")?), ( "file_path", - native_field(&seed, "seed_file")?.getattr("file_path")?, + native_field(&native_field(&seed, "seed_file")?, "file_path")?, ), ("columns", object(py, extract_seed_columns(&seed)?)?), ( "csv_settings", - native_field(&seed, "schema_entry")?.getattr("csv_settings")?, + native_field(&native_field(&seed, "schema_entry")?, "csv_settings")?, ), ("fingerprint_definition", metadata_json.clone()), ("fingerprint_version_hash", version_hash), @@ -362,7 +362,7 @@ type Target = (Option, String); /// Read only fingerprint state required by selected direct seeds. #[pyfunction] #[pyo3(signature = (*, adapter, connection, seeds))] -fn read_selected_seed_fingerprints<'py>( +pub(crate) fn read_selected_seed_fingerprints<'py>( adapter: &Bound<'py, PyAny>, connection: &Bound<'py, PyAny>, seeds: &Bound<'py, PyAny>, @@ -436,8 +436,7 @@ fn read_selected_seed_fingerprints<'py>( )?))?; let items = PyList::new( py, - latest - .getattr("fingerprints")? + native_field(&latest, "fingerprints")? .call_method0("items")? .try_iter()? .collect::>>()?, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/contexts.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/contexts.rs index 44111a26d4..58595655e7 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/contexts.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/contexts.rs @@ -1,6 +1,6 @@ //! Relation, model-plan and entry-build contexts: built from keyword arguments and read by plan entry construction. -use pyo3::prelude::{Bound, Py, PyAny, PyAnyMethods, PyResult, Python}; +use pyo3::prelude::{Bound, Py, PyAny, PyResult, Python}; use pyo3::pymethods; use pyo3::types::{PyDict, PyFrozenSet}; @@ -8,8 +8,8 @@ use crate::bindings::_helpers::compile_driver::python_calls::KeywordArguments; use crate::bindings::_helpers::planner_entries::models::{ ModelPlanContext, PlanEntryBuildInputs, PlannerRelationsContext, }; +use crate::bindings::_helpers::records::construct::enum_member; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; /// The argument `name`, or `default()` when it was not passed. fn value_or<'py>( @@ -167,5 +167,5 @@ impl PlanEntryBuildInputs { /// The microbatch limit action used when a project configures none. pub(crate) fn default_microbatch_limit_action(py: Python<'_>) -> PyResult> { - registry::class(py, RecordId::MicrobatchLimitAction)?.getattr("ERROR") + enum_member(py, RecordId::MicrobatchLimitAction, "ERROR") } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/display_plan.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/display_plan.rs index a7cbf6ed29..2caf473c20 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/display_plan.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/display_plan.rs @@ -41,10 +41,11 @@ fn display_action( } fn config<'py>(model: &Bound<'py, PyAny>, key: &str) -> PyResult> { - Ok( - authored_config_str(&native_field(model, "config")?.getattr("values")?, key)? - .unwrap_or_else(|| model.py().None().into_bound(model.py())), - ) + Ok(authored_config_str( + &native_field(&native_field(model, "config")?, "values")?, + key, + )? + .unwrap_or_else(|| model.py().None().into_bound(model.py()))) } fn display_entry<'py>( @@ -126,7 +127,7 @@ fn display_entry<'py>( /// Build a display-only plan when warehouse planning cannot fully resolve. #[pyfunction] #[pyo3(signature = (*, project, selected_model_names, full_refresh))] -fn build_display_only_sqlbuild_plan<'py>( +pub(crate) fn build_display_only_sqlbuild_plan<'py>( project: &Bound<'py, PyAny>, selected_model_names: &Bound<'py, PyAny>, full_refresh: bool, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/execution_plan.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/execution_plan.rs index 940c305f91..61289d856c 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/execution_plan.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/execution_plan.rs @@ -31,9 +31,9 @@ use crate::bindings::_helpers::planner_phases::scopes::resolve_planner_scopes; use crate::bindings::_helpers::planner_phases::warehouse_state::gather_planner_warehouse_state; use crate::bindings::_helpers::planner_resolution::cursor_overrides::type_cursor_overrides; use crate::bindings::_helpers::planner_warehouse::source_freshness::build_planner_source_freshness_result; +use crate::bindings::_helpers::records::construct::enum_member; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; use pyo3::types::PyDict; use pyo3::{pyfunction, wrap_pyfunction}; @@ -53,7 +53,7 @@ fn scopes_and_cursor_overrides<'py>( )?; let selected = class_field(&scopes, |s: &PlannerScopeResolution| &s.selected_scope)?; let models_by_name = native_field(&selected, "models_by_name")?; - let model_type = registry::class(py, RecordId::CompiledResourceType)?.getattr("MODEL")?; + let model_type = enum_member(py, RecordId::CompiledResourceType, "MODEL")?; let mut models: Vec> = Vec::new(); for key in native_field(&selected, "selected_keys")?.try_iter()? { let key = key?; @@ -234,9 +234,7 @@ pub(crate) fn build_execution_plan<'py>( selected_keys: native_field(&inspection, "selected_keys")?.unbind(), sql_test_selection: native_field(&inspection, "sql_test_selection")?.unbind(), }, - args.value("policies")? - .getattr("plan_sql_tests")? - .is_truthy()?, + native_field(&args.value("policies")?, "plan_sql_tests")?.is_truthy()?, )?; let planned = (|| { let catalog = call_with( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/mod.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/mod.rs index a8dddfd37f..145ad2dc23 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/mod.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/mod.rs @@ -1,9 +1,9 @@ //! Plan entry and plan output assembly over native planner resolution. -mod clone_plans; +pub(crate) mod clone_plans; pub(crate) mod command_plans; pub(crate) mod contexts; -mod display_plan; +pub(crate) mod display_plan; pub(crate) mod execution_plan; pub(crate) mod model_entries; pub(crate) mod models; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/model_entries.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/model_entries.rs index a79330227f..2f32727daf 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/model_entries.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/model_entries.rs @@ -41,9 +41,9 @@ use crate::bindings::_helpers::planner_strategy::strategy::{ materialization_of, resolve_model_plan_action, }; use crate::bindings::_helpers::planner_strategy::values::text; +use crate::bindings::_helpers::records::construct::enum_member; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::semantic_shapes::source_bindings::source_binding_diagnostics; use pyo3::prelude::{Bound, Py, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; use pyo3::types::{PyDict, PyDictMethods}; @@ -224,7 +224,7 @@ fn check_binding_diagnostics<'py>( selected_keys: &Bound<'py, PyAny>, ) -> PyResult<()> { let py = project.py(); - let sql_test = registry::class(py, RecordId::CompiledResourceType)?.getattr("SQL_TEST")?; + let sql_test = enum_member(py, RecordId::CompiledResourceType, "SQL_TEST")?; let mut errors: Vec> = Vec::new(); for diagnostic in native_field(project, "diagnostics")?.try_iter()? { let diagnostic = diagnostic?; @@ -266,7 +266,7 @@ fn check_binding_diagnostics<'py>( let mut lines: Vec = Vec::new(); for diagnostic in &errors { let position = |field: &str| -> PyResult { - let value = diagnostic.getattr(field)?; + let value = native_field(diagnostic, field)?; Ok(if value.is_truthy()? { value.str()?.to_string() } else { @@ -363,7 +363,7 @@ fn check_microbatch_concurrency<'py>( ("model_name", none(py)), ( "severity", - registry::class(py, RecordId::WarningSeverity)?.getattr("WARNING")?, + enum_member(py, RecordId::WarningSeverity, "WARNING")?, ), ("code", string(py, MICROBATCH_COMPLEXITY_CODE)), ( @@ -454,7 +454,7 @@ pub(crate) fn plan_entries<'py>( )?; let sql_analysis_enabled = native_field(&settings, "sql_analysis")?.is_truthy()?; let skip = planner_type(py, RecordId::PlanAction, "SKIP")?; - let microbatch = registry::class(py, RecordId::IncrementalMode)?.getattr("MICROBATCH")?; + let microbatch = enum_member(py, RecordId::IncrementalMode, "MICROBATCH")?; let mut entries: Vec> = Vec::new(); let mut warnings: Vec> = Vec::new(); for key in native_field(&scope, "execution_order")?.try_iter()? { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/plan_output.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/plan_output.rs index 0789fc370c..ea11b26fed 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/plan_output.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/plan_output.rs @@ -14,9 +14,9 @@ use crate::bindings::_helpers::planner_scope::sources::build_source_load_entries use crate::bindings::_helpers::planner_strategy::audit_entries::build_selected_audit_entries; use crate::bindings::_helpers::planner_strategy::fixture_context::planning_context; use crate::bindings::_helpers::planner_strategy::model_entries::extract_seed_columns; +use crate::bindings::_helpers::records::construct::record_class; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::sql_test_glue::models::SqlTestAssembly; use crate::bindings::_helpers::sql_test_glue::plan_assembly::assemble_sql_test_plans; use crate::bindings::_helpers::sql_test_glue::plan_driver::resolve_sql_test_model_chains; @@ -76,23 +76,16 @@ fn seed_plan_reason<'py>( if !reason.is_none() { return Ok(reason); } - if !snapshot - .getattr("existing_relations")? - .contains(seed_name)? - { + if !native_field(snapshot, "existing_relations")?.contains(seed_name)? { return planner_type(py, RecordId::PlanReason, "FIRST_RUN"); } - let fingerprint = snapshot - .getattr("fingerprints")? - .getattr("seeds")? + let fingerprint = native_field(&native_field(snapshot, "fingerprints")?, "seeds")? .call_method1("get", (seed_name,))?; if fingerprint.is_none() { return planner_type(py, RecordId::PlanReason, "FIRST_RUN"); } if !expected_version_hash.is_none() - && fingerprint - .getattr("version_hash")? - .ne(expected_version_hash)? + && native_field(&fingerprint, "version_hash")?.ne(expected_version_hash)? { return planner_type(py, RecordId::PlanReason, "CONFIG_CHANGED"); } @@ -125,12 +118,12 @@ fn seed_entries<'py>( ("destination", native_field(&seed, "destination")?), ( "file_path", - native_field(&seed, "seed_file")?.getattr("file_path")?, + native_field(&native_field(&seed, "seed_file")?, "file_path")?, ), ("columns", object(py, extract_seed_columns(&seed)?)?), ( "csv_settings", - native_field(&seed, "schema_entry")?.getattr("csv_settings")?, + native_field(&native_field(&seed, "schema_entry")?, "csv_settings")?, ), ( "fingerprint_definition", @@ -196,10 +189,10 @@ fn hook_provider_usages<'py>( ) -> PyResult>> { let py = entry.py(); let mut usages: Vec> = Vec::new(); - let python_hook = registry::class(py, RecordId::PythonHookEntry)?; + let python_hook = record_class(py, RecordId::PythonHookEntry)?; let mut hooks: Vec> = Vec::new(); for field in ["pre_hooks", "post_hooks"] { - let hook_entry = entry.getattr(field)?; + let hook_entry = native_field(entry, field)?; if hook_entry.is_none() { continue; } @@ -374,7 +367,7 @@ pub(super) fn function_entry<'py>( ], )?), )?; - let field = |name: &str| function.getattr(name); + let field = |name: &str| native_field(function, name); planner_model( py, RecordId::FunctionPlanEntry, @@ -408,7 +401,7 @@ fn function_entries<'py>( ) -> PyResult>> { let py = project.py(); let functions = native_field(changes, "functions")?; - let previous = native_field(snapshot, "fingerprints")?.getattr("functions")?; + let previous = native_field(&native_field(snapshot, "fingerprints")?, "functions")?; let mut entries: Vec> = Vec::new(); for function in native_field(project, "functions")?.try_iter()? { let function = function?; @@ -485,7 +478,7 @@ pub(super) fn selected_test_entries<'py>( if selected.is_empty() { return Ok(SelectedTests { entries, warnings }); } - let analysis = native_field(project, "settings")?.getattr("sql_analysis")?; + let analysis = native_field(&native_field(project, "settings")?, "sql_analysis")?; let fixture_context = if analysis.is_truthy()? { Bound::new(py, planning_context(project, None)?)?.into_any() } else { @@ -694,7 +687,7 @@ pub(crate) fn plan_output_fields<'py>( ), ( "python_identity_fingerprints", - native_field(&snapshot, "fingerprints")?.getattr("python_nodes")?, + native_field(&native_field(&snapshot, "fingerprints")?, "python_nodes")?, ), ]) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/test_planning.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/test_planning.rs index ed2e2d34d0..26487ca4f9 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/test_planning.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/test_planning.rs @@ -14,13 +14,11 @@ use pyo3::types::PyDict; use pyo3::{pyclass, pyfunction, pymethods, wrap_pyfunction}; use crate::bindings::_helpers::boundary::panics::compiler_guard; -use crate::bindings::_helpers::compile_driver::python_calls::{ - KeywordArguments, call_with, imported, -}; +use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, imported}; use crate::bindings::_helpers::native_threads::exit_signal::signal_interpreter_exit; use crate::bindings::_helpers::planner_entries::plan_output::plan_selected_sql_tests; +use crate::bindings::_helpers::records::construct::new_record_with; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; const TEST_PLANNING_THREAD: &str = "sqlbuild-test-planning"; /// How long interpreter exit waits for SQL test planning threads still running native code. @@ -180,8 +178,9 @@ pub(crate) fn start_planning( enabled: bool, ) -> PyResult> { if !enabled { - let empty = call_with( - ®istry::class(py, RecordId::PlannedSqlTests)?, + let empty = new_record_with( + py, + RecordId::PlannedSqlTests, &[ ("selected_keys", request.selected_keys.bind(py).clone()), ( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/values.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/values.rs index 2e0bdc8483..f8b62f1a38 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/values.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/values.rs @@ -1,7 +1,9 @@ //! Python boundary values shared by plan entry and plan output assembly. -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported, keywords}; -use crate::bindings::_helpers::records::ids::{ErrorId, NativeItem, RecordId}; +use crate::bindings::_helpers::compile_driver::python_calls::keywords; +use crate::bindings::_helpers::records::construct::{enum_member, new_record_with}; +use crate::bindings::_helpers::records::fields::replaced_keywords; +use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; use pyo3::IntoPyObject; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyErr, PyResult, Python}; @@ -10,13 +12,13 @@ use pyo3::types::PyString; pub(super) const MICROBATCH_CONCURRENCY_MESSAGE: &str = "batch_concurrency > 1 requires concurrent microbatches; selected model(s): "; -/// `sqlbuild._native.name(**pairs)`. +/// The native record `name(**pairs)`. pub(super) fn native<'py>( py: Python<'py>, - name: impl Into, + name: RecordId, pairs: &[(&str, Bound<'py, PyAny>)], ) -> PyResult> { - call_with(®istry::item(py, name)?, pairs) + new_record_with(py, name, pairs) } /// The planner record `name(**pairs)`. @@ -25,7 +27,7 @@ pub(super) fn planner_model<'py>( name: RecordId, pairs: &[(&str, Bound<'py, PyAny>)], ) -> PyResult> { - call_with(®istry::class(py, name)?, pairs) + new_record_with(py, name, pairs) } /// One member of a planner enumeration. @@ -34,7 +36,7 @@ pub(super) fn planner_type<'py>( class: RecordId, member: &str, ) -> PyResult> { - registry::member(py, class, member) + enum_member(py, class, member) } /// A Python string. @@ -57,8 +59,7 @@ pub(super) fn replaced<'py>( object: &Bound<'py, PyAny>, fields: &[(&str, Bound<'py, PyAny>)], ) -> PyResult> { - imported(object.py(), "dataclasses", "replace")? - .call((object,), Some(&keywords(object.py(), fields)?)) + replaced_keywords(object, &keywords(object.py(), fields)?) } /// `class(message, **fields)` from `module`, as a raised error. diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_identity/graph.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_identity/graph.rs index 2343090563..4d2c2da06c 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_identity/graph.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_identity/graph.rs @@ -22,10 +22,11 @@ use sqlbuild_analysis::planner_identity::models::{AvailableUpstreams, GraphKey, use crate::bindings::_helpers::analysis::explicit_references::{ GateRow, HookRow, native_execution_gate_edges, }; -use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; +use crate::bindings::_helpers::compile_driver::python_calls::KeywordArguments; use crate::bindings::_helpers::graph::algorithms::{closure, inverted}; use crate::bindings::_helpers::graph::keys::KeyCache; use crate::bindings::_helpers::planner_identity::hashing::graph_key; +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::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -87,14 +88,13 @@ pub(crate) fn build_execution_upstream_deps<'py>( let py: Python<'py> = project.py(); let upstream: Bound<'py, PyDict> = PyDict::new(py); for collection in ["models", "sources", "seeds", "functions"] { - for resource in project.getattr(collection)?.try_iter()? { + for resource in native_field(project, collection)?.try_iter()? { let resource: Bound<'py, PyAny> = resource?; upstream.set_item( native_field(&resource, "key")?, PyList::new( py, - resource - .getattr("deps")? + native_field(&resource, "deps")? .try_iter()? .collect::>>()?, )?, @@ -373,11 +373,11 @@ pub(crate) fn check_buildability<'py>( Some(name_set(&deferred_relations)?) }, }; - let entry_class: Bound<'py, PyAny> = registry::class(py, RecordId::MissingUpstream)?; let mut entries: Vec> = Vec::new(); for missing in missing_upstreams(&selected, &deps, &available) { - entries.push(call_with( - &entry_class, + entries.push(new_record_with( + py, + RecordId::MissingUpstream, &[ ("key", objects.object(&missing.key)), ( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_identity/identities.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_identity/identities.rs index 2bfa79d714..3829a4699b 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_identity/identities.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_identity/identities.rs @@ -15,20 +15,17 @@ use sqlbuild_analysis::planner_identity::main::model_local_identity_hash::model_ use sqlbuild_analysis::planner_identity::models::{GraphKey, IdentityGraph, IdentityNode}; use crate::bindings::_helpers::analysis::model_reads; -use crate::bindings::_helpers::compile_driver::python_calls::{ - KeywordArguments, call_with, imported, -}; +use crate::bindings::_helpers::compile_driver::python_calls::KeywordArguments; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::native_host::user_code::UserCodeHost; use crate::bindings::_helpers::planner_identity::hashing::{graph_key, model_query_hash}; use crate::bindings::_helpers::planner_identity::metadata::{ function_local_hashes, model_version_identity_metadata_json, seed_identity, }; +use crate::bindings::_helpers::records::construct::new_record_with; use crate::bindings::_helpers::records::fields::field as native_field; +use crate::bindings::_helpers::records::fields::replaced; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; - -const DATACLASSES_MODULE: &str = "dataclasses"; fn key_of(kind: &str, name: &str) -> GraphKey { GraphKey { @@ -197,8 +194,9 @@ pub(crate) fn direct_model_version_identities<'py>( version_hashes.set_item(&name, model_hash)?; } } - call_with( - ®istry::class(py, RecordId::DirectModelVersionIdentities)?, + new_record_with( + py, + RecordId::DirectModelVersionIdentities, &[ ("function_local_hashes", function_hashes.into_any()), ("seed_version_hashes", seed_hashes.into_any()), @@ -237,8 +235,7 @@ pub(crate) fn with_honest_model_write_hashes<'py>( let mut graph = IdentityGraph::default(); let mut base_hashes: Vec<(GraphKey, String)> = Vec::new(); let mut function_deps: HashMap> = HashMap::new(); - for item in scope - .getattr("upstream_deps")? + for item in native_field(&scope, "upstream_deps")? .call_method0("items")? .try_iter()? { @@ -265,7 +262,7 @@ pub(crate) fn with_honest_model_write_hashes<'py>( } } let recorded_seeds: Bound<'py, PyAny> = - native_field(&snapshot, "fingerprints")?.getattr("seeds")?; + native_field(&native_field(&snapshot, "fingerprints")?, "seeds")?; for (name, seed_hash) in string_pairs(&native_field(&identities, "seed_version_hashes")?)? { let seed_key: GraphKey = key_of(SEED_RESOURCE, &name); graph.nodes.insert( @@ -284,8 +281,7 @@ pub(crate) fn with_honest_model_write_hashes<'py>( base_hashes.push((seed_key, write_hash)); } let local_hashes: Bound<'py, PyAny> = native_field(&identities, "model_local_hashes")?; - for model in scope - .getattr("models_by_name")? + for model in native_field(&scope, "models_by_name")? .call_method0("values")? .try_iter()? { @@ -300,7 +296,7 @@ pub(crate) fn with_honest_model_write_hashes<'py>( ); } let recorded_models: Bound<'py, PyAny> = - native_field(&snapshot, "fingerprints")?.getattr("models")?; + native_field(&native_field(&snapshot, "fingerprints")?, "models")?; for item in recorded_models.call_method0("items")?.try_iter()? { let (name, fingerprint): (String, Bound<'py, PyAny>) = item?.extract()?; let version_hash: Bound<'py, PyAny> = native_field(&fingerprint, "version_hash")?; @@ -319,11 +315,9 @@ pub(crate) fn with_honest_model_write_hashes<'py>( graph_write_identity_hashes(&graph, &selected, base_hashes) .into_iter() .collect(); - let replace: Bound<'py, PyAny> = imported(py, DATACLASSES_MODULE, "replace")?; let model_changes: Bound<'py, PyDict> = PyDict::new(py); model_changes.update( - changes - .getattr("models")? + native_field(&changes, "models")? .downcast::()? .as_mapping(), )?; @@ -353,7 +347,6 @@ pub(crate) fn with_honest_model_write_hashes<'py>( model_changes.set_item( &name, replace_with( - &replace, &change, ( "fingerprint_version_hash", @@ -362,7 +355,7 @@ pub(crate) fn with_honest_model_write_hashes<'py>( )?, )?; } - replace_with(&replace, &changes, ("models", model_changes.into_any())) + replace_with(&changes, ("models", model_changes.into_any())) } /// Resolved actions with recomputed model changes patched in where available. @@ -373,11 +366,9 @@ pub(crate) fn merge_recomputed_model_changes<'py>( changes: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = resolved_actions.py(); - let replace: Bound<'py, PyAny> = imported(py, DATACLASSES_MODULE, "replace")?; let recomputed: Bound<'py, PyAny> = native_field(changes, "models")?; let merged: Bound<'py, PyDict> = PyDict::new(py); - for item in resolved_actions - .getattr("models")? + for item in native_field(resolved_actions, "models")? .call_method0("items")? .try_iter()? { @@ -386,19 +377,16 @@ pub(crate) fn merge_recomputed_model_changes<'py>( if change.is_none() { change = native_field(&resolved, "change")?; } - merged.set_item(name, replace_with(&replace, &resolved, ("change", change))?)?; + merged.set_item(name, replace_with(&resolved, ("change", change))?)?; } - replace_with(&replace, resolved_actions, ("models", merged.into_any())) + replace_with(resolved_actions, ("models", merged.into_any())) } fn replace_with<'py>( - replace: &Bound<'py, PyAny>, instance: &Bound<'py, PyAny>, (field, value): (&str, Bound<'py, PyAny>), ) -> PyResult> { - let keywords: Bound<'py, PyDict> = PyDict::new(replace.py()); - keywords.set_item(field, value)?; - replace.call((instance,), Some(&keywords)) + replaced(instance, &[(field, value)]) } pub(crate) fn register(module: &Bound<'_, PyModule>) -> 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 f830a766ec..50cb22e106 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_identity/metadata.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_identity/metadata.rs @@ -102,7 +102,7 @@ pub(crate) fn model_version_identity_metadata_json<'py>( } } } - let config_values: Bound<'py, PyAny> = native_field(model, "config")?.getattr("values")?; + let config_values: Bound<'py, PyAny> = native_field(&native_field(model, "config")?, "values")?; let signature: Bound<'py, PyDict> = execution_signature(model, &config_values, hook_version_hashes)?; version_identity_metadata_json( @@ -217,7 +217,7 @@ fn contract_output_signature<'py>( fn column_signature<'py>(column: &Bound<'py, PyAny>) -> PyResult> { let entry: Bound<'py, PyDict> = PyDict::new(column.py()); for field in ["name", "type", "nullable"] { - entry.set_item(field, column.getattr(field)?)?; + entry.set_item(field, native_field(column, field)?)?; } Ok(entry) } @@ -235,7 +235,7 @@ fn family_signatures<'py>(families: &Bound<'py, PyAny>) -> PyResult) -> PyResult< let payload: Bound<'_, PyDict> = PyDict::new(py); payload.set_item( "contract", - model - .getattr("config")? - .getattr("values")? + native_field(&native_field(model, "config")?, "values")? .call_method1("get", (CONTRACT_CONFIG_KEY,))?, )?; let columns: Bound<'_, PyList> = PyList::empty(py); @@ -284,7 +282,8 @@ pub(crate) fn function_local_hashes<'py>( for function in functions.try_iter()? { let function: Bound<'py, PyAny> = function?; let name: String = native_field(&function, "name")?.extract()?; - let language: Bound<'py, PyAny> = native_field(&function, "language")?.getattr("value")?; + let language: Bound<'py, PyAny> = + native_field(&native_field(&function, "language")?, "value")?; let body_hash: String = function_definition_hash( py, &name, @@ -307,7 +306,7 @@ pub(crate) fn function_local_hashes<'py>( payload.set_item("body_sql", body_hash)?; payload.set_item("language", language)?; for field in ["runtime_version", "entry_point", "packages"] { - payload.set_item(field, function.getattr(field)?)?; + payload.set_item(field, native_field(&function, field)?)?; } hashes.set_item( name, @@ -331,7 +330,7 @@ fn name_type_pairs<'py>(items: &Bound<'py, PyAny>) -> PyResult, name: &str) -> PyResult { - let value: Bound<'_, PyAny> = match owner.getattr(name) { + let value: Bound<'_, PyAny> = match native_field(owner, name) { Ok(value) => value, Err(_) => return Ok(String::new()), }; @@ -361,8 +360,7 @@ pub(crate) fn compiled_function_fingerprint_sql(function: &Bound<'_, PyAny>) -> return_columns: rendered_pairs(&native_field(function, "return_columns")?)?, runtime_version: text_or_empty(function, "runtime_version")?, entry_point: text_or_empty(function, "entry_point")?, - packages: function - .getattr("packages")? + packages: native_field(function, "packages")? .try_iter()? .map(|package| package?.extract::()) .collect::>()?, @@ -383,7 +381,7 @@ pub(crate) fn seed_identity(seed: &Bound<'_, PyAny>) -> PyResult<(String, String PyList::empty(py).into_any() } else { seed_rows( - &native_field(seed, "seed_file")?.getattr("file_path")?, + &native_field(&native_field(seed, "seed_file")?, "file_path")?, &csv_settings, )? }; @@ -411,7 +409,7 @@ fn seed_rows<'py>( }; let options: Bound<'py, PyDict> = PyDict::new(py); for option in SEED_DIALECT_OPTIONS { - let value: Bound<'py, PyAny> = csv_settings.getattr(option)?; + let value: Bound<'py, PyAny> = native_field(csv_settings, option)?; if !value.is_none() { options.set_item(option, value)?; } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_identity/staleness.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_identity/staleness.rs index 99901a016b..4e703fefba 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_identity/staleness.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_identity/staleness.rs @@ -1,6 +1,7 @@ //! Warnings for selected models stale through changed upstreams outside the run set. use crate::bindings::_helpers::planner_changes::results::native_enum; +use crate::bindings::_helpers::records::construct::new_record_with; use std::collections::{HashMap, HashSet}; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; @@ -16,12 +17,11 @@ use sqlbuild_analysis::planner_identity::models::{ GraphKey, StaleWarning, StalenessGraph, StalenessNames, }; -use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; +use crate::bindings::_helpers::compile_driver::python_calls::KeywordArguments; use crate::bindings::_helpers::planner_changes::detect::{ModelPass, model_change}; use crate::bindings::_helpers::planner_identity::hashing::graph_key; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; /// Change kinds that mean the model's own identity changed. const OWN_CHANGE_KINDS: [&str; 5] = [ @@ -123,11 +123,9 @@ fn changed_model_names(inputs: &StalenessInputs<'_>) -> PyResult fn changed_seed_names(inputs: &StalenessInputs<'_>) -> PyResult> { let recorded: Bound<'_, PyAny> = - native_field(&inputs.snapshot, "fingerprints")?.getattr("seeds")?; + native_field(&native_field(&inputs.snapshot, "fingerprints")?, "seeds")?; let mut changed: HashSet = HashSet::new(); - for item in inputs - .identities - .getattr("seed_version_hashes")? + for item in native_field(&inputs.identities, "seed_version_hashes")? .call_method0("items")? .try_iter()? { @@ -147,7 +145,6 @@ fn plan_warnings<'py>( py: Python<'py>, warnings: Vec, ) -> PyResult> { - let warning_class: Bound<'py, PyAny> = registry::class(py, RecordId::PlanWarning)?; let severity: Bound<'py, PyAny> = native_enum(py, RecordId::WarningSeverity, "warning")?; let mut rendered: Vec> = Vec::with_capacity(warnings.len()); for warning in warnings { @@ -156,8 +153,9 @@ fn plan_warnings<'py>( "upstream(s)", &warning.trigger_names, ); - rendered.push(call_with( - &warning_class, + rendered.push(new_record_with( + py, + RecordId::PlanWarning, &[ ( "model_name", @@ -261,12 +259,11 @@ fn classify_selection_staleness_warnings<'py>( sources: name_set(&native_field(graph, "changed_source_names")?)?, }, }; - let warning_class: Bound<'py, PyAny> = - registry::class(py, RecordId::SelectionStalenessWarning)?; let mut warnings: Vec> = Vec::new(); for warning in classify_selection_staleness(&staleness) { - warnings.push(call_with( - &warning_class, + warnings.push(new_record_with( + py, + RecordId::SelectionStalenessWarning, &[ ( "model_name", diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/column_candidates.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/column_candidates.rs index c439eae2a3..73f6fa81c0 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/column_candidates.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/column_candidates.rs @@ -27,8 +27,7 @@ pub(super) fn candidates<'py>( { continue; } - let model = scope - .getattr("models_by_name")? + let model = native_field(&scope, "models_by_name")? .call_method1("get", (native_field(&key, "name")?,))?; if model.is_none() { continue; @@ -43,8 +42,7 @@ pub(super) fn candidates<'py>( { continue; } - let warehouse = snapshot - .getattr("existing_columns")? + let warehouse = native_field(&snapshot, "existing_columns")? .call_method1("get", (&name, PyTuple::empty(py)))?; let declared = PyDict::new(py); let schema = native_field(&model, "schema_entry")?; @@ -60,13 +58,10 @@ pub(super) fn candidates<'py>( } } } - let fingerprint = snapshot - .getattr("fingerprints")? - .getattr("models")? + let fingerprint = native_field(&native_field(&snapshot, "fingerprints")?, "models")? .call_method1("get", (&name,))?; let changed = !fingerprint.is_none() - && !fingerprint - .getattr("definition_hash")? + && !native_field(&fingerprint, "definition_hash")? .eq(definition_hash(&model, &snapshot)?)?; if !warehouse.is_truthy()? || (declared.is_empty() && !changed) { continue; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/column_entries.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/column_entries.rs index 5385aa4854..3a165d5625 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/column_entries.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/column_entries.rs @@ -14,9 +14,9 @@ use crate::bindings::_helpers::planner_migrations::constants::{AUTOMATIC, DONE}; use crate::bindings::_helpers::planner_migrations::models::{CheckedColumns, ColumnCandidate}; use crate::bindings::_helpers::planner_phases::objects::object; use crate::bindings::_helpers::planner_phases::runtime::runtime_field; +use crate::bindings::_helpers::records::construct::{enum_from_value, enum_member}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::state_rows::migration_projections::newest_column; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; use pyo3::types::{PyDict, PyDictMethods, PyFrozenSet, PyListMethods, PyTuple}; @@ -96,8 +96,7 @@ pub(super) fn decide<'py>( (origin.is_some(), has_destination), outputs.contains(&origin_key), ); - let decision = - registry::class(py, RecordId::ColumnMigrationDecision)?.call1((decision_name,))?; + let decision = enum_from_value(py, RecordId::ColumnMigrationDecision, decision_name)?; if discovery == AUTOMATIC && native_field(&decision, "blocks_build")?.is_truthy()? { continue; } @@ -119,7 +118,7 @@ pub(super) fn decide<'py>( ("destination_column", destination), ( "discovery", - registry::class(py, RecordId::MigrationDiscovery)?.call1((discovery,))?, + enum_from_value(py, RecordId::MigrationDiscovery, discovery)?, ), ("decision", decision), ( @@ -127,9 +126,7 @@ pub(super) fn decide<'py>( if done { native_field(&newest, "target_name")? } else { - runtime - .getattr("project")? - .getattr("effective_target_name")? + native_field(&native_field(runtime, "project")?, "effective_target_name")? }, ), ( @@ -155,9 +152,7 @@ pub(super) fn checked<'py>( let py = runtime.py(); let mut pending = false; for entry in &entries { - pending |= native_field(entry, "decision")? - .getattr("renames")? - .is_truthy()?; + pending |= native_field(&native_field(entry, "decision")?, "renames")?.is_truthy()?; } if !pending { return Ok(CheckedColumns { @@ -175,9 +170,10 @@ pub(super) fn checked<'py>( native_field(&destination, "name")? }; let reason = call_with( - &runtime - .getattr("adapter")? - .getattr("column_rename_unavailable_reason")?, + &native_field( + &native_field(runtime, "adapter")?, + "column_rename_unavailable_reason", + )?, &[ ( "connection", @@ -195,10 +191,7 @@ pub(super) fn checked<'py>( let mut checked: Vec> = Vec::new(); let mut warnings: Vec> = Vec::new(); for entry in entries { - if !native_field(&entry, "decision")? - .getattr("renames")? - .is_truthy()? - { + if !native_field(&native_field(&entry, "decision")?, "renames")?.is_truthy()? { checked.push(entry); } else if native_field(&entry, "discovery")?.eq("manual")? { checked.push(replace( @@ -206,8 +199,7 @@ pub(super) fn checked<'py>( &[ ( "decision", - registry::class(py, RecordId::ColumnMigrationDecision)? - .getattr("UNSUPPORTED")?, + enum_member(py, RecordId::ColumnMigrationDecision, "UNSUPPORTED")?, ), ("message", reason.clone()), ], diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/column_objects.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/column_objects.rs index 6bc3a383e4..78a498dc4f 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/column_objects.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/column_objects.rs @@ -1,13 +1,13 @@ //! Boundary conversions for the column migration planner. use crate::bindings::_helpers::analysis::model_reads; -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; use crate::bindings::_helpers::model_config::config_values::config_str as authored_config_str; use crate::bindings::_helpers::planner_identity::hashing::model_definition_hash; use crate::bindings::_helpers::planner_phases::objects::object; +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::{NativeItem, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::fields::replaced_keywords; +use crate::bindings::_helpers::records::ids::RecordId; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; use pyo3::types::{PyDict, PyDictMethods, PyString}; @@ -27,7 +27,7 @@ pub(super) fn replace<'py>( for (name, field) in fields { kwargs.set_item(name, field)?; } - imported(value.py(), "dataclasses", "replace")?.call((value,), Some(&kwargs)) + replaced_keywords(value, &kwargs) } pub(super) fn model<'py>( @@ -35,22 +35,23 @@ pub(super) fn model<'py>( name: RecordId, fields: &[(&str, Bound<'py, PyAny>)], ) -> PyResult> { - call_with(®istry::class(py, name)?, fields) + new_record_with(py, name, fields) } pub(super) fn native<'py>( py: Python<'py>, - name: impl Into, + name: RecordId, fields: &[(&str, Bound<'py, PyAny>)], ) -> PyResult> { - call_with(®istry::item(py, name)?, fields) + new_record_with(py, name, fields) } pub(super) fn config<'py>(model: &Bound<'py, PyAny>, key: &str) -> PyResult> { - Ok( - authored_config_str(&native_field(model, "config")?.getattr("values")?, key)? - .unwrap_or_else(|| model.py().None().into_bound(model.py())), - ) + Ok(authored_config_str( + &native_field(&native_field(model, "config")?, "values")?, + key, + )? + .unwrap_or_else(|| model.py().None().into_bound(model.py()))) } pub(super) fn definition_hash<'py>( @@ -63,8 +64,7 @@ pub(super) fn definition_hash<'py>( model.py(), &model_reads::name_object(model)?.extract::()?, &model_reads::query_sql(model)?.extract::()?, - snapshot - .getattr("column_dialect")? + native_field(snapshot, "column_dialect")? .extract::>()? .as_deref(), )?, 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 b05e2eaf77..fd474377f4 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 @@ -47,10 +47,7 @@ pub(super) fn overlay<'py>( native_field(entry, "origin_column")?, native_field(entry, "destination_column")?, )?; - if native_field(entry, "decision")? - .getattr("renames")? - .is_truthy()? - { + if native_field(&native_field(entry, "decision")?, "renames")?.is_truthy()? { pending.set_item( lower(&native_field(entry, "origin_column")?)?, native_field(entry, "destination_column")?, @@ -61,8 +58,7 @@ pub(super) fn overlay<'py>( continue; } let mut renamed: Vec> = Vec::new(); - for column in snapshot - .getattr("existing_columns")? + for column in native_field(snapshot, "existing_columns")? .call_method1("get", (&name, PyTuple::empty(py)))? .try_iter()? { @@ -145,9 +141,7 @@ pub(super) fn cursors<'py>( } else { py.None().into_bound(py) }; - if native_field(entry, "decision")? - .getattr("renames")? - .is_truthy()? + if native_field(&native_field(entry, "decision")?, "renames")?.is_truthy()? && !cursor.is_none() && lower(&cursor)? == lower(&native_field(entry, "destination_column")?)? { @@ -163,10 +157,10 @@ pub(super) fn cursors<'py>( let target = if physical.is_truthy()? { physical } else { - let qualified = models - .get_item(&name)? - .getattr("destination")? - .getattr("qualified_name")?; + let qualified = native_field( + &native_field(&models.get_item(&name)?, "destination")?, + "qualified_name", + )?; if qualified.is_truthy()? { qualified } else { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/column_warnings.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/column_warnings.rs index 18abe9e14e..b274979dc9 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/column_warnings.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/column_warnings.rs @@ -6,9 +6,9 @@ use crate::bindings::_helpers::planner_migrations::constants::{ DONE, MISSING_ORIGIN_ALLOW_OUTCOME, MISSING_ORIGIN_CONFIRMATION_OUTCOME, MISSING_ORIGIN_DENY_OUTCOME, }; +use crate::bindings::_helpers::records::construct::enum_member; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use pyo3::exceptions::PyKeyError; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; use sqlbuild_analysis::planner_migrations::main::column_warning::column_warning; @@ -28,11 +28,11 @@ pub(super) fn warning<'py>( ("model_name", native_field(entry, "model_name")?), ( "severity", - registry::class(py, RecordId::WarningSeverity)?.getattr(if error { - "ERROR" - } else { - "WARNING" - })?, + enum_member( + py, + RecordId::WarningSeverity, + if error { "ERROR" } else { "WARNING" }, + )?, ), ("message", text(py, message)), ("code", text(py, code)), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/columns.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/columns.rs index 292a8a600d..91a92f9499 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/columns.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/columns.rs @@ -11,10 +11,10 @@ use sqlbuild_analysis::planner_migrations::main::{ }; use sqlbuild_analysis::planner_migrations::models::Projection; -use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; +use crate::bindings::_helpers::compile_driver::python_calls::KeywordArguments; +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; -use crate::bindings::_helpers::records::registry; fn projections(shape: &Bound<'_, PyAny>) -> PyResult> { let mut result: Vec = Vec::new(); @@ -119,8 +119,9 @@ pub(crate) fn rename_hints<'py>( ); let mut result: Vec> = Vec::new(); for hint in hints { - result.push(call_with( - ®istry::class(py, RecordId::ColumnRenameHint)?, + result.push(new_record_with( + py, + RecordId::ColumnRenameHint, &[ ("model_name", arguments.value("model_name")?), ( 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 6ac4996040..df77c45227 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/compatibility.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/compatibility.rs @@ -2,15 +2,15 @@ use crate::bindings::_helpers::analysis::model_reads; use crate::bindings::_helpers::analysis::model_reads::inferred_rows; +use crate::bindings::_helpers::compile_driver::python_calls::KeywordArguments; use crate::bindings::_helpers::compile_driver::python_calls::keywords; -use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; use crate::bindings::_helpers::planner_changes::detect::detect_model_changes; use crate::bindings::_helpers::planner_migrations::models::{ CompatibilityCheck, MigrationCompatibilityResult, }; +use crate::bindings::_helpers::records::construct::{enum_from_value, new_record, new_record_with}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use pyo3::exceptions::PyValueError; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; use pyo3::types::{PyDict, PyDictMethods, PyTuple, PyTupleMethods}; @@ -19,9 +19,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> { - model - .getattr("config")? - .getattr("values")? + native_field(&native_field(model, "config")?, "values")? .call_method1("get", (key,))? .extract() } @@ -43,15 +41,16 @@ fn blocking<'py>( .filter(|value| !value.is_empty()) .unwrap_or_else(|| "append_new_columns".to_owned()) }; - registry::class( + new_record( py, if snapshot { RecordId::SnapshotSchemaChangePolicy } else { RecordId::OnSchemaChange }, - )? - .call1((&policy,))?; + (&policy,), + None, + )?; let mut findings: Vec = Vec::new(); for finding in findings_in.try_iter()? { let finding = finding?; @@ -137,8 +136,9 @@ pub(super) fn check_migration_compatibility<'py>( model_reads::name_object(&model)?, check.origin_columns.clone(), )?; - let snapshot = call_with( - ®istry::class(py, RecordId::WarehouseSnapshot)?, + let snapshot = new_record_with( + py, + RecordId::WarehouseSnapshot, &[ ("existing_relations", relations.into_any()), ("existing_columns", columns.into_any()), @@ -198,7 +198,7 @@ fn result<'py>( findings: Bound<'py, PyTuple>, ) -> PyResult> { Ok(MigrationCompatibilityResult { - status: registry::class(py, RecordId::MigrationCompatibility)?.call1((status,))?, + status: enum_from_value(py, RecordId::MigrationCompatibility, status)?, findings: findings.into_any(), }) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/discovery.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/discovery.rs index 5c70a6033a..b23a6cb872 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/discovery.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/discovery.rs @@ -46,9 +46,7 @@ pub(super) fn discover_model_migrations<'py>( let metadata = discovery_models::metadata(&runtime, &models)?; let inputs = FingerprintInputs { metadata_jsons: metadata.into_any(), - dialect: runtime - .getattr("adapter")? - .call_method0("sql_analysis_dialect")?, + dialect: native_field(&runtime, "adapter")?.call_method0("sql_analysis_dialect")?, fingerprints: cache, }; call_with( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/discovery_events.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/discovery_events.rs index 6df7bfbf46..6732bc6c9e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/discovery_events.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/discovery_events.rs @@ -67,12 +67,9 @@ pub(super) fn renames<'py>( state: &Bound<'py, PyAny>, ) -> PyResult> { let py = runtime.py(); - let target = runtime - .getattr("project")? - .getattr("effective_target_name")?; + let target = native_field(&native_field(runtime, "project")?, "effective_target_name")?; let ordered = PyList::empty(py); - let events: Vec> = state - .getattr("events")? + let events: Vec> = native_field(state, "events")? .try_iter()? .collect::>()?; for (index, event) in events.iter().enumerate() { @@ -109,9 +106,7 @@ pub(super) fn resumed<'py>( state: &Bound<'py, PyAny>, ) -> PyResult> { let result = PyDict::new(runtime.py()); - let target = runtime - .getattr("project")? - .getattr("effective_target_name")?; + let target = native_field(&native_field(runtime, "project")?, "effective_target_name")?; let events = native_field(state, "events")?; for item in models { let name = native_field(item, "name")?; @@ -124,8 +119,7 @@ pub(super) fn resumed<'py>( continue; } let event_target = native_field(&event, "target_name")?; - if event - .getattr("destination")? + if native_field(&event, "destination")? .call_method1("matches", (relation(&destination)?,))? .is_truthy()? && (event_target.is_none() || event_target.eq(&target)?) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/discovery_models.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/discovery_models.rs index fb01a8af4c..2e6597b9e7 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/discovery_models.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/discovery_models.rs @@ -17,9 +17,11 @@ use std::collections::BTreeMap; pub(super) fn ordered<'py>(runtime: &Bound<'py, PyAny>) -> PyResult>> { let mut models: BTreeMap> = BTreeMap::new(); let mut dependencies: BTreeMap> = BTreeMap::new(); - for item in runtime_field(runtime, |runtime| &runtime.project)? - .getattr("models")? - .try_iter()? + for item in native_field( + &runtime_field(runtime, |runtime| &runtime.project)?, + "models", + )? + .try_iter()? { let item = item?; let name: String = native_field(&item, "name")?.extract()?; @@ -46,20 +48,18 @@ pub(super) fn triggered( declared: &Bound<'_, PyDict>, ) -> PyResult { let py = scope.py(); - let selected: Vec> = scope - .getattr("selected_keys")? + let selected: Vec> = native_field(scope, "selected_keys")? .try_iter()? .collect::>()?; let mut relevant = PyNodeSet(PySet::new(py, &selected)?); breadth_first_reach(selected, None, &mut relevant, |key| { - scope - .getattr("upstream_deps")? + native_field(scope, "upstream_deps")? .call_method1("get", (key, pyo3::types::PyTuple::empty(py)))? .try_iter()? .collect::>>>() })?; let relevant = relevant.0; - let fingerprints = native_field(snapshot, "fingerprints")?.getattr("models")?; + let fingerprints = native_field(&native_field(snapshot, "fingerprints")?, "models")?; for item in models { let name = native_field(item, "name")?; if relevant.contains(native_field(item, "key")?)? @@ -82,8 +82,7 @@ pub(super) fn metadata<'py>( py, build_function_local_hashes( &native_field(&project, "functions")?, - project - .getattr("sql_analysis_dialect")? + native_field(&project, "sql_analysis_dialect")? .extract::>()? .as_deref(), )?, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/discovery_requests.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/discovery_requests.rs index cdccb3cc10..bc9ed26749 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/discovery_requests.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/discovery_requests.rs @@ -9,9 +9,9 @@ use crate::bindings::_helpers::planner_migrations::discovery_events::{ use crate::bindings::_helpers::planner_migrations::models::{ ModelMigrationDeclaration, ModelMigrationRequest, }; +use crate::bindings::_helpers::records::construct::enum_member; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; use pyo3::types::{ PyDict, PyDictMethods, PyFrozenSet, PyListMethods, PySet, PySetMethods, PyString, PyTuple, @@ -93,8 +93,7 @@ pub(super) fn candidates<'py>( } let event = newest(&native_field(state, "events")?, &location)?; if !event.is_none() - && event - .getattr("origin")? + && native_field(&event, "origin")? .call_method1("matches", (relation(&location)?,))? .is_truthy()? { @@ -114,10 +113,7 @@ pub(super) fn automatic<'py>( let py = runtime.py(); let mut requests: Vec> = Vec::new(); for item in models { - if !scope - .getattr("selected_keys")? - .contains(native_field(item, "key")?)? - { + if !native_field(scope, "selected_keys")?.contains(native_field(item, "key")?)? { continue; } let name = native_field(item, "name")?; @@ -145,8 +141,7 @@ pub(super) fn automatic<'py>( if let Some((location, origin_model)) = origin { requests.push(ModelMigrationRequest { model: item.clone(), - discovery: registry::class(py, RecordId::MigrationDiscovery)? - .getattr("AUTOMATIC")?, + discovery: enum_member(py, RecordId::MigrationDiscovery, "AUTOMATIC")?, raw_origin: None, origin_location: Some(location), origin_model, @@ -166,10 +161,7 @@ pub(super) fn warnings<'py>( let py = scope.py(); let selected = PySet::empty(py)?; for item in models { - if scope - .getattr("selected_keys")? - .contains(native_field(item, "key")?)? - { + if native_field(scope, "selected_keys")?.contains(native_field(item, "key")?)? { selected.add(native_field(item, "name")?)?; } } @@ -179,7 +171,7 @@ pub(super) fn warnings<'py>( if selected.contains(&name)? { warnings.push(native(py, RecordId::PlanWarning, &[ ("model_name", PyString::new(py, &name).into_any()), - ("severity", registry::class(py, RecordId::WarningSeverity)?.getattr("WARNING")?), + ("severity", enum_member(py, RecordId::WarningSeverity, "WARNING")?), ("message", PyString::new(py, &format!("'{name}' matches {detail}; no automatic migration was inferred. Add migrate_from to choose one.")).into_any()), ("code", PyString::new(py, "M107").into_any()), ])?); diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/fingerprint.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/fingerprint.rs index b8e3e1006d..31e4eea1b3 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/fingerprint.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/fingerprint.rs @@ -3,6 +3,7 @@ use crate::bindings::_helpers::analysis::model_reads; use crate::bindings::_helpers::compile_driver::python_calls::KeywordArguments; use crate::bindings::_helpers::records::fields::field as native_field; +use crate::bindings::_helpers::records::fields::replaced_keywords; use pyo3::exceptions::PyValueError; use pyo3::prelude::{Bound, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; use pyo3::types::{PyDict, PyDictMethods, PyFrozenSet, PyTuple}; @@ -90,7 +91,7 @@ pub(crate) fn with_migration_fingerprints<'py>( py: Python<'py>, options: Option>, ) -> PyResult> { - use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; + use crate::bindings::_helpers::compile_driver::python_calls::call_with; let arguments = KeywordArguments::read( py, "with_migration_fingerprints", @@ -123,7 +124,7 @@ pub(crate) fn with_migration_fingerprints<'py>( )?; let kwargs = PyDict::new(py); kwargs.set_item("migration_fingerprint", fingerprint)?; - attached.push(imported(py, "dataclasses", "replace")?.call((entry,), Some(&kwargs))?); + attached.push(replaced_keywords(&entry, &kwargs)?); } cache.call_method0("persist")?; PyTuple::new(py, attached) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/model_entries.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/model_entries.rs index 1837c63193..7d74bf6b36 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/model_entries.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/model_entries.rs @@ -11,6 +11,7 @@ use crate::bindings::_helpers::planner_migrations::model_handover::{ use crate::bindings::_helpers::planner_migrations::models::{ CompatibilityCheck, MigrationDecisionRequest, }; +use crate::bindings::_helpers::records::construct::{enum_from_value, enum_member}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -40,7 +41,7 @@ pub(super) fn decision<'py>(entry: &Bound<'py, PyAny>, name: &str) -> PyResult( ("discovery", request.discovery.clone()), ( "decision", - registry::class(py, RecordId::MigrationDecision)?.getattr("MIGRATE")?, + enum_member(py, RecordId::MigrationDecision, "MIGRATE")?, ), ( "compatibility", - registry::class(py, RecordId::MigrationCompatibility)?.getattr("NOT_CHECKED")?, + enum_member(py, RecordId::MigrationCompatibility, "NOT_CHECKED")?, ), ("origin_model", origin_model), ("origin", origin.clone()), ("destination", destination.clone()), ( "target_name", - runtime - .getattr("project")? - .getattr("effective_target_name")?, + native_field(&native_field(&runtime, "project")?, "effective_target_name")?, ), ( "origin_version_hash", @@ -143,12 +142,10 @@ pub(super) fn decide<'py>( ], )?; let recorded = !newest.is_none() - && newest - .getattr("destination")? + && native_field(&newest, "destination")? .call_method1("matches", (relation(&destination)?,))? .is_truthy()? - && newest - .getattr("origin")? + && native_field(&newest, "origin")? .call_method1("matches", (relation(&origin)?,))? .is_truthy()?; if request.identity_only { @@ -161,9 +158,7 @@ pub(super) fn decide<'py>( ); } let inherited = if recorded && !applies(&item, &newest, &snapshot)? { - snapshot - .getattr("fingerprints")? - .getattr("models")? + native_field(&native_field(&snapshot, "fingerprints")?, "models")? .call_method1("get", (native_field(&item, "name")?,))? } else { empty_version(&item, &fingerprint)? @@ -193,9 +188,7 @@ pub(super) fn decide<'py>( &native_field(&newest, "origin_version_hash")?, )? } else { - snapshot - .getattr("fingerprints")? - .getattr("models")? + native_field(&native_field(&snapshot, "fingerprints")?, "models")? .call_method1("get", (native_field(&item, "name")?,))? }; return result( @@ -225,27 +218,22 @@ pub(super) fn decide<'py>( model: item.clone(), origin_relation, origin_columns: state.call_method1("relation_columns", (&origin,))?, - sql_analysis_enabled: runtime - .getattr("project")? - .getattr("settings")? - .getattr("sql_analysis")?, + sql_analysis_enabled: native_field( + &native_field(&native_field(&runtime, "project")?, "settings")?, + "sql_analysis", + )?, column_dialect: native_field(&snapshot, "column_dialect")?, snapshots_config: decision_request.snapshots_config.clone(), })?; let name = model_decision( - snapshot - .getattr("existing_relations")? - .contains(native_field(&item, "name")?)?, + native_field(&snapshot, "existing_relations")?.contains(native_field(&item, "name")?)?, ( !newest.is_none(), !newest.is_none() - && newest - .getattr("origin")? + && native_field(&newest, "origin")? .call_method1("matches", (relation(&destination)?,))? .is_truthy()?, - snapshot - .getattr("fingerprints")? - .getattr("models")? + native_field(&native_field(&snapshot, "fingerprints")?, "models")? .contains(native_field(&item, "name")?)?, ), request.force, @@ -257,10 +245,7 @@ pub(super) fn decide<'py>( ("compatibility_findings", compatibility.findings), ], )?; - if !entry - .getattr("decision")? - .getattr("moves_data")? - .is_truthy()? + if !native_field(&native_field(&entry, "decision")?, "moves_data")?.is_truthy()? || native_field(&entry, "blocks_build")?.is_truthy()? { return result(entry, py.None().into_bound(py), false); diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/model_handover.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/model_handover.rs index 9d713cfc26..9f6f6ac829 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/model_handover.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/model_handover.rs @@ -16,9 +16,7 @@ pub(super) fn applies( event: &Bound<'_, PyAny>, snapshot: &Bound<'_, PyAny>, ) -> PyResult { - let own = snapshot - .getattr("fingerprints")? - .getattr("models")? + let own = native_field(&native_field(snapshot, "fingerprints")?, "models")? .call_method1("get", (model_reads::name_object(model)?,))?; Ok(own.is_none() || presumed_utc(&native_field(&own, "ts")?)? diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/model_overlay.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/model_overlay.rs index 60f7564254..f7eafc4b5a 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/model_overlay.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/model_overlay.rs @@ -21,17 +21,14 @@ pub(super) fn overlay<'py>( } let runtime = phase.runtime.clone(); let py = runtime.py(); - let relations = snapshot - .getattr("existing_relations")? + let relations = native_field(&snapshot, "existing_relations")? .cast_into::()? .copy()?; - let columns = snapshot - .getattr("existing_columns")? + let columns = native_field(&snapshot, "existing_columns")? .cast_into::()? .copy()?; let fingerprints = native_field(&snapshot, "fingerprints")?; - let models = fingerprints - .getattr("models")? + let models = native_field(&fingerprints, "models")? .cast_into::()? .copy()?; let redirected = PyDict::new(py); @@ -45,11 +42,7 @@ pub(super) fn overlay<'py>( } else { models.set_item(&name, handover)?; } - if !entry - .getattr("decision")? - .getattr("moves_data")? - .is_truthy()? - { + if !native_field(&native_field(entry, "decision")?, "moves_data")?.is_truthy()? { continue; } let origin = native_field(entry, "origin")?; @@ -84,8 +77,7 @@ pub(super) fn overlay<'py>( redirected.set_item(name, qualified)?; } } - let cursors = snapshot - .getattr("cursor_snapshots")? + let cursors = native_field(&snapshot, "cursor_snapshots")? .cast_into::()? .copy()?; let gathered = super::planning_boundary::redirected_cursor_snapshots( @@ -118,9 +110,11 @@ pub(super) fn overlay<'py>( } fn cursor_enabled(runtime: &Bound<'_, PyAny>, name: &Bound<'_, PyAny>) -> PyResult { - for item in runtime_field(runtime, |runtime| &runtime.project)? - .getattr("models")? - .try_iter()? + for item in native_field( + &runtime_field(runtime, |runtime| &runtime.project)?, + "models", + )? + .try_iter()? { let item = item?; if native_field(&item, "name")?.eq(name)? { 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 32e4de77cc..8cc7381729 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 @@ -14,9 +14,9 @@ use crate::bindings::_helpers::planner_migrations::planning_origin::{ }; 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::ids::RecordId; -use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; use pyo3::types::{ PyBool, PyDict, PyDictMethods, PyFrozenSet, PySet, PySetMethods, PyString, PyTuple, @@ -24,7 +24,7 @@ use pyo3::types::{ fn declared<'py>(item: &Bound<'py, PyAny>) -> PyResult>> { let py = item.py(); - let values = native_field(item, "config")?.getattr("values")?; + let values = native_field(&native_field(item, "config")?, "values")?; let origin = values.call_method1("get", ("migrate_from",))?; if !origin.is_instance_of::() { return Ok(None); @@ -32,7 +32,7 @@ fn declared<'py>(item: &Bound<'py, PyAny>) -> PyResult( { continue; } - let item = scope - .getattr("models_by_name")? + let item = native_field(scope, "models_by_name")? .call_method1("get", (native_field(&key, "name")?,))?; if !item.is_none() && let Some(request) = declared(&item)? @@ -91,9 +90,11 @@ fn discover<'py>( let scope = phase.scope.clone(); let snapshot = phase.snapshot.clone(); let mut declared_requests: Vec> = Vec::new(); - for item in runtime_field(&runtime, |runtime| &runtime.project)? - .getattr("models")? - .try_iter()? + for item in native_field( + &runtime_field(&runtime, |runtime| &runtime.project)?, + "models", + )? + .try_iter()? { if let Some(request) = declared(&item?)? { declared_requests.push(request); @@ -168,7 +169,7 @@ fn finish<'py>( )?; let project_config = runtime_field(&runtime, |runtime| &runtime.project_config)?; let snapshots_config = if project_config.is_none() { - registry::class(py, RecordId::SnapshotsConfig)?.call0()? + new_record(py, RecordId::SnapshotsConfig, (), None)? } else { native_field(&project_config, "snapshots")? }; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/old_name_entries.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/old_name_entries.rs index 86f7b37a68..c6c867664d 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/old_name_entries.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/old_name_entries.rs @@ -12,9 +12,9 @@ use crate::bindings::_helpers::planner_migrations::old_names::declared_column_al use crate::bindings::_helpers::planner_migrations::old_names::old_name_view_skip_reason; 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, record_class}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; use pyo3::types::{PyBool, PyList, PyTuple}; @@ -29,7 +29,7 @@ impl<'py> OldNamePlanner<'py> { let py = runtime.py(); let project_config = runtime_field(&runtime, |runtime| &runtime.project_config)?; let config = if project_config.is_none() { - registry::class(py, RecordId::MigrationsConfig)?.call0()? + new_record(py, RecordId::MigrationsConfig, (), None)? } else { native_field(&project_config, "migrations")? }; @@ -52,16 +52,14 @@ impl<'py> OldNamePlanner<'py> { let mut newest: Option<(Bound<'py, PyTuple>, Bound<'py, PyAny>)> = None; for history in &self.histories { let movement = native_field(history, "move")?; - if !movement - .getattr("destination")? + if !native_field(&movement, "destination")? .call_method1("matches", (destination,))? .is_truthy()? { continue; } if let Some(origin) = origin - && !movement - .getattr("origin")? + && !native_field(&movement, "origin")? .call_method1("matches", (origin,))? .is_truthy()? { @@ -119,9 +117,7 @@ impl<'py> OldNamePlanner<'py> { else { return Ok(None); }; - if !history - .getattr("move")? - .getattr("destination_model")? + if !native_field(&native_field(&history, "move")?, "destination_model")? .eq(native_field(item, "name")?)? { return Ok(None); @@ -277,7 +273,7 @@ impl<'py> OldNamePlanner<'py> { &[ ( "model_name", - native_field(history, "move")?.getattr("destination_model")?, + native_field(&native_field(history, "move")?, "destination_model")?, ), ( "origin", @@ -290,11 +286,11 @@ impl<'py> OldNamePlanner<'py> { ("action", action(&self.runtime, "NONE")?), ( "target_name", - native_field(history, "move")?.getattr("target_name")?, + native_field(&native_field(history, "move")?, "target_name")?, ), ( "migration_event_id", - native_field(history, "move")?.getattr("event_id")?, + native_field(&native_field(history, "move")?, "event_id")?, ), ("reason", reason), ], @@ -302,8 +298,8 @@ impl<'py> OldNamePlanner<'py> { .map(Some); } } - let retention = native_field(history, "required")?.getattr("view_retention")?; - let duration = registry::class(py, RecordId::Duration)?.call_method1( + let retention = native_field(&native_field(history, "required")?, "view_retention")?; + let duration = record_class(py, RecordId::Duration)?.call_method1( "parse", (if retention.is_truthy()? { retention.clone() @@ -340,7 +336,7 @@ impl<'py> OldNamePlanner<'py> { ("action", action(&self.runtime, action_name)?), ( "target_name", - native_field(history, "move")?.getattr("target_name")?, + native_field(&native_field(history, "move")?, "target_name")?, ), ("retention", retention), ("expires_at", expires_at), @@ -358,7 +354,7 @@ impl<'py> OldNamePlanner<'py> { ), ( "migration_event_id", - native_field(history, "move")?.getattr("event_id")?, + native_field(&native_field(history, "move")?, "event_id")?, ), ( "grants_copied", @@ -387,5 +383,5 @@ impl<'py> OldNamePlanner<'py> { } fn action<'py>(runtime: &Bound<'py, PyAny>, name: &str) -> PyResult> { - registry::class(runtime.py(), RecordId::OldNameViewAction)?.getattr(name) + enum_member(runtime.py(), RecordId::OldNameViewAction, name) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/old_name_objects.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/old_name_objects.rs index 6eff236782..52b8e9dcca 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/old_name_objects.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/old_name_objects.rs @@ -4,9 +4,9 @@ use crate::bindings::_helpers::compile_driver::python_calls::call_with; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::planner_migrations::column_objects::{config, model}; +use crate::bindings::_helpers::records::construct::{new_record_with, record_class}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::state_rows::migration_projections::old_name_retention; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; @@ -24,16 +24,11 @@ pub(super) fn location<'py>( ("name", native_field(relation, "name")?), ]; let qualified = call_with( - &runtime - .getattr("adapter")? - .getattr("render_qualified_name")?, + &native_field(&native_field(runtime, "adapter")?, "render_qualified_name")?, &fields, )?; fields.push(("qualified_name", qualified)); - call_with( - ®istry::class(runtime.py(), RecordId::CompiledRelationLocation)?, - &fields, - ) + new_record_with(runtime.py(), RecordId::CompiledRelationLocation, &fields) } pub(super) fn stores_history(item: &Bound<'_, PyAny>) -> PyResult { @@ -45,7 +40,10 @@ pub(super) fn retention<'py>( item: &Bound<'py, PyAny>, project: &Bound<'py, PyAny>, ) -> PyResult> { - old_name_retention(&native_field(item, "config")?.getattr("values")?, project) + old_name_retention( + &native_field(&native_field(item, "config")?, "values")?, + project, + ) } pub(super) fn expires<'py>( @@ -53,7 +51,7 @@ pub(super) fn expires<'py>( now: &Bound<'py, PyAny>, ) -> PyResult> { let duration = - registry::class(now.py(), RecordId::Duration)?.call_method1("parse", (retention,))?; + record_class(now.py(), RecordId::Duration)?.call_method1("parse", (retention,))?; if duration.is_none() { Ok(now.py().None().into_bound(now.py())) } else { 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 a79839ee77..e7967e5971 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 @@ -23,25 +23,25 @@ fn schemas<'py>( let snapshot = phase.snapshot.clone(); let needed = PySet::empty(py)?; for item in selected { - let schema = native_field(item, "destination")?.getattr("schema")?; + let schema = native_field(&native_field(item, "destination")?, "schema")?; if !schema.is_none() { needed.add(schema)?; } } for entry in migration_entries.try_iter()? { - let schema = native_field(&entry?, "origin")?.getattr("schema")?; + let schema = native_field(&native_field(&entry?, "origin")?, "schema")?; if !schema.is_none() { needed.add(schema)?; } } - let mut include_project = runtime - .getattr("project")? - .getattr("unmatched_literal_sql_relations")? - .is_truthy()?; + let mut include_project = native_field( + &native_field(&runtime, "project")?, + "unmatched_literal_sql_relations", + )? + .is_truthy()?; if !include_project { for item in selected { - let existing = snapshot - .getattr("existing_relations")? + let existing = native_field(&snapshot, "existing_relations")? .call_method1("get", (native_field(item, "name")?,))?; if !existing.is_none() && host::entry(py, AdapterHost::NormalizeRelationType)? @@ -74,8 +74,7 @@ pub(crate) fn plan_old_name_views<'py>( let runtime = phase.runtime.clone(); let scope = phase.scope.clone(); let project = runtime_field(&runtime, |runtime| &runtime.project)?; - let models: Vec> = project - .getattr("models")? + let models: Vec> = native_field(&project, "models")? .try_iter()? .collect::>()?; let by_name = PyDict::new(py); @@ -105,7 +104,7 @@ pub(crate) fn plan_old_name_views<'py>( let mut views: Vec> = Vec::new(); for history in native_field(&state, "old_name_histories")?.try_iter()? { let history = history?; - let history_target = native_field(&history, "move")?.getattr("target_name")?; + let history_target = native_field(&native_field(&history, "move")?, "target_name")?; if !history_target.is_none() && !history_target.eq(&target)? { continue; } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/old_name_references.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/old_name_references.rs index 29a95213ef..73660a9ae4 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/old_name_references.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/old_name_references.rs @@ -7,9 +7,9 @@ use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::native_host::user_code::UserCodeHost; use crate::bindings::_helpers::planner_migrations::column_objects::{native, text}; use crate::bindings::_helpers::planner_phases::objects::object; +use crate::bindings::_helpers::records::construct::enum_member; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::references::relations::match_project_relation; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; use pyo3::types::{PyDict, PyDictMethods, PyStringMethods, PyTuple}; @@ -53,9 +53,10 @@ pub(super) fn errors<'py>( entries: &Bound<'py, PyAny>, ) -> PyResult>> { let py = runtime.py(); - let literals = runtime - .getattr("project")? - .getattr("unmatched_literal_sql_relations")?; + let literals = native_field( + &native_field(runtime, "project")?, + "unmatched_literal_sql_relations", + )?; if !literals.is_truthy()? { return Ok(Vec::new()); } @@ -100,7 +101,7 @@ pub(super) fn errors<'py>( } let mut parts: Vec = Vec::new(); for field in ["database", "schema", "name"] { - let part = relation.getattr(field)?; + let part = native_field(&relation, field)?; if part.is_truthy()? { parts.push(part.extract()?); } @@ -120,8 +121,7 @@ pub(super) fn errors<'py>( &native_field(&matched, "ref")?, None, )?; - let path = literal - .getattr("relative_path")? + let path = native_field(&literal, "relative_path")? .str()? .to_str()? .to_owned(); @@ -133,7 +133,7 @@ pub(super) fn errors<'py>( ("model_name", py.None().into_bound(py)), ( "severity", - registry::class(py, RecordId::WarningSeverity)?.getattr("ERROR")?, + enum_member(py, RecordId::WarningSeverity, "ERROR")?, ), ( "message", diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/old_names.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/old_names.rs index 5925e19d1a..111e2ddea5 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/old_names.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/old_names.rs @@ -1,13 +1,13 @@ //! Existing old-name planner models at the native decision boundary. use crate::bindings::_helpers::analysis::model_reads; -use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; +use crate::bindings::_helpers::compile_driver::python_calls::KeywordArguments; use crate::bindings::_helpers::native_host::adapter::AdapterHost; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::planner_migrations::old_name_objects::relation; +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::ids::RecordId; -use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; use pyo3::types::{PyDict, PySet, PyTuple}; use pyo3::{IntoPyObject, pyfunction, wrap_pyfunction}; @@ -136,13 +136,14 @@ pub(crate) fn old_name_claim_errors<'py>( } let message = claim_message(&name.extract::()?, &view, &arguments.value("now")?)?; - errors.push(call_with( - ®istry::class(py, RecordId::PlanWarning)?, + errors.push(new_record_with( + py, + RecordId::PlanWarning, &[ ("model_name", name.clone()), ( "severity", - registry::class(py, RecordId::WarningSeverity)?.getattr("ERROR")?, + enum_member(py, RecordId::WarningSeverity, "ERROR")?, ), ("message", message.into_pyobject(py)?.into_any()), ("code", "M114".into_pyobject(py)?.into_any()), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/planning_boundary.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/planning_boundary.rs index 1b846482ba..b3e1d78400 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/planning_boundary.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/planning_boundary.rs @@ -36,7 +36,7 @@ pub(super) fn redirected_cursor_snapshots<'py>( let start = if config.is_none() { py.None().into_bound(py) } else { - native_field(&config, "cursors")?.getattr("start")? + native_field(&native_field(&config, "cursors")?, "start")? }; let cursor_scope = native( py, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/planning_execution.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/planning_execution.rs index b507dc28d1..d6ccd21cd0 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/planning_execution.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/planning_execution.rs @@ -5,9 +5,9 @@ use crate::bindings::_helpers::native_host::adapter::AdapterHost; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::planner_migrations::column_objects::replace; use crate::bindings::_helpers::planner_phases::runtime::runtime_field; +use crate::bindings::_helpers::records::construct::record_class; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; use pyo3::types::PyBool; @@ -25,7 +25,7 @@ fn migration_promotion<'py>( decision: &Bound<'py, PyAny>, ) -> PyResult> { let adapter = runtime_field(runtime, |runtime| &runtime.adapter)?; - let promotion = registry::class(runtime.py(), RecordId::MigrationPromotion)?; + let promotion = record_class(runtime.py(), RecordId::MigrationPromotion)?; if adapter .call_method0("supports_transactional_ddl")? .is_truthy()? @@ -49,11 +49,11 @@ pub(super) fn migration_with_execution<'py>( let transient = if native_field(&adapter, "adapter_name")?.eq("snowflake")? { PyBool::new( py, - model - .getattr("config")? - .getattr("table_type")? - .getattr("value")? - .eq("transient")?, + native_field( + &native_field(&native_field(model, "config")?, "table_type")?, + "value", + )? + .eq("transient")?, ) .to_owned() .into_any() diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/planning_origin.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/planning_origin.rs index a669f815d0..4b325ae02c 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/planning_origin.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/planning_origin.rs @@ -4,6 +4,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::call_with; use crate::bindings::_helpers::planner_migrations::constants::QUALIFIED_RELATION_MAX_PARTS; use crate::bindings::_helpers::planner_migrations::models::ModelMigrationRequest; use crate::bindings::_helpers::planner_phases::runtime::runtime_field; +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::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -16,17 +17,12 @@ pub(super) fn location<'py>( fields: &[(&str, Bound<'py, PyAny>)], ) -> PyResult> { let qualified = call_with( - &runtime - .getattr("adapter")? - .getattr("render_qualified_name")?, + &native_field(&native_field(runtime, "adapter")?, "render_qualified_name")?, fields, )?; let mut fields = fields.to_vec(); fields.push(("qualified_name", qualified)); - call_with( - ®istry::class(runtime.py(), RecordId::CompiledRelationLocation)?, - &fields, - ) + new_record_with(runtime.py(), RecordId::CompiledRelationLocation, &fields) } fn named<'py>( @@ -35,9 +31,11 @@ fn named<'py>( runtime: &Bound<'py, PyAny>, state: &Bound<'py, PyAny>, ) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> { - for item in runtime_field(runtime, |runtime| &runtime.project)? - .getattr("models")? - .try_iter()? + for item in native_field( + &runtime_field(runtime, |runtime| &runtime.project)?, + "models", + )? + .try_iter()? { let item = item?; if native_field(&item, "name")?.eq(name)? { @@ -76,7 +74,7 @@ pub(super) fn planning_database<'py>(runtime: &Bound<'py, PyAny>) -> PyResult(runtime: &Bound<'py, PyAny>) -> PyResult(runtime: &Bound<'py, PyAny>) -> PyResult> { let schemas = PySet::empty(runtime.py())?; - for item in runtime_field(runtime, |runtime| &runtime.project)? - .getattr("models")? - .try_iter()? + for item in native_field( + &runtime_field(runtime, |runtime| &runtime.project)?, + "models", + )? + .try_iter()? { - let schema = native_field(&item?, "destination")?.getattr("schema")?; + let schema = native_field(&native_field(&item?, "destination")?, "schema")?; if !schema.is_none() { schemas.add(schema)?; } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/projections.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/projections.rs index 1494a50e7f..45c6f43720 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/projections.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/projections.rs @@ -1,10 +1,10 @@ //! Adapt existing Python query shape models to pure migration proofs. -use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; +use crate::bindings::_helpers::compile_driver::python_calls::KeywordArguments; use crate::bindings::_helpers::planner_migrations::errors::ProofError; +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; -use crate::bindings::_helpers::records::registry; use pyo3::exceptions::PyValueError; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; use pyo3::types::{PyDict, PyFrozenSet, PyTuple}; @@ -54,8 +54,9 @@ pub(crate) fn parse_query_shape<'py>( }; let mut projections: Vec> = Vec::new(); for item in shape.projections { - projections.push(call_with( - ®istry::class(py, RecordId::QueryProjection)?, + projections.push(new_record_with( + py, + RecordId::QueryProjection, &[ ("name", item.name.into_pyobject(py)?.into_any()), ("expression", item.expression.into_pyobject(py)?.into_any()), @@ -74,8 +75,9 @@ pub(crate) fn parse_query_shape<'py>( Some(relations) => PyTuple::new(py, relations)?.into_any(), None => py.None().into_bound(py), }; - call_with( - ®istry::class(py, RecordId::QueryShape)?, + new_record_with( + py, + RecordId::QueryShape, &[ ("projections", PyTuple::new(py, projections)?.into_any()), ("body", shape.body.into_pyobject(py)?.into_any()), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/state_inspection.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/state_inspection.rs index 470131e94e..ebbc7073c1 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/state_inspection.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/state_inspection.rs @@ -110,8 +110,7 @@ fn fingerprint_key(fingerprint: &Bound<'_, PyAny>) -> PyResult { }; Ok(FingerprintKey { order: FingerprintOrder { - timestamp: fingerprint - .getattr("ts")? + timestamp: native_field(fingerprint, "ts")? .call_method0("isoformat")? .extract()?, run_id: native_field(fingerprint, "run_id")?.extract()?, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/execution_changes.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/execution_changes.rs index fa39d71c99..8adedd54f4 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/execution_changes.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/execution_changes.rs @@ -60,7 +60,7 @@ fn config_or<'py>( } let mut value = config.clone(); for name in path { - value = value.getattr(*name)?; + value = native_field(&value, name)?; } Ok(value) } @@ -83,9 +83,10 @@ pub(crate) fn build_planner_entry_results<'py>( deferral: deferral.clone(), project_config: config.clone(), local_config: runtime_field(runtime, |runtime| &runtime.local_config)?, - known_source_columns: warehouse - .getattr("inspection_relations")? - .getattr("source_warehouse_columns")?, + known_source_columns: native_field( + &native_field(warehouse, "inspection_relations")?, + "source_warehouse_columns", + )?, relation_keys: py.None().into_bound(py), })?; let propagation = native_field(source_freshness, "propagation")?; @@ -104,10 +105,10 @@ pub(crate) fn build_planner_entry_results<'py>( let limit_action = if config.is_none() { default_microbatch_limit_action(py)? } else { - config - .getattr("microbatches")? - .getattr("limits")? - .getattr("action")? + native_field( + &native_field(&native_field(&config, "microbatches")?, "limits")?, + "action", + )? }; let build_inputs = Bound::new( py, @@ -115,8 +116,7 @@ pub(crate) fn build_planner_entry_results<'py>( source_freshness_blocked_model_names: blocked.unbind(), external_blocked_model_names: PyFrozenSet::new( py, - overrides - .getattr("external_blocked_model_names")? + native_field(overrides, "external_blocked_model_names")? .try_iter()? .collect::>>()?, )? diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/full_refresh.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/full_refresh.rs index 5f1d7d43c8..7fca1039bc 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/full_refresh.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/full_refresh.rs @@ -11,10 +11,7 @@ use sqlbuild_analysis::planner_warehouse::main::full_rebuild_cause::full_rebuild use sqlbuild_analysis::planner_warehouse::models::FullRebuildFacts; fn configured_full_refresh<'py>(model: &Bound<'py, PyAny>) -> PyResult> { - model - .getattr("config")? - .getattr("values")? - .call_method1("get", ("full_refresh",)) + native_field(&native_field(model, "config")?, "values")?.call_method1("get", ("full_refresh",)) } /// dbt-compatible nullable model full-refresh semantics. @@ -56,8 +53,7 @@ pub(crate) fn describe_full_rebuild_cause( ) -> PyResult { Ok(full_rebuild_cause(&FullRebuildFacts { full_refresh, - change_kind: change_result - .getattr("change_kind")? + change_kind: native_field(change_result, "change_kind")? .str()? .extract::()?, query_changed: native_field(change_result, "query_changed")?.is_truthy()?, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/identities.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/identities.rs index be23229cb2..14fb5e8c77 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/identities.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/identities.rs @@ -108,7 +108,7 @@ fn reusable_model_names( execution .getattr(field)? .call_method1("get", (&name,))? - .eq(stale.getattr(field)?.call_method1("get", (&name,))?) + .eq(native_field(&stale, field)?.call_method1("get", (&name,))?) }; if same("model_version_hashes")? && same("model_metadata_jsons")? { names.insert(name.extract()?); @@ -158,15 +158,11 @@ pub(crate) fn detect_stale_warning_changes<'py>( let reusable = reusable_model_names( identities, execution_changes, - project - .getattr("settings")? - .getattr("query_change_tracking")? - .is_truthy()?, + native_field(&native_field(project, "settings")?, "query_change_tracking")?.is_truthy()?, )?; let stale_scope = class_field(scopes, |s: &PlannerScopeResolution| &s.stale_warning_scope)?; let mut remaining: Vec<_> = Vec::new(); - for key in stale_scope - .getattr("all_keys")? + for key in native_field(&stale_scope, "all_keys")? .call_method0("values")? .try_iter()? { @@ -205,8 +201,7 @@ pub(crate) fn detect_stale_warning_changes<'py>( ), )?; let models = PyDict::new(py); - for (name, change) in execution_changes - .getattr("models")? + for (name, change) in native_field(execution_changes, "models")? .downcast::()? .iter() { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/objects.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/objects.rs index e1198f52f9..c66284f8e2 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/objects.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/objects.rs @@ -1,11 +1,13 @@ //! Boundary helpers shared by the native planner phases. -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported, keywords}; +use crate::bindings::_helpers::compile_driver::python_calls::{call_with, keywords}; use crate::bindings::_helpers::model_config::config_values::config_str as authored_config_str; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::native_host::models::HostId; +use crate::bindings::_helpers::records::construct::{enum_member, new_record, new_record_with}; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{ErrorId, NativeItem, RecordId}; +use crate::bindings::_helpers::records::fields::replaced_keywords; +use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyErr, PyResult, Python}; use pyo3::types::{PyFrozenSet, PyString, PyTuple}; @@ -36,16 +38,16 @@ pub(crate) fn planner_model<'py>( name: RecordId, fields: &[(&str, Bound<'py, PyAny>)], ) -> PyResult> { - call_with(®istry::class(py, name)?, fields) + new_record_with(py, name, fields) } -/// `sqlbuild._native.name(**fields)`. +/// The native record `name(**fields)`. pub(crate) fn native<'py>( py: Python<'py>, - name: impl Into, + name: RecordId, fields: &[(&str, Bound<'py, PyAny>)], ) -> PyResult> { - call_with(®istry::item(py, name)?, fields) + new_record_with(py, name, fields) } /// `dataclasses.replace(value, **fields)`. @@ -53,8 +55,7 @@ pub(crate) fn replace<'py>( value: &Bound<'py, PyAny>, fields: &[(&str, Bound<'py, PyAny>)], ) -> PyResult> { - imported(value.py(), "dataclasses", "replace")? - .call((value,), Some(&keywords(value.py(), fields)?)) + replaced_keywords(value, &keywords(value.py(), fields)?) } /// A staying-Python enum member from its value. @@ -72,7 +73,7 @@ pub(crate) fn native_member<'py>( class: RecordId, value: &str, ) -> PyResult> { - registry::class(py, class)?.call1((value,)) + new_record(py, class, (value,), None) } /// `PlannerInputError(message, code=code, help=help)`. @@ -148,7 +149,7 @@ pub(crate) fn by_name<'py>( field: &str, ) -> PyResult> { let nodes = pyo3::types::PyDict::new(project.py()); - for node in project.getattr(field)?.try_iter()? { + for node in native_field(project, field)?.try_iter()? { let node = node?; pyo3::types::PyDictMethods::set_item(&nodes, native_field(&node, "name")?, node)?; } @@ -157,9 +158,12 @@ pub(crate) fn by_name<'py>( /// A model config value as text, `None` when absent or not text. pub(crate) fn config_str(model: &Bound<'_, PyAny>, key: &str) -> PyResult> { - authored_config_str(&native_field(model, "config")?.getattr("values")?, key)? - .map(|value| value.extract()) - .transpose() + authored_config_str( + &native_field(&native_field(model, "config")?, "values")?, + key, + )? + .map(|value| value.extract()) + .transpose() } /// The compiled object key of the model `name`. @@ -167,12 +171,13 @@ pub(crate) fn model_key<'py>( py: Python<'py>, name: &Bound<'py, PyAny>, ) -> PyResult> { - call_with( - ®istry::class(py, RecordId::CompiledObjectKey)?, + new_record_with( + py, + RecordId::CompiledObjectKey, &[ ( "resource_type", - registry::class(py, RecordId::CompiledResourceType)?.getattr("MODEL")?, + enum_member(py, RecordId::CompiledResourceType, "MODEL")?, ), ("name", name.clone()), ], diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/output_assembly.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/output_assembly.rs index 1b3478de46..422bf9f1eb 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/output_assembly.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/output_assembly.rs @@ -1,7 +1,6 @@ //! Plan output assembly: storage policies, warnings and direct-mode metadata. use crate::bindings::_helpers::analysis::model_reads; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; use crate::bindings::_helpers::compile_driver::python_calls::keywords; use crate::bindings::_helpers::planner_entries::models::PlanOutputInputs; use crate::bindings::_helpers::planner_entries::plan_output::plan_output_fields; @@ -21,9 +20,9 @@ use crate::bindings::_helpers::planner_phases::retention::{plan_retention, plan_ use crate::bindings::_helpers::planner_phases::runtime::runtime_field; use crate::bindings::_helpers::planner_phases::runtime::warehouse_field; use crate::bindings::_helpers::planner_warehouse::source_freshness::source_freshness_unknown_warnings; +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; -use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; use pyo3::pyfunction; use pyo3::types::{PyBool, PyDict, PyDictMethods, PyTuple}; @@ -63,7 +62,7 @@ impl<'py> PlanOutputFields<'py> { } self.set("metadata", metadata.into_any()); } - call_with(®istry::class(py, RecordId::PlanOutput)?, &self.fields) + new_record_with(py, RecordId::PlanOutput, &self.fields) } } @@ -172,14 +171,13 @@ pub(crate) fn add_storage_policies<'py>( ) -> PyResult> { let py = runtime.py(); let mut entries = output.field("model_entries")?; - if policies - .getattr("record_migration_fingerprints")? - .is_truthy()? - { + if native_field(policies, "record_migration_fingerprints")?.is_truthy()? { let models = PyDict::new(py); - for model in runtime_field(runtime, |runtime| &runtime.project)? - .getattr("models")? - .try_iter()? + for model in native_field( + &runtime_field(runtime, |runtime| &runtime.project)?, + "models", + )? + .try_iter()? { let model = model?; models.set_item(model_reads::name_object(&model)?, model)?; @@ -201,8 +199,7 @@ pub(crate) fn add_storage_policies<'py>( ), ( "dialect", - runtime - .getattr("adapter")? + native_field(runtime, "adapter")? .call_method0("sql_analysis_dialect")?, ), ], @@ -333,11 +330,10 @@ fn freshness_metadata<'py>(source_freshness: &Bound<'py, PyAny>) -> PyResult = Vec::new(); - for unknown in source_freshness - .getattr("unknown_sources")? + for unknown in native_field(source_freshness, "unknown_sources")? .call_method0("values")? .try_iter()? { @@ -346,7 +342,7 @@ fn freshness_metadata<'py>(source_freshness: &Bound<'py, PyAny>) -> PyResult( let py = runtime.py(); let project_config = runtime_field(runtime, |runtime| &runtime.project_config)?; let local_config = runtime_field(runtime, |runtime| &runtime.local_config)?; - let target_name = runtime - .getattr("project")? - .getattr("effective_target_name")?; + let target_name = native_field(&native_field(runtime, "project")?, "effective_target_name")?; if project_config.is_none() || local_config.is_none() || target_name.is_none() { return native(py, RecordId::TargetConfig, &[]); } @@ -59,9 +57,11 @@ fn selected_models<'py>( .into_iter() .collect(); let mut models: Vec<_> = Vec::new(); - for model in runtime_field(runtime, |runtime| &runtime.project)? - .getattr("models")? - .try_iter()? + for model in native_field( + &runtime_field(runtime, |runtime| &runtime.project)?, + "models", + )? + .try_iter()? { let model = model?; if names.contains(&model_reads::model_name(&model)?) { @@ -72,10 +72,7 @@ fn selected_models<'py>( } fn adapter_is(runtime: &Bound<'_, PyAny>, name: &str) -> PyResult { - runtime - .getattr("adapter")? - .getattr("adapter_name")? - .eq(name) + native_field(&native_field(runtime, "adapter")?, "adapter_name")?.eq(name) } /// Entries for selected Snowflake table-type drift. @@ -91,11 +88,11 @@ pub(crate) fn plan_table_types<'py>( let snowflake = adapter_is(runtime, SNOWFLAKE_ADAPTER)?; let mut declared: Vec<_> = Vec::new(); for model in &models { - if model - .getattr("config")? - .getattr("table_type")? - .getattr("declared")? - .is_truthy()? + if native_field( + &native_field(&native_field(model, "config")?, "table_type")?, + "declared", + )? + .is_truthy()? { declared.push(model_reads::model_name(model)?); } @@ -114,11 +111,9 @@ pub(crate) fn plan_table_types<'py>( return tuple(py, Vec::new()); } let target = effective_target_config(runtime)?; - let relations = warehouse - .getattr("snapshot")? - .getattr("existing_relations")?; + let relations = native_field(&native_field(warehouse, "snapshot")?, "existing_relations")?; let adapter = runtime_field(runtime, |runtime| &runtime.adapter)?; - let materialization = registry::class(py, RecordId::MaterializationType)?; + let materialization = record_class(py, RecordId::MaterializationType)?; let mut entries: Vec<_> = Vec::new(); for model in models { let config = native_field(&model, "config")?; @@ -162,7 +157,7 @@ pub(crate) fn plan_table_types<'py>( ("model_name", model_reads::name_object(&model)?), ("destination", destination), ("copy_name", text(py, ©_name(&target_name, limit))), - ("desired_type", native_field(&table_type, "value")?.getattr("value")?), + ("desired_type", native_field(&native_field(&table_type, "value")?, "value")?), ( "actual_type", optional_text( @@ -176,11 +171,11 @@ pub(crate) fn plan_table_types<'py>( }), ), ), - ("source", native_field(&table_type, "source")?.getattr("value")?), + ("source", native_field(&native_field(&table_type, "source")?, "value")?), ("downgrade", downgrade.into_pyobject(py)?.to_owned().into_any()), ( "downgrade_policy", - native_field(&target, "table_type_downgrade")?.getattr("value")?, + native_field(&native_field(&target, "table_type_downgrade")?, "value")?, ), ( "irreversible_warning", @@ -215,9 +210,10 @@ fn entry<'py>( ) -> PyResult> { let py = base.request.py(); let days = |name: &str| -> PyResult> { - base.state - .as_ref() - .map_or_else(|| Ok(py.None().into_bound(py)), |state| state.getattr(name)) + base.state.as_ref().map_or_else( + || Ok(py.None().into_bound(py)), + |state| native_field(state, name), + ) }; planner_model( py, @@ -268,9 +264,10 @@ fn render_changes<'py>( fields.push(("state", state.clone())); } call_with( - &runtime - .getattr("adapter")? - .getattr("render_retention_changes")?, + &native_field( + &native_field(runtime, "adapter")?, + "render_retention_changes", + )?, &fields, ) } @@ -295,7 +292,7 @@ fn inspect<'py>( } fn state_direction(state: &Bound<'_, PyAny>, desired_days: i64) -> PyResult<&'static str> { - let optional = |name: &str| -> PyResult> { state.getattr(name)?.extract() }; + let optional = |name: &str| -> PyResult> { native_field(state, name)?.extract() }; Ok(retention_direction( [ optional("delta_log_retention_days")?, @@ -363,13 +360,11 @@ fn relation_retention<'py>( request: request.clone(), model_names: tuple(py, vec![model_reads::name_object(model)?])?, state: None, - source: native_field(&retention, "source")?.getattr("value")?, + source: native_field(&native_field(&retention, "source")?, "value")?, }; let snowflake = adapter_is(runtime, SNOWFLAKE_ADAPTER)?; - let table_type = native_field(&config, "table_type")?.getattr("value")?; - let relations = warehouse - .getattr("snapshot")? - .getattr("existing_relations")?; + let table_type = native_field(&native_field(&config, "table_type")?, "value")?; + let relations = native_field(&native_field(warehouse, "snapshot")?, "existing_relations")?; if !relations.contains(model_reads::name_object(model)?)? { if snowflake && desired_days > 1 && table_type.eq(TRANSIENT_TABLE_TYPE)? { return Err(transient_error(py, &name, desired_days, "set ")); @@ -394,9 +389,10 @@ fn relation_retention<'py>( let mut listed = py.None().into_bound(py); if native_field(&relation, "identity")?.eq(&lookup)? { listed = call_with( - &runtime - .getattr("adapter")? - .getattr("retention_state_from_relation")?, + &native_field( + &native_field(runtime, "adapter")?, + "retention_state_from_relation", + )?, &[("request", request.clone()), ("relation", relation.clone())], )?; } @@ -457,10 +453,7 @@ fn bigquery_retention<'py>( ) -> PyResult>> { let py = runtime.py(); let target = effective_target_config(runtime)?; - if !target - .getattr("owns_time_travel_retention_namespace")? - .is_truthy()? - { + if !native_field(&target, "owns_time_travel_retention_namespace")?.is_truthy()? { return Err(planner_error( py, "BigQuery managed time_travel_retention requires target owns_time_travel_retention_namespace = true", @@ -469,12 +462,14 @@ fn bigquery_retention<'py>( } type Scope = (Option, String); let mut managed: BTreeMap>> = BTreeMap::new(); - for model in runtime_field(runtime, |runtime| &runtime.project)? - .getattr("models")? - .try_iter()? + for model in native_field( + &runtime_field(runtime, |runtime| &runtime.project)?, + "models", + )? + .try_iter()? { let model = model?; - let retention = native_field(&model, "config")?.getattr("time_travel_retention")?; + let retention = native_field(&native_field(&model, "config")?, "time_travel_retention")?; let destination = model_reads::destination(&model)?; if native_field(&retention, "unmanaged")?.is_truthy()? || native_field(&retention, "desired_days")?.is_none() @@ -508,7 +503,7 @@ fn bigquery_retention<'py>( let mut sources: BTreeSet = BTreeSet::new(); let mut names: Vec<_> = Vec::new(); for item in &scope_models { - let retention = native_field(item, "config")?.getattr("time_travel_retention")?; + let retention = native_field(&native_field(item, "config")?, "time_travel_retention")?; let desired = native_field(&retention, "desired_days")?; details.push(format!( "{}={}d", @@ -597,7 +592,7 @@ pub(crate) fn plan_retention<'py>( let py = runtime.py(); let mut models: Vec<_> = Vec::new(); for model in selected_models(runtime, scope)? { - let retention = native_field(&model, "config")?.getattr("time_travel_retention")?; + let retention = native_field(&native_field(&model, "config")?, "time_travel_retention")?; if !native_field(&retention, "unmanaged")?.is_truthy()? && !native_field(&retention, "desired_days")?.is_none() { @@ -616,9 +611,13 @@ pub(crate) fn plan_retention<'py>( } entries }; - let policy = effective_target_config(runtime)? - .getattr("time_travel_retention_decrease")? - .getattr("value")?; + let policy = native_field( + &native_field( + &effective_target_config(runtime)?, + "time_travel_retention_decrease", + )?, + "value", + )?; let mut decided: Vec<_> = Vec::new(); for entry in entries { decided.push(replace(&entry, &[("decrease_policy", policy.clone())])?); diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/warehouse_state.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/warehouse_state.rs index 9784b71f37..ae15c80b06 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/warehouse_state.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/warehouse_state.rs @@ -73,7 +73,7 @@ pub(crate) fn gather_planner_warehouse_state<'py>( if config.is_none() { py.None().into_bound(py) } else { - native_field(&config, "cursors")?.getattr("start")? + native_field(&native_field(&config, "cursors")?, "start")? }, ), ( @@ -85,11 +85,13 @@ pub(crate) fn gather_planner_warehouse_state<'py>( let adapter = runtime_field(runtime, |runtime| &runtime.adapter)?; let stale_keys = pyo3::types::PyFrozenSet::new( py, - class_field(scopes, |s: &PlannerScopeResolution| &s.stale_warning_scope)? - .getattr("all_keys")? - .call_method0("values")? - .try_iter()? - .collect::>>()?, + native_field( + &class_field(scopes, |s: &PlannerScopeResolution| &s.stale_warning_scope)?, + "all_keys", + )? + .call_method0("values")? + .try_iter()? + .collect::>>()?, )? .into_any(); let snapshot = warehouse_snapshot(&SnapshotRequest { @@ -126,11 +128,7 @@ pub(crate) fn gather_planner_warehouse_state<'py>( })?; let physical = PyDict::new(py); for entry in &migrations.entries { - if entry - .getattr("decision")? - .getattr("moves_data")? - .is_truthy()? - { + if native_field(&native_field(entry, "decision")?, "moves_data")?.is_truthy()? { let origin = native_field(entry, "origin")?; let qualified = native_field(&origin, "qualified_name")?; physical.set_item( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_bounds.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_bounds.rs index f7ebb34e6a..b340482346 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_bounds.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_bounds.rs @@ -1,16 +1,16 @@ //! Snapshot conversion and cursor window policy boundary. +use crate::bindings::_helpers::compile_driver::python_calls::KeywordArguments; use crate::bindings::_helpers::compile_driver::python_calls::keywords; -use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; use crate::bindings::_helpers::cursor::conversion::{CursorSession, algebra_error}; use crate::bindings::_helpers::cursor::scalars::try_parse; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::planner_resolution::maximum_safety::apply_maximum_start_policy; use crate::bindings::_helpers::planner_resolution::values::string; +use crate::bindings::_helpers::records::construct::{new_record, new_record_with, record_class}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; use pyo3::types::PyDict; use pyo3::{IntoPyObject, pyfunction, wrap_pyfunction}; @@ -71,7 +71,7 @@ pub(super) fn duration(value: &Bound<'_, PyAny>) -> PyResult) -> PyResult PyResult { parsed.getattr(name)?.extract() }; + let field = |name: &str| -> PyResult { native_field(&parsed, name)?.extract() }; Ok(Some(DurationSpan { months: field("years")? * 12 + field("months")?, days: field("days")?, @@ -193,11 +193,11 @@ pub(crate) fn compute_cursor_bounds<'py>( &["cursor_grain", "maximum_start_policy"], ), )?; - let bounds_class = registry::class(py, RecordId::CursorBounds)?; if args.flag("is_microbatch")? { let sentinel = host::entry(py, ExecutorHost::BoundSentinel)?; - return call_with( - &bounds_class, + return new_record_with( + py, + RecordId::CursorBounds, &[ ("start", native_field(&sentinel, "START")?), ("end", native_field(&sentinel, "END")?), @@ -261,15 +261,16 @@ pub(crate) fn compute_cursor_bounds<'py>( ), ]); let replayed = apply_typed_cursor_replay_policy(py, Some(keywords(py, &replay_args)?))?; - let bounds = call_with( - &bounds_class, + let bounds = new_record_with( + py, + RecordId::CursorBounds, &[("start", replayed), ("end", session.python_scalar(&end)?)], )?; let policy = args.value("maximum_start_policy")?; let policy = if policy.is_truthy()? { policy } else { - registry::class(py, RecordId::MaximumStartPolicyInputs)?.call0()? + new_record(py, RecordId::MaximumStartPolicyInputs, (), None)? }; let mut policy_args = vec![ ("bounds", bounds), 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 7326f8870f..98c8ccb4f9 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 @@ -27,6 +27,7 @@ use crate::bindings::_helpers::planner_resolution::cursor_windows::without_desti use crate::bindings::_helpers::planner_resolution::future_safety::resolve_future_cursor_safety; use crate::bindings::_helpers::planner_resolution::models::ModelSqlRequest; use crate::bindings::_helpers::planner_resolution::values::string; +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::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -34,7 +35,7 @@ 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(model, "config")?.getattr("values")?; + let values: Bound<'py, PyAny> = native_field(&native_field(model, "config")?, "values")?; let value: Option> = if key == CURSOR_START { config_cursor_bound(&values, key)? } else { @@ -45,8 +46,9 @@ fn config<'py>(model: &Bound<'py, PyAny>, key: &str) -> PyResult) -> PyResult> { let sentinel = host::entry(py, ExecutorHost::BoundSentinel)?; - call_with( - ®istry::class(py, RecordId::CursorBounds)?, + new_record_with( + py, + RecordId::CursorBounds, &[ ("start", native_field(&sentinel, "START")?), ("end", native_field(&sentinel, "END")?), @@ -55,8 +57,7 @@ fn unbounded(py: Python<'_>) -> PyResult> { } fn destination_exists(model: &Bound<'_, PyAny>, snapshot: &Bound<'_, PyAny>) -> PyResult { - let relation = snapshot - .getattr("existing_relations")? + let relation = native_field(snapshot, "existing_relations")? .call_method1("get", (model_reads::name_object(model)?,))?; if relation.is_none() { return Ok(false); @@ -120,8 +121,7 @@ pub(crate) fn compute_model_cursor_bounds<'py>( return Ok(bounded); } let snapshot = &request.snapshot; - let mut cursor_snapshot = snapshot - .getattr("cursor_snapshots")? + let mut cursor_snapshot = native_field(snapshot, "cursor_snapshots")? .call_method1("get", (model_reads::name_object(&model)?,))?; let roles = resolve_cursor_input_roles(&model)?; let producers = class_field(context, |c: &ModelPlanContext| { @@ -166,10 +166,7 @@ pub(crate) fn compute_model_cursor_bounds<'py>( if empty.is_truthy()? { return empty_cursor_window(&cursor_type, &config(&model, "cursor_start")?); } - if !cursor_snapshot - .getattr("watermarks_available")? - .is_truthy()? - { + if !native_field(&cursor_snapshot, "watermarks_available")?.is_truthy()? { let unreadable = native_field(&cursor_snapshot, "unreadable_input_names")?; let names = if unreadable.is_truthy()? { unreadable @@ -202,8 +199,9 @@ pub(crate) fn compute_model_cursor_bounds<'py>( } else { py.None().into_bound(py) }; - let policy = call_with( - ®istry::class(py, RecordId::MaximumStartPolicyInputs)?, + let policy = new_record_with( + py, + RecordId::MaximumStartPolicyInputs, &[ ("config", start_config), ( 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 585e1fd5c5..fc58ce0f19 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 @@ -4,6 +4,7 @@ use crate::bindings::_helpers::analysis::model_reads; use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; use crate::bindings::_helpers::model_config::config_values::config_str as authored_config_str; use crate::bindings::_helpers::planner_resolution::values::{method, string}; +use crate::bindings::_helpers::records::construct::enum_from_value; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -47,7 +48,7 @@ pub(crate) fn type_cursor_overrides<'py>( } let mut models: Vec<(String, String)> = Vec::new(); for model in selected { - let values = native_field(model, "config")?.getattr("values")?; + 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))) }; @@ -62,7 +63,7 @@ pub(crate) fn type_cursor_overrides<'py>( let mut integer_literals = true; if models.is_empty() { for field in ["start", "end"] { - let value = overrides.getattr(field)?; + let value = native_field(overrides, field)?; if value.is_none() { continue; } @@ -81,7 +82,7 @@ pub(crate) fn type_cursor_overrides<'py>( ("help", string(py, "select models with one cursor type, or set each type explicitly with --start-cursor-ts/--end-cursor-ts and --start-cursor-int/--end-cursor-int")), ])) { Ok(error) => PyErr::from_value(error), Err(error) => error } })?; - let kind = registry::class(py, RecordId::CursorType)?.call1((kind,))?; + let kind = enum_from_value(py, RecordId::CursorType, kind)?; method(overrides, "typed_as", &[("cursor_type", kind)]) } 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 4756bff2a7..d0a1c85447 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::compile_driver::python_calls::call_with; +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::ids::RecordId; -use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult}; use pyo3::types::PyString; use pyo3::{pyfunction, wrap_pyfunction}; @@ -38,13 +38,13 @@ fn resolve_policy<'py>( fields: &PolicyFields, ) -> PyResult> { let py = model.py(); - let class = registry::class(py, fields.class)?; + let class = record_class(py, fields.class)?; let configured = if project_config.is_truthy()? { project_config.clone() } else { class.call0()? }; - let values = native_field(model, "config")?.getattr("values")?; + let values = native_field(&native_field(model, "config")?, "values")?; let raw_distance = values.call_method1("get", (fields.distance_key,))?; let raw_action = values.call_method1("get", (fields.action_key,))?; let mut distance = configured.getattr(fields.distance)?; @@ -57,7 +57,7 @@ fn resolve_policy<'py>( } let mut action = native_field(&configured, "action")?; if raw_action.is_instance_of::() { - action = registry::class(py, RecordId::FutureCursorAction)?.call1((raw_action,))?; + action = enum_from_value(py, RecordId::FutureCursorAction, raw_action)?; } if distance.is_none() { return Ok(py.None().into_bound(py)); 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 318f829f48..ec9d3d7352 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 @@ -29,6 +29,7 @@ use crate::bindings::_helpers::planner_resolution::cursor_windows::without_desti use crate::bindings::_helpers::planner_resolution::future_safety::resolve_future_cursor_safety; use crate::bindings::_helpers::planner_resolution::values::string; use crate::bindings::_helpers::planner_strategy::model_entries::resolve_microbatch_lookback; +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::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -81,10 +82,11 @@ 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(model, "config")?.getattr("values")?, key)? - .unwrap_or_else(|| model.py().None().into_bound(model.py())), - ) + Ok(authored_config_str( + &native_field(&native_field(model, "config")?, "values")?, + key, + )? + .unwrap_or_else(|| model.py().None().into_bound(model.py()))) } /// One cursor-bound model configuration entry as its token text. @@ -94,7 +96,10 @@ pub(crate) fn config_bound<'py>( ) -> PyResult> { object( model.py(), - config_cursor_bound(&native_field(model, "config")?.getattr("values")?, key)?, + config_cursor_bound( + &native_field(&native_field(model, "config")?, "values")?, + key, + )?, ) } @@ -171,8 +176,9 @@ fn timestamp_value<'py>(clock: &Bound<'py, PyAny>, scalar: &Scalar) -> PyResult< let ScalarValue::Timestamp(timestamp) = &scalar.value else { return Ok(clock.py().None().into_bound(clock.py())); }; - call_with( - ®istry::class(clock.py(), RecordId::TimestampValue)?, + new_record_with( + clock.py(), + RecordId::TimestampValue, &[("value", replaced(clock, ×tamp.local, timestamp.fold)?)], ) } @@ -223,8 +229,9 @@ fn rolling_window_bounds<'py>( handle: None, }; let (start, end) = session.checked(rolling_window(&clock, &span, grain))?; - let mut bounds = call_with( - ®istry::class(py, RecordId::CursorBounds)?, + let mut bounds = new_record_with( + py, + RecordId::CursorBounds, &[ ("start", timestamp_value(&moment, &start)?), ("end", timestamp_value(&moment, &end)?), @@ -313,8 +320,9 @@ fn snapshot_bounds<'py>( request: BoundsRequest<'py>, ) -> PyResult> { let py = model.py(); - let policy = call_with( - ®istry::class(py, RecordId::MaximumStartPolicyInputs)?, + let policy = new_record_with( + py, + RecordId::MaximumStartPolicyInputs, &[ ("config", inputs.policies.1.clone()), ("invocation_time", inputs.invocation_time.clone()), @@ -356,8 +364,7 @@ fn model_cursor_snapshot<'py>( model: &Bound<'py, PyAny>, snapshot: &Bound<'py, PyAny>, ) -> PyResult> { - snapshot - .getattr("cursor_snapshots")? + native_field(snapshot, "cursor_snapshots")? .call_method1("get", (model_reads::name_object(model)?,)) } @@ -544,11 +551,11 @@ pub(crate) fn model_cursor_overrides<'py>( None }; if let Some((start_field, end_field)) = typed_fields { - let typed_start = cursor_overrides.getattr(start_field)?; + let typed_start = native_field(cursor_overrides, start_field)?; if !typed_start.is_none() { resolved_start = token(py, &typed_start)?; } - let typed_end = cursor_overrides.getattr(end_field)?; + let typed_end = native_field(cursor_overrides, end_field)?; if !typed_end.is_none() { resolved_end = token(py, &typed_end)?; } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_safety.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_safety.rs index cde6947fd6..ce18e918e7 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_safety.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_safety.rs @@ -3,7 +3,9 @@ use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported, keywords}; use crate::bindings::_helpers::cursor::conversion::CursorSession; use crate::bindings::_helpers::planner_resolution::cursor_bounds::duration; +use crate::bindings::_helpers::records::construct::{new_record_with, record_class}; use crate::bindings::_helpers::records::fields::field as native_field; +use crate::bindings::_helpers::records::fields::replaced_keywords; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyErr, PyResult, Python}; @@ -44,10 +46,8 @@ pub(super) fn safety_duration(value: &Bound<'_, PyAny>, future: bool) -> PyResul pub(super) fn timestamp_bound(value: &Bound<'_, PyAny>) -> PyResult { let py = value.py(); - Ok( - value.is_instance(®istry::class(py, RecordId::DateValue)?)? - || value.is_instance(®istry::class(py, RecordId::TimestampValue)?)?, - ) + Ok(value.is_instance(&record_class(py, RecordId::DateValue)?)? + || value.is_instance(&record_class(py, RecordId::TimestampValue)?)?) } pub(super) fn utc_clock<'py>(clock: &Bound<'py, PyAny>) -> PyResult> { @@ -63,21 +63,14 @@ pub(super) fn horizon<'py>( let py = session.py(); let (clock, grain) = clock_and_grain; let mut clock = utc_clock(clock)?; - let date_only = discovered.is_instance(®istry::class(py, RecordId::DateValue)?)?; - if date_only - || native_field(discovered, "value")? - .getattr("tzinfo")? - .is_none() - { + let date_only = discovered.is_instance(&record_class(py, RecordId::DateValue)?)?; + if date_only || native_field(&native_field(discovered, "value")?, "tzinfo")?.is_none() { clock = call_with( &native_field(&clock, "replace")?, &[("tzinfo", py.None().into_bound(py))], )?; } - let clock = call_with( - ®istry::class(py, RecordId::TimestampValue)?, - &[("value", clock)], - )?; + let clock = new_record_with(py, RecordId::TimestampValue, &[("value", clock)])?; let grain = if grain.is_truthy()? { session.grain(grain)?.unwrap_or(Grain::Second) } else { @@ -95,6 +88,5 @@ pub(super) fn replace<'py>( object: &Bound<'py, PyAny>, fields: &[(&str, Bound<'py, PyAny>)], ) -> PyResult> { - imported(object.py(), "dataclasses", "replace")? - .call((object,), Some(&keywords(object.py(), fields)?)) + replaced_keywords(object, &keywords(object.py(), fields)?) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_warnings.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_warnings.rs index 905ac9abb2..a3e08a42b8 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_warnings.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_warnings.rs @@ -1,11 +1,10 @@ //! Prominent warnings for applied future-end and automatic-start cursor safety. -use crate::bindings::_helpers::compile_driver::python_calls::call_with; use crate::bindings::_helpers::cursor::scalars::render_value; use crate::bindings::_helpers::planner_resolution::values::string; +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::ids::RecordId; -use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult}; use pyo3::types::{PyString, PyTuple}; use pyo3::{pyfunction, wrap_pyfunction}; @@ -14,7 +13,7 @@ const FUTURE_CAPPED_CODE: &str = "FUTURE_CURSOR_CAPPED"; const START_CAPPED_CODE: &str = "AUTOMATIC_START_CAPPED"; fn rendered(evidence: &Bound<'_, PyAny>, field: &str) -> PyResult { - render_value(&evidence.getattr(field)?) + render_value(&native_field(evidence, field)?) } /// Render a prominent warning for applied future cursor policy. @@ -59,7 +58,7 @@ pub(crate) fn maximum_start_message(bounds: &Bound<'_, PyAny>) -> PyResult( (maximum_start_message(bounds)?, START_CAPPED_CODE), ] { let Some(message) = message else { continue }; - all.push(call_with( - ®istry::class(py, RecordId::PlanWarning)?, + all.push(new_record_with( + py, + RecordId::PlanWarning, &[ ("model_name", model_name.clone()), ( "severity", - registry::class(py, RecordId::WarningSeverity)?.getattr("WARNING")?, + enum_member(py, RecordId::WarningSeverity, "WARNING")?, ), ("code", string(py, code)), ("message", string(py, &message)), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_windows.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_windows.rs index bf4c2be6ea..df8eb54a4d 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_windows.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/cursor_windows.rs @@ -1,10 +1,12 @@ //! Empty-window decisions and snapshot grain conversion. -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; +use crate::bindings::_helpers::compile_driver::python_calls::call_with; use crate::bindings::_helpers::cursor::conversion::CursorSession; use crate::bindings::_helpers::cursor::scalars::parse_value; use crate::bindings::_helpers::planner_resolution::values::string; +use crate::bindings::_helpers::records::construct::{new_record, new_record_with}; use crate::bindings::_helpers::records::fields::field as native_field; +use crate::bindings::_helpers::records::fields::replaced_keywords; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; @@ -22,14 +24,14 @@ fn decide_empty_cursor_inputs<'py>( has_window_start: bool, ) -> PyResult> { let py = empty_input_names.py(); - let class = registry::class(py, RecordId::EmptyCursorInputDecision)?; if !empty_input_names.is_truthy()? || !empty_inputs(waits_for_every_input, inputs_with_rows, has_window_start) { - return class.call0(); + return new_record(py, RecordId::EmptyCursorInputDecision, (), None); } - call_with( - &class, + new_record_with( + py, + RecordId::EmptyCursorInputDecision, &[ ("input_names", empty_input_names.clone()), ( @@ -45,29 +47,23 @@ fn decide_empty_cursor_inputs<'py>( pub(crate) fn cursor_inputs_without_rows<'py>( cursor_snapshot: &Bound<'py, PyAny>, ) -> PyResult> { - if cursor_snapshot - .getattr("unreadable_input_names")? - .is_truthy()? - { - return registry::class(cursor_snapshot.py(), RecordId::EmptyCursorInputDecision)?.call0(); + if native_field(cursor_snapshot, "unreadable_input_names")?.is_truthy()? { + return new_record( + cursor_snapshot.py(), + RecordId::EmptyCursorInputDecision, + (), + None, + ); } let rows = native_field(cursor_snapshot, "upstream_maxes")?.is_truthy()? - || cursor_snapshot - .getattr("upstream_terminal_ends")? - .is_truthy()? - || cursor_snapshot - .getattr("upstream_availability_ends")? - .is_truthy()?; + || native_field(cursor_snapshot, "upstream_terminal_ends")?.is_truthy()? + || native_field(cursor_snapshot, "upstream_availability_ends")?.is_truthy()?; let start = !native_field(cursor_snapshot, "target_max")?.is_none() || native_field(cursor_snapshot, "upstream_mins")?.is_truthy()? - || cursor_snapshot - .getattr("upstream_terminal_starts")? - .is_truthy()?; + || native_field(cursor_snapshot, "upstream_terminal_starts")?.is_truthy()?; decide_empty_cursor_inputs( &native_field(cursor_snapshot, "empty_input_names")?, - !cursor_snapshot - .getattr("cursor_watermark_mode")? - .eq("any")?, + !native_field(cursor_snapshot, "cursor_watermark_mode")?.eq("any")?, rows, start, ) @@ -94,13 +90,15 @@ pub(crate) fn empty_cursor_window<'py>( }; let mut bound = parse_value(&raw, &kind)?; if cursor_start.is_none() && !kind.eq("integer")? { - bound = call_with( - ®istry::class(py, RecordId::TimestampValue)?, + bound = new_record_with( + py, + RecordId::TimestampValue, &[("value", native_field(&bound, "value")?)], )?; } - call_with( - ®istry::class(py, RecordId::CursorBounds)?, + new_record_with( + py, + RecordId::CursorBounds, &[("start", bound.clone()), ("end", bound)], ) } @@ -181,7 +179,7 @@ pub(crate) fn without_destination_cursor<'py>( for name in ["target_max", "physical_target_max", "target_eligible_max"] { kwargs.set_item(name, py.None())?; } - imported(py, "dataclasses", "replace")?.call((cursor_snapshot,), Some(&kwargs)) + replaced_keywords(cursor_snapshot, &kwargs) } #[pyfunction] @@ -240,7 +238,7 @@ pub(crate) fn normalize_cursor_snapshot_grain<'py>( }; let kwargs = PyDict::new(py); for key in ["target_max", "target_eligible_max"] { - kwargs.set_item(key, normalize(cursor_snapshot.getattr(key)?)?)?; + kwargs.set_item(key, normalize(native_field(cursor_snapshot, key)?)?)?; } for key in ["upstream_mins", "upstream_maxes"] { let values = cursor_snapshot @@ -265,7 +263,7 @@ pub(crate) fn normalize_cursor_snapshot_grain<'py>( ends.push((normalize(pair.get_item(0)?)?, pair.get_item(1)?)); } kwargs.set_item("upstream_end_inputs", PyTuple::new(py, ends)?)?; - imported(py, "dataclasses", "replace")?.call((cursor_snapshot,), Some(&kwargs)) + replaced_keywords(cursor_snapshot, &kwargs) } pub(super) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/future_safety.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/future_safety.rs index 9a1d12aa84..56c1be8f6d 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/future_safety.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/future_safety.rs @@ -5,9 +5,9 @@ use crate::bindings::_helpers::cursor::conversion::CursorSession; use crate::bindings::_helpers::planner_resolution::cursor_safety::{ horizon, replace, safety_duration, safety_error, timestamp_bound, utc_clock, }; +use crate::bindings::_helpers::records::construct::{new_record_with, record_class}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; -use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; use pyo3::types::{PyDict, PyTuple}; use pyo3::{IntoPyObject, pyfunction, wrap_pyfunction}; @@ -115,20 +115,21 @@ pub(crate) fn resolve_future_cursor_safety<'py>( }; let determining_field = |field: &str| -> PyResult> { if let Some((item, _)) = &determining { - item.getattr(field) + native_field(item, field) } else { Ok(py.None().into_bound(py)) } }; - let safety = call_with( - ®istry::class(py, RecordId::FutureCursorSafetyEvidence)?, + let safety = new_record_with( + py, + RecordId::FutureCursorSafetyEvidence, &[ ("action", native_field(&config, "action")?), ("max_distance", distance), ( "invocation_time", call_with( - ®istry::class(py, RecordId::TimestampValue)?, + &record_class(py, RecordId::TimestampValue)?, &[("value", utc_clock(&clock)?)], )?, ), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/maximum_safety.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/maximum_safety.rs index b376d39a61..776e136cfb 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/maximum_safety.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/maximum_safety.rs @@ -13,6 +13,7 @@ use crate::bindings::_helpers::planner_resolution::cursor_safety::{ horizon, replace, safety_duration, safety_error, timestamp_bound, utc_clock, }; use crate::bindings::_helpers::planner_resolution::values::string; +use crate::bindings::_helpers::records::construct::{new_record_with, record_class}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -184,15 +185,16 @@ pub(crate) fn apply_maximum_start_policy<'py>( }; let relation = native_field(&snapshot, "target_relation")?; let column = native_field(&snapshot, "destination_cursor_column")?; - let evidence = call_with( - ®istry::class(py, RecordId::MaximumStartSafetyEvidence)?, + let evidence = new_record_with( + py, + RecordId::MaximumStartSafetyEvidence, &[ ("action", native_field(&config, "action")?), ("max_ahead", native_field(&config, "max_ahead")?), ( "invocation_time", call_with( - ®istry::class(py, RecordId::TimestampValue)?, + &record_class(py, RecordId::TimestampValue)?, &[("value", utc_clock(&clock)?)], )?, ), @@ -274,7 +276,7 @@ fn exceeded_message(evidence: &Bound<'_, PyAny>, non_idempotent: bool) -> PyResu rendered(&native_field(evidence, "physical_target_max")?)?, rendered(&native_field(evidence, "effective_start")?)?, rendered(&native_field(evidence, "maximum_allowed_start")?)?, - native_field(evidence, "action")?.getattr("value")?.str()?, + native_field(&native_field(evidence, "action")?, "value")?.str()?, native_field(evidence, "target_relation")?.str()?, native_field(evidence, "cursor_column")?.str()?, )) 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 ab2abd45d0..41b40b77f5 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/orchestration.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/orchestration.rs @@ -4,9 +4,7 @@ use crate::bindings::_helpers::analysis::model_reads; use crate::bindings::_helpers::attachments::cursor_intrinsics::resolve_cursor_intrinsics; use crate::bindings::_helpers::attachments::reference_markers::assert_no_unresolved_sql_markers; use crate::bindings::_helpers::compile_attachment::cursor_roles::resolve_cursor_input_roles; -use crate::bindings::_helpers::compile_driver::python_calls::{ - KeywordArguments, call_with, imported, -}; +use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; 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::planner_entries::models::ModelPlanContext; @@ -18,7 +16,9 @@ use crate::bindings::_helpers::planner_resolution::refs::{ resolve_udf_references, }; use crate::bindings::_helpers::planner_resolution::sources::resolve_sources; +use crate::bindings::_helpers::records::construct::record_class; use crate::bindings::_helpers::records::fields::field as native_field; +use crate::bindings::_helpers::records::fields::replaced_keywords; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; @@ -106,13 +106,13 @@ impl<'py> Resolution<'py> { fn hook(&self, value: Bound<'py, PyAny>, label: &str) -> PyResult> { let py = value.py(); - if value.is_instance(®istry::class(py, RecordId::SqlHookEntry)?)? { + if value.is_instance(&record_class(py, RecordId::SqlHookEntry)?)? { let kwargs = PyDict::new(py); kwargs.set_item( "statement", self.unbounded(native_field(&value, "statement")?, label)?, )?; - return imported(py, "dataclasses", "replace")?.call((value,), Some(&kwargs)); + return replaced_keywords(&value, &kwargs); } let is_list = value.is_instance_of::(); if is_list || value.is_instance_of::() { @@ -194,7 +194,7 @@ pub(crate) fn resolve_model_sql<'py>( let model = &request.model; let py = model.py(); let cursor_type = authored_config_str( - &native_field(model, "config")?.getattr("values")?, + &native_field(&native_field(model, "config")?, "values")?, "cursor_type", )? .unwrap_or_else(|| py.None().into_bound(py)); @@ -231,13 +231,11 @@ pub(crate) fn resolve_model_sql<'py>( fn append_cursor_inclusive(model: &Bound<'_, PyAny>) -> PyResult { let py = model.py(); let strategy = authored_config_str( - &native_field(model, "config")?.getattr("values")?, + &native_field(&native_field(model, "config")?, "values")?, "incremental_strategy", )? .unwrap_or_else(|| py.None().into_bound(py)); - let raw = model - .getattr("config")? - .getattr("values")? + 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()?) } @@ -256,7 +254,8 @@ fn intrinsic_bounds<'py>( &[ ( "value", - PyString::new(py, &sentinel_to_token(&bounds.getattr(field)?)?).into_any(), + PyString::new(py, &sentinel_to_token(&native_field(bounds, field)?)?) + .into_any(), ), ("cursor_type", cursor_type.clone()), ], @@ -341,7 +340,7 @@ pub(crate) fn model_hook_entries<'py>( model: &Bound<'py, PyAny>, resolution: &Resolution<'py>, ) -> PyResult> { - let values = native_field(model, "config")?.getattr("values")?; + let values = native_field(&native_field(model, "config")?, "values")?; let name: String = model_reads::model_name(model)?; let mut hooks: Vec> = Vec::new(); for key in ["pre_hooks", "post_hooks"] { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/sources.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/sources.rs index 4d3d5f2379..75d986c8cd 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/sources.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/sources.rs @@ -65,8 +65,7 @@ fn check_columns( "S401", )); } - if entry - .getattr("contract")? + if native_field(entry, "contract")? .extract::>()? .as_deref() != Some("enforced") @@ -265,8 +264,7 @@ fn resolve_one( let expression = !native_field(&entry, "expression")?.is_none(); if columns.is_truthy()? && (!expression - || entry - .getattr("contract")? + || native_field(&entry, "contract")? .extract::>()? .as_deref() == Some("enforced")) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/values.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/values.rs index 0bcd5c6a96..adba771ab2 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/values.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/values.rs @@ -3,6 +3,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; use crate::bindings::_helpers::cursor::scalars::sentinel_to_token; use crate::bindings::_helpers::planner_resolution::models::CursorFiltering; +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; use pyo3::types::PyString; use sqlbuild_sqltext::resolution::main::cursor_subquery::cursor_subquery; @@ -17,7 +18,7 @@ pub(super) fn method<'py>( name: &str, values: &[(&str, Bound<'py, PyAny>)], ) -> PyResult> { - call_with(&object.getattr(name)?, values) + call_with(&native_field(object, name)?, values) } pub(super) fn cursor_wrap( @@ -40,7 +41,7 @@ pub(super) fn cursor_wrap( let py = adapter.py(); let cursor_type = &filtering.cursor_type; let literal = |bound: &str| -> PyResult { - let token = sentinel_to_token(&bounds.getattr(bound)?)?; + let token = sentinel_to_token(&native_field(bounds, bound)?)?; method( adapter, "render_cursor_bound_literal", diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/artifacts.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/artifacts.rs index 649f4bd0aa..dc13c15357 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/artifacts.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/artifacts.rs @@ -1,14 +1,14 @@ //! Scenario artifact dataclasses and adapter identifier callbacks. -use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; +use crate::bindings::_helpers::compile_driver::python_calls::KeywordArguments; use crate::bindings::_helpers::planner_scenario::constants::{ SCENARIO_PLAN_HASH_COLLISION, SCENARIO_PLAN_INVALID_HASH_PREFIX, SCENARIO_PLAN_RELATION_COLLISION, }; use crate::bindings::_helpers::planner_scenario::values::{error, string}; +use crate::bindings::_helpers::records::construct::{enum_from_value, new_record_with}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::relation_naming::fitting::fitted_logical_name; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python, pyfunction}; use pyo3::types::{PyDict, PyDictMethods, PyTuple}; @@ -152,10 +152,11 @@ pub(crate) fn build_scenario_artifact_name( identifier_limit: i64, ) -> PyResult { let py = kind.py(); - let kind: String = registry::class(py, RecordId::ScenarioArtifactKind)? - .call1((kind,))? - .getattr("value")? - .extract()?; + let kind: String = native_field( + &enum_from_value(py, RecordId::ScenarioArtifactKind, kind)?, + "value", + )? + .extract()?; build_scenario_artifact_physical_name(hash_prefix, &kind, logical_name, identifier_limit) } @@ -167,8 +168,9 @@ pub(crate) fn parse_scenario_artifact_physical_name<'py>( let Some((hash, kind, logical)) = parse_artifact_name::parse_artifact_name(name) else { return Ok(py.None().into_bound(py)); }; - call_with( - ®istry::class(py, RecordId::ParsedScenarioArtifactName)?, + new_record_with( + py, + RecordId::ParsedScenarioArtifactName, &[ ("hash_prefix", string(py, hash)), ("kind", string(py, kind)), @@ -222,15 +224,20 @@ pub(crate) fn build_scenario_relation_map<'py>( normalize.call1((&physical,))? }; if let Some(existing) = seen.get_item(&normalized)? { - let kind = registry::class(py, RecordId::ScenarioArtifactKind)?; - let old_kind: String = kind - .call1((native_field(&existing, "kind")?,))? - .getattr("value")? - .extract()?; - let new_kind: String = kind - .call1((native_field(&artifact, "kind")?,))? - .getattr("value")? - .extract()?; + let old_kind: String = enum_from_value( + py, + RecordId::ScenarioArtifactKind, + native_field(&existing, "kind")?, + )? + .getattr("value")? + .extract()?; + let new_kind: String = enum_from_value( + py, + RecordId::ScenarioArtifactKind, + native_field(&artifact, "kind")?, + )? + .getattr("value")? + .extract()?; let old_name: String = native_field(&existing, "logical_name")?.extract()?; let new_name: String = native_field(&artifact, "logical_name")?.extract()?; return Err(error( @@ -243,16 +250,18 @@ pub(crate) fn build_scenario_relation_map<'py>( )?); } seen.set_item(normalized, &artifact)?; - artifacts.push(call_with( - ®istry::class(py, RecordId::ScenarioArtifactName)?, + artifacts.push(new_record_with( + py, + RecordId::ScenarioArtifactName, &[ ("identity", artifact), ("physical_name", string(py, &physical)), ], )?); } - call_with( - ®istry::class(py, RecordId::ScenarioRelationMap)?, + new_record_with( + py, + RecordId::ScenarioRelationMap, &[ ("scenario_name", args.value("scenario_name")?), ("hash_prefix", args.value("hash_prefix")?), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/cli.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/cli.rs index d41ffba177..f8d04366c5 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/cli.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/cli.rs @@ -12,6 +12,7 @@ use crate::bindings::_helpers::planner_scenario::graph::plan_scenario_graph; use crate::bindings::_helpers::planner_scenario::models::ScenarioRelations; use crate::bindings::_helpers::planner_scenario::relations::build_scenario_relation_plan; use crate::bindings::_helpers::planner_scenario::values::string; +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::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -40,7 +41,7 @@ pub(crate) fn build_cli_scenario_plan<'py>( ), )?; let scenario = args.value("scenario")?; - let project = args.value("pipeline_result")?.getattr("project")?; + let project = native_field(&args.value("pipeline_result")?, "project")?; let adapter = args.value("adapter")?; let (graph, warnings) = plan_scenario_graph(&scenario, &project, true, None)?; raise_for_warnings(warnings.as_any())?; @@ -55,7 +56,7 @@ pub(crate) fn build_cli_scenario_plan<'py>( ("scenarios", native_field(&project, "sql_scenarios")?), ( "run_namespace", - native_field(&project, "scenario")?.getattr("run_namespace")?, + native_field(&native_field(&project, "scenario")?, "run_namespace")?, ), ], )?), @@ -69,13 +70,14 @@ pub(crate) fn build_cli_scenario_plan<'py>( ("seed_names", "SEED"), ("model_names", "MODEL"), ] { - for name in graph.getattr(field)?.try_iter()? { - identities.push(call_with( - ®istry::class(py, RecordId::ScenarioArtifactIdentity)?, + for name in native_field(&graph, field)?.try_iter()? { + identities.push(new_record_with( + py, + RecordId::ScenarioArtifactIdentity, &[ ( "kind", - registry::class(py, RecordId::ScenarioArtifactKind)?.getattr(kind)?, + enum_member(py, RecordId::ScenarioArtifactKind, kind)?, ), ("logical_name", name?), ], @@ -141,7 +143,7 @@ fn namespace<'py>( models.set_item(model_reads::name_object(&model)?, model)?; } for field in ["target_model_names", "model_names"] { - for name in graph.getattr(field)?.try_iter()? { + for name in native_field(graph, field)?.try_iter()? { if let Some(model) = models.get_item(name?)? { let destination = model_reads::destination(&model)?; return Ok(( @@ -156,7 +158,7 @@ fn namespace<'py>( fn raise_for_warnings(warnings: &Bound<'_, PyAny>) -> PyResult<()> { let py = warnings.py(); - let severity = registry::class(py, RecordId::WarningSeverity)?.getattr("ERROR")?; + let severity = enum_member(py, RecordId::WarningSeverity, "ERROR")?; let mut messages: Vec = Vec::new(); let mut code = None; for warning in warnings.try_iter()? { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/entries.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/entries.rs index f2d801ae36..5febab502a 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/entries.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/entries.rs @@ -1,7 +1,7 @@ //! Existing seed and function plan dataclass construction. +use crate::bindings::_helpers::compile_driver::python_calls::KeywordArguments; use crate::bindings::_helpers::compile_driver::python_calls::keywords; -use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; use crate::bindings::_helpers::compiled_project::loop_classes::by_attribute; use crate::bindings::_helpers::planner_identity::metadata::build_compiled_function_fingerprint_sql; use crate::bindings::_helpers::planner_resolution::orchestration::resolve_function_sql; @@ -10,9 +10,9 @@ use crate::bindings::_helpers::planner_scenario::{ models::SqlContext, sql::required_target, values::error, }; use crate::bindings::_helpers::planner_strategy::model_entries::extract_seed_columns; +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; -use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python, pyfunction}; use pyo3::types::PyString; use pyo3::types::{PyDict, PyDictMethods, PyTuple}; @@ -47,20 +47,21 @@ pub(super) fn seed_entries<'py>( &name.extract::()?, "seed", )?; - result.push(call_with( - ®istry::class(py, RecordId::SeedPlanEntry)?, + result.push(new_record_with( + py, + RecordId::SeedPlanEntry, &[ ("key", native_field(&seed, "key")?), ("name", native_field(&seed, "name")?), ("destination", destination), ( "file_path", - native_field(&seed, "seed_file")?.getattr("file_path")?, + native_field(&native_field(&seed, "seed_file")?, "file_path")?, ), ("columns", extract_seed_columns(&seed)?.into_any()), ( "csv_settings", - native_field(&seed, "schema_entry")?.getattr("csv_settings")?, + native_field(&native_field(&seed, "schema_entry")?, "csv_settings")?, ), ], )?); @@ -112,7 +113,7 @@ pub(super) fn function_entry<'py>( "arguments", "returns", ] { - fields.push((name, function.getattr(name)?)); + fields.push((name, native_field(function, name)?)); } fields.push(("body_sql", body)); fields.push(( @@ -128,9 +129,9 @@ pub(super) fn function_entry<'py>( "entry_point", "packages", ] { - fields.push((name, function.getattr(name)?)); + fields.push((name, native_field(function, name)?)); } - call_with(®istry::class(py, RecordId::FunctionPlanEntry)?, &fields) + new_record_with(py, RecordId::FunctionPlanEntry, &fields) } #[pyfunction] diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/execution.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/execution.rs index f7a0043f37..333241eb0f 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/execution.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/execution.rs @@ -1,10 +1,8 @@ //! Assemble scenario execution plans through existing planner and adapter APIs. use crate::bindings::_helpers::analysis::model_reads; +use crate::bindings::_helpers::compile_driver::python_calls::KeywordArguments; use crate::bindings::_helpers::compile_driver::python_calls::keywords; -use crate::bindings::_helpers::compile_driver::python_calls::{ - KeywordArguments, call_with, imported, -}; use crate::bindings::_helpers::compiled_project::loop_classes::by_attribute; use crate::bindings::_helpers::compiled_project::records::compiled_model_record; use crate::bindings::_helpers::planner_entries::model_entries::plan_model; @@ -23,9 +21,10 @@ use crate::bindings::_helpers::planner_scenario::{ }; use crate::bindings::_helpers::planner_strategy::fixture_completion::complete_fixtures as complete_relation_fixtures; use crate::bindings::_helpers::planner_strategy::fixture_context::planning_context; +use crate::bindings::_helpers::records::construct::{enum_member, new_record, new_record_with}; use crate::bindings::_helpers::records::fields::field as native_field; +use crate::bindings::_helpers::records::fields::replaced_keywords; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python, pyfunction}; use pyo3::types::{PyBool, PyDict, PyDictMethods, PyFrozenSet, PyTuple}; @@ -68,7 +67,7 @@ pub(crate) fn build_scenario_execution_plan<'py>( let snapshot = if snapshot.is_truthy()? { snapshot } else { - registry::class(py, RecordId::WarehouseSnapshot)?.call0()? + new_record(py, RecordId::WarehouseSnapshot, (), None)? }; let columns = args.value("source_warehouse_columns")?; let columns = if columns.is_truthy()? { @@ -91,9 +90,7 @@ pub(crate) fn build_scenario_execution_plan<'py>( for key in ordered.try_iter()? { let key = key?; if native_field(&key, "resource_type")?.eq("model")? - && graph - .getattr("model_names")? - .contains(native_field(&key, "name")?)? + && native_field(&graph, "model_names")?.contains(native_field(&key, "name")?)? { model_keys.push(key); } @@ -134,7 +131,7 @@ pub(crate) fn build_scenario_execution_plan<'py>( let changes = PyDict::new(py); changes.set_item("destination", destination)?; changes.set_item("query_sql", query)?; - let model = imported(py, "dataclasses", "replace")?.call((record,), Some(&changes))?; + let model = replaced_keywords(&record, &changes)?; let model_context = model_context(&project, &context, (&models, &locations, &columns))?; let result = object( py, @@ -158,7 +155,7 @@ pub(crate) fn build_scenario_execution_plan<'py>( ("full_refresh", PyBool::new(py, true).to_owned().into_any()), ( "cursor_overrides", - registry::class(py, RecordId::CursorOverridePair)?.call0()?, + new_record(py, RecordId::CursorOverridePair, (), None)?, ), ], )?), @@ -170,8 +167,9 @@ pub(crate) fn build_scenario_execution_plan<'py>( } } let (expected, assertions) = expectations(&scenario, &context)?; - let plan = call_with( - ®istry::class(py, RecordId::ScenarioExecutionPlan)?, + let plan = new_record_with( + py, + RecordId::ScenarioExecutionPlan, &[ ("key", native_field(&scenario, "key")?), ("name", native_field(&scenario, "name")?), @@ -205,7 +203,7 @@ fn complete_fixtures<'py>( ("MODEL", "__ref__"), ("SEED", "__seed__"), ] { - let kind = registry::class(py, RecordId::CompiledResourceType)?.getattr(kind)?; + let kind = enum_member(py, RecordId::CompiledResourceType, kind)?; groups.push(PyTuple::new( py, [kind, extract_fixtures(scenario, prefix)?.into_any()], @@ -297,8 +295,9 @@ fn expectations<'py>( &native_field(&cte, "sql_body")?.extract::()?, false, )?; - expected.push(call_with( - ®istry::class(py, RecordId::ScenarioExpectedExpectationPlan)?, + expected.push(new_record_with( + py, + RecordId::ScenarioExpectedExpectationPlan, &[ ("model_name", string(py, name)), ("actual_destination", target), @@ -315,8 +314,9 @@ fn expectations<'py>( &native_field(&cte, "sql_body")?.extract::()?, false, )?; - assertions.push(call_with( - ®istry::class(py, RecordId::ScenarioAssertionExpectationPlan)?, + assertions.push(new_record_with( + py, + RecordId::ScenarioAssertionExpectationPlan, &[ ( "name", diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/fixtures.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/fixtures.rs index dc88417c35..e9c6175598 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/fixtures.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/fixtures.rs @@ -1,15 +1,15 @@ //! Materialize fixture SQL plans while preserving adapter and completion boundaries. -use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; +use crate::bindings::_helpers::compile_driver::python_calls::KeywordArguments; use crate::bindings::_helpers::planner_scenario::constants::SCENARIO_PLAN_MISSING_FIXTURE_SQL; use crate::bindings::_helpers::planner_scenario::{ models::SqlContext, sql::{required_target, resolve_sql, source_label}, values::{error, string}, }; +use crate::bindings::_helpers::records::construct::{enum_from_value, new_record_with}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python, pyfunction}; use pyo3::types::{PyDict, PyDictMethods, PyTuple}; @@ -70,7 +70,7 @@ pub(super) fn fixture_plans<'py>( ), ] { let fixtures = extract_fixtures(scenario, &format!("__{kind}__"))?; - for name in graph.getattr(names)?.try_iter()? { + for name in native_field(graph, names)?.try_iter()? { let name: String = name?.extract()?; let target = required_target(&context.relations, locations, &name, kind)?; let Some(mut sql) = fixtures.get_item(&name)? else { @@ -90,12 +90,13 @@ pub(super) fn fixture_plans<'py>( } else { format!("WITH {} {sql}", helpers.join(", ")) }; - result.push(call_with( - ®istry::class(py, RecordId::ScenarioFixturePlan)?, + result.push(new_record_with( + py, + RecordId::ScenarioFixturePlan, &[ ( "kind", - registry::class(py, RecordId::ScenarioArtifactKind)?.call1((kind,))?, + enum_from_value(py, RecordId::ScenarioArtifactKind, kind)?, ), ("logical_name", string(py, &name)), ("destination", target), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/graph.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/graph.rs index dc1dd028e4..7adf670153 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/graph.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/graph.rs @@ -1,13 +1,12 @@ //! Convert project views into pure scenario dependency facts. use crate::bindings::_helpers::analysis::model_reads; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; use crate::bindings::_helpers::graph::keys::{KeyCache, native_key}; use crate::bindings::_helpers::planner_scenario::constants::SCENARIO_PLAN_GRAPH_VALIDATION; use crate::bindings::_helpers::planner_scenario::values::string; +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::ids::RecordId; -use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, pyfunction}; use pyo3::types::PyTuple; use sqlbuild_analysis::planner_scenario::main::graph::graph; @@ -26,9 +25,7 @@ pub(crate) fn plan_scenario_graph<'py>( let mut input = ScenarioGraphInput { name: native_field(scenario, "name")?.extract()?, expected: native_field(scenario, "expected_model_names")?.extract()?, - assertions: scenario - .getattr("assertion_target_model_names")? - .extract()?, + assertions: native_field(scenario, "assertion_target_model_names")?.extract()?, targets: native_field(scenario, "target_model_names")?.extract()?, refs: native_field(scenario, "ref_fixture_names")?.extract()?, sources: native_field(scenario, "source_fixture_names")?.extract()?, @@ -38,8 +35,7 @@ pub(crate) fn plan_scenario_graph<'py>( }; for model in native_field(project, "models")?.try_iter()? { let model = model?; - let deps = model - .getattr("deps")? + let deps = native_field(&model, "deps")? .try_iter()? .map(|dep| native_key(&dep?)) .collect::>()?; @@ -60,8 +56,9 @@ pub(crate) fn plan_scenario_graph<'py>( let output = graph(&input); input.targets.sort(); input.assertions.sort(); - let plan = call_with( - ®istry::class(py, RecordId::ScenarioGraphPlan)?, + let plan = new_record_with( + py, + RecordId::ScenarioGraphPlan, &[ ("key", native_field(scenario, "key")?), ("name", native_field(scenario, "name")?), @@ -103,17 +100,14 @@ pub(crate) fn plan_scenario_graph<'py>( ("model_name", py.None().into_bound(py)), ( "severity", - registry::class(py, RecordId::WarningSeverity)?.getattr("ERROR")?, + enum_member(py, RecordId::WarningSeverity, "ERROR")?, ), ("message", string(py, &message)), ]; if coded { values.push(("code", string(py, SCENARIO_PLAN_GRAPH_VALIDATION))); } - warnings.push(call_with( - ®istry::class(py, RecordId::PlanWarning)?, - &values, - )?); + warnings.push(new_record_with(py, RecordId::PlanWarning, &values)?); } Ok((plan, PyTuple::new(py, warnings)?)) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/relations.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/relations.rs index e701a6d8ea..2e75f9a3f8 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/relations.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/relations.rs @@ -1,17 +1,16 @@ //! Build scenario relation locations through adapter qualification callbacks. -use crate::bindings::_helpers::compile_driver::python_calls::{ - KeywordArguments, call_with, imported, -}; +use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; use crate::bindings::_helpers::planner_scenario::constants::{REF, SOURCE}; use crate::bindings::_helpers::planner_scenario::constants::{ SCENARIO_PLAN_INTERNAL, SCENARIO_PLAN_MISSING_RELATION_TARGET, }; use crate::bindings::_helpers::planner_scenario::models::ScenarioRelations; use crate::bindings::_helpers::planner_scenario::values::error; +use crate::bindings::_helpers::records::construct::new_record_with; use crate::bindings::_helpers::records::fields::field as native_field; +use crate::bindings::_helpers::records::fields::replaced_keywords; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python, pyfunction}; use pyo3::types::{PyDict, PyDictMethods}; @@ -85,7 +84,7 @@ pub(crate) fn build_scenario_relation_plan<'py>( ("dbt_ref_fixture_names", "dbt_ref", &dbt_refs), ("seed_names", "seed", &seeds), ] { - for name in graph.getattr(field)?.try_iter()? { + for name in native_field(graph, field)?.try_iter()? { let name: String = name?.extract()?; let target = target_for_artifact(args, &artifacts, kind, &name)?; locations.set_item(&name, &target)?; @@ -97,13 +96,12 @@ pub(crate) fn build_scenario_relation_plan<'py>( .ok_or_else(|| pyo3::exceptions::PyKeyError::new_err(name.clone()))?; let changes = PyDict::new(py); for field in ["database", "schema"] { - changes.set_item(field, target.getattr(field)?)?; + changes.set_item(field, native_field(&target, field)?)?; } changes.set_item("table", native_field(&target, "name")?)?; changes.set_item("expression", py.None())?; changes.set_item("type_enforcement", false)?; - let replacement = - imported(py, "dataclasses", "replace")?.call((entry,), Some(&changes))?; + let replacement = replaced_keywords(&entry, &changes)?; source_map.set_item(&name, replacement)?; } } @@ -120,8 +118,9 @@ pub(crate) fn build_scenario_relation_plan<'py>( }; seed_fixtures.set_item(name, target)?; } - call_with( - ®istry::class(py, RecordId::ScenarioRelationPlan)?, + new_record_with( + py, + RecordId::ScenarioRelationPlan, &[ ("scenario_name", native_field(graph, "name")?), ("relation_map", args.relation_map.clone()), @@ -162,8 +161,9 @@ fn target_for_artifact<'py>( ("name", physical.clone()), ], )?; - call_with( - ®istry::class(py, RecordId::CompiledRelationLocation)?, + new_record_with( + py, + RecordId::CompiledRelationLocation, &[ ("database", database), ("schema", schema), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/sql.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/sql.rs index 14e10ff9f6..ee1b1115ea 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/sql.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/sql.rs @@ -105,7 +105,7 @@ pub(super) fn required_target<'py>( name: &str, kind: &str, ) -> PyResult> { - let target = relations.getattr(field)?.call_method1("get", (name,))?; + let target = native_field(relations, field)?.call_method1("get", (name,))?; if target.is_none() { return Err(error( relations.py(), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_scope/loaders.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_scope/loaders.rs index 41cc14bbb3..2fb987e094 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_scope/loaders.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_scope/loaders.rs @@ -6,9 +6,9 @@ use crate::bindings::_helpers::graph::views::edge_dict; use crate::bindings::_helpers::planner_scope::values::{ dependency_names, edges, keys, loader_graph, loader_names, project_graph, }; +use crate::bindings::_helpers::records::construct::new_record; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, IntoPyObject, PyAny, PyAnyMethods, PyResult, pyfunction}; use pyo3::types::{PyDict, PyDictMethods, PyFrozenSet, PyTuple}; use sqlbuild_analysis::planner_scope::main::{ @@ -54,7 +54,7 @@ pub(crate) fn loader_to_source_entry<'py>( kwargs.set_item("name", &name)?; kwargs.set_item("loader", &name)?; for field in ["database", "schema", "table"] { - kwargs.set_item(field, parts.getattr(field)?)?; + kwargs.set_item(field, native_field(&parts, field)?)?; } for field in [ "write_strategy", @@ -63,12 +63,12 @@ pub(crate) fn loader_to_source_entry<'py>( "contract", "columns", ] { - kwargs.set_item(field, loader.getattr(field)?)?; + kwargs.set_item(field, native_field(loader, field)?)?; } let meta = PyDict::new(py); meta.set_item("sqlbuild_loader_node", true)?; kwargs.set_item("meta", meta)?; - registry::class(py, RecordId::SourceEntry)?.call((), Some(&kwargs)) + new_record(py, RecordId::SourceEntry, (), Some(&kwargs)) } pub(crate) fn intermediate_map<'py>( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_scope/scope.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_scope/scope.rs index 72ac0202d4..e83da0b555 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_scope/scope.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_scope/scope.rs @@ -1,6 +1,6 @@ //! Planner scope assembly using native graph and selector operations. -use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; +use crate::bindings::_helpers::compile_driver::python_calls::KeywordArguments; use crate::bindings::_helpers::graph::keys::KeyCache; use crate::bindings::_helpers::graph::project_graph::NativeProjectGraph; use crate::bindings::_helpers::graph::views::lineage_graph_names; @@ -12,9 +12,9 @@ use crate::bindings::_helpers::planner_scope::loaders::expand_selected_loader_de use crate::bindings::_helpers::planner_scope::values::{edges, keys}; use crate::bindings::_helpers::planner_selection::carrier::{Carrier, py_strings}; use crate::bindings::_helpers::planner_selection::selectors::resolve_graph_selection; +use crate::bindings::_helpers::records::construct::{new_record, new_record_with}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python, pyfunction}; use pyo3::types::PyDictMethods; use pyo3::types::{PyDict, PyFrozenSet, PyTuple}; @@ -34,10 +34,7 @@ pub(crate) fn managed_source_upstream_keys<'py>( let mut managed: HashSet = HashSet::new(); for source in native_field(project, "sources")?.try_iter()? { let source = source?; - if !native_field(&source, "source_entry")? - .getattr("loader")? - .is_none() - { + if !native_field(&native_field(&source, "source_entry")?, "loader")?.is_none() { managed.insert(native_field(&source, "name")?.extract()?); } } @@ -132,10 +129,11 @@ pub(crate) fn build_planner_scope<'py>( } let mut tests = args.value("sql_test_selection")?; if !tests.is_truthy()? { - tests = registry::class(py, RecordId::SqlTestSelection)?.call0()?; + tests = new_record(py, RecordId::SqlTestSelection, (), None)?; } - call_with( - ®istry::class(py, RecordId::PlannerScope)?, + new_record_with( + py, + RecordId::PlannerScope, &[ ("upstream_deps", upstream), ("downstream_deps", downstream), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_scope/sources.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_scope/sources.rs index eb19c0135a..5aca0fa350 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_scope/sources.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_scope/sources.rs @@ -4,9 +4,9 @@ use crate::bindings::_helpers::graph::algorithms::closure; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::planner_scope::loaders::intermediate_map; +use crate::bindings::_helpers::records::construct::new_record; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, pyfunction}; use pyo3::types::{PyBool, PyDict, PyTuple}; use pyo3::types::{PyDictMethods, PySetMethods}; @@ -56,7 +56,6 @@ pub(crate) fn build_source_load_entries<'py>( is_reload: bool, ) -> PyResult> { let py = execution_order.py(); - let class = registry::class(py, RecordId::SourceLoadPlanEntry)?; let kinds = host::entry(py, ExecutorHost::ExecutionResourceKind)?; let mut entries: Vec> = Vec::new(); for key in execution_order.try_iter()? { @@ -84,7 +83,7 @@ pub(crate) fn build_source_load_entries<'py>( "cursor_column", "unique_key", ] { - kwargs.set_item(field, entry.getattr(field)?)?; + kwargs.set_item(field, native_field(&entry, field)?)?; } let table = native_field(&entry, "table")?; kwargs.set_item( @@ -95,8 +94,7 @@ pub(crate) fn build_source_load_entries<'py>( native_field(&entry, "name")? }, )?; - let intermediate = entry - .getattr("meta")? + let intermediate = native_field(&entry, "meta")? .call_method1("get", ("sqlbuild_loader_node",))? .is(PyBool::new(py, true)); kwargs.set_item( @@ -113,7 +111,12 @@ pub(crate) fn build_source_load_entries<'py>( native_field(&integration, "kind")? }, )?; - entries.push(class.call((), Some(&kwargs))?); + entries.push(new_record( + py, + RecordId::SourceLoadPlanEntry, + (), + Some(&kwargs), + )?); } PyTuple::new(py, entries) } 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 c4d14e7408..3b706511a8 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 @@ -12,11 +12,11 @@ use sqlbuild_analysis::planner_selection::models::{ ModelSelectorNames, SelectableModel, SelectorExpansion, }; -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; +use crate::bindings::_helpers::compile_driver::python_calls::imported; 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; -use crate::bindings::_helpers::records::registry; const SHARED_EXCEPTIONS_MODULE: &str = "sqlbuild.errors.contracts.exceptions"; @@ -31,13 +31,16 @@ fn shared_input_error(carrier: &Carrier<'_>, message: &str) -> PyErr { /// Split `+selector+` marker syntax from the selector core without resolving the core. #[pyfunction] -fn split_selector_expansion<'py>(raw: &Bound<'py, PyString>) -> PyResult> { +pub(crate) fn split_selector_expansion<'py>( + raw: &Bound<'py, PyString>, +) -> PyResult> { let py: Python<'py> = raw.py(); let carrier: Carrier<'py> = Carrier::single(raw)?; let expansion: SelectorExpansion = split(&carrier.send(raw)?).map_err(|message| shared_input_error(&carrier, &message))?; - call_with( - ®istry::class(py, RecordId::SelectorExpansion)?, + new_record_with( + py, + RecordId::SelectorExpansion, &[ ("core", carrier.back(&expansion.core)?.into_any()), ( @@ -57,10 +60,8 @@ fn split_selector_expansion<'py>(raw: &Bound<'py, PyString>) -> PyResult) -> PyResult> { - let tags: Bound<'_, PyAny> = model - .getattr("config")? - .getattr("values")? - .call_method1("get", ("tags",))?; + let tags: Bound<'_, PyAny> = + native_field(&native_field(model, "config")?, "values")?.call_method1("get", ("tags",))?; if !tags.is_instance_of::() && !tags.is_instance_of::() { return Ok(Vec::new()); } @@ -71,9 +72,7 @@ fn selectable_model(model: &Bound<'_, PyAny>) -> PyResult { Ok(SelectableModel { name: native_field(model, "name")?.extract()?, tags: model_tags(model)?, - folder: model - .getattr("relative_path")? - .getattr("parent")? + folder: native_field(&native_field(model, "relative_path")?, "parent")? .call_method0("as_posix")? .extract()?, }) @@ -82,13 +81,12 @@ fn selectable_model(model: &Bound<'_, PyAny>) -> PyResult { /// One SQLBuild-owned model selector core's model names, and its path selector when translated. #[pyfunction] #[pyo3(signature = (*, project, term))] -fn resolve_sqlbuild_model_selector_names<'py>( +pub(crate) fn resolve_sqlbuild_model_selector_names<'py>( project: &Bound<'py, PyAny>, term: &Bound<'py, PyString>, ) -> PyResult> { let py: Python<'py> = project.py(); - let models: Vec = project - .getattr("models")? + let models: Vec = native_field(project, "models")? .try_iter()? .map(|model| selectable_model(&model?)) .collect::>()?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_selection/selectors.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_selection/selectors.rs index 2a77feee21..efe7476d2b 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_selection/selectors.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_selection/selectors.rs @@ -14,15 +14,15 @@ use sqlbuild_analysis::graph::main::resolve_selectors::resolve_selectors; use sqlbuild_analysis::graph::models::{ParsedSelector, ProjectGraph}; use sqlbuild_analysis::planner_selection::main::unit_test_selector_rejection::unit_test_selector_rejection; -use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; +use crate::bindings::_helpers::compile_driver::python_calls::KeywordArguments; use crate::bindings::_helpers::graph::algorithms::closure; use crate::bindings::_helpers::graph::keys::KeyCache; use crate::bindings::_helpers::graph::project_graph::{CompiledProjectGraph, NativeProjectGraph}; use crate::bindings::_helpers::graph::views::{GraphViewMappings, graph_from_view_mappings}; use crate::bindings::_helpers::planner_selection::carrier::{Carrier, py_strings}; +use crate::bindings::_helpers::records::construct::{enum_from_value, new_record_with}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; const VIEW_OPTIONS: &[&str] = &[ "all_keys", @@ -44,13 +44,11 @@ pub(crate) fn parsed_selector<'py>( value, upstream, downstream, - } => call_with( - ®istry::class(py, RecordId::ParsedSelector)?, + } => new_record_with( + py, + RecordId::ParsedSelector, &[ - ( - "kind", - registry::class(py, RecordId::SelectorKind)?.call1((kind,))?, - ), + ("kind", enum_from_value(py, RecordId::SelectorKind, kind)?), ("value", carrier.back(&value)?.into_any()), ("upstream", PyBool::new(py, upstream).to_owned().into_any()), ( @@ -64,8 +62,9 @@ pub(crate) fn parsed_selector<'py>( end, upstream, downstream, - } => call_with( - ®istry::class(py, RecordId::PathSelector)?, + } => new_record_with( + py, + RecordId::PathSelector, &[ ("start_name", carrier.back(&start)?.into_any()), ("end_name", carrier.back(&end)?.into_any()), @@ -158,7 +157,7 @@ pub(crate) fn select_project_graph<'py>( /// Model, source, seed and function selectors resolved against compiled graph mappings. #[pyfunction] #[pyo3(signature = (**options))] -fn resolve_project_selectors<'py>( +pub(crate) fn resolve_project_selectors<'py>( py: Python<'py>, options: Option>, ) -> PyResult> { @@ -178,7 +177,7 @@ fn resolve_project_selectors<'py>( /// The keys whitespace-separated selector tokens match, without required build resources. #[pyfunction] #[pyo3(signature = (**options))] -fn resolve_project_selector_tokens<'py>( +pub(crate) fn resolve_project_selector_tokens<'py>( py: Python<'py>, options: Option>, ) -> PyResult> { @@ -210,7 +209,7 @@ fn arguments_graph(arguments: &KeywordArguments<'_>) -> PyResult( +pub(crate) fn match_project_selector<'py>( parsed: &Bound<'py, PyAny>, all_keys: &Bound<'py, PyAny>, tag_index: &Bound<'py, PyAny>, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_selection/unit_tests.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_selection/unit_tests.rs index b7cb0c1ccf..c08d164649 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_selection/unit_tests.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_selection/unit_tests.rs @@ -10,11 +10,10 @@ use sqlbuild_analysis::planner_selection::models::{ SelectorGroups, UnitTestSelectorNames, UnitTestSelectorSplit, }; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; use crate::bindings::_helpers::planner_selection::carrier::{Carrier, py_strings}; +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; -use crate::bindings::_helpers::records::registry; const RESOURCE_ENTRY_FILES: [(&str, &str); 3] = [ ("schema_files", "model_entries"), @@ -57,19 +56,19 @@ fn unit_test_names(discovered: &Bound<'_, PyAny>) -> PyResult> fn resource_names(discovered: &Bound<'_, PyAny>) -> PyResult> { let mut names: BTreeSet = BTreeSet::new(); for field in RESOURCE_PATH_FILES { - for file in discovered.getattr(field)?.try_iter()? { + for file in native_field(discovered, field)?.try_iter()? { names.insert(stem(&native_field(&file?, "file_path")?)?); } } for (field, entries) in RESOURCE_ENTRY_FILES { - for file in discovered.getattr(field)?.try_iter()? { + for file in native_field(discovered, field)?.try_iter()? { for entry in file?.getattr(entries)?.try_iter()? { names.insert(native_field(&entry?, "name")?.extract()?); } } } for field in RESOURCE_NODES { - for node in discovered.getattr(field)?.try_iter()? { + for node in native_field(discovered, field)?.try_iter()? { names.insert(native_field(&node?, "name")?.extract()?); } } @@ -111,8 +110,9 @@ fn split_python<'py>( accepts_unit_tests, ) .map_err(|error| carrier.error(error))?; - let selection: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::SqlTestSelection)?, + let selection: Bound<'py, PyAny> = new_record_with( + py, + RecordId::SqlTestSelection, &[ ("names", PyFrozenSet::new(py, &split.test_names)?.into_any()), ( @@ -129,8 +129,9 @@ fn split_python<'py>( ), ], )?; - call_with( - ®istry::class(py, RecordId::UnitTestSelectorSplit)?, + new_record_with( + py, + RecordId::UnitTestSelectorSplit, &[ ("select", restored(&carrier, &split.select)?.into_any()), ("exclude", restored(&carrier, &split.exclude)?.into_any()), @@ -154,7 +155,7 @@ pub(crate) fn split_project_unit_test_selectors<'py>( /// Split unit-test selectors from resource selectors for a command that runs unit tests. #[pyfunction] #[pyo3(signature = (*, select, exclude, discovered_inputs))] -fn split_unit_test_selectors<'py>( +pub(crate) fn split_unit_test_selectors<'py>( select: &Bound<'py, PyAny>, exclude: &Bound<'py, PyAny>, discovered_inputs: &Bound<'py, PyAny>, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/audit_entries.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/audit_entries.rs index 23ca89d791..33b174bb4b 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/audit_entries.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/audit_entries.rs @@ -8,6 +8,7 @@ use crate::bindings::_helpers::planner_phases::objects::object; use crate::bindings::_helpers::planner_strategy::audits::{ reads_outside_target_lineage, resolve_attachment_kind, resolve_effective_run_scope, }; +use crate::bindings::_helpers::records::construct::{enum_from_value, new_record, new_record_with}; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; use pyo3::pyfunction; use pyo3::types::{ @@ -15,7 +16,7 @@ use pyo3::types::{ }; use crate::bindings::_helpers::boundary::keyword_request::Request; -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; +use crate::bindings::_helpers::compile_driver::python_calls::imported; use crate::bindings::_helpers::planner_strategy::constants::{ AUDIT_TYPES_MODULE, COMPILE_TYPES_MODULE, MODEL_ATTACHMENT, SOURCE_ATTACHMENT, @@ -23,7 +24,6 @@ use crate::bindings::_helpers::planner_strategy::constants::{ use crate::bindings::_helpers::planner_strategy::models::AuditPlanning; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; /// Render and validate one audit query with the supplied adapter callbacks. fn render_query<'py>( @@ -60,7 +60,7 @@ fn audit_enum<'py>( default: &str, ) -> PyResult> { let py = audit.py(); - let value = audit.getattr(attribute)?; + let value = native_field(audit, attribute)?; imported(py, AUDIT_TYPES_MODULE, enum_name)?.call1((if value.is_none() { PyString::new(py, default).into_any() } else { @@ -146,7 +146,7 @@ fn plan_audit<'py>( "always_run", "description", ] { - values.set_item(field, audit.getattr(field)?)?; + values.set_item(field, native_field(&audit, field)?)?; } let measurement = native_field(&audit, "measurement_contract")?; for field in ["value_column", "sample_count_column", "sample_unit"] { @@ -155,7 +155,7 @@ fn plan_audit<'py>( if measurement.is_none() { py.None().into_bound(py) } else { - measurement.getattr(field)? + native_field(&measurement, field)? }, )?; } @@ -185,7 +185,7 @@ fn plan_audit<'py>( ] { values.set_item(field, value)?; } - registry::class(py, RecordId::AuditPlanEntry)?.call((), Some(&values)) + new_record(py, RecordId::AuditPlanEntry, (), Some(&values)) } /// Select attached audits by their logical target and singular audits by dependency overlap. @@ -202,10 +202,10 @@ pub(crate) fn audit_is_selected( let target_value = imported(py, COMPILE_TYPES_MODULE, "AttachedAuditTargetKind")? .call1((kind,))? .getattr("value")?; - let resource_type = - registry::class(py, RecordId::CompiledResourceType)?.call1((target_value,))?; - let key = call_with( - ®istry::class(py, RecordId::CompiledObjectKey)?, + let resource_type = enum_from_value(py, RecordId::CompiledResourceType, target_value)?; + let key = new_record_with( + py, + RecordId::CompiledObjectKey, &[("resource_type", resource_type), ("name", name)], )?; return selected_keys.contains(key); @@ -227,7 +227,7 @@ pub(crate) fn build_selected_audit_entries<'py>( let request = Request::new("build_selected_audit_entries", request)?; let py = request.py(); let project = request.get("project")?; - let selected_keys = request.get("scope")?.getattr("selected_keys")?; + let selected_keys = native_field(&request.get("scope")?, "selected_keys")?; let mut selected: Vec> = Vec::new(); for audit in native_field(&project, "audits")?.try_iter()? { let audit = audit?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/audits.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/audits.rs index e08ffec28f..b862f48152 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/audits.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/audits.rs @@ -9,12 +9,12 @@ use sqlbuild_analysis::planner_strategy::main::effective_run_scope::effective_ru use sqlbuild_analysis::planner_strategy::main::singular_attachment::singular_attachment; use sqlbuild_analysis::planner_strategy::models::{AuditAttachmentKind, AuditRunScope}; -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; +use crate::bindings::_helpers::compile_driver::python_calls::imported; use crate::bindings::_helpers::graph::algorithms::closure; use crate::bindings::_helpers::planner_strategy::values::{input_error, optional_string, text}; +use crate::bindings::_helpers::records::construct::{enum_from_value, new_record_with}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; -use crate::bindings::_helpers::records::registry; const AUDITING_TYPES_MODULE: &str = "sqlbuild.compiler.auditing.types"; const COMPILE_TYPES_MODULE: &str = "sqlbuild.compiler.compile.types"; @@ -23,12 +23,13 @@ const FINAL_RUN_SCOPE: &str = "final"; /// `CompiledObjectKey(resource_type=CompiledResourceType(kind), name=name)`. fn object_key<'py>(py: Python<'py>, kind: &str, name: &str) -> PyResult> { - call_with( - ®istry::class(py, RecordId::CompiledObjectKey)?, + new_record_with( + py, + RecordId::CompiledObjectKey, &[ ( "resource_type", - registry::class(py, RecordId::CompiledResourceType)?.call1((kind,))?, + enum_from_value(py, RecordId::CompiledResourceType, kind)?, ), ("name", PyString::new(py, name).into_any()), ], 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 1b589fff66..f737702e6f 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 @@ -10,7 +10,6 @@ use sqlbuild_analysis::planner_strategy::models::{CursorInputColumnCheck, Cursor use crate::bindings::_helpers::analysis::model_reads; use crate::bindings::_helpers::boundary::keyword_request::Request; use crate::bindings::_helpers::compile_attachment::cursor_roles::resolve_cursor_input_roles; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; 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::planner_identity::metadata::or_empty; @@ -21,14 +20,14 @@ use crate::bindings::_helpers::planner_strategy::constants::{ use crate::bindings::_helpers::planner_strategy::lineage::resolve_lineage_reference; use crate::bindings::_helpers::planner_strategy::models::CursorInputLookups; use crate::bindings::_helpers::planner_strategy::values::{input_error, text}; +use crate::bindings::_helpers::records::construct::{enum_from_value, new_record_with}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; -use crate::bindings::_helpers::records::registry; 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(model, "config")?.getattr("values") + native_field(&native_field(model, "config")?, "values") } /// Whether a model is incremental with a cursor column. @@ -108,7 +107,10 @@ pub(crate) fn reject_unexposed_cursor_input_columns<'py>( let warehouse_columns: &Bound<'py, PyAny> = &lookups.source_warehouse_columns; let model_name: String = model_reads::model_name(model)?; for role in ["filter_inputs", "watermark_inputs"] { - for item in roles.getattr(role)?.call_method0("items")?.try_iter()? { + for item in native_field(&roles, role)? + .call_method0("items")? + .try_iter()? + { let (input_name, column): (String, String) = item?.extract()?; let reference: Bound<'py, PyAny> = resolve_lineage_reference( model, @@ -207,7 +209,7 @@ fn input_kind<'py>( SOURCE_REFERENCE => "source", _ => return Ok(py.None().into_bound(py)), }; - registry::class(py, RecordId::GraphResourceKind)?.call1((value,)) + enum_from_value(py, RecordId::GraphResourceKind, value) } /// A producer model's terminal cursor bound parsed with the consumer's cursor type. @@ -268,7 +270,6 @@ pub(crate) fn build_cursor_input_relations<'py>( Some(cursor_type) if cursor_type.is_truthy()? => cursor_type, _ => PyString::new(py, TIMESTAMP_CURSOR).into_any(), }; - let class: Bound<'py, PyAny> = registry::class(py, RecordId::CursorInputRelation)?; let mut relations: Vec> = Vec::new(); let watermarks: Bound<'py, PyAny> = native_field(&resolve_cursor_input_roles(&model)?, "watermark_inputs")?; @@ -297,8 +298,9 @@ pub(crate) fn build_cursor_input_relations<'py>( None => py.None().into_bound(py), }; let model_backed: bool = kind == MODEL_REFERENCE && model_locations.contains(&name)?; - relations.push(call_with( - &class, + relations.push(new_record_with( + py, + RecordId::CursorInputRelation, &[ ("relation", PyString::new(py, &relation).into_any()), ("cursor_column", column), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/fixture_context.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/fixture_context.rs index 81d2f1d76c..f291797db3 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/fixture_context.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/fixture_context.rs @@ -71,9 +71,9 @@ pub(crate) fn planning_context( ("sources", "source_entry", SOURCE_REFERENCE_KIND, true), ("seeds", "schema_entry", SEED_REFERENCE_KIND, false), ] { - for resource in project.getattr(collection)?.try_iter()? { + for resource in native_field(project, collection)?.try_iter()? { let resource = resource?; - let entry = resource.getattr(entry)?; + let entry = native_field(&resource, entry)?; let columns: Vec = columns(&native_field(&entry, "columns")?, false)?; let authoritative = if source { native_field(&entry, "contract")?.eq("enforced")? 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 965a96576a..7034907f5e 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 @@ -30,9 +30,7 @@ pub(crate) fn model_fixture_relation(model: &Bound<'_, PyAny>) -> PyResult = Vec::new(); - for key in model - .getattr("dep_rows")? - .extract::>()? - { + for key in native_field(model, "dep_rows")?.extract::>()? { if schema.iter().any(|relation| relation.key == key) { deps.push(key); } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/fixture_values.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/fixture_values.rs index c13379eff9..062b6a02bd 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/fixture_values.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/fixture_values.rs @@ -1,8 +1,8 @@ //! Fixture boundary records and adapter callbacks. +use crate::bindings::_helpers::records::construct::new_record; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; use pyo3::types::{PyDict, PyDictMethods}; use sqlbuild_analysis::planner_fixtures::models::FixtureColumn; @@ -36,7 +36,7 @@ pub(crate) fn record<'py>( name: RecordId, kwargs: &Bound<'py, PyDict>, ) -> PyResult> { - registry::class(py, name)?.call((), Some(kwargs)) + new_record(py, name, (), Some(kwargs)) } pub(crate) fn label(key: &Bound<'_, PyAny>) -> PyResult { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/lineage.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/lineage.rs index 6423ef4f64..e6aad3b0ab 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/lineage.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/lineage.rs @@ -48,7 +48,7 @@ fn references<'py>(owner: &Bound<'py, PyAny>) -> PyResult> /// Whether a model's `incremental_mode` is microbatch. fn microbatch_flag(model: &Bound<'_, PyAny>) -> PyResult { - let values: Bound<'_, PyAny> = native_field(model, "config")?.getattr("values")?; + let values: Bound<'_, PyAny> = native_field(&native_field(model, "config")?, "values")?; Ok(match config_str(&values, "incremental_mode")? { Some(mode) => mode.eq(MICROBATCH_MODE)?, None => false, @@ -57,8 +57,7 @@ fn microbatch_flag(model: &Bound<'_, PyAny>) -> PyResult { /// A model's watermark input names. fn watermark_names(model: &Bound<'_, PyAny>) -> PyResult> { - resolve_cursor_input_roles(model)? - .getattr("watermark_inputs")? + native_field(&resolve_cursor_input_roles(model)?, "watermark_inputs")? .try_iter()? .map(|name| text(&name?)) .collect() @@ -85,7 +84,8 @@ impl<'py> ReferenceGraph for ProjectLineage<'py> { fn producer_facts(&self, name: &str) -> PyResult { let producer: Bound<'py, PyAny> = self.models_by_name.get_item(name)?; - let values: Bound<'py, PyAny> = native_field(&producer, "config")?.getattr("values")?; + let values: Bound<'py, PyAny> = + native_field(&native_field(&producer, "config")?, "values")?; let capped: bool = match microbatch_limit_config(&values)?.1 { Some(action) => CAP_ACTIONS.contains(&text(&action)?.as_str()), None => false, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/microbatch_limits.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/microbatch_limits.rs index 26f52ceb9b..38ebd8a91b 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/microbatch_limits.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/microbatch_limits.rs @@ -1,6 +1,7 @@ //! Microbatch limits applied to planned model entries in CLI, project and model precedence. use crate::bindings::_helpers::planner_changes::results::native_enum; +use crate::bindings::_helpers::records::construct::{enum_member, new_record_with, record_class}; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyErr, PyResult, Python}; use pyo3::types::{PyString, PyTuple}; use pyo3::{IntoPyObject, pyfunction}; @@ -11,7 +12,6 @@ use sqlbuild_core::plan_display::errors::UnknownCursorGrain; use sqlbuild_core::plan_display::main::effective_timestamp_grain::effective_timestamp_grain; use sqlbuild_core::plan_display::models::TimestampGrainFacts; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; use crate::bindings::_helpers::compiled_project::records::replace; use crate::bindings::_helpers::plan_output::display::resolve_effective_microbatch_batch_size; use crate::bindings::_helpers::planner_strategy::microbatches::{ @@ -20,7 +20,6 @@ use crate::bindings::_helpers::planner_strategy::microbatches::{ use crate::bindings::_helpers::planner_strategy::values::{input_error, optional_string, text}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; -use crate::bindings::_helpers::records::registry; const ERROR_ACTION: &str = "error"; const SKIP_ACTION: &str = "skip"; @@ -33,7 +32,7 @@ type LimitedEntry<'py> = (Bound<'py, PyAny>, Option>); /// `CursorGrain(grain)`'s error for a grain outside the known grains. pub(crate) fn unknown_grain(py: Python<'_>, error: UnknownCursorGrain) -> PyErr { - registry::class(py, RecordId::CursorGrain) + record_class(py, RecordId::CursorGrain) .and_then(|class| class.call1((error.0,))) .err() .unwrap_or_else(|| pyo3::exceptions::PyValueError::new_err("unknown cursor grain")) @@ -151,8 +150,9 @@ fn limited<'py>( if action_text == ERROR_ACTION { return Err(input_error(py, ErrorId::CompileInput, &message, None)); } - let plan_warning: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::PlanWarning)?, + let plan_warning: Bound<'py, PyAny> = new_record_with( + py, + RecordId::PlanWarning, &[ ("model_name", name), ( @@ -204,9 +204,7 @@ fn default_limit<'py>( configured: Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = entry.py(); - if entry - .getattr("microbatch_strategy")? - .eq(ROLLING_WINDOW_STRATEGY)? + if native_field(entry, "microbatch_strategy")?.eq(ROLLING_WINDOW_STRATEGY)? && !native_field(entry, "reason")?.eq(FULL_REFRESH_REASON)? && native_field(entry, "start_cursor_override")?.is_none() && native_field(entry, "end_cursor_override")?.is_none() @@ -231,12 +229,8 @@ pub(crate) fn apply_configured_microbatch_limits<'py>( let py: Python<'py> = entry.py(); let configured: Bound<'py, PyAny> = native_field(inputs, "max_microbatches")?; let configured_action: Bound<'py, PyAny> = native_field(inputs, "microbatch_limit_action")?; - if inputs - .getattr("max_microbatches_is_override")? - .is_truthy()? - { - let error: Bound<'py, PyAny> = - registry::class(py, RecordId::MicrobatchLimitAction)?.getattr("ERROR")?; + if native_field(inputs, "max_microbatches_is_override")?.is_truthy()? { + let error: Bound<'py, PyAny> = enum_member(py, RecordId::MicrobatchLimitAction, "ERROR")?; return with_warnings(limited(entry, configured, error)?, Vec::new()); } let declared: Bound<'py, PyAny> = native_field(entry, "declared_microbatch_limit_action")?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/microbatches.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/microbatches.rs index 52164499b0..6f5a1455ed 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/microbatches.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/microbatches.rs @@ -18,13 +18,14 @@ use sqlbuild_analysis::planner_strategy::types::Timeline; use sqlbuild_cursor::algebra::main::parse_duration::parse_duration; use sqlbuild_cursor::algebra::models::{DurationDigits, DurationSpan}; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; use crate::bindings::_helpers::cursor::durations::shifted_moment; use crate::bindings::_helpers::plan_output::display::aligned_timestamp_bounds; use crate::bindings::_helpers::planner_strategy::values::text; +use crate::bindings::_helpers::records::construct::{ + enum_from_value, new_record_with, record_class, +}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; const INTEGER_CURSOR: &str = "integer"; const TIMESTAMP_CURSOR: &str = "timestamp"; @@ -87,7 +88,7 @@ fn typed_bounds<'py>( for value in [&start, &end] { let mut typed: bool = false; for class in classes { - typed = typed || value.is_instance(®istry::class(py, *class)?)?; + typed = typed || value.is_instance(&record_class(py, *class)?)?; } if !typed { return Ok(None); @@ -157,8 +158,9 @@ fn cursor_bounds<'py>( end: Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = start.py(); - call_with( - ®istry::class(py, RecordId::CursorBounds)?, + new_record_with( + py, + RecordId::CursorBounds, &[("start", start), ("end", end)], ) } @@ -169,7 +171,7 @@ fn cursor_value<'py>( class: RecordId, value: Bound<'py, PyAny>, ) -> PyResult> { - call_with(®istry::class(py, class)?, &[("value", value)]) + new_record_with(py, class, &[("value", value)]) } /// One concrete range capped to `max_batches` batch windows from the end the action keeps. @@ -273,7 +275,7 @@ pub(crate) fn microbatch_limit_config<'py>( .filter(|action| action.is_instance_of::()); if let (Some(max_batches), Some(action)) = (max_batches, action) { let action: Bound<'py, PyAny> = - registry::class(py, RecordId::MicrobatchLimitAction)?.call1((action,))?; + enum_from_value(py, RecordId::MicrobatchLimitAction, action)?; return Ok((Some(max_batches), Some(action))); } } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/model_entries.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/model_entries.rs index 1166105cf1..c1013e8b9b 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/model_entries.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/model_entries.rs @@ -32,9 +32,9 @@ use crate::bindings::_helpers::planner_strategy::strategy::{ model_warning_records, resolve_schema_actions, warning_inputs, }; use crate::bindings::_helpers::planner_strategy::values::{input_error, optional_string, text}; +use crate::bindings::_helpers::records::construct::{enum_from_value, new_record, new_record_with}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; -use crate::bindings::_helpers::records::registry; /// Model config keys copied to the plan entry as optional strings, with their entry fields. const STRING_FIELDS: [(&str, &str); 15] = [ @@ -146,7 +146,7 @@ fn on_schema_change<'py>(model: &Bound<'py, PyAny>) -> PyResult() { - match registry::class(py, RecordId::OnSchemaChange)?.call1((&raw,)) { + match enum_from_value(py, RecordId::OnSchemaChange, &raw) { Ok(policy) => return Ok(policy), Err(error) if !error.is_instance_of::(py) => return Err(error), Err(_) => {} @@ -355,8 +355,9 @@ fn ddl_cursor_bounds<'py>(request: &ModelEntryDecisions<'py>) -> PyResult = host::entry(py, ExecutorHost::BoundSentinel)?; - call_with( - ®istry::class(py, RecordId::CursorBounds)?, + new_record_with( + py, + RecordId::CursorBounds, &[ ("start", native_field(&sentinel, "START")?), ("end", native_field(&sentinel, "END")?), @@ -510,8 +511,7 @@ pub(crate) fn assemble_model_plan_entry<'py>( let schema_actions: Bound<'py, PyAny> = resolve_schema_actions(&native_field(change, "schema_findings")?, &policy)?.into_any(); let enforcement: Bound<'py, PyAny> = type_enforcement(model)?; - let warehouse_columns: Bound<'py, PyAny> = snapshot - .getattr("existing_columns")? + let warehouse_columns: Bound<'py, PyAny> = native_field(snapshot, "existing_columns")? .call_method1("get", (&name, PyTuple::empty(py)))?; let warnings: Bound<'py, PyTuple> = model_warnings( request, @@ -521,11 +521,13 @@ pub(crate) fn assemble_model_plan_entry<'py>( 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 table_type: Bound<'py, PyAny> = model - .getattr("config")? - .getattr("table_type")? - .getattr("value")? - .getattr("value")?; + let table_type: Bound<'py, PyAny> = native_field( + &native_field( + &native_field(&native_field(model, "config")?, "table_type")?, + "value", + )?, + "value", + )?; let ddl: String = logical_ddl( request, (&unique_key, &merge_exclude), @@ -540,7 +542,7 @@ pub(crate) fn assemble_model_plan_entry<'py>( entry.set_item("logical_ddl", table_type_ddl(&ddl, &text(&table_type)?))?; passed_fields(request, &entry)?; for field in CHANGE_FIELDS { - entry.set_item(field, change.getattr(field)?)?; + entry.set_item(field, native_field(change, field)?)?; } config_fields(request, &values, &entry)?; entry.set_item("unique_key", unique_key)?; @@ -551,10 +553,9 @@ pub(crate) fn assemble_model_plan_entry<'py>( contract_fields(model, &values, &entry)?; entry.set_item("pre_hooks", &request.hooks.0)?; entry.set_item("post_hooks", &request.hooks.1)?; - let fingerprint: Bound<'py, PyAny> = snapshot - .getattr("fingerprints")? - .getattr("models")? - .call_method1("get", (&name,))?; + let fingerprint: Bound<'py, PyAny> = + native_field(&native_field(snapshot, "fingerprints")?, "models")? + .call_method1("get", (&name,))?; entry.set_item( "previous_query_sql", if fingerprint.is_none() { @@ -569,7 +570,6 @@ pub(crate) fn assemble_model_plan_entry<'py>( text(&request.materialization_type)? == CUSTOM_MATERIALIZATION, &entry, )?; - let plan_entry: Bound<'py, PyAny> = - registry::class(py, RecordId::ModelPlanEntry)?.call((), Some(&entry))?; + let plan_entry: Bound<'py, PyAny> = new_record(py, RecordId::ModelPlanEntry, (), Some(&entry))?; Ok((plan_entry, warnings)) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/strategy.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/strategy.rs index f3057d8e3a..da08459f8e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/strategy.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/strategy.rs @@ -2,6 +2,7 @@ use crate::bindings::_helpers::analysis::model_reads; use crate::bindings::_helpers::planner_changes::results::native_enum; +use crate::bindings::_helpers::records::construct::new_record_with; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; use pyo3::types::{PyBool, PyDict, PyString, PyTuple}; use pyo3::{IntoPyObject, pyfunction, wrap_pyfunction}; @@ -14,14 +15,13 @@ use sqlbuild_analysis::planner_strategy::models::{ ModelStrategy, PlanAction, PlanReason, SchemaActionKind, WarningInputs, }; -use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; +use crate::bindings::_helpers::compile_driver::python_calls::KeywordArguments; use crate::bindings::_helpers::planner_changes::results::planner_enum; use crate::bindings::_helpers::planner_strategy::values::{ change_facts, config_value, finding_rows, input_error, materialization_value, model_materialization, optional_string, plan_warning, text, }; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; -use crate::bindings::_helpers::records::registry; fn action_value(action: PlanAction) -> &'static str { match action { @@ -114,7 +114,6 @@ pub(crate) fn resolve_schema_actions<'py>( on_schema_change: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = schema_findings.py(); - let class: Bound<'py, PyAny> = registry::class(py, RecordId::SchemaAction)?; let policy: Option = schema_policy(on_schema_change)?; let mut actions: Vec> = Vec::new(); for row in schema_actions(&finding_rows(schema_findings)?, policy.as_deref()) { @@ -123,8 +122,9 @@ pub(crate) fn resolve_schema_actions<'py>( SchemaActionKind::DropColumn => "drop_column", SchemaActionKind::AlterColumnType => "alter_column_type", }; - actions.push(call_with( - &class, + actions.push(new_record_with( + py, + RecordId::SchemaAction, &[ ("kind", native_enum(py, RecordId::SchemaActionKind, kind)?), ( 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 d899781756..2e7b609e08 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/values.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/values.rs @@ -1,6 +1,7 @@ //! Conversions between the planner's Python models and the native strategy rows. use crate::bindings::_helpers::planner_changes::results::native_enum; +use crate::bindings::_helpers::records::construct::new_record_with; use pyo3::IntoPyObject; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyErr, PyResult, Python}; use pyo3::types::{PyDict, PyDictMethods, PyString}; @@ -13,7 +14,6 @@ use sqlbuild_analysis::planner_strategy::models::{ ChangeFacts, MaterializationType, PlanChangeKind, WarningRow, WarningSeverity, }; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -40,10 +40,7 @@ pub(crate) fn config_value<'py>( model: &Bound<'py, PyAny>, key: &str, ) -> PyResult> { - model - .getattr("config")? - .getattr("values")? - .call_method1("get", (key,)) + native_field(&native_field(model, "config")?, "values")?.call_method1("get", (key,)) } /// The materialization type of a model's `materialized` config. @@ -107,21 +104,19 @@ pub(crate) fn finding_rows(findings: &Bound<'_, PyAny>) -> PyResult) -> PyResult { - let backfill: String = text(&native_field(change, "backfill")?.getattr("action")?)?; + let backfill: String = text(&native_field(&native_field(change, "backfill")?, "action")?)?; Ok(ChangeFacts { kind: change_kind(&text(&native_field(change, "change_kind")?)?), backfill_full: backfill == FULL_BACKFILL, backfill_forward_only: backfill == FORWARD_ONLY_BACKFILL, query_changed: native_field(change, "query_changed")?.is_truthy()?, - changed_functions: change - .getattr("changed_functions")? + changed_functions: native_field(change, "changed_functions")? .try_iter()? .map(|name| text(&name?)) .collect::>()?, config_changed: native_field(change, "config_changed")?.is_truthy()?, schema_findings: finding_rows(&native_field(change, "schema_findings")?)?, - recorded_build_relation_missing: change - .getattr("recorded_build_relation_missing")? + recorded_build_relation_missing: native_field(change, "recorded_build_relation_missing")? .is_truthy()?, }) } @@ -137,8 +132,9 @@ pub(crate) fn plan_warning<'py>( WarningSeverity::Warning => "warning", WarningSeverity::Info => "info", }; - call_with( - ®istry::class(py, RecordId::PlanWarning)?, + new_record_with( + py, + RecordId::PlanWarning, &[ ("model_name", model_name.clone()), ( 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 6aaaacff26..9f436db537 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/cursors.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/cursors.rs @@ -28,9 +28,9 @@ use crate::bindings::_helpers::planner_warehouse::snapshot::{ SnapshotRequest, inspection_concurrency, }; use crate::bindings::_helpers::planner_warehouse::watermarks::validate_watermark_column; +use crate::bindings::_helpers::records::construct::record_class; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::references::source_relations::render_source_relation; use pyo3::IntoPyObject; use pyo3::exceptions::PyException; @@ -195,10 +195,7 @@ fn runtime_producer_names( let mut loaded: HashSet = HashSet::new(); for source in native_field(project, "sources")?.try_iter()? { let source = source?; - if !native_field(&source, "source_entry")? - .getattr("loader")? - .is_none() - { + if !native_field(&native_field(&source, "source_entry")?, "loader")?.is_none() { loaded.insert(native_field(&source, "name")?.extract()?); } } @@ -224,8 +221,9 @@ fn upstream_relation<'py>( let name = native_field(reference, "ref_name")?; let name_text: String = name.extract()?; let is_selected = selected.is_some_and(|names| names.contains(&name_text)); - let qualified = - |node: Bound<'py, PyAny>| native_field(&node, "destination")?.getattr("qualified_name"); + let qualified = |node: Bound<'py, PyAny>| { + native_field(&native_field(&node, "destination")?, "qualified_name") + }; if (kind.eq("ref")? || kind.eq("seed")?) && !deferred.is_none() && deferred.contains(&name)? @@ -285,7 +283,10 @@ fn terminal_bound<'py>( }; let value = object( py, - config_cursor_bound(&native_field(&upstream, "config")?.getattr("values")?, key)?, + config_cursor_bound( + &native_field(&native_field(&upstream, "config")?, "values")?, + key, + )?, )?; if value.is_none() { return Ok(None); @@ -870,7 +871,7 @@ fn eligible_target_maxes<'py>( ( "is_date", horizon - .is_instance(®istry::class(py, DATE_VALUE)?)? + .is_instance(&record_class(py, DATE_VALUE)?)? .into_pyobject(py)? .to_owned() .into_any(), 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 5a321299ae..9f1cab5335 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 @@ -77,8 +77,7 @@ fn source_candidates<'py>( let mut names: BTreeSet<_> = BTreeSet::new(); for entry in &scoped { schemas.insert( - entry - .getattr("schema")? + native_field(entry, "schema")? .extract::>()? .unwrap_or_default(), ); @@ -182,10 +181,7 @@ pub(crate) fn semantic_source_columns<'py>( let selected_keys = &args.value("selected_keys")?; let py = project.py(); - if !project - .getattr("settings")? - .getattr("sql_analysis")? - .is_truthy()? + if !native_field(&native_field(project, "settings")?, "sql_analysis")?.is_truthy()? || connection.is_none() { return Ok(columns.clone()); @@ -193,9 +189,7 @@ 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 = model - .getattr("config")? - .getattr("values")? + 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)) 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 b588fec416..a31ce1cf1c 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/snapshot.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/snapshot.rs @@ -72,7 +72,7 @@ pub(crate) fn destination_nodes<'py>( ) -> PyResult>> { let mut nodes: Vec<_> = Vec::new(); for field in ["models", "seeds", "functions"] { - for node in project.getattr(field)?.try_iter()? { + for node in native_field(project, field)?.try_iter()? { nodes.push(node?); } } @@ -103,7 +103,7 @@ fn resolve_database<'py>( if !selected_node(selected, &node)? { continue; } - let database = native_field(&node, "destination")?.getattr("database")?; + let database = native_field(&native_field(&node, "destination")?, "database")?; if !database.is_none() { return Ok(database); } @@ -120,7 +120,7 @@ fn target_schemas( if !selected_node(selected, &node)? { continue; } - let schema = native_field(&node, "destination")?.getattr("schema")?; + let schema = native_field(&native_field(&node, "destination")?, "schema")?; if !schema.is_none() { schemas.insert(schema.extract()?); } @@ -157,11 +157,7 @@ fn model_upstream_names( } let lookup = if is_ref { models } else { seeds }; if let Some(node) = lookup.get_item(&name)? { - names.insert( - native_field(&node, "destination")? - .getattr("name")? - .extract()?, - ); + names.insert(native_field(&native_field(&node, "destination")?, "name")?.extract()?); } } Ok(names) @@ -175,11 +171,9 @@ fn metadata_name_filter( let mut names: BTreeSet = BTreeSet::new(); if selected.is_none() { for field in ["models", "seeds"] { - for node in project.getattr(field)?.try_iter()? { + for node in native_field(project, field)?.try_iter()? { names.insert( - native_field(&node?, "destination")? - .getattr("name")? - .extract()?, + native_field(&native_field(&node?, "destination")?, "name")?.extract()?, ); } } @@ -209,11 +203,8 @@ fn metadata_name_filter( &selected_names, )?); } else if let Some(seed) = seeds.get_item(&name)? { - names.insert( - native_field(&seed, "destination")? - .getattr("name")? - .extract()?, - ); + names + .insert(native_field(&native_field(&seed, "destination")?, "name")?.extract()?); } else if let Some(entry) = sources.get_item(&name)? { names.insert(source_table_name(&entry)?); } @@ -357,7 +348,7 @@ fn optional_names<'py>( fn location_identity<'py>(location: &Bound<'py, PyAny>) -> PyResult> { let py = location.py(); let part = |name: &str| -> PyResult> { - let value = location.getattr(name)?; + let value = native_field(location, name)?; if value.is_none() { Ok(value) } else { @@ -480,9 +471,7 @@ fn shares_unfiltered(project: &Bound<'_, PyAny>, selected: &Bound<'_, PyAny>) -> if !names.contains(&model_reads::model_name(&model)?) { all_selected = false; } - if model - .getattr("config")? - .getattr("values")? + if native_field(&native_field(&model, "config")?, "values")? .call_method1("get", (MIGRATE_FROM_CONFIG_KEY,))? .is_instance_of::() { @@ -554,9 +543,7 @@ fn gather_fingerprints<'py>( ), ( "render_read_latest_sql", - request - .adapter - .getattr("render_read_latest_fingerprints_sql")?, + native_field(&request.adapter, "render_read_latest_fingerprints_sql")?, ), ( "node_names", @@ -634,7 +621,7 @@ fn scope_field<'py>(scope: &Bound<'py, PyAny>, name: &str) -> PyResult( ) -> PyResult>> { match source_entries.filter(|entries| !entries.is_none()) { Some(entries) => entries.try_iter()?.collect(), - None => project - .getattr("sources")? + None => native_field(project, "sources")? .try_iter()? .map(|source| native_field(&source?, "source_entry")) .collect(), @@ -110,8 +109,7 @@ pub(crate) fn source_inspection<'py>( if schema.is_none() { continue; } - let key: String = entry - .getattr("database")? + let key: String = native_field(entry, "database")? .extract::>()? .unwrap_or_default(); let schema: String = schema.extract()?; 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 4321c6ccdc..3563b6c80a 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 @@ -13,8 +13,8 @@ use crate::bindings::_helpers::planner_phases::objects::{ use crate::bindings::_helpers::planner_warehouse::constants::{ MODEL_HOOK_CONFIG_KEYS, SQL_HOOK_ENTRY, }; +use crate::bindings::_helpers::records::construct::record_class; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; use pyo3::pyfunction; use pyo3::types::PyDict as KeywordDict; @@ -50,9 +50,9 @@ fn hook_source_reads( ) -> PyResult> { let mut names: Vec = Vec::new(); let py = model.py(); - let hook_entry = registry::class(py, SQL_HOOK_ENTRY)?; + let hook_entry = record_class(py, SQL_HOOK_ENTRY)?; let source_kind = host::entry(py, UserCodeHost::SqlResourceRefKind)?.getattr("SOURCE")?; - let values = native_field(model, "config")?.getattr("values")?; + let values = native_field(&native_field(model, "config")?, "values")?; for key in MODEL_HOOK_CONFIG_KEYS { let value = values.call_method1("get", (key,))?; let entries: Vec> = @@ -158,8 +158,10 @@ fn source_target_name<'py>( if active.is_none() || project_config.is_none() || local_config.is_none() { return Ok(py.None().into_bound(py)); } - let deferred = resolve_target(project_config, local_config, active.clone())? - .getattr("defer_sources_to")?; + let deferred = native_field( + &resolve_target(project_config, local_config, active.clone())?, + "defer_sources_to", + )?; Ok(if deferred.is_truthy()? { deferred } else { @@ -170,10 +172,8 @@ fn source_target_name<'py>( fn raw_source_entry<'py>(source: &Bound<'py, PyAny>) -> PyResult> { let declared = native_field(source, "source_entry")?; let name = native_field(&declared, "name")?; - for entry in source - .getattr("source_file")? - .getattr("source_entries")? - .try_iter()? + for entry in + native_field(&native_field(source, "source_file")?, "source_entries")?.try_iter()? { let entry = entry?; if native_field(&entry, "name")?.eq(&name)? { @@ -309,9 +309,8 @@ pub(crate) fn build_source_read_map<'py>( let result = source_map.copy()?; for source in native_field(project, "sources")?.try_iter()? { let source = source?; - let name: String = native_field(&source, "source_entry")? - .getattr("name")? - .extract()?; + let name: String = + native_field(&native_field(&source, "source_entry")?, "name")?.extract()?; if !selected.contains(&name) { continue; } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/source_freshness.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/source_freshness.rs index 3ebd4939b5..bcaba41d37 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/source_freshness.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/source_freshness.rs @@ -12,9 +12,9 @@ use crate::bindings::_helpers::planner_phases::objects::{ }; use crate::bindings::_helpers::planner_warehouse::constants::RUN_SCOPE; use crate::bindings::_helpers::planner_warehouse::snapshot::{destination_nodes, lower}; +use crate::bindings::_helpers::records::construct::enum_member; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; use pyo3::types::PyDict as KeywordDict; use pyo3::types::{PyBool, PyDict, PyDictMethods}; @@ -31,8 +31,7 @@ pub(crate) fn source_freshness_unknown_warnings<'py>( ) -> PyResult> { let py = source_freshness.py(); let mut reasons: Vec<(String, Vec)> = Vec::new(); - for unknown in source_freshness - .getattr("unknown_sources")? + for unknown in native_field(source_freshness, "unknown_sources")? .call_method0("values")? .try_iter()? { @@ -57,7 +56,7 @@ pub(crate) fn source_freshness_unknown_warnings<'py>( ("model_name", py.None().into_bound(py)), ( "severity", - registry::class(py, RecordId::WarningSeverity)?.getattr("WARNING")?, + enum_member(py, RecordId::WarningSeverity, "WARNING")?, ), ("message", text(py, &message)), ], @@ -69,7 +68,7 @@ pub(crate) fn source_freshness_unknown_warnings<'py>( fn state_database<'py>(project: &Bound<'py, PyAny>) -> PyResult> { for node in destination_nodes(project)? { - let database = native_field(&node, "destination")?.getattr("database")?; + let database = native_field(&native_field(&node, "destination")?, "database")?; if !database.is_none() { return Ok(database); } @@ -83,7 +82,7 @@ fn state_schemas( ) -> PyResult> { let mut schemas: BTreeSet<_> = BTreeSet::new(); for node in destination_nodes(project)? { - let schema = native_field(&node, "destination")?.getattr("schema")?; + let schema = native_field(&native_field(&node, "destination")?, "schema")?; if !schema.is_none() { schemas.insert(schema.extract()?); } @@ -152,8 +151,7 @@ pub(crate) fn build_planner_source_freshness_result<'py>( } let sources = tuple( py, - relations - .getattr("source_read_map")? + native_field(relations, "source_read_map")? .call_method0("values")? .try_iter()? .collect::>()?, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/source_tables.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/source_tables.rs index 4e05137381..864bbce797 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/source_tables.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/source_tables.rs @@ -73,8 +73,7 @@ fn read_relation<'py>( fn declaration_location(source: &Bound<'_, PyAny>) -> PyResult { let file = native_field(source, "source_file")?; - let path: String = file - .getattr("relative_path")? + let path: String = native_field(&file, "relative_path")? .call_method0("as_posix")? .extract()?; let line = yaml_entry_line( 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 7e6a3dd1f9..d1607f2e4a 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/watermarks.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/watermarks.rs @@ -91,7 +91,7 @@ pub(crate) fn validate_watermark_column<'py>( (Some(WATERMARK_CONTRACT_CODE), None), )); } - let values = native_field(&upstream, "config")?.getattr("values")?; + let values = native_field(&native_field(&upstream, "config")?, "values")?; if !values.call_method1("get", ("contract",))?.eq("enforced")? { return Ok(()); } @@ -114,7 +114,10 @@ pub(crate) fn validate_watermark_column<'py>( let Some(seed) = seeds.get_item(&ref_name)? else { return Ok(()); }; - declared_columns(&native_field(&seed, "schema_entry")?.getattr("columns")?)? + declared_columns(&native_field( + &native_field(&seed, "schema_entry")?, + "columns", + )?)? } else { return Ok(()); }; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/presentation_text/style.rs b/crates/sqlbuild-python/src/bindings/_helpers/presentation_text/style.rs index 9a6bb6d0b2..a692d65322 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/presentation_text/style.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/presentation_text/style.rs @@ -23,7 +23,7 @@ impl PythonCliStyle { impl PythonCliStyle { #[new] #[pyo3(signature = (*, use_color))] - fn new(use_color: &Bound<'_, PyAny>) -> PyResult { + pub(crate) fn new(use_color: &Bound<'_, PyAny>) -> PyResult { Ok(Self { use_color: use_color.is_truthy()?, }) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/project_assembly/declared_relations.rs b/crates/sqlbuild-python/src/bindings/_helpers/project_assembly/declared_relations.rs index e68586513e..e2c2c9a375 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/project_assembly/declared_relations.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/project_assembly/declared_relations.rs @@ -8,9 +8,9 @@ use crate::bindings::_helpers::compiled_project::model_input_view::{ attached_columns, attached_schema, }; use crate::bindings::_helpers::compiled_project::schema_facts::python_column_facts; +use crate::bindings::_helpers::records::construct::{enum_member, record_class}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; const UNKNOWN_TYPE: &str = "UNKNOWN"; const NOT_NULL_AUDIT_NAME: &str = "not_null"; @@ -77,7 +77,7 @@ pub(crate) fn declared_relations<'py>( inputs: Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = inputs.py(); - let nullability: Bound<'py, PyAny> = registry::class(py, RecordId::InferredNullability)?; + let nullability: Bound<'py, PyAny> = record_class(py, RecordId::InferredNullability)?; let types: Bound<'py, PyDict> = PyDict::new(py); let nulls: Bound<'py, PyDict> = PyDict::new(py); let complete: Bound<'py, PyDict> = PyDict::new(py); @@ -90,11 +90,10 @@ pub(crate) fn declared_relations<'py>( (schema.name, attached_columns(&model)?); types.set_item(&name, column_types(py, &columns, false)?)?; nulls.set_item(&name, column_nullability(&columns, &nullability)?)?; - let contract: Option> = model - .getattr("config")? - .getattr("values")? - .downcast_into::()? - .get_item(CONTRACT_KEY)?; + let contract: Option> = + native_field(&native_field(&model, "config")?, "values")? + .downcast_into::()? + .get_item(CONTRACT_KEY)?; let enforced: bool = match contract { Some(contract) => contract.eq(ENFORCED_CONTRACT)?, None => false, @@ -140,7 +139,10 @@ pub(crate) fn declared_relations<'py>( types.set_item(&name, column_types(py, &columns, true)?)?; let unknown: Bound<'py, PyDict> = PyDict::new(py); for column in &columns { - unknown.set_item(&column.name, nullability.getattr(UNKNOWN_MEMBER)?)?; + unknown.set_item( + &column.name, + enum_member(py, RecordId::InferredNullability, UNKNOWN_MEMBER)?, + )?; } nulls.set_item(&name, unknown)?; complete.set_item(&name, column_types(py, &columns, true)?)?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/project_assembly/native_resources.rs b/crates/sqlbuild-python/src/bindings/_helpers/project_assembly/native_resources.rs index 93de569bf1..2386aa0130 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/project_assembly/native_resources.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/project_assembly/native_resources.rs @@ -123,7 +123,7 @@ fn variables<'py>(effective_vars: &Bound<'py, PyAny>) -> PyResult, attribute: &str) -> PyResult> { - value.getattr(attribute)?.extract() + native_field(value, attribute)?.extract() } /// Python's own `os.environ` lookup of every name an `ENV:` template may read. @@ -183,19 +183,12 @@ fn model_facts(model_input: &Bound<'_, PyAny>, checks: &Bound<'_, PyAny>) -> PyR syntax_checks.push(SyntaxCheck { sql, placeholders }); } Ok(ModelFacts { - name: model_file - .getattr("file_path")? - .getattr("stem")? - .extract()?, - directory: model_file - .getattr("relative_path")? - .getattr("parent")? + name: native_field(&native_field(&model_file, "file_path")?, "stem")?.extract()?, + directory: native_field(&native_field(&model_file, "relative_path")?, "parent")? .str()? .to_string(), tags: tags( - &model_input - .getattr("config")? - .getattr("values")? + &native_field(&native_field(model_input, "config")?, "values")? .call_method1("get", ("tags",))?, )?, references: references(reference_rows), @@ -205,7 +198,7 @@ fn model_facts(model_input: &Bound<'_, PyAny>, checks: &Bound<'_, PyAny>) -> PyR /// Every item of `inputs.`. fn items<'py>(inputs: &Bound<'py, PyAny>, attribute: &str) -> PyResult>> { - inputs.getattr(attribute)?.try_iter()?.collect() + native_field(inputs, attribute)?.try_iter()?.collect() } /// The native assembly request for compile inputs, and the exceptions opaque vars raise. @@ -227,7 +220,7 @@ fn project_request<'py>( }) }; let defaults: Bound<'py, PyAny> = - native_field(inputs, "project_config")?.getattr("defaults")?; + native_field(&native_field(inputs, "project_config")?, "defaults")?; let defaults = SeedDefaults { database: optional_text(&defaults, "database")?, schema: optional_text(&defaults, "schema")?, @@ -240,8 +233,7 @@ fn project_request<'py>( let entry: Bound<'py, PyAny> = native_field(seed_input, "schema_entry")?; seeds.push(SeedFacts { name: native_field(&entry, "name")?.extract()?, - tags: entry - .getattr("tags")? + tags: native_field(&entry, "tags")? .try_iter()? .map(|tag| tag?.extract()) .collect::>()?, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/project_assembly/syntax_checks.rs b/crates/sqlbuild-python/src/bindings/_helpers/project_assembly/syntax_checks.rs index cf8b344cf6..cd5f88fbe5 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/project_assembly/syntax_checks.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/project_assembly/syntax_checks.rs @@ -13,18 +13,15 @@ use crate::bindings::_helpers::records::fields::field as native_field; const HOOK_NAMES: [&str; 2] = ["pre_hooks", "post_hooks"]; fn config_value<'py>(model_input: &Bound<'py, PyAny>, key: &str) -> PyResult> { - model_input - .getattr("config")? - .getattr("values")? - .call_method1("get", (key,)) + native_field(&native_field(model_input, "config")?, "values")?.call_method1("get", (key,)) } fn model_name(model_input: &Bound<'_, PyAny>) -> PyResult { - model_input - .getattr("model_file")? - .getattr("file_path")? - .getattr("stem")? - .extract() + native_field( + &native_field(&native_field(model_input, "model_file")?, "file_path")?, + "stem", + )? + .extract() } /// The model's `@@@name` placeholder defaults as strings, or `None`. diff --git a/crates/sqlbuild-python/src/bindings/_helpers/project_assembly/syntax_validation.rs b/crates/sqlbuild-python/src/bindings/_helpers/project_assembly/syntax_validation.rs index b8ded37b92..dc5226d709 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/project_assembly/syntax_validation.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/project_assembly/syntax_validation.rs @@ -11,6 +11,7 @@ use sqlbuild_analysis::assembly::project::main::sql_analysis_opt_out_help::sql_a use sqlbuild_analysis::assembly::project::models::{SyntaxCheck, SyntaxMode}; use crate::bindings::_helpers::project_assembly::resources::syntax_error_message; +use crate::bindings::_helpers::records::construct::record_class; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -162,8 +163,8 @@ pub(crate) fn validate_hook_sql_syntax( model_name: &model_name, placeholders: &placeholders, dialect: &dialect, - sql_hook_entry: registry::class(py, RecordId::SqlHookEntry)?, - python_hook_entry: registry::class(py, RecordId::PythonHookEntry)?, + sql_hook_entry: record_class(py, RecordId::SqlHookEntry)?, + python_hook_entry: record_class(py, RecordId::PythonHookEntry)?, }; check_hook_value( &context, @@ -229,7 +230,7 @@ pub(crate) fn hook_statements( /// The hook SQL strings a hook value holds, in validation order. #[pyfunction] pub(crate) fn hook_sql_statements(value: Bound<'_, PyAny>) -> PyResult> { - let sql_hook_entry: Bound<'_, PyAny> = registry::class(value.py(), RecordId::SqlHookEntry)?; + let sql_hook_entry: Bound<'_, PyAny> = record_class(value.py(), RecordId::SqlHookEntry)?; hook_statements(&value, &sql_hook_entry) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/query_command/dag.rs b/crates/sqlbuild-python/src/bindings/_helpers/query_command/dag.rs index f1f72fbcec..183172518e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/query_command/dag.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/query_command/dag.rs @@ -6,12 +6,14 @@ use pyo3::types::{PyBool, PyDict}; use pyo3::{pyfunction, wrap_pyfunction}; use sqlbuild_core::query_command::main::render_dag_summary::render_dag_summary; -use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; +use crate::bindings::_helpers::compile_driver::project::build_project_graph; +use crate::bindings::_helpers::compile_driver::python_calls::KeywordArguments; +use crate::bindings::_helpers::compile_driver::python_calls::keywords; +use crate::bindings::_helpers::dag_artifact::artifact::build_dag_json; +use crate::bindings::_helpers::dbt_interop::reference_resolver::resolve_external_sql_reference_resolver; use crate::bindings::_helpers::graph::python_node_graph::python_node_graph; use crate::bindings::_helpers::query_command::project::{ProjectAdapter, project_adapter}; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::NativeAttr; -use crate::bindings::_helpers::records::registry; fn array_length(document: &serde_json::Value, key: &str) -> PyResult { document @@ -24,7 +26,10 @@ fn array_length(document: &serde_json::Value, key: &str) -> PyResult { /// Run one `sqb dag` invocation. #[pyfunction] #[pyo3(signature = (**options))] -fn run_dag_command<'py>(py: Python<'py>, options: Option>) -> PyResult { +pub(crate) fn run_dag_command<'py>( + py: Python<'py>, + options: Option>, +) -> PyResult { let arguments: KeywordArguments<'py> = KeywordArguments::read( py, "run_dag_command", @@ -43,39 +48,31 @@ fn run_dag_command<'py>(py: Python<'py>, options: Option>) -> discovered, adapter, } = project_adapter(py, project_dir)?; - let resolver: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ResolveExternalSqlReferenceResolver)?, - &[ - ("project_dir", project_dir), - ("discovered_inputs", discovered.clone()), - ], - )?; - let graph: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::BuildProjectGraph)?, - &[ - ("discovered_inputs", discovered.clone()), - ("adapter", adapter), - ( - "no_sql_validation", - PyBool::new(py, no_sql_validation).to_owned().into_any(), - ), - ("cli_vars", cli_vars), - ("external_sql_reference_resolver", resolver), - ], + let resolver: Bound<'py, PyAny> = + resolve_external_sql_reference_resolver(project_dir, discovered.clone())?; + let graph: Bound<'py, PyAny> = build_project_graph( + py, + Some(keywords( + py, + &[ + ("discovered_inputs", discovered.clone()), + ("adapter", adapter), + ( + "no_sql_validation", + PyBool::new(py, no_sql_validation).to_owned().into_any(), + ), + ("cli_vars", cli_vars), + ("external_sql_reference_resolver", resolver), + ], + )?), )?; let python_graph: Bound<'py, PyAny> = python_node_graph(&discovered)?; - let dag_json: String = call_with( - ®istry::native(py, NativeAttr::BuildDagJson)?, - &[ - ("graph", graph), - ( - "project_name", - native_field(&discovered, "project_config")?.getattr("name")?, - ), - ("python_graph", python_graph), - ], - )? - .extract()?; + let dag_json: String = build_dag_json( + &graph, + &native_field(&native_field(&discovered, "project_config")?, "name")? + .extract::()?, + Some(&python_graph), + )?; let rendered: String = if json_output { format!("{dag_json}\n") } else { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/query_command/project.rs b/crates/sqlbuild-python/src/bindings/_helpers/query_command/project.rs index 4bd16132a2..eeaaa42ae5 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/query_command/project.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/query_command/project.rs @@ -4,11 +4,10 @@ use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; use crate::bindings::_helpers::command_runtime::adapters::resolve_cli_adapter; use crate::bindings::_helpers::compile_driver::discovery::discover_project_inputs; +use crate::bindings::_helpers::compile_driver::python_calls::imported; use crate::bindings::_helpers::compile_driver::python_calls::keywords; -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; +use crate::bindings::_helpers::contracts::spec_resolution::resolve_effective_adapter_name; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::NativeAttr; -use crate::bindings::_helpers::records::registry; /// The project directory, defaulting to the working directory, with its inputs and adapter. pub(crate) struct ProjectAdapter<'py> { @@ -29,15 +28,9 @@ pub(crate) fn project_adapter<'py>( py, Some(keywords(py, &[("project_dir", project_dir.clone())])?), )?; - let adapter_name: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ResolveEffectiveAdapterName)?, - &[ - ( - "project_config", - native_field(&discovered, "project_config")?, - ), - ("local_config", native_field(&discovered, "local_config")?), - ], + let adapter_name: Bound<'py, PyAny> = resolve_effective_adapter_name( + &native_field(&discovered, "project_config")?, + &native_field(&discovered, "local_config")?, )?; let adapter: Bound<'py, PyAny> = resolve_cli_adapter(&adapter_name, &project_dir)?; Ok(ProjectAdapter { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/query_command/query.rs b/crates/sqlbuild-python/src/bindings/_helpers/query_command/query.rs index 64d6817660..4d8b471f14 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/query_command/query.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/query_command/query.rs @@ -13,6 +13,8 @@ use sqlbuild_core::query_command::main::render_query_long::render_query_long; use sqlbuild_core::query_command::main::render_query_table::render_query_table; use sqlbuild_core::query_command::models::QueryDisplay; +use crate::bindings::_helpers::command_runtime::connection::resolve_project_connection_config; +use crate::bindings::_helpers::compile_driver::python_calls::keywords; use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; use crate::bindings::_helpers::native_host::adapter::AdapterHost; use crate::bindings::_helpers::native_host::host; @@ -21,7 +23,7 @@ use crate::bindings::_helpers::query_command::query_values::{ csv_text, display_text, json_cell, python_json_rows, strict_width, }; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr}; +use crate::bindings::_helpers::records::ids::ErrorId; use crate::bindings::_helpers::records::registry; const LONG_FORMAT: &str = "long"; @@ -146,16 +148,19 @@ fn run_sql<'py>( limit: Option, ) -> PyResult> { let py: Python<'py> = sql.py(); - let connection_config: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ResolveProjectConnectionConfig)?, - &[ - ("discovered_inputs", project.discovered.clone()), - ("project_dir", project.project_dir.clone()), - ( - "selected_target", - selected_target.unwrap_or_else(|| py.None().into_bound(py)), - ), - ], + let connection_config: Bound<'py, PyAny> = resolve_project_connection_config( + py, + Some(keywords( + py, + &[ + ("discovered_inputs", project.discovered.clone()), + ("project_dir", project.project_dir.clone()), + ( + "selected_target", + selected_target.unwrap_or_else(|| py.None().into_bound(py)), + ), + ], + )?), )?; let connection: Bound<'py, PyAny> = project .adapter @@ -199,8 +204,7 @@ fn run_sql<'py>( } fn rows<'py>(result: &Bound<'py, PyAny>) -> PyResult>>> { - result - .getattr("rows")? + native_field(result, "rows")? .try_iter()? .map(|row| row?.try_iter()?.collect::>>()) .collect() @@ -243,8 +247,7 @@ fn render_json( /// The result in the requested format; a statement without columns prints `OK`. fn render(result: &Bound<'_, PyAny>, output_format: &str, limit: Option) -> PyResult { let py: Python<'_> = result.py(); - let columns: Vec = result - .getattr("columns")? + let columns: Vec = native_field(result, "columns")? .try_iter()? .map(|column| column?.extract()) .collect::>()?; @@ -282,7 +285,10 @@ fn render(result: &Bound<'_, PyAny>, output_format: &str, limit: Option) -> /// Execute ad hoc SQL against the active project connection. #[pyfunction] #[pyo3(signature = (**options))] -fn run_query_command(py: Python<'_>, options: Option>) -> PyResult { +pub(crate) fn run_query_command( + py: Python<'_>, + options: Option>, +) -> PyResult { let arguments: KeywordArguments<'_> = KeywordArguments::read( py, "run_query_command", diff --git a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/compiled.rs b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/compiled.rs index 5597535659..c164b65794 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/compiled.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/compiled.rs @@ -4,6 +4,7 @@ use pyo3::IntoPyObjectExt; use pyo3::prelude::{Bound, Py, PyAny, PyAnyMethods, PyErr, PyResult, Python}; use pyo3::types::{PyDict, PyDictMethods, PyList, PyListMethods, PyTuple}; +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::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -35,9 +36,7 @@ const TEST_PARENT_NAME_FIELD: usize = 12; /// The native string enum `class(value)`. fn coerce_native(py: Python<'_>, value: &Py, class: RecordId) -> PyResult> { - Ok(registry::class(py, class)? - .call1((value.bind(py),))? - .unbind()) + Ok(new_record(py, class, (value.bind(py),), None)?.unbind()) } /// `value` coerced to `CompiledResourceType` unless it is `None`. @@ -78,7 +77,7 @@ pub(crate) fn finish_compiler_diagnostic( keywords.set_item("line", values[DIAGNOSTIC_LINE_FIELD].bind(py))?; keywords.set_item("column", values[DIAGNOSTIC_COLUMN_FIELD].bind(py))?; let location: Bound<'_, PyAny> = - registry::class(py, RecordId::SourceLocation)?.call((), Some(&keywords))?; + new_record(py, RecordId::SourceLocation, (), Some(&keywords))?; values[DIAGNOSTIC_LOCATION_FIELD] = location.unbind(); } let location: Bound<'_, PyAny> = values[DIAGNOSTIC_LOCATION_FIELD].bind(py).clone(); @@ -93,8 +92,7 @@ pub(crate) fn finish_compiler_diagnostic( /// `CompilerDiagnostic.is_error`: whether the diagnostic fails the command. pub(crate) fn diagnostic_is_error(instance: &Bound<'_, PyAny>) -> PyResult> { let py: Python<'_> = instance.py(); - let error: Bound<'_, PyAny> = - registry::class(py, RecordId::DiagnosticSeverity)?.getattr("ERROR")?; + let error: Bound<'_, PyAny> = enum_member(py, RecordId::DiagnosticSeverity, "ERROR")?; native_field(instance, "severity")? .eq(error)? .into_py_any(py) @@ -196,7 +194,7 @@ pub(crate) fn finish_sql_test( let parent: Bound<'_, PyAny> = if block_name.is_truthy()? { block_name } else { - native_field(&test_file, "relative_path")?.getattr("stem")? + native_field(&native_field(&test_file, "relative_path")?, "stem")? }; values[TEST_PARENT_NAME_FIELD] = parent.unbind(); } @@ -226,7 +224,7 @@ pub(crate) fn record_reference_rows(instance: &Bound<'_, PyAny>) -> PyResult, name: String) -> PyResult> { - let values: Bound<'_, PyAny> = native_field(instance, "config")?.getattr("values")?; + let values: Bound<'_, PyAny> = native_field(&native_field(instance, "config")?, "values")?; entry_relation_key_row( "ref", name, @@ -238,11 +236,11 @@ fn model_relation_key_row(instance: &Bound<'_, PyAny>, name: String) -> PyResult /// `CompileModelInput.relation_key_row`: keyed by the model file's stem. pub(crate) fn input_relation_key_row(instance: &Bound<'_, PyAny>) -> PyResult> { - let name: String = instance - .getattr("model_file")? - .getattr("file_path")? - .getattr("stem")? - .extract()?; + let name: String = native_field( + &native_field(&native_field(instance, "model_file")?, "file_path")?, + "stem", + )? + .extract()?; model_relation_key_row(instance, name) } @@ -362,8 +360,12 @@ pub(crate) fn build_context_declaration_expansion( let keywords: Bound<'_, PyDict> = PyDict::new(py); keywords.set_item( "declarations", - registry::class(py, RecordId::DeclarationResolutionContext)? - .call((), Some(&declarations))?, + new_record( + py, + RecordId::DeclarationResolutionContext, + (), + Some(&declarations), + )?, )?; keywords.set_item("value_renderer", native_field(instance, "value_renderer")?)?; keywords.set_item( @@ -371,9 +373,13 @@ pub(crate) fn build_context_declaration_expansion( native_field(instance, "collection_rendering")?, )?; keywords.set_item("resolver", native_field(instance, "declaration_resolver")?)?; - Ok(registry::class(py, RecordId::DeclarationExpansionContext)? - .call((), Some(&keywords))? - .unbind()) + Ok(new_record( + py, + RecordId::DeclarationExpansionContext, + (), + Some(&keywords), + )? + .unbind()) } /// A `CompileInputError` carrying `message`. diff --git a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/cursors.rs b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/cursors.rs index 18e18bd252..e98819a728 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/cursors.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/cursors.rs @@ -7,9 +7,9 @@ use crate::bindings::_helpers::compile_driver::python_calls::imported; use crate::bindings::_helpers::cursor::scalars::parse_value; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; +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::ids::RecordId; -use crate::bindings::_helpers::records::registry; /// One cursor value conversion. type Conversion = for<'py> fn(&Bound<'py, PyAny>) -> PyResult>; @@ -26,7 +26,7 @@ const CURSOR_ALGEBRA_ERROR: &str = "CursorAlgebraError"; pub(crate) fn inferred<'py>(raw: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = raw.py(); for class in SCALAR_CLASSES { - if raw.is_instance(®istry::class(py, class)?)? { + if raw.is_instance(&record_class(py, class)?)? { return Ok(raw.clone()); } } @@ -121,16 +121,14 @@ pub(crate) fn finish_cursor_snapshot( .collect::>()?; values[13] = PyTuple::new(py, pairs)?.into_any().unbind(); let mode: Bound<'_, PyAny> = - registry::class(py, RecordId::CursorWatermarkMode)?.call1((values[10].bind(py),))?; + enum_from_value(py, RecordId::CursorWatermarkMode, values[10].bind(py))?; values[10] = mode.unbind(); Ok(values) } /// `ModelCursorSnapshot.watermarks_available`: whether every physical watermark was read. pub(crate) fn snapshot_watermarks_available(instance: &Bound<'_, PyAny>) -> PyResult> { - let available: bool = !instance - .getattr("unavailable_watermark_tags")? - .is_truthy()?; + let available: bool = !native_field(instance, "unavailable_watermark_tags")?.is_truthy()?; Ok(pyo3::types::PyBool::new(instance.py(), available) .to_owned() .into_any() @@ -178,7 +176,7 @@ pub(crate) fn finish_input_relation( ) -> PyResult>> { if !values[2].bind(py).is_none() { let grain: Bound<'_, PyAny> = - registry::class(py, RecordId::CursorGrain)?.call1((values[2].bind(py),))?; + enum_from_value(py, RecordId::CursorGrain, values[2].bind(py))?; values[2] = grain.unbind(); } converted(py, values, (&[5, 6], optional_inferred)) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/discovery.rs b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/discovery.rs index bf2008d88c..b9eab8efd4 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/discovery.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/discovery.rs @@ -16,10 +16,7 @@ const SCALAR_TYPE_LABELS: [(&str, &str); 6] = [ /// `ConstantDeclaration.logical_type`: the normalized logical type. pub(crate) fn constant_logical_type(declaration: &Bound<'_, PyAny>) -> PyResult> { - Ok(declaration - .getattr("value")? - .getattr("logical_type")? - .unbind()) + Ok(native_field(&native_field(declaration, "value")?, "logical_type")?.unbind()) } /// `ConstantDeclaration.scalar_type`: the SQL scalar type label, or the upper-cased logical type. @@ -29,9 +26,7 @@ pub(crate) fn constant_scalar_type(declaration: &Bound<'_, PyAny>) -> PyResult

( } fn part(duration: &Bound<'_, PyAny>, name: &str) -> PyResult { - duration.getattr(name)?.extract() + native_field(duration, name)?.extract() } fn total_months(duration: &Bound<'_, PyAny>) -> PyResult { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/error_inits.rs b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/error_inits.rs index 5d07014474..5bba071938 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/error_inits.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/error_inits.rs @@ -104,7 +104,7 @@ fn macro_argument_copy<'py>( if name == LOCATION_FIELD { continue; } - keywords.set_item(name, error.getattr(name)?)?; + keywords.set_item(name, native_field(error, name)?)?; } let help: Bound<'py, PyAny> = native_field(error, "help")?; if !help.is_truthy()? { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/freshness.rs b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/freshness.rs index 0fdfe4b00e..16443fb97d 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/freshness.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/freshness.rs @@ -1,11 +1,12 @@ //! Validation and identity of the natively hosted source freshness state record. -use pyo3::prelude::{Bound, Py, PyAny, PyAnyMethods, PyResult, Python}; +use pyo3::prelude::{Bound, Py, PyAny, PyResult, Python}; use pyo3::types::PyDict; use crate::bindings::_helpers::freshness::observation::require_aware_utc_datetime; +use crate::bindings::_helpers::records::construct::new_record; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; /// `SourceFreshnessRecord.observed_at` position. const OBSERVED_AT_FIELD: usize = 9; @@ -32,9 +33,7 @@ pub(crate) fn freshness_record_identity(instance: &Bound<'_, PyAny>) -> PyResult let py: Python<'_> = instance.py(); let keywords: Bound<'_, PyDict> = PyDict::new(py); for name in IDENTITY_FIELDS { - pyo3::types::PyDictMethods::set_item(&keywords, name, instance.getattr(name)?)?; + pyo3::types::PyDictMethods::set_item(&keywords, name, native_field(instance, name)?)?; } - Ok(registry::class(py, RecordId::SourceFreshnessIdentity)? - .call((), Some(&keywords))? - .unbind()) + Ok(new_record(py, RecordId::SourceFreshnessIdentity, (), Some(&keywords))?.unbind()) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/lexical.rs b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/lexical.rs index 92ca032d2c..3d65e47dca 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/lexical.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/lexical.rs @@ -29,7 +29,7 @@ fn sorted_texts(values: &Bound<'_, PyAny>) -> PyResult> { /// The lexical rules a `SqlLexicalSyntax` record holds. fn record_syntax(instance: &Bound<'_, PyAny>) -> PyResult { - let flag = |name: &str| -> PyResult { instance.getattr(name)?.is_truthy() }; + let flag = |name: &str| -> PyResult { native_field(instance, name)?.is_truthy() }; Ok(LexicalSyntax { backslash_escape_quotes: sorted_texts(&native_field(instance, "backslash_escape_quotes")?)?, escape_string_prefix: flag("escape_string_prefix")?, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/lineage_facts.rs b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/lineage_facts.rs index 6427234029..d4cab8b10a 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/lineage_facts.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/lineage_facts.rs @@ -6,9 +6,9 @@ use pyo3::prelude::{Bound, Py, PyAny, PyAnyMethods, PyResult, Python}; use pyo3::types::{PyDict, PyDictMethods, PySlice, PyTuple, PyTupleMethods}; use crate::bindings::_helpers::record_behaviours::call_arguments::call_argument; +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::ids::RecordId; -use crate::bindings::_helpers::records::registry; /// `ColumnTransformKind` members by native transform code. const TRANSFORM_KINDS: [&str; 6] = [ @@ -41,7 +41,7 @@ fn coded_member( let Some(member) = members.get(code) else { return Err(PyIndexError::new_err("tuple index out of range")); }; - Ok(registry::member(py, class, member)?.unbind()) + Ok(enum_member(py, class, member)?.unbind()) } /// `CompactLineageFacts.resource_name(index)`: the model's name for a native relation index. @@ -54,9 +54,8 @@ fn resource_name<'py>( if !name.is_none() { return Ok(name); } - let pooled: Bound<'py, PyAny> = facts - .getattr("resource_name_indexes")? - .call_method1("get", (index, index))?; + let pooled: Bound<'py, PyAny> = + native_field(facts, "resource_name_indexes")?.call_method1("get", (index, index))?; native_field(facts, "string_pool")?.get_item(pooled) } @@ -80,7 +79,6 @@ fn project_row(facts: &Bound<'_, PyAny>, row: &Bound<'_, PyAny>) -> PyResult, ) = row.extract()?; - let source_class: Bound<'_, PyAny> = registry::class(py, RecordId::CompiledLineageSourceFact)?; let mut upstream: Vec> = Vec::new(); for source in sources.try_iter()? { let source: Bound<'_, PyTuple> = source?.cast_into::()?; @@ -88,7 +86,12 @@ fn project_row(facts: &Bound<'_, PyAny>, row: &Bound<'_, PyAny>) -> PyResult = PyDict::new(py); keywords.set_item("output_column", pool.get_item(name_index)?)?; @@ -109,15 +112,12 @@ fn project_row(facts: &Bound<'_, PyAny>, row: &Bound<'_, PyAny>) -> PyResult) -> PyResult> { - let projected: Vec> = instance - .getattr("rows")? + let projected: Vec> = native_field(instance, "rows")? .try_iter()? .map(|row| project_row(instance, &row?)) .collect::>()?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/macros.rs b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/macros.rs index 9acd116b41..7e6704149a 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/macros.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/macros.rs @@ -23,8 +23,7 @@ pub(crate) fn macro_injects_context(instance: &Bound<'_, PyAny>) -> PyResult = py.import("inspect")?.into_any(); let parameters: Bound<'_, PyTuple> = PyTuple::new( py, - inspect - .getattr("signature")? + native_field(&inspect, "signature")? .call1((native_field(instance, "function")?,))? .getattr("parameters")? .call_method0("values")? @@ -59,9 +58,8 @@ pub(crate) fn context_render_constant( )?); } let _ = native_field(instance, "constants")?.get_item(&name)?; - let declaration: Bound<'_, PyAny> = instance - .getattr("_constant_declarations")? - .call_method1("get", (&name,))?; + let declaration: Bound<'_, PyAny> = + native_field(instance, "_constant_declarations")?.call_method1("get", (&name,))?; if declaration.is_none() { return Err(compile_input_error( py, @@ -89,8 +87,7 @@ pub(crate) fn context_render_enum_member( } let enum_name: Bound<'_, PyAny> = call_argument((args, kwargs), "enum_name", 0)?; let member_name: Bound<'_, PyAny> = call_argument((args, kwargs), "member_name", 1)?; - let value: Bound<'_, PyAny> = instance - .getattr("enums")? + let value: Bound<'_, PyAny> = native_field(instance, "enums")? .get_item(enum_name)? .get_item(member_name)?; render_enum_member_value(&value)?.into_py_any(instance.py()) @@ -146,13 +143,9 @@ fn resource_rows( /// A model's configured tags and folder. fn model_tags(model: &Bound<'_, PyAny>) -> PyResult { - let tags: Bound<'_, PyAny> = model - .getattr("config")? - .getattr("values")? - .call_method1("get", ("tags",))?; - let folder: String = model - .getattr("relative_path")? - .getattr("parent")? + let tags: Bound<'_, PyAny> = + native_field(&native_field(model, "config")?, "values")?.call_method1("get", ("tags",))?; + let folder: String = native_field(&native_field(model, "relative_path")?, "parent")? .str()? .extract()?; Ok((graph_tags(&tags)?, Some(folder))) @@ -165,9 +158,7 @@ fn source_tags(_source: &Bound<'_, PyAny>) -> PyResult { /// A seed's schema entry tags. fn seed_tags(seed: &Bound<'_, PyAny>) -> PyResult { - let tags: Vec = seed - .getattr("schema_entry")? - .getattr("tags")? + let tags: Vec = native_field(&native_field(seed, "schema_entry")?, "tags")? .try_iter()? .map(|tag| tag?.extract()) .collect::>()?; @@ -176,8 +167,7 @@ fn seed_tags(seed: &Bound<'_, PyAny>) -> PyResult { /// A function's tags. fn function_tags(function: &Bound<'_, PyAny>) -> PyResult { - let tags: Vec = function - .getattr("tags")? + let tags: Vec = native_field(function, "tags")? .try_iter()? .map(|tag| tag?.extract()) .collect::>()?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/migrations.rs b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/migrations.rs index 2b7d5b410e..019a336fb0 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/migrations.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/migrations.rs @@ -10,9 +10,9 @@ use sqlbuild_core::migration_values::main::plan_label::plan_label; use sqlbuild_core::migration_values::models::{ColumnDecisionTraits, DecisionTraits}; use crate::bindings::_helpers::record_behaviours::call_arguments::call_argument; +use crate::bindings::_helpers::records::construct::enum_member; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; fn value(member: &Bound<'_, PyAny>) -> PyResult { member.str()?.extract() @@ -194,17 +194,15 @@ pub(crate) fn history_status( } else { "PENDING_ARCHIVE" }; - Ok(registry::class(py, RecordId::OldNameViewStatus)? - .getattr(status)? - .unbind()) + Ok(enum_member(py, RecordId::OldNameViewStatus, status)?.unbind()) } /// `OldNameViewHistory.old`: the relation name the compatibility view occupies. pub(crate) fn history_old(history: &Bound<'_, PyAny>) -> PyResult> { - Ok(native_field(history, "required")?.getattr("old")?.unbind()) + Ok(native_field(&native_field(history, "required")?, "old")?.unbind()) } /// `OldNameViewHistory.new`: the relation the compatibility view reads. pub(crate) fn history_new(history: &Bound<'_, PyAny>) -> PyResult> { - Ok(native_field(history, "required")?.getattr("new")?.unbind()) + Ok(native_field(&native_field(history, "required")?, "new")?.unbind()) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/planner.rs b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/planner.rs index f7f6d813a8..9d23534fc3 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/planner.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/planner.rs @@ -12,6 +12,9 @@ use sqlbuild_core::migration_values::models::{DecisionTraits, ModelMigrationFact use crate::bindings::_helpers::compile_driver::python_calls::imported; use crate::bindings::_helpers::cursor::scalars::parse_value; use crate::bindings::_helpers::record_behaviours::call_arguments::call_argument; +use crate::bindings::_helpers::records::construct::{ + enum_from_value, enum_member, new_record, record_class, +}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -57,7 +60,7 @@ pub(crate) fn old_name_runs_steps(instance: &Bound<'_, PyAny>) -> PyResult) -> PyResult> { for name in ["python_source_read_map", "source_read_map"] { - let entries: Bound<'_, PyAny> = instance.getattr(name)?; + let entries: Bound<'_, PyAny> = native_field(instance, name)?; if entries.is_truthy()? { return Ok(entries.unbind()); } @@ -211,7 +214,7 @@ fn override_integer<'py>( } let parsed: Bound<'py, PyAny> = parse_value(value, &PyString::new(py, INTEGER_CURSOR).into_any())?; - if parsed.is_instance(®istry::class(py, RecordId::IntegerValue)?)? { + if parsed.is_instance(&record_class(py, RecordId::IntegerValue)?)? { return Ok(parsed); } Err(invalid()) @@ -262,7 +265,7 @@ pub(crate) fn overrides_typed_as( return Ok(instance.clone().unbind()); } let cursor_type: String = text(&call_argument((args, kwargs), "cursor_type", 0)?)?; - let field = |name: &str| instance.getattr(name); + let field = |name: &str| native_field(instance, name); let (start, end) = (field(OVERRIDE_FIELDS[4])?, field(OVERRIDE_FIELDS[5])?); let keywords: Bound<'_, PyDict> = PyDict::new(py); if cursor_type == INTEGER_CURSOR { @@ -305,9 +308,9 @@ pub(crate) fn forward_only_backfill(py: Python<'_>) -> PyResult let keywords: Bound<'_, PyDict> = PyDict::new(py); keywords.set_item( "action", - registry::class(py, RecordId::BackfillAction)?.getattr("FORWARD_ONLY")?, + enum_member(py, RecordId::BackfillAction, "FORWARD_ONLY")?, )?; - registry::class(py, RecordId::BackfillResult)?.call((), Some(&keywords)) + new_record(py, RecordId::BackfillResult, (), Some(&keywords)) } /// `ScenarioArtifactIdentity.kind` coerced to `ScenarioArtifactKind`. @@ -316,9 +319,7 @@ pub(crate) fn finish_scenario_artifact_kind( mut values: Vec>, ) -> PyResult>> { if let Some(kind) = values.first_mut() { - *kind = registry::class(py, RecordId::ScenarioArtifactKind)? - .call1((kind.bind(py),))? - .unbind(); + *kind = enum_from_value(py, RecordId::ScenarioArtifactKind, kind.bind(py))?.unbind(); } Ok(values) } @@ -330,8 +331,7 @@ pub(crate) fn rename_hint_message(hint: &Bound<'_, PyAny>) -> PyResult } else { "similar to" }; - let candidates: Vec = hint - .getattr("candidate_columns")? + let candidates: Vec = native_field(hint, "candidate_columns")? .try_iter()? .map(|candidate| Ok(candidate?.str()?.to_string())) .collect::>()?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/rule_shapes.rs b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/rule_shapes.rs index 833a295f66..deab03422d 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/rule_shapes.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/rule_shapes.rs @@ -6,9 +6,9 @@ use pyo3::prelude::{Bound, Py, PyAny, PyAnyMethods, PyResult, Python}; use pyo3::types::{PyDict, PyDictMethods, PyTuple, PyTupleMethods}; use crate::bindings::_helpers::compile_driver::python_calls::imported; +use crate::bindings::_helpers::records::construct::{enum_member, new_record, record_class}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; /// Keyword fields every `RuleOption` constructor requires. const REQUIRED_OPTION_FIELDS: [&str; 3] = ["name", "default", "description"]; @@ -38,18 +38,15 @@ pub(crate) fn project_path_depth(instance: &Bound<'_, PyAny>) -> PyResult) -> PyResult> { - let project: Bound<'_, PyAny> = - registry::class(instance.py(), RecordId::RuleSubject)?.getattr("PROJECT")?; - instance - .getattr("subject")? + let project: Bound<'_, PyAny> = enum_member(instance.py(), RecordId::RuleSubject, "PROJECT")?; + native_field(instance, "subject")? .is(&project) .into_py_any(instance.py()) } /// `RuleResult.finding_count`. pub(crate) fn rule_result_finding_count(instance: &Bound<'_, PyAny>) -> PyResult> { - instance - .getattr("findings")? + native_field(instance, "findings")? .len()? .into_py_any(instance.py()) } @@ -61,7 +58,7 @@ pub(crate) fn rule_option_class_getitem( _kwargs: Option<&Bound<'_, PyDict>>, ) -> PyResult> { let py: Python<'_> = receiver.py(); - let class: Bound<'_, PyAny> = registry::class(py, RecordId::RuleOption)?; + let class: Bound<'_, PyAny> = record_class(py, RecordId::RuleOption)?; Ok(imported(py, "types", "GenericAlias")? .call1((class, args.get_item(0)?))? .unbind()) @@ -102,9 +99,7 @@ fn rule_option( for (name, value) in extra { fields.set_item(*name, value)?; } - Ok(registry::class(py, RecordId::RuleOption)? - .call((), Some(&fields))? - .unbind()) + Ok(new_record(py, RecordId::RuleOption, (), Some(&fields))?.unbind()) } /// The builtin type `name`. diff --git a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/scopes.rs b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/scopes.rs index 4778b7c67b..2e33a0e9d5 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/scopes.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/scopes.rs @@ -18,7 +18,7 @@ const COMPLETENESS_SECTIONS: [&str; 6] = [ pub(crate) fn completeness_complete(instance: &Bound<'_, PyAny>) -> PyResult> { let mut complete: bool = true; for section in COMPLETENESS_SECTIONS { - complete = complete && instance.getattr(section)?.is_truthy()?; + complete = complete && native_field(instance, section)?.is_truthy()?; } complete.into_py_any(instance.py()) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/values.rs b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/values.rs index 2a836823f1..29ff62861c 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/values.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/values.rs @@ -7,9 +7,7 @@ use pyo3::types::{PyDict, PyDictMethods, PyTuple}; /// `SqlLogicalType.display_name`: the kind, with its element type in angle brackets. pub(crate) fn logical_type_display_name(instance: &Bound<'_, PyAny>) -> PyResult> { - let kind: String = native_field(instance, "kind")? - .getattr("value")? - .extract()?; + let kind: String = native_field(&native_field(instance, "kind")?, "value")?.extract()?; let element: Bound<'_, PyAny> = native_field(instance, "element_type")?; if element.is_none() { return kind.into_py_any(instance.py()); @@ -20,9 +18,7 @@ pub(crate) fn logical_type_display_name(instance: &Bound<'_, PyAny>) -> PyResult /// `SqlValue.kind`: the outer logical kind. pub(crate) fn sql_value_kind(instance: &Bound<'_, PyAny>) -> PyResult> { - Ok(native_field(instance, "logical_type")? - .getattr("kind")? - .unbind()) + Ok(native_field(&native_field(instance, "logical_type")?, "kind")?.unbind()) } /// `FingerprintSet`: index the fingerprints by `(node_type, node_name)` when not given. diff --git a/crates/sqlbuild-python/src/bindings/_helpers/records/construct.rs b/crates/sqlbuild-python/src/bindings/_helpers/records/construct.rs new file mode 100644 index 0000000000..1375da8705 --- /dev/null +++ b/crates/sqlbuild-python/src/bindings/_helpers/records/construct.rs @@ -0,0 +1,70 @@ +//! Native record classes and string enums held natively, so records are built without a module lookup. + +use pyo3::IntoPyObject; +use pyo3::call::PyCallArgs; +use pyo3::exceptions::PyRuntimeError; +use pyo3::prelude::{Bound, Py, PyAny, PyAnyMethods, PyModule, PyResult, Python}; +use pyo3::sync::PyOnceLock; +use pyo3::types::PyDict; + +use crate::bindings::_helpers::compile_driver::python_calls::keywords; +use crate::bindings::_helpers::records::ids::RecordId; + +/// Each `RecordId`'s class, captured when the extension module is initialised. +static CLASSES: PyOnceLock>> = PyOnceLock::new(); + +/// Hold every registered record class and string enum, read once while the module initialises. +pub(crate) fn capture(module: &Bound<'_, PyModule>) -> PyResult<()> { + CLASSES.get_or_try_init(module.py(), || { + RecordId::ALL + .iter() + .map(|id| module.getattr(id.name()).map(Bound::unbind)) + .collect::>>>() + })?; + Ok(()) +} + +/// The native record class or string enum `id` as registered, ignoring later attribute replacement. +pub(crate) fn record_class(py: Python<'_>, id: RecordId) -> PyResult> { + let classes: &Vec> = CLASSES + .get(py) + .ok_or_else(|| PyRuntimeError::new_err("native record classes are not registered"))?; + Ok(classes[id as usize].bind(py).clone()) +} + +/// A new `id` record built exactly as calling its class builds it, finishers included. +pub(crate) fn new_record<'py, A: PyCallArgs<'py>>( + py: Python<'py>, + id: RecordId, + args: A, + kwargs: Option<&Bound<'py, PyDict>>, +) -> PyResult> { + record_class(py, id)?.call(args, kwargs) +} + +/// A new `id` record built from `(field, value)` keyword pairs. +pub(crate) fn new_record_with<'py>( + py: Python<'py>, + id: RecordId, + pairs: &[(&str, Bound<'py, PyAny>)], +) -> PyResult> { + new_record(py, id, (), Some(&keywords(py, pairs)?)) +} + +/// The member `name` of the native string enum `id`, looked up rather than constructed. +pub(crate) fn enum_member<'py>( + py: Python<'py>, + id: RecordId, + name: &str, +) -> PyResult> { + record_class(py, id)?.getattr(name) +} + +/// The member of the native string enum `id` whose value is `value`, as `Enum(value)` finds it. +pub(crate) fn enum_from_value<'py, V: IntoPyObject<'py>>( + py: Python<'py>, + id: RecordId, + value: V, +) -> PyResult> { + record_class(py, id)?.call1((value,)) +} diff --git a/crates/sqlbuild-python/src/bindings/_helpers/records/fields.rs b/crates/sqlbuild-python/src/bindings/_helpers/records/fields.rs index c462f7d879..b722ead7da 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/records/fields.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/records/fields.rs @@ -16,8 +16,8 @@ use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::records::constants::{ FROZEN_INSTANCE_ERROR, ORDERED_RECORDS, RECORD_FINISHERS, RECORD_SPEC_ATTRIBUTE, RECORDS, }; +use crate::bindings::_helpers::records::construct::{enum_member, new_record}; use crate::bindings::_helpers::records::models::{FieldDefault, NativeRecord, RecordSpec}; -use crate::bindings::_helpers::records::registry; /// Every registered record spec, in registration order. pub(crate) fn all_specs() -> impl Iterator { @@ -60,11 +60,20 @@ pub(crate) fn replaced<'py>( value: &Bound<'py, PyAny>, changes: &[(&str, Bound<'py, PyAny>)], ) -> PyResult> { - let py: Python<'py> = value.py(); - let keywords: Bound<'py, PyDict> = PyDict::new(py); + let keywords: Bound<'py, PyDict> = PyDict::new(value.py()); for (name, changed) in changes { keywords.set_item(*name, changed)?; } + replaced_keywords(value, &keywords) +} + +/// `dataclasses.replace(value, **keywords)`: a native record is rebuilt from its own values. +pub(crate) fn replaced_keywords<'py>( + value: &Bound<'py, PyAny>, + keywords: &Bound<'py, PyDict>, +) -> PyResult> { + let py: Python<'py> = value.py(); + let keywords: Bound<'py, PyDict> = keywords.copy()?; let Ok(record) = value.cast::() else { return imported(py, "dataclasses", "replace")?.call((value,), Some(&keywords)); }; @@ -157,8 +166,8 @@ fn default_value<'py>( FieldDefault::EmptyFrozenset => Ok(PyFrozenSet::empty(py)?.into_any()), FieldDefault::Member(module, class, member) => imported(py, module, class)?.getattr(member), FieldDefault::HostMember(class, member) => host::entry(py, class)?.getattr(member), - FieldDefault::NativeMember(class, member) => registry::class(py, class)?.getattr(member), - FieldDefault::NativeCall(class) => registry::class(py, class)?.call0(), + FieldDefault::NativeMember(class, member) => enum_member(py, class, member), + FieldDefault::NativeCall(class) => new_record(py, class, (), None), FieldDefault::Factory(factory) => factory(py), FieldDefault::CallText(module, name, text) => imported(py, module, name)?.call1((text,)), } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/records/ids.rs b/crates/sqlbuild-python/src/bindings/_helpers/records/ids.rs index f6a44b1f4d..5a02286c8a 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/records/ids.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/records/ids.rs @@ -1018,42 +1018,9 @@ native_ids! { } } -/// Any registered native name, for helpers that accept records, errors and module attributes. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) enum NativeItem { - Record(RecordId), - Error(ErrorId), - Attr(NativeAttr), -} - impl RecordId { /// The record registered under `name`, for class names a pure-Rust crate chose. pub(crate) fn from_name(name: &str) -> Option { Self::ALL.iter().copied().find(|id| id.name() == name) } } - -impl NativeAttr { - /// The attribute registered under `name`, for function names a pure-Rust crate chose. - pub(crate) fn from_name(name: &str) -> Option { - Self::ALL.iter().copied().find(|id| id.name() == name) - } -} - -impl From for NativeItem { - fn from(id: RecordId) -> Self { - Self::Record(id) - } -} - -impl From for NativeItem { - fn from(id: ErrorId) -> Self { - Self::Error(id) - } -} - -impl From for NativeItem { - fn from(id: NativeAttr) -> Self { - Self::Attr(id) - } -} diff --git a/crates/sqlbuild-python/src/bindings/_helpers/records/mod.rs b/crates/sqlbuild-python/src/bindings/_helpers/records/mod.rs index 7d5d3b8228..ea4f36f1a0 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/records/mod.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/records/mod.rs @@ -1,4 +1,5 @@ pub(crate) mod constants; +pub(crate) mod construct; pub(crate) mod error_classes; pub(crate) mod fields; pub(crate) mod ids; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/records/registry.rs b/crates/sqlbuild-python/src/bindings/_helpers/records/registry.rs index abc0274b79..0d0d3c3c17 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/records/registry.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/records/registry.rs @@ -9,12 +9,10 @@ use pyo3::types::PyString; use crate::bindings::_helpers::records::constants::{ENUMS, ERRORS, NATIVE_MODULE}; use crate::bindings::_helpers::records::fields::all_specs; -use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr, NativeItem, RecordId}; +use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr, RecordId}; /// The extension module, imported once for attribute lookups. static MODULE: PyOnceLock> = PyOnceLock::new(); -/// Each `RecordId`'s interned attribute name, by variant position. -static CLASS_NAMES: PyOnceLock>> = PyOnceLock::new(); /// Each `ErrorId`'s interned attribute name, by variant position. static ERROR_NAMES: PyOnceLock>> = PyOnceLock::new(); /// Each `NativeAttr`'s interned attribute name, by variant position. @@ -98,16 +96,6 @@ pub(crate) fn verify_attributes(module: &Bound<'_, PyModule>) -> PyResult<()> { ))) } -/// The native record class or string enum `id`, read now so a replaced attribute is seen. -pub(crate) fn class(py: Python<'_>, id: RecordId) -> PyResult> { - attribute( - py, - &CLASS_NAMES, - RecordId::ALL.iter().map(|id| id.name()), - id as usize, - ) -} - /// The native error class `id`, read now so a replaced attribute is seen. pub(crate) fn error(py: Python<'_>, id: ErrorId) -> PyResult> { attribute( @@ -128,24 +116,6 @@ pub(crate) fn native(py: Python<'_>, id: NativeAttr) -> PyResult( - py: Python<'py>, - id: RecordId, - name: &str, -) -> PyResult> { - class(py, id)?.getattr(name) -} - -/// Any registered native name. -pub(crate) fn item(py: Python<'_>, id: impl Into) -> PyResult> { - match id.into() { - NativeItem::Record(id) => class(py, id), - NativeItem::Error(id) => error(py, id), - NativeItem::Attr(id) => native(py, id), - } -} - /// The native record class or string enum a pure-Rust crate names, failing as `getattr` would. pub(crate) fn record_named(name: &str) -> PyResult { RecordId::from_name(name).ok_or_else(|| { @@ -154,12 +124,3 @@ pub(crate) fn record_named(name: &str) -> PyResult { )) }) } - -/// The native module attribute a pure-Rust crate names, failing as `getattr` would. -pub(crate) fn attribute_named(name: &str) -> PyResult { - NativeAttr::from_name(name).ok_or_else(|| { - PyAttributeError::new_err(format!( - "module 'sqlbuild._native' has no attribute '{name}'" - )) - }) -} diff --git a/crates/sqlbuild-python/src/bindings/_helpers/refactor/contract_compare.rs b/crates/sqlbuild-python/src/bindings/_helpers/refactor/contract_compare.rs index 66e2939713..cb899e603c 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/refactor/contract_compare.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/refactor/contract_compare.rs @@ -7,14 +7,15 @@ use pyo3::{pyfunction, wrap_pyfunction}; use sqlbuild_core::text::main::python_casefold::python_casefold; use crate::bindings::_helpers::boundary::keyword_request::Request; +use crate::bindings::_helpers::contracts::spec_resolution::matching_dynamic_families; use crate::bindings::_helpers::native_host::adapter::AdapterHost; use crate::bindings::_helpers::native_host::host; +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::ids::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::fields::replaced; +use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::type_system::equality::types_equal as native_types_equal; -const DATACLASSES_MODULE: &str = "dataclasses"; const MODEL_RESOURCE: &str = "model"; const SOURCE_RESOURCE: &str = "source"; @@ -22,15 +23,13 @@ const SOURCE_RESOURCE: &str = "source"; struct Vocabulary<'py> { finding: Bound<'py, PyAny>, kinds: Bound<'py, PyAny>, - replace: Bound<'py, PyAny>, } impl<'py> Vocabulary<'py> { fn new(py: Python<'py>) -> PyResult { Ok(Self { - finding: registry::class(py, RecordId::ContractFinding)?, - kinds: registry::class(py, RecordId::ContractFindingKind)?, - replace: py.import(DATACLASSES_MODULE)?.getattr("replace")?, + finding: record_class(py, RecordId::ContractFinding)?, + kinds: record_class(py, RecordId::ContractFindingKind)?, }) } @@ -61,11 +60,7 @@ impl<'py> Vocabulary<'py> { item: &Bound<'py, PyAny>, fields: Vec<(&str, Bound<'py, PyAny>)>, ) -> PyResult> { - let keywords: Bound<'py, PyDict> = PyDict::new(self.py()); - for (name, value) in fields { - keywords.set_item(name, value)?; - } - self.replace.call((item,), Some(&keywords)) + replaced(item, &fields) } } @@ -88,7 +83,7 @@ fn keyword_call<'py>( fn qualified(item: &Bound<'_, PyAny>) -> PyResult { let mut parts: Vec = Vec::new(); for name in ["database", "schema", "relation"] { - let part: Option = item.getattr(name)?.extract()?; + let part: Option = native_field(item, name)?.extract()?; if let Some(part) = part.filter(|part| !part.is_empty()) { parts.push(part); } @@ -117,7 +112,7 @@ fn column_infos<'py>( } fn resource_type<'py>(py: Python<'py>, value: &str) -> PyResult> { - registry::class(py, RecordId::CompiledResourceType)?.call1((value,)) + enum_from_value(py, RecordId::CompiledResourceType, value) } /// The evidence a model's authored contract offers, before inspection. @@ -134,7 +129,7 @@ fn model_evidence<'py>(model: &Bound<'py, PyAny>) -> PyResult> ) }; keyword_call( - ®istry::class(py, RecordId::ContractEvidence)?, + &record_class(py, RecordId::ContractEvidence)?, vec![ ("resource_type", resource_type(py, MODEL_RESOURCE)?), ("resource_name", native_field(model, "name")?), @@ -185,7 +180,7 @@ fn source_evidence<'py>( text(py, &relation) }; keyword_call( - ®istry::class(py, RecordId::ContractEvidence)?, + &record_class(py, RecordId::ContractEvidence)?, vec![ ("resource_type", resource), ("resource_name", name), @@ -199,7 +194,7 @@ fn source_evidence<'py>( ("physical_columns", py.None().into_bound(py)), ( "source_path", - native_field(source, "source_file")?.getattr("relative_path")?, + native_field(&native_field(source, "source_file")?, "relative_path")?, ), ("findings", PyTuple::new(py, findings)?.into_any()), ], @@ -252,13 +247,8 @@ fn undeclared_finding<'py>( let py: Python<'py> = vocabulary.py(); let name: String = native_field(physical, "name")?.extract()?; let physical_type: Bound<'py, PyAny> = native_field(physical, "type")?; - let matching: Bound<'py, PyAny> = keyword_call( - ®istry::native(py, NativeAttr::MatchingDynamicFamilies)?, - vec![ - ("families", native_field(item, "dynamic_columns")?), - ("column_name", text(py, &name)), - ], - )?; + let matching: Bound<'py, PyAny> = + matching_dynamic_families(&native_field(item, "dynamic_columns")?, &name)?.into_any(); if matching.len()? == 1 { let family: Bound<'py, PyAny> = matching.get_item(0)?; let family_type: Bound<'py, PyAny> = native_field(&family, "type")?; @@ -376,8 +366,7 @@ fn compared<'py>( vec![("findings", PyTuple::new(py, [finding])?.into_any())], ); } - let declared: Vec> = item - .getattr("declared_columns")? + let declared: Vec> = native_field(item, "declared_columns")? .try_iter()? .collect::>()?; let physical: Vec> = physical_columns.try_iter()?.collect::>()?; @@ -469,7 +458,9 @@ fn relation_info<'py>(item: &Bound<'py, PyAny>) -> PyResult> { /// Selected declarations compared with one bulk read of physical columns. #[pyfunction] #[pyo3(signature = (**request))] -fn compare_contracts<'py>(request: Option<&Bound<'py, PyDict>>) -> PyResult> { +pub(crate) fn compare_contracts<'py>( + request: Option<&Bound<'py, PyDict>>, +) -> PyResult> { let request = Request::new("compare_contracts", request)?; let py: Python<'py> = request.py(); let adapter: &Bound<'py, PyAny> = &request.get("adapter")?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/refactor/contract_edits.rs b/crates/sqlbuild-python/src/bindings/_helpers/refactor/contract_edits.rs index c541041842..b91f063a45 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/refactor/contract_edits.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/refactor/contract_edits.rs @@ -15,6 +15,7 @@ use sqlbuild_refactor::refactoring::models::{ use sqlbuild_refactor::refactoring::types::PhysicalColumn; use crate::bindings::_helpers::boundary::panics::compiler_guard; +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::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -70,7 +71,7 @@ fn physical_rows(columns: &Bound<'_, PyAny>) -> PyResult> { /// The model's MODEL header with physical columns adopted, or the ownership conflict. #[pyfunction] #[pyo3(signature = (*, model, physical_columns, overwrite))] -fn edit_contract_model( +pub(crate) fn edit_contract_model( py: Python<'_>, model: &Bound<'_, PyAny>, physical_columns: &Bound<'_, PyAny>, @@ -116,22 +117,16 @@ fn edit_contract_model( /// The source's YAML declaration with physical columns adopted, or the ownership conflict. #[pyfunction] #[pyo3(signature = (*, source, physical_columns, overwrite, contents = None))] -fn edit_contract_source( +pub(crate) fn edit_contract_source( source: &Bound<'_, PyAny>, physical_columns: &Bound<'_, PyAny>, overwrite: bool, contents: Option, ) -> PyResult<(Option, Option)> { - let discovered_contents: String = source - .getattr("source_file")? - .getattr("contents")? - .extract()?; + let discovered_contents: String = + native_field(&native_field(source, "source_file")?, "contents")?.extract()?; let mut columns: Vec<(String, Option)> = Vec::new(); - for column in source - .getattr("source_entry")? - .getattr("columns")? - .try_iter()? - { + for column in native_field(&native_field(source, "source_entry")?, "columns")?.try_iter()? { let column: Bound<'_, PyAny> = column?; columns.push(( native_field(&column, "name")?.extract()?, @@ -153,7 +148,7 @@ fn edit_contract_source( /// The findings an edit leaves, plus an ownership-conflict finding when the edit was refused. #[pyfunction] #[pyo3(signature = (*, findings, overwrite, conflict))] -fn remaining_contract_findings<'py>( +pub(crate) fn remaining_contract_findings<'py>( py: Python<'py>, findings: &Bound<'py, PyAny>, overwrite: bool, @@ -172,11 +167,11 @@ fn remaining_contract_findings<'py>( keywords.set_item("resource_name", native_field(first, "resource_name")?)?; keywords.set_item( "kind", - registry::class(py, RecordId::ContractFindingKind)?.getattr("OWNERSHIP_CONFLICT")?, + enum_member(py, RecordId::ContractFindingKind, "OWNERSHIP_CONFLICT")?, )?; keywords.set_item("message", conflict)?; let ownership: Bound<'py, PyAny> = - registry::class(py, RecordId::ContractFinding)?.call((), Some(&keywords))?; + new_record(py, RecordId::ContractFinding, (), Some(&keywords))?; let mut remaining: Vec> = findings.clone(); remaining.push(ownership); return PyTuple::new(py, remaining); diff --git a/crates/sqlbuild-python/src/bindings/_helpers/refactor/offline_compile.rs b/crates/sqlbuild-python/src/bindings/_helpers/refactor/offline_compile.rs index 48394339ee..a4c6bef6a6 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/refactor/offline_compile.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/refactor/offline_compile.rs @@ -5,11 +5,14 @@ use std::path::Path; use pyo3::IntoPyObject; use pyo3::exceptions::PyValueError; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyErr, PyResult, Python}; -use pyo3::types::{PyBool, PyString}; +use pyo3::types::{PyBool, PyString, PyTuple}; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; +use crate::bindings::_helpers::compile_command::target_writer::static_sql_test_planning_diagnostics; +use crate::bindings::_helpers::compile_driver::analysis::analyze_compile_project; +use crate::bindings::_helpers::compile_driver::python_calls::keywords; +use crate::bindings::_helpers::records::construct::{enum_member, new_record, new_record_with}; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr, RecordId}; +use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; const DEFAULT_ERROR_CODE: &str = "E001"; @@ -20,10 +23,6 @@ pub(crate) struct OfflineCompile<'py> { pub(crate) errors: Vec>, } -fn enum_member<'py>(py: Python<'py>, class: RecordId, member: &str) -> PyResult> { - registry::member(py, class, member) -} - fn matches_any_class(py: Python<'_>, error: &PyErr, classes: &[ErrorId]) -> PyResult { for class in classes { if error.value(py).is_instance(®istry::error(py, *class)?)? { @@ -46,8 +45,9 @@ fn compile_error_diagnostic<'py>(py: Python<'py>, error: &PyErr) -> PyResult(py: Python<'py>, error: &PyErr) -> PyResult( project_dir: &Path, no_cache: bool, ) -> PyResult> { - let selection: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::CompileAnalysisSelection)?, + let selection: Bound<'py, PyAny> = new_record_with( + py, + RecordId::CompileAnalysisSelection, &[("no_cache", PyBool::new(py, no_cache).to_owned().into_any())], )?; - call_with( - ®istry::native(py, NativeAttr::AnalyzeCompileProject)?, - &[ - ("project_dir", project_dir.into_pyobject(py)?.into_any()), - ( - "no_sql_validation", - PyBool::new(py, false).to_owned().into_any(), - ), - ("selected_target", py.None().into_bound(py)), - ( - "lineage_mode", - registry::class(py, RecordId::CompileLineageMode)?.getattr("NONE")?, - ), - ("cli_vars", py.None().into_bound(py)), - ( - "profile_flags", - registry::class(py, RecordId::CompileProfileFlags)?.call0()?, - ), - ("analysis_selection", selection), - ("status", py.None().into_bound(py)), - ], + analyze_compile_project( + py, + Some(keywords( + py, + &[ + ("project_dir", project_dir.into_pyobject(py)?.into_any()), + ( + "no_sql_validation", + PyBool::new(py, false).to_owned().into_any(), + ), + ("selected_target", py.None().into_bound(py)), + ( + "lineage_mode", + enum_member(py, RecordId::CompileLineageMode, "NONE")?, + ), + ("cli_vars", py.None().into_bound(py)), + ( + "profile_flags", + new_record(py, RecordId::CompileProfileFlags, (), None)?, + ), + ("analysis_selection", selection), + ("status", py.None().into_bound(py)), + ], + )?), ) } @@ -154,22 +159,25 @@ pub(crate) fn compile_for_refactor<'py>( let mut errors: Vec> = error_diagnostics(native_field(&analysis, "diagnostics")?)?; if errors.is_empty() { - let planned: PyResult> = call_with( - ®istry::native(py, NativeAttr::StaticSqlTestPlanningDiagnostics)?, - &[ - ( - "target_dir", - project_dir.join("target").into_pyobject(py)?.into_any(), - ), - ("adapter", native_field(&analysis, "adapter")?), - ( - "project", - native_field(&analysis, "graph")?.getattr("project")?, - ), - ], + let planned: PyResult> = static_sql_test_planning_diagnostics( + py, + Some(keywords( + py, + &[ + ( + "target_dir", + project_dir.join("target").into_pyobject(py)?.into_any(), + ), + ("adapter", native_field(&analysis, "adapter")?), + ( + "project", + native_field(&native_field(&analysis, "graph")?, "project")?, + ), + ], + )?), ); match planned { - Ok(diagnostics) => errors.extend(error_diagnostics(diagnostics)?), + Ok(diagnostics) => errors.extend(error_diagnostics(diagnostics.into_any())?), Err(error) if matches_any_class( py, @@ -182,8 +190,9 @@ pub(crate) fn compile_for_refactor<'py>( Err(error) => return Err(error), } } - let project: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::RefactorProject)?, + let project: Bound<'py, PyAny> = new_record_with( + py, + RecordId::RefactorProject, &[ ("project_dir", project_dir.into_pyobject(py)?.into_any()), ("graph", native_field(&analysis, "graph")?), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/refactor/python_strings.rs b/crates/sqlbuild-python/src/bindings/_helpers/refactor/python_strings.rs index 021a22be97..ffd72ff179 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/refactor/python_strings.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/refactor/python_strings.rs @@ -9,9 +9,9 @@ use sqlbuild_pysource::string_literals::main::string_literal_locations::string_l use sqlbuild_pysource::string_literals::models::StringQuery; use crate::bindings::_helpers::boundary::keyword_request::Request; +use crate::bindings::_helpers::records::construct::new_record; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; /// The discovered Python declarations whose files a refactoring searches. const PYTHON_DECLARATIONS: [&str; 8] = [ @@ -31,7 +31,7 @@ fn python_paths<'py>( ) -> PyResult, Bound<'py, PyAny>)>> { let mut paths: Vec<(Vec, Bound<'py, PyAny>)> = Vec::new(); for declarations in PYTHON_DECLARATIONS { - for item in discovered.getattr(declarations)?.try_iter()? { + for item in native_field(discovered, declarations)?.try_iter()? { let path: Bound<'py, PyAny> = native_field(&item?, "relative_path")?; let parts: Vec = native_field(&path, "parts")?.extract()?; if !paths.iter().any(|(known, _)| *known == parts) { @@ -58,7 +58,7 @@ fn read_text(path: PathBuf) -> Option { /// Flag Python strings that name a model or column; with context, only SQL naming it too. #[pyfunction] #[pyo3(signature = (**request))] -fn find_python_string_locations<'py>( +pub(crate) fn find_python_string_locations<'py>( request: Option<&Bound<'py, PyDict>>, ) -> PyResult> { let request = Request::new("find_python_string_locations", request)?; @@ -70,7 +70,6 @@ fn find_python_string_locations<'py>( Some(context) => context.extract()?, None => None, }; - let location_type: Bound<'py, PyAny> = registry::class(py, RecordId::ManualLocation)?; let version = py.version_info(); let mut locations: Vec> = Vec::new(); for (_, relative_path) in python_paths(&request.get("discovered")?)? { @@ -91,7 +90,12 @@ fn find_python_string_locations<'py>( keywords.set_item("line", line)?; keywords.set_item("column", column)?; keywords.set_item("reason", &reason)?; - locations.push(location_type.call((), Some(&keywords))?); + locations.push(new_record( + py, + RecordId::ManualLocation, + (), + Some(&keywords), + )?); } } PyTuple::new(py, locations) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/refactor/refactor_command.rs b/crates/sqlbuild-python/src/bindings/_helpers/refactor/refactor_command.rs index 938af86448..93aa40f975 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/refactor/refactor_command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/refactor/refactor_command.rs @@ -305,8 +305,10 @@ fn run(py: Python<'_>, command: &Command<'_>, status: &Status<'_>) -> PyResult = - target_lookup(&native_field(&before.project, "graph")?.getattr("all_keys")?)?; + let names: HashMap = target_lookup(&native_field( + &native_field(&before.project, "graph")?, + "all_keys", + )?)?; let lookup = |name: &str| names.get(name).cloned(); let request: RefactorRequest = refactor_request_for_target(&target_request(&command.request)?, &lookup) @@ -406,7 +408,7 @@ fn finish(py: Python<'_>, command: &Command<'_>, outcome: &Outcome) -> PyResult< /// Plan, verify in a scratch copy, and write one rename or move. #[pyfunction] #[pyo3(signature = (*, request))] -fn run_refactor_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { +pub(crate) fn run_refactor_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { let project_dir: Bound<'_, PyAny> = match native_field(request, "project_dir")? { directory if !directory.is_none() => directory, _ => py.import("pathlib")?.getattr("Path")?.call_method0("cwd")?, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/refactor/refactor_facts.rs b/crates/sqlbuild-python/src/bindings/_helpers/refactor/refactor_facts.rs index 6c7ee9d889..df38754cff 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/refactor/refactor_facts.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/refactor/refactor_facts.rs @@ -11,9 +11,11 @@ use sqlbuild_refactor::refactoring::models::{ SchemaColumnFacts, SqlFileRole, }; +use crate::bindings::_helpers::model_config::config_values::config_str; +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::ids::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::ids::RecordId; +use crate::bindings::_helpers::refactor::python_strings::find_python_string_locations; use crate::bindings::_helpers::scopes::scope_lookup::build_scope_lookup; use crate::bindings::_helpers::scopes::scope_reports::scope_move_preview; use crate::bindings::_helpers::scopes::scope_validation::scope_relocated_declarations; @@ -27,10 +29,6 @@ fn posix(path: &Bound<'_, PyAny>) -> PyResult { path.call_method0("as_posix")?.extract() } -fn native<'py>(py: Python<'py>, name: NativeAttr) -> PyResult> { - registry::native(py, name) -} - fn discovered_files(files: &Bound<'_, PyAny>) -> PyResult> { let mut discovered: Vec = Vec::new(); for item in files.try_iter()? { @@ -59,7 +57,7 @@ fn resource_columns( let resource: Bound<'_, PyAny> = resource?; facts.push(ResourceColumnsFacts { name: native_field(&resource, "name")?.extract()?, - columns: column_names(&native_field(&resource.getattr(entry)?, "columns")?)?, + columns: column_names(&native_field(&native_field(&resource, entry)?, "columns")?)?, }); } Ok(facts) @@ -82,8 +80,7 @@ fn spans(spans: &Bound<'_, PyAny>) -> PyResult> { fn expansion_facts(expansion: &Bound<'_, PyAny>) -> PyResult { Ok(ExpansionFacts { expanded_sql: native_field(expansion, "expanded_sql")?.extract()?, - passes: expansion - .getattr("passes")? + passes: native_field(expansion, "passes")? .try_iter()? .map(|pass| spans(&pass?)) .collect::>()?, @@ -120,14 +117,11 @@ fn model(model: &Bound<'_, PyAny>, expansion: Option<&Bound<'_, PyAny>>) -> PyRe let values: Bound<'_, PyDict> = PyDict::new(py); values.call_method1( "update", - (native_field(model, "config")?.getattr("values")?,), + (native_field(&native_field(model, "config")?, "values")?,), )?; - let options: Bound<'_, PyDict> = PyDict::new(py); - options.set_item("values", &values)?; - options.set_item("key", MATERIALIZED_KEY)?; - let materialized: Option = registry::native(py, NativeAttr::GetConfigStr)? - .call((), Some(&options))? - .extract()?; + let materialized: Option = config_str(&values, MATERIALIZED_KEY)? + .map(|value| value.extract()) + .transpose()?; let migrate_from: Option> = values.get_item(MIGRATE_FROM_KEY)?; let mut deps: Vec = Vec::new(); for dep in native_field(model, "deps")?.try_iter()? { @@ -170,11 +164,11 @@ pub(crate) fn model_facts( if refactor_project.is_none() { return Ok(None); } - let project: Bound<'_, PyAny> = native_field(refactor_project, "graph")?.getattr("project")?; + let project: Bound<'_, PyAny> = + native_field(&native_field(refactor_project, "graph")?, "project")?; let project_dir: Bound<'_, PyAny> = native_field(refactor_project, "project_dir")?; let mut expansions: HashMap> = HashMap::new(); - let items: Vec> = project - .getattr("sql_expansions")? + let items: Vec> = native_field(&project, "sql_expansions")? .call_method0("items")? .try_iter()? .collect::>()?; @@ -223,10 +217,10 @@ fn authored_files(discovered: &Bound<'_, PyAny>) -> PyResult> ]; let mut files: Vec = Vec::new(); for (role, attribute, body, blocks) in groups { - for item in discovered.getattr(attribute)?.try_iter()? { + for item in native_field(discovered, attribute)?.try_iter()? { let item: Bound<'_, PyAny> = item?; let texts: Vec = match (body, blocks) { - (Some(body), _) => vec![item.getattr(body)?.extract()?], + (Some(body), _) => vec![native_field(&item, body)?.extract()?], (None, true) => block_bodies(&native_field(&item, "blocks")?)?, (None, false) => Vec::new(), }; @@ -266,10 +260,7 @@ fn python_locations( )?; } let mut locations: Vec = Vec::new(); - for location in native(py, NativeAttr::FindPythonStringLocations)? - .call((), Some(&options))? - .try_iter()? - { + for location in find_python_string_locations(Some(&options))?.try_iter()? { let location: Bound<'_, PyAny> = location?; locations.push(ManualLocation { path: native_field(&location, "path")?.extract()?, @@ -286,7 +277,7 @@ pub(crate) fn refactor_facts( project: &Bound<'_, PyAny>, request: &RefactorRequest, ) -> PyResult { - let compiled: Bound<'_, PyAny> = native_field(project, "graph")?.getattr("project")?; + let compiled: Bound<'_, PyAny> = native_field(&native_field(project, "graph")?, "project")?; let discovered: Bound<'_, PyAny> = native_field(project, "discovered")?; let dialect: Option = native_field(&compiled, "sql_analysis_dialect")?.extract()?; let mut yaml_files: Vec = @@ -320,10 +311,10 @@ pub(crate) fn declaration_placement( (model, destination): (&str, &str), ) -> PyResult { let py: Python<'_> = project.py(); - let index: Bound<'_, PyAny> = project - .getattr("graph")? - .getattr("project")? - .getattr("scope_index")?; + let index: Bound<'_, PyAny> = native_field( + &native_field(&native_field(project, "graph")?, "project")?, + "scope_index", + )?; let lookup: Bound<'_, PyAny> = build_scope_lookup(py, &index)?.into_bound(py); let (preview, diagnostics) = scope_move_preview( py, @@ -336,13 +327,10 @@ pub(crate) fn declaration_placement( None } else { let options: Bound<'_, PyDict> = PyDict::new(py); - options.set_item( - "kind", - registry::class(py, RecordId::ResourceKind)?.getattr("MODEL")?, - )?; + options.set_item("kind", enum_member(py, RecordId::ResourceKind, "MODEL")?)?; options.set_item("name", model)?; let resource: Bound<'_, PyAny> = - registry::class(py, RecordId::ResourceIdentity)?.call((), Some(&options))?; + new_record(py, RecordId::ResourceIdentity, (), Some(&options))?; let records: Option> = scope_relocated_declarations(py, &index, &resource, destination)?; if let Some(records) = records { @@ -376,9 +364,7 @@ pub(crate) fn declaration_placement( for record in native_field(&index, "declarations")?.try_iter()? { let record: Bound<'_, PyAny> = record?; let identity: Bound<'_, PyAny> = native_field(&record, "identity")?; - let kind: String = native_field(&identity, "kind")? - .getattr("value")? - .extract()?; + let kind: String = native_field(&native_field(&identity, "kind")?, "value")?.extract()?; let name: String = native_field(&identity, "name")?.extract()?; declarations.push(PlacedDeclaration { key: identity_key(&identity)?, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/references/relations.rs b/crates/sqlbuild-python/src/bindings/_helpers/references/relations.rs index 46d397cf98..1c1dcbc8f8 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/references/relations.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/references/relations.rs @@ -78,12 +78,10 @@ pub(crate) fn match_project_relation<'py>( default_schema: Option, ) -> PyResult>> { let name: String = python_casefold(&native_field(relation, "name")?.extract::()?); - let schema: Option = relation - .getattr("schema")? + let schema: Option = native_field(relation, "schema")? .extract::>()? .or(default_schema); - let database: Option = relation - .getattr("database")? + let database: Option = native_field(relation, "database")? .extract::>()? .or(default_database); for candidate in native_field(index, "relations")?.try_iter()? { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/render/authored_sql.rs b/crates/sqlbuild-python/src/bindings/_helpers/render/authored_sql.rs index e4de6e0117..16158a20b5 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/render/authored_sql.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/render/authored_sql.rs @@ -8,9 +8,9 @@ use pyo3::{pyfunction, wrap_pyfunction}; use crate::bindings::_helpers::boundary::keyword_request::Request; use crate::bindings::_helpers::compile_attachment::sql_expansion_context::SqlExpansionInputs; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; use crate::bindings::_helpers::model_config::template_data::record_reads; use crate::bindings::_helpers::model_config::unicode_environment::unicode_environment; +use crate::bindings::_helpers::records::construct::{new_record, new_record_with, record_class}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; @@ -66,10 +66,9 @@ fn substituted<'py>( /// `ExpansionSpan` objects for span rows, in order. fn span_objects<'py>(py: Python<'py>, spans: &[SpanRow]) -> PyResult> { - let span_class: Bound<'py, PyAny> = registry::class(py, RecordId::ExpansionSpan)?; let objects: Vec> = spans .iter() - .map(|span| span_class.call1(*span)) + .map(|span| new_record(py, RecordId::ExpansionSpan, *span, None)) .collect::>()?; PyTuple::new(py, objects) } @@ -87,15 +86,12 @@ fn declaration_context<'py>( if declarations.is_none() { PyDict::new(py).into_any() } else { - declarations.getattr(*field)? + native_field(declarations, field)? }, )) }) .collect::>()?; - call_with( - ®istry::class(py, RecordId::DeclarationResolutionContext)?, - &fields, - ) + new_record_with(py, RecordId::DeclarationResolutionContext, &fields) } /// `sql` with natively scanned enum and constant references spliced, and their spans. @@ -119,7 +115,7 @@ fn declarations_expanded<'py>( collection_rendering.clone().unbind(), ); let classes = ExpansionClasses { - usage_record: registry::class(py, RecordId::UsageRecord)?, + usage_record: record_class(py, RecordId::UsageRecord)?, compile_error: registry::error(py, ErrorId::CompileInput)?, }; let path: String = file_path.str()?.extract()?; @@ -280,12 +276,11 @@ fn input_declarations<'py>( file_path: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = file_path.py(); - let resolution_class: Bound<'py, PyAny> = - registry::class(py, RecordId::DeclarationResolutionContext)?; let resolver: &Bound<'py, PyAny> = inputs.declaration_resolver.bind(py); let declarations: Bound<'py, PyAny> = if resolver.is_none() { - call_with( - &resolution_class, + new_record_with( + py, + RecordId::DeclarationResolutionContext, &[ ("enums", inputs.enums.bind(py).clone()), ("constants", inputs.constants.bind(py).clone()), @@ -301,10 +296,11 @@ fn input_declarations<'py>( return Ok(declarations); } let merged = |field: &str| -> PyResult> { - declarations.getattr(field)?.bitor(local.getattr(field)?) + native_field(&declarations, field)?.bitor(native_field(&local, field)?) }; - call_with( - &resolution_class, + new_record_with( + py, + RecordId::DeclarationResolutionContext, &[ ("enums", merged("enums")?), ("constants", merged("constants")?), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/render/declaration_indexes.rs b/crates/sqlbuild-python/src/bindings/_helpers/render/declaration_indexes.rs index 0ee96d3607..081322332f 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/render/declaration_indexes.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/render/declaration_indexes.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use crate::bindings::_helpers::records::fields::field as native_field; +use crate::bindings::_helpers::records::fields::replaced_keywords; use crate::bindings::_helpers::records::ids::ErrorId; use crate::bindings::_helpers::records::registry; use pyo3::prelude::{ @@ -11,8 +12,6 @@ use pyo3::prelude::{ use pyo3::types::{PyDict, PyDictMethods, PyTuple}; use pyo3::{pyfunction, wrap_pyfunction}; -const DATACLASSES_MODULE: &str = "dataclasses"; - /// `CompileInputError(message)`. fn compile_input_error(py: Python<'_>, message: String) -> PyErr { match registry::error(py, ErrorId::CompileInput).and_then(|class| class.call1((message,))) { @@ -22,7 +21,7 @@ fn compile_input_error(py: Python<'_>, message: String) -> PyErr { } fn text(owner: &Bound<'_, PyAny>, name: &str) -> PyResult { - Ok(owner.getattr(name)?.str()?.to_string()) + Ok(native_field(owner, name)?.str()?.to_string()) } /// Add each declaration of `files` to `index`, rejecting a name declared twice. @@ -78,7 +77,6 @@ struct SchemaResolution<'py> { resolved: HashMap>, /// Names in the order Python's `resolved` dict gains them. order: Vec, - replace: Bound<'py, PyAny>, } impl<'py> SchemaResolution<'py> { @@ -122,7 +120,7 @@ impl<'py> SchemaResolution<'py> { } let fields: Bound<'py, PyDict> = PyDict::new(self.py); fields.set_item("columns", PyTuple::new(self.py, columns)?)?; - let resolved: Bound<'py, PyAny> = self.replace.call((declaration,), Some(&fields))?; + let resolved: Bound<'py, PyAny> = replaced_keywords(&declaration, &fields)?; self.resolved.insert(name.to_owned(), resolved.clone()); self.order.push(name.to_owned()); Ok(resolved) @@ -204,7 +202,6 @@ pub(crate) fn public_model_schema_index<'py>( by_name, resolved: HashMap::new(), order: Vec::new(), - replace: py.import(DATACLASSES_MODULE)?.getattr("replace")?, }; let index: Bound<'py, PyDict> = PyDict::new(py); for name in &names { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/render/declaration_values.rs b/crates/sqlbuild-python/src/bindings/_helpers/render/declaration_values.rs index 7c4dd7b3fe..368dce5d64 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/render/declaration_values.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/render/declaration_values.rs @@ -9,9 +9,13 @@ use pyo3::{IntoPyObject, pyclass, pyfunction, wrap_pyfunction}; use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; +use crate::bindings::_helpers::records::construct::{ + enum_member, new_record, new_record_with, record_class, +}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::scopes::declaration_contexts::NativeDeclarationContexts; use crate::bindings::_helpers::scopes::scope_lookup::build_scope_lookup; const NO_DECLARATION_KIND: u8 = 255; @@ -31,7 +35,7 @@ fn declaration_identity<'py>( owner: Option<&Bound<'py, PyAny>>, ) -> PyResult> { let (identity_class, kinds) = classes; - let kind: Bound<'py, PyAny> = kinds.getattr(kind)?; + let kind: Bound<'py, PyAny> = native_field(kinds, kind)?; match owner { Some(owner) => identity_class.call1((kind, name, owner)), None => identity_class.call1((kind, name)), @@ -45,12 +49,12 @@ fn runtime_declarations<'py>( ) -> PyResult> { let py: Python<'py> = discovered.py(); let classes = ( - registry::class(py, RecordId::DeclarationIdentity)?, - registry::class(py, RecordId::DeclarationKind)?, + record_class(py, RecordId::DeclarationIdentity)?, + record_class(py, RecordId::DeclarationKind)?, ); let declarations: Bound<'py, PyDict> = PyDict::new(py); for (files, kind) in [("enum_files", "ENUM"), ("constant_files", "CONSTANT")] { - for file in discovered.getattr(files)?.try_iter()? { + for file in native_field(discovered, files)?.try_iter()? { for declaration in native_field(&file?, "declarations")?.try_iter()? { let declaration: Bound<'py, PyAny> = declaration?; let identity = declaration_identity( @@ -63,25 +67,23 @@ fn runtime_declarations<'py>( } } } - let resource_identity: Bound<'py, PyAny> = registry::class(py, RecordId::ResourceIdentity)?; - let model_kind: Bound<'py, PyAny> = - registry::class(py, RecordId::ResourceKind)?.getattr("MODEL")?; + let model_kind: Bound<'py, PyAny> = enum_member(py, RecordId::ResourceKind, "MODEL")?; for model_file in native_field(discovered, "model_files")?.try_iter()? { let model_file: Bound<'py, PyAny> = model_file?; - let name: Bound<'py, PyAny> = model_file - .getattr("header_values")? - .call_method1("get", ("name",))?; + let name: Bound<'py, PyAny> = + native_field(&model_file, "header_values")?.call_method1("get", ("name",))?; let name: Bound<'py, PyAny> = if name.is_instance_of::() && name.is_truthy()? { name } else { - native_field(&model_file, "relative_path")?.getattr("stem")? + native_field(&native_field(&model_file, "relative_path")?, "stem")? }; - let owner: Bound<'py, PyAny> = resource_identity.call1((&model_kind, name))?; + let owner: Bound<'py, PyAny> = + new_record(py, RecordId::ResourceIdentity, (&model_kind, name), None)?; for (attribute, kind) in [ ("enum_declarations", "ENUM"), ("constant_declarations", "CONSTANT"), ] { - for declaration in model_file.getattr(attribute)?.try_iter()? { + for declaration in native_field(&model_file, attribute)?.try_iter()? { let declaration: Bound<'py, PyAny> = declaration?; let identity = declaration_identity( &classes, @@ -111,12 +113,12 @@ fn runtime_declarations<'py>( /// Resources whose declarations differ from their directory's: private owners and grantees. fn resource_specific<'py>(scope_index: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = scope_index.py(); - let private: Bound<'py, PyAny> = - registry::class(py, RecordId::ScopeKind)?.getattr("PRIVATE")?; + let private: Bound<'py, PyAny> = enum_member(py, RecordId::ScopeKind, "PRIVATE")?; let mut resources: Vec> = Vec::new(); for declaration in native_field(scope_index, "declarations")?.try_iter()? { let declaration: Bound<'py, PyAny> = declaration?; - let owner: Bound<'py, PyAny> = native_field(&declaration, "identity")?.getattr("owner")?; + let owner: Bound<'py, PyAny> = + native_field(&native_field(&declaration, "identity")?, "owner")?; if native_field(&declaration, "scope")?.is(&private) && !owner.is_none() { resources.push(owner); } @@ -140,8 +142,9 @@ pub(crate) fn declaration_scope_resolver<'py>( } else { lookup.clone() }; - let projection: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::DeclarationRuntimeProjection)?, + let projection: Bound<'py, PyAny> = new_record_with( + py, + RecordId::DeclarationRuntimeProjection, &[( "declarations", imported(py, TYPES_MODULE, "MappingProxyType")?.call1((&declarations,))?, @@ -149,8 +152,9 @@ pub(crate) fn declaration_scope_resolver<'py>( )?; let native_contexts: Bound<'py, PyAny> = native_declaration_contexts(&lookup, declarations.as_any())?; - call_with( - ®istry::class(py, RecordId::DeclarationScopeResolver)?, + new_record_with( + py, + RecordId::DeclarationScopeResolver, &[ ("project_dir", native_field(discovered, "project_dir")?), ("lookup", lookup), @@ -206,15 +210,15 @@ pub(crate) fn native_declaration_contexts<'py>( declarations: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = lookup.py(); - let kind_type: Bound<'py, PyAny> = registry::class(py, RecordId::DeclarationKind)?; + let kind_type: Bound<'py, PyAny> = record_class(py, RecordId::DeclarationKind)?; let kinds: Vec> = DECLARATION_KINDS .iter() - .map(|kind| kind_type.getattr(*kind)) + .map(|kind| enum_member(py, RecordId::DeclarationKind, kind)) .collect::>()?; let value_classes: Vec> = vec![ - registry::class(py, RecordId::EnumDeclaration)?, - registry::class(py, RecordId::ConstantDeclaration)?, - registry::class(py, RecordId::LoadedMacro)?, + record_class(py, RecordId::EnumDeclaration)?, + record_class(py, RecordId::ConstantDeclaration)?, + record_class(py, RecordId::LoadedMacro)?, ]; let index: Bound<'py, PyAny> = native_field(lookup, "visibility_index")?; let lookup_declarations: Bound<'py, PyAny> = native_field(lookup, "declarations")?; @@ -243,18 +247,18 @@ pub(crate) fn native_declaration_contexts<'py>( )?); } let reasons: Bound<'py, PyDict> = PyDict::new(py); - for reason in registry::class(py, RecordId::VisibilityReason)?.try_iter()? { + for reason in record_class(py, RecordId::VisibilityReason)?.try_iter()? { let reason: Bound<'py, PyAny> = reason?; reasons.set_item(native_field(&reason, "value")?, reason)?; } let classes: Bound<'py, PyDict> = PyDict::new(py); classes.set_item( "visibility_record", - registry::class(py, RecordId::VisibilityRecord)?, + record_class(py, RecordId::VisibilityRecord)?, )?; classes.set_item( "resolution_context", - registry::class(py, RecordId::DeclarationResolutionContext)?, + record_class(py, RecordId::DeclarationResolutionContext)?, )?; classes.set_item("reasons", reasons)?; let keys: Bound<'py, PyDict> = PyDict::new(py); @@ -276,7 +280,7 @@ pub(crate) fn native_declaration_contexts<'py>( lookups.set_item("declarations", &lookup_declarations)?; lookups.set_item( "index_declarations", - native_field(lookup, "index")?.getattr("declarations")?, + native_field(&native_field(lookup, "index")?, "declarations")?, )?; for (name, class) in [ ("resource_record", RecordId::ResourceRecord), @@ -284,16 +288,13 @@ pub(crate) fn native_declaration_contexts<'py>( ("resource_identity", RecordId::ResourceIdentity), ("declaration_identity", RecordId::DeclarationIdentity), ] { - lookups.set_item(name, registry::class(py, class)?)?; + lookups.set_item(name, record_class(py, class)?)?; } - lookups.set_item( - "resource_kind", - registry::class(py, RecordId::ResourceKind)?, - )?; + lookups.set_item("resource_kind", record_class(py, RecordId::ResourceKind)?)?; lookups.set_item("declaration_kind", kind_type)?; lookups.set_item( "private_scope", - registry::class(py, RecordId::ScopeKind)?.getattr("PRIVATE")?, + enum_member(py, RecordId::ScopeKind, "PRIVATE")?, )?; lookups.set_item( "identity_error", @@ -305,21 +306,21 @@ pub(crate) fn native_declaration_contexts<'py>( )?; let global_positions: Bound<'py, PyList> = PyList::new( py, - index - .getattr("global_positions")? + native_field(&index, "global_positions")? .try_iter()? .collect::>>()?, )?; - registry::native(py, NativeAttr::NativeDeclarationContexts)?.call1(( - PyList::new(py, positions)?, + let contexts: NativeDeclarationContexts = NativeDeclarationContexts::new( + PyList::new(py, positions)?.extract()?, ( - global_positions, - listed_positions(&native_field(&index, "local_positions")?)?, - listed_positions(&native_field(&index, "inherited_positions")?)?, + global_positions.extract()?, + listed_positions(&native_field(&index, "local_positions")?)?.extract()?, + listed_positions(&native_field(&index, "inherited_positions")?)?.extract()?, ), - classes, - (keys, lookups), - )) + classes.extract()?, + (keys.extract()?, lookups.extract()?), + ); + Ok(Bound::new(py, contexts)?.into_any()) } /// Hand one scope lookup's positions and runtime values to the native classifier. @@ -341,8 +342,7 @@ pub(crate) fn resolved_declaration_context<'py>( let py: Python<'py> = resolver.py(); let resource: Bound<'py, PyAny> = resource.map_or_else(|| py.None().into_bound(py), Clone::clone); - resolver - .getattr("native_contexts")? + native_field(resolver, "native_contexts")? .call_method1("resolve", (resolver, file_path, resource))? .get_item(0) } @@ -367,7 +367,7 @@ fn rendered_constant<'py>( "render_typed_object" }; let rendered: String = - call_with(&value_renderer.getattr(method)?, &[("value", value)])?.extract()?; + call_with(&native_field(value_renderer, method)?, &[("value", value)])?.extract()?; let size: usize = rendered.len(); if size > MAX_SQL_VALUE_SIZE { let context: String = format!( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/render/macro_call_classes.rs b/crates/sqlbuild-python/src/bindings/_helpers/render/macro_call_classes.rs index 32828a6b24..0ae4d7344b 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/render/macro_call_classes.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/render/macro_call_classes.rs @@ -2,12 +2,12 @@ use std::collections::HashMap; -use pyo3::FromPyObject; use pyo3::prelude::{Bound, Py, PyAny, PyAnyMethods, PyResult, Python}; use pyo3::types::{PyDict, PyDictMethods}; use sqlbuild_render::macro_calls::main::call_class_store_text::call_class_store_text; +use crate::bindings::_helpers::macro_bridge::store_keys::{context_store_token, macro_store_token}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::render::macro_expansion_facts::ResourceRef; @@ -32,14 +32,6 @@ pub(crate) struct CallClass { persistent: bool, } -/// `macro_store_token` and `context_store_token`, the value text of a stored call's inputs. -#[derive(FromPyObject)] -#[pyo3(from_item_all)] -pub(crate) struct StoreTokenClasses<'py> { - macro_token: Bound<'py, PyAny>, - context_token: Bound<'py, PyAny>, -} - /// The call classes, class ids and tokens one compile's memo has seen. #[derive(Debug, Default)] pub(crate) struct CallClassState { @@ -67,7 +59,6 @@ pub(crate) struct ClassInputs<'a, 'py> { pub(crate) declarations: &'a Bound<'py, PyAny>, pub(crate) loaded_macros: &'a Bound<'py, PyDict>, pub(crate) macro_context: &'a Bound<'py, PyAny>, - pub(crate) store: &'a StoreTokenClasses<'py>, } impl CallClassState { @@ -108,11 +99,7 @@ impl CallClassState { } let mut injects_context: bool = false; for name in tree_names { - if macros - .get_item(name)? - .getattr("injects_context")? - .is_truthy()? - { + if native_field(¯os.get_item(name)?, "injects_context")?.is_truthy()? { injects_context = true; break; } @@ -175,15 +162,12 @@ impl CallClassState { let declarations: &Bound<'_, PyAny> = inputs.declarations; let visible: bool = !loaded.is_none() && (declarations.is_none() - || !declarations - .getattr("inaccessible_macros")? - .contains(name)?); + || !native_field(declarations, "inaccessible_macros")?.contains(name)?); let token: NameToken = if visible { let identity: Bound<'_, PyAny> = if declarations.is_none() { py.None().into_bound(py) } else { - declarations - .getattr("macro_records")? + native_field(declarations, "macro_records")? .get_item(name)? .getattr("identity")? }; @@ -191,11 +175,7 @@ impl CallClassState { let arguments: Bound<'_, PyDict> = PyDict::new(py); arguments.set_item("loaded", &loaded)?; arguments.set_item("identity", &identity)?; - inputs - .store - .macro_token - .call((), Some(&arguments))? - .extract()? + macro_store_token(py, Some(arguments))? } else { None }; @@ -239,11 +219,7 @@ impl CallClassState { let arguments: Bound<'_, PyDict> = PyDict::new(owner.py()); arguments.set_item("macro_context", inputs.macro_context)?; arguments.set_item("declarations", declarations)?; - inputs - .store - .context_token - .call((), Some(&arguments))? - .extract()? + context_store_token(owner.py(), Some(arguments))? } else { None }; @@ -270,14 +246,12 @@ fn identity_key(identity: &Bound<'_, PyAny>) -> PyResult> { None } else { Some(( - native_field(&owner, "kind")?.getattr("value")?.extract()?, + native_field(&native_field(&owner, "kind")?, "value")?.extract()?, native_field(&owner, "name")?.extract()?, )) }; Ok(Some(( - native_field(identity, "kind")? - .getattr("value")? - .extract()?, + native_field(&native_field(identity, "kind")?, "value")?.extract()?, native_field(identity, "name")?.extract()?, owner_key, ))) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/render/macro_evaluation.rs b/crates/sqlbuild-python/src/bindings/_helpers/render/macro_evaluation.rs index cd0c168c06..8bb2a77dd0 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/render/macro_evaluation.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/render/macro_evaluation.rs @@ -128,9 +128,7 @@ fn called_macro<'py>( .invocation_context .call((), Some(&context_arguments))?; expansion.facts.borrow_mut().begin_rendering(approved); - let token: PyResult> = run - .handle - .getattr("render_relation") + let token: PyResult> = native_field(run.handle, "render_relation") .and_then(|renderer| classes.relation_renderer.call_method1("set", (renderer,))); let result: PyResult> = token .as_ref() @@ -291,13 +289,14 @@ fn call_arguments<'py>( } let mut references: Vec = Vec::with_capacity(arguments.typed_references.len()); for (function, reference_name) in &arguments.typed_references { - let kind: String = run - .classes - .ref_kinds - .get_item(function)? - .ok_or_else(|| PyValueError::new_err(function.clone()))? - .getattr("value")? - .extract()?; + let kind: String = native_field( + &run.classes + .ref_kinds + .get_item(function)? + .ok_or_else(|| PyValueError::new_err(function.clone()))?, + "value", + )? + .extract()?; references.push((kind, reference_name.clone())); } run.expansion diff --git a/crates/sqlbuild-python/src/bindings/_helpers/render/macro_expansion.rs b/crates/sqlbuild-python/src/bindings/_helpers/render/macro_expansion.rs index 1545aedf17..c44bc2db8f 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/render/macro_expansion.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/render/macro_expansion.rs @@ -17,9 +17,7 @@ use sqlbuild_render::macro_calls::models::{ use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::render::declaration_expansion::usage_visibility; -use crate::bindings::_helpers::render::macro_call_classes::{ - CallClass, ClassInputs, StoreTokenClasses, -}; +use crate::bindings::_helpers::render::macro_call_classes::{CallClass, ClassInputs}; use crate::bindings::_helpers::render::macro_call_memo::{MacroCallMemo, poisoned}; use crate::bindings::_helpers::render::macro_call_rows::{SpanRow, span_row}; use crate::bindings::_helpers::render::macro_evaluation::{CallText, Evaluation, evaluate_call}; @@ -63,8 +61,6 @@ pub(crate) struct ExpansionClasses<'py> { pub(crate) ref_kinds: Bound<'py, PyDict>, /// Typed reference function by `SqlResourceRefKind` value. ref_functions: Bound<'py, PyDict>, - /// `macro_store_token` and `context_store_token`. - store_tokens: StoreTokenClasses<'py>, pub(crate) relation_renderer: Bound<'py, PyAny>, /// Builds the `MacroContext` a macro is called with from the visible declarations. pub(crate) invocation_context: Bound<'py, PyAny>, @@ -180,7 +176,6 @@ impl NativeMacroExpansion { declarations: self.declarations.bind(py), loaded_macros: self.loaded_macros.bind(py), macro_context: self.macro_context.bind(py), - store: &classes.store_tokens, }; let mut state = memo.classes.lock().map_err(poisoned)?; let mut found: Vec = Vec::with_capacity(scan.sites.len()); @@ -382,8 +377,7 @@ impl NativeMacroExpansion { let visibility: Bound<'py, PyAny> = if declarations.is_none() { PyTuple::empty(py).into_any() } else { - declarations - .getattr("macro_visibility")? + native_field(declarations, "macro_visibility")? .call_method1("get", (name, PyTuple::empty(py)))? }; let usages: Vec> = if visibility.is_truthy()? { @@ -552,8 +546,7 @@ impl NativeMacroExpansion { ) -> PyResult> { let declarations: &Bound<'py, PyAny> = self.declarations.bind(py); if !declarations.is_none() { - return declarations - .getattr("macro_records")? + return native_field(declarations, "macro_records")? .get_item(name)? .getattr("identity"); } @@ -682,9 +675,7 @@ fn top_level_output( /// A `SqlResourceRef`'s kind value and name. pub(crate) fn resource_key(reference: &Bound<'_, PyAny>) -> PyResult { Ok(( - native_field(reference, "kind")? - .getattr("value")? - .extract()?, + native_field(&native_field(reference, "kind")?, "value")?.extract()?, native_field(reference, "name")?.extract()?, )) } @@ -711,9 +702,7 @@ fn relationship_consumer( if !consumer.is_instance(&classes.resource_identity)? { return Ok(false); } - let kind: String = native_field(consumer, "kind")? - .getattr("value")? - .extract()?; + let kind: String = native_field(&native_field(consumer, "kind")?, "value")?.extract()?; Ok(RELATIONSHIP_CONSUMER_KINDS.contains(&kind.as_str())) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/render/macro_expansion_result.rs b/crates/sqlbuild-python/src/bindings/_helpers/render/macro_expansion_result.rs index 5111817fb5..28650810ba 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/render/macro_expansion_result.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/render/macro_expansion_result.rs @@ -7,12 +7,16 @@ use pyo3::types::{PyDict, PyDictMethods, PyList, PyString, PyStringMethods, PyTu use pyo3::{pyfunction, wrap_pyfunction}; use crate::bindings::_helpers::compile_driver::python_calls::{ - KeywordArguments, call_with, imported, keywords, + KeywordArguments, imported, keywords, }; +use crate::bindings::_helpers::discovery::guidance::explicit_references_help; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::native_host::user_code::UserCodeHost; +use crate::bindings::_helpers::records::construct::{ + enum_member, new_record, new_record_with, record_class, +}; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr, RecordId}; +use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::render::declaration_values::resolved_declaration_context; use crate::bindings::_helpers::render::macro_call_rows::span_row; @@ -54,38 +58,27 @@ pub(crate) struct ExpansionRequest<'py> { } fn generated_classes(py: Python<'_>) -> PyResult> { - let help: Bound<'_, PyAny> = registry::native(py, NativeAttr::ExplicitReferencesHelp)?.call( - (), - Some(&keywords( - py, - &[("allowed", PyString::new(py, GENERATED_ALLOWED).into_any())], - )?), - )?; + let help: Bound<'_, PyAny> = + PyString::new(py, &explicit_references_help(GENERATED_ALLOWED)).into_any(); let markers: Bound<'_, PyAny> = PyList::new(py, GENERATED_REFERENCE_MARKERS)?.into_any(); keywords( py, &[ ( "compiler_diagnostic", - registry::class(py, RecordId::CompilerDiagnostic)?, - ), - ( - "report_diagnostic", - registry::native(py, NativeAttr::ReportCompileDiagnostic)?, + record_class(py, RecordId::CompilerDiagnostic)?, ), ( "source_location", - registry::class(py, RecordId::SourceLocation)?, + record_class(py, RecordId::SourceLocation)?, ), ( "resource_identity", - registry::class(py, RecordId::ResourceIdentity)?, + record_class(py, RecordId::ResourceIdentity)?, ), ( "generic_syntax", - registry::class(py, RecordId::SqlLexicalSyntax)? - .call0()? - .getattr("native_mapping")?, + new_record(py, RecordId::SqlLexicalSyntax, (), None)?.getattr("native_mapping")?, ), ("generated_help", help), ("all_markers", markers), @@ -94,7 +87,7 @@ fn generated_classes(py: Python<'_>) -> PyResult> { } fn built_classes(py: Python<'_>) -> PyResult> { - let declaration_kind: Bound<'_, PyAny> = registry::class(py, RecordId::DeclarationKind)?; + let declaration_kind: Bound<'_, PyAny> = record_class(py, RecordId::DeclarationKind)?; let ref_kind: Bound<'_, PyAny> = host::entry(py, UserCodeHost::SqlResourceRefKind)?; let ref_kinds: Bound<'_, PyDict> = PyDict::new(py); let ref_functions: Bound<'_, PyDict> = PyDict::new(py); @@ -102,19 +95,6 @@ fn built_classes(py: Python<'_>) -> PyResult> { ref_kinds.set_item(function, ref_kind.call1((kind,))?)?; ref_functions.set_item(kind, function)?; } - let store_tokens: Bound<'_, PyDict> = keywords( - py, - &[ - ( - "macro_token", - registry::native(py, NativeAttr::MacroStoreToken)?, - ), - ( - "context_token", - registry::native(py, NativeAttr::ContextStoreToken)?, - ), - ], - )?; keywords( py, &[ @@ -129,22 +109,22 @@ fn built_classes(py: Python<'_>) -> PyResult> { ), ( "declaration_identity", - registry::class(py, RecordId::DeclarationIdentity)?, + record_class(py, RecordId::DeclarationIdentity)?, ), ("macro_kind", native_field(&declaration_kind, "MACRO")?), ("declaration_kind", declaration_kind), - ("usage_record", registry::class(py, RecordId::UsageRecord)?), + ("usage_record", record_class(py, RecordId::UsageRecord)?), ( "runtime_usage", - registry::class(py, RecordId::UsageKind)?.getattr("RUNTIME")?, + enum_member(py, RecordId::UsageKind, "RUNTIME")?, ), ( "resource_identity", - registry::class(py, RecordId::ResourceIdentity)?, + record_class(py, RecordId::ResourceIdentity)?, ), ( "model_kind", - registry::class(py, RecordId::ResourceKind)?.getattr("MODEL")?, + enum_member(py, RecordId::ResourceKind, "MODEL")?, ), ( "resource_ref", @@ -152,7 +132,6 @@ fn built_classes(py: Python<'_>) -> PyResult> { ), ("ref_kinds", ref_kinds.into_any()), ("ref_functions", ref_functions.into_any()), - ("store_tokens", store_tokens.into_any()), ( "relation_renderer", host::entry(py, UserCodeHost::RelationRenderer)?, @@ -188,7 +167,7 @@ fn result_model<'py>( py: Python<'py>, pairs: &[(&str, Bound<'py, PyAny>)], ) -> PyResult> { - call_with(®istry::class(py, RecordId::MacroExpansionResult)?, pairs) + new_record_with(py, RecordId::MacroExpansionResult, pairs) } /// The macro expansion of `request`'s SQL, or `None` when it calls no macro. @@ -217,10 +196,7 @@ pub(crate) fn macro_output(request: ExpansionRequest<'_>) -> PyResult = Bound::new(py, expansion)?; @@ -234,12 +210,12 @@ pub(crate) fn expansion_result<'py>(request: ExpansionRequest<'py>) -> PyResult< let Some(output) = macro_output(request)? else { return result_model(py, &[("sql", sql.into_any())]); }; - let span_class: Bound<'py, PyAny> = registry::class(py, RecordId::ExpansionSpan)?; let mut spans: Vec> = Vec::with_capacity(output.spans.len()); for span in output.spans { let (source_start, source_end, output_start, output_end) = span_row(span); - spans.push(call_with( - &span_class, + spans.push(new_record_with( + py, + RecordId::ExpansionSpan, &[ ("source_start", source_start.into_pyobject(py)?.into_any()), ("source_end", source_end.into_pyobject(py)?.into_any()), @@ -256,7 +232,7 @@ pub(crate) fn expansion_result<'py>(request: ExpansionRequest<'py>) -> PyResult< .iter() .map(|(kind, name)| format!("{}(\"{name}\")", typed_reference_function(kind))) .collect(); - let syntax: Bound<'py, PyAny> = registry::class(py, RecordId::SqlLexicalSyntax)?.call0()?; + let syntax: Bound<'py, PyAny> = new_record(py, RecordId::SqlLexicalSyntax, (), None)?; extract_references(&calls.join(" "), &syntax)?.into_any() }; result_model( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/render/macro_generated_references.rs b/crates/sqlbuild-python/src/bindings/_helpers/render/macro_generated_references.rs index 8cb827b0b4..87fa73b089 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/render/macro_generated_references.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/render/macro_generated_references.rs @@ -11,6 +11,7 @@ use sqlbuild_sqltext::sql_references::models::{ }; use sqlbuild_sqltext::sql_scan::models::LexicalSyntax; +use crate::bindings::_helpers::compile_driver::diagnostic_collector::report_compile_diagnostic; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::sqltext::lexical_syntax::LexicalSyntaxInput; @@ -31,7 +32,6 @@ const MODEL_KIND: &str = "model"; #[pyo3(from_item_all)] pub(crate) struct GeneratedReferenceClasses<'py> { compiler_diagnostic: Bound<'py, PyAny>, - report_diagnostic: Bound<'py, PyAny>, source_location: Bound<'py, PyAny>, resource_identity: Bound<'py, PyAny>, generic_syntax: LexicalSyntaxInput, @@ -98,9 +98,7 @@ fn resource_kind( return Ok(None); } Ok(Some(( - native_field(consumer, "kind")? - .getattr("value")? - .extract()?, + native_field(&native_field(consumer, "kind")?, "value")?.extract()?, native_field(consumer, "name")?.extract()?, ))) } @@ -247,13 +245,9 @@ impl<'py> Reporter<'_, 'py> { } fn report(&self, key: Bound<'py, PyTuple>, fields: &Bound<'py, PyDict>) -> PyResult<()> { - let arguments: Bound<'py, PyDict> = PyDict::new(self.py()); - arguments.set_item("key", key)?; - arguments.set_item( - "diagnostic", - self.classes.compiler_diagnostic.call((), Some(fields))?, - )?; - let _ = self.classes.report_diagnostic.call((), Some(&arguments))?; - Ok(()) + report_compile_diagnostic( + key.as_any(), + &self.classes.compiler_diagnostic.call((), Some(fields))?, + ) } } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/result_output/assets.rs b/crates/sqlbuild-python/src/bindings/_helpers/result_output/assets.rs index 106c358225..24ebb20da0 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/result_output/assets.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/result_output/assets.rs @@ -43,7 +43,10 @@ fn targets<'py>( }; let py: Python<'py> = plan.py(); let names: Bound<'py, PyDict> = PyDict::new(py); - for item in plan.getattr(field)?.call_method0("items")?.try_iter()? { + for item in native_field(plan, field)? + .call_method0("items")? + .try_iter()? + { let (name, location): (Bound<'py, PyAny>, Bound<'py, PyAny>) = item?.extract()?; names.set_item(name, native_field(&location, "qualified_name")?)?; } @@ -64,20 +67,12 @@ fn target<'py>( fn last_recorded_sql<'py>(result: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = result.py(); - if !result - .getattr("status")? - .getattr("value")? - .eq(FAILED_STATUS)? - { + if !native_field(&native_field(result, "status")?, "value")?.eq(FAILED_STATUS)? { return Ok(py.None().into_bound(py)); } let events: Vec> = items(&native_field(result, "lifecycle_events")?)?; for event in events.iter().rev() { - if event - .getattr("kind")? - .getattr("value")? - .eq(SQL_EVENT_KIND)? - { + if native_field(&native_field(event, "kind")?, "value")?.eq(SQL_EVENT_KIND)? { return native_field(event, "content"); } } @@ -96,7 +91,7 @@ fn microbatch<'py>(result: &Bound<'py, PyAny>) -> PyResult> { ("strategy", "microbatch_strategy"), ("reason", "microbatch_plan_reason"), ] { - let value: Bound<'py, PyAny> = result.getattr(field)?; + let value: Bound<'py, PyAny> = native_field(result, field)?; if !value.is_none() { state.set_item(key, value)?; } @@ -140,7 +135,7 @@ fn microbatch<'py>(result: &Bound<'py, PyAny>) -> PyResult> { "microbatch_replay_requirement_state", ), ] { - state.set_item(key, result.getattr(field)?)?; + state.set_item(key, native_field(result, field)?)?; } let intervals: Bound<'py, PyList> = PyList::empty(py); for interval in items(&native_field(result, "microbatch_accounting_intervals")?)? { @@ -154,7 +149,7 @@ fn microbatch<'py>(result: &Bound<'py, PyAny>) -> PyResult> { "event_id", ] .into_iter() - .map(|field| Ok((field, interval.getattr(field)?))) + .map(|field| Ok((field, native_field(&interval, field)?))) .collect::>()?; pyo3::types::PyListMethods::append(&intervals, dict(py, fields)?)?; } @@ -173,12 +168,12 @@ fn microbatch<'py>(result: &Bound<'py, PyAny>) -> PyResult> { } fn value<'py>(result: &Bound<'py, PyAny>, field: &str) -> PyResult> { - native_field(&result.getattr(field)?, "value") + native_field(&native_field(result, field)?, "value") } /// `.value` when it is set. fn optional_value<'py>(result: &Bound<'py, PyAny>, field: &str) -> PyResult> { - let value: Bound<'py, PyAny> = result.getattr(field)?; + let value: Bound<'py, PyAny> = native_field(result, field)?; if !value.is_truthy()? { return Ok(value.py().None().into_bound(value.py())); } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/result_output/checks.rs b/crates/sqlbuild-python/src/bindings/_helpers/result_output/checks.rs index b2f50535f9..3a29d5e392 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/result_output/checks.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/result_output/checks.rs @@ -20,11 +20,11 @@ const COLLECTION_KINDS: [&str; 2] = ["list", "set"]; const ERROR_FIELDS: [&str; 3] = ["error_code", "error_help", "error_message"]; fn value<'py>(result: &Bound<'py, PyAny>, field: &str) -> PyResult> { - native_field(&result.getattr(field)?, "value") + native_field(&native_field(result, field)?, "value") } fn optional_value<'py>(result: &Bound<'py, PyAny>, field: &str) -> PyResult> { - let value: Bound<'py, PyAny> = result.getattr(field)?; + let value: Bound<'py, PyAny> = native_field(result, field)?; if value.is_none() { return Ok(value); } @@ -37,7 +37,7 @@ fn fields<'py>( ) -> PyResult)>> { names .iter() - .map(|name| Ok((*name, result.getattr(*name)?))) + .map(|name| Ok((*name, native_field(result, name)?))) .collect() } @@ -48,7 +48,7 @@ fn tuple_of<'py>(py: Python<'py>, entries: Vec>) -> PyResult< /// A SQL value as plain JSON: decimals as text, collections as lists, structs as objects. fn sql_value_json<'py>(value: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = value.py(); - let kind: String = text(&native_field(value, "kind")?.getattr("value")?)?; + let kind: String = text(&native_field(&native_field(value, "kind")?, "value")?)?; let inner: Bound<'py, PyAny> = native_field(value, "value")?; if kind == DECIMAL_KIND { return Ok(inner.str()?.into_any()); @@ -247,7 +247,7 @@ pub(crate) fn measurement_thresholds<'py>( } let rendered: Bound<'py, PyDict> = PyDict::new(thresholds.py()); for severity in ["warn", "error"] { - let bound: Bound<'py, PyAny> = thresholds.getattr(severity)?; + let bound: Bound<'py, PyAny> = native_field(thresholds, severity)?; if !bound.is_none() { rendered.set_item(severity, threshold_bound(&bound)?)?; } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/result_output/documents.rs b/crates/sqlbuild-python/src/bindings/_helpers/result_output/documents.rs index e27d6af154..29dacfc6df 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/result_output/documents.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/result_output/documents.rs @@ -130,7 +130,7 @@ fn emitted<'py>( fn count_status(results: &[Bound<'_, PyAny>], field: &str, status: &str) -> PyResult { let mut count: usize = 0; for result in results { - if native_field(&result.getattr(field)?, "value")?.eq(status)? { + if native_field(&native_field(result, field)?, "value")?.eq(status)? { count += 1; } } @@ -140,7 +140,7 @@ fn count_status(results: &[Bound<'_, PyAny>], field: &str, status: &str) -> PyRe fn count_truthy(results: &[Bound<'_, PyAny>], field: &str) -> PyResult { let mut count: usize = 0; for result in results { - if result.getattr(field)?.is_truthy()? { + if native_field(result, field)?.is_truthy()? { count += 1; } } @@ -176,13 +176,13 @@ fn sequence<'py>(py: Python<'py>, value: Option>) -> Bound<'py } fn count(result: &Bound<'_, PyAny>, field: &str) -> PyResult { - result.getattr(field)?.extract() + native_field(result, field)?.extract() } /// Format build command execution results as JSON. #[pyfunction] #[pyo3(signature = (**options))] -fn format_build_execution_json<'py>( +pub(crate) fn format_build_execution_json<'py>( py: Python<'py>, options: Option>, ) -> PyResult { @@ -202,7 +202,7 @@ fn audit_projection<'py>( ("failed_count", "audit_result_projection_failed_count"), ] .into_iter() - .map(|(key, field)| Ok((key, result.getattr(field)?))) + .map(|(key, field)| Ok((key, native_field(result, field)?))) .collect::>()?; let failed: bool = counts[2].1.is_truthy()?; let mut entries: Vec<(&'static str, Bound<'py, PyAny>)> = vec![( @@ -225,7 +225,7 @@ fn audit_projection<'py>( /// The build document with its audit-history projection, or a minimal degraded document. #[pyfunction] #[pyo3(signature = (**options))] -fn format_degradable_build_execution_json<'py>( +pub(crate) fn format_degradable_build_execution_json<'py>( py: Python<'py>, options: Option>, ) -> PyResult { @@ -284,7 +284,7 @@ fn build_json<'py>( let mut retained: Vec>> = Vec::new(); let mut partial: bool = false; for field in BUILD_RESULT_FIELDS { - let all: Bound<'py, PyAny> = result.getattr(field)?; + let all: Bound<'py, PyAny> = native_field(&result, field)?; let kept: Vec> = projection.terminal_results(&all)?; partial = partial || kept.len() != all.len()?; retained.push(kept); @@ -304,7 +304,7 @@ fn build_json<'py>( for warned in nodes.iter().chain(&checks_run).chain(&retained[3]) { python_warnings += native_field(warned, "warning_messages")?.len()?; } - let result_status: String = text(&native_field(&result, "status")?.getattr("value")?)?; + let result_status: String = text(&native_field(&native_field(&result, "status")?, "value")?)?; let status: String = if result_status == FAILED || python_fail > 0 || check_fail > 0 { FAILED.to_owned() } else { @@ -381,7 +381,7 @@ fn build_json<'py>( /// Format seed command execution results as JSON. #[pyfunction] #[pyo3(signature = (*, results, plan))] -fn format_seed_execution_json<'py>( +pub(crate) fn format_seed_execution_json<'py>( py: Python<'py>, results: Bound<'py, PyAny>, plan: Bound<'py, PyAny>, @@ -412,7 +412,7 @@ fn format_seed_execution_json<'py>( /// Format load command execution results as JSON. #[pyfunction] #[pyo3(signature = (*, results))] -fn format_load_execution_json<'py>( +pub(crate) fn format_load_execution_json<'py>( py: Python<'py>, results: Bound<'py, PyAny>, ) -> PyResult { @@ -443,7 +443,7 @@ fn format_load_execution_json<'py>( /// Format direct clone command execution results as JSON. #[pyfunction] #[pyo3(signature = (*, result, resource_types_by_name))] -fn format_clone_execution_json<'py>( +pub(crate) fn format_clone_execution_json<'py>( py: Python<'py>, result: Bound<'py, PyAny>, resource_types_by_name: Bound<'py, PyAny>, @@ -457,7 +457,7 @@ fn format_clone_execution_json<'py>( if !projection.resource_terminal_recorded(&name, &format!("{resource_type}:{name}"))? { continue; } - let status: Bound<'py, PyAny> = native_field(&item, "status")?.getattr("value")?; + let status: Bound<'py, PyAny> = native_field(&native_field(&item, "status")?, "value")?; failure_count += usize::from(status.eq(FAILED)?); warning_count += usize::from(status.eq("warning")?); assets.push(clone_asset(&projection, &item, &resource_type)?); @@ -487,7 +487,7 @@ fn format_clone_execution_json<'py>( /// Format test command execution results as JSON. #[pyfunction] #[pyo3(signature = (*, results))] -fn format_test_execution_json<'py>( +pub(crate) fn format_test_execution_json<'py>( py: Python<'py>, results: Bound<'py, PyAny>, ) -> PyResult { @@ -518,7 +518,7 @@ fn format_test_execution_json<'py>( /// Format audit command execution results as JSON. #[pyfunction] #[pyo3(signature = (*, results, configured_concurrency = None, worker_count = None))] -fn format_audit_execution_json<'py>( +pub(crate) fn format_audit_execution_json<'py>( py: Python<'py>, results: Bound<'py, PyAny>, configured_concurrency: Option>, @@ -591,7 +591,7 @@ fn namespace_execution<'py>( fn scenario_failed(result: &Bound<'_, PyAny>, local: bool) -> PyResult { if !local { - return native_field(result, "status")?.getattr("value")?.eq(FAILED); + return native_field(&native_field(result, "status")?, "value")?.eq(FAILED); } let local_status: Bound<'_, PyAny> = native_field(result, "local_status")?; if local_status.is_none() { @@ -604,7 +604,7 @@ fn scenario_failed(result: &Bound<'_, PyAny>, local: bool) -> PyResult { /// Format scenario test command execution results as JSON. #[pyfunction] #[pyo3(signature = (**options))] -fn format_scenario_execution_json<'py>( +pub(crate) fn format_scenario_execution_json<'py>( py: Python<'py>, options: Option>, ) -> PyResult { @@ -694,7 +694,7 @@ fn format_scenario_execution_json<'py>( /// Format scenario snapshot sync/refresh execution results as JSON. #[pyfunction] #[pyo3(signature = (**options))] -fn format_scenario_snapshot_execution_json<'py>( +pub(crate) fn format_scenario_snapshot_execution_json<'py>( py: Python<'py>, options: Option>, ) -> PyResult { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/result_output/integration_records.rs b/crates/sqlbuild-python/src/bindings/_helpers/result_output/integration_records.rs index 718ab80d5d..bda92085b5 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/result_output/integration_records.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/result_output/integration_records.rs @@ -82,12 +82,12 @@ fn entries(values: Vec<(&str, Bound<'_, PyAny>)>) -> Checked { } fn value<'py>(result: &Bound<'py, PyAny>, field: &str) -> PyResult> { - native_field(&result.getattr(field)?, "value") + native_field(&native_field(result, field)?, "value") } /// `.value` when it is truthy, else `None`. fn truthy_value<'py>(result: &Bound<'py, PyAny>, field: &str) -> PyResult> { - let value: Bound<'py, PyAny> = result.getattr(field)?; + let value: Bound<'py, PyAny> = native_field(result, field)?; if !value.is_truthy()? { return Ok(result.py().None().into_bound(result.py())); } @@ -104,7 +104,7 @@ fn planned_target<'py>( if plan.is_none() { return Ok(py.None().into_bound(py)); } - let locations: Bound<'py, PyAny> = plan.getattr(field)?; + let locations: Bound<'py, PyAny> = native_field(plan, field)?; if !locations.contains(name)? { return Ok(py.None().into_bound(py)); } @@ -120,7 +120,7 @@ fn model_microbatch(result: &Bound<'_, PyAny>) -> PyResult { let values: Bound<'_, pyo3::types::PyDict> = pyo3::types::PyDict::new(py); pyo3::types::PyDictMethods::set_item(&values, "run_type", run_type)?; for (key, field) in MICROBATCH_FIELDS { - let item: Bound<'_, PyAny> = result.getattr(field)?; + let item: Bound<'_, PyAny> = native_field(result, field)?; if !item.is_none() { pyo3::types::PyDictMethods::set_item(&values, key, item)?; } @@ -356,7 +356,7 @@ fn terminal_fields(terminal: &Bound<'_, PyAny>, event_sequence: usize) -> PyResu let payload: Bound<'_, PyAny> = native_field(terminal, "payload")?; let get = |key: &str| payload.call_method1("get", (key,)); let attribute = |key: &str| -> PyResult { - Ok(required_json(&terminal.getattr(key)?)?.unwrap_or(JsonValue::Null)) + Ok(required_json(&native_field(terminal, key)?)?.unwrap_or(JsonValue::Null)) }; let mut fields: Entries = vec![ ( @@ -377,7 +377,10 @@ fn terminal_fields(terminal: &Bound<'_, PyAny>, event_sequence: usize) -> PyResu ("invocation_id".to_owned(), attribute("invocation_id")?), ]; for key in ["run_id", "resource_id", "resource_attempt_id"] { - fields.push((key.to_owned(), text_or_empty(&terminal.getattr(key)?)?)); + fields.push(( + key.to_owned(), + text_or_empty(&native_field(terminal, key)?)?, + )); } fields.push(("operation_id".to_owned(), attribute("operation_id")?)); fields.push(("statement_id".to_owned(), attribute("statement_id")?)); diff --git a/crates/sqlbuild-python/src/bindings/_helpers/result_output/projection.rs b/crates/sqlbuild-python/src/bindings/_helpers/result_output/projection.rs index 80dbb1d709..32dfecc1e6 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/result_output/projection.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/result_output/projection.rs @@ -65,7 +65,7 @@ pub(crate) fn audit_check_id(result: &Bound<'_, PyAny>) -> PyResult { /// The canonical resource name and identifier of an executor result. pub(crate) fn resource_identity(result: &Bound<'_, PyAny>) -> PyResult> { - let name = |field: &str| -> PyResult { text(&result.getattr(field)?) }; + let name = |field: &str| -> PyResult { text(&native_field(result, field)?) }; Ok(Some(match result_kind(result)? { ResultKind::Model => { let resource: String = name("model_name")?; @@ -88,7 +88,7 @@ pub(crate) fn resource_identity(result: &Bound<'_, PyAny>) -> PyResult { let resource: String = name("node_name")?; - let kind: String = text(&native_field(result, "kind")?.getattr("value")?)?; + let kind: String = text(&native_field(&native_field(result, "kind")?, "value")?)?; (resource.clone(), format!("{kind}:{resource}")) } ResultKind::PythonCheck => { @@ -209,11 +209,7 @@ impl<'py> Projection<'py> { else { return Ok(None); }; - number( - &terminal - .getattr("payload")? - .call_method1("get", ("duration_ms",))?, - ) + number(&native_field(&terminal, "payload")?.call_method1("get", ("duration_ms",))?) } /// The rounded canonical duration of a result, else its own `duration_ms`. diff --git a/crates/sqlbuild-python/src/bindings/_helpers/result_output/resource_ids.rs b/crates/sqlbuild-python/src/bindings/_helpers/result_output/resource_ids.rs index 21130eb179..82b98a8bc7 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/result_output/resource_ids.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/result_output/resource_ids.rs @@ -73,7 +73,7 @@ pub(crate) fn sql_test_identity( test_name_field: &str, ) -> PyResult { sql_test_resource_id( - &result.getattr(test_name_field)?.extract::()?, + &native_field(result, test_name_field)?.extract::()?, native_field(result, "source_path")?, native_field(result, "block_index")?.extract()?, native_field(result, "case_name")?.extract()?, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/result_output/terminal_index.rs b/crates/sqlbuild-python/src/bindings/_helpers/result_output/terminal_index.rs index 2efa5b5e67..e8efe3bb2e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/result_output/terminal_index.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/result_output/terminal_index.rs @@ -70,7 +70,7 @@ fn payload_attempt_number(payload: &Bound<'_, PyAny>) -> PyResult> { } fn optional_text(event: &Bound<'_, PyAny>, name: &str) -> PyResult> { - event.getattr(name)?.extract() + native_field(event, name)?.extract() } /// The facts of one lifecycle event that terminal indexing reads. @@ -153,7 +153,7 @@ impl TerminalEventIndex { #[pymethods] impl TerminalEventIndex { #[new] - fn new(py: Python<'_>) -> PyResult { + pub(crate) fn new(py: Python<'_>) -> PyResult { Ok(Self { state: Mutex::new(IndexState { index: TerminalIndex::default(), @@ -294,7 +294,7 @@ pub(crate) fn current_terminal_event_index(py: Python<'_>) -> PyResult) -> TerminalEventIndexScope { +pub(crate) fn terminal_event_index_scope(index: Py) -> TerminalEventIndexScope { TerminalEventIndexScope { index, token: Mutex::new(None), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/builtin_catalogue.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/builtin_catalogue.rs index 9317352ed2..2b27e8f645 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/builtin_catalogue.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/builtin_catalogue.rs @@ -10,8 +10,8 @@ use sqlbuild_rules::rules::main::selected_sql_codes::selected_sql_codes; use sqlbuild_rules::rules::models::{BuiltinOption, BuiltinOptionKind}; use crate::bindings::_helpers::boundary::panics::compiler_guard; +use crate::bindings::_helpers::records::construct::{new_record, record_class}; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; const FAMILY_SUFFIX_LENGTH: usize = 3; @@ -55,13 +55,15 @@ fn rule<'py>( check: &Bound<'py, PyAny>, metadata: RuleMetadata, ) -> PyResult> { - let option_class: Bound<'py, PyAny> = registry::class(py, RecordId::RuleOption)?; + let option_class: Bound<'py, PyAny> = record_class(py, RecordId::RuleOption)?; let options: Vec> = builtin_options(&metadata.code) .into_iter() .map(|declared| option(&option_class, declared)) .collect::>()?; let guidance: Bound<'py, PyAny> = match metadata.guidance { - Some(guidance) => registry::class(py, RecordId::RuleGuidance)?.call( + Some(guidance) => new_record( + py, + RecordId::RuleGuidance, (), Some(&keywords( py, @@ -110,7 +112,7 @@ fn rule<'py>( ("guidance", guidance), ], )?; - registry::class(py, RecordId::Rule)?.call((), Some(&arguments)) + new_record(py, RecordId::Rule, (), Some(&arguments)) } /// Every built-in rule as a `Rule` whose `check` is the given placeholder. @@ -118,7 +120,7 @@ pub(crate) fn builtin_rules<'py>( py: Python<'py>, check: Bound<'py, PyAny>, ) -> PyResult>> { - let subjects: Bound<'py, PyAny> = registry::class(py, RecordId::RuleSubject)?; + let subjects: Bound<'py, PyAny> = record_class(py, RecordId::RuleSubject)?; compiler_guard(|| Ok(catalogue()))? .into_iter() .map(|metadata| rule(py, &subjects, &check, metadata)) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/catalogue.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/catalogue.rs index 7eeb1913e9..955823e09e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/catalogue.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/catalogue.rs @@ -16,9 +16,8 @@ use crate::bindings::_helpers::boundary::panics::compiler_guard; use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported, keywords}; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::native_host::user_code::UserCodeHost; +use crate::bindings::_helpers::planner_scope::scope::build_planner_scope; use crate::bindings::_helpers::records::fields::{field as record_field, path_posix, replaced}; -use crate::bindings::_helpers::records::ids::NativeAttr; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::rules::builtin_catalogue::builtin_rules; use crate::bindings::_helpers::rules::config_json::asdict_json; use crate::bindings::_helpers::rules::custom_rule_evidence::verify_custom_rules as verify_rule_sources; @@ -278,7 +277,7 @@ pub(crate) fn verify_custom_rules<'py>( /// The validated built-in and custom rule catalogue sorted by code. #[pyfunction] #[pyo3(signature = (*, config, project_dir, include_custom = true))] -fn build_rule_catalogue<'py>( +pub(crate) fn build_rule_catalogue<'py>( config: &Bound<'py, PyAny>, project_dir: &Bound<'py, PyAny>, include_custom: bool, @@ -355,7 +354,6 @@ pub(crate) fn resolved_ignore_selectors<'py>( if !scoped { return Ok(config.clone()); } - let scope: Bound<'py, PyAny> = registry::native(py, NativeAttr::BuildPlannerScope)?; let path_by_key: HashMap<(String, String), String> = resource_paths(project)?; let mut resolved: Vec> = Vec::with_capacity(ignores.len()); for ignore in ignores { @@ -364,9 +362,12 @@ pub(crate) fn resolved_ignore_selectors<'py>( resolved.push(ignore); continue; } - let selected: Bound<'py, PyAny> = call_with( - &scope, - &[("project", project.clone()), ("select", selectors.clone())], + let selected: Bound<'py, PyAny> = build_planner_scope( + py, + Some(keywords( + py, + &[("project", project.clone()), ("select", selectors.clone())], + )?), )?; let mut selector_paths: BTreeSet = BTreeSet::new(); for key in record_field(&selected, "selected_keys")?.try_iter()? { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/constants.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/constants.rs index 5ff1bb5822..81f80b8efa 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/constants.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/constants.rs @@ -26,3 +26,6 @@ pub(crate) const CUSTOM_HOST_CANCELLED_MARKER: &str = "cancelled"; pub(crate) const TYPE_PROOF_RULE_CODES: [&str; 1] = ["SQBRCONTRACT105"]; /// Runtime identity custom-rule hosts and their cache entries record. pub(crate) const CUSTOM_HOST_RUNTIME_VERSION: &str = "sqlbuild-rules-custom-v3"; + +/// Native rules evaluations answered from the response memo. +pub(crate) const RULES_MEMO_REUSES: &str = "rules_memo_reuses"; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/custom_evaluation.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/custom_evaluation.rs index 41e812d345..d6b9e3722e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/custom_evaluation.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/custom_evaluation.rs @@ -16,10 +16,13 @@ use sqlbuild_rules::custom_rule_cache::models::CustomEvaluation; use sqlbuild_rules::custom_rule_cache::types::ReadSet; use crate::bindings::_helpers::cache::native_store::current_code_identity; -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; +use crate::bindings::_helpers::compile_driver::python_calls::imported; +use crate::bindings::_helpers::compile_driver::python_calls::keywords; +use crate::bindings::_helpers::compiled_project::records::compiled_project_record; +use crate::bindings::_helpers::records::construct::{new_record, new_record_with}; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::fields::replaced_keywords; +use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::rules::constants::{ CUSTOM_HOST_RUNTIME_VERSION, CUSTOM_RULES_CACHE_FILE, }; @@ -27,8 +30,12 @@ use crate::bindings::_helpers::rules::custom_hosts::{ HostCancel, HostRequest, host_payload_project, run_custom_hosts, }; use crate::bindings::_helpers::rules::custom_rule_cache::CustomRuleCache; +use crate::bindings::_helpers::rules::custom_rule_cache::custom_rule_identity; +use crate::bindings::_helpers::rules::custom_rule_evidence::custom_rule_implementation_fingerprint; +use crate::bindings::_helpers::rules::custom_rule_evidence::custom_rule_import_closure; use crate::bindings::_helpers::rules::fact_encoder::{Encoded, reproducible}; use crate::bindings::_helpers::rules::fact_replay::{FactReplay, is_tree_fact}; +use crate::bindings::_helpers::rules::rule_run::rules_bulk_cache_path; const PROJECT_SUBJECT: &str = "project"; const MODEL_SUBJECT: &str = "model"; @@ -88,11 +95,8 @@ pub(crate) fn evaluate_custom_rules_cached<'py>( if custom.is_empty() { return outcome(py, (Vec::new(), 0, 0), (0, 0)); } - let enabled: bool = run - .config - .getattr("cache")? - .getattr("enabled")? - .is_truthy()?; + let enabled: bool = + native_field(&native_field(run.config, "cache")?, "enabled")?.is_truthy()?; let mut rows: Vec<(String, Option, Option)> = Vec::new(); for (code, rule) in &custom { let identity: Option = if enabled { @@ -107,19 +111,10 @@ pub(crate) fn evaluate_custom_rules_cached<'py>( rows.push((code.clone(), identity, subject_value(rule)?)); } let cache_path: Option = if enabled { - Some( - call_with( - ®istry::native(py, NativeAttr::RulesBulkCachePath)?, - &[ - ("project_dir", run.project_dir.clone()), - ( - "file_name", - pyo3::types::PyString::new(py, CUSTOM_RULES_CACHE_FILE).into_any(), - ), - ], - )? - .extract()?, - ) + Some(rules_bulk_cache_path( + run.project_dir.extract()?, + CUSTOM_RULES_CACHE_FILE, + )) } else { None }; @@ -179,8 +174,9 @@ pub(crate) fn outcome<'py>( (findings, hits, misses): (Vec>, usize, usize), (custom_ms, custom_cpu_ms): (i64, i64), ) -> PyResult> { - call_with( - ®istry::class(py, RecordId::CustomRulesOutcome)?, + new_record_with( + py, + RecordId::CustomRulesOutcome, &[ ("findings", PyTuple::new(py, findings)?.into_any()), ("cache_hits", hits.into_bound_py_any(py)?), @@ -308,10 +304,7 @@ fn orjson_key(key: &FactKey) -> String { /// The project without state no rule fact reads, before digests and host payloads use it. fn host_project<'py>(project: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = project.py(); - let options: Bound<'py, PyDict> = PyDict::new(py); - options.set_item("include_sql_expansions", false)?; - let record: Bound<'py, PyAny> = registry::native(py, NativeAttr::CompiledProjectRecord)? - .call((project,), Some(&options))?; + let record: Bound<'py, PyAny> = compiled_project_record(project, false)?; let changes: Bound<'py, PyDict> = PyDict::new(py); for (name, cleared) in HOST_PROJECT_CLEARED { match cleared { @@ -322,9 +315,9 @@ fn host_project<'py>(project: &Bound<'py, PyAny>) -> PyResult> } changes.set_item( "scope_index", - registry::class(py, RecordId::ScopeIndex)?.call0()?, + new_record(py, RecordId::ScopeIndex, (), None)?, )?; - imported(py, "dataclasses", "replace")?.call((record,), Some(&changes)) + replaced_keywords(&record, &changes) } fn subject_value(rule: &Bound<'_, PyAny>) -> PyResult> { @@ -352,26 +345,15 @@ fn rule_identity( let fingerprint: Bound<'_, PyAny> = match fingerprint { Some(fingerprint) => fingerprint.into_bound_py_any(py)?, None => { - let closure: Bound<'_, PyAny> = call_with( - ®istry::native(py, NativeAttr::CustomRuleImportClosure)?, - &[ - ("rule", rule.clone()), - ("project_dir", run.project_dir.clone()), - ], - )?; - call_with( - ®istry::native(py, NativeAttr::CustomRuleImplementationFingerprint)?, - &[ - ("rule", rule.clone()), - ("project_dir", run.project_dir.clone()), - ("import_closure", closure), - ], - )? + let closure: Bound<'_, PyAny> = + custom_rule_import_closure(rule, run.project_dir)?.into_any(); + custom_rule_implementation_fingerprint(rule, run.project_dir, Some(&closure))? + .into_bound_py_any(py)? } }; let subject: Option = subject_value(rule)?; - call_with( - ®istry::native(py, NativeAttr::CustomRuleIdentity)?, + custom_rule_identity(Some(&keywords( + py, &[ ( "code_identity", @@ -395,8 +377,7 @@ fn rule_identity( run.project_dir.call_method0("resolve")?.str()?.into_any(), ), ], - )? - .extract() + )?)) } /// This thread's CPU plus that of finished child processes, which run the rule hosts. diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/custom_hosts.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/custom_hosts.rs index 832fc8e2da..da3c4f08cb 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/custom_hosts.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/custom_hosts.rs @@ -26,9 +26,12 @@ use sqlbuild_rules::custom_host_pool::types::{HostInvocation, HostPlan}; use sqlbuild_rules::engine::main::custom_host::run_custom_host_json; use crate::bindings::_helpers::compile_driver::python_calls::imported; +use crate::bindings::_helpers::compiled_project::records::compiled_project_record; use crate::bindings::_helpers::native_host::constants::CUSTOM_HOST_LAUNCH_MODULE; +use crate::bindings::_helpers::records::construct::new_record; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr, RecordId}; +use crate::bindings::_helpers::records::fields::replaced_keywords; +use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::rules::constants::{ CUSTOM_HOST_ABANDONED_SUFFIX, CUSTOM_HOST_CANCELLED_MARKER, CUSTOM_HOST_RUNTIME_VERSION, @@ -411,22 +414,20 @@ pub(crate) fn host_payload_project<'py>( project: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = project.py(); - let model_class: Bound<'py, PyAny> = registry::class(py, RecordId::CompiledModel)?; let mut models: Vec> = Vec::new(); for model in native_field(project, "models")?.try_iter()? { let model: Bound<'py, PyAny> = model?; let fields: Bound<'py, PyDict> = PyDict::new(py); for name in PAYLOAD_MODEL_FIELDS { - fields.set_item(name, model.getattr(name)?)?; + fields.set_item(name, native_field(&model, name)?)?; } - models.push(model_class.call((), Some(&fields))?); + models.push(new_record(py, RecordId::CompiledModel, (), Some(&fields))?); } - let record: Bound<'py, PyAny> = - registry::native(py, NativeAttr::CompiledProjectRecord)?.call1((project,))?; + let record: Bound<'py, PyAny> = compiled_project_record(project, true)?; let options: Bound<'py, PyDict> = PyDict::new(py); options.set_item("models", PyTuple::new(py, models)?)?; options.set_item("native_project", py.None())?; - imported(py, "dataclasses", "replace")?.call((record,), Some(&options)) + replaced_keywords(&record, &options) } /// Usable CPUs: the process CPU count capped by any cgroup CPU quota. diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/custom_rule_cache.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/custom_rule_cache.rs index a1afcc0327..cc5d761e9f 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/custom_rule_cache.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/custom_rule_cache.rs @@ -30,7 +30,7 @@ use crate::bindings::_helpers::rules::findings::{FindingClasses, invalid_finding /// One custom rule's cache identity from its metadata, source fingerprint and settings. #[pyfunction] #[pyo3(signature = (**request))] -fn custom_rule_identity(request: Option<&Bound<'_, PyDict>>) -> PyResult { +pub(crate) fn custom_rule_identity(request: Option<&Bound<'_, PyDict>>) -> PyResult { let request = Request::new("custom_rule_identity", request)?; Ok(rule_identity(&RuleIdentityInput { code_identity: request.get("code_identity")?.extract()?, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/custom_rule_evidence.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/custom_rule_evidence.rs index d3dd9b1398..108536a0e1 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/custom_rule_evidence.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/custom_rule_evidence.rs @@ -59,7 +59,7 @@ fn closure_paths<'py>(py: Python<'py>, paths: Vec) -> PyResult( +pub(crate) fn custom_rule_import_closure<'py>( rule: &Bound<'py, PyAny>, project_dir: &Bound<'py, PyAny>, ) -> PyResult> { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/dsl_headers.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/dsl_headers.rs index 091d27f073..4514422393 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/dsl_headers.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/dsl_headers.rs @@ -12,10 +12,9 @@ use sqlbuild_rules::dsl_headers::main::sql_body_ranges::sql_body_ranges as body_ use sqlbuild_rules::dsl_headers::models::{HeaderRulesConfig, HeaderSpan, HeaderViolation}; use crate::bindings::_helpers::boundary::panics::value_error; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; +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; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::rules::lint_pipeline::{LintViolationRow, ViolationClasses}; use crate::bindings::_helpers::rules::native_header_parser::NativeHeaderParser; @@ -71,12 +70,12 @@ fn scan_headers<'py>( first_only: bool, ) -> PyResult> { let py: Python<'py> = contents.py(); - let class: Bound<'py, PyAny> = registry::class(py, RecordId::HeaderSpan)?; let headers: Vec> = scan(&text(contents)?, first_only) .into_iter() .map(|header| { - call_with( - &class, + new_record_with( + py, + RecordId::HeaderSpan, &[ ("kind", PyString::new(py, &header.kind).into_any()), ("start", header.start.into_pyobject(py)?.into_any()), @@ -135,7 +134,7 @@ fn lint_native_headers<'py>( /// `contents` with header formatting fixes applied, and the faults relocation reported. #[pyfunction] #[pyo3(signature = (*, contents, file_path, config))] -fn format_native_headers<'py>( +pub(crate) fn format_native_headers<'py>( contents: &Bound<'py, PyAny>, file_path: &Bound<'py, PyAny>, config: &Bound<'py, PyAny>, @@ -168,7 +167,7 @@ fn parse_faults<'py>( /// Parse-safe files, and every file's authored newline convention. #[pyfunction] #[pyo3(signature = (*, files, config))] -fn safe_format_files<'py>( +pub(crate) fn safe_format_files<'py>( files: &Bound<'py, PyDict>, config: &Bound<'py, PyAny>, ) -> PyResult<(Bound<'py, PyDict>, Bound<'py, PyDict>)> { @@ -199,7 +198,7 @@ fn safe_format_files<'py>( /// Every rewrite whose header still parses, with the faults of the discarded ones appended. #[pyfunction] #[pyo3(signature = (*, updated, config, faults))] -fn reject_unparseable_header_rewrites<'py>( +pub(crate) fn reject_unparseable_header_rewrites<'py>( updated: &Bound<'py, PyDict>, config: &Bound<'py, PyAny>, faults: &Bound<'py, PyList>, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/fact_replay.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/fact_replay.rs index ec38b06a57..28e05abf1b 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/fact_replay.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/fact_replay.rs @@ -21,10 +21,10 @@ use crate::bindings::_helpers::compiled_project::function_views::NativeCompiledF use crate::bindings::_helpers::compiled_project::project_view::NativeProjectView; use crate::bindings::_helpers::compiled_project::seed_views::NativeCompiledSeed; use crate::bindings::_helpers::compiled_project::source_views::NativeCompiledSource; +use crate::bindings::_helpers::records::construct::record_class; use crate::bindings::_helpers::records::fields::field as record_field_or_attribute; use crate::bindings::_helpers::records::fields::record_spec; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::rules::fact_encoder::{ Encoded, FactEncoder, FactFailure, error_digest, value_digest, }; @@ -806,7 +806,7 @@ fn item_fields<'py>(encoder: &FactEncoder, item: &Bound<'py, PyAny>) -> PyResult .collect::>()?; return Ok((encoder.qualified_name(&class)?, names, values)); }; - let class: Bound<'py, PyAny> = registry::class(py, record)?; + let class: Bound<'py, PyAny> = record_class(py, record)?; let class: &Bound<'py, PyType> = class.downcast::()?; let names: Vec<&'static str> = record_spec(class)? .fields diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/findings.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/findings.rs index 7a459f62a6..bd70da9235 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/findings.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/findings.rs @@ -1,5 +1,6 @@ //! Rule findings converted between the Python `Finding` model and native rows and payloads. +use crate::bindings::_helpers::records::construct::record_class; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -21,7 +22,7 @@ pub(crate) struct FindingClasses<'py> { impl<'py> FindingClasses<'py> { pub(crate) fn new(py: Python<'py>) -> PyResult { Ok(Self { - finding: registry::class(py, RecordId::Finding)?, + finding: record_class(py, RecordId::Finding)?, path: py.import(PATHLIB_MODULE)?.getattr(PATH_CLASS)?, }) } @@ -83,8 +84,7 @@ pub(crate) fn finding_fault(finding: &Bound<'_, PyAny>) -> PyResult { Ok(Fault { unevaluated: native_field(finding, "unevaluated")?.extract()?, code: native_field(finding, "code")?.extract()?, - path: finding - .getattr("path")? + path: native_field(finding, "path")? .call_method0("as_posix")? .extract()?, line: native_field(finding, "line")?.extract()?, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/fix_facts.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/fix_facts.rs index 8fe9e4a7c9..d679428d87 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/fix_facts.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/fix_facts.rs @@ -4,11 +4,10 @@ use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; use pyo3::pyfunction; use pyo3::types::{PyDict, PyDictMethods, PyTuple}; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; +use crate::bindings::_helpers::records::construct::new_record_with; use crate::bindings::_helpers::records::fields::field as record_field; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::rules::fact_replay::project_attribute; /// Index a compilation by model key and diagnostic owner without changing its fact views. @@ -56,8 +55,9 @@ pub(crate) fn compile_facts<'py>( let count: i64 = counts.get_item(&identity)?.extract()?; counts.set_item(identity, count + 1)?; } - call_with( - ®istry::class(py, RecordId::CompileFacts)?, + new_record_with( + py, + RecordId::CompileFacts, &[ ("models", models.into_any()), ("model_paths", model_paths.into_any()), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/fix_session.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/fix_session.rs index f5a82effb1..7acfa28d88 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/fix_session.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/fix_session.rs @@ -16,12 +16,11 @@ use sqlbuild_rules::fix_verification::models::{FixEdit, FixEligibility}; use crate::bindings::_helpers::compile_attachment::sql_expansion_context::SqlExpansionInputs; use crate::bindings::_helpers::compile_driver::project::build_compiled_project; -use crate::bindings::_helpers::compile_driver::python_calls::{ - KeywordArguments, call_with, keywords, -}; +use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, keywords}; use crate::bindings::_helpers::discovery::model_file_record::NativeModelFile; use crate::bindings::_helpers::native_host::adapter::AdapterHost; use crate::bindings::_helpers::native_host::host; +use crate::bindings::_helpers::records::construct::new_record_with; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::fields::{field, replaced}; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; @@ -657,8 +656,9 @@ fn report<'py>( reason: &str, ) -> PyResult> { let py = source.py(); - call_with( - ®istry::class(py, RecordId::RuleFixResult)?, + new_record_with( + py, + RecordId::RuleFixResult, &[ ("file_path", field(source, "file_path")?), ("code", field(source, "code")?), @@ -687,8 +687,9 @@ fn violation<'py>( details: (&str, &str, &str, &str), ) -> PyResult> { let py = path.py(); - call_with( - ®istry::class(py, RecordId::LintViolation)?, + new_record_with( + py, + RecordId::LintViolation, &[ ("file_path", path.clone()), ("line", 1_i64.into_pyobject(py)?.into_any()), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/fix_verdict.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/fix_verdict.rs index 8aa4c95f2d..d58ccb5322 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/fix_verdict.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/fix_verdict.rs @@ -8,10 +8,9 @@ use pyo3::types::{PyDict, PyDictMethods, PyList, PyListMethods, PyTuple}; use sqlbuild_rules::fix_verification::main::compare::compare; use sqlbuild_rules::fix_verification::models::{DiagnosticComparison, ModelComparison}; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; +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; -use crate::bindings::_helpers::records::registry; /// Compare compile facts using native decisions and the existing Python boundary models. #[pyfunction] @@ -71,8 +70,9 @@ pub(crate) fn fix_verdict<'py>( for (owner, reason) in result.failing { failing.set_item(owners.get_item(owner)?, reason)?; } - call_with( - ®istry::class(py, RecordId::FixVerdict)?, + new_record_with( + py, + RecordId::FixVerdict, &[ ("failing", failing.into_any()), ( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/fixture_formatting.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/fixture_formatting.rs index 5d85cd1d93..0e03ecccca 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/fixture_formatting.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/fixture_formatting.rs @@ -25,7 +25,7 @@ fn simplify_fixture_null_projections(sql: &str, relation: &Bound<'_, PyAny>) -> #[pyfunction] #[pyo3(signature = (*, files, project_dir, discovered_inputs, sql_lexical_syntax))] -fn format_redundant_fixture_nulls<'py>( +pub(crate) fn format_redundant_fixture_nulls<'py>( files: &Bound<'py, PyDict>, project_dir: &Bound<'py, PyAny>, discovered_inputs: &Bound<'py, PyAny>, @@ -34,8 +34,7 @@ fn format_redundant_fixture_nulls<'py>( compiler_guard(|| { let py = files.py(); let relations = fixture_schemas::relations(discovered_inputs)?; - let syntax: LexicalSyntax = sql_lexical_syntax - .getattr("native_mapping")? + let syntax: LexicalSyntax = native_field(sql_lexical_syntax, "native_mapping")? .extract::()? .into(); let updated = PyDict::new(py); @@ -67,9 +66,7 @@ fn format_redundant_fixture_nulls<'py>( let block = block?; Ok(FixtureFormatBlock { sql_body: native_field(&block, "sql_body")?.extract()?, - model_mode: block - .getattr("mode")? - .getattr("value")? + model_mode: native_field(&native_field(&block, "mode")?, "value")? .extract::()? == MODEL_TEST_MODE, }) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/guidance.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/guidance.rs index da53ed57d0..061a2f62ce 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/guidance.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/guidance.rs @@ -92,7 +92,10 @@ fn rule_text(rule: &Bound<'_, PyAny>, config: Option<&Bound<'_, PyAny>>) -> PyRe /// `sqb rules show` text for one rule, with effective thresholds when a config is given. #[pyfunction] #[pyo3(signature = (*, rule, config = None))] -fn format_rule(rule: &Bound<'_, PyAny>, config: Option>) -> PyResult { +pub(crate) fn format_rule( + rule: &Bound<'_, PyAny>, + config: Option>, +) -> PyResult { let config: Option> = config.filter(|config| !config.is_none()); let native: Option = config.as_ref().map(native_rules_config).transpose()?; let text: RuleText = rule_text(rule, config.as_ref())?; @@ -186,7 +189,7 @@ fn lock_path(project_dir: &Path) -> PyResult { /// Install every owned rules skill target, or with `check` report whether all are fresh. #[pyfunction] #[pyo3(signature = (*, config, project_dir, check))] -fn install_rules_skills( +pub(crate) fn install_rules_skills( config: &Bound<'_, PyAny>, project_dir: &Bound<'_, PyAny>, check: bool, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_headers.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_headers.rs index 724e4ca762..2fc215f8b7 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_headers.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_headers.rs @@ -5,10 +5,9 @@ use pyo3::pyfunction; use pyo3::types::PyBool; use sqlbuild_rules::lint_text::main::named_declaration_role_kind::named_declaration_role_kind; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; +use crate::bindings::_helpers::records::construct::{enum_from_value, new_record, new_record_with}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; const HOOKS_FOLDER: &str = "hooks"; @@ -28,18 +27,19 @@ pub(crate) fn lint_file_role<'py>( .call_method1("is_relative_to", (project_dir,))? .is_truthy()? { - return registry::class(py, RecordId::LintFileRole)?.call0(); + return new_record(py, RecordId::LintFileRole, (), None); } file_path.call_method1("relative_to", (project_dir,))? } }; let parts: Vec = native_field(&relative, "parts")?.extract()?; let kind: Bound<'py, PyAny> = match named_declaration_role_kind(&parts) { - Some(kind) => registry::class(py, RecordId::DeclarationKind)?.call1((kind,))?, + Some(kind) => enum_from_value(py, RecordId::DeclarationKind, kind)?, None => py.None().into_bound(py), }; - call_with( - ®istry::class(py, RecordId::LintFileRole)?, + new_record_with( + py, + RecordId::LintFileRole, &[ ("declaration_kind", kind), ("in_project", PyBool::new(py, true).to_owned().into_any()), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_inputs.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_inputs.rs index 23a843bc46..0252f0f7e6 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_inputs.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_inputs.rs @@ -83,8 +83,7 @@ pub(crate) fn model_lint_expansion( if record.is_none() { return Ok(None); } - let path: PathBuf = model - .getattr("model_file")? + let path: PathBuf = native_field(model, "model_file")? .getattr(FILE_PATH_ATTRIBUTE)? .extract()?; Ok(Some(( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_pipeline.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_pipeline.rs index fe3ae76b86..9c64ee4dc4 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_pipeline.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_pipeline.rs @@ -36,6 +36,7 @@ use sqlbuild_rules::sql_lint::types::PreparedSql; use crate::bindings::_helpers::boundary::panics::value_error; use crate::bindings::_helpers::compile_attachment::sql_expansion_context::SqlExpansionInputs; use crate::bindings::_helpers::compile_driver::python_calls::call_with; +use crate::bindings::_helpers::records::construct::{new_record_with, record_class}; use crate::bindings::_helpers::records::fields::field as record_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -317,9 +318,9 @@ pub(crate) struct ViolationClasses<'py> { impl<'py> ViolationClasses<'py> { pub(crate) fn new(py: Python<'py>) -> PyResult { Ok(Self { - violation: registry::class(py, RecordId::LintViolation)?, - edit: registry::class(py, RecordId::LintEdit)?, - severity: registry::class(py, RecordId::LintSeverity)?, + violation: record_class(py, RecordId::LintViolation)?, + edit: record_class(py, RecordId::LintEdit)?, + severity: record_class(py, RecordId::LintSeverity)?, }) } @@ -416,8 +417,9 @@ impl LintOutcome { for (file, message) in &self.unexpandable { unexpandable.set_item(&paths[*file], message)?; } - call_with( - ®istry::class(py, RecordId::LintRunResult)?, + new_record_with( + py, + RecordId::LintRunResult, &[ ( "files_checked", diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_preparation.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_preparation.rs index 8e063a9d8e..155fe9f742 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_preparation.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_preparation.rs @@ -6,10 +6,10 @@ use pyo3::types::{PyBool, PyDict, PyTuple}; use sqlbuild_rules::lint_text::models::{ExpansionSpan, LintSqlBody}; use crate::bindings::_helpers::compile_attachment::sql_expansion_context::SqlExpansionInputs; -use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; +use crate::bindings::_helpers::compile_driver::python_calls::KeywordArguments; +use crate::bindings::_helpers::records::construct::{new_record_with, record_class}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::rules::lint_pipeline::{ BodyRole, SingleBody, SingleBodyRequest, prepare_single_body, }; @@ -36,7 +36,7 @@ fn pass_objects<'py>( py: Python<'py>, passes: &[Vec], ) -> PyResult> { - let class: Bound<'py, PyAny> = registry::class(py, RecordId::ExpansionSpan)?; + let class: Bound<'py, PyAny> = record_class(py, RecordId::ExpansionSpan)?; let passes: Vec> = passes .iter() .map(|spans| span_objects(&class, spans)) @@ -86,8 +86,9 @@ pub(crate) fn prepare_lint_body<'py>( })?; let body: &LintSqlBody = &prepared.body; let flag = |value: bool| PyBool::new(py, value).to_owned().into_any(); - call_with( - ®istry::class(py, RecordId::LintBody)?, + new_record_with( + py, + RecordId::LintBody, &[ ("file_path", file_path.clone()), ("body_start", body.body_start.into_pyobject(py)?.into_any()), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_text.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_text.rs index 0183cc8d3b..7efa8a6682 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_text.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_text.rs @@ -22,10 +22,11 @@ use sqlbuild_rules::lint_text::models::{ }; use crate::bindings::_helpers::boundary::panics::{compiler_guard, value_error}; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; +use crate::bindings::_helpers::records::construct::{ + enum_from_value, enum_member, new_record_with, +}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::rules::lint_pipeline::{ LintViolationRow, NATIVE_ENGINE, ViolationClasses, }; @@ -130,9 +131,11 @@ fn named_declaration_role_kind<'py>( ) -> PyResult>> { let parts: Vec = native_field(relative_path, "parts")?.extract()?; match role_kind(&parts) { - Some(kind) => Ok(Some( - registry::class(relative_path.py(), RecordId::DeclarationKind)?.call1((kind,))?, - )), + Some(kind) => Ok(Some(enum_from_value( + relative_path.py(), + RecordId::DeclarationKind, + kind, + )?)), None => Ok(None), } } @@ -206,7 +209,7 @@ fn relative_parts(path: &std::path::Path, project_dir: &std::path::Path) -> Opti /// Format the SQL bodies of `files`; return changed texts by path and declined-body faults. #[pyfunction] -fn format_lint_sql_bodies<'py>( +pub(crate) fn format_lint_sql_bodies<'py>( py: Python<'py>, files: Vec<(Bound<'py, PyAny>, String)>, project_dir: PathBuf, @@ -232,15 +235,12 @@ fn format_lint_sql_bodies<'py>( for (index, text) in result.formatted { formatted.set_item(&files[index].0, text)?; } - let violation: Bound<'py, PyAny> = registry::class(py, RecordId::LintViolation)?; - let fault: Bound<'py, PyAny> = registry::class(py, RecordId::LintSeverity)?.getattr("FAULT")?; + let fault: Bound<'py, PyAny> = enum_member(py, RecordId::LintSeverity, "FAULT")?; let faults: Vec> = result .faults .into_iter() .map(|item| { - call_with( - &violation, - &[ + new_record_with(py, RecordId::LintViolation, &[ ("file_path", files[item.file].0.clone()), ("line", item.line.into_pyobject(py)?.into_any()), ("column", 1_usize.into_pyobject(py)?.into_any()), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/relation_keys.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/relation_keys.rs index f673fe6c24..d5f429d621 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/relation_keys.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/relation_keys.rs @@ -54,8 +54,7 @@ pub(crate) fn relation_key_row( } for audit in declared_audits.try_iter()? { let audit: Bound<'_, PyAny> = audit?; - let argument: Option> = audit - .getattr("arguments")? + let argument: Option> = native_field(&audit, "arguments")? .downcast::()? .get_item(UNIQUE_AUDIT_COLUMN_ARGUMENT)?; audits.push(( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/rows.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/rows.rs index bd58de2122..7ab29e953d 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/rows.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/rows.rs @@ -28,20 +28,21 @@ use sqlbuild_rules::rule_cache::main::rules_bulk_cache_path::rules_bulk_cache_pa use crate::bindings::_helpers::boundary::keyword_request::Request; use crate::bindings::_helpers::boundary::panics::{compiler_guard, value_error}; use crate::bindings::_helpers::cache::native_store::current_code_identity; -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; +use crate::bindings::_helpers::compile_driver::python_calls::imported; use crate::bindings::_helpers::compiled_project::audit_views::NativeCompiledAudit; use crate::bindings::_helpers::compiled_project::project::NativeCompiledProject; use crate::bindings::_helpers::compiled_project::records::compiled_project_facts; use crate::bindings::_helpers::compiled_project::records::declaration_rows; use crate::bindings::_helpers::compiled_project::values::plain_value; +use crate::bindings::_helpers::records::construct::new_record_with; use crate::bindings::_helpers::records::fields::field; -use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::rules::config_json::asdict_json; use crate::bindings::_helpers::rules::fact_replay::{project_attribute, project_models}; use crate::bindings::_helpers::rules::findings::{FindingClasses, finding_fault}; use crate::bindings::_helpers::rules::selection::rules_error; use crate::bindings::_helpers::rules::sql_test_objects::{scenario_fact, sql_test_fact}; +use crate::bindings::_helpers::scopes::scope_reports::metadata_projection; use crate::bindings::_helpers::type_system::equality::types_equal; use crate::bindings::types::CompilerDetach; @@ -74,13 +75,13 @@ struct ProjectRows<'py> { /// A natively built rules request held until it is evaluated once. #[pyclass(module = "sqlbuild._native")] -struct NativeRulesRequest { +pub(crate) struct NativeRulesRequest { request: Mutex>, } #[pymethods] impl NativeRulesRequest { - /// The SQL test and scenario facts this request carries, as `{"sql_tests", "sql_scenarios"}` JSON. + /// The SQL test, scenario and scope facts this request carries, as `{"sql_tests", "sql_scenarios", "scope_index"}` JSON. fn sql_facts_json(&self) -> PyResult { let guard = self .request @@ -92,6 +93,7 @@ impl NativeRulesRequest { serde_json::to_string(&serde_json::json!({ "sql_tests": rows.request.sql_tests, "sql_scenarios": rows.request.sql_scenarios, + "scope_index": rows.request.scope_index, })) .map_err(value_error) } @@ -100,7 +102,9 @@ impl NativeRulesRequest { /// Build the built-in rules request natively from the compiled project's rows. #[pyfunction] #[pyo3(signature = (**options))] -fn build_rules_request(options: Option<&Bound<'_, PyDict>>) -> PyResult { +pub(crate) fn build_rules_request( + options: Option<&Bound<'_, PyDict>>, +) -> PyResult { let request = Request::new("build_rules_request", options)?; let py: Python<'_> = request.py(); as_rules_error(py, built_request(&request)) @@ -119,10 +123,9 @@ fn built_request(options: &Request<'_, '_>) -> PyResult { "project_dir": resolved.str()?.to_str()?, "dialect": dialect.extract::()?, "config": config_value(&config)?, - "scope_index": plain_value(&call_with( - ®istry::native(py, NativeAttr::ScopeMetadataProjection)?, - &[("index", project_attribute(&project, "scope_index")?)], - )?)?, + "scope_index": plain_value( + metadata_projection(py, &project_attribute(&project, "scope_index")?)?.bind(py), + )?, "defer_suppressions": true, "custom_rules": plain_value(&options.get("custom_payloads")?)?, "rules_cache_path": path_text(py, rules_bulk_cache_path(&resolved_path, NATIVE_RULES_CACHE_FILE))?, @@ -218,7 +221,7 @@ fn built_request(options: &Request<'_, '_>) -> PyResult { /// Answer the request from the response memo or evaluate it, decoding findings natively. #[pyfunction] -fn evaluate_rules_request<'py>( +pub(crate) fn evaluate_rules_request<'py>( py: Python<'py>, request: &Bound<'py, NativeRulesRequest>, ) -> PyResult> { @@ -249,8 +252,9 @@ fn evaluated<'py>( .into_iter() .map(|fault| classes.finding_of_fault(fault, false)) .collect::>()?; - call_with( - ®istry::class(py, RecordId::NativeRulesEvaluation)?, + new_record_with( + py, + RecordId::NativeRulesEvaluation, &[ ("findings", PyTuple::new(py, findings)?.into_any()), ( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/rule_api.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/rule_api.rs index 0366b908fd..bb591563ad 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/rule_api.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/rule_api.rs @@ -17,6 +17,8 @@ use sqlbuild_rules::project_tree::main::project_tree_entries::project_tree_entri use sqlbuild_rules::project_tree::models::ProjectTreeEntry; use crate::bindings::_helpers::compile_driver::python_calls::imported; +use crate::bindings::_helpers::records::construct::new_record; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -55,7 +57,7 @@ fn position<'py>( return Ok(explicit.clone()); } let py: Python<'py> = node.py(); - let value: Bound<'py, PyAny> = match node.getattr(attribute) { + let value: Bound<'py, PyAny> = match native_field(node, attribute) { Ok(value) => value, Err(error) if error.is_instance_of::(py) => { DEFAULT_POSITION.into_pyobject(py)?.into_any() @@ -152,7 +154,6 @@ fn escapes(path: &Bound<'_, PyAny>) -> PyResult { fn project_tree_paths<'py>(root: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = root.py(); let root: PathBuf = root.extract()?; - let class: Bound<'py, PyAny> = registry::class(py, RecordId::ProjectPath)?; let mut paths: Vec<(Bound<'py, PyAny>, bool)> = project_tree_entries(&root) .into_iter() .map(|ProjectTreeEntry { relative, is_file }| { @@ -179,7 +180,7 @@ fn project_tree_paths<'py>(root: &Bound<'py, PyAny>) -> PyResult>()?; PyTuple::new(py, nodes) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/rule_cache.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/rule_cache.rs index a5515d7d8e..ac9dcc3ecf 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/rule_cache.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/rule_cache.rs @@ -222,8 +222,7 @@ fn lookup_sql_rule_cache<'py>( } fn finding_payload(finding: &Bound<'_, PyAny>) -> PyResult { - let path: String = finding - .getattr("path")? + let path: String = native_field(finding, "path")? .call_method0("as_posix")? .extract()?; Ok(json!({ diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/rule_dispatch.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/rule_dispatch.rs index 73ef523fab..aa7b557440 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/rule_dispatch.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/rule_dispatch.rs @@ -18,14 +18,16 @@ use crate::bindings::_helpers::cache::native_store::current_code_identity; use crate::bindings::_helpers::compile_driver::python_calls::{ KeywordArguments, call_with, keywords, }; +use crate::bindings::_helpers::compile_driver::stage_work::count_stage; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::native_host::user_code::UserCodeHost; +use crate::bindings::_helpers::records::construct::new_record_with; use crate::bindings::_helpers::records::fields::field; -use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::rules::catalogue::{ resolved_ignore_selectors, rule_catalogue, select_rules, }; +use crate::bindings::_helpers::rules::constants::RULES_MEMO_REUSES; use crate::bindings::_helpers::rules::constants::TYPE_PROOF_RULE_CODES; use crate::bindings::_helpers::rules::custom_evaluation::{ CustomRun, evaluate_custom_rules_cached, outcome, @@ -33,6 +35,9 @@ use crate::bindings::_helpers::rules::custom_evaluation::{ use crate::bindings::_helpers::rules::custom_hosts::HostCancel; use crate::bindings::_helpers::rules::custom_rule_evidence::verify_custom_rules; use crate::bindings::_helpers::rules::rows::finalize_rule_findings_rows; +use crate::bindings::_helpers::rules::rows::{ + NativeRulesRequest, build_rules_request, evaluate_rules_request, +}; use crate::bindings::_helpers::rules::rule_plan::plan_rule_run; use crate::bindings::_helpers::rules::rule_run::finish_rule_run; use crate::bindings::_helpers::rules::selection::{rule_selected, selected_rule_codes}; @@ -308,7 +313,7 @@ fn evaluate_native<'py>( custom: CustomSource, ) -> PyResult> { let py: Python<'py> = run.project.py(); - let prepared: PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> = + let prepared: PyResult<(Bound<'py, PyAny>, Bound<'py, NativeRulesRequest>)> = native_request(run, custom_payloads); let custom: Option = match (custom, &prepared) { (CustomSource::Started(handle), Ok(_)) => handle, @@ -321,9 +326,8 @@ fn evaluate_native<'py>( (CustomSource::Deferred, Ok((payloads, _))) => start_custom_rules(run, payloads)?, (CustomSource::Deferred, Err(_)) => None, }; - let (_, request): (Bound<'py, PyAny>, Bound<'py, PyAny>) = prepared?; - let evaluated: PyResult> = native(py, NativeAttr::EvaluateRulesRequest) - .and_then(|evaluate| evaluate.call1((request,))); + let (_, request): (Bound<'py, PyAny>, Bound<'py, NativeRulesRequest>) = prepared?; + let evaluated: PyResult> = evaluate_rules_request(py, &request); cancel_on_interrupt(py, &evaluated, &run.cancel); let custom: PyResult> = match custom { Some(handle) => joined(py, handle, &run.cancel), @@ -347,12 +351,16 @@ fn evaluate_native<'py>( .into_any() }; let reused: bool = field(&native_result, "reused")?.is_truthy()?; + if reused { + count_stage(py, RULES_MEMO_REUSES, 1)?; + } let native_hits: i64 = field(&native_result, "cache_hits")?.extract()?; let native_misses: i64 = field(&native_result, "cache_misses")?.extract()?; let custom_hits: i64 = field(&custom, "cache_hits")?.extract()?; let custom_misses: i64 = field(&custom, "cache_misses")?.extract()?; - call_with( - ®istry::class(py, RecordId::RulesResult)?, + new_record_with( + py, + RecordId::RulesResult, &[ ("findings", findings), ( @@ -390,7 +398,7 @@ fn evaluate_native<'py>( fn native_request<'py>( run: &NativeRun<'py>, custom_payloads: Option>, -) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> { +) -> PyResult<(Bound<'py, PyAny>, Bound<'py, NativeRulesRequest>)> { let py: Python<'py> = run.project.py(); let payloads: Bound<'py, PyAny> = match custom_payloads { Some(payloads) => payloads, @@ -406,25 +414,28 @@ fn native_request<'py>( for code in TYPE_PROOF_RULE_CODES { include_type_proof = include_type_proof || rule_selected(&run.config, code)?; } - let request: Bound<'py, PyAny> = call_with( - &native(py, NativeAttr::BuildRulesRequest)?, - &[ - ("project", run.project.clone()), - ("config", run.config.clone()), - ("project_dir", run.project_dir.clone()), - ("dialect", run.dialect.clone().into_bound_py_any(py)?), - ( - "initial_findings", - run.initial_findings - .clone() - .unwrap_or_else(|| PyTuple::empty(py).into_any()), - ), - ("custom_payloads", payloads.clone()), - ( - "include_type_proof", - include_type_proof.into_bound_py_any(py)?, - ), - ], + let request: Bound<'py, NativeRulesRequest> = Bound::new( + py, + build_rules_request(Some(&keywords( + py, + &[ + ("project", run.project.clone()), + ("config", run.config.clone()), + ("project_dir", run.project_dir.clone()), + ("dialect", run.dialect.clone().into_bound_py_any(py)?), + ( + "initial_findings", + run.initial_findings + .clone() + .unwrap_or_else(|| PyTuple::empty(py).into_any()), + ), + ("custom_payloads", payloads.clone()), + ( + "include_type_proof", + include_type_proof.into_bound_py_any(py)?, + ), + ], + )?))?, )?; Ok((payloads, request)) } @@ -537,10 +548,6 @@ fn cancel_on_interrupt(py: Python<'_>, result: &PyResult>, canc } } -fn native<'py>(py: Python<'py>, name: NativeAttr) -> PyResult> { - registry::native(py, name) -} - pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> { module.add_function(wrap_pyfunction!(evaluate_project_rules, module)?)?; module.add_function(wrap_pyfunction!(evaluate_native_rules, module)?)?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/rule_plan.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/rule_plan.rs index 126272ba37..60127a6820 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/rule_plan.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/rule_plan.rs @@ -8,11 +8,13 @@ use std::path::Path; use crate::bindings::_helpers::boundary::keyword_request::Request; use crate::bindings::_helpers::compile_driver::python_calls::call_with; +use crate::bindings::_helpers::compile_driver::python_calls::keywords; +use crate::bindings::_helpers::compiled_project::records::project_with; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::native_host::user_code::UserCodeHost; +use crate::bindings::_helpers::records::construct::new_record_with; use crate::bindings::_helpers::records::fields::{field, replaced}; -use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::rules::catalogue::{ references_custom_rules, resolved_ignore_selectors, rule_catalogue, select_rules, verify_custom_rules, @@ -97,8 +99,9 @@ pub(crate) fn plan_rule_run<'py>( ("ignore", PyTuple::empty(py).into_any()), ], )?; - call_with( - ®istry::class(py, RecordId::RuleRunPlan)?, + new_record_with( + py, + RecordId::RuleRunPlan, &[ ("effective_config", effective_config), ("project_dir", project_dir), @@ -156,12 +159,12 @@ fn selected_models_project<'py>( models.push(entry.model); } } - call_with( - ®istry::native(py, NativeAttr::ProjectWith)?, - &[ - ("project", project.clone()), - ("models", PyTuple::new(py, models)?.into_any()), - ], + project_with( + project, + Some(keywords( + py, + &[("models", PyTuple::new(py, models)?.into_any())], + )?), ) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/rule_run.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/rule_run.rs index 3b3961fc8e..5f53eaf5ca 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/rule_run.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/rule_run.rs @@ -12,11 +12,11 @@ use sqlbuild_rules::rule_results::main::group_unevaluated::group_unevaluated; use sqlbuild_rules::rule_results::models::RuleFinding; use crate::bindings::_helpers::boundary::panics::compiler_guard; -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; +use crate::bindings::_helpers::compile_driver::python_calls::imported; +use crate::bindings::_helpers::records::construct::new_record_with; use crate::bindings::_helpers::records::fields::field as record_field; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::rules::fact_replay::project_models; use crate::bindings::_helpers::rules::results::rule_finding; use crate::bindings::_helpers::rules::rows::finalize_rule_findings_rows; @@ -29,8 +29,9 @@ fn integer(object: &Bound<'_, PyAny>, field: &str) -> PyResult { } fn python_finding<'py>(py: Python<'py>, finding: RuleFinding) -> PyResult> { - call_with( - ®istry::class(py, RecordId::Finding)?, + new_record_with( + py, + RecordId::Finding, &[ ("code", finding.code.into_bound_py_any(py)?), ( @@ -97,8 +98,9 @@ pub(crate) fn finish_rule_run<'py>( let total = |field: &str| -> PyResult> { (integer(result, field)? + integer(sql_result, field)?).into_bound_py_any(py) }; - call_with( - ®istry::class(py, RecordId::RulesRunResult)?, + new_record_with( + py, + RecordId::RulesRunResult, &[ ("findings", PyTuple::new(py, findings)?.into_any()), ( @@ -141,7 +143,7 @@ pub(crate) fn run_rules<'py>( /// One bulk Rules cache file owned by the native compiler engine. #[pyfunction] #[pyo3(signature = (*, project_dir, file_name))] -fn rules_bulk_cache_path(project_dir: PathBuf, file_name: &str) -> PathBuf { +pub(crate) fn rules_bulk_cache_path(project_dir: PathBuf, file_name: &str) -> PathBuf { bulk_cache_path(&project_dir, file_name) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/rules_config.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/rules_config.rs index 3b8c057520..784f7ef243 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/rules_config.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/rules_config.rs @@ -13,6 +13,7 @@ use sqlbuild_rules::configuration::main::load_rules_config::load_rules_config as use sqlbuild_rules::models::RulesConfig; use crate::bindings::_helpers::boundary::panics::compiler_guard; +use crate::bindings::_helpers::records::construct::new_record; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -30,7 +31,7 @@ impl<'py> Models<'py> { for (name, value) in fields { arguments.set_item(name, value)?; } - registry::class(self.py, class)?.call((), Some(&arguments)) + new_record(self.py, class, (), Some(&arguments)) } } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/sql_rules.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/sql_rules.rs index 0d996c3266..1d5f4dafbd 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/sql_rules.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/sql_rules.rs @@ -18,10 +18,11 @@ use sqlbuild_rules::rule_results::models::RuleFinding; use crate::bindings::_helpers::analysis::model_reads::output_proven; use crate::bindings::_helpers::boundary::keyword_request::Request; -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported, keywords}; +use crate::bindings::_helpers::compile_driver::python_calls::{imported, keywords}; use crate::bindings::_helpers::compiled_project::compiled_model_view::{ NativeCompiledModel, SqlRuleInputs, SqlRuleKey, }; +use crate::bindings::_helpers::records::construct::new_record_with; use crate::bindings::_helpers::records::fields::field; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; @@ -156,8 +157,9 @@ pub(crate) fn run_planned_sql_rules<'py>( let (findings, hits, misses) = evaluated_sql_rules(&run, (&lint_key, identities), project_files)?; let sql_ms: i64 = (started.elapsed().as_secs_f64() * 1000.0).round_ties_even() as i64; - call_with( - ®istry::class(py, RecordId::SqlRulesEvaluation)?, + new_record_with( + py, + RecordId::SqlRulesEvaluation, &[ ("findings", finding_objects(py, findings)?.into_any()), ("cache_hits", hits.into_bound_py_any(py)?), @@ -169,13 +171,13 @@ pub(crate) fn run_planned_sql_rules<'py>( /// Finding rows as `Finding` records. fn finding_objects(py: Python<'_>, findings: Vec) -> PyResult> { - let class: Bound<'_, PyAny> = registry::class(py, RecordId::Finding)?; let path_class: Bound<'_, PyAny> = imported(py, PATHLIB_MODULE, "Path")?; let objects: Vec> = findings .into_iter() .map(|finding| { - call_with( - &class, + new_record_with( + py, + RecordId::Finding, &[ ("code", PyString::new(py, &finding.code).into_any()), ("path", path_class.call1((finding.path,))?), @@ -266,8 +268,7 @@ fn model_posix_path(model: &Bound<'_, PyAny>) -> PyResult { if let Ok(view) = model.downcast::() { return Ok(view.get().sql_rule_inputs(model.py(), false)?.posix_path); } - model - .getattr("relative_path")? + native_field(model, "relative_path")? .call_method0("as_posix")? .extract() } @@ -275,9 +276,7 @@ fn model_posix_path(model: &Bound<'_, PyAny>) -> PyResult { /// Whether the model's config values set `sql_analysis` to `False`. fn python_excluded(model: &Bound<'_, PyAny>) -> PyResult { let excluded: Bound<'_, PyAny> = false.into_bound_py_any(model.py())?; - Ok(model - .getattr("config")? - .getattr("values")? + Ok(native_field(&native_field(model, "config")?, "values")? .call_method1("get", ("sql_analysis",))? .is(&excluded)) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/sql_test_objects.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/sql_test_objects.rs index d81d234335..963703505b 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/sql_test_objects.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/sql_test_objects.rs @@ -13,9 +13,9 @@ use sqlbuild_rules::models::{ use crate::bindings::_helpers::boundary::panics::value_error; use crate::bindings::_helpers::compiled_project::records::plain_typed_value; use crate::bindings::_helpers::compiled_project::values::plain_value; +use crate::bindings::_helpers::records::construct::record_class; use crate::bindings::_helpers::records::fields::field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; const DESCRIPTION_KEY: &str = "description"; @@ -88,7 +88,7 @@ fn tested_resources(test: &Bound<'_, PyAny>) -> PyResult> { fn with_payload(test: &Bound<'_, PyAny>, mut fact: SqlTestFact) -> PyResult { let payload: Bound<'_, PyAny> = field(test, "payload")?; let model_payload: Bound<'_, PyAny> = - registry::class(test.py(), RecordId::CompiledModelSqlTestPayload)?; + record_class(test.py(), RecordId::CompiledModelSqlTestPayload)?; if !payload.is_instance(&model_payload)? { return Ok(fact); } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/scenario_command/capture_command.rs b/crates/sqlbuild-python/src/bindings/_helpers/scenario_command/capture_command.rs index 598976dc7e..6cc919eaf7 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/scenario_command/capture_command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/scenario_command/capture_command.rs @@ -37,7 +37,10 @@ fn count(label: &str, value: usize) -> SummaryCount { /// Run one `sqb scenario capture` request. #[pyfunction] #[pyo3(signature = (*, request))] -fn run_scenario_capture_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { +pub(crate) fn run_scenario_capture_command( + py: Python<'_>, + request: &Bound<'_, PyAny>, +) -> PyResult { let no_sql_validation: Bound<'_, PyAny> = native_field(request, "no_sql_validation")?; if let Some(error) = sql_analysis_required_error( SqlAnalysisCommand::Capture, @@ -56,10 +59,8 @@ fn run_scenario_capture_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> P let stream: Bound<'_, PyAny> = py.import("sys")?.getattr("stdout")?; let planning: Bound<'_, PyAny> = write_headers(&stream, &project, (EXECUTION_COMMAND, 1, style))?; - let force: bool = request - .getattr("limit_inputs")? - .getattr("force")? - .is_truthy()?; + let force: bool = + native_field(&native_field(request, "limit_inputs")?, "force")?.is_truthy()?; write(&stream, &format!("{}\n\n", snapshot_capture_warning(force)))?; flush(&stream)?; let pipeline: Bound<'_, PyAny> = compile( @@ -117,10 +118,10 @@ fn run_scenario_capture_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> P ("adapter", adapter.clone()), ( "project_name", - project - .discovered - .getattr("project_config")? - .getattr("name")?, + native_field( + &native_field(&project.discovered, "project_config")?, + "name", + )?, ), ("settings", settings), ("connection_hooks", hooks), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/scenario_command/conversions.rs b/crates/sqlbuild-python/src/bindings/_helpers/scenario_command/conversions.rs index 6c506050f9..005ee770c3 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/scenario_command/conversions.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/scenario_command/conversions.rs @@ -18,9 +18,9 @@ use sqlbuild_core::scenario_command::models::{ use crate::bindings::_helpers::compile_driver::python_calls::call_with; use crate::bindings::_helpers::native_host::adapter::AdapterHost; use crate::bindings::_helpers::native_host::host; +use crate::bindings::_helpers::records::construct::enum_from_value; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; const SUCCESS_STATUS: &str = "success"; const FAILED_STATUS: &str = "failed"; @@ -93,13 +93,16 @@ fn assertion_row(assertion: &Bound<'_, PyAny>) -> PyResult { fn retained_relations(relation_map: &Bound<'_, PyAny>) -> PyResult> { let py: Python<'_> = relation_map.py(); - let kinds: Bound<'_, PyAny> = registry::class(py, RecordId::ScenarioArtifactKind)?; items(&native_field(relation_map, "artifacts")?)? .iter() .map(|artifact| { let identity: Bound<'_, PyAny> = native_field(artifact, "identity")?; Ok(RetainedRelation { - kind: enum_value(&kinds.call1((native_field(&identity, "kind")?,))?)?, + kind: enum_value(&enum_from_value( + py, + RecordId::ScenarioArtifactKind, + native_field(&identity, "kind")?, + )?)?, logical_name: native_field(&identity, "logical_name")?.extract()?, physical_name: native_field(artifact, "physical_name")?.extract()?, }) @@ -299,7 +302,7 @@ pub(crate) fn write_warehouse_runtime_target( .iter() .map(|value| { Ok(NamedStatements { - name: value.getattr(field)?.extract()?, + name: native_field(value, field)?.extract()?, statements: sql_statements(value)?, }) }) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/scenario_command/presentation.rs b/crates/sqlbuild-python/src/bindings/_helpers/scenario_command/presentation.rs index 99217f2c63..1d40c492fb 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/scenario_command/presentation.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/scenario_command/presentation.rs @@ -13,12 +13,12 @@ use sqlbuild_core::scenario_command::main::scenario_activity_message::scenario_a use sqlbuild_core::scenario_command::models::ScenarioNamespace; use crate::bindings::_helpers::build_progress::transient_status::status_reporter; +use crate::bindings::_helpers::cli::progress_reporters::ConnectionProgressReporter; use crate::bindings::_helpers::compile_driver::python_calls::call_with; +use crate::bindings::_helpers::compile_driver::python_calls::keywords; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::NativeAttr; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::scenario_command::conversions::{ capture_row, scenario_result_row, write_local_runtime_target, write_warehouse_runtime_target, }; @@ -161,15 +161,22 @@ pub(crate) fn connection_hooks<'py>( (adapter_name, style): (&str, CliStyle), ) -> PyResult> { let py: Python<'py> = stream.py(); - let reporter: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ConnectionProgressReporter)?, - &[ - ("adapter_name", text(py, adapter_name)), - ("blank_line_after_complete", boolean(py, true)), - ("stream", stream.clone()), - ("use_color", boolean(py, style.use_color)), - ], - )?; + let reporter: Bound<'py, PyAny> = Bound::new( + py, + ConnectionProgressReporter::new( + py, + Some(keywords( + py, + &[ + ("adapter_name", text(py, adapter_name)), + ("blank_line_after_complete", boolean(py, true)), + ("stream", stream.clone()), + ("use_color", boolean(py, style.use_color)), + ], + )?), + )?, + )? + .into_any(); let relay: Bound<'py, ScenarioConnectionRelay> = Bound::new( py, ScenarioConnectionRelay { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/scenario_command/setup.rs b/crates/sqlbuild-python/src/bindings/_helpers/scenario_command/setup.rs index 3f5f1bb1fb..c5e47f3d11 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/scenario_command/setup.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/scenario_command/setup.rs @@ -20,16 +20,26 @@ use sqlbuild_core::scenario_command::models::{ SnapshotLimitInputs, SnapshotLimits, SqlAnalysisCommand, }; +use crate::bindings::_helpers::cli::progress_reporters::PlanningProgressReporter; +use crate::bindings::_helpers::cli::progress_text::write_execution_header; use crate::bindings::_helpers::command_runtime::adapters::resolve_cli_adapter; +use crate::bindings::_helpers::command_runtime::connection::resolve_project_connection_config; use crate::bindings::_helpers::compile_driver::discovery::discover_project_inputs; +use crate::bindings::_helpers::compile_driver::pipeline_entries::run_compile_only_pipeline; use crate::bindings::_helpers::compile_driver::python_calls::keywords; use crate::bindings::_helpers::compile_driver::python_calls::{call_with, within}; +use crate::bindings::_helpers::contracts::spec_resolution::resolve_effective_adapter_name; +use crate::bindings::_helpers::contracts::spec_resolution::resolve_effective_scenario_config; +use crate::bindings::_helpers::dbt_interop::reference_resolver::resolve_external_sql_reference_resolver; use crate::bindings::_helpers::discovery::guidance::sql_analysis_guidance; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; +use crate::bindings::_helpers::planner_selection::unit_tests::reject_unit_test_selectors; use crate::bindings::_helpers::presentation_text::terminal::stdout_supports_color; +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::{ErrorId, NativeAttr, RecordId}; +use crate::bindings::_helpers::records::fields::replaced_keywords; +use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; const METADATA_MODULE: &str = "importlib.metadata"; @@ -87,10 +97,7 @@ pub(crate) struct ScenarioProject<'py> { } fn config_namespace(config: &Bound<'_, PyAny>) -> PyResult> { - config - .getattr("scenario")? - .getattr("run_namespace")? - .extract() + native_field(&native_field(config, "scenario")?, "run_namespace")?.extract() } /// `dataclasses.replace(value, **{name: replacement})`. @@ -100,11 +107,7 @@ fn replaced<'py>( ) -> PyResult> { let changes: Bound<'py, PyDict> = PyDict::new(value.py()); changes.set_item(name, replacement)?; - value - .py() - .import("dataclasses")? - .getattr("replace")? - .call((value,), Some(&changes)) + replaced_keywords(value, &changes) } /// Discover the project, reject unit-test selectors and apply the effective namespace. @@ -122,14 +125,7 @@ pub(crate) fn discover_project<'py>( )?; let selectors: Bound<'py, PyAny> = native_field(request, "selectors")?; let exclude: Bound<'py, PyAny> = native_field(request, "exclude")?; - call_with( - ®istry::native(py, NativeAttr::RejectUnitTestSelectors)?, - &[ - ("select", selectors.clone()), - ("exclude", exclude.clone()), - ("discovered_inputs", discovered.clone()), - ], - )?; + reject_unit_test_selectors(&selectors.clone(), &exclude.clone(), &discovered.clone())?; let local_config: Bound<'py, PyAny> = native_field(&discovered, "local_config")?; let candidates: NamespaceCandidates = NamespaceCandidates { cli: native_field(request, "scenario_namespace")?.extract()?, @@ -187,19 +183,9 @@ pub(crate) fn require_sql_analysis( /// The adapter named by the project's effective configuration. pub(crate) fn project_adapter_name(project: &ScenarioProject<'_>) -> PyResult { - let py: Python<'_> = project.discovered.py(); - call_with( - ®istry::native(py, NativeAttr::ResolveEffectiveAdapterName)?, - &[ - ( - "project_config", - native_field(&project.discovered, "project_config")?, - ), - ( - "local_config", - native_field(&project.discovered, "local_config")?, - ), - ], + resolve_effective_adapter_name( + &native_field(&project.discovered, "project_config")?, + &native_field(&project.discovered, "local_config")?, )? .extract() } @@ -226,12 +212,15 @@ pub(crate) fn project_connection_config<'py>( project: &ScenarioProject<'py>, ) -> PyResult> { let py: Python<'py> = project.discovered.py(); - call_with( - ®istry::native(py, NativeAttr::ResolveProjectConnectionConfig)?, - &[ - ("discovered_inputs", project.discovered.clone()), - ("project_dir", project.project_dir.clone()), - ], + resolve_project_connection_config( + py, + Some(keywords( + py, + &[ + ("discovered_inputs", project.discovered.clone()), + ("project_dir", project.project_dir.clone()), + ], + )?), ) } @@ -251,24 +240,25 @@ pub(crate) fn write_headers<'py>( let py: Python<'py> = stream.py(); write(stream, &render_namespace_header(&project.namespace))?; let selectors: Vec = project.selectors.extract()?; - let planning: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::PlanningProgressReporter)?, - &[ - ("stream", stream.clone()), - ("use_color", boolean(py, style.use_color)), - ], - )?; + let planning: Bound<'py, PyAny> = Bound::new( + py, + PlanningProgressReporter::new(py, stream, style.use_color)?, + )? + .into_any(); write(stream, "\n")?; let target: Option = (!selectors.is_empty()).then(|| selectors.join(" ")); - call_with( - ®istry::native(py, NativeAttr::WriteExecutionHeader)?, - &[ - ("stream", stream.clone()), - ("command", text(py, command)), - ("target", optional_text(py, target.as_deref())), - ("concurrency", int(py, concurrency)), - ("use_color", boolean(py, style.use_color)), - ], + write_execution_header( + py, + Some(keywords( + py, + &[ + ("stream", stream.clone()), + ("command", text(py, command)), + ("target", optional_text(py, target.as_deref())), + ("concurrency", int(py, concurrency)), + ("use_color", boolean(py, style.use_color)), + ], + )?), )?; Ok(planning) } @@ -291,19 +281,13 @@ pub(crate) fn compile<'py>( planning, } = inputs; let py: Python<'py> = project.discovered.py(); - let resolver: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ResolveExternalSqlReferenceResolver)?, - &[ - ("project_dir", project.project_dir.clone()), - ("discovered_inputs", project.discovered.clone()), - ], + let resolver: Bound<'py, PyAny> = resolve_external_sql_reference_resolver( + project.project_dir.clone(), + project.discovered.clone(), )?; let mut pairs: Vec<(&str, Bound<'py, PyAny>)> = options.to_vec(); pairs.push(("external_sql_reference_resolver", resolver)); - let options: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::CompilePipelineOptions)?, - &pairs, - )?; + let options: Bound<'py, PyAny> = new_record_with(py, RecordId::CompilePipelineOptions, &pairs)?; let lifecycle: Bound<'py, PyAny> = call_with( &host::entry(py, ExecutorHost::OperationLifecycle)?, &[ @@ -312,14 +296,17 @@ pub(crate) fn compile<'py>( ], )?; within(&lifecycle, |_| { - call_with( - ®istry::native(py, NativeAttr::RunCompileOnlyPipeline)?, - &[ - ("discovered_inputs", project.discovered.clone()), - ("adapter", adapter.clone()), - ("options", options.clone()), - ("on_progress", native_field(planning, "on_progress")?), - ], + run_compile_only_pipeline( + py, + Some(keywords( + py, + &[ + ("discovered_inputs", project.discovered.clone()), + ("adapter", adapter.clone()), + ("options", options.clone()), + ("on_progress", native_field(planning, "on_progress")?), + ], + )?), ) }) } @@ -330,11 +317,10 @@ pub(crate) fn selected_scenarios<'py>( (selectors, exclude, project_dir): (&[String], &[String], &Bound<'py, PyAny>), ) -> PyResult> { let py: Python<'py> = pipeline.py(); - let compiled: Vec> = pipeline - .getattr("project")? - .getattr("sql_scenarios")? - .try_iter()? - .collect::>()?; + let compiled: Vec> = + native_field(&native_field(pipeline, "project")?, "sql_scenarios")? + .try_iter()? + .collect::>()?; let mut files: Vec = Vec::with_capacity(compiled.len()); for scenario in &compiled { let file: Bound<'py, PyAny> = native_field(scenario, "scenario_file")?; @@ -358,7 +344,7 @@ pub(crate) fn selected_scenarios<'py>( } fn optional_int(value: &Bound<'_, PyAny>, name: &str) -> PyResult> { - value.getattr(name)?.extract() + native_field(value, name)?.extract() } /// The snapshot capture limits from the request, the scenario config and the defaults. @@ -368,18 +354,9 @@ pub(crate) fn capture_limits<'py>( ) -> PyResult> { let py: Python<'py> = request.py(); let inputs: Bound<'py, PyAny> = native_field(request, "limit_inputs")?; - let configured: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ResolveEffectiveScenarioConfig)?, - &[ - ( - "project_config", - native_field(&project.discovered, "project_config")?, - ), - ( - "local_config", - native_field(&project.discovered, "local_config")?, - ), - ], + let configured: Bound<'py, PyAny> = resolve_effective_scenario_config( + &native_field(&project.discovered, "project_config")?, + &native_field(&project.discovered, "local_config")?, )? .getattr("snapshot_limits")?; let limits: SnapshotLimits = resolve_snapshot_limits( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/scenario_command/snapshot_sync.rs b/crates/sqlbuild-python/src/bindings/_helpers/scenario_command/snapshot_sync.rs index a4479105cd..69f78e3f35 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/scenario_command/snapshot_sync.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/scenario_command/snapshot_sync.rs @@ -1,18 +1,20 @@ //! `sqb scenario test --local --sync-snapshots` and `--refresh`: capture stale snapshots first. use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; -use pyo3::types::{PyTuple, PyTupleMethods}; +use pyo3::types::{PyString, PyTuple, PyTupleMethods}; use sqlbuild_core::cli_text::main::format_summary_footer::format_summary_footer; use sqlbuild_core::cli_text::models::SummaryCount; use sqlbuild_core::scenario_command::main::snapshot_capture_warning::snapshot_capture_warning; use sqlbuild_core::scenario_command::models::ScenarioNamespace; +use crate::bindings::_helpers::cli::progress_reporters::PlanningProgressReporter; use crate::bindings::_helpers::compile_driver::python_calls::call_with; +use crate::bindings::_helpers::compile_driver::python_calls::keywords; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::NativeAttr; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::result_output::documents::format_scenario_snapshot_execution_json; +use crate::bindings::_helpers::result_output::documents::write_execution_json_output; use crate::bindings::_helpers::scenario_command::conversions::succeeded; use crate::bindings::_helpers::scenario_command::presentation::{ CompletionKind, SectionStatus, begin_section, connection_hooks, progress_relay, @@ -38,28 +40,30 @@ impl LocalSync<'_> { namespace: &ScenarioNamespace, ) -> PyResult { let py: Python<'_> = request.py(); - let payload: Bound<'_, PyAny> = call_with( - ®istry::native(py, NativeAttr::FormatScenarioSnapshotExecutionJson)?, - &[ - ( - "run_namespace", - optional_text(py, namespace.value.as_deref()), - ), - ("namespace_source", text(py, &namespace.source)), - ("results", self.results.clone().into_any()), - ("refresh", boolean(py, self.refresh)), - ], - )?; - call_with( - ®istry::native(py, NativeAttr::WriteExecutionJsonOutput)?, - &[ - ("payload", payload), - ("json_output", native_field(request, "json_output")?), - ( - "json_output_path", - native_field(request, "json_output_path")?, - ), - ], + let payload: Bound<'_, PyAny> = PyString::new( + py, + &format_scenario_snapshot_execution_json( + py, + Some(keywords( + py, + &[ + ( + "run_namespace", + optional_text(py, namespace.value.as_deref()), + ), + ("namespace_source", text(py, &namespace.source)), + ("results", self.results.clone().into_any()), + ("refresh", boolean(py, self.refresh)), + ], + )?), + )?, + ) + .into_any(); + write_execution_json_output( + py, + payload, + native_field(request, "json_output")?.extract()?, + native_field(request, "json_output_path")?, )?; Ok(self.exit_code) } @@ -81,11 +85,10 @@ pub(crate) fn sync_local_snapshots<'py>( let py: Python<'py> = request.py(); let refresh: bool = native_field(request, "refresh")?.is_truthy()?; let project_connection: Bound<'py, PyAny> = project_connection_config(&run.project)?; - let project_name: Bound<'py, PyAny> = run - .project - .discovered - .getattr("project_config")? - .getattr("name")?; + let project_name: Bound<'py, PyAny> = native_field( + &native_field(&run.project.discovered, "project_config")?, + "name", + )?; let names: Vec = call_with( &host::entry(py, ExecutorHost::SelectScenarioSnapshotCaptureCandidates)?, &[ @@ -113,13 +116,11 @@ pub(crate) fn sync_local_snapshots<'py>( refresh, }); } - let planning: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::PlanningProgressReporter)?, - &[ - ("stream", run.stream.clone()), - ("use_color", boolean(py, run.style.use_color)), - ], - )?; + let planning: Bound<'py, PyAny> = Bound::new( + py, + PlanningProgressReporter::new(py, &run.stream, run.style.use_color)?, + )? + .into_any(); let compiled: PyResult> = compile( &run.project, CompileInputs { @@ -140,10 +141,8 @@ pub(crate) fn sync_local_snapshots<'py>( finished?; let scenarios: Bound<'py, PyTuple> = selected_scenarios(&pipeline, (&names, &[], &run.project.project_dir))?; - let force: bool = request - .getattr("limit_inputs")? - .getattr("force")? - .is_truthy()?; + let force: bool = + native_field(&native_field(request, "limit_inputs")?, "force")?.is_truthy()?; let section: SectionStatus<'py> = begin_section( &run.stream, ( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/scenario_command/test_command.rs b/crates/sqlbuild-python/src/bindings/_helpers/scenario_command/test_command.rs index 624d65d4bd..ab9aa1935a 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/scenario_command/test_command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/scenario_command/test_command.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; use std::time::Instant; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; -use pyo3::types::{PyDict, PyDictMethods, PyTuple, PyTupleMethods}; +use pyo3::types::{PyDict, PyDictMethods, PyString, PyTuple, PyTupleMethods}; use pyo3::{pyfunction, wrap_pyfunction}; use sqlbuild_core::cli_text::main::format_summary_footer::format_summary_footer; use sqlbuild_core::cli_text::models::{CliStyle, SummaryCount}; @@ -13,11 +13,14 @@ use sqlbuild_core::scenario_command::models::{ ScenarioNamespace, ScenarioTestFlags, SqlAnalysisCommand, }; +use crate::bindings::_helpers::compile_attachment::effective_config::build_effective_settings_config; use crate::bindings::_helpers::compile_driver::python_calls::call_with; +use crate::bindings::_helpers::compile_driver::python_calls::keywords; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; -use crate::bindings::_helpers::records::ids::NativeAttr; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::fields::field as native_field; +use crate::bindings::_helpers::result_output::documents::format_scenario_execution_json; +use crate::bindings::_helpers::result_output::documents::write_execution_json_output; use crate::bindings::_helpers::scenario_command::conversions::succeeded; use crate::bindings::_helpers::scenario_command::presentation::{ CompletionKind, SectionStatus, activity, begin_section, connection_hooks, progress_relay, @@ -50,7 +53,7 @@ pub(crate) struct TestRun<'py> { } fn flag(request: &Bound<'_, PyAny>, name: &str) -> PyResult { - request.getattr(name)?.is_truthy() + native_field(request, name)?.is_truthy() } fn prepare<'py>(py: Python<'py>, request: &Bound<'py, PyAny>) -> PyResult> { @@ -95,11 +98,10 @@ fn prepare<'py>(py: Python<'py>, request: &Bound<'py, PyAny>) -> PyResult = request.getattr("concurrency")?; let concurrency: i64 = if requested.is_none() { - call_with( - ®istry::native(py, NativeAttr::BuildEffectiveSettingsConfig)?, - &[("discovered_inputs", project.discovered.clone())], + native_field( + &build_effective_settings_config(&project.discovered.clone())?, + "concurrency", )? - .getattr("concurrency")? .extract()? } else { requested.extract()? @@ -145,29 +147,34 @@ fn finish( flush(&run.stream)?; write_completion(&run.stream, (&run.project.namespace, succeeded))?; let namespace: &ScenarioNamespace = &run.project.namespace; - let payload: Bound<'_, PyAny> = call_with( - ®istry::native(py, NativeAttr::FormatScenarioExecutionJson)?, - &[ - ("results", results.clone().into_any()), - ("local", boolean(py, run.local)), - ( - "run_namespace", - optional_text(py, namespace.value.as_deref()), - ), - ("namespace_source", text(py, &namespace.source)), - ( - "duration_ms", - pyo3::types::PyFloat::new(py, elapsed_seconds * 1000.0).into_any(), - ), - ], - )?; - call_with( - ®istry::native(py, NativeAttr::WriteExecutionJsonOutput)?, - &[ - ("payload", payload), - ("json_output", request.getattr("json_output")?), - ("json_output_path", request.getattr("json_output_path")?), - ], + let payload: Bound<'_, PyAny> = PyString::new( + py, + &format_scenario_execution_json( + py, + Some(keywords( + py, + &[ + ("results", results.clone().into_any()), + ("local", boolean(py, run.local)), + ( + "run_namespace", + optional_text(py, namespace.value.as_deref()), + ), + ("namespace_source", text(py, &namespace.source)), + ( + "duration_ms", + pyo3::types::PyFloat::new(py, elapsed_seconds * 1000.0).into_any(), + ), + ], + )?), + )?, + ) + .into_any(); + write_execution_json_output( + py, + payload, + request.getattr("json_output")?.extract()?, + request.getattr("json_output_path")?, )?; Ok(()) } @@ -211,11 +218,10 @@ fn run_scenarios( run.style, ), )?; - let project_name: Bound<'_, PyAny> = run - .project - .discovered - .getattr("project_config")? - .getattr("name")?; + let project_name: Bound<'_, PyAny> = native_field( + &native_field(&run.project.discovered, "project_config")?, + "name", + )?; let mut arguments: Vec<(&str, Bound<'_, PyAny>)> = vec![ ("pipeline_result", pipeline.clone()), ("scenarios", scenarios.clone().into_any()), @@ -306,7 +312,10 @@ fn run_scenarios( /// Run one `sqb scenario test` request. #[pyfunction] #[pyo3(signature = (*, request))] -fn run_scenario_test_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { +pub(crate) fn run_scenario_test_command( + py: Python<'_>, + request: &Bound<'_, PyAny>, +) -> PyResult { let run: TestRun<'_> = prepare(py, request)?; let planning: Bound<'_, PyAny> = write_headers( &run.stream, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/scopes/compile_usages.rs b/crates/sqlbuild-python/src/bindings/_helpers/scopes/compile_usages.rs index 8858b1a1c9..053d61bc1b 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/scopes/compile_usages.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/scopes/compile_usages.rs @@ -18,9 +18,11 @@ use sqlbuild_scopes::scope_index::models::{ DeclarationKind, DeclaredScope, ResourceIdentity, ResourceKind, ScopeKind, }; +use crate::bindings::_helpers::compile_driver::diagnostic_collector::report_compile_diagnostic; use crate::bindings::_helpers::compiled_project::model_loop_named::scope_path_message; +use crate::bindings::_helpers::records::construct::new_record; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr, RecordId}; +use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; const INACCESSIBLE_DECLARATION_CODE: &str = "S006"; @@ -96,15 +98,13 @@ pub(crate) fn model_schema_scope_usages<'py>( } let mut records: Vec> = Vec::new(); let mut record_rows: Vec<(IdentityRow, RecordRow)> = Vec::new(); - let declarations = native_field(resolver, "lookup")?.getattr("declarations")?; + let declarations = native_field(&native_field(resolver, "lookup")?, "declarations")?; for item in declarations.call_method0("items")?.try_iter()? { let (identity, found): (Bound<'py, PyAny>, Bound<'py, PyAny>) = item?.extract()?; if !found.is_truthy()? || !native_field(&identity, "owner")?.is_none() || !SCHEMA_USAGE_KINDS.contains( - &identity - .getattr("kind")? - .getattr("value")? + &native_field(&native_field(&identity, "kind")?, "value")? .extract::()? .as_str(), ) @@ -115,9 +115,7 @@ pub(crate) fn model_schema_scope_usages<'py>( let recorded: Bound<'py, PyAny> = native_field(&record, "identity")?; record_rows.push(( ( - native_field(&recorded, "kind")? - .getattr("value")? - .extract()?, + native_field(&native_field(&recorded, "kind")?, "value")?.extract()?, native_field(&recorded, "name")?.extract()?, ), record_row(&record)?, @@ -150,13 +148,12 @@ pub(crate) fn model_schema_scope_usages<'py>( )); } report_inaccessible(py, diagnostics)?; - let usage_record = registry::class(py, RecordId::UsageRecord)?; let mut built: Vec> = Vec::with_capacity(usages.len()); for (consumer, used) in usages { let fields: Bound<'py, PyDict> = PyDict::new(py); fields.set_item("consumer", native_field(&records[consumer], "identity")?)?; fields.set_item("declaration", native_field(&records[used], "identity")?)?; - built.push(usage_record.call((), Some(&fields))?); + built.push(new_record(py, RecordId::UsageRecord, (), Some(&fields))?); } PyTuple::new(py, built) } @@ -167,20 +164,17 @@ fn record_row(record: &Bound<'_, PyAny>) -> PyResult { let owner: Bound<'_, PyAny> = native_field(&identity, "owner")?; Ok(( native_field(record, "path")?.extract()?, - native_field(record, "scope")?.getattr("value")?.extract()?, + native_field(&native_field(record, "scope")?, "value")?.extract()?, if owner.is_none() { None } else { Some(( - native_field(&owner, "kind")?.getattr("value")?.extract()?, + native_field(&native_field(&owner, "kind")?, "value")?.extract()?, native_field(&owner, "name")?.extract()?, )) }, native_field(record, "owning_path")?.extract()?, - record - .getattr("ownership_root")? - .getattr("path")? - .extract()?, + native_field(&native_field(record, "ownership_root")?, "path")?.extract()?, )) } @@ -189,8 +183,6 @@ fn report_inaccessible(py: Python<'_>, diagnostics: Vec) -> PyRes if diagnostics.is_empty() { return Ok(()); } - let report = registry::native(py, NativeAttr::ReportCompileDiagnostic)?; - let compiler_diagnostic = registry::class(py, RecordId::CompilerDiagnostic)?; let path_class = py.import("pathlib")?.getattr("Path")?; for (key, message, path, help) in diagnostics { let fields: Bound<'_, PyDict> = PyDict::new(py); @@ -200,10 +192,10 @@ fn report_inaccessible(py: Python<'_>, diagnostics: Vec) -> PyRes fields.set_item("message", message)?; fields.set_item("path", path_class.call1((path,))?)?; fields.set_item("help", help)?; - let arguments: Bound<'_, PyDict> = PyDict::new(py); - arguments.set_item("key", PyTuple::new(py, key)?)?; - arguments.set_item("diagnostic", compiler_diagnostic.call((), Some(&fields))?)?; - report.call((), Some(&arguments))?; + report_compile_diagnostic( + PyTuple::new(py, key)?.as_any(), + &new_record(py, RecordId::CompilerDiagnostic, (), Some(&fields))?, + )?; } Ok(()) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/scopes/declaration_contexts.rs b/crates/sqlbuild-python/src/bindings/_helpers/scopes/declaration_contexts.rs index 293558c237..8dd5605aa1 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/scopes/declaration_contexts.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/scopes/declaration_contexts.rs @@ -17,7 +17,7 @@ const ENUM_KIND: u8 = 0; const CONSTANT_KIND: u8 = 1; const MACRO_KIND: u8 = 2; -type TableRow = ( +pub(crate) type TableRow = ( Vec, HashMap>, HashMap>, @@ -26,7 +26,7 @@ type VisibleRow<'py> = (Bound<'py, PyAny>, VisibleEntry, Option, Py, Py, u8, Py, u8, u32); +pub(crate) struct PositionRow(Py, Py, Py, u8, Py, u8, u32); /// The Python classes and enum members the contexts are built from. #[derive(FromPyObject)] @@ -50,7 +50,7 @@ pub(crate) struct NativeDeclarationContexts { /// The Python lookup mappings a matching resource's private positions and grants come from. #[derive(FromPyObject)] #[pyo3(from_item_all)] -struct LookupMappings { +pub(crate) struct LookupMappings { identity_keys: Py, private_positions: Py, grants_by_resource: Py, @@ -69,7 +69,7 @@ struct ContextParts<'py> { #[pymethods] impl NativeDeclarationContexts { #[new] - fn new( + pub(crate) fn new( positions: Vec, table: TableRow, classes: ContextClasses, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/scopes/declaration_resolution.rs b/crates/sqlbuild-python/src/bindings/_helpers/scopes/declaration_resolution.rs index 96947690b0..7ad10099ab 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/scopes/declaration_resolution.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/scopes/declaration_resolution.rs @@ -11,6 +11,7 @@ use sqlbuild_scopes::scope_index::models::{ DeclarationIdentity, IdentityFailure, QualifiedIdentity, ResourceIdentity, ScopePathFailure, }; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::scopes::declaration_contexts::NativeDeclarationContexts; const PATH_CACHE_PREFIX: &str = ""; @@ -276,11 +277,11 @@ impl NativeDeclarationContexts { let py: Python<'py> = context.py(); let fields: Bound<'py, PyDict> = PyDict::new(py); for field in COPIED_FIELDS { - let mapping: Bound<'py, PyDict> = context.getattr(field)?.downcast_into()?; + let mapping: Bound<'py, PyDict> = native_field(context, field)?.downcast_into()?; fields.set_item(field, mapping.copy()?)?; } for field in VISIBILITY_FIELDS { - let mapping: Bound<'py, PyDict> = context.getattr(field)?.downcast_into()?; + let mapping: Bound<'py, PyDict> = native_field(context, field)?.downcast_into()?; fields.set_item(field, self.rebound_visibility(&mapping, consumer)?)?; } fields.set_item(intern!(py, "consumer"), consumer)?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/scopes/relationship_names.rs b/crates/sqlbuild-python/src/bindings/_helpers/scopes/relationship_names.rs index ecce43864f..e8652d8e54 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/scopes/relationship_names.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/scopes/relationship_names.rs @@ -13,6 +13,8 @@ use sqlbuild_scopes::relationship_names::models::{ use sqlbuild_sqltext::sql_scan::models::LexicalSyntax; use crate::bindings::_helpers::boundary::panics::compiler_error; +use crate::bindings::_helpers::records::construct::new_record; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::sqltext::lexical_syntax::LexicalSyntaxInput; @@ -86,8 +88,7 @@ fn extract_sql_test_ctes<'py>( file_label: &str, syntax: &Bound<'py, PyAny>, ) -> PyResult> { - let syntax: LexicalSyntax = syntax - .getattr("native_mapping")? + let syntax: LexicalSyntax = native_field(syntax, "native_mapping")? .extract::()? .into(); let outcome: TopLevelCtes = py @@ -108,13 +109,17 @@ fn extract_sql_test_ctes<'py>( return Err(PyErr::from_value(error)); } }; - let cte_class: Bound<'py, PyAny> = registry::class(py, RecordId::CompileSqlTestCte)?; let mut built: Vec> = Vec::with_capacity(ctes.len()); for (name, body) in ctes { let arguments: Bound<'py, PyDict> = PyDict::new(py); arguments.set_item("name", name)?; arguments.set_item("sql_body", body)?; - built.push(cte_class.call((), Some(&arguments))?); + built.push(new_record( + py, + RecordId::CompileSqlTestCte, + (), + Some(&arguments), + )?); } PyTuple::new(py, built) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_command.rs b/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_command.rs index ac95e6dfb9..6d817d96f2 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_command.rs @@ -14,9 +14,12 @@ use sqlbuild_scopes::scope_text::main::scope_result_failed::scope_result_failed; use sqlbuild_scopes::scope_text::models::ScopeTextRequest; use crate::bindings::_helpers::presentation_text::terminal::stdout_supports_color; +use crate::bindings::_helpers::records::construct::{enum_from_value, new_record}; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr, RecordId}; +use crate::bindings::_helpers::records::fields::replaced_keywords; +use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::scopes::scope_loading::load_or_build_scope_index; use crate::bindings::_helpers::scopes::scope_lookup::build_scope_lookup; use crate::bindings::_helpers::scopes::scope_objects::ScopeClasses; use crate::bindings::_helpers::scopes::scope_report_objects::read_result; @@ -160,10 +163,9 @@ fn report_filters<'py>( py: Python<'py>, request: &Bound<'py, PyAny>, ) -> PyResult> { - let kind_class: Bound<'_, PyAny> = registry::class(py, RecordId::DeclarationKind)?; let mut kinds: Vec> = Vec::new(); for kind in native_field(request, "kinds")?.try_iter()? { - kinds.push(kind_class.call1((kind?,))?); + kinds.push(enum_from_value(py, RecordId::DeclarationKind, kind?)?); } let options: Bound<'_, PyDict> = keywords( py, @@ -183,7 +185,7 @@ fn report_filters<'py>( ("globals", native_field(request, "globals")?), ], )?; - registry::class(py, RecordId::ScopeReportFilters)?.call((), Some(&options)) + new_record(py, RecordId::ScopeReportFilters, (), Some(&options)) } /// `dataclasses.replace(report, **changes)`. @@ -191,11 +193,7 @@ fn replaced<'py>( report: &Bound<'py, PyAny>, changes: &Bound<'py, PyDict>, ) -> PyResult> { - report - .py() - .import("dataclasses")? - .getattr("replace")? - .call((report,), Some(changes)) + replaced_keywords(report, changes) } /// The report with extra diagnostics and a part of the result merged in. @@ -333,7 +331,7 @@ fn use_color( /// Load offline compiler facts, run one pure query, and present its result. #[pyfunction] #[pyo3(signature = (*, request, load_scope_index = None, output_stream = None))] -fn run_scope_command( +pub(crate) fn run_scope_command( py: Python<'_>, request: &Bound<'_, PyAny>, load_scope_index: Option>, @@ -343,18 +341,23 @@ fn run_scope_command( directory if !directory.is_none() => directory, _ => py.import("pathlib")?.getattr("Path")?.call_method0("cwd")?, }; - let loader: Bound<'_, PyAny> = match load_scope_index { - Some(loader) => loader, - None => registry::native(py, NativeAttr::LoadOrBuildScopeIndex)?, + let index: Bound<'_, PyAny> = match load_scope_index { + Some(loader) => { + let options: Bound<'_, PyDict> = keywords( + py, + &[ + ("project_dir", project_dir), + ("no_cache", native_field(request, "no_cache")?), + ], + )?; + loader.call((), Some(&options))? + } + None => load_or_build_scope_index( + &project_dir, + native_field(request, "no_cache")?.is_truthy()?, + )? + .into_any(), }; - let options: Bound<'_, PyDict> = keywords( - py, - &[ - ("project_dir", project_dir), - ("no_cache", native_field(request, "no_cache")?), - ], - )?; - let index: Bound<'_, PyAny> = loader.call((), Some(&options))?; let lookup: Bound<'_, PyAny> = build_scope_lookup(py, &index)?.into_bound(py); let target: Option = qualify_target(py, &lookup, native_field(request, "target")?.extract()?)?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_errors.rs b/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_errors.rs index 663f1e4a99..df43d56320 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_errors.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_errors.rs @@ -1,8 +1,9 @@ //! Report a scope index's error findings as individual compile diagnostics. +use crate::bindings::_helpers::compile_driver::diagnostic_collector::report_compile_diagnostic; +use crate::bindings::_helpers::records::construct::new_record; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::ids::RecordId; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; use pyo3::types::{PyDict, PyDictMethods, PyString, PyTuple}; @@ -11,8 +12,6 @@ const ERROR_SEVERITY: &str = "error"; /// `report_scope_index_errors`: each error-severity finding of `index`, keyed for the collector. pub(crate) fn report_scope_index_errors(index: &Bound<'_, PyAny>) -> PyResult<()> { let py: Python<'_> = index.py(); - let report = registry::native(py, NativeAttr::ReportCompileDiagnostic)?; - let compiler_diagnostic = registry::class(py, RecordId::CompilerDiagnostic)?; let path_class = py.import("pathlib")?.getattr("Path")?; for item in native_field(index, "diagnostics")?.try_iter()? { let item: Bound<'_, PyAny> = item?; @@ -23,7 +22,7 @@ pub(crate) fn report_scope_index_errors(index: &Bound<'_, PyAny>) -> PyResult<() { continue; } - let code: Bound<'_, PyAny> = native_field(&item, "code")?.getattr("value")?; + let code: Bound<'_, PyAny> = native_field(&native_field(&item, "code")?, "value")?; let message: Bound<'_, PyAny> = native_field(&item, "message")?; let path: Bound<'_, PyAny> = native_field(&item, "path")?; let declaration: Bound<'_, PyAny> = native_field(&item, "declaration")?; @@ -53,11 +52,9 @@ pub(crate) fn report_scope_index_errors(index: &Bound<'_, PyAny>) -> PyResult<() } fields.set_item("line", native_field(&item, "line")?)?; fields.set_item("column", native_field(&item, "column")?)?; - let diagnostic: Bound<'_, PyAny> = compiler_diagnostic.call((), Some(&fields))?; - let arguments: Bound<'_, PyDict> = PyDict::new(py); - arguments.set_item("key", key)?; - arguments.set_item("diagnostic", diagnostic)?; - report.call((), Some(&arguments))?; + let diagnostic: Bound<'_, PyAny> = + new_record(py, RecordId::CompilerDiagnostic, (), Some(&fields))?; + report_compile_diagnostic(key.as_any(), &diagnostic)?; } Ok(()) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_index.rs b/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_index.rs index 6e8970b78b..372ecbce62 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_index.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_index.rs @@ -123,7 +123,7 @@ fn header_name<'py>(file: &Bound<'py, PyAny>) -> PyResult> { } fn items<'py>(item: &Bound<'py, PyAny>, name: &str) -> PyResult>> { - item.getattr(name)?.try_iter()?.collect() + native_field(item, name)?.try_iter()?.collect() } impl<'py> Walk<'py> { @@ -274,9 +274,7 @@ impl<'py> Walk<'py> { .map(|rendering| native_field(&rendering, "value")) .transpose()?; self.classes.model(RecordId::ConstantMetadata)?.call1(( - declaration - .getattr("logical_type")? - .getattr("display_name")?, + native_field(&native_field(declaration, "logical_type")?, "display_name")?, if is_collection { Some(native_field(&kind, "value")?) } else { @@ -462,7 +460,7 @@ impl<'py> Walk<'py> { [None, None, None], ); let fallback: Bound<'py, PyAny> = or_else(native_field(item, "declaration_root")?, || { - native_field(item, "relative_path")?.getattr("parent") + native_field(&native_field(item, "relative_path")?, "parent") })?; let placement = Self::file_placement(item, text(&fallback)?)?; self.add_declaration(values, placement) @@ -472,7 +470,7 @@ impl<'py> Walk<'py> { let audit_kind: String = DeclarationKind::Audit.as_str().to_owned(); for audit_file in items(inputs, "audit_files")? { let stem_name: Bound<'py, PyAny> = - native_field(&audit_file, "relative_path")?.getattr("stem")?; + native_field(&native_field(&audit_file, "relative_path")?, "stem")?; if value_of(&audit_file, "declaration_kind")? == audit_kind { self.add_named(&audit_file, DeclarationKind::Audit, &stem_name)?; continue; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_loading.rs b/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_loading.rs index 7ea3883b93..80cdf70d97 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_loading.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_loading.rs @@ -20,9 +20,9 @@ use crate::bindings::_helpers::frontier::compile_frontier::{FrontierStage, bridg use crate::bindings::_helpers::native_host::adapter::AdapterHost; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::native_host::user_code::UserCodeHost; +use crate::bindings::_helpers::records::construct::{new_record, new_record_with}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::scopes::scope_cache::{ project_fingerprint, read_scope_index_cache, write_scope_index_cache, }; @@ -82,8 +82,9 @@ fn compiled_index<'py>( let collection_rendering: Bound<'py, PyAny> = resolve_effective_collection_rendering(&project_config, &py.None().into_bound(py)) .and_then(|value| value.into_bound_py_any(py))?; - let adapter_context: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::CompileAdapterContext)?, + let adapter_context: Bound<'py, PyAny> = new_record_with( + py, + RecordId::CompileAdapterContext, &[ ("value_renderer", adapter.clone()), ("collection_rendering", collection_rendering), @@ -170,7 +171,7 @@ fn tolerant_index<'py>( /// Load or build scope facts without credentials, a connection, or warehouse access. #[pyfunction] #[pyo3(signature = (*, project_dir, no_cache=false))] -fn load_or_build_scope_index<'py>( +pub(crate) fn load_or_build_scope_index<'py>( project_dir: &Bound<'py, PyAny>, no_cache: bool, ) -> PyResult> { @@ -186,7 +187,7 @@ fn load_or_build_scope_index<'py>( return Ok(cached.into_bound(py)); } } - let mut syntax: Bound<'py, PyAny> = registry::class(py, RecordId::SqlLexicalSyntax)?.call0()?; + let mut syntax: Bound<'py, PyAny> = new_record(py, RecordId::SqlLexicalSyntax, (), None)?; let compiled: PyResult> = match project_adapter(&resolved) { Ok((discovered, adapter)) => { syntax = native_field(&adapter, "sql_lexical_syntax")?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_objects.rs b/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_objects.rs index 60fd2ec87e..ab0f6eb8d3 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_objects.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_objects.rs @@ -17,9 +17,9 @@ use sqlbuild_scopes::scope_report::models::{ ProjectionGrant, ProjectionIndex, ProjectionStanding, ProjectionUsage, ReportValues, }; +use crate::bindings::_helpers::records::construct::{new_record, record_class}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::scopes::placement::{ IdentityRow, QualifiedRow, RootRow, declaration_identity, declaration_record, ownership_root, qualified_identity, resource_identity, @@ -41,12 +41,12 @@ impl<'py> ScopeClasses<'py> { /// One scope model class. pub(crate) fn model(&self, name: RecordId) -> PyResult> { - registry::class(self.py, name) + record_class(self.py, name) } /// The member of scope enum `name` with `value`; an unknown value raises `ValueError`. pub(crate) fn member(&self, name: RecordId, value: &str) -> PyResult> { - registry::class(self.py, name)?.call1((value,)) + new_record(self.py, name, (value,), None) } /// The interpreter the classes belong to. @@ -65,26 +65,26 @@ pub(crate) fn enum_text(value: &Bound<'_, PyAny>) -> PyResult { } pub(crate) fn value_of(item: &Bound<'_, PyAny>, name: &str) -> PyResult { - native_field(&item.getattr(name)?, "value")?.extract() + native_field(&native_field(item, name)?, "value")?.extract() } pub(crate) fn field<'py, T: pyo3::FromPyObject<'py>>( item: &Bound<'py, PyAny>, name: &str, ) -> PyResult { - item.getattr(name)?.extract() + native_field(item, name)?.extract() } pub(crate) fn optional_item<'py>( item: &Bound<'py, PyAny>, name: &str, ) -> PyResult>> { - let value: Bound<'py, PyAny> = item.getattr(name)?; + let value: Bound<'py, PyAny> = native_field(item, name)?; Ok(if value.is_none() { None } else { Some(value) }) } fn items<'py>(item: &Bound<'py, PyAny>, name: &str) -> PyResult>> { - item.getattr(name)?.try_iter()?.collect() + native_field(item, name)?.try_iter()?.collect() } /// A resource identity's `(kind, name)` row. @@ -726,5 +726,5 @@ pub(crate) fn tuple_attribute<'py>( item: &Bound<'py, PyAny>, name: &str, ) -> PyResult> { - Ok(item.getattr(name)?.downcast_into::()?) + Ok(native_field(item, name)?.downcast_into::()?) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_report_objects.rs b/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_report_objects.rs index 9c14efc5f1..68014b23f0 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_report_objects.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_report_objects.rs @@ -26,7 +26,7 @@ type PyObject = Py; const PATHLIB_MODULE: &str = "pathlib"; fn texts(item: &Bound<'_, PyAny>, name: &str) -> PyResult> { - item.getattr(name)? + native_field(item, name)? .try_iter()? .map(|value| value?.str()?.extract()) .collect() @@ -462,7 +462,7 @@ fn read_declaration_reports( item: &Bound<'_, PyAny>, name: &str, ) -> PyResult> { - item.getattr(name)? + native_field(item, name)? .try_iter()? .map(|report| read_declaration_report(&report?)) .collect() @@ -552,8 +552,7 @@ fn read_report(report: &Bound<'_, PyAny>) -> PyResult { directory: field(&resource, "directory")?, duplicate_count: field(&resource, "duplicate_count")?, }, - scope_chain: report - .getattr("scope_chain")? + scope_chain: native_field(report, "scope_chain")? .try_iter()? .map(|entry| { let entry: Bound<'_, PyAny> = entry?; @@ -569,8 +568,7 @@ fn read_report(report: &Bound<'_, PyAny>) -> PyResult { relationship_scope: read_declaration_reports(report, "relationship_scope")?, nearby_unavailable: read_declaration_reports(report, "nearby_unavailable")?, filters: read_filters(&native_field(report, "filters")?)?, - sections: report - .getattr("sections")? + sections: native_field(report, "sections")? .try_iter()? .map(|section| read_section(§ion?)) .collect::>()?, @@ -596,8 +594,7 @@ pub(crate) fn read_result( if result.is_instance(&classes.model(RecordId::ScopeBrowseResult)?)? { return Ok(ScopeResult::Browse(BrowseResult { folder: field(result, "folder")?, - folders: result - .getattr("folders")? + folders: native_field(result, "folders")? .try_iter()? .map(|folder| read_folder(&folder?)) .collect::>()?, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_reports.rs b/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_reports.rs index 909afc238f..46e3030a70 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_reports.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_reports.rs @@ -83,8 +83,7 @@ impl LookupReports { fn lookup_reports(lookup: &Bound<'_, PyAny>) -> PyResult { let index: Bound<'_, PyAny> = native_field(lookup, "index")?; let native: Py = native_field(lookup, "native")?.extract()?; - let rows: Vec = index - .getattr("declarations")? + let rows: Vec = native_field(&index, "declarations")? .try_iter()? .map(|record| Ok(declaration_values(&read_declaration(&record?)?))) .collect::>()?; @@ -166,7 +165,7 @@ pub(crate) fn scope_report( match build_scope_report(reports.source(native), &request) { Ok(report) => { let diagnostics: Bound<'_, PyAny> = - native_field(lookup, "index")?.getattr("diagnostics")?; + native_field(&native_field(lookup, "index")?, "diagnostics")?; Ok(report_object(&classes, report, (&selected, &diagnostics))?.unbind()) } Err(failure) => Err(failure_error(py, &failure)?), @@ -192,7 +191,7 @@ pub(crate) fn scope_explain( match explain_scope_declaration(reports.source(native), &request) { Ok((explanation, diagnostics)) => { let index_diagnostics: Bound<'_, PyAny> = - native_field(lookup, "index")?.getattr("diagnostics")?; + native_field(&native_field(lookup, "index")?, "diagnostics")?; Ok(( explanation_object(&classes, explanation)?.unbind(), diagnostics_object(&classes, &index_diagnostics, raised(diagnostics))?.unbind(), @@ -230,7 +229,8 @@ pub(crate) fn scope_browse( let request: FolderRequest = folder_request(&classes, request, None)?; let native: &NativeScopeLookup = reports.lookup.get(); let result = browse_scope_folders(reports.source(native), &request); - let diagnostics: Bound<'_, PyAny> = native_field(lookup, "index")?.getattr("diagnostics")?; + let diagnostics: Bound<'_, PyAny> = + native_field(&native_field(lookup, "index")?, "diagnostics")?; Ok(browse_object(&classes, result, &diagnostics)?.unbind()) } @@ -249,7 +249,7 @@ pub(crate) fn scope_list( match list_scope_declarations(reports.source(native), &request) { Ok(result) => { let diagnostics: Bound<'_, PyAny> = - native_field(lookup, "index")?.getattr("diagnostics")?; + native_field(&native_field(lookup, "index")?, "diagnostics")?; Ok(list_object(&classes, result, &diagnostics)?.unbind()) } Err(failure) => Err(failure_error(py, &failure)?), @@ -275,7 +275,7 @@ pub(crate) fn scope_move_preview( match preview_scope_move(reports.source(native), &request) { Ok((preview, diagnostics)) => { let index_diagnostics: Bound<'_, PyAny> = - native_field(lookup, "index")?.getattr("diagnostics")?; + native_field(&native_field(lookup, "index")?, "diagnostics")?; Ok(( preview .map(|preview| Ok::<_, PyErr>(move_object(&classes, preview)?.unbind())) @@ -317,7 +317,7 @@ pub(crate) fn serialize_scope_report( /// `scope_metadata_projection` of one `ScopeIndex`, as the dictionary Python builds. #[pyfunction] #[pyo3(name = "scope_metadata_projection", signature = (*, index))] -fn metadata_projection(py: Python<'_>, index: &Bound<'_, PyAny>) -> PyResult { +pub(crate) fn metadata_projection(py: Python<'_>, index: &Bound<'_, PyAny>) -> PyResult { let index: ProjectionIndex = projection_index(read_index(index)?); let projection: JsonValue = scope_metadata_projection(&index); json_to_python(py, projection, false) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_tolerant.rs b/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_tolerant.rs index 36ed52f3c4..4b89e2aeb9 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_tolerant.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_tolerant.rs @@ -34,7 +34,7 @@ fn posix(path: &Bound<'_, PyAny>) -> PyResult { } fn items<'py>(item: &Bound<'py, PyAny>, name: &str) -> PyResult>> { - item.getattr(name)?.try_iter()?.collect() + native_field(item, name)?.try_iter()?.collect() } /// New diagnostics with their order keys. diff --git a/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_validation.rs b/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_validation.rs index 4e656bcc32..bd9a5be4d8 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_validation.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_validation.rs @@ -14,7 +14,9 @@ use sqlbuild_scopes::placement::models::{ use sqlbuild_scopes::scope_index::models::ResourceIdentity; use crate::bindings::_helpers::boundary::panics::compiler_error; +use crate::bindings::_helpers::records::construct::enum_member; use crate::bindings::_helpers::records::fields::field as native_field; +use crate::bindings::_helpers::records::fields::replaced_keywords; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::scopes::compile_usages::{ @@ -27,7 +29,6 @@ use crate::bindings::_helpers::scopes::scope_objects::{ }; use crate::bindings::types::CompilerDetach; -const DATACLASSES_MODULE: &str = "dataclasses"; const RUNTIME_USAGE: usize = 2; /// Compile input groups whose items carry declaration usages, after the model inputs. const COMPILE_USAGE_GROUPS: [&str; 5] = [ @@ -48,9 +49,7 @@ pub(crate) fn replace<'py>( for (name, value) in changes { keywords.set_item(*name, value)?; } - py.import(DATACLASSES_MODULE)? - .getattr("replace")? - .call((item,), Some(&keywords)) + replaced_keywords(item, &keywords) } fn tuple_from<'py>(py: Python<'py>, items: Vec>) -> PyResult> { @@ -75,8 +74,7 @@ fn dependency_row( let dependencies: Option> = match optional_item(record, "macro")? { None => None, Some(metadata) => Some( - metadata - .getattr("dependencies")? + native_field(&metadata, "dependencies")? .try_iter()? .map(|item| qualified_row(&item?)) .collect::>()?, @@ -349,9 +347,10 @@ pub(crate) fn scope_index_with_compile_usages<'py>( } else { native_field(&scope, "resolver")? }; - let schema_files: Bound<'py, PyAny> = inputs - .getattr("discovered_inputs")? - .getattr("model_schema_files")?; + let schema_files: Bound<'py, PyAny> = native_field( + &native_field(inputs, "discovered_inputs")?, + "model_schema_files", + )?; let mut usages: Vec> = Vec::new(); usages.extend(usage_records( model_schema_scope_usages(&schema_files, &resolver)?.as_any(), @@ -362,15 +361,15 @@ pub(crate) fn scope_index_with_compile_usages<'py>( usages.extend(usage_records(&native_field(&model, "declaration_usages")?)?); } for group in COMPILE_USAGE_GROUPS { - for item in inputs.getattr(group)?.try_iter()? { + for item in native_field(inputs, group)?.try_iter()? { usages.extend(usage_records(&native_field(&item?, "declaration_usages")?)?); } } - let enforce_placement: bool = inputs - .getattr("project_config")? - .getattr("scopes")? - .getattr("enforce_placement")? - .extract()?; + let enforce_placement: bool = native_field( + &native_field(&native_field(inputs, "project_config")?, "scopes")?, + "enforce_placement", + )? + .extract()?; Ok(scope_placement_validated_index( &native_field(inputs, "scope_index")?, enforce_placement, @@ -384,8 +383,7 @@ pub(crate) fn scope_index_with_compile_usages<'py>( #[pyo3(signature = (*, index))] pub(crate) fn validate_scope_index(index: &Bound<'_, PyAny>) -> Result<(), PyErr> { let py: Python<'_> = index.py(); - let error: Bound<'_, PyAny> = - registry::class(py, RecordId::ScopeDiagnosticSeverity)?.getattr("ERROR")?; + let error: Bound<'_, PyAny> = enum_member(py, RecordId::ScopeDiagnosticSeverity, "ERROR")?; let mut errors: Vec> = Vec::new(); for item in native_field(index, "diagnostics")?.try_iter()? { let item: Bound<'_, PyAny> = item?; @@ -398,7 +396,7 @@ pub(crate) fn validate_scope_index(index: &Bound<'_, PyAny>) -> Result<(), PyErr } let mut message: String = String::from("Invalid declaration scope index:"); for item in &errors { - let code: String = native_field(item, "code")?.getattr("value")?.extract()?; + let code: String = native_field(&native_field(item, "code")?, "value")?.extract()?; let text: String = native_field(item, "message")?.extract()?; message.push_str(&format!("\n[{code}] {text}")); } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/seed_command/command.rs b/crates/sqlbuild-python/src/bindings/_helpers/seed_command/command.rs index 82362541fa..f43a61d2c9 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/seed_command/command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/seed_command/command.rs @@ -14,14 +14,27 @@ use sqlbuild_core::seed_command::main::render_seed_row::render_seed_row; use sqlbuild_core::seed_command::main::seed_exit_code::seed_exit_code; use sqlbuild_core::seed_command::models::SeedRow; +use crate::bindings::_helpers::cli::progress::current_native_progress_projector; +use crate::bindings::_helpers::cli::progress_reporters::ConnectionProgressReporter; +use crate::bindings::_helpers::cli::progress_text::write_execution_header; use crate::bindings::_helpers::command_runtime::invocation::resolve_command_invocation; +use crate::bindings::_helpers::compile_driver::pipeline_entries::build_static_pipeline_result; use crate::bindings::_helpers::compile_driver::plan_result::connection_with_hooks; use crate::bindings::_helpers::compile_driver::python_calls::call_with; +use crate::bindings::_helpers::compile_driver::python_calls::keywords; +use crate::bindings::_helpers::compile_driver::selection_phases::compile_static_command_context; +use crate::bindings::_helpers::dbt_interop::reference_resolver::resolve_external_sql_reference_resolver; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; +use crate::bindings::_helpers::planner_entries::command_plans::build_seed_command_plan; +use crate::bindings::_helpers::planner_entries::command_plans::read_selected_seed_fingerprints; +use crate::bindings::_helpers::records::construct::{new_record_with, record_class}; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::fields::replaced_keywords; +use crate::bindings::_helpers::records::ids::RecordId; +use crate::bindings::_helpers::result_output::documents::format_seed_execution_json; +use crate::bindings::_helpers::result_output::documents::write_execution_json_output; +use crate::bindings::_helpers::result_output::event_writer::ExecutionEventWriter; const SUCCESS_STATUS: &str = "success"; const FAILED_STATUS: &str = "failed"; @@ -32,7 +45,7 @@ fn text<'py>(py: Python<'py>, value: &str) -> Bound<'py, PyAny> { } fn status_value(result: &Bound<'_, PyAny>) -> PyResult { - native_field(result, "status")?.getattr("value")?.extract() + native_field(&native_field(result, "status")?, "value")?.extract() } /// Forward positional connection timing to a reporter that takes keywords. @@ -128,10 +141,7 @@ impl SeedCompletionRelay { py.import("builtins")?.getattr("int")?.call1((duration,))?; let changes: Bound<'_, PyDict> = PyDict::new(py); changes.set_item("duration_ms", milliseconds)?; - result = py - .import("dataclasses")? - .getattr("replace")? - .call((result,), Some(&changes))?; + result = replaced_keywords(&result, &changes)?; } } let ordinal: usize = *self @@ -176,28 +186,30 @@ fn compile_seeds<'py>( let adapter: Bound<'py, PyAny> = native_field(invocation, "adapter")?; let connection_config: Bound<'py, PyAny> = native_field(invocation, "connection_config")?; let discovered: Bound<'py, PyAny> = native_field(invocation, "discovered_inputs")?; - let connection_progress: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ConnectionProgressReporter)?, - &[ - ("adapter_name", native_field(invocation, "adapter_name")?), - ("stream", stream.clone()), - ("use_color", native_field(invocation, "use_color")?), - ], - )?; + let connection_progress: Bound<'py, PyAny> = Bound::new( + py, + ConnectionProgressReporter::new( + py, + Some(keywords( + py, + &[ + ("adapter_name", native_field(invocation, "adapter_name")?), + ("stream", stream.clone()), + ("use_color", native_field(invocation, "use_color")?), + ], + )?), + )?, + )? + .into_any(); stream.call_method1("write", ("\n",))?; stream.call_method0("flush")?; - let resolver: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ResolveExternalSqlReferenceResolver)?, - &[ - ( - "project_dir", - native_field(invocation, "effective_project_dir")?, - ), - ("discovered_inputs", discovered.clone()), - ], + let resolver: Bound<'py, PyAny> = resolve_external_sql_reference_resolver( + native_field(invocation, "effective_project_dir")?, + discovered.clone(), )?; - let options: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::CompilePipelineOptions)?, + let options: Bound<'py, PyAny> = new_record_with( + py, + RecordId::CompilePipelineOptions, &[ ("selected_target", native_field(request, "selected_target")?), ("select", native_field(request, "select")?), @@ -207,14 +219,14 @@ fn compile_seeds<'py>( ("external_sql_reference_resolver", resolver), ], )?; - let context: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::CompileStaticCommandContext)?, + let context: Bound<'py, PyAny> = compile_static_command_context(Some(&keywords( + py, &[ ("discovered_inputs", discovered), ("adapter", adapter.clone()), ("options", options), ], - )?; + )?))?; let project: Bound<'py, PyAny> = native_field(&context, "project")?; let scope: Bound<'py, PyAny> = native_field(&context, "scope")?; let selected_keys: Bound<'py, PyAny> = native_field(&scope, "selected_keys")?; @@ -248,38 +260,34 @@ fn compile_seeds<'py>( )?; let connection: Bound<'py, PyAny> = connection_with_hooks(&adapter, &connection_config, &hooks)?; - let read: PyResult> = call_with( - ®istry::native(py, NativeAttr::ReadSelectedSeedFingerprints)?, - &[ - ("adapter", adapter.clone()), - ("connection", connection.clone()), - ("seeds", PyTuple::new(py, &selected)?.into_any()), - ], - ); + let read: PyResult> = read_selected_seed_fingerprints( + &adapter, + &connection, + &PyTuple::new(py, &selected)?.into_any(), + ) + .map(Bound::into_any); let closed: PyResult> = adapter.call_method1("close", (connection,)); let read: Bound<'py, PyAny> = read?; closed?; read }; - let plan_output: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::BuildSeedCommandPlan)?, - &[ - ("project", project), - ("scope", scope), - ("relations", native_field(&context, "relations")?), - ("fingerprints", fingerprints), - ], + let plan_output: Bound<'py, PyAny> = build_seed_command_plan( + &project, + &scope, + &native_field(&context, "relations")?, + Some(fingerprints), )?; - call_with( - ®istry::native(py, NativeAttr::BuildStaticPipelineResult)?, - &[("context", context), ("plan_output", plan_output)], + build_static_pipeline_result( + py, + Some(keywords( + py, + &[("context", context), ("plan_output", plan_output)], + )?), ) } fn seed_names(pipeline: &Bound<'_, PyAny>) -> PyResult> { - pipeline - .getattr("plan_output")? - .getattr("seed_entries")? + native_field(&native_field(pipeline, "plan_output")?, "seed_entries")? .try_iter()? .map(|entry| native_field(&entry?, "name")?.extract()) .collect() @@ -304,7 +312,7 @@ fn write_header( header.set_item("target", py.None())?; header.set_item("concurrency", concurrency)?; header.set_item("use_color", use_color)?; - registry::native(py, NativeAttr::WriteExecutionHeader)?.call((), Some(&header))?; + write_execution_header(py, Some(header))?; Ok(()) } @@ -326,9 +334,8 @@ fn load_seeds<'py>( .map(|(index, name)| (name.clone(), index + 1)) .collect(); let event_writer: Bound<'py, PyAny> = - registry::native(py, NativeAttr::ExecutionEventWriter)?.call0()?; - let projector: Bound<'py, PyAny> = - registry::native(py, NativeAttr::CurrentNativeProgressProjector)?.call0()?; + Bound::new(py, ExecutionEventWriter::new(py, None)?)?.into_any(); + let projector: Bound<'py, PyAny> = current_native_progress_projector(py)?; let projector: Option> = if projector.is_none() { None } else { @@ -357,18 +364,25 @@ fn load_seeds<'py>( projector, }, )?; - let connection_progress: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ConnectionProgressReporter)?, - &[ - ("adapter_name", native_field(invocation, "adapter_name")?), - ("stream", stream), - ( - "blank_line_after_complete", - PyBool::new(py, true).to_owned().into_any(), - ), - ("use_color", use_color), - ], - )?; + let connection_progress: Bound<'py, PyAny> = Bound::new( + py, + ConnectionProgressReporter::new( + py, + Some(keywords( + py, + &[ + ("adapter_name", native_field(invocation, "adapter_name")?), + ("stream", stream), + ( + "blank_line_after_complete", + PyBool::new(py, true).to_owned().into_any(), + ), + ("use_color", use_color), + ], + )?), + )?, + )? + .into_any(); let relay: Bound<'py, SeedConnectionRelay> = connection_relay(&connection_progress)?; let loaded: PyResult>> = (|| { call_with( @@ -384,9 +398,10 @@ fn load_seeds<'py>( ("run_id", native_field(&project, "run_id")?), ( "query_change_tracking", - project - .getattr("settings")? - .getattr("query_change_tracking")?, + native_field( + &native_field(&project, "settings")?, + "query_change_tracking", + )?, ), ( "on_seed_complete", @@ -418,17 +433,17 @@ fn load_seeds<'py>( /// Run one `sqb seed` request. #[pyfunction] #[pyo3(signature = (*, request))] -fn run_seed_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { +pub(crate) fn run_seed_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { let invocation: Bound<'_, PyAny> = - resolve_command_invocation(request, ®istry::class(py, RecordId::SeedInvocation)?)?; + resolve_command_invocation(request, &record_class(py, RecordId::SeedInvocation)?)?; let pipeline: Bound<'_, PyAny> = compile_seeds(request, &invocation)?; let requested: Bound<'_, PyAny> = native_field(request, "concurrency")?; let concurrency: i64 = if requested.is_none() { - pipeline - .getattr("project")? - .getattr("settings")? - .getattr("concurrency")? - .extract::()? + native_field( + &native_field(&native_field(&pipeline, "project")?, "settings")?, + "concurrency", + )? + .extract::()? } else { requested.extract::()? } @@ -459,23 +474,20 @@ fn run_seed_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult ),), )?; stream.call_method0("flush")?; - let payload: Bound<'_, PyAny> = call_with( - ®istry::native(py, NativeAttr::FormatSeedExecutionJson)?, - &[ - ("results", PyTuple::new(py, &results)?.into_any()), - ("plan", native_field(&pipeline, "plan_output")?), - ], - )?; - call_with( - ®istry::native(py, NativeAttr::WriteExecutionJsonOutput)?, - &[ - ("payload", payload), - ("json_output", native_field(request, "json_output")?), - ( - "json_output_path", - native_field(request, "json_output_path")?, - ), - ], + let payload: Bound<'_, PyAny> = PyString::new( + py, + &format_seed_execution_json( + py, + PyTuple::new(py, &results)?.into_any(), + native_field(&pipeline, "plan_output")?, + )?, + ) + .into_any(); + write_execution_json_output( + py, + payload, + native_field(request, "json_output")?.extract()?, + native_field(request, "json_output_path")?, )?; Ok(seed_exit_code(failed)) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/completion.rs b/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/completion.rs index 15d805883f..d96aedc5b0 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/completion.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/completion.rs @@ -11,6 +11,7 @@ use sqlbuild_analysis::semantic_checks::models::{ use crate::bindings::_helpers::analysis::model_reads::{inferred_names, lineage_inputs}; use crate::bindings::_helpers::boundary::panics::compiler_error; use crate::bindings::_helpers::compiled_project::compiled_model_view::LineageInput; +use crate::bindings::_helpers::compiled_project::records::with_binding_diagnostics; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::semantic_checks::failures::semantic_error; use crate::bindings::_helpers::semantic_checks::metadata::{ShapeRows, shape_rows}; @@ -81,12 +82,9 @@ pub(crate) fn completed_diagnostics<'py>( .into_iter() .map(|position| rebuilt[position].clone()) .collect(); - updated.push(classes.call( - &classes.with_binding_diagnostics, - &[ - ("model", model), - ("diagnostics", &PyTuple::new(py, selected)?.into_any()), - ], + updated.push(with_binding_diagnostics( + model, + &PyTuple::new(py, selected)?.into_any(), )?); } PyTuple::new(py, updated)?.into_any() @@ -204,7 +202,7 @@ fn completion_model( rejected_opt_out_file: if opt_out.is_none() { None } else { - Some(native_field(&opt_out, "path")?.getattr("name")?.extract()?) + Some(native_field(&native_field(&opt_out, "path")?, "name")?.extract()?) }, }) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/constants.rs b/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/constants.rs new file mode 100644 index 0000000000..a134a86130 --- /dev/null +++ b/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/constants.rs @@ -0,0 +1,4 @@ +//! Stage-work names the semantic completion stage counts. + +/// Single-model schema revalidations run after type recovery. +pub(crate) const SCHEMA_REVALIDATIONS: &str = "schema_revalidations"; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/inputs.rs b/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/inputs.rs index 47ba40d9f2..96fb5d1496 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/inputs.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/inputs.rs @@ -7,9 +7,10 @@ use crate::bindings::_helpers::analysis::binding_catalog::ignores_quoted_case; use crate::bindings::_helpers::analysis::schema_request::{ SchemaRequestParts, schema_validation_request, }; +use crate::bindings::_helpers::analysis_session::stage_functions::infer_native_query_columns; +use crate::bindings::_helpers::records::construct::new_record; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::semantic_checks::resource_sql::resource_sql_diagnostics_of; use crate::bindings::_helpers::semantic_shapes::declarations::{declared_types, function_names}; use crate::bindings::models::ProjectCatalog; @@ -45,8 +46,7 @@ pub(crate) fn sql_test_columns<'py>( if !payload.is_instance(model_test_payload)? { continue; } - for cte in payload - .getattr("authored_ctes")? + for cte in native_field(&payload, "authored_ctes")? .try_iter()? .chain(native_field(&payload, "expected_ctes")?.try_iter()?) { @@ -57,20 +57,23 @@ pub(crate) fn sql_test_columns<'py>( if bodies.is_empty() { return Ok(columns); } - let query_class: Bound<'py, PyAny> = registry::class(py, RecordId::NativeColumnQuery)?; let mut queries: Vec> = Vec::with_capacity(bodies.len()); for sql in bodies.keys() { let arguments: Bound<'py, PyDict> = PyDict::new(py); arguments.set_item("sql", sql)?; arguments.set_item("mode", "batch")?; arguments.set_item("recover_cte_facts", false)?; - queries.push(query_class.call((), Some(&arguments))?); + queries.push(new_record( + py, + RecordId::NativeColumnQuery, + (), + Some(&arguments), + )?); } let arguments: Bound<'py, PyDict> = PyDict::new(py); arguments.set_item("queries", PyTuple::new(py, queries)?)?; arguments.set_item("profile", profile)?; - let analyses: Bound<'py, PyAny> = - registry::native(py, NativeAttr::InferNativeQueryColumns)?.call((), Some(&arguments))?; + let analyses: Bound<'py, PyAny> = infer_native_query_columns(Some(&arguments))?.into_any(); for (sql, analysis) in bodies.keys().iter().zip(analyses.try_iter()?) { let analysis: Bound<'py, PyAny> = analysis?; let mut names: Vec> = Vec::new(); @@ -126,7 +129,8 @@ pub(crate) fn resource_sql_diagnostics<'py>( shapes: &Bound<'py, PyAny>, profile: &Bound<'py, PyAny>, ) -> PyResult> { - let native: Bound<'py, PyAny> = native_field(project, "binding_catalog")?.getattr("native")?; + let native: Bound<'py, PyAny> = + native_field(&native_field(project, "binding_catalog")?, "native")?; Ok(resource_sql_diagnostics_of( native.extract::>()?, project, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/metadata.rs b/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/metadata.rs index b62d2db3fb..f3bece1aa5 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/metadata.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/metadata.rs @@ -55,11 +55,7 @@ pub(crate) fn metadata_diagnostics<'py>( catalog: &validation::ProjectCatalog, ) -> PyResult>> { let py: Python<'py> = classes.py(); - if !project - .getattr("settings")? - .getattr("sql_analysis")? - .is_truthy()? - { + if !native_field(&native_field(project, "settings")?, "sql_analysis")?.is_truthy()? { return Ok(Vec::new()); } let (inputs, request) = metadata_request(classes, project, profile)?; @@ -86,7 +82,7 @@ pub(crate) fn metadata_diagnostics<'py>( for (index, finding) in outcome.sources { let source: &Bound<'py, PyAny> = &inputs.sources[index]; let path: Bound<'py, PyAny> = - native_field(source, "source_file")?.getattr("relative_path")?; + native_field(&native_field(source, "source_file")?, "relative_path")?; diagnostics.push(diagnostic( classes, finding, @@ -119,10 +115,7 @@ fn metadata_request<'py>( let mut tests: Vec> = Vec::new(); for test in native_field(project, "sql_tests")?.try_iter()? { let test: Bound<'py, PyAny> = test?; - if test - .getattr("payload")? - .is_instance(&classes.model_test_payload)? - { + if native_field(&test, "payload")?.is_instance(&classes.model_test_payload)? { tests.push(test); } } @@ -139,7 +132,7 @@ fn metadata_request<'py>( let mut metadata_sources: Vec = Vec::with_capacity(sources.len()); for source in &sources { let cursor: Bound<'py, PyAny> = - native_field(source, "source_entry")?.getattr("cursor_column")?; + native_field(&native_field(source, "source_entry")?, "cursor_column")?; let index: usize = file_index(&files, &native_field(source, "source_file")?)?; metadata_sources.push(MetadataSource { name: native_field(source, "name")?.extract()?, @@ -161,9 +154,7 @@ fn metadata_request<'py>( } let request = MetadataRequest { dialect: native_field(profile, "sql_analysis_dialect")?.extract()?, - quoted_ignore_case: profile - .getattr("quoted_identifiers_ignore_case")? - .extract()?, + quoted_ignore_case: native_field(profile, "quoted_identifiers_ignore_case")?.extract()?, return_types: pairs(&native_field(profile, "function_return_types")?)?, functions, shapes: shape_rows(&shapes)?, @@ -248,8 +239,7 @@ fn test_ctes( columns_by_sql: &Bound<'_, PyAny>, ) -> PyResult> { let mut ctes: Vec = Vec::new(); - for cte in payload - .getattr("authored_ctes")? + for cte in native_field(payload, "authored_ctes")? .try_iter()? .chain(native_field(payload, "expected_ctes")?.try_iter()?) { @@ -308,7 +298,7 @@ fn sql_test_diagnostic<'py>( end_column: i64, ) -> PyResult> { let py: Python<'py> = classes.py(); - let path: Bound<'py, PyAny> = native_field(test, "test_file")?.getattr("relative_path")?; + let path: Bound<'py, PyAny> = native_field(&native_field(test, "test_file")?, "relative_path")?; let location: Bound<'py, PyAny> = classes.call( &classes.location, &[ @@ -406,9 +396,7 @@ fn diagnostic<'py>( /// Python's `config.get(sql_analysis) is not False and model.binding_validated`. fn checked(classes: &StageClasses<'_>, model: &Bound<'_, PyAny>) -> PyResult { - let opted: Bound<'_, PyAny> = model - .getattr("config")? - .getattr("values")? + let opted: Bound<'_, PyAny> = native_field(&native_field(model, "config")?, "values")? .call_method1("get", (&classes.sql_analysis_key,))?; if opted.is(PyBool::new(model.py(), false)) { return Ok(false); @@ -424,7 +412,7 @@ fn text(value: &Bound<'_, PyAny>) -> PyResult> { } fn metadata_model(classes: &StageClasses<'_>, model: &Bound<'_, PyAny>) -> PyResult { - let values: Bound<'_, PyAny> = native_field(model, "config")?.getattr("values")?; + let values: Bound<'_, PyAny> = native_field(&native_field(model, "config")?, "values")?; let get = |key: &str| values.call_method1("get", (key,)); let mut references: Vec = Vec::new(); for key in METADATA_CONFIG_KEYS { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/mod.rs b/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/mod.rs index 5075c7d3f0..692d959381 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/mod.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/mod.rs @@ -1,4 +1,5 @@ pub(crate) mod completion; +pub(crate) mod constants; pub(crate) mod failures; pub(crate) mod help_catalogue; pub(crate) mod inputs; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/resource_sql.rs b/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/resource_sql.rs index f999653e4a..f0a57820b7 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/resource_sql.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/resource_sql.rs @@ -16,6 +16,7 @@ use sqlbuild_analysis::semantic_checks::models::{ use crate::bindings::_helpers::boundary::panics::compiler_error; use crate::bindings::_helpers::compiled_project::audit_views::audit_reference_rows; use crate::bindings::_helpers::compiled_project::model_loop_host::ReferenceRow; +use crate::bindings::_helpers::records::construct::record_class; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -55,8 +56,8 @@ pub(crate) fn resource_sql_diagnostics_of<'py>( Err(failure) => return Err(failure_error(py, failure)?), }; let classes = DiagnosticClasses { - compiler_diagnostic: registry::class(py, RecordId::CompilerDiagnostic)?, - source_location: registry::class(py, RecordId::SourceLocation)?, + compiler_diagnostic: record_class(py, RecordId::CompilerDiagnostic)?, + source_location: record_class(py, RecordId::SourceLocation)?, path: py.import("pathlib")?.getattr("Path")?, }; let built: Vec> = diagnostics @@ -72,7 +73,7 @@ fn request( profile: &Bound<'_, PyAny>, ) -> PyResult { let py: Python<'_> = project.py(); - let reference_class: Bound<'_, PyAny> = registry::class(py, RecordId::CompileSqlReference)?; + let reference_class: Bound<'_, PyAny> = record_class(py, RecordId::CompileSqlReference)?; let mut audits: Vec = Vec::new(); for audit in native_field(project, "audits")?.try_iter()? { audits.push(audit_input(&audit?, &reference_class)?); @@ -95,13 +96,12 @@ fn request( } Ok(ResourceSqlRequest { dialect: native_field(profile, "sql_analysis_dialect")?.extract()?, - quoted_ignore_case: profile - .getattr("quoted_identifiers_ignore_case")? - .extract()?, - require_sql_analysis: project - .getattr("settings")? - .getattr("require_sql_analysis")? - .extract()?, + quoted_ignore_case: native_field(profile, "quoted_identifiers_ignore_case")?.extract()?, + require_sql_analysis: native_field( + &native_field(project, "settings")?, + "require_sql_analysis", + )? + .extract()?, shapes: shapes .iter() .map(|(name, columns)| shape(&name, &columns)) @@ -142,9 +142,7 @@ fn audit_input( target_name.extract()?, )) }; - let first_part: Option = file - .getattr("file_path")? - .getattr("parts")? + let first_part: Option = native_field(&native_field(&file, "file_path")?, "parts")? .try_iter()? .next() .transpose()? @@ -153,8 +151,8 @@ fn audit_input( let mut bodies: Vec<(Option, Option)> = Vec::new(); for attribute in ["sql_body", "measure_sql", "evidence_sql"] { bodies.push(( - audit.getattr(attribute)?.extract()?, - block.getattr(attribute)?.extract()?, + native_field(audit, attribute)?.extract()?, + native_field(&block, attribute)?.extract()?, )); } Ok(AuditSqlInput { @@ -179,8 +177,7 @@ fn sql_test_input(test: &Bound<'_, PyAny>) -> PyResult { let block: Bound<'_, PyAny> = native_field(test, "test_block")?; Ok(SqlTestSqlInput { name: native_field(test, "name")?.extract()?, - expected_model_names: test - .getattr("expected_model_names")? + expected_model_names: native_field(test, "expected_model_names")? .try_iter()? .map(|name| name?.extract()) .collect::>()?, @@ -196,7 +193,7 @@ fn model_input( model: &Bound<'_, PyAny>, reference_class: &Bound<'_, PyAny>, ) -> PyResult { - let values: Bound<'_, PyAny> = native_field(model, "config")?.getattr("values")?; + let values: Bound<'_, PyAny> = native_field(&native_field(model, "config")?, "values")?; let mut hooks: Vec = Vec::new(); for key in MODEL_HOOK_KEYS { let entries: Bound<'_, PyAny> = values.call_method1("get", (key,))?; @@ -248,7 +245,7 @@ fn text(value: &Bound<'_, PyAny>) -> PyResult> { /// `getattr(value, name, None)`. fn optional_attribute<'py>(value: &Bound<'py, PyAny>, name: &str) -> PyResult> { if value.hasattr(name)? { - value.getattr(name) + native_field(value, name) } else { Ok(value.py().None().into_bound(value.py())) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/stage_objects.rs b/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/stage_objects.rs index 4d3645ec91..88ef5a4e07 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/stage_objects.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/stage_objects.rs @@ -1,10 +1,12 @@ //! The Python models, enums and compile helpers the semantic completion stage reads and builds. +use crate::bindings::_helpers::compiled_project::records::project_with; use crate::bindings::_helpers::native_host::adapter::AdapterHost; use crate::bindings::_helpers::native_host::host; +use crate::bindings::_helpers::records::construct::{enum_member, record_class}; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::fields::replaced; +use crate::bindings::_helpers::records::ids::RecordId; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; use pyo3::types::{PyDict, PyDictMethods, PyString}; @@ -12,7 +14,6 @@ const COMPILE_TYPES_MODULE: &str = "sqlbuild.compiler.compile.types"; const UNNEEDED_SQL_ANALYSIS_OPT_OUT_CODE: &str = "P009"; const SQL_ANALYSIS_CONFIG_KEY: &str = "sql_analysis"; -const DATACLASSES_MODULE: &str = "dataclasses"; /// Imported once per stage run. pub(crate) struct StageClasses<'py> { @@ -26,11 +27,6 @@ pub(crate) struct StageClasses<'py> { pub(crate) source_resource: Bound<'py, PyAny>, pub(crate) sql_test_resource: Bound<'py, PyAny>, pub(crate) opt_out_code: Bound<'py, PyAny>, - pub(crate) replace: Bound<'py, PyAny>, - pub(crate) project_with: Bound<'py, PyAny>, - pub(crate) with_type_recovery: Bound<'py, PyAny>, - pub(crate) with_binding_diagnostics: Bound<'py, PyAny>, - pub(crate) schema_validations: Bound<'py, PyAny>, pub(crate) normalize_type: Bound<'py, PyAny>, pub(crate) sql_analysis_key: Bound<'py, PyAny>, pub(crate) udf_kind: Bound<'py, PyAny>, @@ -40,26 +36,20 @@ pub(crate) struct StageClasses<'py> { impl<'py> StageClasses<'py> { pub(crate) fn new(py: Python<'py>) -> PyResult { let types = py.import(COMPILE_TYPES_MODULE)?; - let severity = registry::class(types.py(), RecordId::DiagnosticSeverity)?; - let resource = registry::class(py, RecordId::CompiledResourceType)?; - let kinds = registry::class(py, RecordId::SqlReferenceKind)?; + let severity = record_class(types.py(), RecordId::DiagnosticSeverity)?; + let resource = record_class(py, RecordId::CompiledResourceType)?; + let kinds = record_class(py, RecordId::SqlReferenceKind)?; Ok(Self { - diagnostic: registry::class(py, RecordId::CompilerDiagnostic)?, - location: registry::class(py, RecordId::SourceLocation)?, - model_test_payload: registry::class(py, RecordId::CompiledModelSqlTestPayload)?, - compile_phase: registry::class(types.py(), RecordId::DiagnosticPhase)? - .getattr("COMPILE")?, + diagnostic: record_class(py, RecordId::CompilerDiagnostic)?, + location: record_class(py, RecordId::SourceLocation)?, + model_test_payload: record_class(py, RecordId::CompiledModelSqlTestPayload)?, + compile_phase: enum_member(types.py(), RecordId::DiagnosticPhase, "COMPILE")?, error: native_field(&severity, "ERROR")?, warning: native_field(&severity, "WARNING")?, model_resource: native_field(&resource, "MODEL")?, source_resource: native_field(&resource, "SOURCE")?, sql_test_resource: native_field(&resource, "SQL_TEST")?, opt_out_code: PyString::new(py, UNNEEDED_SQL_ANALYSIS_OPT_OUT_CODE).into_any(), - replace: py.import(DATACLASSES_MODULE)?.getattr("replace")?, - project_with: registry::native(py, NativeAttr::ProjectWith)?, - with_type_recovery: registry::native(py, NativeAttr::WithTypeRecovery)?, - with_binding_diagnostics: registry::native(py, NativeAttr::WithBindingDiagnostics)?, - schema_validations: registry::native(py, NativeAttr::SchemaValidations)?, normalize_type: host::entry(py, AdapterHost::NormalizeType)?, sql_analysis_key: PyString::new(py, SQL_ANALYSIS_CONFIG_KEY).into_any(), udf_kind: native_field(&kinds, "UDF")?, @@ -84,17 +74,17 @@ impl<'py> StageClasses<'py> { callable.call((), Some(&kwargs)) } - /// `dataclasses.replace(value, **pairs)`. + /// `value` with `pairs` replaced. pub(crate) fn replaced( &self, value: &Bound<'py, PyAny>, pairs: &[(&str, &Bound<'py, PyAny>)], ) -> PyResult> { - let kwargs: Bound<'py, PyDict> = PyDict::new(self.py()); - for (name, value) in pairs { - kwargs.set_item(*name, *value)?; - } - self.replace.call((value,), Some(&kwargs)) + let owned: Vec<(&str, Bound<'py, PyAny>)> = pairs + .iter() + .map(|(name, changed)| (*name, (*changed).clone())) + .collect(); + replaced(value, &owned) } /// `project_with(project=project, **pairs)`. @@ -103,8 +93,10 @@ impl<'py> StageClasses<'py> { project: &Bound<'py, PyAny>, pairs: &[(&str, &Bound<'py, PyAny>)], ) -> PyResult> { - let mut all: Vec<(&str, &Bound<'py, PyAny>)> = vec![("project", project)]; - all.extend_from_slice(pairs); - self.call(&self.project_with, &all) + let kwargs: Bound<'py, PyDict> = PyDict::new(self.py()); + for (name, value) in pairs { + kwargs.set_item(*name, *value)?; + } + project_with(project, Some(kwargs)) } } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/type_recovery.rs b/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/type_recovery.rs index ca4dff59d9..2e99060e06 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/type_recovery.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/type_recovery.rs @@ -12,11 +12,15 @@ use sqlbuild_analysis::semantic_checks::models::{ use sqlbuild_analysis::semantic_checks::types::RevisedBinding; use sqlbuild_analysis::semantic_validation::models as validation; +use crate::bindings::_helpers::analysis::binding_catalog::schema_validations; use crate::bindings::_helpers::analysis::model_reads::{inferred_names, lineage_inputs}; use crate::bindings::_helpers::analysis_session::session::NativeModelAnalysisSession; use crate::bindings::_helpers::boundary::panics::compiler_error; +use crate::bindings::_helpers::compile_driver::stage_work::count_stage; use crate::bindings::_helpers::compiled_project::compiled_model_view::LineageInput; +use crate::bindings::_helpers::compiled_project::records::with_type_recovery; use crate::bindings::_helpers::records::fields::field as native_field; +use crate::bindings::_helpers::semantic_checks::constants::SCHEMA_REVALIDATIONS; use crate::bindings::_helpers::semantic_checks::failures::{internal_error, semantic_error}; use crate::bindings::_helpers::semantic_checks::inputs::revalidation_request; use crate::bindings::_helpers::semantic_checks::session_facts::{ @@ -107,20 +111,17 @@ pub(crate) fn recovered_types<'py>( } kept.push(classes.replaced(original, &[("notes", &PyTuple::new(py, notes)?.into_any())])?); } - let poisoned_outputs: Bound<'py, PyAny> = PyFrozenSet::new(py, &poisoned)?.into_any(); + let poisoned_outputs: Bound<'py, PyFrozenSet> = PyFrozenSet::new(py, &poisoned)?; let mut recovered: Vec> = Vec::with_capacity(models.len()); for (model, positions) in models.iter().zip(outcome.model_bindings) { let selected: Vec> = positions .into_iter() .map(|position| kept[position].clone()) .collect(); - recovered.push(classes.call( - &classes.with_type_recovery, - &[ - ("model", model), - ("diagnostics", &PyTuple::new(py, selected)?.into_any()), - ("poisoned", &poisoned_outputs), - ], + recovered.push(with_type_recovery( + model, + &PyTuple::new(py, selected)?.into_any(), + &poisoned_outputs, )?); } classes.project_with( @@ -270,12 +271,9 @@ fn revalidated<'py>( for index in models { let request: Bound<'py, PyAny> = revalidation_request(project, &project_models.get_item(*index)?, &unknown)?; - let result: Bound<'py, PyAny> = classes - .call( - &classes.schema_validations, - &[("requests", &PyTuple::new(py, [request])?.into_any())], - )? - .get_item(0)?; + count_stage(py, SCHEMA_REVALIDATIONS, 1)?; + let result: Bound<'py, PyAny> = + schema_validations(py, &PyTuple::new(py, [request])?.into_any())?.get_item(0)?; let mut rows: Vec = Vec::new(); for item in native_field(&result, "diagnostics")?.try_iter()? { let item: Bound<'py, PyAny> = item?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/semantic_shapes/constants.rs b/crates/sqlbuild-python/src/bindings/_helpers/semantic_shapes/constants.rs new file mode 100644 index 0000000000..d1ffa807bf --- /dev/null +++ b/crates/sqlbuild-python/src/bindings/_helpers/semantic_shapes/constants.rs @@ -0,0 +1,4 @@ +//! Stage-work names the semantic shape stage counts. + +/// Model queries analysed again against inspected source columns. +pub(crate) const SOURCE_REBINDING_ANALYSES: &str = "source_rebinding_analyses"; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/semantic_shapes/coverage.rs b/crates/sqlbuild-python/src/bindings/_helpers/semantic_shapes/coverage.rs index 3e59eaa646..d756c0ffa4 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/semantic_shapes/coverage.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/semantic_shapes/coverage.rs @@ -20,16 +20,11 @@ const OPEN_KINDS: [&str; 3] = [SOURCE_KIND, "ref", "dbt_ref"]; pub(crate) fn semantic_coverage<'py>(project: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = project.py(); let reasons: Bound<'py, PyDict> = PyDict::new(py); - if !project - .getattr("settings")? - .getattr("sql_analysis")? - .is_truthy()? - { + if !native_field(&native_field(project, "settings")?, "sql_analysis")?.is_truthy()? { return Ok(reasons); } let shapes: Bound<'py, PyDict> = project_shapes(project, None)?; - let models: Vec> = project - .getattr("models")? + let models: Vec> = native_field(project, "models")? .try_iter()? .collect::>()?; let mut names: Vec = Vec::with_capacity(models.len()); @@ -79,9 +74,7 @@ pub(crate) fn semantic_coverage<'py>(project: &Bound<'py, PyAny>) -> PyResult = model - .getattr("config")? - .getattr("values")? + let opted: Bound<'py, PyAny> = native_field(&native_field(model, "config")?, "values")? .call_method1("get", ("sql_analysis",))?; if opted.is(PyBool::new(py, false)) { reasons.set_item(name, PyTuple::new(py, ["SQL analysis disabled"])?)?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/semantic_shapes/mod.rs b/crates/sqlbuild-python/src/bindings/_helpers/semantic_shapes/mod.rs index 9afb6d4c00..7f20b6c6ef 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/semantic_shapes/mod.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/semantic_shapes/mod.rs @@ -1,3 +1,4 @@ +pub(crate) mod constants; pub(crate) mod coverage; pub(crate) mod declarations; pub(crate) mod registration; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/semantic_shapes/shapes.rs b/crates/sqlbuild-python/src/bindings/_helpers/semantic_shapes/shapes.rs index 88c68f1fc8..0e45d20bc2 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/semantic_shapes/shapes.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/semantic_shapes/shapes.rs @@ -10,15 +10,17 @@ use crate::bindings::_helpers::analysis::binding_catalog::{ use crate::bindings::_helpers::analysis::model_reads::{ declared_rows, dynamic_columns_declared, inferred_rows, star_flags, unchecked_columns, }; +use crate::bindings::_helpers::analysis::normalization::normalize_analysis_sql; +use crate::bindings::_helpers::analysis_session::stage_functions::infer_native_expression_source_shapes; use crate::bindings::_helpers::boundary::keyword_request::Request; use crate::bindings::_helpers::compiled_project::compiled_model_view::{DeclaredRow, InferredRow}; use crate::bindings::_helpers::native_host::adapter::AdapterHost; use crate::bindings::_helpers::native_host::host; +use crate::bindings::_helpers::records::construct::enum_member; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::fields::replaced_keywords; +use crate::bindings::_helpers::records::ids::RecordId; -const DATACLASSES_MODULE: &str = "dataclasses"; const UNKNOWN: &str = "UNKNOWN"; const GENERIC_DIALECT: &str = "generic"; /// Snapshot validity start column when the model names none. @@ -77,9 +79,8 @@ pub(crate) fn published_shape<'py>( ) -> PyResult> { let py: Python<'py> = model.py(); let shape: Bound<'py, PyDict> = binding_shape(model, columns, inputs, profile)?; - let values: Bound<'py, PyAny> = native_field(model, "config")?.getattr("values")?; - let snapshot: Bound<'py, PyAny> = - registry::class(py, RecordId::MaterializationType)?.getattr("SNAPSHOT")?; + let values: Bound<'py, PyAny> = native_field(&native_field(model, "config")?, "values")?; + let snapshot: Bound<'py, PyAny> = enum_member(py, RecordId::MaterializationType, "SNAPSHOT")?; if values .call_method1("get", ("materialized",))? .eq(snapshot)? @@ -111,11 +112,7 @@ fn binding_shape<'py>( let dialect: Bound<'py, PyAny> = native_field(profile, "sql_analysis_dialect")?; let sensitive: bool = !dialect.is_none() && CASE_SENSITIVE_BINDING_DIALECTS.contains(&dialect.extract::()?.as_str()); - if profile - .getattr("quoted_identifiers_ignore_case")? - .is_truthy()? - || !sensitive - { + if native_field(profile, "quoted_identifiers_ignore_case")?.is_truthy()? || !sensitive { return Ok(columns.clone()); } let mut catalog: Bound<'py, PyAny> = native_field(profile, "binding_catalog")?; @@ -138,13 +135,12 @@ fn binding_shape<'py>( request.set_item("stubs", PyDict::new(py))?; request.set_item("placeholders", PyDict::new(py))?; let sql: Bound<'py, PyAny> = - registry::native(py, NativeAttr::NormalizeAnalysisSql)?.call1((request,))?; + PyString::new(py, &normalize_analysis_sql(py, request.extract()?)?).into_any(); let arguments: Bound<'py, PyDict> = PyDict::new(py); arguments.set_item("sql", sql)?; arguments.set_item("columns", columns)?; arguments.set_item("inputs", inputs)?; - Ok(catalog - .getattr("inferred_schema")? + Ok(native_field(&catalog, "inferred_schema")? .call((), Some(&arguments))? .downcast_into::()?) } @@ -212,9 +208,7 @@ pub(crate) fn project_profile<'py>( }; let arguments: Bound<'py, PyDict> = PyDict::new(py); arguments.set_item("binding_catalog", native_field(project, "binding_catalog")?)?; - py.import(DATACLASSES_MODULE)? - .getattr("replace")? - .call((profile,), Some(&arguments)) + replaced_keywords(&profile, &arguments) } /// Closed shapes by relation name; absence means open, never an empty physical table. @@ -231,7 +225,7 @@ pub(crate) fn project_shapes<'py>( native_field(&seed, "name")?, declared_columns( py, - &native_field(&seed, "schema_entry")?.getattr("columns")?, + &native_field(&native_field(&seed, "schema_entry")?, "columns")?, )?, )?; } @@ -251,8 +245,7 @@ pub(crate) fn project_shapes<'py>( shapes.set_item(format!("{TABLE_FUNCTION_PREFIX}{name}"), shape)?; } } - let enforced: Bound<'py, PyAny> = - registry::class(py, RecordId::ContractPolicy)?.getattr("ENFORCED")?; + let enforced: Bound<'py, PyAny> = enum_member(py, RecordId::ContractPolicy, "ENFORCED")?; let mut expression_names: Vec> = Vec::new(); let mut expressions: Vec> = Vec::new(); for source in native_field(project, "sources")?.try_iter()? { @@ -275,8 +268,7 @@ pub(crate) fn project_shapes<'py>( arguments.set_item("expressions", PyTuple::new(py, expressions)?)?; arguments.set_item("profile", &profile)?; let source_shapes: Bound<'py, PyAny> = - registry::native(py, NativeAttr::InferNativeExpressionSourceShapes)? - .call((), Some(&arguments))?; + infer_native_expression_source_shapes(Some(&arguments))?.into_any(); for (name, shape) in expression_names.iter().zip(source_shapes.try_iter()?) { let shape: Bound<'py, PyAny> = shape?; if !shape.is_none() { @@ -295,8 +287,7 @@ fn model_shapes<'py>( shapes: &Bound<'py, PyDict>, ) -> PyResult<()> { let py: Python<'py> = project.py(); - let mut pending: Vec> = project - .getattr("models")? + let mut pending: Vec> = native_field(project, "models")? .try_iter()? .collect::>()?; while !pending.is_empty() { @@ -306,10 +297,9 @@ fn model_shapes<'py>( let inferred: Option> = inferred_rows(model)?; let rows: Bound<'py, PyAny> = native_field(model, "reference_rows")?; let relations: Vec = relation_row_names(&rows)?; - let contract: Bound<'py, PyAny> = model - .getattr("config")? - .getattr("values")? - .call_method1("get", ("contract",))?; + let contract: Bound<'py, PyAny> = + native_field(&native_field(model, "config")?, "values")? + .call_method1("get", ("contract",))?; if contract.eq(enforced)? && let Some(declared) = &declared && !dynamic_columns_declared(model)? diff --git a/crates/sqlbuild-python/src/bindings/_helpers/semantic_shapes/source_bindings.rs b/crates/sqlbuild-python/src/bindings/_helpers/semantic_shapes/source_bindings.rs index fe32823a87..16c3bb82a2 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/semantic_shapes/source_bindings.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/semantic_shapes/source_bindings.rs @@ -12,18 +12,23 @@ use crate::bindings::_helpers::analysis::binding_catalog::{ use crate::bindings::_helpers::analysis::schema_request::{ SchemaRequestParts, schema_validation_request, }; +use crate::bindings::_helpers::analysis_session::stage_functions::infer_native_query_columns; use crate::bindings::_helpers::attachments::cursor_intrinsics::analysis_cursor_sql; use crate::bindings::_helpers::boundary::keyword_request::Request; +use crate::bindings::_helpers::compile_driver::python_calls::keywords; +use crate::bindings::_helpers::compile_driver::stage_work::count_stage; +use crate::bindings::_helpers::records::construct::{enum_from_value, enum_member, record_class}; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; -use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::records::fields::replaced_keywords; +use crate::bindings::_helpers::records::ids::RecordId; +use crate::bindings::_helpers::semantic_shapes::constants::SOURCE_REBINDING_ANALYSES; use crate::bindings::_helpers::semantic_shapes::declarations::{declared_types, function_names}; use crate::bindings::_helpers::semantic_shapes::shapes::{ model_inputs, project_shapes, published_shape, }; +use crate::bindings::models::BindingPositions; const BINDING_UNKNOWN_TABLE_INTERNAL_CODE: &str = "B000"; -const DATACLASSES_MODULE: &str = "dataclasses"; const GRAPHLIB_MODULE: &str = "graphlib"; const REF_KIND: &str = "ref"; const SOURCE_KIND: &str = "source"; @@ -42,10 +47,7 @@ pub(crate) fn source_binding_diagnostics<'py>( let py: Python<'py> = request.py(); let project: Object<'py> = request.get("project")?; let columns: Object<'py> = request.get("columns")?; - if !project - .getattr("settings")? - .getattr("sql_analysis")? - .is_truthy()? + if !native_field(&native_field(&project, "settings")?, "sql_analysis")?.is_truthy()? || !columns.is_truthy()? { return Ok(PyTuple::empty(py)); @@ -153,17 +155,13 @@ fn rebound_models<'py>( .getattr("TopologicalSorter")? .call1((graph,))? .call_method0("static_order")?; - let query_class: Object<'py> = registry::class(py, RecordId::NativeColumnQuery)?; - let unknown: Object<'py> = - registry::class(py, RecordId::InferredNullability)?.getattr("UNKNOWN")?; - let infer: Object<'py> = registry::native(py, NativeAttr::InferNativeQueryColumns)?; + let query_class: Object<'py> = record_class(py, RecordId::NativeColumnQuery)?; + let unknown: Object<'py> = enum_member(py, RecordId::InferredNullability, "UNKNOWN")?; let mut affected: HashSet = HashSet::new(); let mut models: Vec> = Vec::new(); for name in order.try_iter()? { let model: Object<'py> = by_name.as_any().get_item(name?)?; - let opted: Object<'py> = model - .getattr("config")? - .getattr("values")? + let opted: Object<'py> = native_field(&native_field(&model, "config")?, "values")? .call_method1("get", ("sql_analysis",))?; if !selected_keys.contains(native_field(&model, "key")?)? || opted.is(PyBool::new(py, false)) @@ -206,15 +204,16 @@ fn rebound_models<'py>( ("references", native_field(&model, "references")?), ], )?; - let analysis: Object<'py> = call( - &infer, + count_stage(py, SOURCE_REBINDING_ANALYSES, 1)?; + let analysis: Object<'py> = infer_native_query_columns(Some(&keywords( + py, &[ ("queries", PyTuple::new(py, [query])?.into_any()), ("profile", profile.clone()), ("column_types_by_table", inputs.clone().into_any()), ("column_nullability_by_table", nullability.into_any()), ], - )? + )?))? .get_item(0)?; let inferred: Object<'py> = native_field(&analysis, "columns")?; let mut all_inputs_closed: bool = true; @@ -270,9 +269,7 @@ fn validated_diagnostics<'py>( let query_sql: String = analysis_cursor_sql( py, &native_field(model, "query_sql")?.extract::()?, - &model - .getattr("config")? - .getattr("values")? + &native_field(&native_field(model, "config")?, "values")? .call_method1("get", ("cursor_type",))?, )?; requests.push(schema_validation_request( @@ -292,8 +289,7 @@ fn validated_diagnostics<'py>( let results: Object<'py> = schema_validations(py, PyTuple::new(py, &requests)?.as_any())?.into_any(); let expansions: Bound<'py, PyDict> = PyDict::new(py); - for item in project - .getattr("sql_expansions")? + for item in native_field(project, "sql_expansions")? .call_method0("items")? .try_iter()? { @@ -301,11 +297,9 @@ fn validated_diagnostics<'py>( expansions.set_item(native_field(&path, "stem")?, expansion)?; } let skipped: Object<'py> = PyString::new(py, BINDING_UNKNOWN_TABLE_INTERNAL_CODE).into_any(); - let diagnostic_class: Object<'py> = registry::class(py, RecordId::CompilerDiagnostic)?; - let severity_class: Object<'py> = registry::class(py, RecordId::DiagnosticSeverity)?; - let phase: Object<'py> = registry::class(py, RecordId::DiagnosticPhase)?.getattr("COMPILE")?; - let model_type: Object<'py> = - registry::class(py, RecordId::CompiledResourceType)?.getattr("MODEL")?; + let diagnostic_class: Object<'py> = record_class(py, RecordId::CompilerDiagnostic)?; + let phase: Object<'py> = enum_member(py, RecordId::DiagnosticPhase, "COMPILE")?; + let model_type: Object<'py> = enum_member(py, RecordId::CompiledResourceType, "MODEL")?; let mut diagnostics: Vec> = Vec::new(); for ((model, request), result) in models.iter().zip(&requests).zip(results.try_iter()?) { let result: Object<'py> = result?; @@ -338,7 +332,11 @@ fn validated_diagnostics<'py>( ("phase", phase.clone()), ( "severity", - severity_class.call1((native_field(&diagnostic, "severity")?,))?, + enum_from_value( + py, + RecordId::DiagnosticSeverity, + native_field(&diagnostic, "severity")?, + )?, ), ("code", native_field(&diagnostic, "code")?), ("message", native_field(&diagnostic, "message")?), @@ -373,7 +371,7 @@ fn positions<'py>( let fields: Vec> = ["source_start", "source_end", "output_start", "output_end"] .into_iter() - .map(|name| span.getattr(name)) + .map(|name| native_field(&span, name)) .collect::>()?; rows.push(PyTuple::new(py, fields)?.into_any()); } @@ -388,7 +386,7 @@ fn positions<'py>( request.set_item("expanded", expanded)?; request.set_item("cleaned", cleaned)?; request.set_item("passes", passes)?; - registry::native(py, NativeAttr::BindingPositions)?.call1((request,)) + Ok(Bound::new(py, BindingPositions::new(request.extract()?)?)?.into_any()) } /// Whether inspected columns differ from the known shape; untyped names match any type. @@ -431,9 +429,7 @@ fn adds_physical_evidence( /// `{str(key): str(value)}` of a dict `placeholders` config, else None. fn placeholders<'py>(model: &Object<'py>) -> PyResult> { let py: Python<'py> = model.py(); - let values: Object<'py> = model - .getattr("config")? - .getattr("values")? + let values: Object<'py> = native_field(&native_field(model, "config")?, "values")? .call_method1("get", ("placeholders",))?; if !values.is_instance_of::() { return Ok(py.None().into_bound(py)); @@ -460,9 +456,7 @@ fn replaced<'py>(value: &Object<'py>, pairs: &[(&str, Object<'py>)]) -> PyResult for (name, item) in pairs { arguments.set_item(*name, item)?; } - py.import(DATACLASSES_MODULE)? - .getattr("replace")? - .call((value,), Some(&arguments)) + replaced_keywords(value, &arguments) } fn call<'py>(callable: &Object<'py>, pairs: &[(&str, Object<'py>)]) -> PyResult> { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/sql_test_command/test_command.rs b/crates/sqlbuild-python/src/bindings/_helpers/sql_test_command/test_command.rs index 4019dbc047..9b5c626bcb 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/sql_test_command/test_command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/sql_test_command/test_command.rs @@ -23,14 +23,30 @@ use sqlbuild_core::test_command::models::{ TestFailure, TestInspectionEntry, TestParameter, TestPlanWarning, }; +use crate::bindings::_helpers::build_progress::nested_progress::NestedCommandProgressCallbacks; use crate::bindings::_helpers::build_progress::transient_status::status_reporter; +use crate::bindings::_helpers::cli::progress::current_native_progress_projector; +use crate::bindings::_helpers::cli::progress_reporters::ConnectionProgressReporter; +use crate::bindings::_helpers::cli::progress_text::{ + format_expectation_detail, format_expectation_name, write_execution_header, +}; use crate::bindings::_helpers::command_runtime::invocation::resolve_reported_command_invocation; +use crate::bindings::_helpers::compile_driver::pipeline_entries::build_static_pipeline_result; +use crate::bindings::_helpers::compile_driver::python_calls::keywords; use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; +use crate::bindings::_helpers::compile_driver::selection_phases::compile_static_command_context; use crate::bindings::_helpers::compile_outputs::runtime_target::write_test_runtime_target; +use crate::bindings::_helpers::dbt_interop::reference_resolver::resolve_external_sql_reference_resolver; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; -use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr, RecordId}; +use crate::bindings::_helpers::planner_entries::command_plans::build_test_command_plan; +use crate::bindings::_helpers::records::construct::{new_record_with, record_class}; +use crate::bindings::_helpers::records::fields::field as native_field; +use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::result_output::documents::format_test_execution_json; +use crate::bindings::_helpers::result_output::documents::write_execution_json_output; +use crate::bindings::_helpers::result_output::event_writer::ExecutionEventWriter; use crate::bindings::_helpers::result_output::integration_values::json_value; use crate::bindings::_helpers::result_output::resource_ids::sql_test_identity; @@ -70,10 +86,9 @@ fn compact_json(payload: &Bound<'_, PyAny>) -> PyResult { /// The label of one test or result: its name, plus source and values for parameterized cases. pub(crate) fn test_label(item: &Bound<'_, PyAny>, name_field: &str) -> PyResult { - let name: String = item.getattr(name_field)?.extract()?; + let name: String = native_field(item, name_field)?.extract()?; let source_path: Bound<'_, PyAny> = item.getattr("source_path")?; - let values: Vec> = item - .getattr("parameter_values")? + let values: Vec> = native_field(item, "parameter_values")? .try_iter()? .collect::>()?; if source_path.is_none() || values.is_empty() { @@ -99,9 +114,7 @@ pub(crate) fn test_label(item: &Bound<'_, PyAny>, name_field: &str) -> PyResult< }; parameters.push(TestParameter { name: value_name, - value_type: declaration - .getattr("value_type")? - .getattr("value")? + value_type: native_field(&native_field(declaration, "value_type")?, "value")? .extract()?, nullable: declaration.getattr("nullable")?.is_truthy()?, rendered: compact_json(&payload)?, @@ -146,22 +159,26 @@ fn outcome_status(outcome: &Bound<'_, PyAny>) -> PyResult<&'static str> { /// Nested `expect` rows of a completed test, one per step. fn expectation_rows<'py>(result: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = result.py(); - let row_class: Bound<'py, PyAny> = registry::class(py, RecordId::NestedProgressChildRow)?; - let name: Bound<'py, PyAny> = registry::native(py, NativeAttr::FormatExpectationName)?; - let detail: Bound<'py, PyAny> = registry::native(py, NativeAttr::FormatExpectationDetail)?; let mut rows: Vec> = Vec::new(); for step in result.getattr("step_results")?.try_iter()? { let step: Bound<'py, PyAny> = step?; - rows.push(call_with( - &row_class, + rows.push(new_record_with( + py, + RecordId::NestedProgressChildRow, &[ ("label", text(py, EXPECT_LABEL)), - ("name", name.call1((step.getattr("model_name")?,))?), + ( + "name", + text( + py, + &format_expectation_name(&step.getattr("model_name")?.extract::()?), + ), + ), ( "status_text", text(py, outcome_status(&step.getattr("outcome")?)?), ), - ("detail", detail.call1((&step,))?), + ("detail", text(py, &format_expectation_detail(&step)?)), ], )?); } @@ -305,15 +322,13 @@ fn compile_tests<'py>( let py: Python<'py> = request.py(); let discovered: Bound<'py, PyAny> = invocation.getattr("discovered_inputs")?; let adapter: Bound<'py, PyAny> = invocation.getattr("adapter")?; - let resolver: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ResolveExternalSqlReferenceResolver)?, - &[ - ("project_dir", invocation.getattr("effective_project_dir")?), - ("discovered_inputs", discovered.clone()), - ], + let resolver: Bound<'py, PyAny> = resolve_external_sql_reference_resolver( + invocation.getattr("effective_project_dir")?, + discovered.clone(), )?; - let options: Bound<'py, PyAny> = call_with( - ®istry::class(py, RecordId::CompilePipelineOptions)?, + let options: Bound<'py, PyAny> = new_record_with( + py, + RecordId::CompilePipelineOptions, &[ ("selected_target", request.getattr("selected_target")?), ("no_sql_validation", request.getattr("no_sql_validation")?), @@ -335,30 +350,34 @@ fn compile_tests<'py>( ("external_sql_reference_resolver", resolver), ], )?; - let context: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::CompileStaticCommandContext)?, + let context: Bound<'py, PyAny> = compile_static_command_context(Some(&keywords( + py, &[ ("discovered_inputs", discovered), ("adapter", adapter.clone()), ("options", options), ( "on_progress", - invocation - .getattr("planning_progress")? - .getattr("on_progress")?, + native_field( + &native_field(invocation, "planning_progress")?, + "on_progress", + )?, ), ], - )?; + )?))?; let case_name: Bound<'py, PyAny> = request.getattr("case_name")?; - let plan_output: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::BuildTestCommandPlan)?, - &[ - ("project", context.getattr("project")?), - ("adapter", adapter), - ("scope", context.getattr("scope")?), - ("relations", context.getattr("relations")?), - ("case_name", case_name.clone()), - ], + let plan_output: Bound<'py, PyAny> = build_test_command_plan( + py, + Some(keywords( + py, + &[ + ("project", context.getattr("project")?), + ("adapter", adapter), + ("scope", context.getattr("scope")?), + ("relations", context.getattr("relations")?), + ("case_name", case_name.clone()), + ], + )?), )?; if !case_name.is_none() && !plan_output.getattr("test_entries")?.is_truthy()? { let cases: Vec = strings(plan_output.getattr("available_test_case_names")?)?; @@ -375,9 +394,12 @@ fn compile_tests<'py>( ), )); } - call_with( - ®istry::native(py, NativeAttr::BuildStaticPipelineResult)?, - &[("context", context), ("plan_output", plan_output)], + build_static_pipeline_result( + py, + Some(keywords( + py, + &[("context", context), ("plan_output", plan_output)], + )?), ) } @@ -419,13 +441,11 @@ fn inspection_entry(entry: &Bound<'_, PyAny>) -> PyResult { /// Write the resolved plan without executing it; returns the exit code. fn inspect(stream: &Bound<'_, PyAny>, plan_output: &Bound<'_, PyAny>) -> PyResult { - let entries: Vec = plan_output - .getattr("test_entries")? + let entries: Vec = native_field(plan_output, "test_entries")? .try_iter()? .map(|entry| inspection_entry(&entry?)) .collect::>()?; - let warnings: Vec = plan_output - .getattr("warnings")? + let warnings: Vec = native_field(plan_output, "warnings")? .try_iter()? .map(|warning| { let warning: Bound<'_, PyAny> = warning?; @@ -463,22 +483,28 @@ fn run_tests<'py>( } } let worker_count: i64 = concurrency.min(i64::try_from(test_count).unwrap_or(i64::MAX)); - let progress: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::NestedCommandProgressCallbacks)?, - &[ - ("total", PyInt::new(py, test_count).into_any()), - ("label", text(py, "test")), - ("stream", stream.clone()), - ("use_color", use_color.clone()), - ( - "name_width", - PyInt::new(py, name_width(&entries)?).into_any(), - ), - ("concurrency", PyInt::new(py, worker_count).into_any()), - ], - )?; - let projector: Bound<'py, PyAny> = - registry::native(py, NativeAttr::CurrentNativeProgressProjector)?.call0()?; + let progress: Bound<'py, PyAny> = Bound::new( + py, + NestedCommandProgressCallbacks::new( + py, + Some(keywords( + py, + &[ + ("total", PyInt::new(py, test_count).into_any()), + ("label", text(py, "test")), + ("stream", stream.clone()), + ("use_color", use_color.clone()), + ( + "name_width", + PyInt::new(py, name_width(&entries)?).into_any(), + ), + ("concurrency", PyInt::new(py, worker_count).into_any()), + ], + )?), + )?, + )? + .into_any(); + let projector: Bound<'py, PyAny> = current_native_progress_projector(py)?; if !projector.is_none() { for entry in entries.try_iter()? { call_with( @@ -492,17 +518,24 @@ fn run_tests<'py>( (render_test_ready((test_count, models.len()), style),), )?; stream.call_method0("flush")?; - let connection: Bound<'py, PyAny> = call_with( - ®istry::native(py, NativeAttr::ConnectionProgressReporter)?, - &[ - ("adapter_name", invocation.getattr("adapter_name")?), - ("stream", stream.clone()), - ("use_color", use_color.clone()), - ], - )?; + let connection: Bound<'py, PyAny> = Bound::new( + py, + ConnectionProgressReporter::new( + py, + Some(keywords( + py, + &[ + ("adapter_name", invocation.getattr("adapter_name")?), + ("stream", stream.clone()), + ("use_color", use_color.clone()), + ], + )?), + )?, + )? + .into_any(); let preflight: Bound<'py, PyAny> = status_reporter(&stream, use_color.is_truthy()?)?; let event_writer: Bound<'py, PyAny> = - registry::native(py, NativeAttr::ExecutionEventWriter)?.call0()?; + Bound::new(py, ExecutionEventWriter::new(py, None)?)?.into_any(); let relay: Bound<'py, TestCallbackRelay> = Bound::new( py, TestCallbackRelay { @@ -565,10 +598,7 @@ fn complete( let py: Python<'_> = request.py(); let mut failures: Vec = Vec::new(); for result in results { - if result - .getattr("outcome")? - .getattr("value")? - .extract::()? + if native_field(&native_field(result, "outcome")?, "value")?.extract::()? == PASS_OUTCOME { continue; @@ -595,17 +625,13 @@ fn complete( )?; stream.call_method0("flush")?; let tuple: Bound<'_, PyAny> = PyTuple::new(py, results)?.into_any(); - let payload: Bound<'_, PyAny> = call_with( - ®istry::native(py, NativeAttr::FormatTestExecutionJson)?, - &[("results", tuple)], - )?; - call_with( - ®istry::native(py, NativeAttr::WriteExecutionJsonOutput)?, - &[ - ("payload", payload), - ("json_output", request.getattr("json_output")?), - ("json_output_path", request.getattr("json_output_path")?), - ], + let payload: Bound<'_, PyAny> = + PyString::new(py, &format_test_execution_json(py, tuple)?).into_any(); + write_execution_json_output( + py, + payload, + request.getattr("json_output")?.extract()?, + request.getattr("json_output_path")?, )?; Ok(test_exit_code(passed, results.len())) } @@ -613,11 +639,11 @@ fn complete( /// Run one `sqb test` request. #[pyfunction] #[pyo3(signature = (*, request))] -fn run_test_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { +pub(crate) fn run_test_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { let inspect_only: bool = request.getattr("inspect")?.is_truthy()?; let invocation: Bound<'_, PyAny> = resolve_reported_command_invocation( request, - ®istry::class(py, RecordId::TestInvocation)?, + &record_class(py, RecordId::TestInvocation)?, !inspect_only, )?; let pipeline: Bound<'_, PyAny> = compile_tests(request, &invocation)?; @@ -627,11 +653,11 @@ fn run_test_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult } let requested: Bound<'_, PyAny> = request.getattr("concurrency")?; let concurrency: i64 = if requested.is_none() { - pipeline - .getattr("project")? - .getattr("settings")? - .getattr("concurrency")? - .extract()? + native_field( + &native_field(&native_field(&pipeline, "project")?, "settings")?, + "concurrency", + )? + .extract()? } else { requested.extract()? }; @@ -642,12 +668,11 @@ fn run_test_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult header.set_item("target", py.None())?; header.set_item("concurrency", concurrency)?; header.set_item("use_color", invocation.getattr("use_color")?)?; - registry::native(py, NativeAttr::WriteExecutionHeader)?.call((), Some(&header))?; + write_execution_header(py, Some(header))?; let results: Vec> = run_tests(&invocation, &pipeline, concurrency)?; write_test_runtime_target( ( - &invocation - .getattr("effective_project_dir")? + &native_field(&invocation, "effective_project_dir")? .call_method1("__truediv__", ("target",))?, &invocation.getattr("adapter")?, ), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/assembly.rs b/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/assembly.rs index 6948cafe2f..55b7a7de1f 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/assembly.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/assembly.rs @@ -77,15 +77,14 @@ fn assembly_model(model: &Bound<'_, PyAny>) -> PyResult { None }; Ok(SqlTestAssemblyModel { - name: model - .getattr("model_file")? - .getattr("file_path")? - .getattr("stem")? - .extract()?, + name: native_field( + &native_field(&native_field(model, "model_file")?, "file_path")?, + "stem", + )? + .extract()?, macro_deps, unscanned_macro_source, - references: model - .getattr("references")? + references: native_field(model, "references")? .try_iter()? .map(|reference| { let reference = reference?; @@ -115,20 +114,16 @@ fn assembly_test(test: &Bound<'_, PyAny>) -> PyResult { expected_ctes: ctes(&payload, "expected_ctes")?, assertion_ctes: ctes(&payload, "assertion_ctes")?, expected_model_names: native_field(&payload, "expected_model_names")?.extract()?, - assertion_target_model_names: payload - .getattr("assertion_target_model_names")? + assertion_target_model_names: native_field(&payload, "assertion_target_model_names")? .extract()?, - reference_target_model_names: payload - .getattr("reference_target_model_names")? + reference_target_model_names: native_field(&payload, "reference_target_model_names")? .extract()?, mock_model_names: native_field(&payload, "mock_model_names")?.extract()?, }) }; Ok(SqlTestAssemblyTest { block_name: native_field(&test_block, "name")?.extract()?, - file_stem: native_field(&test_file, "file_path")? - .getattr("stem")? - .extract()?, + file_stem: native_field(&native_field(&test_file, "file_path")?, "stem")?.extract()?, relative_path: relative_path.call_method0("as_posix")?.extract()?, relative_stem: native_field(&relative_path, "stem")?.extract()?, contents: native_field(&test_file, "contents")?.extract()?, @@ -136,8 +131,7 @@ fn assembly_test(test: &Bound<'_, PyAny>) -> PyResult { block_index: native_field(&test_block, "test_index")?.extract()?, sql_body: native_field(test, "sql_body")?.extract()?, case_name: native_field(test, "case_name")?.extract()?, - parameter_schema: test - .getattr("parameter_schema")? + parameter_schema: native_field(test, "parameter_schema")? .try_iter()? .map(|parameter| { let parameter = parameter?; @@ -148,8 +142,7 @@ fn assembly_test(test: &Bound<'_, PyAny>) -> PyResult { )) }) .collect::>()?, - parameter_values: test - .getattr("parameter_values")? + parameter_values: native_field(test, "parameter_values")? .try_iter()? .map(|entry| { let (name, value): (String, Bound<'_, PyAny>) = entry?.extract()?; @@ -176,9 +169,7 @@ fn ctes(owner: &Bound<'_, PyAny>, attribute: &str) -> PyResult> /// One `SqlValue`, read by its logical kind; decimals keep `as_tuple()`'s digits. fn parameter_value(value: &Bound<'_, PyAny>) -> PyResult { - let kind: String = native_field(value, "logical_type")? - .getattr("kind")? - .extract()?; + let kind: String = native_field(&native_field(value, "logical_type")?, "kind")?.extract()?; let payload = native_field(value, "value")?; Ok(match kind.as_str() { "string" => SqlTestParameterValue::String(payload.extract()?), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/authored_ctes.rs b/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/authored_ctes.rs index bd9a742f8a..b5055426ea 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/authored_ctes.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/authored_ctes.rs @@ -54,8 +54,7 @@ impl AuthoredBody { Ok(Self { sql: sql.extract()?, contents: native_field(file, "contents")?.extract()?, - relative_path: file - .getattr("relative_path")? + relative_path: native_field(file, "relative_path")? .call_method0("as_posix")? .extract()?, file_label: native_field(file, "relative_path")?.str()?.extract()?, @@ -118,7 +117,7 @@ fn check_bodies( /// Reject the first authored test block, then scenario, whose CTE names cannot be read. #[pyfunction] -fn validate_authored_sql_test_ctes( +pub(crate) fn validate_authored_sql_test_ctes( py: Python<'_>, test_files: Bound<'_, PyAny>, scenario_files: Bound<'_, PyAny>, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/compiled_tests.rs b/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/compiled_tests.rs index c7cb9d5ec8..029f6f42a9 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/compiled_tests.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/compiled_tests.rs @@ -5,9 +5,9 @@ use pyo3::types::{PyDict, PyDictMethods, PyString, PyTuple}; use pyo3::{IntoPyObject, pyfunction, wrap_pyfunction}; use crate::bindings::_helpers::boundary::panics::compiler_error; +use crate::bindings::_helpers::records::construct::{enum_member, record_class}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::sql_test_glue::assembly::{ AssemblyRequest, AssemblyRow, DiagnosticRow, FactsRow, assemble_compiled_sql_tests, }; @@ -61,13 +61,13 @@ struct Classes<'py> { impl<'py> Classes<'py> { fn new(py: Python<'py>) -> PyResult { Ok(Self { - direct_input: registry::class(py, RecordId::CompileDirectLogicSqlTestInputPayload)?, - resource_type: registry::class(py, RecordId::CompiledResourceType)?, - phase: registry::class(py, RecordId::DiagnosticPhase)?.getattr("COMPILE")?, - severity: registry::class(py, RecordId::DiagnosticSeverity)?.getattr("ERROR")?, - assembly: registry::class(py, RecordId::NativeSqlTestAssembly)?, - failure: registry::class(py, RecordId::NativeSqlTestFailure)?, - location: registry::class(py, RecordId::SourceLocation)?, + direct_input: record_class(py, RecordId::CompileDirectLogicSqlTestInputPayload)?, + resource_type: record_class(py, RecordId::CompiledResourceType)?, + phase: enum_member(py, RecordId::DiagnosticPhase, "COMPILE")?, + severity: enum_member(py, RecordId::DiagnosticSeverity, "ERROR")?, + assembly: record_class(py, RecordId::NativeSqlTestAssembly)?, + failure: record_class(py, RecordId::NativeSqlTestFailure)?, + location: record_class(py, RecordId::SourceLocation)?, code: PyString::new(py, super::constants::SQL_TEST_HELPER_REFERENCE_CODE).into_any(), }) } @@ -77,7 +77,7 @@ impl<'py> Classes<'py> { } fn model(&self, name: RecordId) -> PyResult> { - registry::class(self.py(), name) + record_class(self.py(), name) } fn object_key(&self, resource_type: &str, name: &str) -> PyResult> { @@ -112,7 +112,7 @@ fn copied<'py>( ) -> PyResult)>> { fields .iter() - .map(|field| Ok((*field, source.getattr(*field)?))) + .map(|field| Ok((*field, native_field(source, field)?))) .collect() } @@ -163,7 +163,7 @@ fn keyed_diagnostic<'py>( ) -> PyResult> { let py: Python<'py> = classes.py(); let path: Bound<'py, PyAny> = - native_field(test_input, "test_file")?.getattr("relative_path")?; + native_field(&native_field(test_input, "test_file")?, "relative_path")?; let key: Bound<'py, PyTuple> = PyTuple::new( py, [ @@ -299,10 +299,11 @@ pub(crate) fn assemble_native_sql_tests<'py>( let request = AssemblyRequest { model_inputs: native_field(inputs, "model_inputs")?.extract()?, test_inputs: test_inputs.clone(), - lexical_syntax: inputs - .getattr("sql_lexical_syntax")? - .getattr("native_mapping")? - .extract()?, + lexical_syntax: native_field( + &native_field(inputs, "sql_lexical_syntax")?, + "native_mapping", + )? + .extract()?, }; let rows: Vec = assemble_compiled_sql_tests(py, request)?; if rows.len() != test_inputs.len() { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/cursor_window.rs b/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/cursor_window.rs index 5245f950ce..6d122f641c 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/cursor_window.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/cursor_window.rs @@ -16,9 +16,9 @@ use crate::bindings::_helpers::compile_driver::python_calls::{call_with, importe use crate::bindings::_helpers::cursor::conversion::CursorSession; use crate::bindings::_helpers::cursor::scalars::{parse_value, render_value}; use crate::bindings::_helpers::model_config::config_values::config_str; +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::ids::RecordId; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::sql_test_glue::constants::{ CURSOR_ALGEBRA_EXCEPTIONS, CURSOR_END_KEY, CURSOR_START_KEY, DEFAULT_INTEGER_END, DEFAULT_INTEGER_START, DEFAULT_TIMESTAMP_END, DEFAULT_TIMESTAMP_START, INTEGER_CURSOR_TYPE, @@ -120,7 +120,7 @@ fn test_window<'py>( let Some(block) = &block else { return Ok(None); }; - let value = block.getattr(name)?; + let value = native_field(block, name)?; Ok((!value.is_none()).then_some(value)) }; let integer = cursor.type_name == INTEGER_CURSOR_TYPE; @@ -238,19 +238,19 @@ fn ordering(left: &Bound<'_, PyAny>, right: &Bound<'_, PyAny>) -> PyResult(model: &Bound<'py, PyAny>) -> PyResult> { let py = model.py(); - let values = native_field(model, "config")?.getattr("values")?; + let values = native_field(&native_field(model, "config")?, "values")?; let declared = config_str(&values, "cursor_type")?.filter(|value| { value .extract::() .is_ok_and(|value| !value.is_empty()) }); - let cursor_type = registry::class(py, RecordId::CursorType)?.call1(( + let cursor_type = record_class(py, RecordId::CursorType)?.call1(( declared.unwrap_or_else(|| PyString::new(py, TIMESTAMP_CURSOR_TYPE).into_any()), ))?; let type_name: String = native_field(&cursor_type, "value")?.extract()?; let grain = match config_str(&values, "cursor_grain")? { Some(grain) if type_name == TIMESTAMP_CURSOR_TYPE => { - let grain = registry::class(py, RecordId::CursorGrain)?.call1((grain,))?; + let grain = enum_from_value(py, RecordId::CursorGrain, grain)?; let session = CursorSession::new(py)?; session .grain(&grain)? @@ -272,8 +272,6 @@ fn test_label(test: &Bound<'_, PyAny>) -> PyResult { Ok(format!( "SQL test '{}' in '{}'", native_field(test, "name")?.str()?, - native_field(test, "test_file")? - .getattr("relative_path")? - .str()? + native_field(&native_field(test, "test_file")?, "relative_path")?.str()? )) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/fixture_validation.rs b/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/fixture_validation.rs index 01856fb93e..4f71a89e3b 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/fixture_validation.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/fixture_validation.rs @@ -16,9 +16,9 @@ use crate::bindings::_helpers::planner_strategy::fixture_sql::{ }; use crate::bindings::_helpers::planner_strategy::models::RelationFixturePlanningContext; use crate::bindings::_helpers::planner_strategy::types::FixtureKey; +use crate::bindings::_helpers::records::construct::record_class; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::sql_test_glue::constants::FIXTURE_PREFIXES; /// Report every statically provable fixture defect or return the completed fixture SQL groups. @@ -31,7 +31,7 @@ pub(super) fn validated_fixtures<'py>( let py = test.py(); let (chain, [refs, sources, seeds, expected]) = request; let adapter = inputs.1; - let resource_types = registry::class(py, RecordId::CompiledResourceType)?; + let resource_types = record_class(py, RecordId::CompiledResourceType)?; let model = native_field(&resource_types, "MODEL")?; let kinds = [ model.clone(), @@ -198,8 +198,7 @@ pub(super) fn fixture_location( .map_err(value_error)?; Ok(format!( "{}:{line}", - test_file - .getattr("relative_path")? + native_field(&test_file, "relative_path")? .call_method0("as_posix")? .extract::()? )) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/plan_assembly.rs b/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/plan_assembly.rs index d2960b60ba..6226495dee 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/plan_assembly.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/plan_assembly.rs @@ -4,12 +4,12 @@ use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python, pyfunction}; use pyo3::types::{PyDict, PyDictMethods, PyTuple}; use crate::bindings::_helpers::analysis::model_reads; -use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; +use crate::bindings::_helpers::compile_driver::python_calls::KeywordArguments; use crate::bindings::_helpers::planner_strategy::fixture_context::planning_context; use crate::bindings::_helpers::planner_strategy::models::RelationFixturePlanningContext; +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; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::sql_test_glue::models::{SqlTestAssembly, SqlTestPlanOptions}; use crate::bindings::_helpers::sql_test_glue::plan_driver::planned as planned_tests; @@ -94,12 +94,12 @@ pub(crate) fn assemble_sql_test_plans<'py>( let model = model?; models.set_item(model_reads::name_object(&model)?, model)?; } - let result_class = registry::class(py, RecordId::SqlTestPlanResult)?; let mut results: Vec> = Vec::new(); for (index, test) in tests.iter().enumerate() { if let Some(diagnostic) = diagnostics.get(&index) { - results.push(call_with( - &result_class, + results.push(new_record_with( + py, + RecordId::SqlTestPlanResult, &[ ("entry", py.None().into_bound(py)), ("fixture_diagnostics", diagnostic.clone()), @@ -119,8 +119,9 @@ pub(crate) fn assemble_sql_test_plans<'py>( context.as_ref().map(Bound::get), )?); if !errors.is_empty() { - results.push(call_with( - &result_class, + results.push(new_record_with( + py, + RecordId::SqlTestPlanResult, &[ ("entry", py.None().into_bound(py)), ("fixture_diagnostics", PyTuple::new(py, errors)?.into_any()), @@ -128,8 +129,9 @@ pub(crate) fn assemble_sql_test_plans<'py>( )?); continue; } - results.push(call_with( - &result_class, + results.push(new_record_with( + py, + RecordId::SqlTestPlanResult, &[ ( "entry", diff --git a/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/plan_driver.rs b/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/plan_driver.rs index 38ee42ab5c..a9f5107290 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/plan_driver.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/plan_driver.rs @@ -1,11 +1,10 @@ //! Host SQL-test request preparation, native planning, and Python result conversion. use crate::bindings::_helpers::compile_driver::compile_timings::add_compile_timing; -use crate::bindings::_helpers::compile_driver::python_calls::{ - KeywordArguments, call_with, imported, -}; +use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, imported}; use crate::bindings::_helpers::planner_phases::objects::object; use crate::bindings::_helpers::planner_strategy::fixture_context::count_chain_resolution; +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::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -28,10 +27,11 @@ pub(super) fn chains<'py>( let request = ChainRequest { models: native_field(project, "models")?.extract()?, tests: tests.extract()?, - lexical_syntax: project - .getattr("sql_lexical_syntax")? - .getattr("native_mapping")? - .extract()?, + lexical_syntax: native_field( + &native_field(project, "sql_lexical_syntax")?, + "native_mapping", + )? + .extract()?, }; let native = resolve_compiled_sql_test_chains(py, request).and_then(|chains| object(py, chains)); @@ -89,10 +89,11 @@ pub(crate) fn planned<'py>( requires_derived_table_aliases: adapter .call_method0("requires_derived_table_aliases")? .extract()?, - lexical_syntax: project - .getattr("sql_lexical_syntax")? - .getattr("native_mapping")? - .extract()?, + lexical_syntax: native_field( + &native_field(project, "sql_lexical_syntax")?, + "native_mapping", + )? + .extract()?, render_sql: artifact || options.render_sql, include_plan: !artifact && options.include_plan, }; @@ -130,8 +131,9 @@ pub(crate) fn planned<'py>( .call1(("native SQL-test planning omitted rendered SQL",))?, )); } - plans.push(call_with( - ®istry::class(py, RecordId::NativeSqlTestArtifact)?, + plans.push(new_record_with( + py, + RecordId::NativeSqlTestArtifact, &[ ("sql", sql), ("model_names", native_field(&plan, "model_names")?), @@ -215,10 +217,7 @@ pub(crate) fn sql_test_plan_error_messages<'py>( for warning in warnings.try_iter()? { let warning = warning?; let message: String = native_field(&warning, "message")?.extract()?; - if warning - .getattr("severity")? - .getattr("value")? - .extract::()? + if native_field(&native_field(&warning, "severity")?, "value")?.extract::()? == super::constants::ERROR_SEVERITY && seen.insert(message.clone()) { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/plan_entries.rs b/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/plan_entries.rs index b59b1d4c69..7bdb0110f8 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/plan_entries.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/plan_entries.rs @@ -5,20 +5,20 @@ use pyo3::types::{PyDict, PyDictMethods, PyListMethods, PyTuple}; use crate::bindings::_helpers::planner_strategy::constants::MODEL_REFERENCE_KIND; use crate::bindings::_helpers::planner_strategy::models::RelationFixturePlanningContext; +use crate::bindings::_helpers::records::construct::{new_record, record_class}; use crate::bindings::_helpers::sql_test_glue::constants::{TABLE_FN, UDF}; use crate::bindings::_helpers::sql_test_glue::plan_values::model_payload; use sqlbuild_analysis::compiler::main::missing_expected_columns::missing_expected_columns; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::sql_test_glue::authored_ctes::prefixed_ctes; fn prefixed_names<'py>(test: &Bound<'py, PyAny>, prefix: &str) -> PyResult> { let py = test.py(); let mut names: Vec = if model_payload(test)? { prefixed_ctes( - &native_field(test, "payload")?.getattr("authored_ctes")?, + &native_field(&native_field(test, "payload")?, "authored_ctes")?, prefix, )? .keys() @@ -49,9 +49,8 @@ fn function_deps<'py>( }; for dep in native_field(&model, "deps")?.try_iter()? { let dep = dep?; - let resource: String = native_field(&dep, "resource_type")? - .getattr("value")? - .extract()?; + let resource: String = + native_field(&native_field(&dep, "resource_type")?, "value")?.extract()?; if resource != UDF && resource != TABLE_FN { continue; } @@ -65,21 +64,17 @@ fn function_deps<'py>( } } else { let payload = native_field(test, "payload")?; - let mode: String = native_field(&payload, "mode")? - .getattr("value")? - .extract()?; + let mode: String = native_field(&native_field(&payload, "mode")?, "value")?.extract()?; if mode == UDF || mode == TABLE_FN { - let names: std::collections::HashSet = payload - .getattr("tested_resource_names")? - .extract::>()? - .into_iter() - .collect(); + let names: std::collections::HashSet = + native_field(&payload, "tested_resource_names")? + .extract::>()? + .into_iter() + .collect(); for function in native_field(project, "functions")?.try_iter()? { let function = function?; let key = native_field(&function, "key")?; - if key - .getattr("resource_type")? - .getattr("value")? + if native_field(&native_field(&key, "resource_type")?, "value")? .extract::()? == mode && names.contains(&native_field(&function, "name")?.extract::()?) @@ -113,7 +108,7 @@ pub(super) fn entry<'py>( "parameter_values", "scope_deps", ] { - fields.set_item(name, test.getattr(name)?)?; + fields.set_item(name, native_field(test, name)?)?; } for (field, prefix) in [ ("mock_ref_names", "__ref__"), @@ -134,7 +129,7 @@ pub(super) fn entry<'py>( fields.set_item("chain", native_field(plan, "chain")?)?; fields.set_item("assertions", native_field(plan, "assertions")?)?; fields.set_item("sql_analysis_enabled", analysis)?; - registry::class(py, RecordId::SqlTestPlanEntry)?.call((), Some(&fields)) + new_record(py, RecordId::SqlTestPlanEntry, (), Some(&fields)) } pub(super) fn expected_diagnostics( @@ -147,7 +142,7 @@ pub(super) fn expected_diagnostics( return Ok(result); }; let py = test.py(); - let resource_types = registry::class(py, RecordId::CompiledResourceType)?; + let resource_types = record_class(py, RecordId::CompiledResourceType)?; for step in native_field(plan, "chain")?.try_iter()? { let step = step?; let name = native_field(&step, "model_name")?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/plan_errors.rs b/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/plan_errors.rs index 04b327563a..3a823683c2 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/plan_errors.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/plan_errors.rs @@ -1,6 +1,7 @@ //! Project native planning failures into existing located diagnostic exceptions. use crate::bindings::_helpers::compile_driver::python_calls::imported; +use crate::bindings::_helpers::records::construct::new_record; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -20,10 +21,7 @@ fn cte_call_location<'py>( let call: Option = call.extract()?; let located: CteLocation = sql_test_cte_location( &native_field(&test_file, "contents")?.extract::()?, - &test - .getattr("test_block")? - .getattr("sql_body")? - .extract::()?, + &native_field(&native_field(test, "test_block")?, "sql_body")?.extract::()?, &cte.extract::()?, call.as_deref(), ); @@ -33,7 +31,7 @@ fn cte_call_location<'py>( arguments.set_item("column", located.column)?; arguments.set_item("end_line", located.end_line)?; arguments.set_item("end_column", located.end_column)?; - registry::class(py, RecordId::SourceLocation)?.call((), Some(&arguments)) + new_record(py, RecordId::SourceLocation, (), Some(&arguments)) } pub(super) fn planning_error( @@ -48,9 +46,7 @@ pub(super) fn planning_error( for test in tests.try_iter()? { let test = test?; if native_field(&test, "name")?.eq(payload.get_item("testName")?)? - && test - .getattr("test_file")? - .getattr("relative_path")? + && native_field(&native_field(&test, "test_file")?, "relative_path")? .str()? .into_any() .eq(payload.get_item("fileLabel")?)? diff --git a/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/plan_fixtures.rs b/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/plan_fixtures.rs index 39a7915d13..8506fa10b6 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/plan_fixtures.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/plan_fixtures.rs @@ -3,9 +3,9 @@ use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; use pyo3::types::{PyDict, PyDictMethods, PyTuple}; -use crate::bindings::_helpers::compile_driver::python_calls::imported; use crate::bindings::_helpers::planner_strategy::models::RelationFixturePlanningContext; use crate::bindings::_helpers::records::fields::field as native_field; +use crate::bindings::_helpers::records::fields::replaced_keywords; use crate::bindings::_helpers::sql_test_glue::authored_ctes::prefixed_ctes; use crate::bindings::_helpers::sql_test_glue::fixture_validation::validated_fixtures; use crate::bindings::_helpers::sql_test_glue::plan_driver::resolve_sql_test_model_chains; @@ -38,7 +38,6 @@ pub(super) fn validated_test<'py>( Ok(completed) => completed, Err(diagnostics) => return Ok(Err(diagnostics)), }; - let replace = imported(py, "dataclasses", "replace")?; let mut authored: Vec> = Vec::new(); for cte in ctes.try_iter()? { let cte = cte?; @@ -50,7 +49,7 @@ pub(super) fn validated_test<'py>( .call_method1("get", (name, native_field(&cte, "sql_body")?))?; let kwargs = PyDict::new(py); kwargs.set_item("sql_body", sql)?; - replacement = replace.call((&cte,), Some(&kwargs))?; + replacement = replaced_keywords(&cte, &kwargs)?; break; } } @@ -66,15 +65,15 @@ pub(super) fn validated_test<'py>( "sql_body", completed[3].call_method1("get", (name, native_field(&cte, "sql_body")?))?, )?; - expected_ctes.push(replace.call((&cte,), Some(&kwargs))?); + expected_ctes.push(replaced_keywords(&cte, &kwargs)?); } let kwargs = PyDict::new(py); kwargs.set_item("authored_ctes", PyTuple::new(py, authored)?)?; kwargs.set_item("expected_ctes", PyTuple::new(py, expected_ctes)?)?; - let payload = replace.call((payload,), Some(&kwargs))?; + let payload = replaced_keywords(&payload, &kwargs)?; let kwargs = PyDict::new(py); kwargs.set_item("payload", payload)?; - replace.call((test,), Some(&kwargs)).map(Ok) + replaced_keywords(test, &kwargs).map(Ok) } pub(super) fn validate<'py>( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/plan_rendering.rs b/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/plan_rendering.rs index 2d264c9324..33632f4654 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/plan_rendering.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/plan_rendering.rs @@ -76,9 +76,7 @@ pub(super) fn overrides<'py>( if !model_payload(test)? { continue; } - let overrides = test - .getattr("payload")? - .getattr("model_query_overrides")? + let overrides = native_field(&native_field(test, "payload")?, "model_query_overrides")? .cast_into::()?; let chain = chains.get(&index); if chain.is_none() @@ -145,7 +143,7 @@ pub(super) fn functions<'py>( let mut result: Vec> = Vec::new(); for function in native_field(project, "functions")?.try_iter()? { let function = function?; - let target = native_field(&function, "destination")?.getattr("qualified_name")?; + let target = native_field(&native_field(&function, "destination")?, "qualified_name")?; if target.is_none() { continue; } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/plan_rows.rs b/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/plan_rows.rs index a4791201f0..51dac5c47a 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/plan_rows.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/plan_rows.rs @@ -1,17 +1,17 @@ //! Materialize native plan rows as the existing executor-facing dataclasses. -use crate::bindings::_helpers::compile_driver::python_calls::call_with; +use crate::bindings::_helpers::records::construct::{enum_from_value, new_record_with}; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::sql_test_glue::planning::{AssertionRow, ChainRow, PlanRow}; use pyo3::IntoPyObjectExt; -use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; +use pyo3::prelude::{Bound, PyAny, PyResult, Python}; use pyo3::types::PyTuple; fn chain_step<'py>(py: Python<'py>, row: ChainRow) -> PyResult> { let (model, sql, expected_sql, lifted, body, expected_columns, expected_lifted) = row; - call_with( - ®istry::class(py, RecordId::ChainStep)?, + new_record_with( + py, + RecordId::ChainStep, &[ ("model_name", model.into_bound_py_any(py)?), ("resolved_sql", sql.into_bound_py_any(py)?), @@ -35,8 +35,9 @@ fn chain_step<'py>(py: Python<'py>, row: ChainRow) -> PyResult fn assertion_step<'py>(py: Python<'py>, row: AssertionRow) -> PyResult> { let (name, sql, lifted, body) = row; - call_with( - ®istry::class(py, RecordId::SqlTestAssertionStep)?, + new_record_with( + py, + RecordId::SqlTestAssertionStep, &[ ("name", name.into_bound_py_any(py)?), ("resolved_sql", sql.into_bound_py_any(py)?), @@ -57,19 +58,23 @@ pub(super) fn plan(py: Python<'_>, row: PlanRow) -> PyResult> { assertions.push(assertion_step(py, value)?); } let mut warnings: Vec> = Vec::new(); - let severity = registry::class(py, RecordId::WarningSeverity)?; for (model_name, level, message) in warning_rows { - warnings.push(call_with( - ®istry::class(py, RecordId::PlanWarning)?, + warnings.push(new_record_with( + py, + RecordId::PlanWarning, &[ ("model_name", model_name.into_bound_py_any(py)?), - ("severity", severity.call1((level,))?), + ( + "severity", + enum_from_value(py, RecordId::WarningSeverity, level)?, + ), ("message", message.into_bound_py_any(py)?), ], )?); } - call_with( - ®istry::class(py, RecordId::NativeSqlTestPlan)?, + new_record_with( + py, + RecordId::NativeSqlTestPlan, &[ ("chain", PyTuple::new(py, chain)?.into_any()), ("assertions", PyTuple::new(py, assertions)?.into_any()), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/plan_values.rs b/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/plan_values.rs index 06cca996b6..54dbc3b4d6 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/plan_values.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/plan_values.rs @@ -4,16 +4,16 @@ use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; use pyo3::types::PyString; use crate::bindings::_helpers::compile_driver::python_calls::imported; +use crate::bindings::_helpers::records::construct::record_class; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; pub(super) fn string<'py>(py: Python<'py>, value: &str) -> Bound<'py, PyAny> { PyString::new(py, value).into_any() } pub(super) fn model_payload(test: &Bound<'_, PyAny>) -> PyResult { - native_field(test, "payload")?.is_instance(®istry::class( + native_field(test, "payload")?.is_instance(&record_class( test.py(), RecordId::CompiledModelSqlTestPayload, )?) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/planning.rs b/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/planning.rs index 7dd187e01d..82d8ff5e76 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/planning.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/sql_test_glue/planning.rs @@ -161,10 +161,7 @@ fn model_dependencies(model: &Bound<'_, PyAny>) -> PyResult> { let mut names: Vec = Vec::new(); for dependency in native_field(model, "deps")?.try_iter()? { let dependency = dependency?; - if dependency - .getattr("resource_type")? - .eq(MODEL_RESOURCE_TYPE)? - { + if native_field(&dependency, "resource_type")?.eq(MODEL_RESOURCE_TYPE)? { names.push(native_field(&dependency, "name")?.extract()?); } } @@ -219,9 +216,7 @@ fn plan_test( }; Ok(SqlTestPlanTest { name: native_field(test, "name")?.extract()?, - file_label: test - .getattr("test_file")? - .getattr("relative_path")? + file_label: native_field(&native_field(test, "test_file")?, "relative_path")? .str()? .extract()?, payload, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/sqltext/authored_values.rs b/crates/sqlbuild-python/src/bindings/_helpers/sqltext/authored_values.rs index 6a7f5ed88e..1be9e9d02e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/sqltext/authored_values.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/sqltext/authored_values.rs @@ -1,7 +1,7 @@ //! Authored header values as the Python header objects: scalars, sets, constants and hooks. +use crate::bindings::_helpers::records::construct::new_record; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use pyo3::prelude::{IntoPyObject, Py, PyAny, PyAnyMethods, PyResult, Python}; use pyo3::types::{PyDict, PyDictMethods, PyList, PyTuple}; use sqlbuild_sqltext::compiler::main::header_word_value::header_word_value; @@ -29,9 +29,7 @@ fn instance( class: RecordId, arguments: &pyo3::Bound<'_, PyDict>, ) -> PyResult> { - Ok(registry::class(py, class)? - .call((), Some(arguments))? - .unbind()) + Ok(new_record(py, class, (), Some(arguments))?.unbind()) } fn values_tuple( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/sqltext/sql_references.rs b/crates/sqlbuild-python/src/bindings/_helpers/sqltext/sql_references.rs index 744ebb6b0b..edba303dd2 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/sqltext/sql_references.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/sqltext/sql_references.rs @@ -3,7 +3,7 @@ use pyo3::prelude::{ Bound, PyAny, PyAnyMethods, PyErr, PyModule, PyModuleMethods, PyResult, Python, }; -use pyo3::types::{PyDict, PyDictMethods, PyTuple}; +use pyo3::types::PyTuple; use pyo3::{IntoPyObject, IntoPyObjectExt, pyclass, pyfunction, pymethods, wrap_pyfunction}; use sqlbuild_sqltext::sql_references::main::extract_sql_references::extract_sql_references; use sqlbuild_sqltext::sql_references::models::{ @@ -12,9 +12,12 @@ use sqlbuild_sqltext::sql_references::models::{ use sqlbuild_sqltext::sql_scan::models::LexicalSyntax; use crate::bindings::_helpers::boundary::panics::compiler_guard; -use crate::bindings::_helpers::compile_driver::python_calls::call_with; +use crate::bindings::_helpers::compile_driver::diagnostic_collector::report_compile_diagnostic; +use crate::bindings::_helpers::records::construct::{ + enum_from_value, enum_member, new_record_with, +}; use crate::bindings::_helpers::records::fields::field as native_field; -use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr, RecordId}; +use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::sqltext::lexical_syntax::LexicalSyntaxInput; @@ -109,17 +112,18 @@ fn scanned<'py>( return Err(PyErr::from_value(error)); } }; - let kind_type: Bound<'py, PyAny> = registry::class(py, RecordId::SqlReferenceKind)?; - let reference_type: Bound<'py, PyAny> = registry::class(py, RecordId::CompileSqlReference)?; - let invalid_type: Bound<'py, PyAny> = registry::class(py, RecordId::InvalidSqlReferenceCall)?; let references: Vec> = scan .references .into_iter() .map(|reference| { - call_with( - &reference_type, + new_record_with( + py, + RecordId::CompileSqlReference, &[ - ("ref_kind", kind_type.call1((reference.kind,))?), + ( + "ref_kind", + enum_from_value(py, RecordId::SqlReferenceKind, reference.kind)?, + ), ("ref_name", text(py, reference.name)?), ("ref_package", text(py, reference.package)?), ( @@ -134,10 +138,14 @@ fn scanned<'py>( .invalid_calls .into_iter() .map(|call| { - call_with( - &invalid_type, + new_record_with( + py, + RecordId::InvalidSqlReferenceCall, &[ - ("ref_kind", kind_type.call1((call.kind,))?), + ( + "ref_kind", + enum_from_value(py, RecordId::SqlReferenceKind, call.kind)?, + ), ("call", text(py, call.call)?), ("start", text(py, call.start)?), ("message", text(py, call.message)?), @@ -159,8 +167,9 @@ fn scan_sql_reference_calls<'py>( ) -> PyResult> { let py: Python<'py> = syntax.py(); let (references, invalid_calls) = scanned(py, sql, syntax)?; - call_with( - ®istry::class(py, RecordId::SqlReferenceScan)?, + new_record_with( + py, + RecordId::SqlReferenceScan, &[ ("references", PyTuple::new(py, references)?.into_any()), ("invalid_calls", PyTuple::new(py, invalid_calls)?.into_any()), @@ -177,32 +186,30 @@ pub(crate) fn extract_references<'py>( ) -> PyResult> { let py: Python<'py> = syntax.py(); let (references, invalid_calls) = scanned(py, sql, syntax)?; - let report: Bound<'py, PyAny> = registry::native(py, NativeAttr::ReportCompileDiagnostic)?; - let diagnostic_type: Bound<'py, PyAny> = registry::class(py, RecordId::CompilerDiagnostic)?; for call in invalid_calls { - let diagnostic: Bound<'py, PyAny> = call_with( - &diagnostic_type, + let diagnostic: Bound<'py, PyAny> = new_record_with( + py, + RecordId::CompilerDiagnostic, &[ ( "phase", - registry::class(py, RecordId::DiagnosticPhase)?.getattr("COMPILE")?, + enum_member(py, RecordId::DiagnosticPhase, "COMPILE")?, ), ( "severity", - registry::class(py, RecordId::DiagnosticSeverity)?.getattr("ERROR")?, + enum_member(py, RecordId::DiagnosticSeverity, "ERROR")?, ), ("code", text(py, REFERENCE_CALL_SYNTAX_CODE)?), ("message", native_field(&call, "message")?), ("help", native_field(&call, "help")?), ], )?; - let keywords: Bound<'py, PyDict> = PyDict::new(py); - keywords.set_item( - "key", - (REFERENCE_CALL_SYNTAX_CODE, "", native_field(&call, "call")?), + report_compile_diagnostic( + (REFERENCE_CALL_SYNTAX_CODE, "", native_field(&call, "call")?) + .into_pyobject(py)? + .as_any(), + &diagnostic, )?; - keywords.set_item("diagnostic", diagnostic)?; - report.call((), Some(&keywords))?; } PyTuple::new(py, references) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/state_rows/freshness.rs b/crates/sqlbuild-python/src/bindings/_helpers/state_rows/freshness.rs index 372caafccd..225750664c 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/state_rows/freshness.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/state_rows/freshness.rs @@ -19,6 +19,7 @@ use sqlbuild_state::freshness::types::TimestampParser; use crate::bindings::_helpers::compile_driver::python_calls::imported; use crate::bindings::_helpers::cursor::values::{DATETIME_MODULE, local_date_time}; +use crate::bindings::_helpers::records::construct::{enum_member, record_class}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; @@ -213,7 +214,7 @@ pub(crate) fn age_policy<'py>( if policy.is_none() { return Ok(None); } - let statuses: Bound<'py, PyAny> = registry::class(py, RecordId::SourceFreshnessAgeStatus)?; + let statuses: Bound<'py, PyAny> = record_class(py, RecordId::SourceFreshnessAgeStatus)?; if !date_time_typed(data_version)? { return native_field(&statuses, "UNKNOWN").map(Some); } @@ -232,7 +233,7 @@ pub(crate) fn age_policy<'py>( AgeStatus::Warn => "WARN", AgeStatus::Error => "ERROR", }; - statuses.getattr(member).map(Some) + enum_member(py, RecordId::SourceFreshnessAgeStatus, member).map(Some) } pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/state_rows/migration_io.rs b/crates/sqlbuild-python/src/bindings/_helpers/state_rows/migration_io.rs index 1efd78978c..681fb831cd 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/state_rows/migration_io.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/state_rows/migration_io.rs @@ -116,7 +116,7 @@ fn subject(table: EventTable, event: &Bound<'_, PyAny>) -> PyResult { EventTable::OldName => format!( "{} old name {}", text(native_field(event, "destination_model")?)?, - text(native_field(event, "old")?.getattr("name")?)? + text(native_field(&native_field(event, "old")?, "name")?)? ), }) } @@ -268,9 +268,7 @@ fn read_renames<'py>(request: Option<&Bound<'py, PyDict>>) -> PyResult( } fn step(fact: &Bound<'_, PyAny>) -> PyResult { - let value: String = native_field(fact, "event_type")? - .getattr("value")? - .extract()?; + let value: String = native_field(&native_field(fact, "event_type")?, "value")?.extract()?; match value.as_str() { "required" => Ok(OldNameStep::Required), "origin_archived" => Ok(OldNameStep::OriginArchived), @@ -111,13 +108,13 @@ pub(crate) fn old_name_views<'py>( }) }) .collect::>()?; - let history = registry::class(py, RecordId::OldNameViewHistory)?; let fact_at = |index: Option| chosen(py, &facts, index); let histories: Vec> = old_name_histories(&move_orders, &fact_keys) .into_iter() .map(|indexes| { - call_with( - &history, + new_record_with( + py, + RecordId::OldNameViewHistory, &[ ("move", moves[indexes.moved].clone()), ("required", facts[indexes.required].clone()), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/state_rows/migration_rows.rs b/crates/sqlbuild-python/src/bindings/_helpers/state_rows/migration_rows.rs index 7576ecfba1..1074c0797e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/state_rows/migration_rows.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/state_rows/migration_rows.rs @@ -16,11 +16,11 @@ use sqlbuild_state::events::main::grants_json::grants_json; use sqlbuild_state::events::main::readable_columns::readable_columns; use crate::bindings::_helpers::boundary::keyword_request::Request; -use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; +use crate::bindings::_helpers::compile_driver::python_calls::imported; use crate::bindings::_helpers::cursor::values::DATETIME_MODULE; +use crate::bindings::_helpers::records::construct::{enum_member, new_record, new_record_with}; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::state_rows::events::typed_insert_sql; use crate::bindings::_helpers::state_rows::migration_objects::{ layout, qualified_table, state_error, @@ -41,7 +41,6 @@ fn create_sql<'py>(request: Option<&Bound<'py, PyDict>>) -> PyResult = registry::class(py, RecordId::StateSqlValueType)?; let column_types: Bound<'py, PyDict> = PyDict::new(py); for column in layout.columns { let member: &str = if TIMESTAMP_COLUMNS.contains(column) { @@ -49,7 +48,10 @@ fn create_sql<'py>(request: Option<&Bound<'py, PyDict>>) -> PyResult = PyTuple::new(py, layout.columns)?.into_any(); let sql: String = state_table_create_sql( @@ -86,7 +88,7 @@ fn optional<'py>(value: Bound<'py, PyAny>, attribute: &str) -> PyResult( event: &Bound<'py, PyAny>, ) -> PyResult>> { let py: Python<'py> = event.py(); - let field = |name: &str| event.getattr(name); + let field = |name: &str| native_field(event, name); let relation = |name: &str| -> PyResult<[Bound<'py, PyAny>; 3]> { - let relation: Bound<'py, PyAny> = event.getattr(name)?; + let relation: Bound<'py, PyAny> = native_field(event, name)?; Ok([ native_field(&relation, "database")?, native_field(&relation, "schema")?, @@ -127,7 +129,7 @@ fn row_values<'py>( values.push(field("destination_column")?); } EventTable::OldName => { - values.push(field("event_type")?.getattr("value")?); + values.push(native_field(&field("event_type")?, "value")?); values.push(field("migration_event_id")?); values.push(field("destination_model")?); values.extend(relation("old")?); @@ -150,8 +152,8 @@ fn row_values<'py>( return Ok(values); } } - values.push(field("discovery")?.getattr("value")?); - values.push(field("decision")?.getattr("value")?); + values.push(native_field(&field("discovery")?, "value")?); + values.push(native_field(&field("decision")?, "value")?); values.push(field("run_id")?); values.push(field("created_at")?); Ok(values) @@ -222,8 +224,9 @@ impl<'py> StoredRow<'_, 'py> { } fn relation(&self, py: Python<'py>, prefix: &str) -> PyResult> { - call_with( - ®istry::class(py, RecordId::MigrationRelation)?, + new_record_with( + py, + RecordId::MigrationRelation, &[ ( "database", @@ -257,7 +260,7 @@ fn enumerated<'py>( class: RecordId, value: Bound<'py, PyAny>, ) -> PyResult> { - registry::class(py, class)?.call1((value,)) + new_record(py, class, (value,), None) } fn known_member<'py>( @@ -280,8 +283,9 @@ fn model_event<'py>(py: Python<'py>, row: &StoredRow<'_, 'py>) -> PyResult(py: Python<'py>, row: &StoredRow<'_, 'py>) -> PyResult(py: Python<'py>, row: &StoredRow<'_, 'py>) -> PyResult> { - call_with( - ®istry::class(py, RecordId::ColumnMigrationEvent)?, + new_record_with( + py, + RecordId::ColumnMigrationEvent, &[ ("event_id", row.text("event_id")?), ("target_name", row.nullable(py, "target_name")?), @@ -379,7 +384,7 @@ fn old_name_event<'py>( ("grants_copied", grants), ("view_sql", row.nullable(py, "view_sql")?), ]; - call_with(®istry::class(py, RecordId::OldNameViewEvent)?, &fields).map(Some) + new_record_with(py, RecordId::OldNameViewEvent, &fields).map(Some) } /// Stored rows of the selected `columns` as events; rows this version cannot read are skipped. diff --git a/crates/sqlbuild-python/src/bindings/_helpers/state_rows/state_tables.rs b/crates/sqlbuild-python/src/bindings/_helpers/state_rows/state_tables.rs index 90a55d550b..eb11cae7ff 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/state_rows/state_tables.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/state_rows/state_tables.rs @@ -19,6 +19,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::call_with; use crate::bindings::_helpers::contracts::state_literals::render_state_sql_literal; use crate::bindings::_helpers::native_host::adapter::AdapterHost; use crate::bindings::_helpers::native_host::host; +use crate::bindings::_helpers::records::fields::field as native_field; const MISSING_SCHEMA_MESSAGE: &str = "source freshness table requires a target schema"; /// Each state value type a state table may declare and the framework type that renders it. @@ -43,7 +44,7 @@ pub(crate) fn state_table_create_sql<'py>( let mut rendered: Vec<(&str, String)> = Vec::with_capacity(DECLARED_TYPES.len()); for (declared, framework) in DECLARED_TYPES { let text: String = render_framework_type - .call1((framework_types.getattr(framework)?,))? + .call1((native_field(&framework_types, framework)?,))? .extract()?; rendered.push((declared, text)); } @@ -123,7 +124,7 @@ fn render_insert_source_freshness_records_sql<'py>( let row: Vec = SOURCE_FRESHNESS_COLUMN_TYPES .iter() .map(|(column, declared)| { - render_state_sql_literal(py, &record.getattr(*column)?, declared) + render_state_sql_literal(py, &native_field(&record, column)?, declared) }) .collect::>()?; rows.push(row); diff --git a/crates/sqlbuild-python/src/bindings/_helpers/type_system/nullability.rs b/crates/sqlbuild-python/src/bindings/_helpers/type_system/nullability.rs index 7a48d910e9..3d72270e91 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/type_system/nullability.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/type_system/nullability.rs @@ -6,14 +6,14 @@ use pyo3::{pyfunction, wrap_pyfunction}; use sqlbuild_analysis::nullability_rules::main::conditional_result_nullability::conditional_result_nullability; use sqlbuild_analysis::nullability_rules::main::safe_cast_nullability::safe_cast_nullability; +use crate::bindings::_helpers::records::construct::{enum_from_value, enum_member}; use crate::bindings::_helpers::records::ids::RecordId; -use crate::bindings::_helpers::records::registry; const UNKNOWN_MEMBER: &str = "UNKNOWN"; /// The `InferredNullability` member whose value is `value`. fn member<'py>(args: &Bound<'py, PyAny>, value: &str) -> PyResult> { - registry::class(args.py(), RecordId::InferredNullability)?.call1((value,)) + enum_from_value(args.py(), RecordId::InferredNullability, value) } /// The text of one argument; an argument that is not a string matches no state. @@ -35,7 +35,7 @@ fn argument_values(args: &Bound<'_, PyAny>, count: usize) -> PyResult(args: &Bound<'py, PyAny>) -> PyResult> { if !args.is_truthy()? { - return registry::class(args.py(), RecordId::InferredNullability)?.getattr(UNKNOWN_MEMBER); + return enum_member(args.py(), RecordId::InferredNullability, UNKNOWN_MEMBER); } args.get_item(0) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/workspace_commands/commands.rs b/crates/sqlbuild-python/src/bindings/_helpers/workspace_commands/commands.rs index a3b11eb88b..a097a9b844 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/workspace_commands/commands.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/workspace_commands/commands.rs @@ -23,6 +23,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, use crate::bindings::_helpers::native_host::facade::FacadeHost; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::presentation_text::terminal::stdout_supports_color; +use crate::bindings::_helpers::records::construct::new_record; use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -100,7 +101,7 @@ fn skill_request(py: Python<'_>, project_dir: PathBuf) -> PyResult( +pub(crate) fn update_sqlbuild_skills<'py>( py: Python<'py>, options: Option>, ) -> PyResult> { @@ -120,18 +121,21 @@ fn update_sqlbuild_skills<'py>( let written: Vec = install(py, &request)?; let keywords: Bound<'py, PyDict> = PyDict::new(py); keywords.set_item("written_paths", PyTuple::new(py, written)?)?; - registry::class(py, RecordId::SkillUpdateResult)?.call((), Some(&keywords)) + new_record(py, RecordId::SkillUpdateResult, (), Some(&keywords)) } /// Check or refresh generated project skills without changing command success. #[pyfunction] #[pyo3(signature = (*, project_dir))] -fn maintain_sqlbuild_skills(py: Python<'_>, project_dir: PathBuf) -> PyResult> { +pub(crate) fn maintain_sqlbuild_skills( + py: Python<'_>, + project_dir: PathBuf, +) -> PyResult> { let message: String = maintain_skills(&project_dir, &package_dir(py)?.join(SKILLS_DIRECTORY)) .map_err(|error| raised(py, error))?; let keywords: Bound<'_, PyDict> = PyDict::new(py); keywords.set_item("message", message)?; - registry::class(py, RecordId::SkillMaintenanceResult)?.call((), Some(&keywords)) + new_record(py, RecordId::SkillMaintenanceResult, (), Some(&keywords)) } /// Return every packaged skill file keyed by skill-relative path, with generated markers. @@ -150,7 +154,10 @@ fn print(py: Python<'_>, text: &str) -> PyResult<()> { /// `sqb skills`: install or update SQLBuild skill files. #[pyfunction] #[pyo3(signature = (**options))] -fn run_skills_command(py: Python<'_>, options: Option>) -> PyResult { +pub(crate) fn run_skills_command( + py: Python<'_>, + options: Option>, +) -> PyResult { let arguments: KeywordArguments<'_> = KeywordArguments::read(py, "run_skills_command", options, SKILLS_OPTIONS)?; let project_dir: Bound<'_, PyAny> = arguments.value("project_dir")?; @@ -176,7 +183,7 @@ fn run_skills_command(py: Python<'_>, options: Option>) -> PyR /// `sqb playground`: create a self-contained playground project. #[pyfunction] -fn run_playground_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { +pub(crate) fn run_playground_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { let project_dir: Bound<'_, PyAny> = native_field(request, "project_dir")?; let base: PathBuf = if project_dir.is_none() { current_dir(py)? diff --git a/crates/sqlbuild-python/src/bindings/main/register.rs b/crates/sqlbuild-python/src/bindings/main/register.rs index 1643c96c6d..29aaf50705 100644 --- a/crates/sqlbuild-python/src/bindings/main/register.rs +++ b/crates/sqlbuild-python/src/bindings/main/register.rs @@ -18,6 +18,7 @@ use crate::bindings::models; pub fn _native(module: &Bound<'_, PyModule>) -> PyResult<()> { records::registration::register(module)?; records::registry::verify_tables()?; + records::construct::capture(module)?; native_host::registration::register(module)?; rules::registration::register(module)?; cli::registration::register(module)?; diff --git a/src/sqlbuild/_native.pyi b/src/sqlbuild/_native.pyi index 8cbcaa2e99..f6103ffdbe 100644 --- a/src/sqlbuild/_native.pyi +++ b/src/sqlbuild/_native.pyi @@ -5196,6 +5196,7 @@ def map_expanded_offset( def infer_fixture_column_facts( *, query_sql: str, inference_profile: ExpressionInferenceProfile ) -> FixtureColumnInference | None: ... +def fixture_inference_count(*, reset: bool = False) -> int: ... def find_macro_call_names(sql: str, /) -> tuple[str, ...]: ... def build_native_declaration_contexts( *, diff --git a/tests/e2e/src/sqlbuild/cli/commands/main/compile/helpers.py b/tests/e2e/src/sqlbuild/cli/commands/main/compile/helpers.py index 71b02a24a6..f56f722feb 100644 --- a/tests/e2e/src/sqlbuild/cli/commands/main/compile/helpers.py +++ b/tests/e2e/src/sqlbuild/cli/commands/main/compile/helpers.py @@ -4805,7 +4805,6 @@ def full_macro_call_runs( _ANALYSIS_SEAMS: tuple[tuple[ModuleType, str], ...] = ( - (native_module, "infer_native_expression_source_shapes"), (plan_artifacts_module, "plan_compiled_sql_test_artifacts"), ) diff --git a/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_native_analysis_seams.py b/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_native_analysis_seams.py index 64e5273d6b..331521fd5d 100644 --- a/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_native_analysis_seams.py +++ b/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_native_analysis_seams.py @@ -26,7 +26,6 @@ NativeAnalysisSeamTestCase( description="models_contracts_lineage_and_sql_tests", expected_native_returns={ - "infer_native_expression_source_shapes": [ANY, ANY, ANY], "plan_compiled_sql_test_artifacts": [ANY], }, expected_stage_work={ @@ -62,7 +61,6 @@ def test_given_project_when_compiling_then_native_seams_answer( stage: stage_work.get(stage, 0) for stage in test_case.expected_stage_work } == test_case.expected_stage_work for seams in (native_seams,): - assert isinstance(seams["infer_native_expression_source_shapes"][0], tuple) planned_artifacts: object = seams["plan_compiled_sql_test_artifacts"][0] assert isinstance(planned_artifacts, tuple) assert planned_artifacts diff --git a/tests/integration/src/sqlbuild/cli/commands/main/helpers.py b/tests/integration/src/sqlbuild/cli/commands/main/helpers.py index 09311ab352..d223da16ea 100644 --- a/tests/integration/src/sqlbuild/cli/commands/main/helpers.py +++ b/tests/integration/src/sqlbuild/cli/commands/main/helpers.py @@ -19,7 +19,6 @@ from sqlbuild._native import ( CompilerStage, CustomHostObserver, - NativeQueryColumns, NativeSqlTestArtifact, NativeSqlTestPlanningError, PlannedStaticSqlTests, @@ -671,24 +670,10 @@ def write_expression_source_project( (project_dir / "models" / "orders.sql").write_text(model_sql, encoding="utf-8") -def record_source_rebinding_analyses(monkeypatch: pytest.MonkeyPatch) -> list[str]: - """Record every model query analysed again against inspected source columns.""" +def source_rebinding_analyses() -> int: + """Model queries analysed again against inspected source columns since the last reset.""" - analysed: list[str] = [] - original: Callable[..., tuple[NativeQueryColumns, ...]] = ( - native_module.infer_native_query_columns - ) - - def recording(**kwargs: Any) -> tuple[NativeQueryColumns, ...]: - analysed.extend(query.sql for query in filter(_reanalysis, kwargs["queries"])) - return original(**kwargs) - - monkeypatch.setattr(native_module, "infer_native_query_columns", recording) - return analysed - - -def _reanalysis(query: Any) -> bool: - return query.mode == "reanalysis" + return native_module.compile_stage_work_info().get("source_rebinding_analyses", 0) def eager_output_column_scans(*, capture_dir: Path) -> tuple[int, int]: diff --git a/tests/integration/src/sqlbuild/cli/commands/main/test_source_rebinding.py b/tests/integration/src/sqlbuild/cli/commands/main/test_source_rebinding.py index 9236a8a81a..3be6b65600 100644 --- a/tests/integration/src/sqlbuild/cli/commands/main/test_source_rebinding.py +++ b/tests/integration/src/sqlbuild/cli/commands/main/test_source_rebinding.py @@ -6,11 +6,12 @@ import pytest +import sqlbuild._native as native from sqlbuild._native import run_cli as main from tests.integration.src.sqlbuild.cli.commands.main._test_types import SourceRebindingTestCase from tests.integration.src.sqlbuild.cli.commands.main.helpers import ( execute_duckdb_sql, - record_source_rebinding_analyses, + source_rebinding_analyses, write_expression_source_project, ) @@ -45,7 +46,6 @@ def test_given_expression_source_when_planning_then_rebinds_only_on_new_column_e test_case: SourceRebindingTestCase, tmp_path: Path, capsys: pytest.CaptureFixture[str], - monkeypatch: pytest.MonkeyPatch, ) -> None: execute_duckdb_sql( db_path=tmp_path / "orders.duckdb", @@ -56,7 +56,7 @@ def test_given_expression_source_when_planning_then_rebinds_only_on_new_column_e source_expression=test_case.source_expression, model_sql=test_case.model_sql, ) - analysed: list[str] = record_source_rebinding_analyses(monkeypatch) + _ = native.compile_stage_work_info(reset=True) _ = capsys.readouterr() exit_code: int = main(["--project-dir", str(tmp_path), "--no-color", "plan"]) @@ -64,7 +64,7 @@ def test_given_expression_source_when_planning_then_rebinds_only_on_new_column_e output: str = "".join(capsys.readouterr()) assert exit_code == test_case.expected_exit_code, output assert test_case.expected_fragment in output - assert bool(analysed) is test_case.expected_rebinding + assert (source_rebinding_analyses() > 0) is test_case.expected_rebinding if __name__ == "__main__": diff --git a/tests/integration/src/sqlbuild/compiler/pipeline/test_semantic_triage.py b/tests/integration/src/sqlbuild/compiler/pipeline/test_semantic_triage.py index 298e7d7a45..25e8701f84 100644 --- a/tests/integration/src/sqlbuild/compiler/pipeline/test_semantic_triage.py +++ b/tests/integration/src/sqlbuild/compiler/pipeline/test_semantic_triage.py @@ -3,7 +3,6 @@ import json from pathlib import Path from typing import Any -from unittest.mock import Mock import pytest @@ -211,7 +210,6 @@ def test_given_cached_validation_when_warm_or_schema_changed_then_reuses_only_ma test_case: SemanticTriageCase, tmp_path: Path, capsys: pytest.CaptureFixture[str], - monkeypatch: pytest.MonkeyPatch, ) -> None: (tmp_path / "sqlbuild_project.toml").write_text( 'name = "orders"\nadapter = "duckdb"\n[rules]\nselect = []\n' @@ -225,22 +223,15 @@ def test_given_cached_validation_when_warm_or_schema_changed_then_reuses_only_ma 'MODEL (description "Test model totals.", materialized view);\nSELECT quantity + 1 AS total FROM __source("orders")' ) args: list[str] = ["--project-dir", str(tmp_path), "compile", "--json"] - with monkeypatch.context() as patch: - repeated_validation: Mock = Mock( - side_effect=AssertionError("cold compile repeated fused validation") - ) - patch.setattr(native, "schema_validations", repeated_validation) - assert main(args) == 0 - repeated_validation.assert_not_called() + _ = native.compile_stage_work_info(reset=True) + assert main(args) == 0 + cold_work: dict[str, int] = native.compile_stage_work_info(reset=True) + assert cold_work.get("schema_revalidations", 0) == 0 + assert cold_work["complete_semantic_project"] == 1 capsys.readouterr() - with monkeypatch.context() as patch: - forbidden: Mock = Mock( - side_effect=AssertionError("warm compile repeated native semantic work") - ) - patch.setattr(native, "schema_validations", forbidden) - patch.setattr(native, "analyze_project_queries_compact_json", forbidden) - assert main(args) == 0 - forbidden.assert_not_called() + assert main(args) == 0 + warm_work: dict[str, int] = native.compile_stage_work_info(reset=True) + assert warm_work.get("schema_revalidations", 0) == 0 capsys.readouterr() source.write_text(declaration.replace("INTEGER", "TIMESTAMP")) assert main(args) == 1 diff --git a/tests/integration/src/sqlbuild/rule_engine/host_payload/_test_types.py b/tests/integration/src/sqlbuild/rule_engine/host_payload/_test_types.py index 63224ceda7..f3e7a28947 100644 --- a/tests/integration/src/sqlbuild/rule_engine/host_payload/_test_types.py +++ b/tests/integration/src/sqlbuild/rule_engine/host_payload/_test_types.py @@ -5,4 +5,5 @@ class HostPayloadFactsTestCase: description: str project_files: dict[str, str] - expected_unpopulated_trimmed_fields: frozenset[str] + expected_dropped_project_fields: frozenset[str] + expected_dropped_model_fields: frozenset[str] diff --git a/tests/integration/src/sqlbuild/rule_engine/host_payload/helpers.py b/tests/integration/src/sqlbuild/rule_engine/host_payload/helpers.py index 67ba931bbd..a4d204b5da 100644 --- a/tests/integration/src/sqlbuild/rule_engine/host_payload/helpers.py +++ b/tests/integration/src/sqlbuild/rule_engine/host_payload/helpers.py @@ -1,15 +1,8 @@ import dataclasses -from collections.abc import Callable from itertools import product from pathlib import Path -from typing import Any -import pytest - -import sqlbuild._native as compile_models from sqlbuild._native import ( - CompiledModel, - CompiledProject, Model, build_compiled_project, custom_rule_fact_digest, @@ -160,26 +153,3 @@ def fact_digests(*, project: CompiledProjectView, root: Path) -> dict[FactKey, s key: custom_rule_fact_digest(project=project, project_dir=root, key=key) for key in every_fact_key(project) } - - -def record_trimmed_fields( - monkeypatch: pytest.MonkeyPatch, -) -> dict[type[object], set[str]]: - """Record replaced fields and omitted constructor fields while the host payload is trimmed.""" - - trimmed: dict[type[object], set[str]] = {CompiledProject: set(), CompiledModel: set()} - replace: Callable[..., Any] = dataclasses.replace - - def recording_replace(obj: Any, /, **changes: object) -> Any: - trimmed.setdefault(type(obj), set()).update(changes) - return replace(obj, **changes) - - def recording_model(**values: Any) -> CompiledModel: - trimmed[CompiledModel].update( - {field.name for field in dataclasses.fields(CompiledModel)} - values.keys() - ) - return CompiledModel(**values) - - monkeypatch.setattr(dataclasses, "replace", recording_replace) - monkeypatch.setattr(compile_models, "CompiledModel", recording_model) - return trimmed diff --git a/tests/integration/src/sqlbuild/rule_engine/host_payload/test_host_payload_facts.py b/tests/integration/src/sqlbuild/rule_engine/host_payload/test_host_payload_facts.py index 69a2e08d13..6d50b35423 100644 --- a/tests/integration/src/sqlbuild/rule_engine/host_payload/test_host_payload_facts.py +++ b/tests/integration/src/sqlbuild/rule_engine/host_payload/test_host_payload_facts.py @@ -5,8 +5,6 @@ import pytest from sqlbuild._native import ( - CompiledModel, - CompiledProject, compiled_project_record, custom_host_payload, ) @@ -20,7 +18,6 @@ compile_files, dropped_fields, fact_digests, - record_trimmed_fields, ) @@ -104,18 +101,45 @@ "SELECT 1\n" ), }, - expected_unpopulated_trimmed_fields=frozenset(), + expected_dropped_project_fields=frozenset( + { + "binding_catalog", + "diagnostics", + "external_sql_reference_resolver", + "hook_functions", + "loaded_macros", + "loader_functions", + "materialization_files", + "models", + "native_project", + "scope_index", + "sql_expansions", + "sql_hook_files", + "sql_scenarios", + "unmatched_literal_sql_relations", + } + ), + expected_dropped_model_fields=frozenset( + { + "authored_query_sql", + "binding_diagnostics", + "enum_columns", + "fast_lineage_columns", + "macro_deps", + "output_column_locations", + "references", + "unchecked_output_columns", + } + ), ), ), ids=lambda case: case.description, ) def test_given_compiled_project_when_trimming_host_payload_then_every_fact_is_unchanged( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch, test_case: HostPayloadFactsTestCase + tmp_path: Path, test_case: HostPayloadFactsTestCase ) -> None: project: CompiledProjectView = compile_files(root=tmp_path, files=test_case.project_files) - trimmed_fields: dict[type[object], set[str]] = record_trimmed_fields(monkeypatch) trimmed: CompiledProjectView = custom_host_payload(project) - monkeypatch.undo() full_digests: dict[FactKey, str | None] = fact_digests(project=project, root=tmp_path) trimmed_digests: dict[FactKey, str | None] = fact_digests(project=trimmed, root=tmp_path) @@ -124,7 +148,7 @@ def test_given_compiled_project_when_trimming_host_payload_then_every_fact_is_un assert tuple(map(public_model, trimmed.models)) == tuple(map(public_model, project.models)) assert ( dropped_fields(before=compiled_project_record(project), after=trimmed) - == trimmed_fields[CompiledProject] - test_case.expected_unpopulated_trimmed_fields + == test_case.expected_dropped_project_fields ) assert ( frozenset().union( @@ -133,5 +157,5 @@ def test_given_compiled_project_when_trimming_host_payload_then_every_fact_is_un for model, trimmed_model in zip(project.models, trimmed.models, strict=True) ) ) - == trimmed_fields[CompiledModel] - test_case.expected_unpopulated_trimmed_fields + == test_case.expected_dropped_model_fields ) diff --git a/tests/unit/src/sqlbuild/cli/commands/_helpers/cost/helpers.py b/tests/unit/src/sqlbuild/cli/commands/_helpers/cost/helpers.py index 1649470087..2cb12be74b 100644 --- a/tests/unit/src/sqlbuild/cli/commands/_helpers/cost/helpers.py +++ b/tests/unit/src/sqlbuild/cli/commands/_helpers/cost/helpers.py @@ -34,3 +34,14 @@ def build_cost_finalization( render=render, had_executable_work=had_executable_work, ) + + +def write_snowflake_project(project_dir: Path) -> None: + """Write a Snowflake project with a `dev` target whose connection uses `ANALYTICS`.""" + + _ = (project_dir / "sqlbuild_project.toml").write_text( + 'name = "shop"\nadapter = "snowflake"\n\n' + '[connection]\naccount = "acct"\ndatabase = "ANALYTICS"\n\n' + "[targets.dev]\n", + encoding="utf-8", + ) diff --git a/tests/unit/src/sqlbuild/cli/commands/_helpers/cost/test_refresh.py b/tests/unit/src/sqlbuild/cli/commands/_helpers/cost/test_refresh.py index 19883313a2..77547cc6a8 100644 --- a/tests/unit/src/sqlbuild/cli/commands/_helpers/cost/test_refresh.py +++ b/tests/unit/src/sqlbuild/cli/commands/_helpers/cost/test_refresh.py @@ -4,7 +4,6 @@ from datetime import UTC, datetime from decimal import Decimal from pathlib import Path -from types import SimpleNamespace from typing import Any import pytest @@ -22,6 +21,7 @@ CostRefreshDegradationTestCase, CostRefreshTestCase, ) +from tests.unit.src.sqlbuild.cli.commands._helpers.cost.helpers import write_snowflake_project from tests.unit.src.sqlbuild.cost.helpers import build_cost_run_record @@ -86,25 +86,11 @@ def test_given_pending_run_when_showing_detail_then_persisted_summary_is_refresh project_dir=tmp_path, record=replace(record, cost=replace(record.cost, status=test_case.initial_status)), ) - _ = (tmp_path / "sqlbuild_project.toml").write_text( - 'name = "shop"\nadapter = "snowflake"\n', encoding="utf-8" - ) - monkeypatch.setattr( - "sqlbuild._native.resolve_effective_adapter_name", - lambda **kwargs: "snowflake", - ) + write_snowflake_project(tmp_path) monkeypatch.setattr( "sqlbuild.adapter.discovery.main.resolve_adapter.resolve_adapter", lambda **kwargs: _RefreshingCostAdapter(), ) - monkeypatch.setattr( - "sqlbuild._native.resolve_project_connection_config", - lambda **kwargs: {"database": "ANALYTICS"}, - ) - monkeypatch.setattr( - "sqlbuild._native.build_effective_target_config", - lambda **kwargs: SimpleNamespace(database=None), - ) request: CostCommandRequest = CostCommandRequest( project_dir=tmp_path, selector=test_case.selector, @@ -189,22 +175,11 @@ def test_given_useful_partial_when_refresh_degrades_then_persisted_summary_is_pr observed_statement_count=test_case.candidate_observed_statement_count, query_count=test_case.candidate_query_count, ) - monkeypatch.setattr( - "sqlbuild._native.discover_project_inputs", - lambda **kwargs: SimpleNamespace(project_config=object(), local_config=object()), - ) - monkeypatch.setattr( - "sqlbuild._native.resolve_effective_adapter_name", - lambda **kwargs: "snowflake", - ) + write_snowflake_project(tmp_path) monkeypatch.setattr( "sqlbuild.adapter.discovery.main.resolve_adapter.resolve_adapter", lambda **kwargs: _StaticCostAdapter(candidate), ) - monkeypatch.setattr( - "sqlbuild._native.resolve_project_connection_config", - lambda **kwargs: {}, - ) request: CostCommandRequest = CostCommandRequest( project_dir=tmp_path, selector="run-partial", @@ -272,22 +247,11 @@ def test_given_persisted_ledger_failure_when_refresh_has_no_coverage_then_failur observed_statement_count=test_case.candidate_observed_statement_count, query_count=test_case.candidate_query_count, ) - monkeypatch.setattr( - "sqlbuild._native.discover_project_inputs", - lambda **kwargs: SimpleNamespace(project_config=object(), local_config=object()), - ) - monkeypatch.setattr( - "sqlbuild._native.resolve_effective_adapter_name", - lambda **kwargs: "snowflake", - ) + write_snowflake_project(tmp_path) monkeypatch.setattr( "sqlbuild.adapter.discovery.main.resolve_adapter.resolve_adapter", lambda **kwargs: _StaticCostAdapter(candidate), ) - monkeypatch.setattr( - "sqlbuild._native.resolve_project_connection_config", - lambda **kwargs: {}, - ) request: CostCommandRequest = CostCommandRequest( project_dir=tmp_path, selector="run-ledger-failure", diff --git a/tests/unit/src/sqlbuild/cli/commands/_helpers/runtime/helpers.py b/tests/unit/src/sqlbuild/cli/commands/_helpers/runtime/helpers.py index a707305d5c..6e4fc81e4e 100644 --- a/tests/unit/src/sqlbuild/cli/commands/_helpers/runtime/helpers.py +++ b/tests/unit/src/sqlbuild/cli/commands/_helpers/runtime/helpers.py @@ -2,6 +2,7 @@ from __future__ import annotations +import json from pathlib import Path from sqlbuild._native import ( @@ -48,3 +49,13 @@ def write_case_adapter_file(*, project_dir: Path, contents: str) -> None: (project_dir / "adapters").mkdir(exist_ok=True) (project_dir / "adapters" / "custom.py").write_text(contents, encoding="utf-8") + + +def write_dbt_profile(profiles_dir: Path, *, profile: str, output: dict[str, object]) -> None: + """Write a `profiles.yml` whose `profile` has one `dev` output with `output`'s fields.""" + + profiles_dir.mkdir(parents=True, exist_ok=True) + fields: str = "".join(f" {name}: {json.dumps(value)}\n" for name, value in output.items()) + (profiles_dir / "profiles.yml").write_text( + f"{profile}:\n target: dev\n outputs:\n dev:\n{fields}", encoding="utf-8" + ) diff --git a/tests/unit/src/sqlbuild/cli/commands/_helpers/runtime/test_connection.py b/tests/unit/src/sqlbuild/cli/commands/_helpers/runtime/test_connection.py index dbd92dbfe6..fd1642cba4 100644 --- a/tests/unit/src/sqlbuild/cli/commands/_helpers/runtime/test_connection.py +++ b/tests/unit/src/sqlbuild/cli/commands/_helpers/runtime/test_connection.py @@ -4,12 +4,10 @@ import pytest -from sqlbuild import _native as native from sqlbuild._native import ( DiscoveredProjectInputs, LocalConfig, LocalTargetConfig, - NormalizedDbtProfileConnection, ProjectConfig, ProjectConfigError, TargetConfig, @@ -29,6 +27,7 @@ ResolveEnvironmentConnectionConfigTestCase, ResolveProjectConnectionConfigTestCase, ) +from tests.unit.src.sqlbuild.cli.commands._helpers.runtime.helpers import write_dbt_profile @pytest.mark.parametrize( @@ -255,26 +254,23 @@ def test_given_named_dbt_profile_connection_when_resolving_then_routes_downstrea name="demo", adapter="snowflake", default_target="dev", - connections={"dbt": {"source": "dbt_profile", "profile": "analytics", "target": "dev"}}, + connections={ + "dbt": { + "source": "dbt_profile", + "profile": "analytics", + "target": "dev", + "profiles_dir": "profiles", + } + }, targets={"dev": TargetConfig(connection_name="dbt")}, ), local_config=LocalConfig(), ) - def resolve_raw_dbt_profile_connection(**kwargs: object) -> NormalizedDbtProfileConnection: - assert kwargs["raw_config"] == { - "source": "dbt_profile", - "profile": "analytics", - "target": "dev", - } - return NormalizedDbtProfileConnection( - adapter="snowflake", connection={"account": "acct", "database": "ANALYTICS"} - ) - - monkeypatch.setattr( - native, - "resolve_raw_dbt_profile_connection", - resolve_raw_dbt_profile_connection, + write_dbt_profile( + tmp_path / "profiles", + profile="analytics", + output={"type": "snowflake", "account": "acct", "database": "ANALYTICS"}, ) connection: dict[str, object] = resolve_project_connection_config( @@ -293,7 +289,7 @@ def resolve_raw_dbt_profile_connection(**kwargs: object) -> NormalizedDbtProfile "source": "dbt_profile", "profile": "analytics", "target": "dev", - "profiles_dir": "../profiles", + "profiles_dir": "profiles", "account": "override-acct", "client_request_mfa_token": True, }, @@ -324,16 +320,10 @@ def test_given_dbt_profile_connection_when_resolving_then_merges_user_overrides( local_config=LocalConfig(), ) - def resolve_raw_dbt_profile_connection(**kwargs: object) -> NormalizedDbtProfileConnection: - return NormalizedDbtProfileConnection( - adapter=BuiltinAdapter.SNOWFLAKE.value, - connection=test_case.profile_connection, - ) - - monkeypatch.setattr( - native, - "resolve_raw_dbt_profile_connection", - resolve_raw_dbt_profile_connection, + write_dbt_profile( + tmp_path / "profiles", + profile="analytics", + output={"type": "snowflake", **test_case.profile_connection}, ) connection: dict[str, object] = resolve_connection_config( diff --git a/tests/unit/src/sqlbuild/cli/commands/main/dbt/_test_types.py b/tests/unit/src/sqlbuild/cli/commands/main/dbt/_test_types.py index 6a939e8c9d..14cfbfc306 100644 --- a/tests/unit/src/sqlbuild/cli/commands/main/dbt/_test_types.py +++ b/tests/unit/src/sqlbuild/cli/commands/main/dbt/_test_types.py @@ -58,7 +58,7 @@ class DbtInitOutputTestCase: class DbtInitProjectDirDefaultTestCase: description: str dbt_project_text: str - expected_dbt_project_dir: str + expected_config_line: str @dataclass(frozen=True) diff --git a/tests/unit/src/sqlbuild/cli/commands/main/dbt/helpers.py b/tests/unit/src/sqlbuild/cli/commands/main/dbt/helpers.py index 41f49a2dd9..a2c6148d42 100644 --- a/tests/unit/src/sqlbuild/cli/commands/main/dbt/helpers.py +++ b/tests/unit/src/sqlbuild/cli/commands/main/dbt/helpers.py @@ -1,5 +1,7 @@ from __future__ import annotations +import json +import sys from pathlib import Path from sqlbuild._native import ( @@ -10,6 +12,8 @@ ) from tests.unit.src.sqlbuild.cli.commands.main.dbt._test_types import DbtAutoInitTestCase +MINIMAL_SQLBUILD_PROJECT: str = 'name = "demo"\nadapter = "duckdb"\n' + def build_empty_dbt_plan() -> DbtInteropPlan: """Build a minimal dbt interop plan for CLI output tests.""" @@ -42,6 +46,34 @@ def prepare_dbt_auto_init_dirs(*, test_case: DbtAutoInitTestCase, tmp_path: Path def write_minimal_sqlbuild_project(project_dir: Path) -> None: """Write a minimal SQLBuild project config for wrapper tests.""" + (project_dir / "sqlbuild_project.toml").write_text(MINIMAL_SQLBUILD_PROJECT, encoding="utf-8") + + +def write_fake_dbt_executable(directory: Path, *, exit_code: int) -> Path: + """Write an executable stand-in for dbt that records its arguments and exits.""" + + script: Path = directory / "fake_dbt" + script.write_text( + f"#!{sys.executable}\n" + "import json, pathlib, sys\n" + f"pathlib.Path({str(directory / 'dbt_args.json')!r}).write_text(json.dumps(sys.argv[1:]))\n" + f"sys.exit({exit_code!r})\n", + encoding="utf-8", + ) + script.chmod(0o755) + return script + + +def read_fake_dbt_args(directory: Path) -> tuple[str, ...]: + """The arguments the stand-in dbt in `directory` last received.""" + + return tuple(json.loads((directory / "dbt_args.json").read_text(encoding="utf-8"))) + + +def write_duckdb_sqlbuild_project(project_dir: Path) -> None: + """Write a SQLBuild project config with an inline DuckDB connection.""" + (project_dir / "sqlbuild_project.toml").write_text( - 'name = "demo"\nadapter = "duckdb"\n', encoding="utf-8" + 'name = "demo"\nadapter = "duckdb"\n\n[connection]\npath = "warehouse.duckdb"\n', + encoding="utf-8", ) diff --git a/tests/unit/src/sqlbuild/cli/commands/main/dbt/test_dbt.py b/tests/unit/src/sqlbuild/cli/commands/main/dbt/test_dbt.py index 5cbfd27540..f3acc3ef7a 100644 --- a/tests/unit/src/sqlbuild/cli/commands/main/dbt/test_dbt.py +++ b/tests/unit/src/sqlbuild/cli/commands/main/dbt/test_dbt.py @@ -8,8 +8,6 @@ from _pytest.capture import CaptureResult from sqlbuild._native import ( - DbtInitRequest, - DbtInitResult, DbtInteropCommand, DbtInteropExecutionRequest, DbtInteropPlan, @@ -22,10 +20,15 @@ DbtPlanProgressTestCase, ) from tests.unit.src.sqlbuild.cli.commands.main.dbt.helpers import ( + MINIMAL_SQLBUILD_PROJECT, build_empty_dbt_plan, prepare_dbt_auto_init_dirs, + read_fake_dbt_args, + write_duckdb_sqlbuild_project, + write_fake_dbt_executable, write_minimal_sqlbuild_project, ) +from tests.unit.src.sqlbuild.integrations.dbt.profile.helpers import write_duckdb_dbt_workspace @pytest.mark.parametrize( @@ -148,7 +151,7 @@ def execute_dbt_interop_from_project(request: DbtInteropExecutionRequest) -> int DbtDebugWrapperTestCase( description="debug runs dbt then SQLBuild diagnostics and skips SQLBuild connection", args=("--project-dir", "dbt_project", "--no-connection"), - expected_dbt_args=("--project-dir", "dbt_project", "--no-connection"), + expected_dbt_args=("debug", "--project-dir"), expected_sqlbuild_no_connection=True, expected_exit_code=0, dbt_exit_code=0, @@ -163,7 +166,7 @@ def execute_dbt_interop_from_project(request: DbtInteropExecutionRequest) -> int "--project-dir", "dbt_project", ), - expected_dbt_args=("--project-dir", "dbt_project"), + expected_dbt_args=("debug", "--project-dir"), expected_sqlbuild_no_connection=False, expected_exit_code=1, dbt_exit_code=1, @@ -177,34 +180,12 @@ def test_given_dbt_debug_command_when_running_then_invokes_dbt_and_sqlbuild_debu monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], ) -> None: - dbt_calls: list[tuple[Path, tuple[str, ...], object, object]] = [] - sqlbuild_calls: list[tuple[Path | None, bool, bool, bool]] = [] - - def debug_dbt_from_project( - *, - project_dir: Path, - args: tuple[str, ...], - stdout_stream: object, - stderr_stream: object, - ) -> int: - dbt_calls.append((project_dir, args, stdout_stream, stderr_stream)) - return test_case.dbt_exit_code - - def run_sqlbuild_debug( - project_dir: Path | None, - no_color: bool, - no_connection: bool, - json_output: bool, - ) -> int: - sqlbuild_calls.append((project_dir, no_color, no_connection, json_output)) - return 0 - - monkeypatch.setattr( - "sqlbuild._native.debug_dbt_from_project", - debug_dbt_from_project, + (tmp_path / "dbt_project").mkdir() + monkeypatch.setenv( + "DBT_EXECUTABLE", + str(write_fake_dbt_executable(tmp_path, exit_code=test_case.dbt_exit_code)), ) - monkeypatch.setattr("sqlbuild._native.run_debug_command", run_sqlbuild_debug) - write_minimal_sqlbuild_project(tmp_path) + write_duckdb_sqlbuild_project(tmp_path) exit_code: int = run_dbt_command( command=DbtInteropCommand.DEBUG, @@ -214,11 +195,16 @@ def run_sqlbuild_debug( ) assert exit_code == test_case.expected_exit_code - assert dbt_calls == [(tmp_path, test_case.expected_dbt_args, sys.stdout, sys.stderr)] - assert sqlbuild_calls == [(tmp_path, True, test_case.expected_sqlbuild_no_connection, False)] + assert read_fake_dbt_args(tmp_path) == ( + *test_case.expected_dbt_args, + str(tmp_path / "dbt_project"), + ) captured: CaptureResult[str] = capsys.readouterr() for fragment in test_case.expected_stderr_fragments: assert fragment in captured.err + assert ("skipped by --no-connection" in captured.out) is ( + test_case.expected_sqlbuild_no_connection + ) @pytest.mark.parametrize( @@ -256,25 +242,6 @@ def test_given_existing_sqlbuild_project_when_running_dbt_command_then_uses_expe ) -> None: dbt_project_dir: Path = prepare_dbt_auto_init_dirs(test_case=test_case, tmp_path=tmp_path) forwarded_project_dirs: list[Path] = [] - init_requests: list[DbtInitRequest] = [] - - def run_dbt_profile_init(*, request: DbtInitRequest) -> DbtInitResult: - init_requests.append(request) - output_dir: Path | None = request.sqb_output_dir - assert output_dir is not None - output_dir.mkdir(parents=True, exist_ok=True) - (output_dir / "sqlbuild_project.toml").write_text( - 'name = "demo"\nadapter = "duckdb"\n', encoding="utf-8" - ) - return DbtInitResult( - output_dir=output_dir, - project_file=output_dir / "sqlbuild_project.toml", - project_name="demo", - adapter="duckdb", - target_name=request.target_name or "dev", - profile_name="demo", - toml='name = "demo"\n', - ) def plan_dbt_interop_from_project( *, @@ -288,10 +255,6 @@ def plan_dbt_interop_from_project( forwarded_project_dirs.append(project_dir) return build_empty_dbt_plan() - monkeypatch.setattr( - "sqlbuild._native.build_dbt_init_project", - run_dbt_profile_init, - ) monkeypatch.setattr( "sqlbuild._native.plan_dbt_interop_from_project", plan_dbt_interop_from_project, @@ -306,7 +269,9 @@ def plan_dbt_interop_from_project( assert exit_code == 0 assert forwarded_project_dirs[0].name == test_case.expected_forwarded_project_dir_name - assert init_requests == [] + assert (forwarded_project_dirs[0] / "sqlbuild_project.toml").read_text(encoding="utf-8") == ( + MINIMAL_SQLBUILD_PROJECT + ) @pytest.mark.parametrize( @@ -342,27 +307,9 @@ def test_given_missing_sqlbuild_twin_when_running_dbt_command_then_initializes_a capsys: pytest.CaptureFixture[str], ) -> None: dbt_project_dir: Path = prepare_dbt_auto_init_dirs(test_case=test_case, tmp_path=tmp_path) + write_duckdb_dbt_workspace(dbt_project_dir) forwarded_project_dirs: list[Path] = [] forwarded_args: list[tuple[str, ...]] = [] - init_requests: list[DbtInitRequest] = [] - - def run_dbt_profile_init(*, request: DbtInitRequest) -> DbtInitResult: - init_requests.append(request) - assert request.progress_callbacks.start is not None - request.progress_callbacks.start("Inspecting dbt project and profile...") - output_dir: Path | None = request.sqb_output_dir - assert output_dir is not None - output_dir.mkdir(parents=True, exist_ok=True) - write_minimal_sqlbuild_project(output_dir) - return DbtInitResult( - output_dir=output_dir, - project_file=output_dir / "sqlbuild_project.toml", - project_name="demo", - adapter="duckdb", - target_name=request.target_name or "dev", - profile_name="demo", - toml='name = "demo"\n', - ) def plan_dbt_interop_from_project( *, @@ -377,10 +324,6 @@ def plan_dbt_interop_from_project( forwarded_args.append(args) return build_empty_dbt_plan() - monkeypatch.setattr( - "sqlbuild._native.build_dbt_init_project", - run_dbt_profile_init, - ) monkeypatch.setattr( "sqlbuild._native.plan_dbt_interop_from_project", plan_dbt_interop_from_project, @@ -402,13 +345,8 @@ def plan_dbt_interop_from_project( assert forwarded_project_dirs[0].name == test_case.expected_forwarded_project_dir_name assert Path(forwarded_args[0][forwarded_args[0].index("--project-dir") + 1]).is_absolute() assert Path(forwarded_args[0][forwarded_args[0].index("--profiles-dir") + 1]).is_absolute() - assert bool(init_requests) is test_case.expected_init_called - request: DbtInitRequest = init_requests[0] - assert request.dbt_project_dir.name == test_case.expected_request_dbt_project_dir_name - assert getattr(request.profiles_dir, "as_posix", lambda: None)() == ( - test_case.expected_request_profiles_dir + twin_config: str = (forwarded_project_dirs[0] / "sqlbuild_project.toml").read_text( + encoding="utf-8" ) - assert request.target_name == test_case.expected_request_target_name - assert request.sqb_output_dir is not None - assert request.sqb_output_dir.name == "sqlbuild_project" - assert request.skip_dbt_debug is True + assert f'project_dir = "../{test_case.expected_request_dbt_project_dir_name}' in twin_config + assert f'target = "{test_case.expected_request_target_name}"' in twin_config diff --git a/tests/unit/src/sqlbuild/cli/commands/main/dbt/test_init.py b/tests/unit/src/sqlbuild/cli/commands/main/dbt/test_init.py index b232b21abc..1238619bcc 100644 --- a/tests/unit/src/sqlbuild/cli/commands/main/dbt/test_init.py +++ b/tests/unit/src/sqlbuild/cli/commands/main/dbt/test_init.py @@ -6,14 +6,14 @@ from _pytest.capture import CaptureResult import sqlbuild._native as dbt_init_module -import sqlbuild._native as dbt_profile_init -from sqlbuild._native import DbtInitCommandRequest, DbtInitRequest, DbtInitResult +from sqlbuild._native import DbtInitCommandRequest from sqlbuild.integrations.dbt.exceptions import DbtProfileError from tests.unit.src.sqlbuild.cli.commands.main.dbt._test_types import ( DbtInitOutputTestCase, DbtInitProjectDirDefaultTestCase, DbtInitValidationOrderTestCase, ) +from tests.unit.src.sqlbuild.integrations.dbt.profile.helpers import write_duckdb_dbt_workspace from tests.unit.src.sqlbuild.presentation.helpers import use_color_terminal @@ -29,7 +29,7 @@ "SQLBuild project created", "Setup summary:", "Config file", - "/workspace/sqlbuild_project/sqlbuild_project.toml", + "sqlbuild_project/sqlbuild_project.toml", "What SQLBuild created:", "SQLBuild twin config", "This points SQLBuild at your dbt project and profile.", @@ -43,34 +43,15 @@ ) def test_given_dbt_init_when_running_then_outputs_progress_and_dbt_first_next_steps( test_case: DbtInitOutputTestCase, + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], ) -> None: - def run_dbt_profile_init(*, request: DbtInitRequest) -> DbtInitResult: - assert request.progress_callbacks.start is not None - assert request.progress_callbacks.complete is not None - request.progress_callbacks.start("Inspecting dbt project and profile...") - request.progress_callbacks.complete("Inspected dbt project and profile.") - return DbtInitResult( - output_dir=Path("/workspace/sqlbuild_project"), - project_file=Path("/workspace/sqlbuild_project/sqlbuild_project.toml"), - project_name="analytics", - adapter="duckdb", - target_name="dev", - profile_name="analytics_profile", - toml='name = "analytics"\n', - ) - - monkeypatch.setattr(dbt_profile_init, "build_dbt_init_project", run_dbt_profile_init) - monkeypatch.setattr( - dbt_profile_init, - "validate_dbt_init_project", - lambda *, request: None, - ) + write_duckdb_dbt_workspace(tmp_path) exit_code: int = dbt_init_module.run_dbt_init_command( DbtInitCommandRequest( - cwd=Path("/workspace"), + cwd=tmp_path, dbt_project_dir="dbt_project", profiles_dir="profiles", profile_name=None, @@ -110,41 +91,15 @@ def run_dbt_profile_init(*, request: DbtInitRequest) -> DbtInitResult: ) def test_given_dbt_init_dry_run_when_running_then_outputs_preview_document( test_case: DbtInitOutputTestCase, + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], ) -> None: - def run_dbt_profile_init(*, request: DbtInitRequest) -> DbtInitResult: - assert request.progress_callbacks.start is not None - assert request.progress_callbacks.complete is not None - request.progress_callbacks.start("Rendering dbt profile connection...") - request.progress_callbacks.complete("Rendered dbt profile connection.") - return DbtInitResult( - output_dir=Path("/workspace/sqlbuild_project"), - project_file=Path("/workspace/sqlbuild_project/sqlbuild_project.toml"), - project_name="analytics", - adapter="duckdb", - target_name="dev", - profile_name="analytics_profile", - toml=( - 'name = "analytics"\n' - 'adapter = "duckdb"\n' - "\n" - "[targets.dev.connection]\n" - 'source = "dbt_profile"\n' - ), - dry_run=True, - ) - - monkeypatch.setattr(dbt_profile_init, "build_dbt_init_project", run_dbt_profile_init) - monkeypatch.setattr( - dbt_profile_init, - "validate_dbt_init_project", - lambda *, request: None, - ) + write_duckdb_dbt_workspace(tmp_path) exit_code: int = dbt_init_module.run_dbt_init_command( DbtInitCommandRequest( - cwd=Path("/workspace"), + cwd=tmp_path, dbt_project_dir="dbt_project", profiles_dir="profiles", profile_name=None, @@ -176,7 +131,7 @@ def run_dbt_profile_init(*, request: DbtInitRequest) -> DbtInitResult: "\033[34m\033[1mSQLBuild project created\033[0m", "\033[1mSetup summary\033[0m:", "\033[34mProject", - "analytics", + "shop", "\033[1mWhat SQLBuild created\033[0m:", "\033[34mSQLBuild twin config\033[0m", "\033[1mNext steps\033[0m:", @@ -190,35 +145,16 @@ def run_dbt_profile_init(*, request: DbtInitRequest) -> DbtInitResult: ) def test_given_color_terminal_when_running_dbt_init_then_it_styles_output( test_case: DbtInitOutputTestCase, + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], ) -> None: - def run_dbt_profile_init(*, request: DbtInitRequest) -> DbtInitResult: - assert request.progress_callbacks.start is not None - assert request.progress_callbacks.complete is not None - request.progress_callbacks.start("Inspecting dbt project and profile...") - request.progress_callbacks.complete("Inspected dbt project and profile.") - return DbtInitResult( - output_dir=Path("/workspace/sqlbuild_project"), - project_file=Path("/workspace/sqlbuild_project/sqlbuild_project.toml"), - project_name="analytics", - adapter="duckdb", - target_name="dev", - profile_name="analytics_profile", - toml='name = "analytics"\n', - ) - + write_duckdb_dbt_workspace(tmp_path) use_color_terminal(monkeypatch) - monkeypatch.setattr(dbt_profile_init, "build_dbt_init_project", run_dbt_profile_init) - monkeypatch.setattr( - dbt_profile_init, - "validate_dbt_init_project", - lambda *, request: None, - ) exit_code: int = dbt_init_module.run_dbt_init_command( DbtInitCommandRequest( - cwd=Path("/workspace"), + cwd=tmp_path, dbt_project_dir="dbt_project", profiles_dir="profiles", profile_name=None, @@ -242,7 +178,7 @@ def run_dbt_profile_init(*, request: DbtInitRequest) -> DbtInitResult: DbtInitProjectDirDefaultTestCase( description="defaults project dir to cwd when dbt project file exists", dbt_project_text="name: analytics\nprofile: analytics\n", - expected_dbt_project_dir=".", + expected_config_line='project_dir = "../{cwd}"', ) ], ids=lambda case: case.description, @@ -251,31 +187,15 @@ def test_given_dbt_project_in_cwd_when_running_dbt_init_then_project_dir_default test_case: DbtInitProjectDirDefaultTestCase, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], ) -> None: (tmp_path / "dbt_project.yml").write_text(test_case.dbt_project_text, encoding="utf-8") - captured_request: dict[str, DbtInitRequest] = {} - - def validate_dbt_profile_init_request(*, request: DbtInitRequest) -> None: - assert request.dbt_project_dir == Path(test_case.expected_dbt_project_dir) - - def run_dbt_profile_init(*, request: DbtInitRequest) -> DbtInitResult: - captured_request["request"] = request - return DbtInitResult( - output_dir=tmp_path / "sqlbuild_project", - project_file=tmp_path / "sqlbuild_project" / "sqlbuild_project.toml", - project_name="analytics", - adapter="duckdb", - target_name="dev", - profile_name="analytics", - toml='name = "analytics"\n', - ) - - monkeypatch.setattr( - dbt_profile_init, - "validate_dbt_init_project", - validate_dbt_profile_init_request, + (tmp_path / "profiles.yml").write_text( + "analytics:\n target: dev\n outputs:\n dev:\n type: duckdb\n" + " path: warehouse.duckdb\n schema: main\n", + encoding="utf-8", ) - monkeypatch.setattr(dbt_profile_init, "build_dbt_init_project", run_dbt_profile_init) + monkeypatch.delenv("DBT_PROFILES_DIR", raising=False) exit_code: int = dbt_init_module.run_dbt_init_command( DbtInitCommandRequest( @@ -292,7 +212,7 @@ def run_dbt_profile_init(*, request: DbtInitRequest) -> DbtInitResult: ) assert exit_code == 0 - assert captured_request["request"].dbt_project_dir == Path(test_case.expected_dbt_project_dir) + assert test_case.expected_config_line.format(cwd=tmp_path.name) in capsys.readouterr().out @pytest.mark.parametrize( @@ -310,18 +230,10 @@ def test_given_invalid_dbt_init_inputs_when_running_then_validation_happens_befo tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: - def validate_dbt_profile_init_request(*, request: DbtInitRequest) -> None: - raise DbtProfileError(f"dbt config file not found: {test_case.expected_error_fragment}") - - def run_dbt_profile_init(*, request: DbtInitRequest) -> DbtInitResult: - raise AssertionError("init should not run after validation failure") - - monkeypatch.setattr( - dbt_profile_init, - "validate_dbt_init_project", - validate_dbt_profile_init_request, + (tmp_path / "dbt_project.yml").write_text( + "name: analytics\nprofile: analytics\n", encoding="utf-8" ) - monkeypatch.setattr(dbt_profile_init, "build_dbt_init_project", run_dbt_profile_init) + monkeypatch.setenv("DBT_PROFILES_DIR", str(tmp_path / "profiles")) with pytest.raises(DbtProfileError, match=test_case.expected_error_fragment): dbt_init_module.run_dbt_init_command( @@ -331,9 +243,11 @@ def run_dbt_profile_init(*, request: DbtInitRequest) -> DbtInitResult: profiles_dir=None, profile_name=None, target_name=None, - sqb_output_dir=None, - dry_run=True, + sqb_output_dir="sqlbuild_project", + dry_run=False, overwrite=False, skip_dbt_debug=True, ) ) + + assert not (tmp_path / "sqlbuild_project").exists() diff --git a/tests/unit/src/sqlbuild/cli/commands/main/execution/helpers.py b/tests/unit/src/sqlbuild/cli/commands/main/execution/helpers.py index 6b1431bc9e..b969aa9ef6 100644 --- a/tests/unit/src/sqlbuild/cli/commands/main/execution/helpers.py +++ b/tests/unit/src/sqlbuild/cli/commands/main/execution/helpers.py @@ -4,19 +4,10 @@ from contextlib import nullcontext from io import StringIO from pathlib import Path -from typing import Any from unittest.mock import Mock import pytest -from sqlbuild import _native as native -from sqlbuild._native import ( - CompiledObjectKey, - CompiledResourceType, - CompiledSqlScenario, - DiscoveredSqlScenarioFile, -) -from sqlbuild.adapter.discovery.main import resolve_adapter as adapters_module from sqlbuild.diagnostics.main import process_resource_reporting as resource_reporting_module from sqlbuild.executor.pipeline.main import run as executor_run_module from tests.unit.src.sqlbuild.presentation.helpers import use_plain_terminal @@ -49,45 +40,49 @@ def isatty(self) -> bool: return self._tty +_SCENARIO_PROJECT_TOML: str = ( + 'name = "project"\nadapter = "duckdb"\n\n[connection]\ndatabase = "project.duckdb"\n' +) +_SCENARIO_SOURCES_YAML: str = ( + "sources:\n" + " - name: raw_orders\n description: Test source raw_orders.\n" + " schema: main\n" + " table: raw_orders\n" + " columns:\n" + " - name: order_id\n type: INTEGER\n nullable: false\n" +) +_SCENARIO_MODEL_SQL: str = ( + 'MODEL (description "Test model orders.");\n\nSELECT order_id FROM __source("raw_orders")\n' +) +VALID_SCENARIO_SQL: str = ( + "SCENARIO (description 'Test scenario orders_paid.');\n\n" + "WITH\n" + "__source__raw_orders AS (SELECT 1 AS order_id),\n" + "__expected__orders AS (SELECT 1 AS order_id)\n" + "SELECT 1\n" +) +INVALID_SCENARIO_SQL: str = "SCENARIO(description 'Test scenario.');\n\nSELECT 1\n" + + def configure_native_scenario_command( *, monkeypatch: pytest.MonkeyPatch, stream: ScenarioProgressStream, - compile_project: Any, + scenario_sql: str, project_dir: Path, ) -> None: - """Isolate the native scenario command from adapters and the warehouse in an empty project.""" - - _ = (project_dir / "sqlbuild_project.toml").write_text( - 'name = "project"\nadapter = "duckdb"\n', encoding="utf-8" - ) + """Write a one-scenario DuckDB project and isolate the command from scenario execution.""" + + files: dict[str, str] = { + "sqlbuild_project.toml": _SCENARIO_PROJECT_TOML, + "sources/raw.yml": _SCENARIO_SOURCES_YAML, + "models/orders.sql": _SCENARIO_MODEL_SQL, + "tests/scenarios/orders_paid.sql": scenario_sql, + } + for relative_path, contents in files.items(): + path: Path = project_dir / relative_path + path.parent.mkdir(parents=True, exist_ok=True) + _ = path.write_text(contents, encoding="utf-8") monkeypatch.setattr(sys, "stdout", stream) use_plain_terminal(monkeypatch) - monkeypatch.setattr(native, "resolve_project_connection_config", lambda **_: {}) - monkeypatch.setattr(native, "run_compile_only_pipeline", compile_project) - monkeypatch.setattr(adapters_module, "resolve_adapter", lambda **_: Mock()) - monkeypatch.setattr(native, "resolve_external_sql_reference_resolver", lambda **_: None) monkeypatch.setattr(executor_run_module, "run_scenario_test_pipeline", lambda **_: ()) - - -def build_compiled_scenario_project(project_dir: Path) -> Mock: - """A compile result with one scenario under tests/scenarios.""" - - scenario_file: DiscoveredSqlScenarioFile = DiscoveredSqlScenarioFile( - file_path=project_dir / "tests/scenarios/orders_paid.sql", - relative_path=Path("tests/scenarios/orders_paid.sql"), - contents="SCENARIO(description 'Test scenario.');\n\nSELECT 1\n", - header_values={}, - sql_body="SELECT 1", - name="orders_paid", - ) - pipeline_result: Mock = Mock() - pipeline_result.project.sql_scenarios = ( - CompiledSqlScenario( - key=CompiledObjectKey(CompiledResourceType.SQL_SCENARIO, "orders_paid"), - name="orders_paid", - scenario_file=scenario_file, - sql_body="SELECT 1", - ), - ) - return pipeline_result diff --git a/tests/unit/src/sqlbuild/cli/commands/main/execution/test_scenario_command.py b/tests/unit/src/sqlbuild/cli/commands/main/execution/test_scenario_command.py index 4adb978cb5..4db2347d6e 100644 --- a/tests/unit/src/sqlbuild/cli/commands/main/execution/test_scenario_command.py +++ b/tests/unit/src/sqlbuild/cli/commands/main/execution/test_scenario_command.py @@ -2,13 +2,11 @@ from contextvars import Token from pathlib import Path -from typing import Any -from unittest.mock import Mock import pytest from sqlbuild._native import ( - CompilePipelineResult, + CompileInputError, NativeProgressProjector, ScenarioTestCommandRequest, run_scenario_test_command, @@ -18,8 +16,9 @@ ScenarioCompilePresentationTestCase, ) from tests.unit.src.sqlbuild.cli.commands.main.execution.helpers import ( + INVALID_SCENARIO_SQL, + VALID_SCENARIO_SQL, ScenarioProgressStream, - build_compiled_scenario_project, configure_native_scenario_command, ) @@ -50,18 +49,10 @@ def test_given_scenario_compile_when_running_then_presentation_has_one_owner( test_case: ScenarioCompilePresentationTestCase, ) -> None: stream: ScenarioProgressStream = ScenarioProgressStream(tty=test_case.tty) - pipeline_result: Mock = build_compiled_scenario_project(tmp_path) - - def compile_project(**kwargs: Any) -> CompilePipelineResult: - on_progress: Any = kwargs["on_progress"] - on_progress("Compiling project...") - on_progress("Compiled project.") - return pipeline_result - configure_native_scenario_command( monkeypatch=monkeypatch, stream=stream, - compile_project=compile_project, + scenario_sql=VALID_SCENARIO_SQL, project_dir=tmp_path, ) events: list[LifecycleEvent] = [] @@ -115,16 +106,10 @@ def test_given_scenario_compile_failure_when_running_then_lifecycle_fails_and_pr test_case: ScenarioCompilePresentationTestCase, ) -> None: stream: ScenarioProgressStream = ScenarioProgressStream(tty=test_case.tty) - - def fail_compile(**kwargs: Any) -> CompilePipelineResult: - on_progress: Any = kwargs["on_progress"] - on_progress("Compiling project...") - raise RuntimeError("compile failed") - configure_native_scenario_command( monkeypatch=monkeypatch, stream=stream, - compile_project=fail_compile, + scenario_sql=INVALID_SCENARIO_SQL, project_dir=tmp_path, ) events: list[LifecycleEvent] = [] @@ -136,7 +121,7 @@ def fail_compile(**kwargs: Any) -> CompilePipelineResult: try: with dispatcher_scope(dispatcher): - with pytest.raises(RuntimeError, match="compile failed"): + with pytest.raises(CompileInputError, match="must declare fixture CTEs"): run_scenario_test_command(request=ScenarioTestCommandRequest(project_dir=tmp_path)) finally: projector.restore(token) diff --git a/tests/unit/src/sqlbuild/cli/commands/main/scope/test_scope.py b/tests/unit/src/sqlbuild/cli/commands/main/scope/test_scope.py index 4f82827a5a..17608c3ab7 100644 --- a/tests/unit/src/sqlbuild/cli/commands/main/scope/test_scope.py +++ b/tests/unit/src/sqlbuild/cli/commands/main/scope/test_scope.py @@ -7,7 +7,6 @@ import pytest -from sqlbuild import _native as native from sqlbuild._native import ScopeCommandRequest, ScopeCompleteness, ScopeIndex, run_scope_command from sqlbuild.adapter.discovery.main import resolve_adapter as adapter_registry from tests.unit.src.sqlbuild.cli.commands.main.scope._test_types import ScopeCommandCase @@ -25,7 +24,6 @@ def fail(**_kwargs: object) -> None: pytest.fail("scope must not resolve adapters or warehouse credentials") monkeypatch.setattr(adapter_registry, "resolve_adapter", fail) - monkeypatch.setattr(native, "resolve_project_connection_config", fail) exit_code: int = run_scope_command( request=ScopeCommandRequest(target="model:orders", json_output=True), load_scope_index=lambda **_kwargs: report_scope_lookup().index, diff --git a/tests/unit/src/sqlbuild/compiler/planner/_helpers/test_fixture_planning_work.py b/tests/unit/src/sqlbuild/compiler/planner/_helpers/test_fixture_planning_work.py index 7f41f096e5..752f165308 100644 --- a/tests/unit/src/sqlbuild/compiler/planner/_helpers/test_fixture_planning_work.py +++ b/tests/unit/src/sqlbuild/compiler/planner/_helpers/test_fixture_planning_work.py @@ -1,11 +1,9 @@ from __future__ import annotations from dataclasses import replace -from unittest.mock import Mock import pytest -import sqlbuild._native as sql_test_assembly from sqlbuild import _native as fixture_completion from sqlbuild._native import ( CompiledObjectKey, @@ -21,6 +19,7 @@ SqlTestSelection, build_selected_test_entries, compiled_model_record, + fixture_inference_count, project_with, ) from sqlbuild.compiler.compile.types import ( @@ -186,7 +185,6 @@ def test_given_many_tests_when_building_entries_then_project_fixture_metadata_is ) def test_given_complete_authoritative_fixture_when_completing_then_skips_column_analysis( test_case: CompleteFixturePlanningTestCase, - monkeypatch: pytest.MonkeyPatch, ) -> None: _, project = build_test_and_project( PlanTestChainTestCase( @@ -218,12 +216,7 @@ def test_given_complete_authoritative_fixture_when_completing_then_skips_column_ }, ) fixture_completion.fixture_read_cache_info(reset=True) - fixture_inference: Mock = Mock(wraps=sql_test_assembly.infer_fixture_column_facts) - monkeypatch.setattr( - sql_test_assembly, - "infer_fixture_column_facts", - fixture_inference, - ) + fixture_inference_count(reset=True) completed: RelationFixtureCompletion = fixture_completion.build_relation_fixture_completion( project=project_with(project=project, models=(model,)), @@ -238,7 +231,7 @@ def test_given_complete_authoritative_fixture_when_completing_then_skips_column_ assert completed.diagnostics == () assert completed.fixture_sql_by_key[source_key] == test_case.expected_fixture_sql assert fixture_completion.fixture_read_cache_info()[0] == test_case.expected_analysis_calls - assert fixture_inference.call_count == test_case.expected_inference_calls + assert fixture_inference_count() == test_case.expected_inference_calls if __name__ == "__main__": diff --git a/tests/unit/src/sqlbuild/compiler/planner/classes/helpers.py b/tests/unit/src/sqlbuild/compiler/planner/classes/helpers.py index a504096ade..066d2796fc 100644 --- a/tests/unit/src/sqlbuild/compiler/planner/classes/helpers.py +++ b/tests/unit/src/sqlbuild/compiler/planner/classes/helpers.py @@ -10,11 +10,7 @@ import pytest -import sqlbuild._native as native_module -from sqlbuild._native import ( - FixtureColumnInference, - infer_fixture_column_facts, -) +from sqlbuild._native import fixture_inference_count from sqlbuild.adapter.contract.models import ExpressionInferenceProfile ORDERS_SQL: str = 'SELECT o.order_id, o.amount_cents FROM __ref("stg_orders") AS o' @@ -91,19 +87,10 @@ def upgrade_sqlbuild(root: Path, monkeypatch: pytest.MonkeyPatch) -> None: } -def count_fixture_inferences(monkeypatch: pytest.MonkeyPatch) -> list[int]: - """Count every fixture inference computed instead of reused.""" - - inferences: list[int] = [] - - def counting( - *, query_sql: str, inference_profile: ExpressionInferenceProfile - ) -> FixtureColumnInference | None: - inferences.append(1) - return infer_fixture_column_facts(query_sql=query_sql, inference_profile=inference_profile) +def reset_fixture_inference_count() -> None: + """Start counting fixture inferences computed instead of reused from zero.""" - monkeypatch.setattr(native_module, "infer_fixture_column_facts", counting) - return inferences + fixture_inference_count(reset=True) class SqlTestsProject: diff --git a/tests/unit/src/sqlbuild/compiler/planner/classes/test_fixture_column_inferences.py b/tests/unit/src/sqlbuild/compiler/planner/classes/test_fixture_column_inferences.py index 8fa5c94b32..ebe6d8d2d6 100644 --- a/tests/unit/src/sqlbuild/compiler/planner/classes/test_fixture_column_inferences.py +++ b/tests/unit/src/sqlbuild/compiler/planner/classes/test_fixture_column_inferences.py @@ -7,6 +7,7 @@ from sqlbuild._native import ( FixtureColumnInference, FixtureColumnInferences, + fixture_inference_count, infer_fixture_column_facts, ) from tests.unit.src.sqlbuild.compiler.planner.classes._test_types import ( @@ -15,7 +16,7 @@ from tests.unit.src.sqlbuild.compiler.planner.classes.helpers import ( FIXTURE_PROFILES, FIXTURE_QUERIES, - count_fixture_inferences, + reset_fixture_inference_count, ) @@ -41,15 +42,16 @@ ids=lambda case: case.description, ) def test_given_fixture_queries_when_inferring_then_matches_direct_inference_with_minimal_work( - test_case: FixtureColumnInferencesTestCase, monkeypatch: pytest.MonkeyPatch + test_case: FixtureColumnInferencesTestCase, ) -> None: - inferences: list[int] = count_fixture_inferences(monkeypatch) + reset_fixture_inference_count() memo: FixtureColumnInferences = FixtureColumnInferences() results: list[FixtureColumnInference | None] = [ memo.infer(query_sql=FIXTURE_QUERIES[query], inference_profile=FIXTURE_PROFILES[profile]) for query, profile in test_case.requests ] + inferences: int = fixture_inference_count() assert results == [ infer_fixture_column_facts( @@ -58,7 +60,7 @@ def test_given_fixture_queries_when_inferring_then_matches_direct_inference_with for query, profile in test_case.requests ] assert all(result is not None for result in results) - assert len(inferences) == test_case.expected_inferences + assert inferences == test_case.expected_inferences if __name__ == "__main__": diff --git a/tests/unit/src/sqlbuild/integrations/dbt/helpers.py b/tests/unit/src/sqlbuild/integrations/dbt/helpers.py index 984791a758..5caa2f16bb 100644 --- a/tests/unit/src/sqlbuild/integrations/dbt/helpers.py +++ b/tests/unit/src/sqlbuild/integrations/dbt/helpers.py @@ -23,6 +23,7 @@ DbtCommandResult, DbtEventStream, DbtEventStreamUpdate, + DbtNodeExecutionResult, DbtRunner, DiscoveredProjectInputs, DiscoveredSqlModelFile, @@ -499,8 +500,9 @@ def fake_dbt_argv( lines: tuple[str, ...], sleep_seconds: float = 0.0, after_lines: tuple[str, ...] = (), + returncode: int = 0, ) -> tuple[str, ...]: - """argv of a stand-in dbt process printing `lines`, pausing, then printing `after_lines`.""" + """argv of a stand-in dbt process printing `lines`, pausing, printing `after_lines`, exiting.""" script: Path = directory / "fake_dbt.py" _ = script.write_text( @@ -508,7 +510,33 @@ def fake_dbt_argv( f"sys.stdout.write({''.join(lines)!r})\n" "sys.stdout.flush()\n" f"time.sleep({sleep_seconds!r})\n" - f"sys.stdout.write({''.join(after_lines)!r})\n", + f"sys.stdout.write({''.join(after_lines)!r})\n" + f"sys.exit({returncode!r})\n", encoding="utf-8", ) return (sys.executable, str(script)) + + +def dbt_result_line(result: DbtNodeExecutionResult) -> str: + """The dbt JSON log line that streams `result`.""" + + return ( + json.dumps( + { + "data": { + "execution_time": result.execution_time, + "index": result.index, + "total": result.total, + "status": result.status, + "node_info": { + "node_name": result.node_name, + "resource_type": result.resource_type, + "node_status": result.status, + "unique_id": result.unique_id, + }, + }, + "info": {"level": "info", "name": "NodeFinished", "msg": result.status}, + } + ) + + "\n" + ) diff --git a/tests/unit/src/sqlbuild/integrations/dbt/test_execute.py b/tests/unit/src/sqlbuild/integrations/dbt/test_execute.py index da5187c81c..4893e6c29e 100644 --- a/tests/unit/src/sqlbuild/integrations/dbt/test_execute.py +++ b/tests/unit/src/sqlbuild/integrations/dbt/test_execute.py @@ -8,7 +8,6 @@ import pytest -import sqlbuild._native as event_module from sqlbuild import _native as execute_module from sqlbuild import _native as phases_module from sqlbuild import _native as plan_module @@ -45,6 +44,8 @@ from tests.unit.src.sqlbuild.integrations.dbt.helpers import ( CompileOnlyDbtRunner, build_orders_dbt_graph, + dbt_result_line, + fake_dbt_argv, ) @@ -226,15 +227,17 @@ def test_given_ordinary_execution_when_compiling_then_never_uses_dbt_state( ) def test_given_ordinary_dbt_events_when_executing_then_no_fingerprint_or_watermark_callback_is_used( test_case: DbtExecutionSummaryFooterTestCase, - monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, ) -> None: - observed: dict[str, object] = {} - - def execute_commands(**kwargs: object) -> DbtCommandExecutionResult: - observed.update(kwargs) - return DbtCommandExecutionResult(returncode=0) - - monkeypatch.setattr(phases_module, "execute_dbt_commands", execute_commands) + streamed: DbtNodeExecutionResult = DbtNodeExecutionResult( + unique_id="model.analytics.orders", + resource_type="model", + node_name="orders", + status="success", + index=1, + total=1, + execution_time=0.1, + ) command: DbtInteropCommand = DbtInteropCommand.RUN invocation: DbtInteropInvocation = DbtInteropInvocation( dbt_executable="dbt", @@ -243,19 +246,20 @@ def execute_commands(**kwargs: object) -> DbtCommandExecutionResult: routed=DbtInteropRoutedArgs(command=command), discovered_inputs=cast(DiscoveredProjectInputs, SimpleNamespace()), effective_sqlbuild_args=(), - dbt_options=DbtCliOptions(project_dir=Path("/dbt")), + dbt_options=DbtCliOptions(project_dir=tmp_path), dbt_vars={}, - runner=cast(object, SimpleNamespace()), + runner=SimpleNamespace(invoke=lambda **_: SimpleNamespace(returncode=1, stdout="")), ) result: DbtCommandExecutionResult = phases_module.execute_dbt_without_state_tracking( - request=DbtInteropExecutionRequest(command=command, project_dir=Path("/project"), args=()), + request=DbtInteropExecutionRequest(command=command, project_dir=tmp_path, args=()), invocation=invocation, - merged_dbt_argv=("dbt", "run"), + merged_dbt_argv=fake_dbt_argv(tmp_path, lines=(dbt_result_line(streamed),)), ) assert result.returncode == 0 - assert "on_node_result" not in observed + assert [node.unique_id for node in result.node_results] == [streamed.unique_id] + assert not (tmp_path / ".sqlbuild").exists() assert test_case.expected_footer == "no-state-callback" @@ -406,7 +410,7 @@ def test_given_dbt_native_state_when_building_execution_argv_then_preserves_nati ) def test_given_planned_dbt_tests_when_executing_then_does_not_resolve_selection_again( test_case: DbtExecutionSelectionStatusTestCase, - monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, ) -> None: class NoSelectionRunner: def invoke(self, **kwargs: object) -> object: @@ -437,17 +441,18 @@ def invoke(self, **kwargs: object) -> object: execution_time=0.1, ), ) - monkeypatch.setattr( - event_module, - "execute_dbt_json_event_stream", - lambda **kwargs: (0, streamed_results), - ) + lines: tuple[str, ...] = tuple(dbt_result_line(result) for result in streamed_results) progress_stream: StringIO = StringIO() result: DbtCommandExecutionResult = execute_dbt_commands( runner=cast(DbtRunner, NoSelectionRunner()), - options=DbtCliOptions(project_dir=Path("/dbt")), - merged_argv=("dbt", "test", "--select", planned_node.unique_id), + options=DbtCliOptions(project_dir=tmp_path), + merged_argv=( + *fake_dbt_argv(tmp_path, lines=lines), + "test", + "--select", + planned_node.unique_id, + ), progress_stream=progress_stream, stdout_stream=StringIO(), use_color=False, @@ -479,7 +484,7 @@ def invoke(self, **kwargs: object) -> object: ids=lambda case: case.description, ) def test_given_missing_streamed_nodes_when_finalizing_then_backfills_selected_artifact_results( - test_case: DbtResultBackfillTestCase, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + test_case: DbtResultBackfillTestCase, tmp_path: Path ) -> None: streamed: DbtNodeExecutionResult = DbtNodeExecutionResult( unique_id="model.analytics.orders", @@ -507,9 +512,6 @@ def test_given_missing_streamed_nodes_when_finalizing_then_backfills_selected_ar ), encoding="utf-8", ) - monkeypatch.setattr( - event_module, "execute_dbt_json_event_stream", lambda **kwargs: (1, (streamed,)) - ) output: StringIO = StringIO() callbacks: list[DbtNodeExecutionResult] = [] progress: list[str] = [] @@ -517,7 +519,7 @@ def test_given_missing_streamed_nodes_when_finalizing_then_backfills_selected_ar result: DbtCommandExecutionResult = execute_dbt_commands( runner=DbtRunner(), options=DbtCliOptions(target_path=tmp_path), - merged_argv=("dbt", "build"), + merged_argv=fake_dbt_argv(tmp_path, lines=(dbt_result_line(streamed),), returncode=1), progress_stream=StringIO(), stdout_stream=output, use_color=False, @@ -530,14 +532,15 @@ def test_given_missing_streamed_nodes_when_finalizing_then_backfills_selected_ar ) assert result.returncode == 1 - assert result.node_results[0] == streamed + assert result.node_results[0].unique_id == streamed.unique_id + assert result.node_results[0].status == streamed.status assert len(result.node_results) == 2 appended: DbtNodeExecutionResult = result.node_results[1] assert appended.unique_id == "model.analytics.customers" assert appended.status == "skipped" assert (appended.index, appended.total) == (2, 3) assert appended.messages[0].message == test_case.expected_message - assert callbacks == [appended] + assert callbacks == [result.node_results[0], appended] assert progress == ["Finalizing dbt run...", "Finalized dbt run."] assert "customers" in output.getvalue() assert "inventory" not in output.getvalue() diff --git a/tests/unit/src/sqlbuild/lint/test_adapter_lexical_syntax.py b/tests/unit/src/sqlbuild/lint/test_adapter_lexical_syntax.py index c723e451f2..5dc2af107a 100644 --- a/tests/unit/src/sqlbuild/lint/test_adapter_lexical_syntax.py +++ b/tests/unit/src/sqlbuild/lint/test_adapter_lexical_syntax.py @@ -4,12 +4,10 @@ from pathlib import Path from typing import ClassVar -from unittest.mock import Mock import pytest import sqlbuild._native as fixture_formatting_module -import sqlbuild._native as native_module from sqlbuild._native import DiscoveredProjectInputs, SqlLexicalSyntax, discover_project_inputs from sqlbuild.adapters.duckdb.classes.duckdb_adapter import DuckDbAdapter from tests.unit.src.sqlbuild.lint._test_types import PassedAdapterSyntaxTestCase @@ -50,26 +48,5 @@ def test_given_passed_syntax_when_formatting_fixture_nulls_then_scanner_uses_it( assert "# ) custom comment" in updated[test_file.resolve()] -@pytest.mark.parametrize( - "test_case", - [PassedAdapterSyntaxTestCase("expansion scope uses the renderer syntax", _CUSTOM_SYNTAX)], - ids=lambda case: case.description, -) -def test_given_value_renderer_when_building_expansion_context_then_scope_uses_its_syntax( - test_case: PassedAdapterSyntaxTestCase, - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - _ = write_fixture_format_project(tmp_path=tmp_path, fixture_sql="SELECT 1 AS id, TRUE AS flag") - build_scope: Mock = Mock(wraps=native_module.build_declaration_scope) - monkeypatch.setattr(native_module, "build_declaration_scope", build_scope) - - _ = native_module.build_sql_expansion_context( - project_dir=tmp_path, value_renderer=_CustomSyntaxDuckDbAdapter() - ) - - assert build_scope.call_args.kwargs["sql_lexical_syntax"] is test_case.expected_syntax - - if __name__ == "__main__": pytest.main([__file__, "-vv"]) diff --git a/tests/unit/src/sqlbuild/lint/test_cli_format.py b/tests/unit/src/sqlbuild/lint/test_cli_format.py index 364dda5bf2..a7f8cf06ea 100644 --- a/tests/unit/src/sqlbuild/lint/test_cli_format.py +++ b/tests/unit/src/sqlbuild/lint/test_cli_format.py @@ -502,7 +502,6 @@ def test_given_test_path_selector_when_formatting_then_work_is_bounded_to_select test_case: FormatSelectionWorkTestCase, tmp_path: Path, capsys: pytest.CaptureFixture[str], - monkeypatch: pytest.MonkeyPatch, ) -> None: (tmp_path / "sqlbuild_project.toml").write_text( PROJECT_TOML + '\n[defaults]\ncontract = "enforced"\n', @@ -548,11 +547,7 @@ def test_given_test_path_selector_when_formatting_then_work_is_bounded_to_select unselected_file: Path = tmp_path / "tests" / "unit" / "customers" / "test_other.sql" unselected_file.parent.mkdir() unselected_file.write_text(fixture_sql, encoding="utf-8") - native_format: Mock = Mock(wraps=native_module.format_lint_sql_bodies) - project_graph: Mock = Mock(side_effect=AssertionError("project graph must not build")) - monkeypatch.setattr(native_module, "format_lint_sql_bodies", native_format) _ = native_module.compile_stage_work_info(reset=True) - monkeypatch.setattr(native_module, "build_project_graph", project_graph) exit_code: int = main( [ @@ -567,10 +562,11 @@ def test_given_test_path_selector_when_formatting_then_work_is_bounded_to_select captured: CaptureResult[str] = capsys.readouterr() assert exit_code == 0 assert f"{test_case.selected_file_count} files checked" in captured.err - assert native_format.call_count == test_case.expected_native_batch_count - assert len(native_format.call_args.args[0]) == test_case.expected_native_file_count - assert native_module.compile_stage_work_info().get("discover_project_inputs", 0) == 0 - assert project_graph.call_count == 0 + stage_work: dict[str, int] = native_module.compile_stage_work_info() + assert stage_work.get("format_lint_batches", 0) == test_case.expected_native_batch_count + assert stage_work.get("format_lint_files", 0) == test_case.expected_native_file_count + assert stage_work.get("discover_project_inputs", 0) == 0 + assert stage_work.get("build_project_graph", 0) == 0 assert all( "CAST(NULL AS VARCHAR)" not in path.read_text(encoding="utf-8") for path in selected_files ) diff --git a/tests/unit/src/sqlbuild/rule_engine/_helpers/engine/helpers.py b/tests/unit/src/sqlbuild/rule_engine/_helpers/engine/helpers.py index 802272118d..7e27738425 100644 --- a/tests/unit/src/sqlbuild/rule_engine/_helpers/engine/helpers.py +++ b/tests/unit/src/sqlbuild/rule_engine/_helpers/engine/helpers.py @@ -1,14 +1,11 @@ """Test helpers for native rules engine boundaries.""" import json -from collections.abc import Callable from dataclasses import dataclass, field from operator import attrgetter from pathlib import Path from typing import Any, cast -import pytest - import sqlbuild._native as native from sqlbuild._native import ( MIN_CUSTOM_RULE_TEST_CASES, @@ -23,7 +20,6 @@ DiscoveredSqlTestBlock, DiscoveredSqlTestFile, Finding, - NativeRulesEvaluation, NativeRulesRequest, Rule, RuleExemption, @@ -51,10 +47,6 @@ _MODEL_SQL: str = "WITH final AS (SELECT 1 AS id) SELECT id FROM final" -_BUILD_RULES_REQUEST: Callable[..., NativeRulesRequest] = native.build_rules_request -_SCOPE_PROJECTION: Callable[..., dict[str, Any]] = native.scope_metadata_projection - - def project_with_scope(*, index: ScopeIndex) -> CompiledProjectView: """Return a minimal compiled project carrying canonical scope facts.""" @@ -67,44 +59,33 @@ def project_with_scope(*, index: ScopeIndex) -> CompiledProjectView: return project_with(project=project, scope_index=index) -def captured_native_request( - *, monkeypatch: pytest.MonkeyPatch, project: CompiledProjectView, project_dir: Path -) -> dict[str, Any]: - """Evaluate through the Python boundary and capture the header and rows handed to Rust.""" - - captured: dict[str, Any] = {} - - def scope_metadata_projection(**kwargs: Any) -> dict[str, Any]: - projection: dict[str, Any] = _SCOPE_PROJECTION(**kwargs) - captured.update(scope_index=projection) - return projection - - def build_rules_request(**kwargs: Any) -> object: - request: NativeRulesRequest = _BUILD_RULES_REQUEST(**kwargs) - captured.update(cast(dict[str, Any], json.loads(request.sql_facts_json()))) - return request - - def evaluate_rules_request(request: object) -> NativeRulesEvaluation: - return NativeRulesEvaluation( - findings=(), - selected_codes=(), - evaluated_models=0, - cache_hits=0, - cache_misses=0, - built_in_ms=0, - reused=False, - ) +def present_metadata(scope_index: dict[str, Any]) -> dict[str, Any]: + """`scope_index` with each declaration's absent metadata kinds dropped, as projected.""" + + declarations: list[dict[str, Any]] = [ + {**declaration, "metadata": _present(declaration["metadata"])} + for declaration in scope_index["declarations"] + ] + return {**scope_index, "declarations": declarations} + + +def _present(metadata: dict[str, Any]) -> dict[str, Any]: + return dict(filter(lambda item: item[1] is not None, metadata.items())) + + +def captured_native_request(*, project: CompiledProjectView, project_dir: Path) -> dict[str, Any]: + """Build the native rules request for `project` and return the facts it carries.""" - monkeypatch.setattr(native, "scope_metadata_projection", scope_metadata_projection) - monkeypatch.setattr(native, "build_rules_request", build_rules_request) - monkeypatch.setattr(native, "evaluate_rules_request", evaluate_rules_request) - native.evaluate_native_rules( + request: NativeRulesRequest = native.build_rules_request( project=project, config=RulesConfig(cache=RulesCacheConfig(enabled=False)), project_dir=project_dir, - catalogue=(), + dialect="generic", + initial_findings=(), + custom_payloads=[], + include_type_proof=False, ) - return captured + return cast(dict[str, Any], json.loads(request.sql_facts_json())) def direct_sql_test(*, mode: SqlTestMode, name: str, block_index: int) -> CompiledSqlTest: diff --git a/tests/unit/src/sqlbuild/rule_engine/_helpers/engine/test_native_facts.py b/tests/unit/src/sqlbuild/rule_engine/_helpers/engine/test_native_facts.py index e6852906cd..87df1531a2 100644 --- a/tests/unit/src/sqlbuild/rule_engine/_helpers/engine/test_native_facts.py +++ b/tests/unit/src/sqlbuild/rule_engine/_helpers/engine/test_native_facts.py @@ -79,7 +79,6 @@ def test_given_compiled_sql_facts_when_evaluating_native_then_exact_safe_rows_ar ) request: dict[str, Any] = captured_native_request( - monkeypatch=monkeypatch, project=project_with(project=project, sql_tests=sql_tests, sql_scenarios=(scenario,)), project_dir=tmp_path, ) diff --git a/tests/unit/src/sqlbuild/rule_engine/_helpers/engine/test_scope_facts.py b/tests/unit/src/sqlbuild/rule_engine/_helpers/engine/test_scope_facts.py index 50a7f3336b..b16ac8ec25 100644 --- a/tests/unit/src/sqlbuild/rule_engine/_helpers/engine/test_scope_facts.py +++ b/tests/unit/src/sqlbuild/rule_engine/_helpers/engine/test_scope_facts.py @@ -15,6 +15,7 @@ from tests.unit.src.sqlbuild.rule_engine._helpers.engine._test_types import ScopePayloadTestCase from tests.unit.src.sqlbuild.rule_engine._helpers.engine.helpers import ( captured_native_request, + present_metadata, project_with_scope, ) @@ -30,12 +31,11 @@ def test_given_compiler_scope_index_when_evaluating_native_then_payload_equals_s index: ScopeIndex = scope_index() request: dict[str, Any] = captured_native_request( - monkeypatch=monkeypatch, project=project_with_scope(index=index), project_dir=tmp_path, ) - assert (request["scope_index"] == scope_metadata_projection(index=index)) is ( + assert (present_metadata(request["scope_index"]) == scope_metadata_projection(index=index)) is ( test_case.expected_result ) assert request["scope_index"]["declarations"][0]["metadata"]["constant"] == { @@ -68,12 +68,10 @@ def test_given_complete_and_partial_scope_indexes_when_evaluating_then_completen ) complete_request: dict[str, Any] = captured_native_request( - monkeypatch=monkeypatch, project=project_with_scope(index=complete), project_dir=tmp_path, ) partial_request: dict[str, Any] = captured_native_request( - monkeypatch=monkeypatch, project=project_with_scope(index=partial), project_dir=tmp_path, ) @@ -104,12 +102,10 @@ def test_given_unsorted_and_absolute_scope_facts_when_projecting_then_order_is_s second: ScopeIndex = replace(first, declarations=tuple(reversed(first.declarations))) first_request: dict[str, Any] = captured_native_request( - monkeypatch=monkeypatch, project=project_with_scope(index=first), project_dir=tmp_path, ) second_request: dict[str, Any] = captured_native_request( - monkeypatch=monkeypatch, project=project_with_scope(index=second), project_dir=tmp_path, ) diff --git a/tests/unit/src/sqlbuild/rule_engine/_helpers/run/helpers.py b/tests/unit/src/sqlbuild/rule_engine/_helpers/run/helpers.py index 6a7b516f4f..4051569c8f 100644 --- a/tests/unit/src/sqlbuild/rule_engine/_helpers/run/helpers.py +++ b/tests/unit/src/sqlbuild/rule_engine/_helpers/run/helpers.py @@ -1,25 +1,12 @@ -from collections.abc import Callable - import pytest -import sqlbuild._native as native from sqlbuild import _native -from sqlbuild._native import NativeRulesEvaluation, NativeRulesRequest - - -def record_reuse(*, monkeypatch: pytest.MonkeyPatch) -> list[bool]: - """Record whether each natively evaluated request was answered from the response memo.""" - reused: list[bool] = [] - evaluate: Callable[[NativeRulesRequest], NativeRulesEvaluation] = _native.evaluate_rules_request - def recording(request: NativeRulesRequest) -> NativeRulesEvaluation: - evaluation: NativeRulesEvaluation = evaluate(request) - reused.append(evaluation.reused) - return evaluation +def memo_reuses() -> int: + """Native rules evaluations answered from the response memo since the last reset.""" - monkeypatch.setattr(native, "evaluate_rules_request", recording) - return reused + return _native.compile_stage_work_info().get("rules_memo_reuses", 0) def rebuild_identity(*, monkeypatch: pytest.MonkeyPatch) -> None: diff --git a/tests/unit/src/sqlbuild/rule_engine/_helpers/run/test_native_rows.py b/tests/unit/src/sqlbuild/rule_engine/_helpers/run/test_native_rows.py index 3e1d2306a4..fb97c862b8 100644 --- a/tests/unit/src/sqlbuild/rule_engine/_helpers/run/test_native_rows.py +++ b/tests/unit/src/sqlbuild/rule_engine/_helpers/run/test_native_rows.py @@ -13,8 +13,8 @@ NativeRowsMemoTestCase, ) from tests.unit.src.sqlbuild.rule_engine._helpers.run.helpers import ( + memo_reuses, rebuild_identity, - record_reuse, ) from tests.unit.src.sqlbuild.rule_engine.main.evaluate.helpers import build_project @@ -59,19 +59,23 @@ def test_given_memoized_response_from_another_build_when_evaluating_natively_the tmp_path: Path, test_case: NativeRowsMemoTestCase, ) -> None: - reused: list[bool] = record_reuse(monkeypatch=monkeypatch) + _ = native.compile_stage_work_info(reset=True) + reused: list[bool] = [] config_values: dict[str, object] = {"materialized": "table"} cold: RulesResult = evaluate_contract_rule( config_values=config_values, project_dir=tmp_path, cache_enabled=True ) + reused.append(memo_reuses() == 1) rebuild_identity(monkeypatch=monkeypatch) rebuilt: RulesResult = evaluate_contract_rule( config_values=config_values, project_dir=tmp_path, cache_enabled=True ) + reused.append(memo_reuses() == 1) warm: RulesResult = evaluate_contract_rule( config_values=config_values, project_dir=tmp_path, cache_enabled=True ) + reused.append(memo_reuses() == 1) assert tuple(reused) == test_case.expected_reused assert rebuilt.findings == warm.findings == cold.findings