diff --git a/crates/sqlbuild-analysis/src/assembly/analysis_session/main/contract_proof_json.rs b/crates/sqlbuild-analysis/src/assembly/analysis_session/main/contract_proof_json.rs new file mode 100644 index 0000000000..75aff2b946 --- /dev/null +++ b/crates/sqlbuild-analysis/src/assembly/analysis_session/main/contract_proof_json.rs @@ -0,0 +1,64 @@ +//! A dynamic pivot proof as `json.dumps(asdict(proof), sort_keys=True, separators=(",", ":"))`. + +use sqlbuild_core::json::main::dumps::dumps; +use sqlbuild_core::json::models::{JsonDialect, JsonValue, StdlibJsonOptions}; + +use crate::assembly::analysis_session::models::ContractProof; + +/// The proof's canonical JSON text, as the SQL rule cache keys a model's contract. +pub fn contract_proof_json(proof: &ContractProof) -> String { + let text = |value: &str| JsonValue::String(value.to_owned()); + let optional = |value: Option<&String>| value.map_or(JsonValue::Null, |value| text(value)); + let fixed: Vec = proof + .fixed_columns + .iter() + .map(|column| { + JsonValue::Object(vec![ + ("name".to_owned(), text(&column.name)), + ("nullability".to_owned(), text(&column.nullability)), + ("type".to_owned(), optional(column.data_type.as_ref())), + ]) + }) + .collect(); + let families: Vec = proof + .families + .iter() + .map(|(name, inferred_type)| { + JsonValue::Object(vec![ + ("inferred_type".to_owned(), optional(inferred_type.as_ref())), + ("name".to_owned(), text(name)), + ]) + }) + .collect(); + let value: JsonValue = JsonValue::Object(vec![ + ( + "bare_dynamic_pivot".to_owned(), + JsonValue::Bool(proof.bare_dynamic_pivot), + ), + ( + "failure_reason".to_owned(), + optional(proof.failure_reason.as_ref()), + ), + ("families".to_owned(), JsonValue::Array(families)), + ("fixed_columns".to_owned(), JsonValue::Array(fixed)), + ( + "input_relations".to_owned(), + JsonValue::Array( + proof + .input_relations + .iter() + .map(|name| text(name)) + .collect(), + ), + ), + ( + "output_proven".to_owned(), + JsonValue::Bool(proof.output_proven), + ), + ]); + let options: StdlibJsonOptions = StdlibJsonOptions { + sort_keys: true, + ..StdlibJsonOptions::new(None).with_separators(",", ":") + }; + dumps(&value, &JsonDialect::Stdlib(options)).unwrap_or_default() +} diff --git a/crates/sqlbuild-analysis/src/assembly/analysis_session/main/mod.rs b/crates/sqlbuild-analysis/src/assembly/analysis_session/main/mod.rs index d4e0b4a64d..92e7d34f8d 100644 --- a/crates/sqlbuild-analysis/src/assembly/analysis_session/main/mod.rs +++ b/crates/sqlbuild-analysis/src/assembly/analysis_session/main/mod.rs @@ -1,4 +1,5 @@ pub mod attach_analysis_cache; +pub mod contract_proof_json; pub mod expression_shapes; pub mod finish_analysis_session; pub mod finished_fact_models; diff --git a/crates/sqlbuild-analysis/src/assembly/analysis_session/tests.rs b/crates/sqlbuild-analysis/src/assembly/analysis_session/tests.rs index d3db69bdad..1f58431987 100644 --- a/crates/sqlbuild-analysis/src/assembly/analysis_session/tests.rs +++ b/crates/sqlbuild-analysis/src/assembly/analysis_session/tests.rs @@ -2,6 +2,8 @@ mod analysis_cache; #[path = "tests/test_analysis_session.rs"] mod analysis_session; +#[path = "tests/test_contract_proof_json.rs"] +mod contract_proof_json; #[path = "tests/test_expression_shapes.rs"] mod expression_shapes; #[path = "tests/helpers.rs"] diff --git a/crates/sqlbuild-analysis/src/assembly/analysis_session/tests/test_contract_proof_json.rs b/crates/sqlbuild-analysis/src/assembly/analysis_session/tests/test_contract_proof_json.rs new file mode 100644 index 0000000000..842ba847ba --- /dev/null +++ b/crates/sqlbuild-analysis/src/assembly/analysis_session/tests/test_contract_proof_json.rs @@ -0,0 +1,53 @@ +use crate::assembly::analysis_session::main::contract_proof_json::contract_proof_json; +use crate::assembly::analysis_session::tests::test_types::ContractProofJsonTestCase; + +use crate::assembly::analysis_session::models::{ColumnFact, ContractProof}; + +#[test] +fn given_contract_proof_when_encoding_then_matches_python_sorted_compact_json() { + let test_cases = [ + ContractProofJsonTestCase { + description: "a proven pivot escapes non-ASCII text as Python does", + proof: ContractProof { + output_proven: true, + fixed_columns: vec![ColumnFact { + name: "a\u{e9}\"".to_owned(), + data_type: None, + nullability: "non_null".to_owned(), + }], + families: vec![("f".to_owned(), Some("INT".to_owned()))], + input_relations: vec!["x".to_owned()], + failure_reason: None, + bare_dynamic_pivot: false, + }, + expected_json: concat!( + r#"{"bare_dynamic_pivot":false,"failure_reason":null,"#, + r#""families":[{"inferred_type":"INT","name":"f"}],"#, + r#""fixed_columns":[{"name":"a\u00e9\"","nullability":"non_null","type":null}],"#, + r#""input_relations":["x"],"output_proven":true}"# + ), + }, + ContractProofJsonTestCase { + description: "a failed proof keeps its reason and empty collections", + proof: ContractProof { + output_proven: false, + fixed_columns: Vec::new(), + families: vec![("g".to_owned(), None)], + input_relations: Vec::new(), + failure_reason: Some("no schema".to_owned()), + bare_dynamic_pivot: true, + }, + expected_json: concat!( + r#"{"bare_dynamic_pivot":true,"failure_reason":"no schema","#, + r#""families":[{"inferred_type":null,"name":"g"}],"fixed_columns":[],"#, + r#""input_relations":[],"output_proven":false}"# + ), + }, + ]; + + for test_case in test_cases { + let json: String = contract_proof_json(&test_case.proof); + + assert_eq!(json, test_case.expected_json, "{}", test_case.description); + } +} diff --git a/crates/sqlbuild-analysis/src/assembly/analysis_session/tests/test_types.rs b/crates/sqlbuild-analysis/src/assembly/analysis_session/tests/test_types.rs index a25f7ee25e..437696de9a 100644 --- a/crates/sqlbuild-analysis/src/assembly/analysis_session/tests/test_types.rs +++ b/crates/sqlbuild-analysis/src/assembly/analysis_session/tests/test_types.rs @@ -1,4 +1,4 @@ -use crate::assembly::analysis_session::models::PivotOutcome; +use crate::assembly::analysis_session::models::{ContractProof, PivotOutcome}; /// A model as `(name, sql, source names, model refs)`. pub(crate) type ModelSpec = ( @@ -183,3 +183,9 @@ pub(crate) struct NativeDialectTestCase { pub(crate) dialect: Option<&'static str>, pub(crate) expected_dialect: Option<&'static str>, } + +pub(crate) struct ContractProofJsonTestCase { + pub(crate) description: &'static str, + pub(crate) proof: ContractProof, + pub(crate) expected_json: &'static str, +} 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 d14c6b8b17..4402d6e877 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/analysis/binding_catalog.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/analysis/binding_catalog.rs @@ -11,6 +11,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; use crate::bindings::models::{CatalogInput, ProjectCatalog}; @@ -88,7 +89,7 @@ impl BindingCatalog { let schemas: &Bound<'_, PyDict> = self.schemas.bind(py); let additions: Bound<'_, PyDict> = PyDict::new(py); for request in requests { - let schema: Bound<'_, PyAny> = request.getattr("schema")?; + let schema: Bound<'_, PyAny> = native_field(request, "schema")?; for item in schema.call_method0("items")?.try_iter()? { let (name, columns): (Bound<'_, PyAny>, Bound<'_, PyAny>) = item?.extract()?; if columns.is_truthy()? && !schemas.contains(&name)? { @@ -103,7 +104,7 @@ impl BindingCatalog { } let mut prepared: Vec = Vec::with_capacity(requests.len()); for request in requests { - let schema: Bound<'_, PyAny> = request.getattr("schema")?; + let schema: Bound<'_, PyAny> = native_field(request, "schema")?; let mut names: Vec = Vec::new(); for name in schema.try_iter()? { names.push(name?.extract()?); @@ -123,7 +124,7 @@ impl BindingCatalog { } closed.push((name, truthy)); } - let sql: String = request.getattr("sql")?.extract()?; + let sql: String = native_field(request, "sql")?.extract()?; prepared.push((sql, closed, conversions::relations(overrides.as_any())?)); } Ok(prepared) @@ -293,14 +294,14 @@ pub(crate) fn schema_validations<'py>( let mut groups: Vec<(Py, bool, Vec)> = Vec::new(); for (index, request) in requests.iter().enumerate() { let key: CatalogKey = ( - native_dialect(&request.getattr("dialect")?)?, + native_dialect(&native_field(request, "dialect")?)?, request .getattr("quoted_identifiers_ignore_case")? .extract()?, - request.getattr("known_functions")?.extract()?, - request.getattr("known_types")?.extract()?, + native_field(request, "known_functions")?.extract()?, + native_field(request, "known_types")?.extract()?, ); - let explicit: Bound<'py, PyAny> = request.getattr("catalog")?; + let explicit: Bound<'py, PyAny> = native_field(request, "catalog")?; let own: bool = !explicit.is_none(); let catalog: Py = if own { explicit.extract()? @@ -309,7 +310,7 @@ pub(crate) fn schema_validations<'py>( } else { let catalog: Py = Py::new( py, - BindingCatalog::create(py, key.clone(), &request.getattr("schema")?)?, + BindingCatalog::create(py, key.clone(), &native_field(request, "schema")?)?, )?; created.push((key, catalog.clone_ref(py))); catalog 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 f7b8cbfe1d..1ddd6ac196 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/analysis/explicit_references.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/analysis/explicit_references.rs @@ -28,6 +28,7 @@ use crate::bindings::_helpers::boundary::panics::compiler_error; use crate::bindings::_helpers::compile_driver::python_calls::imported; 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::registry; use crate::bindings::types::CompilerDetach; @@ -65,8 +66,8 @@ impl<'py> PythonSqlOwner<'py> { Ok(Self { label, kind, - function: node.getattr("function")?, - relative_path: node.getattr("relative_path")?, + function: native_field(node, "function")?, + relative_path: native_field(node, "relative_path")?, own_refs: Vec::new(), upstream_loader_by_source: Vec::new(), }) @@ -117,7 +118,7 @@ fn explicit_reference_report<'py>( } let request = ReferenceRequest { enforce_explicit, - dialect: project.getattr("sql_analysis_dialect")?.extract()?, + dialect: native_field(project, "sql_analysis_dialect")?.extract()?, explicit_help: if enforce_explicit { references_help(HARD_CODED_ALLOWED) } else { @@ -158,7 +159,7 @@ fn python_sql_owners<'py>( for (label, attribute) in NODE_OWNERS { for node in discovered_inputs.getattr(attribute)?.try_iter()? { let node: Bound<'py, PyAny> = node?; - let name: String = node.getattr("name")?.extract()?; + let name: String = native_field(&node, "name")?.extract()?; owners.push(PythonSqlOwner::new( format!("{label}:{name}"), NODE_KIND, @@ -169,11 +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 project.getattr("models")?.try_iter()? { + 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 = model.getattr("name")?.extract()?; + let model_name: String = native_field(&model, "name")?.extract()?; for hook_name in hook_names.call((), Some(&keywords))?.try_iter()? { attached .entry(hook_name?.extract()?) @@ -181,9 +182,9 @@ fn python_sql_owners<'py>( .insert(model_name.clone()); } } - for hook in project.getattr("hook_functions")?.try_iter()? { + for hook in native_field(project, "hook_functions")?.try_iter()? { let hook: Bound<'py, PyAny> = hook?; - let name: String = hook.getattr("name")?.extract()?; + let name: String = native_field(&hook, "name")?.extract()?; let mut owner = PythonSqlOwner::new(format!("hook:{name}"), HOOK_KIND, &hook)?; owner.own_refs = attached .get(&name) @@ -203,7 +204,7 @@ fn loader_owners<'py>( ) -> PyResult>> { let py: Python<'py> = project.py(); let mut sources_by_loader: BTreeMap> = BTreeMap::new(); - for source in project.getattr("sources")?.try_iter()? { + for source in native_field(project, "sources")?.try_iter()? { let source: Bound<'py, PyAny> = source?; let loader: Option = source .getattr("source_entry")? @@ -213,16 +214,16 @@ fn loader_owners<'py>( sources_by_loader .entry(loader) .or_default() - .insert(source.getattr("name")?.extract()?); + .insert(native_field(&source, "name")?.extract()?); } } let read_definition = host::entry(py, UserCodeHost::ReadLoaderDefinition)?; let mut owners: Vec> = Vec::new(); - for loader in discovered_inputs.getattr("loader_functions")?.try_iter()? { + for loader in native_field(discovered_inputs, "loader_functions")?.try_iter()? { let loader: Bound<'py, PyAny> = loader?; - let name: String = loader.getattr("name")?.extract()?; + let name: String = native_field(&loader, "name")?.extract()?; let mut upstream: Vec<(String, String)> = Vec::new(); - for dependency in loader.getattr("depends_on")?.try_iter()? { + for dependency in native_field(&loader, "depends_on")?.try_iter()? { let dependency: Bound<'py, PyAny> = dependency?; if !dependency.is_callable() { continue; @@ -231,7 +232,7 @@ fn loader_owners<'py>( if definition.is_none() { continue; } - let definition_name: String = definition.getattr("name")?.extract()?; + let definition_name: String = native_field(&definition, "name")?.extract()?; let shown: String = match dependency.getattr_opt("__name__")? { Some(shown) => shown.extract()?, None => definition_name.clone(), @@ -341,7 +342,7 @@ fn python_report<'py>( } let literal_relation: Bound<'py, PyAny> = registry::class(reference_models.py(), RecordId::LiteralSqlRelation)?; - let relation_name: Bound<'py, PyAny> = reference_models.getattr("RelationName")?; + 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 { let owner: &PythonSqlOwner<'py> = &owners[row.owner]; @@ -392,8 +393,8 @@ pub(crate) fn hard_coded_relation_remedy( upstream_loader_by_source: Option<&Bound<'_, PyAny>>, ) -> PyResult { let target: ResourceKey = ( - r#ref.getattr("kind")?.getattr("value")?.extract()?, - r#ref.getattr("name")?.extract()?, + native_field(r#ref, "kind")?.getattr("value")?.extract()?, + native_field(r#ref, "name")?.extract()?, ); let mut upstream_loaders: Vec<(String, String)> = Vec::new(); if let Some(loaders) = upstream_loader_by_source.filter(|loaders| !loaders.is_none()) { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/analysis/model_reads.rs b/crates/sqlbuild-python/src/bindings/_helpers/analysis/model_reads.rs index 747d81f21a..87f5a5e8fb 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/analysis/model_reads.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/analysis/model_reads.rs @@ -5,6 +5,7 @@ use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; use crate::bindings::_helpers::compiled_project::compiled_model_view::{ DeclaredRow, InferredRow, LineageInput, NativeCompiledModel, }; +use crate::bindings::_helpers::records::fields::field as native_field; /// `(output, transform kind, confidence, [(resource type, resource, column)])` of one lineage output. pub(crate) type LineageFactRow = (String, String, String, Vec<(String, String, String)>); @@ -24,7 +25,7 @@ pub(crate) fn inferred_rows(model: &Bound<'_, PyAny>) -> PyResult() { return view.get().inferred_rows(model.py()); } - let rows: Bound<'_, PyAny> = model.getattr("inferred_rows")?; + let rows: Bound<'_, PyAny> = native_field(model, "inferred_rows")?; if rows.is_none() { return Ok(None); } @@ -51,7 +52,7 @@ pub(crate) fn declared_rows(model: &Bound<'_, PyAny>) -> PyResult() { return view.get().schema_column_rows(model.py()); } - model.getattr("schema_column_rows")?.extract() + native_field(model, "schema_column_rows")?.extract() } /// Whether the attached schema declares dynamic column families. @@ -59,7 +60,7 @@ pub(crate) fn dynamic_columns_declared(model: &Bound<'_, PyAny>) -> PyResult() { return view.get().dynamic_columns_declared(model.py()); } - model.getattr("has_dynamic_columns")?.is_truthy() + native_field(model, "has_dynamic_columns")?.is_truthy() } /// `(has star, star resolved)` of the model's analysed query. @@ -72,8 +73,8 @@ pub(crate) fn star_flags(model: &Bound<'_, PyAny>) -> PyResult<(bool, bool)> { )); } Ok(( - model.getattr("fast_lineage_has_star")?.is_truthy()?, - model.getattr("fast_lineage_star_resolved")?.is_truthy()?, + native_field(model, "fast_lineage_has_star")?.is_truthy()?, + native_field(model, "fast_lineage_star_resolved")?.is_truthy()?, )) } @@ -94,7 +95,7 @@ pub(crate) fn lineage_inputs(model: &Bound<'_, PyAny>) -> PyResult() { return Ok(view.get().lineage_inputs(model.py())?.unwrap_or_default()); } - let columns: Bound<'_, PyAny> = model.getattr("fast_lineage_columns")?; + let columns: Bound<'_, PyAny> = native_field(model, "fast_lineage_columns")?; if !columns.is_truthy()? { return Ok(Vec::new()); } @@ -102,14 +103,14 @@ pub(crate) fn lineage_inputs(model: &Bound<'_, PyAny>) -> PyResult = output?; let mut upstream: Vec<(String, String)> = Vec::new(); - for source in output.getattr("upstream_columns")?.try_iter()? { + for source in native_field(&output, "upstream_columns")?.try_iter()? { let source: Bound<'_, PyAny> = source?; upstream.push(( - source.getattr("resource_name")?.extract()?, - source.getattr("column_name")?.extract()?, + native_field(&source, "resource_name")?.extract()?, + native_field(&source, "column_name")?.extract()?, )); } - lineage.push((output.getattr("output_column")?.extract()?, upstream)); + lineage.push((native_field(&output, "output_column")?.extract()?, upstream)); } Ok(lineage) } @@ -119,7 +120,7 @@ pub(crate) fn binding_validated(model: &Bound<'_, PyAny>) -> PyResult { if let Ok(view) = model.downcast::() { return view.get().binding_validated(model.py()); } - model.getattr("binding_validated")?.is_truthy() + native_field(model, "binding_validated")?.is_truthy() } /// Whether the model's dynamic pivot proof proved its output columns. @@ -127,8 +128,8 @@ pub(crate) fn output_proven(model: &Bound<'_, PyAny>) -> PyResult { if let Ok(view) = model.downcast::() { return view.get().output_proven(model.py()); } - let proof: Bound<'_, PyAny> = model.getattr("dynamic_column_contract")?; - Ok(!proof.is_none() && proof.getattr("output_proven")?.is_truthy()?) + let proof: Bound<'_, PyAny> = native_field(model, "dynamic_column_contract")?; + Ok(!proof.is_none() && native_field(&proof, "output_proven")?.is_truthy()?) } /// Each lineage output with its transform kind and confidence values, `None` without lineage. @@ -148,7 +149,7 @@ pub(crate) fn lineage_facts(model: &Bound<'_, PyAny>) -> PyResult = model.getattr("fast_lineage_columns")?; + let columns: Bound<'_, PyAny> = native_field(model, "fast_lineage_columns")?; if columns.is_none() { return Ok(None); } @@ -156,18 +157,18 @@ pub(crate) fn lineage_facts(model: &Bound<'_, PyAny>) -> PyResult = output?; let mut sources: Vec<(String, String, String)> = Vec::new(); - for source in output.getattr("upstream_columns")?.try_iter()? { + for source in native_field(&output, "upstream_columns")?.try_iter()? { let source: Bound<'_, PyAny> = source?; sources.push(( - source.getattr("resource_type")?.extract()?, - source.getattr("resource_name")?.extract()?, - source.getattr("column_name")?.extract()?, + native_field(&source, "resource_type")?.extract()?, + native_field(&source, "resource_name")?.extract()?, + native_field(&source, "column_name")?.extract()?, )); } facts.push(( - output.getattr("output_column")?.extract()?, - output.getattr("transform_kind")?.str()?.extract()?, - output.getattr("confidence")?.str()?.extract()?, + native_field(&output, "output_column")?.extract()?, + native_field(&output, "transform_kind")?.str()?.extract()?, + native_field(&output, "confidence")?.str()?.extract()?, sources, )); } @@ -186,11 +187,11 @@ pub(crate) fn proven_inputs(model: &Bound<'_, PyAny>) -> PyResult() { return view.get().proven_inputs(model.py()); } - let proof: Bound<'_, PyAny> = model.getattr("dynamic_column_contract")?; - if proof.is_none() || !proof.getattr("output_proven")?.is_truthy()? { + let proof: Bound<'_, PyAny> = native_field(model, "dynamic_column_contract")?; + if proof.is_none() || !native_field(&proof, "output_proven")?.is_truthy()? { return Ok(None); } - proof.getattr("input_relations")?.extract().map(Some) + native_field(&proof, "input_relations")?.extract().map(Some) } /// The model's name. @@ -198,7 +199,7 @@ pub(crate) fn model_name(model: &Bound<'_, PyAny>) -> PyResult { if let Ok(view) = model.downcast::() { return view.get().name(model.py()); } - model.getattr("name")?.extract() + native_field(model, "name")?.extract() } /// The model's compiled query SQL. @@ -210,7 +211,7 @@ pub(crate) fn query_sql<'py>(model: &Bound<'py, PyAny>) -> PyResult(model: &Bound<'py, PyAny>) -> PyResult() { return Ok(view.get().key(model.py())?.into_bound(model.py())); } - model.getattr("key") + native_field(model, "key") } /// The model's destination `CompiledRelationLocation`. @@ -226,7 +227,7 @@ pub(crate) fn destination<'py>(model: &Bound<'py, PyAny>) -> PyResult() { return Ok(view.get().destination(model.py())?.into_bound(model.py())); } - model.getattr("destination") + native_field(model, "destination") } /// The model's name as a Python string. @@ -235,5 +236,5 @@ pub(crate) fn name_object<'py>(model: &Bound<'py, PyAny>) -> PyResult = HookEntries::imported(py)?; let mut resources: Vec = Vec::new(); let mut models: Vec = Vec::new(); - for model in project.getattr("models")?.try_iter()? { + for model in native_field(project, "models")?.try_iter()? { let model: Bound<'_, PyAny> = model?; resources.push(GraphResource { - key: model.getattr("key_row")?.extract()?, - deps: model.getattr("dep_rows")?.extract()?, + key: native_field(&model, "key_row")?.extract()?, + deps: native_field(&model, "dep_rows")?.extract()?, }); models.push(model_references(&model, &entries, retains_old_names)?); } let mut sources: Vec = Vec::new(); - for source in project.getattr("sources")?.try_iter()? { + for source in native_field(project, "sources")?.try_iter()? { let source: Bound<'_, PyAny> = source?; resources.push(graph_resource(&source)?); - let entry: Bound<'_, PyAny> = source.getattr("source_entry")?; + let entry: Bound<'_, PyAny> = native_field(&source, "source_entry")?; sources.push(SourceRelation { - name: source.getattr("name")?.extract()?, - table: entry.getattr("table")?.extract()?, - schema: entry.getattr("schema")?.extract()?, - database: entry.getattr("database")?.extract()?, - expression: !entry.getattr("expression")?.is_none(), + name: native_field(&source, "name")?.extract()?, + table: native_field(&entry, "table")?.extract()?, + schema: native_field(&entry, "schema")?.extract()?, + database: native_field(&entry, "database")?.extract()?, + expression: !native_field(&entry, "expression")?.is_none(), }); } let mut seeds: Vec = Vec::new(); - for seed in project.getattr("seeds")?.try_iter()? { + for seed in native_field(project, "seeds")?.try_iter()? { let seed: Bound<'_, PyAny> = seed?; resources.push(graph_resource(&seed)?); - let destination: Bound<'_, PyAny> = seed.getattr("destination")?; + let destination: Bound<'_, PyAny> = native_field(&seed, "destination")?; seeds.push(NamedRelation { - name: seed.getattr("name")?.extract()?, + name: native_field(&seed, "name")?.extract()?, relation: Relation { - name: destination.getattr("name")?.extract()?, - schema: destination.getattr("schema")?.extract()?, - database: destination.getattr("database")?.extract()?, + name: native_field(&destination, "name")?.extract()?, + schema: native_field(&destination, "schema")?.extract()?, + database: native_field(&destination, "database")?.extract()?, }, }); } - for function in project.getattr("functions")?.try_iter()? { + for function in native_field(project, "functions")?.try_iter()? { resources.push(graph_resource(&function?)?); } let mut audits: Vec = Vec::new(); - for audit in project.getattr("audits")?.try_iter()? { + for audit in native_field(project, "audits")?.try_iter()? { audits.push(audit_gate(&audit?)?); } let mut hooks: Vec = Vec::new(); - for hook in project.getattr("hook_functions")?.try_iter()? { + for hook in native_field(project, "hook_functions")?.try_iter()? { let hook: Bound<'_, PyAny> = hook?; hooks.push(PythonHook { - name: hook.getattr("name")?.extract()?, + name: native_field(&hook, "name")?.extract()?, path: hook .getattr("relative_path")? .call_method0("as_posix")? .extract()?, - reads: ref_keys(&hook.getattr("reads")?)?, + reads: ref_keys(&native_field(&hook, "reads")?)?, }); } Ok(ProjectFacts { @@ -118,35 +119,35 @@ fn model_references( entries: &HookEntries<'_>, retains_old_names: bool, ) -> PyResult { - let values: Bound<'_, PyAny> = model.getattr("config")?.getattr("values")?; + let values: Bound<'_, PyAny> = native_field(model, "config")?.getattr("values")?; let (database, schema, name): (Option, Option, String) = - model.getattr("destination_row")?.extract()?; + native_field(model, "destination_row")?.extract()?; let mut sql_hooks: Vec = Vec::new(); for hook_key in MODEL_HOOK_KEYS { for (index, entry) in hook_items(&values.call_method1("get", (hook_key,))?)? .into_iter() .enumerate() { - if !entry.is_instance(&entries.sql)? || !entry.getattr("reads")?.is_truthy()? { + if !entry.is_instance(&entries.sql)? || !native_field(&entry, "reads")?.is_truthy()? { continue; } - let path: Bound<'_, PyAny> = entry.getattr("relative_path")?; + let path: Bound<'_, PyAny> = native_field(&entry, "relative_path")?; sql_hooks.push(SqlHook { hook_key: hook_key.to_owned(), index, - name: entry.getattr("name")?.extract()?, + name: native_field(&entry, "name")?.extract()?, path: if path.is_none() { None } else { Some(path.str()?.extract()?) }, - reads: ref_keys(&entry.getattr("reads")?)?, + reads: ref_keys(&native_field(&entry, "reads")?)?, }); } } Ok(ModelReferences { - name: model.getattr("name")?.extract()?, - path: model.getattr("relative_path")?.str()?.extract()?, + name: native_field(model, "name")?.extract()?, + path: native_field(model, "relative_path")?.str()?.extract()?, destination: Relation { name, schema, @@ -177,7 +178,7 @@ fn python_hook_names( if !item.is_instance(&entries.python)? { continue; } - let name: String = item.getattr("name")?.extract()?; + let name: String = native_field(&item, "name")?.extract()?; if !names.contains(&name) { names.push(name); } @@ -215,37 +216,37 @@ fn migrate_from(values: &Bound<'_, PyAny>, retains_old_names: bool) -> PyResult< } fn audit_gate(audit: &Bound<'_, PyAny>) -> PyResult { - let kind: Bound<'_, PyAny> = audit.getattr("attached_target_kind")?; - let name: Bound<'_, PyAny> = audit.getattr("attached_target_name")?; + let kind: Bound<'_, PyAny> = native_field(audit, "attached_target_kind")?; + let name: Bound<'_, PyAny> = native_field(audit, "attached_target_name")?; let target: Option = if kind.is_none() || name.is_none() { None } else { Some((kind.str()?.extract()?, name.extract()?)) }; Ok(AuditGate { - name: audit.getattr("name")?.extract()?, + name: native_field(audit, "name")?.extract()?, path: audit .getattr("audit_file")? .getattr("relative_path")? .str()? .extract()?, target, - scope_deps: object_keys(&audit.getattr("scope_deps")?)?, + scope_deps: object_keys(&native_field(audit, "scope_deps")?)?, }) } fn graph_resource(resource: &Bound<'_, PyAny>) -> PyResult { Ok(GraphResource { - key: object_key(&resource.getattr("key")?)?, - deps: object_keys(&resource.getattr("deps")?)?, + key: object_key(&native_field(resource, "key")?)?, + deps: object_keys(&native_field(resource, "deps")?)?, }) } /// A `CompiledObjectKey` as its `(resource type, name)` pair. fn object_key(key: &Bound<'_, PyAny>) -> PyResult { Ok(( - key.getattr("resource_type")?.str()?.extract()?, - key.getattr("name")?.extract()?, + native_field(key, "resource_type")?.str()?.extract()?, + native_field(key, "name")?.extract()?, )) } @@ -259,8 +260,8 @@ pub(crate) fn ref_keys(refs: &Bound<'_, PyAny>) -> PyResult> { .map(|reference| { let reference: Bound<'_, PyAny> = reference?; Ok(( - reference.getattr("kind")?.str()?.extract()?, - reference.getattr("name")?.extract()?, + native_field(&reference, "kind")?.str()?.extract()?, + native_field(&reference, "name")?.extract()?, )) }) .collect() 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 6a4870fc49..ca168965cf 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/analysis/required_columns.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/analysis/required_columns.rs @@ -10,6 +10,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; use crate::bindings::types::CompilerDetach; @@ -27,8 +28,8 @@ fn analyze_resolved_column_reads<'py>( for reference in references.try_iter()? { let reference: Bound<'py, PyAny> = reference?; inputs.push(ReferenceInput { - kind: reference.getattr("ref_kind")?.str()?.extract()?, - name: reference.getattr("ref_name")?.extract()?, + kind: native_field(&reference, "ref_kind")?.str()?.extract()?, + name: native_field(&reference, "ref_name")?.extract()?, }); } let reads: Vec = py diff --git a/crates/sqlbuild-python/src/bindings/_helpers/analysis/sql_test_comparisons.rs b/crates/sqlbuild-python/src/bindings/_helpers/analysis/sql_test_comparisons.rs index c45df16436..f0680c0d38 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/analysis/sql_test_comparisons.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/analysis/sql_test_comparisons.rs @@ -11,6 +11,7 @@ use sqlbuild_analysis::compiler::main::sql_test_rendering::render_json; use crate::bindings::_helpers::boundary::keyword_request::Request; use crate::bindings::_helpers::boundary::panics::value_error; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::types::CompilerDetach; const RENDER_WORKERS: usize = 4; @@ -133,17 +134,17 @@ pub(crate) fn comparison_request( .map(|assertion| { let assertion: Bound<'_, PyAny> = assertion?; Ok(json!({ - "name": assertion.getattr("name")?.extract::()?, - "resolvedSql": assertion.getattr("resolved_sql")?.extract::()?, - "liftedCtes": assertion.getattr("lifted_ctes")?.extract::>()?, - "comparisonBodySql": assertion.getattr("comparison_body_sql")?.extract::>()?, + "name": native_field(&assertion, "name")?.extract::()?, + "resolvedSql": native_field(&assertion, "resolved_sql")?.extract::()?, + "liftedCtes": native_field(&assertion, "lifted_ctes")?.extract::>()?, + "comparisonBodySql": native_field(&assertion, "comparison_body_sql")?.extract::>()?, })) }) .collect::>()?; Ok(json!({ "chain": chain, "assertions": assertions, - "sqlAnalysisEnabled": entry.getattr("sql_analysis_enabled")?.extract::()?, + "sqlAnalysisEnabled": native_field(entry, "sql_analysis_enabled")?.extract::()?, "setDifferenceOperator": set_difference_operator, "sqlAnalysisDialect": sql_analysis_dialect, })) @@ -152,13 +153,13 @@ pub(crate) fn comparison_request( /// The native request of one planned chain step. pub(crate) fn chain_step_request(step: &Bound<'_, PyAny>) -> PyResult { Ok(json!({ - "modelName": step.getattr("model_name")?.extract::()?, - "resolvedSql": step.getattr("resolved_sql")?.extract::()?, - "expectedCteSql": step.getattr("expected_cte_sql")?.extract::>()?, - "liftedCtes": step.getattr("lifted_ctes")?.extract::>()?, - "comparisonBodySql": step.getattr("comparison_body_sql")?.extract::>()?, - "expectedColumns": step.getattr("expected_columns")?.extract::>>()?, - "expectedLiftedCtes": step.getattr("expected_lifted_ctes")?.extract::>()?, + "modelName": native_field(step, "model_name")?.extract::()?, + "resolvedSql": native_field(step, "resolved_sql")?.extract::()?, + "expectedCteSql": native_field(step, "expected_cte_sql")?.extract::>()?, + "liftedCtes": native_field(step, "lifted_ctes")?.extract::>()?, + "comparisonBodySql": native_field(step, "comparison_body_sql")?.extract::>()?, + "expectedColumns": native_field(step, "expected_columns")?.extract::>>()?, + "expectedLiftedCtes": native_field(step, "expected_lifted_ctes")?.extract::>()?, })) } @@ -172,13 +173,13 @@ fn render_sql_test_difference_sample_sql( let request = Request::new("render_sql_test_difference_sample_sql", options)?; let test_entry: Bound<'_, PyAny> = request.get("test_entry")?; let step: Bound<'_, PyAny> = request.get("step")?; - if step.getattr("expected_cte_sql")?.is_none() { + if native_field(&step, "expected_cte_sql")?.is_none() { return Ok(String::new()); } let settings: RenderSettings = RenderSettings::read(options)?; let request_json: String = serde_json::to_string(&json!({ "step": chain_step_request(&step)?, - "sqlAnalysisEnabled": test_entry.getattr("sql_analysis_enabled")?.extract::()?, + "sqlAnalysisEnabled": native_field(&test_entry, "sql_analysis_enabled")?.extract::()?, "setDifferenceOperator": settings.set_difference_operator, "sqlAnalysisDialect": settings.sql_analysis_dialect, "direction": request.get("direction")?.extract::()?, 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 18d65c3499..b8d14bb4b6 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 @@ -14,6 +14,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; use crate::bindings::types::CompilerDetach; @@ -46,7 +47,7 @@ fn infer_fixture_column_facts<'py>( let Some((true, Some(columns), false)) = rows.into_iter().next() else { return Ok(py.None().into_bound(py)); }; - let dialect: Option = profile.getattr("sql_analysis_dialect")?.extract()?; + let dialect: Option = native_field(&profile, "sql_analysis_dialect")?.extract()?; let facts: ProjectionFacts = py .compiler_detach(|| projection_facts(&query_sql, dialect.as_deref())) .map_err(|reason| compiler_error(native_failure(PROJECTION_FACTS_CONTEXT, &reason)))?; 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 41b25aaa3f..1af733ffbd 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,6 +1,7 @@ //! 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::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}; @@ -40,7 +41,7 @@ impl FixtureColumnInferences { ], ) }; - if !inference_profile.getattr("binding_catalog")?.is_none() { + if !native_field(inference_profile, "binding_catalog")?.is_none() { return compute(); } let inferred = self.profile_results(py, inference_profile)?; 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 4b6db613c0..d305219977 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,6 +22,7 @@ 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::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}; @@ -66,7 +67,7 @@ pub(crate) fn analyze_models<'py>( session: None, }); } - let catalog: Bound<'py, PyAny> = request.profile.getattr("binding_catalog")?; + let catalog: Bound<'py, PyAny> = native_field(&request.profile, "binding_catalog")?; let (rules, adapter_rules) = nullability_rules(&request.profile)?; let session_request: RequestRow = session_request(request, &catalog, rules)?.extract()?; let (cache, adapter) = match adapter_rules { @@ -76,7 +77,7 @@ pub(crate) fn analyze_models<'py>( let session: Bound<'py, NativeModelAnalysisSession> = Bound::new( py, start_model_analysis_session( - catalog.getattr("native")?.extract()?, + native_field(&catalog, "native")?.extract()?, session_request, cache, adapter, @@ -100,7 +101,7 @@ fn session_request<'py>( } let items: Vec> = vec![ profile_dialect(profile)?.into_bound_py_any(py)?, - case_sensitive_shapes(profile, &profile.getattr("sql_analysis_dialect")?)? + case_sensitive_shapes(profile, &native_field(profile, "sql_analysis_dialect")?)? .into_bound_py_any(py)?, return_types(profile)?.into_bound_py_any(py)?, rules.into_bound_py_any(py)?, @@ -108,7 +109,7 @@ fn session_request<'py>( shape_rows(&request.column_types)?.into_bound_py_any(py)?, shape_rows(&request.nullability)?.into_bound_py_any(py)?, shape_rows(&request.complete_schemas)?.into_bound_py_any(py)?, - shape_rows(&catalog.getattr("schemas")?)?.into_bound_py_any(py)?, + shape_rows(&native_field(catalog, "schemas")?)?.into_bound_py_any(py)?, families.into_bound_py_any(py)?, model_rows(py, &request.model_inputs)?, ]; @@ -121,12 +122,14 @@ fn analysis_store(request: &ModelAnalysisRequest<'_>) -> PyResult( (types, nullability): (&Bound<'py, PyDict>, &Bound<'py, PyDict>), ) -> PyResult<()> { let py: Python<'py> = catalog.py(); - let schemas: Bound<'py, PyAny> = catalog.getattr("schemas")?; + let schemas: Bound<'py, PyAny> = native_field(catalog, "schemas")?; let additions: Bound<'py, PyDict> = PyDict::new(py); for (name, shape) in schema_additions { if !schemas.contains(&name)? { @@ -259,13 +262,13 @@ fn model_rows<'py>( registry::class(py, RecordId::MaterializationType)?.getattr("SNAPSHOT")?; let mut rows: Vec> = Vec::with_capacity(model_inputs.len()); for model_input in model_inputs { - let values: Bound<'py, PyAny> = model_input.getattr("config")?.getattr("values")?; + let values: Bound<'py, PyAny> = native_field(model_input, "config")?.getattr("values")?; let get = |key: &str| values.call_method1("get", (key,)); let query_sql: Bound<'py, PyAny> = PyString::new( py, &analysis_cursor_sql( py, - &model_input.getattr("query_sql")?.extract::()?, + &native_field(model_input, "query_sql")?.extract::()?, &get("cursor_type")?, )?, ) @@ -279,7 +282,7 @@ fn model_rows<'py>( } } let mut references: Vec<(String, String)> = Vec::new(); - for row in model_input.getattr("reference_rows")?.try_iter()? { + for row in native_field(model_input, "reference_rows")?.try_iter()? { let row: Bound<'py, PyAny> = row?; references.push(( row.get_item(0)?.str()?.extract()?, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/analysis_session/nullability_rules.rs b/crates/sqlbuild-python/src/bindings/_helpers/analysis_session/nullability_rules.rs index 16562c1969..a8f7182300 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/analysis_session/nullability_rules.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/analysis_session/nullability_rules.rs @@ -2,6 +2,7 @@ use std::sync::{Arc, Mutex, PoisonError}; +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::prelude::{Py, PyAny, PyAnyMethods, PyDictMethods, PyErr, Python}; use pyo3::types::{PyDict, PyTuple}; use sqlbuild_analysis::assembly::analysis_session::models::NullabilityCallback; @@ -31,7 +32,8 @@ pub(crate) fn nullability_callback( .map(|value| enum_type.call1((*value,))) .collect::, _>>()?; let result = rule.call1((PyTuple::new(python, values)?,))?; - let value: String = enum_type.call1((result,))?.getattr("value")?.extract()?; + let value: String = + native_field(&enum_type.call1((result,))?, "value")?.extract()?; Ok(NULLABILITIES .into_iter() .find(|known| *known == value) 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 175bf780ee..f93ebe17f3 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,6 +9,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; @@ -84,7 +85,7 @@ pub(crate) fn pairs(mapping: &Bound<'_, PyAny>) -> PyResult { /// `profile.sql_analysis_dialect or "generic"`. pub(crate) fn profile_dialect(profile: &Bound<'_, PyAny>) -> PyResult { - dialect_or_generic(&profile.getattr("sql_analysis_dialect")?) + dialect_or_generic(&native_field(profile, "sql_analysis_dialect")?) } /// `dialect or "generic"`. @@ -126,7 +127,7 @@ pub(crate) fn profile_catalog<'py>( profile: &Bound<'py, PyAny>, dialect: &str, ) -> PyResult> { - let catalog: Bound<'py, PyAny> = profile.getattr("binding_catalog")?; + let catalog: Bound<'py, PyAny> = native_field(profile, "binding_catalog")?; if catalog.is_truthy()? { return Ok(catalog); } @@ -148,5 +149,5 @@ pub(crate) fn profile_catalog<'py>( /// `profile.function_return_types` items in order. pub(crate) fn return_types(profile: &Bound<'_, PyAny>) -> PyResult { - pairs(&profile.getattr("function_return_types")?) + pairs(&native_field(profile, "function_return_types")?) } 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 493e4c5d6d..14310b945c 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 @@ -20,6 +20,7 @@ use crate::bindings::_helpers::analysis_session::session::{ infer_expression_source_shapes, prove_dynamic_column_contracts, }; use crate::bindings::_helpers::boundary::keyword_request::Request; +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; @@ -109,7 +110,7 @@ fn infer_native_expression_source_shapes<'py>( let profile: Bound<'py, PyAny> = request.get("profile")?; let dialect: String = aliased_dialect(py, profile_dialect(&profile)?)?; let catalog: Bound<'py, PyAny> = profile_catalog(&profile, &dialect)?; - let cache: Bound<'py, PyAny> = catalog.getattr("expression_shapes")?; + let cache: Bound<'py, PyAny> = native_field(&catalog, "expression_shapes")?; let pending: Bound<'py, PyDict> = PyDict::new(py); for expression in expressions.try_iter()? { let expression: Bound<'py, PyAny> = expression?; @@ -119,7 +120,7 @@ fn infer_native_expression_source_shapes<'py>( } if !pending.is_empty() { let (rules, adapter_rules) = nullability_rules(&profile)?; - let native: PyRef<'py, ProjectCatalog> = catalog.getattr("native")?.extract()?; + let native: PyRef<'py, ProjectCatalog> = native_field(&catalog, "native")?.extract()?; let pending_names: Vec = pending .keys() .iter() @@ -181,7 +182,7 @@ fn prove_native_dynamic_contracts<'py>( rows.push((sql, family_rows(&families)?)); } let contracts: Vec = if session.is_none() { - let by_table: Bound<'py, PyAny> = tables.getattr("dynamic_families_by_table")?; + let by_table: Bound<'py, PyAny> = native_field(&tables, "dynamic_families_by_table")?; let mut families: Vec<(String, Vec)> = Vec::new(); for item in by_table.call_method0("items")?.try_iter()? { let (name, declared): (String, Bound<'py, PyAny>) = item?.extract()?; @@ -190,10 +191,13 @@ fn prove_native_dynamic_contracts<'py>( prove_dynamic_column_contracts( py, ( - dialect_or_generic(&tables.getattr("dialect")?)?, - shape_rows(&tables.getattr("column_types_by_table")?)?, - shape_rows(&tables.getattr("authoritative_column_types_by_table")?)?, - shape_rows(&tables.getattr("column_nullability_by_table")?)?, + dialect_or_generic(&native_field(&tables, "dialect")?)?, + shape_rows(&native_field(&tables, "column_types_by_table")?)?, + shape_rows(&native_field( + &tables, + "authoritative_column_types_by_table", + )?)?, + shape_rows(&native_field(&tables, "column_nullability_by_table")?)?, families, rows, ), @@ -215,12 +219,12 @@ pub(crate) fn family_rows(families: &Bound<'_, PyAny>) -> PyResult = family?; rows.push(( - family.getattr("name")?.extract()?, - family.getattr("pivot_column")?.extract()?, - family.getattr("value_column")?.extract()?, - family.getattr("aggregate")?.extract()?, - family.getattr("type")?.extract()?, - family.getattr("name_pattern")?.extract()?, + native_field(&family, "name")?.extract()?, + native_field(&family, "pivot_column")?.extract()?, + native_field(&family, "value_column")?.extract()?, + native_field(&family, "aggregate")?.extract()?, + native_field(&family, "type")?.extract()?, + native_field(&family, "name_pattern")?.extract()?, )); } Ok(rows) @@ -250,13 +254,16 @@ pub(crate) fn query_column_rows( let dialect: String = aliased_dialect(py, normalization_dialect.clone())?; let mut batch: bool = false; for query in &queries { - if query.getattr("mode")?.eq(BATCH_MODE)? { + if native_field(query, "mode")?.eq(BATCH_MODE)? { batch = true; break; } } let catalog: Option> = if batch { - Some(profile_catalog(profile, &dialect)?.getattr("native")?) + Some(native_field( + &profile_catalog(profile, &dialect)?, + "native", + )?) } else { None }; @@ -284,7 +291,7 @@ pub(crate) fn query_column_rows( } fn query_row(query: &Bound<'_, PyAny>) -> PyResult { - let placeholders: Bound<'_, PyAny> = query.getattr("placeholders")?; + let placeholders: Bound<'_, PyAny> = native_field(query, "placeholders")?; let placeholders: Pairs = if placeholders.is_truthy()? { placeholders .call_method0("items")? @@ -296,10 +303,10 @@ fn query_row(query: &Bound<'_, PyAny>) -> PyResult { }; let mut analysis_names: Vec = Vec::new(); let mut lineage: Vec<(String, String, String)> = Vec::new(); - for reference in query.getattr("references")?.try_iter()? { + for reference in native_field(query, "references")?.try_iter()? { let reference: Bound<'_, PyAny> = reference?; - let kind: String = reference.getattr("ref_kind")?.extract()?; - let name: String = reference.getattr("ref_name")?.extract()?; + let kind: String = native_field(&reference, "ref_kind")?.extract()?; + let name: String = native_field(&reference, "ref_name")?.extract()?; let analysis_name: String = if kind == TABLE_FUNCTION_KIND { format!("{TABLE_FUNCTION_ANALYSIS_PREFIX}{name}") } else { @@ -321,12 +328,12 @@ fn query_row(query: &Bound<'_, PyAny>) -> PyResult { } } Ok(( - query.getattr("sql")?.extract()?, + native_field(query, "sql")?.extract()?, placeholders, analysis_names, lineage, - query.getattr("recover_cte_facts")?.extract()?, - query.getattr("mode")?.extract()?, + native_field(query, "recover_cte_facts")?.extract()?, + native_field(query, "mode")?.extract()?, )) } 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 1dc77c5478..e0550e59f0 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/audit_command/command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/audit_command/command.rs @@ -15,6 +15,7 @@ use crate::bindings::_helpers::command_runtime::invocation::resolve_reported_com use crate::bindings::_helpers::compile_driver::python_calls::{call_with, within}; 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, RecordId}; use crate::bindings::_helpers::records::registry; @@ -101,28 +102,34 @@ fn compile_audits<'py>( invocation: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = request.py(); - let discovered: Bound<'py, PyAny> = invocation.getattr("discovered_inputs")?; - let adapter: Bound<'py, PyAny> = invocation.getattr("adapter")?; + 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", invocation.getattr("effective_project_dir")?), + ( + "project_dir", + native_field(invocation, "effective_project_dir")?, + ), ("discovered_inputs", discovered.clone()), ], )?; let options: Bound<'py, PyAny> = call_with( ®istry::class(py, RecordId::CompilePipelineOptions)?, &[ - ("selected_target", request.getattr("selected_target")?), - ("no_sql_validation", request.getattr("no_sql_validation")?), - ("defer_to", request.getattr("defer_to")?), - ("select", request.getattr("select")?), - ("exclude", request.getattr("exclude")?), + ("selected_target", native_field(request, "selected_target")?), + ( + "no_sql_validation", + native_field(request, "no_sql_validation")?, + ), + ("defer_to", native_field(request, "defer_to")?), + ("select", native_field(request, "select")?), + ("exclude", native_field(request, "exclude")?), ( "connection_config", - invocation.getattr("connection_config")?, + native_field(invocation, "connection_config")?, ), - ("cli_vars", request.getattr("cli_vars")?), + ("cli_vars", native_field(request, "cli_vars")?), ("external_sql_reference_resolver", resolver), ], )?; @@ -143,10 +150,10 @@ fn compile_audits<'py>( let plan_output: Bound<'py, PyAny> = call_with( ®istry::native(py, NativeAttr::BuildAuditCommandPlan)?, &[ - ("project", context.getattr("project")?), + ("project", native_field(&context, "project")?), ("adapter", adapter), - ("scope", context.getattr("scope")?), - ("relations", context.getattr("relations")?), + ("scope", native_field(&context, "scope")?), + ("relations", native_field(&context, "relations")?), ], )?; call_with( @@ -170,8 +177,9 @@ fn prepare_run<'py>( pipeline: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = request.py(); - let entries: Bound<'py, PyAny> = pipeline.getattr("plan_output")?.getattr("audit_entries")?; - let requested: Bound<'py, PyAny> = request.getattr("concurrency")?; + let entries: Bound<'py, PyAny> = + native_field(pipeline, "plan_output")?.getattr("audit_entries")?; + let requested: Bound<'py, PyAny> = native_field(request, "concurrency")?; let concurrency: i64 = if requested.is_none() { pipeline .getattr("project")? @@ -183,8 +191,8 @@ fn prepare_run<'py>( }; let audit_count: usize = entries.len()?; let worker_count: i64 = concurrency.min(i64::try_from(audit_count).unwrap_or(i64::MAX)); - let stream: Bound<'py, PyAny> = invocation.getattr("progress_stream")?; - let use_color: Bound<'py, PyAny> = invocation.getattr("use_color")?; + 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)?, &[ @@ -199,15 +207,15 @@ fn prepare_run<'py>( if !projector.is_none() { for entry in entries.try_iter()? { call_with( - &projector.getattr("expect_resource_enrichment")?, - &[("resource_name", entry?.getattr("name")?)], + &native_field(&projector, "expect_resource_enrichment")?, + &[("resource_name", native_field(&entry?, "name")?)], )?; } } let connection: Bound<'py, PyAny> = call_with( ®istry::native(py, NativeAttr::ConnectionProgressReporter)?, &[ - ("adapter_name", invocation.getattr("adapter_name")?), + ("adapter_name", native_field(invocation, "adapter_name")?), ("stream", stream), ("use_color", use_color), ], @@ -227,8 +235,8 @@ fn write_headers( concurrency: i64, ) -> PyResult<()> { let py: Python<'_> = invocation.py(); - let stream: Bound<'_, PyAny> = invocation.getattr("progress_stream")?; - let use_color: Bound<'_, PyAny> = invocation.getattr("use_color")?; + let stream: Bound<'_, PyAny> = native_field(invocation, "progress_stream")?; + let use_color: Bound<'_, PyAny> = native_field(invocation, "use_color")?; stream.call_method1("write", ("\n",))?; let header: Bound<'_, PyDict> = PyDict::new(py); header.set_item("stream", &stream)?; @@ -237,10 +245,11 @@ fn write_headers( header.set_item("concurrency", concurrency)?; header.set_item("use_color", &use_color)?; registry::native(py, NativeAttr::WriteExecutionHeader)?.call((), Some(&header))?; - let entries: Bound<'_, PyAny> = pipeline.getattr("plan_output")?.getattr("audit_entries")?; + let entries: Bound<'_, PyAny> = + native_field(pipeline, "plan_output")?.getattr("audit_entries")?; let mut models: BTreeSet = BTreeSet::new(); for entry in entries.try_iter()? { - let target: Bound<'_, PyAny> = entry?.getattr("attached_target_name")?; + let target: Bound<'_, PyAny> = native_field(&entry?, "attached_target_name")?; if target.is_truthy()? { models.insert(target.extract()?); } @@ -263,8 +272,8 @@ fn execute<'py>( run: &AuditRun<'py>, ) -> PyResult>> { let py: Python<'py> = invocation.py(); - let plan: Bound<'py, PyAny> = pipeline.getattr("plan_output")?; - let project: Bound<'py, PyAny> = pipeline.getattr("project")?; + 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 relay: Bound<'py, AuditCallbackRelay> = Bound::new( @@ -277,25 +286,25 @@ fn execute<'py>( )?; let executed: PyResult>> = (|| { run.progress - .call_method1("set_result_callback", (relay.getattr("on_result")?,))?; + .call_method1("set_result_callback", (native_field(&relay, "on_result")?,))?; let projector: Bound<'py, PyAny> = registry::native(py, NativeAttr::CurrentNativeProgressProjector)?.call0()?; if !projector.is_none() { let ordinals: Bound<'py, PyDict> = PyDict::new(py); let mut total: usize = 0; - for entry in plan.getattr("audit_entries")?.try_iter()? { + for entry in native_field(&plan, "audit_entries")?.try_iter()? { total += 1; - ordinals.set_item(entry?.getattr("name")?, total)?; + ordinals.set_item(native_field(&entry?, "name")?, total)?; } call_with( - &projector.getattr("configure_resources")?, + &native_field(&projector, "configure_resources")?, &[ ("ordinals", ordinals.into_any()), ("total", PyInt::new(py, total).into_any()), ], )?; } - let run_id: Bound<'py, PyAny> = project.getattr("run_id")?; + let run_id: Bound<'py, PyAny> = native_field(&project, "run_id")?; let ledger_path: Bound<'py, PyAny> = invocation .getattr("effective_project_dir")? .call_method1("__truediv__", ("target",))? @@ -318,23 +327,32 @@ fn execute<'py>( &[ ( "on_connection_start", - run.connection.getattr("on_connection_start")?, + native_field(&run.connection, "on_connection_start")?, ), ( "on_connection_complete", - relay.getattr("on_connection_complete")?, + native_field(&relay, "on_connection_complete")?, + ), + ( + "on_connection_error", + native_field(&relay, "on_connection_error")?, + ), + ( + "on_audit_start", + native_field(&run.progress, "on_item_start")?, ), - ("on_connection_error", relay.getattr("on_connection_error")?), - ("on_audit_start", run.progress.getattr("on_item_start")?), ( "on_audit_physical_complete", - run.progress.getattr("on_item_physical_complete")?, + native_field(&run.progress, "on_item_physical_complete")?, ), ( "on_audit_complete", - run.progress.getattr("on_item_complete")?, + native_field(&run.progress, "on_item_complete")?, + ), + ( + "on_audit_error", + native_field(&run.progress, "on_item_error")?, ), - ("on_audit_error", run.progress.getattr("on_item_error")?), ], )?; call_with( @@ -343,9 +361,9 @@ fn execute<'py>( ("plan", plan.clone()), ( "connection_config", - invocation.getattr("connection_config")?, + native_field(invocation, "connection_config")?, ), - ("adapter", invocation.getattr("adapter")?), + ("adapter", native_field(invocation, "adapter")?), ( "max_concurrency", PyInt::new(py, run.concurrency).into_any(), @@ -354,11 +372,11 @@ fn execute<'py>( ("run_id", run_id.clone()), ( "storage_database", - project.getattr("effective_target_database")?, + native_field(&project, "effective_target_database")?, ), ( "storage_schema", - project.getattr("effective_target_schema")?, + native_field(&project, "effective_target_schema")?, ), ], )? @@ -403,7 +421,7 @@ fn audit_payload<'py>( counts.set_item(field, projection.getattr(field)?)?; } document.set_item("audit_result_projection", &counts)?; - if projection.getattr("degraded")?.is_truthy()? { + if native_field(&projection, "degraded")?.is_truthy()? { document.set_item("projection_degraded", true)?; let reason: Bound<'py, PyDict> = PyDict::new(py); reason.set_item("reason", PROJECTION_FAILURE)?; @@ -448,9 +466,9 @@ fn complete( _ => {} } } - let stream: Bound<'_, PyAny> = invocation.getattr("progress_stream")?; + let stream: Bound<'_, PyAny> = native_field(invocation, "progress_stream")?; let style: CliStyle = CliStyle { - use_color: invocation.getattr("use_color")?.is_truthy()?, + use_color: native_field(invocation, "use_color")?.is_truthy()?, }; stream.call_method1("write", (render_audit_summary(counts, style),))?; stream.call_method0("flush")?; @@ -462,8 +480,11 @@ fn complete( ®istry::native(py, NativeAttr::WriteExecutionJsonOutput)?, &[ ("payload", payload), - ("json_output", request.getattr("json_output")?), - ("json_output_path", request.getattr("json_output_path")?), + ("json_output", native_field(request, "json_output")?), + ( + "json_output_path", + native_field(request, "json_output_path")?, + ), ], )?; Ok(audit_exit_code(counts.failed)) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/auditing/identity.rs b/crates/sqlbuild-python/src/bindings/_helpers/auditing/identity.rs index c1c762345f..b7b2dbef43 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/auditing/identity.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/auditing/identity.rs @@ -1,5 +1,6 @@ //! Audit gate identities of planned audits, built as `AuditGateIdentity` objects. +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}; @@ -13,7 +14,7 @@ use sqlbuild_analysis::auditing::models::{ const MEASUREMENT_MODE: &str = "measurement"; fn enum_value(value: &Bound<'_, PyAny>) -> PyResult { - value.getattr("value")?.extract() + native_field(value, "value")?.extract() } /// One `MeasurementThresholdBound`, or `None`. @@ -21,13 +22,13 @@ pub(crate) fn threshold_bound(bound: &Bound<'_, PyAny>) -> PyResult ThresholdBound::Below(bound.getattr("limit")?.extract()?), - "above" => ThresholdBound::Above(bound.getattr("limit")?.extract()?), + "below" => ThresholdBound::Below(native_field(bound, "limit")?.extract()?), + "above" => ThresholdBound::Above(native_field(bound, "limit")?.extract()?), _ => ThresholdBound::Outside( - bound.getattr("lower")?.extract()?, - bound.getattr("upper")?.extract()?, + native_field(bound, "lower")?.extract()?, + native_field(bound, "upper")?.extract()?, ), })) } @@ -38,36 +39,37 @@ pub(crate) fn thresholds(value: &Bound<'_, PyAny>) -> PyResult) -> PyResult { let measurement: Option = - if enum_value(&audit.getattr("evaluation_mode")?)? == MEASUREMENT_MODE { + if enum_value(&native_field(audit, "evaluation_mode")?)? == MEASUREMENT_MODE { Some(MeasurementFacts { - value_column: audit.getattr("value_column")?.extract()?, - sample_count_column: audit.getattr("sample_count_column")?.extract()?, - sample_unit: audit.getattr("sample_unit")?.extract()?, - thresholds: thresholds(&audit.getattr("thresholds")?)?, - minimum_samples: audit.getattr("minimum_samples")?.extract()?, - evidence_unresolved_sql: audit.getattr("evidence_unresolved_sql")?.extract()?, - evidence_resolved_sql: audit.getattr("evidence_resolved_sql")?.extract()?, + value_column: native_field(audit, "value_column")?.extract()?, + sample_count_column: native_field(audit, "sample_count_column")?.extract()?, + sample_unit: native_field(audit, "sample_unit")?.extract()?, + thresholds: thresholds(&native_field(audit, "thresholds")?)?, + minimum_samples: native_field(audit, "minimum_samples")?.extract()?, + evidence_unresolved_sql: native_field(audit, "evidence_unresolved_sql")? + .extract()?, + evidence_resolved_sql: native_field(audit, "evidence_resolved_sql")?.extract()?, }) } else { None }; Ok(PlannedAudit { - name: audit.getattr("name")?.extract()?, - attachment_kind: enum_value(&audit.getattr("attachment_kind")?)?, - attached_target_name: audit.getattr("attached_target_name")?.extract()?, - attached_column_name: audit.getattr("attached_column_name")?.extract()?, - severity: enum_value(&audit.getattr("severity")?)?, - run_scope: enum_value(&audit.getattr("effective_run_scope")?)?, - always_run: audit.getattr("always_run")?.is_truthy()?, - unresolved_sql: audit.getattr("unresolved_sql")?.extract()?, - resolved_sql: audit.getattr("resolved_sql")?.extract()?, + name: native_field(audit, "name")?.extract()?, + attachment_kind: enum_value(&native_field(audit, "attachment_kind")?)?, + attached_target_name: native_field(audit, "attached_target_name")?.extract()?, + attached_column_name: native_field(audit, "attached_column_name")?.extract()?, + severity: enum_value(&native_field(audit, "severity")?)?, + run_scope: enum_value(&native_field(audit, "effective_run_scope")?)?, + always_run: native_field(audit, "always_run")?.is_truthy()?, + unresolved_sql: native_field(audit, "unresolved_sql")?.extract()?, + resolved_sql: native_field(audit, "resolved_sql")?.extract()?, measurement, }) } @@ -104,7 +106,7 @@ fn build_audit_gate_identity<'py>( "binding_key", identity.binding_key.into_pyobject(py)?.into_any(), ), - ("audit_name", entry.getattr("name")?), + ("audit_name", native_field(entry, "name")?), ( "definition_fingerprint", identity @@ -116,24 +118,24 @@ fn build_audit_gate_identity<'py>( "execution_fingerprint", identity.execution_fingerprint.into_pyobject(py)?.into_any(), ), - ("severity", entry.getattr("severity")?), + ("severity", native_field(entry, "severity")?), ( "run_scope_phase", - entry.getattr("effective_run_scope")?.getattr("value")?, + native_field(entry, "effective_run_scope")?.getattr("value")?, ), ( "attachment_kind", - entry.getattr("attachment_kind")?.getattr("value")?, + native_field(entry, "attachment_kind")?.getattr("value")?, ), ( "attached_target_name", - entry.getattr("attached_target_name")?, + native_field(entry, "attached_target_name")?, ), ( "attached_column_name", - entry.getattr("attached_column_name")?, + native_field(entry, "attached_column_name")?, ), - ("always_run", entry.getattr("always_run")?), + ("always_run", native_field(entry, "always_run")?), ], )?); } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/auditing/render.rs b/crates/sqlbuild-python/src/bindings/_helpers/auditing/render.rs index 3e07e6b131..cdfa2b756f 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/auditing/render.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/auditing/render.rs @@ -6,6 +6,7 @@ use pyo3::{pyfunction, wrap_pyfunction}; use sqlbuild_analysis::auditing::main::replace_reference_calls::replace_reference_calls; use crate::bindings::_helpers::boundary::keyword_request::Request; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::references::source_relations::render_source_relation; const REF_FUNCTION: &str = "__ref"; @@ -23,7 +24,7 @@ fn qualified<'py>(mapping: &Bound<'py, PyAny>, name: &str) -> PyResult = location.getattr("qualified_name")?; + let qualified: Bound<'py, PyAny> = native_field(&location, "qualified_name")?; Ok(Some(qualified).filter(|qualified| !qualified.is_none())) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/authored_values/python_nodes.rs b/crates/sqlbuild-python/src/bindings/_helpers/authored_values/python_nodes.rs index ddaa1338cc..708459ad89 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/authored_values/python_nodes.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/authored_values/python_nodes.rs @@ -16,6 +16,7 @@ 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::native_host::user_code::UserCodeHost; +use crate::bindings::_helpers::records::fields::field as native_field; const RANDOM_MODULE: &str = "random"; const MAX_ATTEMPTS_TOO_LOW: &str = "RetryPolicy max_attempts must be at least 1"; @@ -104,16 +105,16 @@ fn retry_delay_seconds<'py>( retry_policy: &Bound<'py, PyAny>, retry_index: &Bound<'py, PyAny>, ) -> PyResult> { - let initial: Bound<'py, PyAny> = retry_policy.getattr("initial_delay_seconds")?; + let initial: Bound<'py, PyAny> = native_field(retry_policy, "initial_delay_seconds")?; let growth: Bound<'py, PyAny> = retry_policy .getattr("backoff_multiplier")? .pow(retry_index, retry_policy.py().None())?; let mut delay: Bound<'py, PyAny> = initial.mul(growth)?; - let maximum: Bound<'py, PyAny> = retry_policy.getattr("max_delay_seconds")?; + let maximum: Bound<'py, PyAny> = native_field(retry_policy, "max_delay_seconds")?; if !maximum.is_none() && maximum.lt(&delay)? { delay = maximum; } - if retry_policy.getattr("jitter")?.is_truthy()? { + if native_field(retry_policy, "jitter")?.is_truthy()? { return imported(retry_policy.py(), RANDOM_MODULE, "uniform")?.call1((0, delay)); } Ok(delay) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/boundary/config_values.rs b/crates/sqlbuild-python/src/bindings/_helpers/boundary/config_values.rs index dbbe603f1d..91c04cf357 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/boundary/config_values.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/boundary/config_values.rs @@ -1,5 +1,6 @@ //! Loaded configuration values as the Python objects `tomllib` builds. +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::IntoPyObjectExt; use pyo3::prelude::{Bound, IntoPyObject, Py, PyAny, PyAnyMethods, PyModule, PyResult, Python}; use pyo3::types::{IntoPyDict, PyDict, PyDictMethods, PyList, PyListMethods}; @@ -31,11 +32,11 @@ fn time(datetime: &Bound<'_, PyModule>, value: ConfigTime) -> PyResult fn date_time(datetime: &Bound<'_, PyModule>, value: ConfigDateTime) -> PyResult { let tzinfo: Option> = match value.utc_offset_seconds { Some(seconds) => { - let delta = datetime.getattr("timedelta")?.call( + let delta = native_field(datetime, "timedelta")?.call( (), Some(&[("seconds", seconds)].into_py_dict(datetime.py())?), )?; - Some(datetime.getattr("timezone")?.call1((delta,))?) + Some(native_field(datetime, "timezone")?.call1((delta,))?) } None => None, }; 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 cdcbc0c17a..3dad08bd91 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/build_command/command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/build_command/command.rs @@ -19,6 +19,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::{call_with, within} 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; @@ -31,7 +32,7 @@ fn run_with_providers(context: &BuildContext<'_>) -> PyResult { context.get("discovered_inputs")?.getattr("providers")?, )], )?; - let built: PyResult = run_planned_build(context, &session.getattr("providers")?); + let built: PyResult = run_planned_build(context, &native_field(&session, "providers")?); let closed: PyResult> = session.call_method0("close"); let exit_code: i32 = built?; closed?; @@ -75,11 +76,11 @@ fn run_within_limits( &[ ( "max_duration_seconds", - limits.getattr("max_duration_seconds")?, + native_field(&limits, "max_duration_seconds")?, ), - ("max_duration", limits.getattr("max_duration")?), + ("max_duration", native_field(&limits, "max_duration")?), ("target_name", context.get("effective_target_name")?), - ("remediation", limits.getattr("remediation")?), + ("remediation", native_field(&limits, "remediation")?), ], )?; within(&deadline, |_| run_with_providers(&context)) @@ -93,7 +94,7 @@ fn run_build_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult = call_with( &host::entry(py, ExecutorHost::ProcessResourceReporting)?, - &[("enabled", request.getattr("debug")?)], + &[("enabled", native_field(request, "debug")?)], )?; within(&reporting, |_| { within(&tracker.call_method0("scope")?, |_| { 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 a93c335b2f..be58089b78 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/build_command/completion.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/build_command/completion.rs @@ -26,6 +26,7 @@ use crate::bindings::_helpers::cost_report::degradation::{ }; 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, RecordId}; use crate::bindings::_helpers::records::registry; @@ -48,18 +49,21 @@ pub(crate) fn finalize_build_cost<'py>( cost: CostRequest<'py>, ) -> PyResult> { let py: Python<'py> = context.py(); - let project: Bound<'py, PyAny> = pipeline.getattr("project")?; + let project: Bound<'py, PyAny> = native_field(pipeline, "project")?; let mut fields: Vec<(&str, Bound<'py, PyAny>)> = vec![ ("project_dir", context.get("effective_project_dir")?), ("adapter_name", context.get("adapter_name")?), ("adapter", context.get("adapter")?), ("connection_config", context.get("connection_config")?), - ("target_name", project.getattr("effective_target_name")?), + ( + "target_name", + native_field(&project, "effective_target_name")?, + ), ( "target_database", - project.getattr("effective_target_database")?, + native_field(&project, "effective_target_database")?, ), - ("run_id", project.getattr("run_id")?), + ("run_id", native_field(&project, "run_id")?), ("build_status", text(py, cost.status)), ("started_at", cost.started_at), ("completed_at", cost.completed_at), @@ -166,7 +170,7 @@ fn run_id_for<'py>( if record.is_none() { return Ok(none(pipeline.py())); } - pipeline.getattr("project")?.getattr("run_id") + native_field(pipeline, "project")?.getattr("run_id") } fn write_json(context: &BuildContext<'_>, payload: Bound<'_, PyAny>) -> PyResult<()> { @@ -174,10 +178,13 @@ fn write_json(context: &BuildContext<'_>, payload: Bound<'_, PyAny>) -> PyResult ®istry::native(context.py(), NativeAttr::WriteExecutionJsonOutput)?, &[ ("payload", payload), - ("json_output", context.request.getattr("json_output")?), + ( + "json_output", + native_field(&context.request, "json_output")?, + ), ( "json_output_path", - context.request.getattr("json_output_path")?, + native_field(&context.request, "json_output_path")?, ), ], )?; @@ -187,14 +194,14 @@ fn write_json(context: &BuildContext<'_>, payload: Bound<'_, PyAny>) -> PyResult /// 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> = pipeline.getattr("plan_output")?; + let plan: Bound<'_, PyAny> = native_field(pipeline, "plan_output")?; if call_with( ®istry::native(py, NativeAttr::PlanHasExecutableWork)?, &[ ("plan", plan.clone()), ( "python_plan_entries", - pipeline.getattr("python_plan_entries")?, + native_field(pipeline, "python_plan_entries")?, ), ], )? @@ -252,8 +259,8 @@ pub(crate) fn finalize_no_work_with_timings( write_timings( context, &PhaseTimings { - compile_seconds: pipeline.getattr("compile_seconds")?.extract()?, - planning_seconds: pipeline.getattr("planning_seconds")?.extract()?, + compile_seconds: native_field(pipeline, "compile_seconds")?.extract()?, + planning_seconds: native_field(pipeline, "planning_seconds")?.extract()?, cost_collection_seconds: Some(cost_seconds), total_seconds: Some(context.started.elapsed().as_secs_f64()), ..PhaseTimings::default() @@ -314,15 +321,16 @@ pub(crate) fn complete_direct_build<'py>( let py: Python<'py> = context.py(); let completed_at: Bound<'py, PyAny> = utc_now(py)?; let result: &Bound<'py, PyAny> = &outcome.result; - let plan: Bound<'py, PyAny> = pipeline.getattr("plan_output")?; - let succeeded: bool = result.getattr("status")?.str()?.extract::()? == SUCCESS_STATUS; + let plan: Bound<'py, PyAny> = native_field(pipeline, "plan_output")?; + let succeeded: bool = + native_field(result, "status")?.str()?.extract::()? == SUCCESS_STATUS; let python_failed: bool = any_failed(&outcome.python_results, |result| { Ok(python_node_failed( - &result.getattr("status")?.str()?.extract::()?, + &native_field(result, "status")?.str()?.extract::()?, )) })?; let checks_failed: bool = any_failed(check_results, |result| { - result.getattr("failed")?.is_truthy() + native_field(result, "failed")?.is_truthy() })?; let exit_code: i32 = build_exit_code(succeeded, (python_failed, checks_failed)); let target_dir: Bound<'py, PyAny> = @@ -350,7 +358,7 @@ pub(crate) fn complete_direct_build<'py>( ®istry::native(py, NativeAttr::FormatBuildFooter)?, &[ ("result", result.clone()), - ("elapsed", preparation.callbacks.getattr("elapsed")?), + ("elapsed", native_field(&preparation.callbacks, "elapsed")?), ("use_color", boolean(py, context.use_color)), ("python_node_results", outcome.python_results.clone()), ], @@ -373,13 +381,13 @@ pub(crate) fn complete_direct_build<'py>( write_json(context, payload)?; render_build_cost(&record, &context.stream, context.use_color)?; if context.verbose { - let timings: Bound<'py, PyAny> = result.getattr("timings")?; + let timings: Bound<'py, PyAny> = native_field(result, "timings")?; let seconds = |name: &str| -> PyResult> { timings.getattr(name)?.extract() }; write_timings( context, &PhaseTimings { - compile_seconds: pipeline.getattr("compile_seconds")?.extract()?, - planning_seconds: pipeline.getattr("planning_seconds")?.extract()?, + compile_seconds: native_field(pipeline, "compile_seconds")?.extract()?, + planning_seconds: native_field(pipeline, "planning_seconds")?.extract()?, connection_preparation_seconds: seconds("connection_preparation_seconds")?, schema_preparation_seconds: seconds("schema_preparation_seconds")?, execution_seconds: seconds("execution_seconds")?, 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 b67e8007ac..a4914d0697 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 @@ -16,6 +16,7 @@ use crate::bindings::_helpers::build_command::policies::build_command_error; use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported, 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, RecordId}; use crate::bindings::_helpers::records::registry; @@ -40,7 +41,9 @@ fn user_error(py: Python<'_>, message: String, code: &str) -> PyErr { } fn resource_type(key: &Bound<'_, PyAny>) -> PyResult { - key.getattr("resource_type")?.getattr("value")?.extract() + native_field(key, "resource_type")? + .getattr("value")? + .extract() } /// Functions and view models are recreated over cloned boundaries instead of cloned. @@ -50,7 +53,7 @@ fn recreated_over_clones(scope: &Bound<'_, PyAny>, key: &Bound<'_, PyAny>) -> Py } let model: Bound<'_, PyAny> = scope .getattr("models_by_name")? - .call_method1("get", (key.getattr("name")?,))?; + .call_method1("get", (native_field(key, "name")?,))?; if model.is_none() { return Ok(false); } @@ -106,15 +109,15 @@ fn traversed_names<'py>( let keys: Bound<'py, PyAny> = call_with( ®istry::native(py, function)?, &[ - ("selected", scope.getattr("selected_keys")?), - ("upstream", scope.getattr("upstream_deps")?), + ("selected", native_field(scope, "selected_keys")?), + ("upstream", native_field(scope, "upstream_deps")?), ("is_clonable", is_clonable.clone()), ("is_view", is_view.clone()), ], )?; let mut names: Vec = Vec::new(); for key in keys.try_iter()? { - names.push(key?.getattr("name")?.extract()?); + names.push(native_field(&key?, "name")?.extract()?); } names.sort(); Ok(names) @@ -122,14 +125,14 @@ fn traversed_names<'py>( /// Selected models except those explicitly disabled. fn selected_executable_model_count(scope: &Bound<'_, PyAny>) -> PyResult { - let models: Bound<'_, PyAny> = scope.getattr("models_by_name")?; + let models: Bound<'_, PyAny> = native_field(scope, "models_by_name")?; let mut count: usize = 0; - for key in scope.getattr("selected_keys")?.try_iter()? { + for key in native_field(scope, "selected_keys")?.try_iter()? { let key: Bound<'_, PyAny> = key?; if resource_type(&key)? != MODEL_TYPE { continue; } - let model: Bound<'_, PyAny> = models.call_method1("get", (key.getattr("name")?,))?; + let model: Bound<'_, PyAny> = models.call_method1("get", (native_field(&key, "name")?,))?; if !model.is_none() && model .getattr("config")? @@ -216,36 +219,43 @@ fn clone_input<'py>( on_item: Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = context.py(); - let project: Bound<'py, PyAny> = pipeline.getattr("destination_project")?; - let destination_models: Bound<'py, PyAny> = pipeline.getattr("destination_model_entries")?; + 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( py, NativeAttr::ResolveTargetConfig, &[ ( "project_config", - prephase.discovered.getattr("project_config")?, + native_field(&prephase.discovered, "project_config")?, + ), + ( + "local_config", + native_field(&prephase.discovered, "local_config")?, ), - ("local_config", prephase.discovered.getattr("local_config")?), ("target_name", prephase.destination.clone()), ], )?; let mut selected: Vec> = Vec::new(); for entry in destination_models.try_iter()? { - selected.push(entry?.getattr("name")?); + selected.push(native_field(&entry?, "name")?); } let retention: Bound<'py, PyAny> = call_with( &host::entry(py, ExecutorHost::BuildDestinationRetentionRequests)?, &[ ("project", project.clone()), - ("adapter_name", prephase.adapter.getattr("adapter_name")?), + ( + "adapter_name", + native_field(&prephase.adapter, "adapter_name")?, + ), ( "selected_model_names", PyFrozenSet::new(py, &selected)?.into_any(), ), ( "namespace_owned", - target.getattr("owns_time_travel_retention_namespace")?, + native_field(&target, "owns_time_travel_retention_namespace")?, ), ], )?; @@ -255,10 +265,10 @@ fn clone_input<'py>( let sources: Bound<'py, PyAny> = call_with( &host::entry(py, ExecutorHost::CloneSourceEntries)?, &[ - ("origin", pipeline.getattr("origin_source_entries")?), + ("origin", native_field(pipeline, "origin_source_entries")?), ( "destination", - pipeline.getattr("destination_source_entries")?, + native_field(pipeline, "destination_source_entries")?, ), ], )?; @@ -268,29 +278,29 @@ fn clone_input<'py>( ("source_entries", sources), ( "origin_model_entries", - pipeline.getattr("origin_model_entries")?, + native_field(pipeline, "origin_model_entries")?, ), ("destination_model_entries", destination_models), ( "origin_seed_entries", - pipeline.getattr("origin_seed_entries")?, + native_field(pipeline, "origin_seed_entries")?, ), ( "destination_seed_entries", - pipeline.getattr("destination_seed_entries")?, + native_field(pipeline, "destination_seed_entries")?, ), ( "destination_function_entries", - pipeline.getattr("destination_function_entries")?, + native_field(pipeline, "destination_function_entries")?, ), ( "execution_order", - pipeline.getattr("clone_plan")?.getattr("execution_order")?, + native_field(pipeline, "clone_plan")?.getattr("execution_order")?, ), ("adapter", prephase.adapter.clone()), ("destination_connection", connection.clone()), ("hard_copy", false.into_pyobject(py)?.to_owned().into_any()), - ("run_id", project.getattr("run_id")?), + ("run_id", native_field(&project, "run_id")?), ( "query_change_tracking", project @@ -321,7 +331,7 @@ fn executed_clone<'py>( (pipeline, connection): (&Bound<'py, PyAny>, &Bound<'py, PyAny>), ) -> PyResult> { let py: Python<'py> = context.py(); - let progress_stream: Bound<'py, PyAny> = context.invocation.getattr("progress_stream")?; + let progress_stream: Bound<'py, PyAny> = native_field(&context.invocation, "progress_stream")?; let execute: Bound<'py, PyAny> = host::entry(py, ExecutorHost::ExecuteClone)?; if progress_stream.is_none() { let inputs: Bound<'py, PyAny> = clone_input( @@ -367,7 +377,7 @@ fn executed_clone<'py>( ("stream", progress_stream), ("title", text(py, "defer clone")), ("caused_by_names", prephase.caused_by.clone()), - ("use_color", context.invocation.getattr("use_color")?), + ("use_color", native_field(&context.invocation, "use_color")?), ("run_clone", run_clone.into_any()), ], ) @@ -376,23 +386,26 @@ fn executed_clone<'py>( fn failed_items(result: &Bound<'_, PyAny>) -> PyResult> { let py: Python<'_> = result.py(); let status: Bound<'_, PyAny> = host::entry(py, ExecutorHost::CloneStatus)?; - let (failed, warning) = (status.getattr("FAILED")?, status.getattr("WARNING")?); + let (failed, warning) = ( + native_field(&status, "FAILED")?, + native_field(&status, "WARNING")?, + ); let mut items: Vec = Vec::new(); - for item in result.getattr("item_results")?.try_iter()? { + for item in native_field(result, "item_results")?.try_iter()? { let item: Bound<'_, PyAny> = item?; - let item_status: Bound<'_, PyAny> = item.getattr("status")?; + let item_status: Bound<'_, PyAny> = native_field(&item, "status")?; if !(item_status.eq(&failed)? || item_status.eq(&warning)?) { continue; } - let message: Bound<'_, PyAny> = item.getattr("message")?; + let message: Bound<'_, PyAny> = native_field(&item, "message")?; let detail: Bound<'_, PyAny> = if message.is_truthy()? { message } else { - item.getattr("action")?.getattr("value")? + native_field(&item, "action")?.getattr("value")? }; items.push(format!( "{}: {}", - item.getattr("name")?.str()?, + native_field(&item, "name")?.str()?, detail.str()? )); } @@ -425,7 +438,10 @@ fn clone_boundaries<'py>( call_with( ®istry::class(py, RecordId::ClonePipelineConnection)?, &[ - ("config", context.invocation.getattr("connection_config")?), + ( + "config", + native_field(&context.invocation, "connection_config")?, + ), ("handle", connection.clone()), ], )?, @@ -435,10 +451,13 @@ fn clone_boundaries<'py>( call_with( ®istry::class(py, RecordId::ClonePipelineOptions)?, &[ - ("no_sql_validation", request.getattr("no_sql_validation")?), - ("no_cache", request.getattr("no_cache")?), + ( + "no_sql_validation", + native_field(request, "no_sql_validation")?, + ), + ("no_cache", native_field(request, "no_cache")?), ("select", PyTuple::new(py, &prephase.select)?.into_any()), - ("cli_vars", request.getattr("cli_vars")?), + ("cli_vars", native_field(request, "cli_vars")?), ], )?, ), @@ -467,30 +486,30 @@ fn clone_boundaries<'py>( "C411", )); } - let project: Bound<'py, PyAny> = pipeline.getattr("destination_project")?; + let project: Bound<'py, PyAny> = native_field(&pipeline, "destination_project")?; call_with( &host::entry(py, ExecutorHost::CopyCloneFingerprints)?, &[ ("result", result), ( "origin_model_entries", - pipeline.getattr("origin_model_entries")?, + native_field(&pipeline, "origin_model_entries")?, ), ( "destination_model_entries", - pipeline.getattr("destination_model_entries")?, + native_field(&pipeline, "destination_model_entries")?, ), ( "origin_seed_entries", - pipeline.getattr("origin_seed_entries")?, + native_field(&pipeline, "origin_seed_entries")?, ), ( "destination_seed_entries", - pipeline.getattr("destination_seed_entries")?, + native_field(&pipeline, "destination_seed_entries")?, ), ("adapter", prephase.adapter.clone()), ("destination_connection", connection.clone()), - ("run_id", project.getattr("run_id")?), + ("run_id", native_field(&project, "run_id")?), ( "query_change_tracking", project @@ -534,7 +553,7 @@ fn run_prephase(context: &BuildContext<'_>, prephase: &Prephase<'_>) -> PyResult )?; let connection: Bound<'_, PyAny> = prephase.adapter.call_method1( "connect", - (context.invocation.getattr("connection_config")?,), + (native_field(&context.invocation, "connection_config")?,), )?; let cloned: PyResult<()> = clone_boundaries(context, prephase, &connection); prephase.adapter.call_method1("close", (connection,))?; @@ -548,13 +567,13 @@ pub(crate) fn run_defer_clone_boundary_prephase( ) -> PyResult<()> { let py: Python<'_> = context.py(); let (request, invocation) = (&context.request, &context.invocation); - let discovered: Bound<'_, PyAny> = invocation.getattr("discovered_inputs")?; + let discovered: Bound<'_, PyAny> = native_field(invocation, "discovered_inputs")?; let split: Bound<'_, PyAny> = native( py, NativeAttr::SplitUnitTestSelectors, &[ - ("select", request.getattr("select")?), - ("exclude", request.getattr("exclude")?), + ("select", native_field(request, "select")?), + ("exclude", native_field(request, "exclude")?), ("discovered_inputs", discovered.clone()), ], )?; @@ -565,24 +584,27 @@ pub(crate) fn run_defer_clone_boundary_prephase( { return Ok(()); } - let adapter: Bound<'_, PyAny> = invocation.getattr("adapter")?; - let project_dir: Bound<'_, PyAny> = invocation.getattr("effective_project_dir")?; + let adapter: Bound<'_, PyAny> = native_field(invocation, "adapter")?; + let project_dir: Bound<'_, PyAny> = native_field(invocation, "effective_project_dir")?; let project: Bound<'_, PyAny> = native( py, NativeAttr::BuildCompiledProject, &[ ("discovered_inputs", discovered.clone()), ("adapter", adapter.clone()), - ("selected_target", request.getattr("selected_target")?), - ("no_sql_validation", request.getattr("no_sql_validation")?), + ("selected_target", native_field(request, "selected_target")?), + ( + "no_sql_validation", + native_field(request, "no_sql_validation")?, + ), ( "analysis_selection", call_with( ®istry::class(py, RecordId::CompileAnalysisSelection)?, - &[("no_cache", request.getattr("no_cache")?)], + &[("no_cache", native_field(request, "no_cache")?)], )?, ), - ("cli_vars", request.getattr("cli_vars")?), + ("cli_vars", native_field(request, "cli_vars")?), ( "external_sql_reference_resolver", external_resolver(&project_dir, &discovered)?, @@ -594,11 +616,11 @@ pub(crate) fn run_defer_clone_boundary_prephase( NativeAttr::BuildPlannerScope, &[ ("project", project.clone()), - ("select", split.getattr("select")?), - ("exclude", split.getattr("exclude")?), + ("select", native_field(&split, "select")?), + ("exclude", native_field(&split, "exclude")?), ( "auto_load_sources", - invocation.getattr("should_load_sources")?, + native_field(invocation, "should_load_sources")?, ), ], )?; @@ -609,8 +631,11 @@ pub(crate) fn run_defer_clone_boundary_prephase( NativeAttr::EnforceModelExecutionLimit, &[ ("model_count", model_count.into_pyobject(py)?.into_any()), - ("target_name", invocation.getattr("effective_target_name")?), - ("limits", invocation.getattr("execution_limits")?), + ( + "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)?, @@ -625,9 +650,9 @@ pub(crate) fn run_defer_clone_boundary_prephase( discovered, adapter, origin, - destination: project.getattr("effective_target_name")?, + destination: native_field(&project, "effective_target_name")?, select, - caused_by: split.getattr("select")?, + caused_by: native_field(&split, "select")?, project_dir, }, ) 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 9d96806187..a3a257648c 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/build_command/execution.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/build_command/execution.rs @@ -24,6 +24,7 @@ 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::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; @@ -75,8 +76,8 @@ fn run_context_facts( concurrency: &Bound<'_, PyAny>, ) -> PyResult { let py: Python<'_> = context.py(); - let project: Bound<'_, PyAny> = pipeline.getattr("project")?; - let plan: Bound<'_, PyAny> = pipeline.getattr("plan_output")?; + let project: Bound<'_, PyAny> = native_field(pipeline, "project")?; + let plan: Bound<'_, PyAny> = native_field(pipeline, "plan_output")?; let discovered: Bound<'_, PyAny> = context.get("discovered_inputs")?; let selected_count: usize = count(&plan, "model_entries")? + count(&plan, "seed_entries")? @@ -84,7 +85,7 @@ fn run_context_facts( + count(&plan, "source_load_entries")? + count(pipeline, "python_plan_entries")?; let mut loaded_sources: usize = 0; - for source in project.getattr("sources")?.try_iter()? { + for source in native_field(&project, "sources")?.try_iter()? { if !source? .getattr("source_entry")? .getattr("loader")? @@ -103,38 +104,41 @@ fn run_context_facts( ®istry::native(py, NativeAttr::DescribeConnectionWarehouse)?, &[ ("discovered_inputs", discovered), - ("selected_target", project.getattr("effective_target_name")?), + ( + "selected_target", + native_field(&project, "effective_target_name")?, + ), ("connection_config", context.get("connection_config")?), ], )?; - let warehouse: Bound<'_, PyAny> = resolved.getattr("warehouse")?; + let warehouse: Bound<'_, PyAny> = native_field(&resolved, "warehouse")?; let warehouse: Option<(String, String)> = if warehouse.is_none() { None } else { Some(( display_text(&warehouse)?.unwrap_or_else(|| "not set".to_owned()), - resolved.getattr("source")?.str()?.extract()?, + native_field(&resolved, "source")?.str()?.extract()?, )) }; - let variables: Bound<'_, PyAny> = project.getattr("effective_vars")?; + let variables: Bound<'_, PyAny> = native_field(&project, "effective_vars")?; let variable = |name: &str| -> PyResult>> { let value: Bound<'_, PyAny> = variables.call_method1("get", (name,))?; Ok((!value.is_none()).then_some(value)) }; let mut selector_files: Vec = Vec::new(); - for file in context.request.getattr("selector_files")?.try_iter()? { + for file in native_field(&context.request, "selector_files")?.try_iter()? { let file: Bound<'_, PyAny> = file?; selector_files.push(SelectorFileFact { - path: file.getattr("path")?.str()?.extract()?, - selector_count: file.getattr("selector_count")?.extract()?, + path: native_field(&file, "path")?.str()?.extract()?, + selector_count: native_field(&file, "selector_count")?.extract()?, }); } Ok(RunContextFacts { command: EXECUTION_COMMAND.to_owned(), - run_id: display_text(&project.getattr("run_id")?)?, - target: display_text(&project.getattr("effective_target_name")?)?, - database: display_text(&project.getattr("effective_target_database")?)?, - schema: display_text(&project.getattr("effective_target_schema")?)?, + run_id: display_text(&native_field(&project, "run_id")?)?, + target: display_text(&native_field(&project, "effective_target_name")?)?, + database: display_text(&native_field(&project, "effective_target_database")?)?, + schema: display_text(&native_field(&project, "effective_target_schema")?)?, warehouse, concurrency: concurrency.extract()?, full_refresh: context.request_flag("full_refresh")?, @@ -153,7 +157,7 @@ pub(crate) fn write_run_context( context: &BuildContext<'_>, pipeline: &Bound<'_, PyAny>, ) -> PyResult<()> { - let concurrency: Bound<'_, PyAny> = context.concurrency(&pipeline.getattr("project")?)?; + let concurrency: Bound<'_, PyAny> = context.concurrency(&native_field(pipeline, "project")?)?; let facts: RunContextFacts = run_context_facts(context, pipeline, &concurrency)?; emit( &context.stream, @@ -171,13 +175,13 @@ fn cursors<'py>( plan: &Bound<'py, PyAny>, ) -> PyResult<[Bound<'py, PyAny>; 4]> { let py: Python<'py> = context.py(); - let mut overrides: Bound<'py, PyAny> = plan.getattr("cursor_overrides")?; + 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", - context.request.getattr("cursor_overrides")?, + native_field(&context.request, "cursor_overrides")?, )], )?; } @@ -185,10 +189,10 @@ fn cursors<'py>( overrides = registry::class(py, RecordId::CursorOverrides)?.call0()?; } Ok([ - cursor_timestamp(&overrides.getattr("start_ts")?)?, - cursor_timestamp(&overrides.getattr("end_ts")?)?, - cursor_integer(&overrides.getattr("start_int")?)?, - cursor_integer(&overrides.getattr("end_int")?)?, + cursor_timestamp(&native_field(&overrides, "start_ts")?)?, + cursor_timestamp(&native_field(&overrides, "end_ts")?)?, + cursor_integer(&native_field(&overrides, "start_int")?)?, + cursor_integer(&native_field(&overrides, "end_int")?)?, ]) } @@ -199,22 +203,23 @@ pub(crate) fn prepare_build_execution<'py>( providers: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = context.py(); - let plan: Bound<'py, PyAny> = pipeline.getattr("plan_output")?; + 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", context.request.getattr("verbose")?), + ("verbose", native_field(&context.request, "verbose")?), ("debug", boolean(py, debug)), ( "event_output_path", - context.request.getattr("event_output_path")?, + native_field(&context.request, "event_output_path")?, ), ], )?; - let concurrency: Bound<'py, PyAny> = context.concurrency(&pipeline.getattr("project")?)?; + let concurrency: Bound<'py, PyAny> = + context.concurrency(&native_field(pipeline, "project")?)?; if context.verbose { write_run_context(context, pipeline)?; } else { @@ -236,7 +241,7 @@ pub(crate) fn prepare_build_execution<'py>( ("stream", context.stream.clone()), ( "blank_line_before_start", - boolean(py, plan.getattr("source_load_entries")?.is_truthy()?), + boolean(py, native_field(&plan, "source_load_entries")?.is_truthy()?), ), ("blank_line_after_complete", boolean(py, true)), ("use_color", boolean(py, context.use_color)), @@ -245,8 +250,8 @@ pub(crate) fn prepare_build_execution<'py>( let cursors: [Bound<'py, PyAny>; 4] = cursors(context, &plan)?; let inputs: LifecycleInputs<'py> = LifecycleInputs { cursors: cursors.clone(), - on_node_start: keyword_relay(callbacks.getattr("on_node_start")?, &NODE_START)?, - on_node_complete: callbacks.getattr("on_node_complete")?, + on_node_start: keyword_relay(native_field(&callbacks, "on_node_start")?, &NODE_START)?, + on_node_complete: native_field(&callbacks, "on_node_complete")?, providers: providers.clone(), }; let lifecycle: Bound<'py, DirectPythonLifecycle> = @@ -279,8 +284,8 @@ pub(crate) fn execute_build_plan<'py>( providers: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = context.py(); - let plan: Bound<'py, PyAny> = pipeline.getattr("plan_output")?; - let project: Bound<'py, PyAny> = pipeline.getattr("project")?; + let plan: Bound<'py, PyAny> = native_field(pipeline, "plan_output")?; + let project: Bound<'py, PyAny> = native_field(pipeline, "project")?; let adapter: Bound<'py, PyAny> = context.get("adapter")?; let connection_config: Bound<'py, PyAny> = context.get("connection_config")?; let callbacks: &Bound<'py, PyAny> = &preparation.callbacks; @@ -290,7 +295,7 @@ pub(crate) fn execute_build_plan<'py>( let runtime: Bound<'py, PyAny> = call_with( &host::entry(py, ExecutorHost::BuildRuntimeParams)?, &[ - ("run_id", project.getattr("run_id")?), + ("run_id", native_field(&project, "run_id")?), ( "runtime_dir", joined(&context.get("effective_project_dir")?, &["target"])?, @@ -333,28 +338,31 @@ pub(crate) fn execute_build_plan<'py>( ("on_migration_progress", line_writer(&context.stream)?), ( "on_node_start", - keyword_relay(callbacks.getattr("on_node_start")?, &NODE_START)?, + keyword_relay(native_field(callbacks, "on_node_start")?, &NODE_START)?, ), ( "on_node_complete", - preparation.lifecycle.getattr("on_node_complete")?, + native_field(&preparation.lifecycle, "on_node_complete")?, + ), + ( + "on_sub_progress", + native_field(callbacks, "on_sub_progress")?, ), - ("on_sub_progress", callbacks.getattr("on_sub_progress")?), ( "on_connection_start", - connection.getattr("on_connection_start")?, + native_field(connection, "on_connection_start")?, ), ( "on_connection_complete", keyword_relay( - connection.getattr("on_connection_complete")?, + native_field(connection, "on_connection_complete")?, &CONNECTION_TIMING, )?, ), ( "on_connection_error", keyword_relay( - connection.getattr("on_connection_error")?, + native_field(connection, "on_connection_error")?, &CONNECTION_TIMING, )?, ), @@ -373,7 +381,7 @@ pub(crate) fn execute_build_plan<'py>( &[ ( "custom_materializations", - pipeline.getattr("custom_materializations")?, + native_field(pipeline, "custom_materializations")?, ), ("loader_functions", lifecycle.loader_functions(py)?), ], @@ -392,7 +400,7 @@ pub(crate) fn execute_build_plan<'py>( ("plan", plan.clone()), ("connection_config", connection_config.clone()), ("adapter", adapter.clone()), - ("settings", project.getattr("settings")?), + ("settings", native_field(&project, "settings")?), ("runtime", runtime), ("callbacks", build_callbacks), ("customizations", customizations), @@ -403,7 +411,7 @@ pub(crate) fn execute_build_plan<'py>( (&plan, &result), &adapter, &connection_config, - &project.getattr("run_id")?, + &native_field(&project, "run_id")?, )?; lifecycle.finalize(py)?; Ok(Outcome { @@ -420,10 +428,10 @@ fn check_runtime<'py>( ) -> PyResult> { let py: Python<'py> = context.py(); let adapter: Bound<'py, PyAny> = context.get("adapter")?; - let project: Bound<'py, PyAny> = pipeline.getattr("project")?; + let project: Bound<'py, PyAny> = native_field(pipeline, "project")?; let names: Vec> = checks .iter() - .map(|check| check.getattr("name")) + .map(|check| native_field(check, "name")) .collect::>()?; call_with( &host::entry(py, ExecutorHost::PythonNodeRuntime)?, @@ -431,10 +439,13 @@ fn check_runtime<'py>( ("adapter", adapter.clone()), ("connection_config", context.get("connection_config")?), ("connection", connection.clone()), - ("run_id", project.getattr("run_id")?), - ("target", project.getattr("effective_target_name")?), - ("vars", project.getattr("effective_vars")?), - ("is_reload", context.request.getattr("reload_sources")?), + ("run_id", native_field(&project, "run_id")?), + ("target", native_field(&project, "effective_target_name")?), + ("vars", native_field(&project, "effective_vars")?), + ( + "is_reload", + native_field(&context.request, "reload_sources")?, + ), ( "default_database", adapter.call_method0("default_database")?, @@ -450,7 +461,7 @@ fn check_runtime<'py>( ®istry::native(py, NativeAttr::BuildProjectRelationTargets)?, &[ ("adapter", adapter), - ("plan_output", pipeline.getattr("plan_output")?), + ("plan_output", native_field(pipeline, "plan_output")?), ], )?, ), @@ -475,28 +486,34 @@ pub(crate) fn run_post_build_python_checks<'py>( ) -> PyResult> { let py: Python<'py> = context.py(); let result: &Bound<'py, PyAny> = &outcome.result; - if result.getattr("status")?.str()?.extract::()? != SUCCESS_STATUS { + if native_field(result, "status")?.str()?.extract::()? != SUCCESS_STATUS { return tuple(py, &[]); } let discovered: Bound<'py, PyAny> = context.get("discovered_inputs")?; - let plan: Bound<'py, PyAny> = pipeline.getattr("plan_output")?; - let load_results: Bound<'py, PyAny> = result.getattr("load_results")?; + let plan: Bound<'py, PyAny> = native_field(pipeline, "plan_output")?; + let load_results: Bound<'py, PyAny> = native_field(result, "load_results")?; let graph: Bound<'py, PyAny> = python_node_graph(&discovered)?; let by_loader = || -> PyResult> { - Ok(load_results_by_loader_name(&plan.getattr("source_map")?, &load_results)?.into_any()) + Ok( + load_results_by_loader_name(&native_field(&plan, "source_map")?, &load_results)? + .into_any(), + ) }; let mut dependencies: Vec> = names_of(&outcome.python_results, "node_name")?; for name in by_loader()?.try_iter()? { dependencies.push(name?); } let mut completed: Vec> = - names_of(&result.getattr("model_results")?, "model_name")?; - completed.extend(names_of(&result.getattr("seed_results")?, "seed_name")?); + names_of(&native_field(result, "model_results")?, "model_name")?; + completed.extend(names_of( + &native_field(result, "seed_results")?, + "seed_name", + )?); completed.extend(names_of(&load_results, "source_name")?); let checks: Vec> = relevant_check_functions( &discovered, &graph, - context.request.getattr("exclude")?, + native_field(&context.request, "exclude")?, (&frozenset(py, &dependencies)?, &frozenset(py, &completed)?), )?; if checks.is_empty() { @@ -512,13 +529,13 @@ pub(crate) fn run_post_build_python_checks<'py>( &discovered, &run_state, (&outcome.python_results, Some(&load_results)), - Some(&plan.getattr("source_map")?), + Some(&native_field(&plan, "source_map")?), )?; let callbacks: Bound<'py, PyAny> = call_with( &host::entry(py, ExecutorHost::PythonCheckCallbacks)?, &[( "on_check_complete", - preparation.callbacks.getattr("write_execution_event")?, + native_field(&preparation.callbacks, "write_execution_event")?, )], )?; call_with( @@ -561,9 +578,9 @@ pub(crate) fn execute_direct_build<'py>( providers: &Bound<'py, PyAny>, ) -> PyResult<(Preparation<'py>, Outcome<'py>, Bound<'py, PyAny>)> { let py: Python<'py> = context.py(); - let project: Bound<'py, PyAny> = pipeline.getattr("project")?; - let run_id: Bound<'py, PyAny> = project.getattr("run_id")?; - let target_name: Bound<'py, PyAny> = project.getattr("effective_target_name")?; + let project: Bound<'py, PyAny> = native_field(pipeline, "project")?; + let run_id: Bound<'py, PyAny> = native_field(&project, "run_id")?; + let target_name: Bound<'py, PyAny> = native_field(&project, "effective_target_name")?; let resource_name: Bound<'py, PyAny> = if target_name.is_truthy()? { target_name } else { @@ -594,7 +611,7 @@ pub(crate) fn execute_direct_build<'py>( let outcome: Outcome<'py> = execute_build_plan(context, pipeline, &preparation, providers)?; let checks_scope: Bound<'py, PyAny> = call_with( - &cost_context.getattr("resource_scope")?, + &native_field(&cost_context, "resource_scope")?, &[ ("resource_type", text(py, "run")), ("resource_name", resource_name.clone()), 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 06f649fff6..89a6712f1f 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/build_command/invocation.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/build_command/invocation.rs @@ -12,6 +12,7 @@ use crate::bindings::_helpers::build_command::support::{boolean, or_none, text}; use crate::bindings::_helpers::command_runtime::adapters::resolve_cli_adapter; use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; 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, RecordId}; use crate::bindings::_helpers::records::registry; @@ -44,7 +45,7 @@ impl<'py> BuildContext<'py> { /// The requested concurrency, or the project setting. pub(crate) fn concurrency(&self, project: &Bound<'py, PyAny>) -> PyResult> { or_none(self.request.getattr("concurrency")?, || { - project.getattr("settings")?.getattr("concurrency") + native_field(project, "settings")?.getattr("concurrency") }) } } @@ -57,8 +58,11 @@ fn target_config<'py>( call_with( ®istry::native(py, NativeAttr::ResolveTargetConfig)?, &[ - ("project_config", discovered.getattr("project_config")?), - ("local_config", discovered.getattr("local_config")?), + ( + "project_config", + native_field(discovered, "project_config")?, + ), + ("local_config", native_field(discovered, "local_config")?), ("target_name", target_name.clone()), ], ) @@ -73,17 +77,17 @@ fn execution_limits<'py>( let limits: Bound<'py, PyAny> = if target_name.is_none() { registry::class(py, RecordId::ExecutionLimitsConfig)?.call0()? } else { - target_config(discovered, target_name)?.getattr("execution_limits")? + native_field(&target_config(discovered, target_name)?, "execution_limits")? }; let limit: DurationLimit = DurationLimit { target_name: target_name.extract()?, adapter_name: adapter_name.to_owned(), - configures_max_duration: !limits.getattr("max_duration")?.is_none(), - max_duration_seconds: limits.getattr("max_duration_seconds")?.extract()?, + configures_max_duration: !native_field(&limits, "max_duration")?.is_none(), + max_duration_seconds: native_field(&limits, "max_duration_seconds")?.extract()?, adapter_limit_seconds: adapter .getattr("execution_duration_limit_seconds")? .extract()?, - remediation: limits.getattr("remediation")?.extract()?, + remediation: native_field(&limits, "remediation")?.extract()?, }; if let Some(error) = duration_limit_error(&limit) { return Err(build_command_error(py, error)); @@ -98,7 +102,7 @@ pub(crate) fn resolve_build_context<'py>( started: Instant, ) -> PyResult> { let py: Python<'py> = request.py(); - let project_dir: Bound<'py, PyAny> = or_none(request.getattr("project_dir")?, || { + let project_dir: Bound<'py, PyAny> = or_none(native_field(request, "project_dir")?, || { imported(py, "pathlib", "Path")?.call_method0("cwd") })?; let discovered: Bound<'py, PyAny> = call_with( @@ -108,23 +112,29 @@ pub(crate) fn resolve_build_context<'py>( ("extract_output_column_locations", boolean(py, false)), ], )?; - let selected_target: Bound<'py, PyAny> = request.getattr("selected_target")?; + 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", discovered.getattr("project_config")?), - ("local_config", discovered.getattr("local_config")?), + ( + "project_config", + native_field(&discovered, "project_config")?, + ), + ("local_config", native_field(&discovered, "local_config")?), ("selected_target", selected_target.clone()), ], )?; let defer_clone_from: Bound<'py, PyAny> = - or_none(request.getattr("defer_clone_from")?, || { + or_none(native_field(request, "defer_clone_from")?, || { if target_name.is_none() { return Ok(target_name.clone()); } - target_config(&discovered, &target_name)?.getattr("defer_clone_from") + native_field( + &target_config(&discovered, &target_name)?, + "defer_clone_from", + ) })?; - if !request.getattr("defer_to")?.is_none() && !defer_clone_from.is_none() { + if !native_field(request, "defer_to")?.is_none() && !defer_clone_from.is_none() { return Err(build_command_error( py, BuildCommandError::new( @@ -137,8 +147,11 @@ pub(crate) fn resolve_build_context<'py>( let adapter_name: String = call_with( ®istry::native(py, NativeAttr::ResolveEffectiveAdapterName)?, &[ - ("project_config", discovered.getattr("project_config")?), - ("local_config", discovered.getattr("local_config")?), + ( + "project_config", + native_field(&discovered, "project_config")?, + ), + ("local_config", native_field(&discovered, "local_config")?), ], )? .extract()?; @@ -151,13 +164,14 @@ pub(crate) fn resolve_build_context<'py>( ("discovered_inputs", discovered.clone()), ("project_dir", project_dir.clone()), ("selected_target", selected_target), - ("cli_vars", request.getattr("cli_vars")?), + ("cli_vars", native_field(request, "cli_vars")?), ], )?; - let machine_output: bool = request.getattr("json_output")?.is_truthy()?; - let debug: bool = request.getattr("debug")?.is_truthy()?; - let use_color: bool = - !request.getattr("no_color")?.is_truthy()? && !machine_output && stdout_supports_color(py)?; + let machine_output: bool = native_field(request, "json_output")?.is_truthy()?; + let debug: bool = native_field(request, "debug")?.is_truthy()?; + let use_color: bool = !native_field(request, "no_color")?.is_truthy()? + && !machine_output + && stdout_supports_color(py)?; let stream: Bound<'py, PyAny> = py.import("sys")?.getattr(if debug || machine_output { "stderr" } else { @@ -178,8 +192,8 @@ pub(crate) fn resolve_build_context<'py>( ("use_color", boolean(py, use_color)), ], )?; - let load_sources: Bound<'py, PyAny> = request.getattr("load_sources")?; - let should_load_sources: bool = request.getattr("reload_sources")?.is_truthy()? + 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)?, @@ -213,7 +227,7 @@ pub(crate) fn resolve_build_context<'py>( invocation, stream, use_color, - verbose: request.getattr("verbose")?.is_truthy()? || debug, + verbose: native_field(request, "verbose")?.is_truthy()? || debug, started, tracker: tracker.clone(), }) 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 6cece1234f..b86d3fed28 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/build_command/lifecycle.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/build_command/lifecycle.rs @@ -18,6 +18,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::{call_with, importe 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::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; @@ -37,11 +38,11 @@ fn node_rows(results: &Bound<'_, PyAny>) -> PyResult> { for result in results.try_iter()? { let result: Bound<'_, PyAny> = result?; rows.push(PythonNodeRow { - kind: result.getattr("kind")?.getattr("value")?.extract()?, - node_name: result.getattr("node_name")?.extract()?, - status: result.getattr("status")?.str()?.extract()?, - error_message: result.getattr("error_message")?.extract()?, - skip_reason: result.getattr("skip_reason")?.extract()?, + kind: native_field(&result, "kind")?.getattr("value")?.extract()?, + node_name: native_field(&result, "node_name")?.extract()?, + status: native_field(&result, "status")?.str()?.extract()?, + error_message: native_field(&result, "error_message")?.extract()?, + skip_reason: native_field(&result, "skip_reason")?.extract()?, }); } Ok(rows) @@ -60,7 +61,7 @@ pub(crate) fn write_python_node_results( fn result_names(results: &Bound<'_, PyAny>) -> PyResult> { results .try_iter()? - .map(|result| result?.getattr("node_name")?.extract()) + .map(|result| native_field(&result?, "node_name")?.extract()) .collect() } @@ -95,12 +96,12 @@ impl DirectPythonLifecycle { return Ok(()); }; let tracker: &Bound<'_, PyAny> = tracker.bind(py); - let previous: HashSet = result_names(&tracker.getattr("results")?)?; + let previous: HashSet = result_names(&native_field(tracker, "results")?)?; tracker.call_method1("record_sql_result", (node_result,))?; let mut new_results: Vec> = Vec::new(); - for result in tracker.getattr("results")?.try_iter()? { + for result in native_field(tracker, "results")?.try_iter()? { let result: Bound<'_, PyAny> = result?; - if !previous.contains(&result.getattr("node_name")?.extract::()?) { + if !previous.contains(&native_field(&result, "node_name")?.extract::()?) { new_results.push(result); } } @@ -127,7 +128,7 @@ impl DirectPythonLifecycle { .try_iter()? .collect::>()?; if let Some(tracker) = &self.read_side_tracker { - for result in tracker.bind(py).getattr("results")?.try_iter()? { + for result in native_field(tracker.bind(py), "results")?.try_iter()? { results.push(result?); } } @@ -144,16 +145,16 @@ impl DirectPythonLifecycle { .collect::>()?; let functions: Vec> = loaders .iter() - .map(|loader| loader.getattr("function")) + .map(|loader| native_field(loader, "function")) .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(loader.getattr("name")?)?; + let keeps_all: bool = !ingress.contains(native_field(&loader, "name")?)?; let mut dependencies: Vec> = Vec::new(); - for dependency in loader.getattr("depends_on")?.try_iter()? { + for dependency in native_field(&loader, "depends_on")?.try_iter()? { let dependency: Bound<'py, PyAny> = dependency?; if keeps_all || functions.contains(&dependency)? { dependencies.push(dependency); @@ -177,14 +178,17 @@ impl DirectPythonLifecycle { for result in self.ingress_load_results.bind(py).try_iter()? { let result: Bound<'py, PyAny> = result?; if failed_only - && result.getattr("status")?.str()?.extract::()? == SUCCESS_STATUS + && native_field(&result, "status")? + .str()? + .extract::()? + == SUCCESS_STATUS { continue; } - let name: Bound<'py, PyAny> = result.getattr("source_name")?; + let name: Bound<'py, PyAny> = native_field(&result, "source_name")?; for entry in &entries { - if entry.getattr("name")?.eq(&name)? { - keys.push(entry.getattr("key")?); + if native_field(entry, "name")?.eq(&name)? { + keys.push(native_field(entry, "key")?); break; } } @@ -230,14 +234,14 @@ impl DirectPythonLifecycle { /// The models, seeds and source loads this run executes and reports results for. fn scheduled_sql_names(plan: &Bound<'_, PyAny>) -> PyResult> { - let scheduled: Bound<'_, PyAny> = - imported(plan.py(), "builtins", "frozenset")?.call1((plan.getattr("execution_order")?,))?; + let scheduled: Bound<'_, PyAny> = imported(plan.py(), "builtins", "frozenset")? + .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()? { let entry: Bound<'_, PyAny> = entry?; - if scheduled.contains(entry.getattr("key")?)? { - names.insert(entry.getattr("name")?.extract()?); + if scheduled.contains(native_field(&entry, "key")?)? { + names.insert(native_field(&entry, "name")?.extract()?); } } } @@ -266,9 +270,12 @@ fn node_runtime<'py>( ("adapter", parts.adapter.clone()), ("connection_config", parts.connection_config.clone()), ("connection", connection.clone()), - ("run_id", parts.project.getattr("run_id")?), - ("target", parts.project.getattr("effective_target_name")?), - ("vars", parts.project.getattr("effective_vars")?), + ("run_id", native_field(parts.project, "run_id")?), + ( + "target", + native_field(parts.project, "effective_target_name")?, + ), + ("vars", native_field(parts.project, "effective_vars")?), ("is_reload", boolean(py, parts.reload)), ("default_database", parts.defaults.0.clone()), ("default_schema", parts.defaults.1.clone()), @@ -295,7 +302,7 @@ fn lifecycle_plan<'py>( pipeline: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = discovered.py(); - let plan_output: Bound<'py, PyAny> = pipeline.getattr("plan_output")?; + 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)?, @@ -303,9 +310,12 @@ fn lifecycle_plan<'py>( ( "sql_keys", imported(py, "builtins", "frozenset")? - .call1((plan_output.getattr("upstream_deps")?,))?, + .call1((native_field(&plan_output, "upstream_deps")?,))?, + ), + ( + "python_node_names", + native_field(pipeline, "python_node_names")?, ), - ("python_node_names", pipeline.getattr("python_node_names")?), ], )?; let plan: Bound<'py, PyAny> = call_with( @@ -316,20 +326,20 @@ fn lifecycle_plan<'py>( imported(py, COMPILE_TYPES, "CompiledResourceType")?.getattr("SOURCE")?; let key_class: Bound<'py, PyAny> = registry::class(py, RecordId::CompiledObjectKey)?; let mut keys: Vec> = Vec::new(); - for name in plan.getattr("ingress_loader_names")?.try_iter()? { + for name in native_field(&plan, "ingress_loader_names")?.try_iter()? { keys.push(call_with( &key_class, &[("name", name?), ("resource_type", source_type.clone())], )?); } let source_map: Bound<'py, PyAny> = - imported(py, "builtins", "dict")?.call1((plan_output.getattr("source_map")?,))?; + imported(py, "builtins", "dict")?.call1((native_field(&plan_output, "source_map")?,))?; source_map.call_method1( "update", (call_with( ®istry::native(py, NativeAttr::BuildIntermediateSourceMap)?, &[ - ("project", pipeline.getattr("project")?), + ("project", native_field(pipeline, "project")?), ("selected_keys", frozenset(py, &keys)?), ], )?,), @@ -348,11 +358,11 @@ fn task_asset_names( let mut known: HashSet = HashSet::new(); for kind in ["task_functions", "asset_functions"] { for node in discovered.getattr(kind)?.try_iter()? { - known.insert(node?.getattr("name")?.extract()?); + known.insert(native_field(&node?, "name")?.extract()?); } } let mut names: Vec = Vec::new(); - for name in pipeline.getattr("python_node_names")?.try_iter()? { + for name in native_field(pipeline, "python_node_names")?.try_iter()? { let name: String = name?.extract()?; if known.contains(&name) { names.push(name); @@ -371,22 +381,26 @@ pub(crate) fn prepare_direct_python_lifecycle<'py>( let discovered: Bound<'py, PyAny> = context.get("discovered_inputs")?; let adapter: Bound<'py, PyAny> = context.get("adapter")?; let connection_config: Bound<'py, PyAny> = context.get("connection_config")?; - let project: Bound<'py, PyAny> = pipeline.getattr("project")?; - let plan_output: Bound<'py, PyAny> = pipeline.getattr("plan_output")?; + let project: Bound<'py, PyAny> = native_field(pipeline, "project")?; + let plan_output: Bound<'py, PyAny> = native_field(pipeline, "plan_output")?; let selected: Vec = if context.request_flag("include_python")? { task_asset_names(&discovered, pipeline)? } else { Vec::new() }; let lifecycle: LifecyclePlan<'py> = lifecycle_plan(&discovered, pipeline)?; - let ingress_names: Bound<'py, PyAny> = lifecycle.plan.getattr("ingress_python_node_names")?; + let ingress_names: Bound<'py, PyAny> = + native_field(&lifecycle.plan, "ingress_python_node_names")?; let required: Bound<'py, PyAny> = call_with( - &lifecycle.graph.getattr("selected_sql_refs")?, + &native_field(&lifecycle.graph, "selected_sql_refs")?, &[( "selected_names", ingress_names.call_method1( "__or__", - (lifecycle.plan.getattr("read_side_python_node_names")?,), + (native_field( + &lifecycle.plan, + "read_side_python_node_names", + )?,), )?, )], )?; @@ -412,10 +426,10 @@ pub(crate) fn prepare_direct_python_lifecycle<'py>( project: &project, reload: context.request_flag("reload_sources")?, defaults: ( - or_none(project.getattr("effective_target_database")?, || { + or_none(native_field(&project, "effective_target_database")?, || { adapter.call_method0("default_database") })?, - or_none(project.getattr("effective_target_schema")?, || { + or_none(native_field(&project, "effective_target_schema")?, || { adapter.call_method0("default_schema") })?, ), @@ -441,7 +455,10 @@ pub(crate) fn prepare_direct_python_lifecycle<'py>( &[ ("python_graph", lifecycle.graph.clone()), ("selected_python_names", ingress_names.clone()), - ("loader_functions", discovered.getattr("loader_functions")?), + ( + "loader_functions", + native_field(&discovered, "loader_functions")?, + ), ("source_map", lifecycle.source_map.clone()), ("runtime", node_runtime(&parts, &connection)?), ("callbacks", callbacks), @@ -451,8 +468,8 @@ pub(crate) fn prepare_direct_python_lifecycle<'py>( let closed: PyResult> = adapter.call_method1("close", (connection,)); let executed: Bound<'py, PyAny> = executed?; closed?; - ingress_python = executed.getattr("python_results")?; - ingress_loads = executed.getattr("load_results")?; + ingress_python = native_field(&executed, "python_results")?; + ingress_loads = native_field(&executed, "load_results")?; write_python_node_results(&context.stream, &ingress_python, context.use_color)?; } let ingress_set: HashSet = ingress_names @@ -472,7 +489,7 @@ pub(crate) fn prepare_direct_python_lifecycle<'py>( read_side_connection = Some(connection.clone().unbind()); let mut scheduled: HashSet = scheduled_sql_names(&plan_output)?; for result in ingress_loads.try_iter()? { - scheduled.insert(result?.getattr("source_name")?.extract()?); + scheduled.insert(native_field(&result?, "source_name")?.extract()?); } let scheduled: Vec = scheduled.into_iter().collect(); let tracker: Bound<'py, PyAny> = call_with( @@ -488,7 +505,7 @@ pub(crate) fn prepare_direct_python_lifecycle<'py>( tracker.call_method1("record_sql_result", (result?,))?; } tracker.call_method0("dispatch_ready_python_nodes")?; - let initial: Bound<'py, PyAny> = tracker.getattr("results")?; + let initial: Bound<'py, PyAny> = native_field(&tracker, "results")?; if initial.is_truthy()? { write_python_node_results(&context.stream, &initial, context.use_color)?; } @@ -507,7 +524,7 @@ pub(crate) fn prepare_direct_python_lifecycle<'py>( read_side_tracker, ingress_python_results: ingress_python.unbind(), ingress_load_results: ingress_loads.unbind(), - ingress_loader_names: lifecycle.plan.getattr("ingress_loader_names")?.unbind(), + ingress_loader_names: native_field(&lifecycle.plan, "ingress_loader_names")?.unbind(), }, ) } 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 e81babcaa9..94469a8dc2 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/build_command/planning.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/build_command/planning.rs @@ -17,6 +17,7 @@ use crate::bindings::_helpers::cli::progress_text::with_verbose_inspection; 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::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; @@ -88,25 +89,25 @@ pub(crate) fn compile_build_plan<'py>(context: &BuildContext<'py>) -> PyResult = py.import("sys")?.into_any(); enforce_plan_policies( - &pipeline.getattr("plan_output")?, + &native_field(pipeline, "plan_output")?, ( &gates, &context @@ -178,7 +182,10 @@ pub(crate) fn enforce_build_plan_policies( &context.get("effective_target_name")?, &context.get("execution_limits")?, )?, - (&sys.getattr("stdin")?, &sys.getattr("stdout")?), + ( + &native_field(&sys, "stdin")?, + &native_field(&sys, "stdout")?, + ), ) } @@ -188,12 +195,12 @@ pub(crate) fn write_compile_target( pipeline: &Bound<'_, PyAny>, ) -> PyResult<()> { let py: Python<'_> = context.py(); - let plan: Bound<'_, PyAny> = pipeline.getattr("plan_output")?; + 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)?, &[ - ("project", pipeline.getattr("project")?), + ("project", native_field(pipeline, "project")?), ("plan_output", plan.clone()), ( "project_name", @@ -203,8 +210,8 @@ pub(crate) fn write_compile_target( .getattr("name")?, ), ("adapter_type", context.get("adapter_name")?), - ("upstream_deps", plan.getattr("upstream_deps")?), - ("downstream_deps", plan.getattr("downstream_deps")?), + ("upstream_deps", native_field(&plan, "upstream_deps")?), + ("downstream_deps", native_field(&plan, "downstream_deps")?), ], )? } else { 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 d00dc913d3..393b729b8a 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/build_command/policies.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/build_command/policies.rs @@ -17,6 +17,7 @@ use sqlbuild_core::build_command::models::{ }; 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::ErrorId; use crate::bindings::_helpers::records::registry; @@ -71,8 +72,8 @@ fn flag(value: &Bound<'_, PyAny>, name: &str) -> PyResult { } fn model_migration(entry: &Bound<'_, PyAny>) -> PyResult { - let origin: Bound<'_, PyAny> = entry.getattr("origin")?; - let qualified: Bound<'_, PyAny> = origin.getattr("qualified_name")?; + let origin: Bound<'_, PyAny> = native_field(entry, "origin")?; + let qualified: Bound<'_, PyAny> = native_field(&origin, "qualified_name")?; let origin_label: String = if qualified.is_truthy()? { qualified.str()?.extract()? } else { @@ -91,7 +92,7 @@ fn model_migration(entry: &Bound<'_, PyAny>) -> PyResult { } fn column_migration(entry: &Bound<'_, PyAny>) -> PyResult { - let decision: Bound<'_, PyAny> = entry.getattr("decision")?; + let decision: Bound<'_, PyAny> = native_field(entry, "decision")?; Ok(ColumnMigrationFact { model_name: text(entry, "model_name")?, destination_column: text(entry, "destination_column")?, @@ -145,7 +146,7 @@ fn plan_facts(plan: &Bound<'_, PyAny>) -> PyResult { { facts.snapshot_refreshes.push(SnapshotRefreshFact { name: text(&entry, "name")?, - historical: !entry.getattr("observed_at_column")?.is_none(), + historical: !native_field(&entry, "observed_at_column")?.is_none(), model_policy: optional_text(&entry, "snapshot_full_refresh")?, }); } @@ -161,7 +162,7 @@ fn plan_facts(plan: &Bound<'_, PyAny>) -> PyResult { for entry in entries(plan, "retention_entries")? { if flag(&entry, "decreases")? { facts.retention_decreases.push(RetentionFact { - model_names: entry.getattr("model_names")?.extract()?, + model_names: native_field(&entry, "model_names")?.extract()?, policy: text(&entry, "decrease_policy")?, }); } @@ -190,8 +191,8 @@ pub(crate) fn model_limit( ) -> PyResult { Ok(ModelLimit { target_name: target_name.extract()?, - max_models: limits.getattr("max_models")?.extract()?, - remediation: limits.getattr("remediation")?.extract()?, + max_models: native_field(limits, "max_models")?.extract()?, + remediation: native_field(limits, "remediation")?.extract()?, }) } 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 9724d0d04e..b98d01ab05 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 @@ -22,6 +22,7 @@ use crate::bindings::_helpers::build_progress::terminal::{ shared_lines, tty_flag, write, }; use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, within}; +use crate::bindings::_helpers::records::fields::field as native_field; /// The identity an audit plan entry or result reports to the progress projector. fn resource_id(value: &Bound<'_, PyAny>, name_field: &str) -> PyResult { @@ -192,7 +193,7 @@ impl AuditProgressReporter { Ok(Self { is_tty: tty_flag(&stream)?, stream: stream.unbind(), - lock: lines.getattr("lock")?.unbind(), + lock: native_field(&lines, "lock")?.unbind(), lines: lines.unbind(), style: CliStyle { use_color: arguments.value("use_color")?.is_truthy()?, 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 84c81681e2..6f5f280900 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 @@ -30,6 +30,7 @@ use crate::bindings::_helpers::build_progress::terminal::{ terminal_columns, tty_flag, write, }; 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; @@ -68,9 +69,9 @@ fn result_kind(result: &Bound<'_, PyAny>) -> PyResult { fn sql_test_resource_id(result: &Bound<'_, PyAny>) -> PyResult { let name: String = text(result, "test_name")?; - let path: Bound<'_, PyAny> = result.getattr("source_path")?; - let block: Bound<'_, PyAny> = result.getattr("block_index")?; - let case: Bound<'_, PyAny> = result.getattr("case_name")?; + let path: Bound<'_, PyAny> = native_field(result, "source_path")?; + let block: Bound<'_, PyAny> = native_field(result, "block_index")?; + let case: Bound<'_, PyAny> = native_field(result, "case_name")?; if path.is_none() || block.is_none() || case.is_none() { return Ok(format!("sql_test:{name}")); } @@ -345,7 +346,7 @@ impl BuildProgressCallbacks { }; write_one(result)?; if result_kind(result)? == ResultKind::Model { - for audit in result.getattr("audit_results")?.try_iter()? { + for audit in native_field(result, "audit_results")?.try_iter()? { write_one(&audit?)?; } } @@ -355,11 +356,11 @@ impl BuildProgressCallbacks { fn scheduler_state(state: &Bound<'_, PyAny>) -> PyResult { Ok(SchedulerStateText { - running: state.getattr("running")?.extract()?, - ready: state.getattr("ready")?.extract()?, - waiting: state.getattr("waiting")?.extract()?, - limit: state.getattr("limit")?.extract()?, - aborted: state.getattr("aborted")?.extract()?, + running: native_field(state, "running")?.extract()?, + ready: native_field(state, "ready")?.extract()?, + waiting: native_field(state, "waiting")?.extract()?, + limit: native_field(state, "limit")?.extract()?, + aborted: native_field(state, "aborted")?.extract()?, }) } @@ -367,7 +368,7 @@ fn statement(telemetry: &Bound<'_, PyAny>) -> PyResult { Ok(StatementText { query_id: optional(telemetry, "query_id")?, status: text(telemetry, "status")?, - elapsed_seconds: telemetry.getattr("elapsed_seconds")?.extract()?, + elapsed_seconds: native_field(telemetry, "elapsed_seconds")?.extract()?, resource_type: text(telemetry, "resource_type")?, resource_name: text(telemetry, "resource_name")?, phase: text(telemetry, "phase")?, @@ -394,9 +395,9 @@ impl BuildProgressCallbacks { let plan: Bound<'_, PyAny> = arguments.value("plan")?; let debug: bool = flag("debug")?; let sys: Bound<'_, PyAny> = py.import("sys")?.into_any(); - let stdout: Bound<'_, PyAny> = sys.getattr("stdout")?; + let stdout: Bound<'_, PyAny> = native_field(&sys, "stdout")?; let stream: Bound<'_, PyAny> = if debug { - sys.getattr("stderr")? + native_field(&sys, "stderr")? } else { stdout.clone() }; @@ -433,7 +434,7 @@ impl BuildProgressCallbacks { plan: plan.unbind(), is_tty: tty_flag(&stdout)? && !debug, stream: stream.unbind(), - lock: lines.getattr("lock")?.unbind(), + lock: native_field(&lines, "lock")?.unbind(), lines: lines.unbind(), event_writer: event_writer.unbind(), projector, @@ -472,7 +473,7 @@ impl BuildProgressCallbacks { { let mut state = callbacks.state(); state.node_name = name; - state.node_kind = resource_kind.getattr("value")?.extract()?; + state.node_kind = native_field(resource_kind, "value")?.extract()?; state.sub_message = String::new(); } if !callbacks.is_tty { 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 fc3fbb5177..a5c341cf17 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 @@ -19,6 +19,7 @@ use crate::bindings::_helpers::build_progress::terminal::{ terminal_columns, tty_flag, write, }; use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, within}; +use crate::bindings::_helpers::records::fields::field as native_field; const MIN_NAME_WIDTH: usize = 50; const ITEM_OPTIONS: (&[&str], &[&str]) = ( @@ -214,7 +215,7 @@ impl NestedCommandProgressCallbacks { Ok(Self { is_tty: tty_flag(&stream)?, stream: stream.unbind(), - lock: lines.getattr("lock")?.unbind(), + lock: native_field(&lines, "lock")?.unbind(), lines: lines.unbind(), label: arguments.value("label")?.extract()?, name_width: name_width.max(MIN_NAME_WIDTH), 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 3c788606b1..39fbc20cf0 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 @@ -11,6 +11,7 @@ use sqlbuild_core::plan_display::models::ModelLabelFacts; use crate::bindings::_helpers::build_progress::result_text::text; use crate::bindings::_helpers::plan_output::display::optional_facts; +use crate::bindings::_helpers::records::fields::field as native_field; const SOURCE_TYPE: &str = "source"; const SQL_TEST_TYPE: &str = "sql_test"; @@ -44,21 +45,22 @@ fn names(plan: &Bound<'_, PyAny>, field: &str) -> PyResult> { /// Whether the execution-order key is a source with a loader. fn loaded_source(plan: &Bound<'_, PyAny>, name: &Bound<'_, PyAny>) -> PyResult { - let source: Bound<'_, PyAny> = plan.getattr("source_map")?.call_method1("get", (name,))?; - Ok(!source.is_none() && !source.getattr("loader")?.is_none()) + let source: Bound<'_, PyAny> = + native_field(plan, "source_map")?.call_method1("get", (name,))?; + Ok(!source.is_none() && !native_field(&source, "loader")?.is_none()) } fn test_tree_children(plan: &Bound<'_, PyAny>) -> PyResult> { let mut children: Vec = Vec::new(); - for entry in plan.getattr("test_entries")?.try_iter()? { + for entry in native_field(plan, "test_entries")?.try_iter()? { let entry: Bound<'_, PyAny> = entry?; - for step in entry.getattr("chain")?.try_iter()? { + for step in native_field(&entry, "chain")?.try_iter()? { let step: Bound<'_, PyAny> = step?; - if step.getattr("expected_cte_sql")?.is_truthy()? { + if native_field(&step, "expected_cte_sql")?.is_truthy()? { children.push(text(&step, "model_name")?); } } - for assertion in entry.getattr("assertions")?.try_iter()? { + for assertion in native_field(&entry, "assertions")?.try_iter()? { children.push(format!("assertion {}", text(&assertion?, "name")?)); } } @@ -68,7 +70,7 @@ fn test_tree_children(plan: &Bound<'_, PyAny>) -> PyResult> { pub(crate) fn build_plan_facts(plan: &Bound<'_, PyAny>) -> PyResult { let mut models: HashMap = HashMap::new(); let mut row_names: Vec = Vec::new(); - for entry in plan.getattr("model_entries")?.try_iter()? { + for entry in native_field(plan, "model_entries")?.try_iter()? { let entry: Bound<'_, PyAny> = entry?; let name: String = text(&entry, "name")?; let facts: ModelRowFacts = row_facts( @@ -88,10 +90,10 @@ pub(crate) fn build_plan_facts(plan: &Bound<'_, PyAny>) -> PyResult = names(plan, "test_entries")?; let mut ordinals: Vec<(String, usize)> = Vec::new(); let mut loaded_sources: usize = 0; - for key in plan.getattr("execution_order")?.try_iter()? { + for key in native_field(plan, "execution_order")?.try_iter()? { let key: Bound<'_, PyAny> = key?; let resource_type: String = text(&key, "resource_type")?; - let name: Bound<'_, PyAny> = key.getattr("name")?; + let name: Bound<'_, PyAny> = native_field(&key, "name")?; if resource_type == SQL_TEST_TYPE { continue; } 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 d02c1b6bad..3a2e3db12c 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 @@ -9,6 +9,7 @@ use sqlbuild_core::progress::main::format_expectation_detail::format_expectation use crate::bindings::_helpers::cli::progress_text::expectation_step; use crate::bindings::_helpers::plan_output::display::{cursor_bound_display, inclusive_cursor_end}; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::sql_test_command::test_command::test_label; const SQL_EVENT: &str = "sql"; @@ -41,7 +42,7 @@ fn items<'py>(value: &Bound<'py, PyAny>, name: &str) -> PyResult) -> PyResult> { - value.getattr("duration_ms")?.extract() + native_field(value, "duration_ms")?.extract() } fn error(value: &Bound<'_, PyAny>) -> PyResult { @@ -62,7 +63,7 @@ fn skip(value: &Bound<'_, PyAny>) -> PyResult { fn events(value: &Bound<'_, PyAny>) -> PyResult> { let mut events: Vec = Vec::new(); for event in items(value, "lifecycle_events")? { - let content: String = event.getattr("content")?.extract()?; + let content: String = native_field(&event, "content")?.extract()?; events.push(if text(&event, "kind")? == SQL_EVENT { LifecycleEventText::Sql(content) } else { @@ -79,8 +80,8 @@ pub(crate) fn audit_text(audit: &Bound<'_, PyAny>) -> PyResult attached_target_name: optional(audit, "attached_target_name")?, run_scope_phase: text(audit, "run_scope_phase")?, outcome: text(audit, "outcome")?, - row_count: audit.getattr("row_count")?.extract()?, - reused: audit.getattr("reused")?.is_truthy()?, + row_count: native_field(audit, "row_count")?.extract()?, + reused: native_field(audit, "reused")?.is_truthy()?, executed_sql: optional(audit, "executed_sql")?, measurement: text(audit, "evaluation_mode")? == MEASUREMENT, measured_value: text(audit, "measured_value")?, @@ -103,13 +104,16 @@ fn hook_text(hook: &Bound<'_, PyAny>) -> PyResult { } fn cursor_range(model: &Bound<'_, PyAny>) -> PyResult> { - let start: Bound<'_, PyAny> = model.getattr("cursor_range_start")?; - let end: Bound<'_, PyAny> = model.getattr("cursor_range_end")?; - if start.is_none() || end.is_none() || model.getattr("empty_cursor_inputs")?.is_truthy()? { + let start: Bound<'_, PyAny> = native_field(model, "cursor_range_start")?; + let end: Bound<'_, PyAny> = native_field(model, "cursor_range_end")?; + if start.is_none() + || end.is_none() + || native_field(model, "empty_cursor_inputs")?.is_truthy()? + { return Ok(None); } - let cursor_type: Bound<'_, PyAny> = model.getattr("cursor_type")?; - let cursor_grain: Bound<'_, PyAny> = model.getattr("cursor_grain")?; + let cursor_type: Bound<'_, PyAny> = native_field(model, "cursor_type")?; + let cursor_grain: Bound<'_, PyAny> = native_field(model, "cursor_grain")?; Ok(Some(( cursor_bound_display(&start, &cursor_type, &cursor_grain)?, inclusive_cursor_end(&end, &cursor_type, &cursor_grain)?, @@ -123,12 +127,12 @@ pub(crate) fn model_text(model: &Bound<'_, PyAny>) -> PyResult failed_phase: optional(model, "failed_phase")?, staging_relation: optional(model, "staging_relation")?, duration_ms: duration(model)?, - batch_count: model.getattr("batch_count")?.extract()?, + batch_count: native_field(model, "batch_count")?.extract()?, batch_size: optional(model, "batch_size")?, - rows_affected: model.getattr("rows_affected")?.extract()?, + rows_affected: native_field(model, "rows_affected")?.extract()?, cursor_range: cursor_range(model)?, empty_cursor_inputs: texts(model, "empty_cursor_inputs")?, - waiting_on_empty_inputs: model.getattr("waiting_on_empty_inputs")?.is_truthy()?, + waiting_on_empty_inputs: native_field(model, "waiting_on_empty_inputs")?.is_truthy()?, recovery_batch_count: model .getattr("microbatch_recovery_batch_count")? .extract()?, @@ -176,7 +180,7 @@ pub(crate) fn load_text(load: &Bound<'_, PyAny>) -> PyResult { loader_name: text(load, "loader_name")?, resource_kind: text(load, "resource_kind")?, status: text(load, "status")?, - rows_loaded: load.getattr("rows_loaded")?.extract()?, + rows_loaded: native_field(load, "rows_loaded")?.extract()?, duration_ms: duration(load)?, events: events(load)?, skip: skip(load)?, @@ -215,8 +219,8 @@ pub(crate) fn python_node_text(node: &Bound<'_, PyAny>) -> PyResult) -> PyResult { Ok(BuildResultText { failed: text(result, "status")? == FAILED, - warning_count: result.getattr("warning_count")?.extract()?, - insufficient_count: result.getattr("insufficient_count")?.extract()?, + warning_count: native_field(result, "warning_count")?.extract()?, + insufficient_count: native_field(result, "insufficient_count")?.extract()?, models: items(result, "model_results")? .iter() .map(model_text) 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 83cf0620ae..63aaf8c68f 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/build_progress/terminal.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/build_progress/terminal.rs @@ -6,6 +6,7 @@ use pyo3::{IntoPyObjectExt, pyfunction, wrap_pyfunction}; use crate::bindings::_helpers::build_progress::transient_lines::shared_coordinator; 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; @@ -107,7 +108,7 @@ fn stop_progress_spinners(py: Python<'_>) -> PyResult<()> { .try_iter()? { let thread: Bound<'_, PyAny> = thread?; - if thread.getattr("name")?.eq(SPINNER_THREAD_NAME)? + if native_field(&thread, "name")?.eq(SPINNER_THREAD_NAME)? && thread.hasattr(STOP_EVENT_ATTRIBUTE)? { thread.getattr(STOP_EVENT_ATTRIBUTE)?.call_method0("set")?; @@ -133,7 +134,7 @@ pub(crate) fn start_spinner<'py>( let thread: Bound<'py, PyAny> = call_with( &imported(py, "threading", "Thread")?, &[ - ("target", owner.getattr("_spin_until_stopped")?), + ("target", native_field(owner, "_spin_until_stopped")?), ("args", PyTuple::new(py, [&event])?.into_any()), ("daemon", PyBool::new(py, true).to_owned().into_any()), ("name", SPINNER_THREAD_NAME.into_bound_py_any(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 c255be2ec5..702e84608b 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 @@ -13,6 +13,7 @@ use crate::bindings::_helpers::build_progress::terminal::{ join_spinner, release, shared_lines, start_spinner, terminal_columns, tty_flag, write, }; use crate::bindings::_helpers::compile_driver::python_calls::{imported, within}; +use crate::bindings::_helpers::records::fields::field as native_field; const NO_PROGRESS_VARIABLE: &str = "SQLBUILD_NO_PROGRESS"; const PROGRESS_DISABLED_VALUES: [&str; 4] = ["1", "on", "true", "yes"]; @@ -96,7 +97,7 @@ impl TransientStatusReporter { let register_hook: bool = !std::mem::replace(&mut reporter.state().exit_hook_registered, true); if register_hook { - imported(py, "atexit", "register")?.call1((slf.getattr("close")?,))?; + imported(py, "atexit", "register")?.call1((native_field(slf, "close")?,))?; } let stream: &Bound<'_, PyAny> = reporter.stream.bind(py); write(stream, HIDE_CURSOR)?; @@ -153,7 +154,7 @@ impl TransientStatusReporter { Ok(Self { enabled: enabled && !progress_spinners_disabled() && tty_flag(stream)?, stream: stream.clone().unbind(), - lock: lines.getattr("lock")?.unbind(), + lock: native_field(&lines, "lock")?.unbind(), lines: lines.unbind(), style: CliStyle { use_color }, state: Mutex::new(StatusState::default()), 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 b5e20e9e3f..5597092321 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/check/check_command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/check/check_command.rs @@ -43,6 +43,7 @@ use crate::bindings::_helpers::graph::python_selection::python_node_names; 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::{ErrorId, NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; @@ -93,8 +94,8 @@ fn refuse_graph_selectors(py: Python<'_>, selectors: &[Bound<'_, PyAny>]) -> PyR let parsed: Bound<'_, PyAny> = parse_project_selector(part?.downcast::()?)?; if parsed.is_instance(&path_selector)? - || parsed.getattr("upstream")?.is_truthy()? - || parsed.getattr("downstream")?.is_truthy()? + || native_field(&parsed, "upstream")?.is_truthy()? + || native_field(&parsed, "downstream")?.is_truthy()? { return Err(user_error( py, @@ -130,8 +131,8 @@ fn selected_check_names<'py>( graph: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = graph.py(); - let select: Bound<'py, PyAny> = request.getattr("select")?; - let exclude: Bound<'py, PyAny> = request.getattr("exclude")?; + let select: Bound<'py, PyAny> = native_field(request, "select")?; + let exclude: Bound<'py, PyAny> = native_field(request, "exclude")?; let selectors: Vec> = select .try_iter()? .chain(exclude.try_iter()?) @@ -157,10 +158,10 @@ fn selected_check_names<'py>( PyFrozenSet::empty(py)?.into_any() }; let mut names: Vec> = Vec::new(); - for node in graph.getattr("nodes")?.try_iter()? { + for node in native_field(graph, "nodes")?.try_iter()? { let node: Bound<'py, PyAny> = node?; - let name: Bound<'py, PyAny> = node.getattr("name")?; - if node.getattr("kind")?.eq(&check_kind)? && !excluded.contains(&name)? { + let name: Bound<'py, PyAny> = native_field(&node, "name")?; + if native_field(&node, "kind")?.eq(&check_kind)? && !excluded.contains(&name)? { names.push(name); } } @@ -174,16 +175,16 @@ fn compile_pipeline<'py>( (graph, check_names): (&Bound<'py, PyAny>, &Bound<'py, PyFrozenSet>), ) -> PyResult> { let py: Python<'py> = request.py(); - let discovered: Bound<'py, PyAny> = invocation.getattr("discovered_inputs")?; + let discovered: Bound<'py, PyAny> = native_field(invocation, "discovered_inputs")?; let required_refs: Bound<'py, PyAny> = call_with( - &graph.getattr("selected_sql_refs")?, + &native_field(graph, "selected_sql_refs")?, &[("selected_names", check_names.clone().into_any())], )?; let closure: Bound<'py, PyAny> = object( py, transitive_closure_many( check_names.as_any(), - &graph.getattr("upstream_deps")?, + &native_field(graph, "upstream_deps")?, false, None, )?, @@ -205,15 +206,15 @@ fn compile_pipeline<'py>( for reference in required_refs.try_iter()? { keys.push(sql_ref_key(py, &reference?)?); } - for source_file in discovered.getattr("source_files")?.try_iter()? { - for source in source_file?.getattr("source_entries")?.try_iter()? { + 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?; - if loaders.contains(source.getattr("loader")?)? { + if loaders.contains(native_field(&source, "loader")?)? { keys.push(call_with( &key_class, &[ ("resource_type", source_type.clone()), - ("name", source.getattr("name")?), + ("name", native_field(&source, "name")?), ], )?); } @@ -221,19 +222,22 @@ fn compile_pipeline<'py>( } let keys: Bound<'py, PyAny> = PyFrozenSet::new(py, keys)?.into_any(); let resolver: Bound<'py, PyAny> = resolve_external_sql_reference_resolver( - invocation.getattr("effective_project_dir")?, + native_field(invocation, "effective_project_dir")?, discovered.clone(), )?; let options: Bound<'py, PyAny> = call_with( ®istry::class(py, RecordId::CompilePipelineOptions)?, &[ - ("selected_target", request.getattr("selected_target")?), - ("no_sql_validation", request.getattr("no_sql_validation")?), + ("selected_target", native_field(request, "selected_target")?), + ( + "no_sql_validation", + native_field(request, "no_sql_validation")?, + ), ( "connection_config", - invocation.getattr("connection_config")?, + native_field(invocation, "connection_config")?, ), - ("cli_vars", request.getattr("cli_vars")?), + ("cli_vars", native_field(request, "cli_vars")?), ("external_sql_reference_resolver", resolver), ], )?; @@ -241,7 +245,7 @@ fn compile_pipeline<'py>( py, &[ ("discovered_inputs", discovered), - ("adapter", invocation.getattr("adapter")?), + ("adapter", native_field(invocation, "adapter")?), ("options", options), ("selected_keys", keys.clone()), ("relation_keys", keys), @@ -254,9 +258,9 @@ fn compile_pipeline<'py>( ], )?))?; let plan_output: Bound<'py, PyAny> = build_relation_command_plan( - &context.getattr("project")?, - &context.getattr("scope")?, - &context.getattr("relations")?, + &native_field(&context, "project")?, + &native_field(&context, "scope")?, + &native_field(&context, "relations")?, )?; build_static_pipeline_result( py, @@ -288,8 +292,8 @@ fn python_selection<'py>( let _py: Python<'py> = request.py(); let python_graph: Bound<'py, PyAny> = python_node_graph(discovered)?; reject_unit_test_selectors( - &request.getattr("select")?, - &request.getattr("exclude")?, + &native_field(request, "select")?, + &native_field(request, "exclude")?, discovered, )?; let check_names: Bound<'py, PyFrozenSet> = selected_check_names(request, &python_graph)?; @@ -301,16 +305,16 @@ fn prepare<'py>( invocation: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = request.py(); - let discovered: Bound<'py, PyAny> = invocation.getattr("discovered_inputs")?; - let adapter: Bound<'py, PyAny> = invocation.getattr("adapter")?; + let discovered: Bound<'py, PyAny> = native_field(invocation, "discovered_inputs")?; + let adapter: Bound<'py, PyAny> = native_field(invocation, "adapter")?; let (planning_graph, planning_names) = python_selection(request, &discovered)?; let pipeline: Bound<'py, PyAny> = compile_pipeline(request, invocation, (&planning_graph, &planning_names))?; let (python_graph, check_names) = python_selection(request, &discovered)?; let mut check_functions: Vec> = Vec::new(); - for check in discovered.getattr("check_functions")?.try_iter()? { + for check in native_field(&discovered, "check_functions")?.try_iter()? { let check: Bound<'py, PyAny> = check?; - if check_names.contains(check.getattr("name")?)? { + if check_names.contains(native_field(&check, "name")?)? { check_functions.push(check); } } @@ -324,11 +328,11 @@ fn prepare<'py>( let lifecycle_plan: Bound<'py, PyAny> = build_python_sql_run_lifecycle(&selection, &python_graph)?; let relation_refs: Bound<'py, PyAny> = call_with( - &python_graph.getattr("selected_sql_refs")?, + &native_field(&python_graph, "selected_sql_refs")?, &[("selected_names", check_names.clone().into_any())], )?; - let project: Bound<'py, PyAny> = pipeline.getattr("project")?; - let plan_output: Bound<'py, PyAny> = pipeline.getattr("plan_output")?; + let project: Bound<'py, PyAny> = native_field(&pipeline, "project")?; + let plan_output: Bound<'py, PyAny> = native_field(&pipeline, "plan_output")?; let relation_targets: Bound<'py, PyAny> = object( py, build_python_relation_targets( @@ -383,17 +387,17 @@ fn node_runtime<'py>( (connection, providers, with_project_relations): (&Bound<'py, PyAny>, &Bound<'py, PyAny>, bool), ) -> PyResult> { let py: Python<'py> = invocation.py(); - let project: Bound<'py, PyAny> = prepared.pipeline.getattr("project")?; + let project: Bound<'py, PyAny> = native_field(&prepared.pipeline, "project")?; let mut fields: Vec<(&str, Bound<'py, PyAny>)> = vec![ - ("adapter", invocation.getattr("adapter")?), + ("adapter", native_field(invocation, "adapter")?), ( "connection_config", - invocation.getattr("connection_config")?, + native_field(invocation, "connection_config")?, ), ("connection", connection.clone()), - ("run_id", project.getattr("run_id")?), - ("target", project.getattr("effective_target_name")?), - ("vars", project.getattr("effective_vars")?), + ("run_id", native_field(&project, "run_id")?), + ("target", native_field(&project, "effective_target_name")?), + ("vars", native_field(&project, "effective_vars")?), ("is_reload", PyBool::new(py, false).to_owned().into_any()), ("default_database", prepared.defaults.0.clone()), ("default_schema", prepared.defaults.1.clone()), @@ -416,7 +420,7 @@ fn record_run_state<'py>( source_map: &Bound<'py, PyAny>, ) -> PyResult<()> { record_python_run_state_results( - &invocation.getattr("discovered_inputs")?, + &native_field(invocation, "discovered_inputs")?, &run_state, (&python_results, load_results.as_ref()), Some(source_map), @@ -434,11 +438,11 @@ fn run_checks<'py>( ), ) -> PyResult> { let py: Python<'py> = invocation.py(); - let plan_output: Bound<'py, PyAny> = prepared.pipeline.getattr("plan_output")?; - let source_map: Bound<'py, PyAny> = plan_output.getattr("source_map")?; + let plan_output: Bound<'py, PyAny> = native_field(&prepared.pipeline, "plan_output")?; + let source_map: Bound<'py, PyAny> = native_field(&plan_output, "source_map")?; let callbacks: Bound<'py, PyAny> = call_with( &host::entry(py, ExecutorHost::IngressCallbacks)?, - &[("use_color", invocation.getattr("use_color")?)], + &[("use_color", native_field(invocation, "use_color")?)], )?; let ingress: Bound<'py, PyAny> = call_with( &host::entry(py, ExecutorHost::ExecuteIngressPythonLoaderNodes)?, @@ -464,9 +468,9 @@ fn run_checks<'py>( ("callbacks", callbacks), ], )?; - let run_state: Bound<'py, PyAny> = ingress.getattr("run_state")?; - let load_results: Bound<'py, PyAny> = ingress.getattr("load_results")?; - let ingress_python: Bound<'py, PyAny> = ingress.getattr("python_results")?; + let run_state: Bound<'py, PyAny> = native_field(&ingress, "run_state")?; + let load_results: Bound<'py, PyAny> = native_field(&ingress, "load_results")?; + let ingress_python: Bound<'py, PyAny> = native_field(&ingress, "python_results")?; record_run_state( invocation, ( @@ -477,8 +481,8 @@ fn run_checks<'py>( &source_map, )?; let read_side: Bound<'py, PyAny> = run_read_side_dependencies(&ReadSideInputs { - adapter: invocation.getattr("adapter")?, - connection_config: invocation.getattr("connection_config")?, + adapter: native_field(invocation, "adapter")?, + connection_config: native_field(invocation, "connection_config")?, connection: connection.clone(), pipeline_result: prepared.pipeline.clone(), python_graph: prepared.python_graph.clone(), @@ -564,9 +568,9 @@ fn execute<'py>( providers: &Bound<'py, PyAny>, ) -> PyResult>> { let py: Python<'py> = invocation.py(); - let adapter: Bound<'py, PyAny> = invocation.getattr("adapter")?; + let adapter: Bound<'py, PyAny> = native_field(invocation, "adapter")?; let connection: Bound<'py, PyAny> = - adapter.call_method1("connect", (invocation.getattr("connection_config")?,))?; + adapter.call_method1("connect", (native_field(invocation, "connection_config")?,))?; let event_writer: Bound<'py, PyAny> = registry::native(py, NativeAttr::ExecutionEventWriter)?.call0()?; let executed: PyResult>> = (|| { @@ -576,7 +580,7 @@ fn execute<'py>( let mut total: usize = 0; for check in prepared.check_functions.try_iter()? { total += 1; - ordinals.set_item(check?.getattr("name")?, total)?; + ordinals.set_item(native_field(&check?, "name")?, total)?; } let options: Bound<'py, PyDict> = PyDict::new(py); options.set_item("ordinals", ordinals)?; @@ -607,9 +611,9 @@ fn complete<'py>( (prepared, results): (&Prepared<'py>, &[Bound<'py, PyAny>]), ) -> PyResult { let py: Python<'py> = request.py(); - let stream: Bound<'py, PyAny> = invocation.getattr("progress_stream")?; + let stream: Bound<'py, PyAny> = native_field(invocation, "progress_stream")?; let style: CliStyle = CliStyle { - use_color: invocation.getattr("use_color")?.extract()?, + use_color: native_field(invocation, "use_color")?.extract()?, }; write_results( &stream, @@ -635,8 +639,8 @@ fn complete<'py>( write_execution_json_output( py, PyString::new(py, &results_json(results)?).into_any(), - request.getattr("json_output")?.is_truthy()?, - request.getattr("json_output_path")?, + native_field(request, "json_output")?.is_truthy()?, + native_field(request, "json_output_path")?, )?, )?; Ok(check_exit_code(&texts)) @@ -651,14 +655,14 @@ fn run_check_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult = invocation.getattr("progress_stream")?; + let stream: Bound<'_, PyAny> = native_field(&invocation, "progress_stream")?; stream.call_method1("write", ("\n",))?; let header: Bound<'_, PyDict> = PyDict::new(py); header.set_item("stream", &stream)?; header.set_item("command", EXECUTION_COMMAND)?; header.set_item("target", py.None())?; header.set_item("concurrency", PyInt::new(py, 1))?; - header.set_item("use_color", invocation.getattr("use_color")?)?; + header.set_item("use_color", native_field(&invocation, "use_color")?)?; write_execution_header(py, Some(header))?; let prepared: Prepared<'_> = prepare(request, &invocation)?; let session: Bound<'_, PyAny> = call_with( @@ -671,8 +675,11 @@ fn run_check_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult = (|| { - let results: Vec> = - execute(&invocation, &prepared, &session.getattr("providers")?)?; + let results: Vec> = execute( + &invocation, + &prepared, + &native_field(&session, "providers")?, + )?; complete(request, &invocation, (&prepared, &results)) })(); let closed: PyResult> = session.call_method0("close"); 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 0937a8a3f9..f0bac785b1 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/check/check_core.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/check/check_core.rs @@ -19,6 +19,7 @@ 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::records::fields::field as native_field; const MISSING_RELATION_CODE: &str = "C682"; const MODEL_KIND: &str = "model"; @@ -56,13 +57,13 @@ fn load_result_as_python_result<'py>( let py: Python<'py> = name.py(); let execution: Bound<'py, PyAny> = host::entry(py, ExecutorHost::ExecutionStatus)?; let node_status: Bound<'py, PyAny> = host::entry(py, UserCodeHost::PythonNodeStatus)?; - let status: Bound<'py, PyAny> = result.getattr("status")?; - let mapped: Bound<'py, PyAny> = if status.eq(execution.getattr("SUCCESS")?)? { - node_status.getattr("SUCCESS")? - } else if status.eq(execution.getattr("SKIPPED")?)? { - node_status.getattr("SKIPPED")? + let status: Bound<'py, PyAny> = native_field(result, "status")?; + let mapped: Bound<'py, PyAny> = if status.eq(native_field(&execution, "SUCCESS")?)? { + native_field(&node_status, "SUCCESS")? + } else if status.eq(native_field(&execution, "SKIPPED")?)? { + native_field(&node_status, "SKIPPED")? } else { - node_status.getattr("FAILED")? + native_field(&node_status, "FAILED")? }; call_with( &host::entry(py, ExecutorHost::PythonNodeExecutionResult)?, @@ -74,7 +75,7 @@ fn load_result_as_python_result<'py>( ), ("status", mapped), ("payload", result.clone()), - ("error_message", result.getattr("error_message")?), + ("error_message", native_field(result, "error_message")?), ], ) } @@ -88,7 +89,7 @@ pub(crate) fn load_results_by_loader_name<'py>( let loader_by_source: Bound<'py, PyDict> = PyDict::new(py); for item in source_map.call_method0("items")?.try_iter()? { let (name, source): (Bound<'py, PyAny>, Bound<'py, PyAny>) = item?.extract()?; - let loader: Bound<'py, PyAny> = source.getattr("loader")?; + let loader: Bound<'py, PyAny> = native_field(&source, "loader")?; if !loader.is_none() { loader_by_source.set_item(name, loader)?; } @@ -96,7 +97,7 @@ pub(crate) fn load_results_by_loader_name<'py>( let by_loader: Bound<'py, PyDict> = PyDict::new(py); for result in load_results.try_iter()? { let result: Bound<'py, PyAny> = result?; - if let Some(loader) = loader_by_source.get_item(result.getattr("source_name")?)? + if let Some(loader) = loader_by_source.get_item(native_field(&result, "source_name")?)? && !loader.is_none() { by_loader.set_item(loader, result)?; @@ -115,7 +116,7 @@ pub(crate) fn record_python_run_state_results<'py>( let py: Python<'py> = discovered_inputs.py(); let record = |function: Bound<'py, PyAny>, result: Bound<'py, PyAny>| -> PyResult<()> { call_with( - &run_state.getattr("record_result")?, + &native_field(run_state, "record_result")?, &[("node_function", function), ("result", result)], )?; Ok(()) @@ -123,13 +124,16 @@ pub(crate) fn record_python_run_state_results<'py>( let result_by_name: Bound<'py, PyDict> = PyDict::new(py); for result in python_results.try_iter()? { let result: Bound<'py, PyAny> = result?; - result_by_name.set_item(result.getattr("node_name")?, result)?; + result_by_name.set_item(native_field(&result, "node_name")?, result)?; } 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()? { let node: Bound<'py, PyAny> = node?; - node_by_name.set_item(node.getattr("name")?, node.getattr("function")?)?; + node_by_name.set_item( + native_field(&node, "name")?, + native_field(&node, "function")?, + )?; } } for (name, function) in node_by_name.iter() { @@ -146,14 +150,14 @@ pub(crate) fn record_python_run_state_results<'py>( } _ => PyDict::new(py), }; - for loader in discovered_inputs.getattr("loader_functions")?.try_iter()? { + for loader in native_field(discovered_inputs, "loader_functions")?.try_iter()? { let loader: Bound<'py, PyAny> = loader?; - let name: Bound<'py, PyAny> = loader.getattr("name")?; + let name: Bound<'py, PyAny> = native_field(&loader, "name")?; let Some(result) = by_loader.get_item(&name)? else { continue; }; record( - loader.getattr("function")?, + native_field(&loader, "function")?, load_result_as_python_result(&name, &result)?, )?; } @@ -169,7 +173,7 @@ pub(crate) fn build_check_relation_targets<'py>( ) -> PyResult> { let py: Python<'py> = adapter.py(); let required: Bound<'py, PyAny> = call_with( - &python_graph.getattr("selected_sql_refs")?, + &native_field(python_graph, "selected_sql_refs")?, &[("selected_names", selected_python_names)], )?; object( @@ -180,8 +184,8 @@ pub(crate) fn build_check_relation_targets<'py>( py, &[ ("adapter", adapter.clone()), - ("project", pipeline_result.getattr("project")?), - ("plan_output", pipeline_result.getattr("plan_output")?), + ("project", native_field(pipeline_result, "project")?), + ("plan_output", native_field(pipeline_result, "plan_output")?), ("required_refs", required), ], )?), @@ -203,12 +207,12 @@ pub(crate) fn relevant_check_functions<'py>( )?; let check_kind: Bound<'py, PyAny> = host::entry(py, UserCodeHost::PythonNodeKind)?.getattr("CHECK")?; - let upstream_deps: Bound<'py, PyAny> = python_graph.getattr("upstream_deps")?; + let upstream_deps: Bound<'py, PyAny> = native_field(python_graph, "upstream_deps")?; let mut check_names: Vec> = Vec::new(); - for node in python_graph.getattr("nodes")?.try_iter()? { + for node in native_field(python_graph, "nodes")?.try_iter()? { let node: Bound<'py, PyAny> = node?; - let name: Bound<'py, PyAny> = node.getattr("name")?; - if !node.getattr("kind")?.eq(&check_kind)? || excluded.contains(&name)? { + let name: Bound<'py, PyAny> = native_field(&node, "name")?; + if !native_field(&node, "kind")?.eq(&check_kind)? || excluded.contains(&name)? { continue; } let upstream: Vec> = upstream_deps @@ -216,8 +220,8 @@ pub(crate) fn relevant_check_functions<'py>( .try_iter()? .collect::>()?; let mut sql_names: Vec> = Vec::new(); - for reference in node.getattr("sql_deps")?.try_iter()? { - sql_names.push(reference?.getattr("name")?); + for reference in native_field(&node, "sql_deps")?.try_iter()? { + sql_names.push(native_field(&reference?, "name")?); } if upstream.is_empty() && sql_names.is_empty() { continue; @@ -235,9 +239,9 @@ pub(crate) fn relevant_check_functions<'py>( } let check_names: Bound<'py, PyFrozenSet> = PyFrozenSet::new(py, &check_names)?; let mut checks: Vec> = Vec::new(); - for check in discovered_inputs.getattr("check_functions")?.try_iter()? { + for check in native_field(discovered_inputs, "check_functions")?.try_iter()? { let check: Bound<'py, PyAny> = check?; - if check_names.contains(check.getattr("name")?)? { + if check_names.contains(native_field(&check, "name")?)? { checks.push(check); } } @@ -259,7 +263,7 @@ pub(crate) struct ReadSideInputs<'py> { } fn reference_kind(reference: &Bound<'_, PyAny>) -> PyResult { - reference.getattr("kind")?.getattr("value")?.extract() + native_field(reference, "kind")?.getattr("value")?.extract() } /// A plan source the check can look up as a table: not an expression, with a table name. @@ -271,8 +275,8 @@ fn table_source<'py>( .getattr("python_source_entries")? .call_method1("get", (name,))?; if source.is_none() - || !source.getattr("expression")?.is_none() - || source.getattr("table")?.is_none() + || !native_field(&source, "expression")?.is_none() + || native_field(&source, "table")?.is_none() { return Ok(None); } @@ -290,7 +294,7 @@ fn located<'py>( }; let location: Bound<'py, PyAny> = plan_output .getattr(attribute)? - .call_method1("get", (reference.getattr("name")?,))?; + .call_method1("get", (native_field(reference, "name")?,))?; Ok((!location.is_none()).then_some(location)) } @@ -302,22 +306,22 @@ fn lookup_location<'py>( let py: Python<'py> = plan_output.py(); let parts: (Bound<'py, PyAny>, Bound<'py, PyAny>, Bound<'py, PyAny>) = if reference_kind(reference)? == SOURCE_KIND { - let Some(source) = table_source(plan_output, &reference.getattr("name")?)? else { + let Some(source) = table_source(plan_output, &native_field(reference, "name")?)? else { return Ok(None); }; ( - source.getattr("database")?, - source.getattr("schema")?, - source.getattr("table")?, + native_field(&source, "database")?, + native_field(&source, "schema")?, + native_field(&source, "table")?, ) } else { let Some(location) = located(plan_output, reference)? else { return Ok(None); }; ( - location.getattr("database")?, - location.getattr("schema")?, - location.getattr("name")?, + native_field(&location, "database")?, + native_field(&location, "schema")?, + native_field(&location, "name")?, ) }; Ok(Some( @@ -330,7 +334,7 @@ fn relation_exists( (database, schema, name): (Bound<'_, PyAny>, Bound<'_, PyAny>, Bound<'_, PyAny>), ) -> PyResult { call_with( - &lookup.getattr("exists")?, + &native_field(lookup, "exists")?, &[("database", database), ("schema", schema), ("name", name)], )? .is_truthy() @@ -343,9 +347,9 @@ fn validate_reference_exists( reference: &Bound<'_, PyAny>, ) -> Result<(), PyErr> { let py: Python<'_> = reference.py(); - let plan_output: Bound<'_, PyAny> = inputs.pipeline_result.getattr("plan_output")?; + let plan_output: Bound<'_, PyAny> = native_field(&inputs.pipeline_result, "plan_output")?; let kind: String = reference_kind(reference)?; - let name: Bound<'_, PyAny> = reference.getattr("name")?; + let name: Bound<'_, PyAny> = native_field(reference, "name")?; let (exists, relation): (bool, Bound<'_, PyAny>) = match kind.as_str() { MODEL_KIND | SEED_KIND => { let Some(location) = located(&plan_output, reference)? else { @@ -365,9 +369,9 @@ fn validate_reference_exists( let exists: bool = relation_exists( lookup, ( - location.getattr("database")?, - location.getattr("schema")?, - location.getattr("name")?, + native_field(&location, "database")?, + native_field(&location, "schema")?, + native_field(&location, "name")?, ), )?; let relation: Bound<'_, PyAny> = call_with( @@ -383,9 +387,9 @@ fn validate_reference_exists( let exists: bool = relation_exists( lookup, ( - source.getattr("database")?, - source.getattr("schema")?, - source.getattr("table")?, + native_field(&source, "database")?, + native_field(&source, "schema")?, + native_field(&source, "table")?, ), )?; (exists, inputs.relation_targets.get_item(reference)?) @@ -411,7 +415,7 @@ fn sorted_references<'py>(references: &Bound<'py, PyAny>) -> PyResult = reference?; let key: (String, String) = ( reference_kind(&reference)?, - reference.getattr("name")?.extract()?, + native_field(&reference, "name")?.extract()?, ); keyed.push((key, reference)); } @@ -424,10 +428,10 @@ fn read_side_references<'py>( read_side_names: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = python_graph.py(); - let nodes: Bound<'py, PyAny> = python_graph.getattr("nodes_by_name")?; + let nodes: Bound<'py, PyAny> = native_field(python_graph, "nodes_by_name")?; let mut references: Vec> = Vec::new(); for name in read_side_names.try_iter()? { - for reference in nodes.get_item(name?)?.getattr("sql_deps")?.try_iter()? { + for reference in native_field(&nodes.get_item(name?)?, "sql_deps")?.try_iter()? { references.push(reference?); } } @@ -436,16 +440,16 @@ fn read_side_references<'py>( fn node_runtime<'py>(inputs: &ReadSideInputs<'py>) -> PyResult> { let py: Python<'py> = inputs.adapter.py(); - let project: Bound<'py, PyAny> = inputs.pipeline_result.getattr("project")?; + let project: Bound<'py, PyAny> = native_field(&inputs.pipeline_result, "project")?; call_with( &host::entry(py, ExecutorHost::PythonNodeRuntime)?, &[ ("adapter", inputs.adapter.clone()), ("connection_config", inputs.connection_config.clone()), ("connection", inputs.connection.clone()), - ("run_id", project.getattr("run_id")?), - ("target", project.getattr("effective_target_name")?), - ("vars", project.getattr("effective_vars")?), + ("run_id", native_field(&project, "run_id")?), + ("target", native_field(&project, "effective_target_name")?), + ("vars", native_field(&project, "effective_vars")?), ("is_reload", false.into_pyobject(py)?.to_owned().into_any()), ( "default_database", @@ -476,7 +480,7 @@ pub(crate) fn run_read_side_dependencies<'py>( inputs.validation_refs.clone() }; let references: Vec> = sorted_references(&to_validate)?; - let plan_output: Bound<'py, PyAny> = inputs.pipeline_result.getattr("plan_output")?; + let plan_output: Bound<'py, PyAny> = native_field(&inputs.pipeline_result, "plan_output")?; let mut locations: Vec> = Vec::new(); for reference in &references { if let Some(location) = lookup_location(&plan_output, reference)? { @@ -508,7 +512,7 @@ pub(crate) fn run_read_side_dependencies<'py>( )?; tracker.call_method0("dispatch_ready_python_nodes")?; tracker.call_method0("finalize_unrun_python_nodes")?; - tracker.getattr("results") + native_field(&tracker, "results") } /// Run read-side Python dependencies for checks against existing SQL relations. 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 2dbdbb94d0..c1a1622a0c 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/check/check_results.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/check/check_results.rs @@ -16,6 +16,7 @@ use sqlbuild_core::json::models::JsonValue; use crate::bindings::_helpers::cli::progress::current_native_progress_projector; use crate::bindings::_helpers::compile_driver::python_calls::KeywordArguments; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::result_output::integration_values::json_value; use crate::bindings::_helpers::result_output::terminal_index::current_terminal_event_index; @@ -44,15 +45,17 @@ fn metadata_json(metadata: &Bound<'_, PyAny>) -> PyResult { /// One executor `PythonCheckExecutionResult` as the JSON output renders it, metadata included. fn check_result_json_text(result: &Bound<'_, PyAny>) -> PyResult { Ok(CheckResultText { - metadata: metadata_json(&result.getattr("metadata")?)?, + metadata: metadata_json(&native_field(result, "metadata")?)?, ..check_result_text(result)? }) } /// One executor `PythonCheckExecutionResult` as the rows and summary render it, without metadata. pub(crate) fn check_result_text(result: &Bound<'_, PyAny>) -> PyResult { - let severity: String = result.getattr("severity")?.getattr("value")?.extract()?; - let status: CheckStatus = if result.getattr("passed")?.is_truthy()? { + let severity: String = native_field(result, "severity")? + .getattr("value")? + .extract()?; + let status: CheckStatus = if native_field(result, "passed")?.is_truthy()? { CheckStatus::Pass } else if severity == WARN_SEVERITY { CheckStatus::Warn @@ -60,13 +63,13 @@ pub(crate) fn check_result_text(result: &Bound<'_, PyAny>) -> PyResult, ) -> PyResult { let group: Option = match checks.get(name) { - Some(check) => optional_text(check.getattr("group")?)?, + Some(check) => optional_text(native_field(check, "group")?)?, None => None, }; if python_graph.is_none() { @@ -101,8 +104,8 @@ fn grouping( .getattr("nodes_by_name")? .call_method1("get", (name,))?; if !node.is_none() { - for reference in node.getattr("sql_deps")?.try_iter()? { - dependencies.push(reference?.getattr("name")?.extract()?); + for reference in native_field(&node, "sql_deps")?.try_iter()? { + dependencies.push(native_field(&reference?, "name")?.extract()?); } } Ok(CheckGrouping { @@ -122,7 +125,7 @@ pub(crate) fn write_results( let mut checks: HashMap> = HashMap::new(); for check in check_functions.try_iter()? { let check: Bound<'_, PyAny> = check?; - checks.insert(check.getattr("name")?.extract()?, check); + checks.insert(native_field(&check, "name")?.extract()?, check); } let projector: Bound<'_, PyAny> = current_native_progress_projector(py)?; let mut groups: Vec<(String, Vec)> = Vec::new(); @@ -171,7 +174,7 @@ pub(crate) fn results_json(results: &[Bound<'_, PyAny>]) -> PyResult { let index: Bound<'_, PyAny> = current_terminal_event_index(py)?; let mut texts: Vec = Vec::new(); for result in results { - let name: Bound<'_, PyAny> = result.getattr("node_name")?; + let name: Bound<'_, PyAny> = native_field(result, "node_name")?; if !index.is_none() { let options: Bound<'_, PyDict> = PyDict::new(py); options.set_item("resource_name", &name)?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cli/argv.rs b/crates/sqlbuild-python/src/bindings/_helpers/cli/argv.rs index 24d8e42913..2d50199dca 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cli/argv.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cli/argv.rs @@ -14,6 +14,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; @@ -53,19 +54,20 @@ impl<'py> PythonHost<'py> { fn vars(&mut self, text: &str) -> PyResult> { let json: Bound<'_, PyModule> = self.py.import("json")?; - let parsed: Bound<'_, PyAny> = match json.getattr("loads")?.call1((self.text(text)?,)) { - Ok(parsed) => parsed, - Err(error) => { - let decode_error: Bound<'_, PyAny> = json.getattr("JSONDecodeError")?; - if !error.value(self.py).is_instance(&decode_error)? { - return Err(error); + let parsed: Bound<'_, PyAny> = + match native_field(&json, "loads")?.call1((self.text(text)?,)) { + Ok(parsed) => parsed, + Err(error) => { + let decode_error: Bound<'_, PyAny> = native_field(&json, "JSONDecodeError")?; + if !error.value(self.py).is_instance(&decode_error)? { + return Err(error); + } + let message: String = native_field(error.value(self.py), "msg")?.extract()?; + return Ok(Err(TypeFailure::ArgumentType(format!( + "--vars must be valid JSON: {message}" + )))); } - let message: String = error.value(self.py).getattr("msg")?.extract()?; - return Ok(Err(TypeFailure::ArgumentType(format!( - "--vars must be valid JSON: {message}" - )))); - } - }; + }; let Ok(parsed) = parsed.downcast_into::() else { return Ok(Err(TypeFailure::ArgumentType( "--vars must be a JSON object".to_owned(), @@ -110,7 +112,7 @@ impl ArgvHost for PythonHost<'_> { fn program_version(&mut self) -> String { self.py .import("importlib.metadata") - .and_then(|metadata| metadata.getattr("version")?.call1((DISTRIBUTION,))) + .and_then(|metadata| native_field(&metadata, "version")?.call1((DISTRIBUTION,))) .and_then(|version| version.extract()) .unwrap_or_else(|_| UNKNOWN_VERSION.to_owned()) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cli/dispatch.rs b/crates/sqlbuild-python/src/bindings/_helpers/cli/dispatch.rs index f2b472f2ce..33e2a625d5 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cli/dispatch.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cli/dispatch.rs @@ -19,6 +19,7 @@ use sqlbuild_core::format_command::main::format_path_selectors::format_path_sele 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::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; @@ -218,7 +219,7 @@ fn selector_inputs<'py>( .collect::>()?; let summary_class: Bound<'py, PyAny> = registry::class(py, RecordId::SelectorFileSummary)?; let mut files: Vec> = Vec::new(); - for raw_path in args.getattr("select_file")?.try_iter()? { + for raw_path in native_field(args, "select_file")?.try_iter()? { let path: Bound<'py, PyAny> = path_class(py)?.call1((raw_path?,))?; let keywords: Bound<'py, PyDict> = PyDict::new(py); keywords.set_item("encoding", UTF8)?; @@ -286,7 +287,7 @@ fn routed_command<'py>( handlers: Option<&Bound<'py, PyAny>>, ) -> PyResult> { let py: Python<'py> = args.py(); - let raw_project_dir: Bound<'py, PyAny> = args.getattr("project_dir")?; + let raw_project_dir: Bound<'py, PyAny> = native_field(args, "project_dir")?; let project_dir: Bound<'py, PyAny> = if raw_project_dir.is_none() { raw_project_dir } else { @@ -295,7 +296,7 @@ fn routed_command<'py>( .call_method0("resolve")? }; let (select, selector_files) = selector_inputs(args)?; - if args.getattr("select_file")?.is_truthy()? { + if native_field(args, "select_file")?.is_truthy()? { withhold_launch_record(py)?; } let invocation = Invocation { @@ -312,7 +313,7 @@ fn routed_command<'py>( Some(handlers) => { let slot: Bound<'py, PyAny> = handlers.getattr(route.slot)?; if slot.is_none() { - let command: String = args.getattr("command")?.str()?.to_string(); + let command: String = native_field(args, "command")?.str()?.to_string(); let code: &str = route.unavailable_code.unwrap_or("C000"); return Err(user_error(py, unavailable_handler(&command, code))); } @@ -334,9 +335,9 @@ pub(crate) fn dispatch_command<'py>( handlers: Option>, ) -> PyResult> { let py: Python<'py> = args.py(); - let warehouse: Bound<'py, PyAny> = args.getattr("warehouse")?; + let warehouse: Bound<'py, PyAny> = native_field(args, "warehouse")?; if !warehouse.is_none() { - let raw_project_dir: Bound<'py, PyAny> = args.getattr("project_dir")?; + let raw_project_dir: Bound<'py, PyAny> = native_field(args, "project_dir")?; let project_dir: Bound<'py, PyAny> = if raw_project_dir.is_none() { path_class(py)?.call_method0("cwd")? } else { @@ -355,9 +356,9 @@ pub(crate) fn dispatch_command<'py>( let scope: Bound<'py, PyAny> = Bound::new( py, command_warehouse_scope( - &args.getattr("command")?, + &native_field(args, "command")?, &warehouse, - Some(&args.getattr("vars")?), + Some(&native_field(args, "vars")?), )?, )? .into_any(); diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cli/entry.rs b/crates/sqlbuild-python/src/bindings/_helpers/cli/entry.rs index ba0bec4ff1..e6a518878e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cli/entry.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cli/entry.rs @@ -22,6 +22,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; use sqlbuild_cli::launcher::constants::LAUNCH_RECORD_OPTION; @@ -200,7 +201,7 @@ fn write_diff_machine_error<'py>( } let py: Python<'py> = invocation.py(); let json_stdout: bool = invocation.flag("json")?; - let json_path: Bound<'py, PyAny> = invocation.args.getattr("json_output")?; + let json_path: Bound<'py, PyAny> = native_field(&invocation.args, "json_output")?; if !json_stdout && json_path.is_none() { return Ok(None); } @@ -215,15 +216,15 @@ fn write_diff_machine_error<'py>( py, &[ ("status", status), - ("code", error.getattr("code")?), - ("message", error.getattr("message")?), - ("help_text", error.getattr("help")?), + ("code", native_field(error, "code")?), + ("message", native_field(error, "message")?), + ("help_text", native_field(error, "help")?), ("size_guard", size_guard), ], )?; if !json_path.is_none() { let published: PyResult<()> = (|| { - let parent: Bound<'py, PyAny> = json_path.getattr("parent")?; + let parent: Bound<'py, PyAny> = native_field(&json_path, "parent")?; let options: Bound<'py, PyDict> = PyDict::new(py); options.set_item("parents", true)?; options.set_item("exist_ok", true)?; @@ -254,9 +255,9 @@ fn write_diff_machine_error<'py>( let document: Bound<'py, PyAny> = render_diff_error( py, &[ - ("status", publication.getattr("status")?), - ("code", publication.getattr("code")?), - ("message", publication.getattr("message")?), + ("status", native_field(&publication, "status")?), + ("code", native_field(&publication, "code")?), + ("message", native_field(&publication, "message")?), ], )?; print_object(&document, None, None)?; @@ -331,7 +332,7 @@ fn report_failure<'py>( if effective.is_instance(®istry::error(py, ErrorId::QueryDiffOutcome)?)? { let line: Bound<'py, PyAny> = PyString::new(py, DIFF_OUTCOME_PREFIX) .into_any() - .add(effective.getattr("status")?.str()?)?; + .add(native_field(&effective, "status")?.str()?)?; print_object(&line, Some("stderr"), None)?; } print_expected_error(invocation, &effective, fallback_code)?; @@ -367,7 +368,7 @@ fn report_skill_freshness(invocation: &Invocation<'_>, policy: InvocationPolicy) return Err(error); } }; - let message: Bound<'_, PyAny> = result.getattr("message")?; + let message: Bound<'_, PyAny> = native_field(&result, "message")?; if message.is_truthy()? { print_object(&message, Some("stderr"), Some(""))?; } @@ -461,7 +462,7 @@ fn handled_dispatch<'py>(invocation: &Invocation<'py>) -> PyResult = error.value(py).clone().into_any(); if error.is_instance_of::(py) { - let code: Bound<'py, PyAny> = value.getattr("code")?; + let code: Bound<'py, PyAny> = native_field(&value, "code")?; return if code.is_instance_of::() { Ok(code) } else { @@ -495,9 +496,9 @@ fn parsed_invocation<'py>( let py: Python<'py> = argv.py(); let use_color: bool = !argv.contains(NO_COLOR_OPTION)? && stdout_supports_color(py)?; let parsed: Bound<'py, PyAny> = parse_cli_invocation(py, argv.extract()?, use_color)?; - let args: Bound<'py, PyAny> = parsed.getattr("args")?; + let args: Bound<'py, PyAny> = native_field(&parsed, "args")?; if args.is_none() { - let exit_code: Bound<'py, PyAny> = parsed.getattr("exit_code")?; + let exit_code: Bound<'py, PyAny> = native_field(&parsed, "exit_code")?; return if exit_code.is_none() { exit(py, 1) } else { @@ -591,7 +592,10 @@ fn run_cli<'py>( within(&external_scope, |_| { let diagnostics: Bound<'py, PyAny> = call_with( &host::entry(py, ExecutorHost::DiagnosticsContext)?, - &[("sqlbuild_invocation_id", identity.getattr("invocation_id")?)], + &[( + "sqlbuild_invocation_id", + native_field(identity, "invocation_id")?, + )], )?; within(&diagnostics, |_| { parsed_invocation(&argv, handlers.clone(), identity) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cli/observability.rs b/crates/sqlbuild-python/src/bindings/_helpers/cli/observability.rs index 798536cf2e..bfeebfe9b0 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cli/observability.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cli/observability.rs @@ -19,6 +19,7 @@ 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; @@ -58,9 +59,9 @@ fn log_dispatch_failure(failure: &Bound<'_, PyAny>) -> PyResult<()> { failure.py(), "Lifecycle event subscriber failed", &[ - ("error_type", failure.getattr("error_type")?), - ("channel", failure.getattr("channel")?), - ("subscriber", failure.getattr("subscriber")?), + ("error_type", native_field(failure, "error_type")?), + ("channel", native_field(failure, "channel")?), + ("subscriber", native_field(failure, "subscriber")?), ], ) } @@ -72,10 +73,10 @@ fn log_exporter_failure(failure: &Bound<'_, PyAny>) -> PyResult<()> { failure.py(), "Event exporter delivery failed", &[ - ("exporter_name", failure.getattr("exporter_name")?), - ("error_type", failure.getattr("error_type")?), - ("event_kind", failure.getattr("event_kind")?), - ("event_severity", failure.getattr("event_severity")?), + ("exporter_name", native_field(failure, "exporter_name")?), + ("error_type", native_field(failure, "error_type")?), + ("event_kind", native_field(failure, "event_kind")?), + ("event_severity", native_field(failure, "event_severity")?), ], ) } @@ -99,11 +100,11 @@ fn log_exporter_summary(summary: &Bound<'_, PyAny>) -> PyResult<()> { fields.push((name, summary.getattr(name)?)); } log_debug(py, "Event exporter delivery summary", &fields)?; - for exporter in summary.getattr("per_exporter")?.try_iter()? { + for exporter in native_field(summary, "per_exporter")?.try_iter()? { let exporter: Bound<'_, PyAny> = exporter?; - let counts: Bound<'_, PyAny> = exporter.getattr("counts")?; + let counts: Bound<'_, PyAny> = native_field(&exporter, "counts")?; let mut fields: Vec<(&str, Bound<'_, PyAny>)> = - vec![("exporter_name", exporter.getattr("exporter_name")?)]; + vec![("exporter_name", native_field(&exporter, "exporter_name")?)]; for name in COUNT_FIELDS { fields.push((name, counts.getattr(name)?)); } @@ -124,7 +125,7 @@ pub(crate) struct InvocationOutcome { #[pymethods] impl InvocationOutcome { fn consume(&self, event: &Bound<'_, PyAny>) -> PyResult<()> { - let event_type: String = event.getattr("event_type")?.extract()?; + let event_type: String = native_field(event, "event_type")?.extract()?; if event_type == INVOCATION_COMPLETED || event_type == INVOCATION_FAILED { self.succeeded .store(event_type == INVOCATION_COMPLETED, Ordering::SeqCst); @@ -172,19 +173,19 @@ fn run_cleanup(py: Python<'_>, result: PyResult, phase: &str) { /// Native export accounting of a Python `EventExportSummary`. fn export_summary(summary: &Bound<'_, PyAny>) -> PyResult { let mut per_exporter: Vec = Vec::new(); - for exporter in summary.getattr("per_exporter")?.try_iter()? { + for exporter in native_field(summary, "per_exporter")?.try_iter()? { let exporter: Bound<'_, PyAny> = exporter?; - let counts: Bound<'_, PyAny> = exporter.getattr("counts")?; + let counts: Bound<'_, PyAny> = native_field(&exporter, "counts")?; per_exporter.push(ExporterLoss { - exporter_name: exporter.getattr("exporter_name")?.extract()?, - dropped: counts.getattr("dropped")?.extract()?, - failed: counts.getattr("failed")?.extract()?, + exporter_name: native_field(&exporter, "exporter_name")?.extract()?, + dropped: native_field(&counts, "dropped")?.extract()?, + failed: native_field(&counts, "failed")?.extract()?, }); } Ok(ExportSummary { - accepted: summary.getattr("accepted")?.extract()?, - dropped: summary.getattr("dropped")?.extract()?, - failed: summary.getattr("failed")?.extract()?, + accepted: native_field(summary, "accepted")?.extract()?, + dropped: native_field(summary, "dropped")?.extract()?, + failed: native_field(summary, "failed")?.extract()?, per_exporter, }) } @@ -321,18 +322,18 @@ impl Observed { terminal_index: &Bound<'_, PyAny>, ) -> PyResult<()> { let mut command_output_sinks: bool = false; - if args.getattr("command")?.ne(LINEAGE_COMMAND)? { + if native_field(args, "command")?.ne(LINEAGE_COMMAND)? { let extensions: Bound<'_, PyAny> = call_with( &host::entry(py, UserCodeHost::DiscoverRuntimeExtensions)?, &[("project_dir", project_dir.clone())], )?; - let event_exporters: Bound<'_, PyAny> = extensions.getattr("event_exporters")?; - let sinks: Bound<'_, PyAny> = extensions.getattr("command_output_sinks")?; + let event_exporters: Bound<'_, PyAny> = native_field(&extensions, "event_exporters")?; + let sinks: Bound<'_, PyAny> = native_field(&extensions, "command_output_sinks")?; command_output_sinks = sinks.is_truthy()?; if event_exporters.is_truthy()? || command_output_sinks { withhold_launch_record(py)?; let timeout: Bound<'_, PyAny> = - extensions.getattr("lifecycle_shutdown_timeout_seconds")?; + native_field(&extensions, "lifecycle_shutdown_timeout_seconds")?; let timeout: f64 = if timeout.is_none() { DEFAULT_EVENT_EXPORT_SHUTDOWN_TIMEOUT_SECONDS } else { @@ -359,18 +360,19 @@ impl Observed { )?; self.exporter_scope = Some(scope.clone().unbind()); call_with( - &scope.getattr("configure_extensions")?, + &native_field(&scope, "configure_extensions")?, &[ ("project_dir", project_dir.clone()), - ("providers", extensions.getattr("providers")?), + ("providers", native_field(&extensions, "providers")?), ("event_exporters", event_exporters), ("command_output_sinks", sinks), ], )?; self.unsubscribe_exporters = - Some(self.subscribe(py, delivery.getattr("enqueue")?)?); + Some(self.subscribe(py, native_field(&delivery, "enqueue")?)?); let outcome: Bound<'_, PyAny> = self.outcome.bind(py).clone().into_any(); - self.unsubscribe_outcome = Some(self.subscribe(py, outcome.getattr("consume")?)?); + self.unsubscribe_outcome = + Some(self.subscribe(py, native_field(&outcome, "consume")?)?); } } self.enter( @@ -441,7 +443,7 @@ impl CliObservabilityScope { registry::native(py, NativeAttr::TerminalEventIndex)?.call0()?; let subscribe = |subscriber: Bound<'_, PyAny>| -> PyResult> { Ok(call_with( - &dispatcher.getattr("subscribe_lifecycle")?, + &native_field(&dispatcher, "subscribe_lifecycle")?, &[ ("subscriber", subscriber), ( @@ -452,15 +454,17 @@ impl CliObservabilityScope { )? .unbind()) }; - let unsubscribe_terminal_index: Py = subscribe(terminal_index.getattr("consume")?)?; + let unsubscribe_terminal_index: Py = + subscribe(native_field(&terminal_index, "consume")?)?; let machine_output: bool = registry::native(py, NativeAttr::MachineOutputMode)? .call1((args,))? .is_truthy()?; - let debug: Bound<'_, PyAny> = args.getattr("debug")?; - let progress_on_stderr: bool = - machine_output || debug.is_truthy()? || args.getattr("command")?.eq(DEBUG_COMMAND)?; + let debug: Bound<'_, PyAny> = native_field(args, "debug")?; + let progress_on_stderr: bool = machine_output + || debug.is_truthy()? + || native_field(args, "command")?.eq(DEBUG_COMMAND)?; let use_color: bool = !machine_output - && !args.getattr("no_color")?.is_truthy()? + && !native_field(args, "no_color")?.is_truthy()? && stdout_supports_color(py)?; let projector: Bound<'_, PyAny> = call_with( ®istry::native(py, NativeAttr::NativeProgressProjector)?, @@ -480,7 +484,7 @@ impl CliObservabilityScope { ("debug", debug), ], )?; - let unsubscribe_progress: Py = subscribe(projector.getattr("consume")?)?; + let unsubscribe_progress: Py = subscribe(native_field(&projector, "consume")?)?; let projector_token: Bound<'_, PyAny> = projector.call_method0("install")?; let mut observed: Observed = Observed { dispatcher: dispatcher.clone().unbind(), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cli/progress.rs b/crates/sqlbuild-python/src/bindings/_helpers/cli/progress.rs index 8236109929..5e9a2722f7 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cli/progress.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cli/progress.rs @@ -17,6 +17,7 @@ use sqlbuild_core::progress::models::{ use sqlbuild_core::terminal_events::models::TerminalRequest; 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; @@ -121,13 +122,13 @@ fn optional_text(event: &Bound<'_, PyAny>, name: &str) -> PyResult) -> PyResult { Ok(ProgressEvent { - event_id: event.getattr("event_id")?.extract()?, - event_type: event.getattr("event_type")?.extract()?, + event_id: native_field(event, "event_id")?.extract()?, + event_type: native_field(event, "event_type")?.extract()?, resource_id: optional_text(event, "resource_id")?, resource_attempt_id: optional_text(event, "resource_attempt_id")?, operation_id: optional_text(event, "operation_id")?, statement_id: optional_text(event, "statement_id")?, - payload: progress_payload(&event.getattr("payload")?)?, + payload: progress_payload(&native_field(event, "payload")?)?, }) } @@ -174,7 +175,7 @@ impl NativeProgressProjector { update: impl FnOnce(ProgressProjection) -> (ProgressProjection, Vec, T), ) -> PyResult { let coordinator: &Bound<'_, PyAny> = self.coordinator.bind(py); - let lock: Bound<'_, PyAny> = coordinator.getattr("lock")?; + let lock: Bound<'_, PyAny> = native_field(coordinator, "lock")?; lock.call_method0("acquire")?; let result: PyResult = (|| { let (lines, value) = { @@ -322,11 +323,11 @@ pub(crate) fn current_native_progress_projector(py: Python<'_>) -> PyResult) -> PyResult { let mut dbt_args: Vec = Vec::new(); - for arg in args.getattr("dbt_args")?.try_iter()? { + for arg in native_field(args, "dbt_args")?.try_iter()? { dbt_args.push(arg?.str()?.extract()?); } Ok(output_mode(&OutputModeFacts { - json: args.getattr("json")?.is_truthy()?, + json: native_field(args, "json")?.is_truthy()?, command: optional_text(args, "command")?, lineage_format: optional_text(args, "lineage_format")?, query_format: optional_text(args, "query_format")?, 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 2792dbbead..33f9dc3f11 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cli/progress_text.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cli/progress_text.rs @@ -19,6 +19,7 @@ use crate::bindings::_helpers::cli::progress::write_persistent; 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; const HEADER_OPTIONS: (&[&str], &[&str]) = ( &["stream", "command", "target", "concurrency", "use_color"], @@ -63,31 +64,31 @@ fn format_expectation_name(model_name: &str) -> String { fn difference_samples(samples: &Bound<'_, PyAny>) -> PyResult> { let mut rows: Vec = Vec::new(); for sample in samples.try_iter()? { - rows.push(sample?.getattr("values")?.extract()?); + rows.push(native_field(&sample?, "values")?.extract()?); } Ok(rows) } pub(crate) fn expectation_step(step: &Bound<'_, PyAny>) -> PyResult { let mut column_differences: Vec<(String, String, String)> = Vec::new(); - for difference in step.getattr("column_differences")?.try_iter()? { + for difference in native_field(step, "column_differences")?.try_iter()? { let difference: Bound<'_, PyAny> = difference?; column_differences.push(( - difference.getattr("name")?.str()?.to_string(), - difference.getattr("actual")?.str()?.to_string(), - difference.getattr("expected")?.str()?.to_string(), + native_field(&difference, "name")?.str()?.to_string(), + native_field(&difference, "actual")?.str()?.to_string(), + native_field(&difference, "expected")?.str()?.to_string(), )); } Ok(ExpectationStep { - model_name: step.getattr("model_name")?.extract()?, - outcome: step.getattr("outcome")?.str()?.to_string(), - actual_row_count: step.getattr("actual_row_count")?.extract()?, - expected_row_count: step.getattr("expected_row_count")?.extract()?, - mismatched_row_count: step.getattr("mismatched_row_count")?.extract()?, - unexpected_row_count: step.getattr("unexpected_row_count")?.extract()?, - missing_row_count: step.getattr("missing_row_count")?.extract()?, - unexpected_samples: difference_samples(&step.getattr("unexpected_samples")?)?, - missing_samples: difference_samples(&step.getattr("missing_samples")?)?, + model_name: native_field(step, "model_name")?.extract()?, + outcome: native_field(step, "outcome")?.str()?.to_string(), + actual_row_count: native_field(step, "actual_row_count")?.extract()?, + expected_row_count: native_field(step, "expected_row_count")?.extract()?, + mismatched_row_count: native_field(step, "mismatched_row_count")?.extract()?, + unexpected_row_count: native_field(step, "unexpected_row_count")?.extract()?, + missing_row_count: native_field(step, "missing_row_count")?.extract()?, + unexpected_samples: difference_samples(&native_field(step, "unexpected_samples")?)?, + missing_samples: difference_samples(&native_field(step, "missing_samples")?)?, column_differences, }) } @@ -100,10 +101,10 @@ fn format_expectation_detail(step_result: &Bound<'_, PyAny>) -> PyResult fn inspection_record(record: &Bound<'_, PyAny>) -> PyResult { Ok(InspectionRecord { - sql: record.getattr("sql")?.extract()?, - elapsed_seconds: record.getattr("elapsed_seconds")?.extract()?, - row_count: record.getattr("row_count")?.extract()?, - error: record.getattr("error")?.extract()?, + sql: native_field(record, "sql")?.extract()?, + elapsed_seconds: native_field(record, "elapsed_seconds")?.extract()?, + row_count: native_field(record, "row_count")?.extract()?, + error: native_field(record, "error")?.extract()?, }) } @@ -177,8 +178,8 @@ pub(crate) fn with_verbose_inspection<'py, T>( } let writer: Bound<'py, VerboseInspectionWriter> = Bound::new(py, VerboseInspectionWriter::new())?; - let sink: Bound<'py, PyAny> = - host::entry(py, AdapterHost::InspectionQuerySink)?.call1((writer.getattr("write")?,))?; + let sink: Bound<'py, PyAny> = host::entry(py, AdapterHost::InspectionQuerySink)? + .call1((native_field(&writer, "write")?,))?; let value: T = within(&sink, |_| body())?; writer.get().write_total(py)?; Ok(value) 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 26b988e82b..1ddcfdbf89 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/clone/clone_command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/clone/clone_command.rs @@ -28,6 +28,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::{call_with, within} use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; 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, RecordId}; use crate::bindings::_helpers::records::registry; @@ -94,7 +95,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 discovered.getattr(config)?.getattr("targets")?.try_iter()? { + for name in native_field(&discovered.getattr(config)?, "targets")?.try_iter()? { names.insert(name?.extract()?); } } @@ -106,8 +107,11 @@ fn clone_policy(discovered: &Bound<'_, PyAny>, (target, flag): (&str, &str)) -> call_with( ®istry::native(py, NativeAttr::ResolveTargetConfig)?, &[ - ("project_config", discovered.getattr("project_config")?), - ("local_config", discovered.getattr("local_config")?), + ( + "project_config", + native_field(discovered, "project_config")?, + ), + ("local_config", native_field(discovered, "local_config")?), ("target_name", text(py, target)), ], )? @@ -118,12 +122,12 @@ fn clone_policy(discovered: &Bound<'_, PyAny>, (target, flag): (&str, &str)) -> /// Discover the project, resolve and validate the targets, and resolve the adapter. fn invocation<'py>(py: Python<'py>, request: &Bound<'py, PyAny>) -> PyResult> { - let project_dir: Bound<'py, PyAny> = match request.getattr("project_dir")? { + let project_dir: Bound<'py, PyAny> = match native_field(request, "project_dir")? { directory if !directory.is_none() => directory, _ => py.import("pathlib")?.getattr("Path")?.call_method0("cwd")?, }; let use_color: bool = - !request.getattr("no_color")?.extract::()? && stdout_supports_color(py)?; + !native_field(request, "no_color")?.extract::()? && stdout_supports_color(py)?; let style: CliStyle = CliStyle { use_color }; let stream: Bound<'py, PyAny> = py.import("sys")?.getattr("stdout")?; let progress: Bound<'py, PyAny> = progress_reporter(&stream, style)?; @@ -136,18 +140,21 @@ fn invocation<'py>(py: Python<'py>, request: &Bound<'py, PyAny>) -> PyResult = request.getattr("destination_target_name")?; + let mut destination: Bound<'py, PyAny> = native_field(request, "destination_target_name")?; if destination.is_none() { destination = call_with( ®istry::native(py, NativeAttr::ResolveTargetName)?, &[ - ("project_config", discovered.getattr("project_config")?), - ("local_config", discovered.getattr("local_config")?), + ( + "project_config", + native_field(&discovered, "project_config")?, + ), + ("local_config", native_field(&discovered, "local_config")?), ("selected_target", py.None().into_bound(py)), ], )?; @@ -156,7 +163,7 @@ fn invocation<'py>(py: Python<'py>, request: &Bound<'py, PyAny>) -> PyResult(py: Python<'py>, request: &Bound<'py, PyAny>) -> PyResult = call_with( ®istry::native(py, NativeAttr::ResolveEffectiveAdapterName)?, &[ - ("project_config", discovered.getattr("project_config")?), - ("local_config", discovered.getattr("local_config")?), + ( + "project_config", + native_field(&discovered, "project_config")?, + ), + ("local_config", native_field(&discovered, "local_config")?), ], )?; let adapter: Bound<'py, PyAny> = resolve_cli_adapter(&adapter_name, &project_dir)?; @@ -196,7 +206,7 @@ fn connect<'py>( ("discovered_inputs", invocation.discovered.clone()), ("project_dir", invocation.project_dir.clone()), ("target_name", text(py, &invocation.destination)), - ("cli_vars", request.getattr("cli_vars")?), + ("cli_vars", native_field(request, "cli_vars")?), ], )?; let progress: Bound<'py, PyAny> = progress_reporter(&invocation.stream, invocation.style)?; @@ -251,10 +261,13 @@ fn prepare<'py>( let options: Bound<'py, PyAny> = call_with( ®istry::class(py, RecordId::ClonePipelineOptions)?, &[ - ("no_sql_validation", request.getattr("no_sql_validation")?), - ("select", request.getattr("select")?), - ("exclude", request.getattr("exclude")?), - ("cli_vars", request.getattr("cli_vars")?), + ( + "no_sql_validation", + native_field(request, "no_sql_validation")?, + ), + ("select", native_field(request, "select")?), + ("exclude", native_field(request, "exclude")?), + ("cli_vars", native_field(request, "cli_vars")?), ], )?; let pipeline: Bound<'py, PyAny> = prepared_clone_pipeline( @@ -266,7 +279,7 @@ fn prepare<'py>( resolver, }, ( - &request.getattr("origin_target_name")?, + &native_field(request, "origin_target_name")?, &text(py, &invocation.destination), ), )?; @@ -292,7 +305,7 @@ fn clone<'py>( (config, connection): (&Bound<'py, PyAny>, &Bound<'py, PyAny>), ) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>, f64)> { let pipeline: Bound<'py, PyAny> = prepare(request, invocation, (config, connection))?; - let origin: String = request.getattr("origin_target_name")?.extract()?; + let origin: String = native_field(request, "origin_target_name")?.extract()?; write_line( &invocation.stream, &render_clone_header( @@ -344,7 +357,10 @@ fn run_clone_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult) -> PyResult> { - let destination: Bound<'_, PyAny> = entry.getattr("destination")?; - let qualified: Bound<'_, PyAny> = destination.getattr("qualified_name")?; + let destination: Bound<'_, PyAny> = native_field(entry, "destination")?; + let qualified: Bound<'_, PyAny> = native_field(&destination, "qualified_name")?; let relation: Bound<'_, PyAny> = if qualified.is_truthy()? { qualified } else { - destination.getattr("name")? + native_field(&destination, "name")? }; if relation.is_none() { return Ok(None); @@ -84,11 +85,14 @@ fn relation_text(entry: &Bound<'_, PyAny>) -> PyResult> { fn relation_width(pipeline: &Bound<'_, PyAny>) -> PyResult { let mut origins: HashMap> = HashMap::new(); for entry in entries(pipeline, &ORIGIN_ENTRY_GROUPS)? { - origins.insert(entry.getattr("name")?.extract()?, relation_text(&entry)?); + origins.insert( + native_field(&entry, "name")?.extract()?, + relation_text(&entry)?, + ); } let mut width: usize = 0; for entry in entries(pipeline, &DESTINATION_ENTRY_GROUPS)? { - let name: String = entry.getattr("name")?.extract()?; + let name: String = native_field(&entry, "name")?.extract()?; let destination: Option = relation_text(&entry)?; let origin: Option<&str> = origins.get(&name).and_then(Option::as_deref); let flow: String = clone_relation_flow_text(&name, (origin, destination.as_deref())); @@ -105,14 +109,17 @@ fn dependency_locations<'py>( let py: Python<'py> = pipeline.py(); let locations: Bound<'py, PyDict> = PyDict::new(py); for entry in entries(pipeline, &DESTINATION_ENTRY_GROUPS)? { - locations.set_item(entry.getattr("key")?, entry.getattr("destination")?)?; + locations.set_item( + native_field(&entry, "key")?, + native_field(&entry, "destination")?, + )?; } - let plan: Bound<'py, PyAny> = pipeline.getattr("clone_plan")?; - let read_map: Bound<'py, PyAny> = plan.getattr("source_read_map")?; + let plan: Bound<'py, PyAny> = native_field(pipeline, "clone_plan")?; + let read_map: Bound<'py, PyAny> = native_field(&plan, "source_read_map")?; let source_map: Bound<'py, PyAny> = if read_map.is_truthy()? { read_map } else { - plan.getattr("source_map")? + native_field(&plan, "source_map")? }; let key_class: Bound<'py, PyAny> = registry::class(py, RecordId::CompiledObjectKey)?; let location_class: Bound<'py, PyAny> = @@ -121,17 +128,17 @@ fn dependency_locations<'py>( imported(py, COMPILE_TYPES, "CompiledResourceType")?.getattr("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> = source.getattr("table")?; + let table: Bound<'py, PyAny> = native_field(&source, "table")?; if table.is_none() { continue; } let place = [ - ("database", source.getattr("database")?), - ("schema", source.getattr("schema")?), + ("database", native_field(&source, "database")?), + ("schema", native_field(&source, "schema")?), ("name", table.clone()), ]; let rendered: Bound<'py, PyAny> = - call_with(&adapter.getattr("render_qualified_name")?, &place)?; + call_with(&native_field(adapter, "render_qualified_name")?, &place)?; let qualified: Bound<'py, PyAny> = if rendered.is_truthy()? { rendered } else { @@ -154,11 +161,11 @@ fn target_config<'py>(invocation: &Invocation<'py>) -> PyResult( (hard_copy, on_item): (Bound<'py, PyAny>, Bound<'py, PyAny>), ) -> PyResult> { let py: Python<'py> = pipeline.py(); - let project: Bound<'py, PyAny> = pipeline.getattr("destination_project")?; - let plan: Bound<'py, PyAny> = pipeline.getattr("clone_plan")?; + let project: Bound<'py, PyAny> = native_field(pipeline, "destination_project")?; + let plan: Bound<'py, PyAny> = native_field(pipeline, "clone_plan")?; let config: Bound<'py, PyAny> = target_config(invocation)?; let selected: Vec> = pipeline .getattr("destination_model_entries")? .try_iter()? - .map(|entry| entry?.getattr("name")) + .map(|entry| native_field(&entry?, "name")) .collect::>()?; let retention: Bound<'py, PyAny> = call_with( &host::entry(py, ExecutorHost::BuildDestinationRetentionRequests)?, @@ -197,7 +204,7 @@ fn execution_input<'py>( ), ( "namespace_owned", - config.getattr("owns_time_travel_retention_namespace")?, + native_field(&config, "owns_time_travel_retention_namespace")?, ), ], )?; @@ -207,10 +214,10 @@ fn execution_input<'py>( let source_entries: Bound<'py, PyAny> = call_with( &host::entry(py, ExecutorHost::CloneSourceEntries)?, &[ - ("origin", pipeline.getattr("origin_source_entries")?), + ("origin", native_field(pipeline, "origin_source_entries")?), ( "destination", - pipeline.getattr("destination_source_entries")?, + native_field(pipeline, "destination_source_entries")?, ), ], )?; @@ -229,18 +236,18 @@ fn execution_input<'py>( fields.push((group, pipeline.getattr(group)?)); } fields.extend([ - ("execution_order", plan.getattr("execution_order")?), + ("execution_order", native_field(&plan, "execution_order")?), ("adapter", invocation.adapter.clone()), ("destination_connection", connection.clone()), ("hard_copy", hard_copy), - ("run_id", project.getattr("run_id")?), + ("run_id", native_field(&project, "run_id")?), ( "query_change_tracking", project .getattr("settings")? .getattr("query_change_tracking")?, ), - ("upstream_deps", plan.getattr("upstream_deps")?), + ("upstream_deps", native_field(&plan, "upstream_deps")?), ( "dependency_locations", dependency_locations(pipeline, &invocation.adapter)?.into_any(), @@ -274,7 +281,7 @@ fn copy_fingerprints<'py>( progress: FingerprintProgress::default(), }, )?; - let project: Bound<'py, PyAny> = pipeline.getattr("destination_project")?; + let project: Bound<'py, PyAny> = native_field(pipeline, "destination_project")?; let mut fields: Vec<(&str, Bound<'py, PyAny>)> = vec![("result", result.clone())]; for group in [ "origin_model_entries", @@ -287,7 +294,7 @@ fn copy_fingerprints<'py>( fields.extend([ ("adapter", invocation.adapter.clone()), ("destination_connection", connection.clone()), - ("run_id", project.getattr("run_id")?), + ("run_id", native_field(&project, "run_id")?), ( "query_change_tracking", project @@ -329,7 +336,7 @@ pub(crate) fn execute_clone_plan<'py>( let started: Instant = Instant::now(); let event_writer: Bound<'py, PyAny> = call_with( ®istry::native(py, NativeAttr::ExecutionEventWriter)?, - &[("path", request.getattr("event_output_path")?)], + &[("path", native_field(request, "event_output_path")?)], )?; let printer: Py = Py::new( py, @@ -346,7 +353,7 @@ pub(crate) fn execute_clone_plan<'py>( invocation, (pipeline, connection), ( - request.getattr("hard_copy")?, + native_field(request, "hard_copy")?, printer.into_bound(py).into_any(), ), )?; 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 4ce45f6f2b..4f2c067f60 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/clone/clone_pipeline.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/clone/clone_pipeline.rs @@ -7,6 +7,7 @@ use pyo3::{pyfunction, wrap_pyfunction}; use crate::bindings::_helpers::boundary::keyword_request::Request; use crate::bindings::_helpers::compile_driver::python_calls::call_with; use crate::bindings::_helpers::contracts::spec_resolution::resolve_target_config; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; @@ -46,17 +47,20 @@ fn compiled_environment<'py>( ("selected_target", target_name.clone()), ( "resolved_connection", - pipeline.connection.getattr("config")?, + native_field(&pipeline.connection, "config")?, + ), + ( + "no_sql_validation", + native_field(options, "no_sql_validation")?, ), - ("no_sql_validation", options.getattr("no_sql_validation")?), ( "analysis_selection", call_with( ®istry::class(py, RecordId::CompileAnalysisSelection)?, - &[("no_cache", options.getattr("no_cache")?)], + &[("no_cache", native_field(options, "no_cache")?)], )?, ), - ("cli_vars", options.getattr("cli_vars")?), + ("cli_vars", native_field(options, "cli_vars")?), ("external_sql_reference_resolver", pipeline.resolver.clone()), ], ) @@ -70,8 +74,8 @@ fn read_target_name<'py>( let py: Python<'py> = target_name.py(); let deferred: Bound<'py, PyAny> = resolve_target_config( py, - &discovered.getattr("project_config")?, - &discovered.getattr("local_config")?, + &native_field(discovered, "project_config")?, + &native_field(discovered, "local_config")?, target_name, )? .getattr("defer_sources_to")?; @@ -111,10 +115,10 @@ pub(crate) fn prepared_clone_pipeline<'py>( ®istry::native(py, NativeAttr::RunClonePlanning)?, &[ ("project", destination_project.clone()), - ("select", pipeline.options.getattr("select")?), - ("exclude", pipeline.options.getattr("exclude")?), + ("select", native_field(&pipeline.options, "select")?), + ("exclude", native_field(&pipeline.options, "exclude")?), ("adapter", pipeline.adapter.clone()), - ("connection", pipeline.connection.getattr("handle")?), + ("connection", native_field(&pipeline.connection, "handle")?), ("origin_project", origin_project.clone()), ("origin_source_project", origin_source_project), ("destination_source_project", destination_source_project), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/clone/clone_progress.rs b/crates/sqlbuild-python/src/bindings/_helpers/clone/clone_progress.rs index 15a3ebb780..ed3a45c08f 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/clone/clone_progress.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/clone/clone_progress.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::exceptions::PyKeyError; use pyo3::prelude::{Bound, Py, PyAny, PyAnyMethods, PyResult, Python}; use pyo3::types::PyDict; @@ -21,19 +22,19 @@ fn optional_text(value: Bound<'_, PyAny>) -> PyResult> { /// One executor `CloneItemResult` as the command renders it. pub(crate) fn clone_item_text(item: &Bound<'_, PyAny>) -> PyResult { - let duration: Bound<'_, PyAny> = item.getattr("duration_seconds")?; + let duration: Bound<'_, PyAny> = native_field(item, "duration_seconds")?; Ok(CloneItemText { - name: item.getattr("name")?.extract()?, - action: item.getattr("action")?.getattr("value")?.extract()?, + name: native_field(item, "name")?.extract()?, + action: native_field(item, "action")?.getattr("value")?.extract()?, status: CloneStatusKind::from_value( &item .getattr("status")? .getattr("value")? .extract::()?, ), - message: optional_text(item.getattr("message")?)?, - origin_relation: optional_text(item.getattr("origin_relation")?)?, - destination_relation: optional_text(item.getattr("destination_relation")?)?, + message: optional_text(native_field(item, "message")?)?, + origin_relation: optional_text(native_field(item, "origin_relation")?)?, + destination_relation: optional_text(native_field(item, "destination_relation")?)?, duration_seconds: if duration.is_none() { None } else { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/adapters.rs b/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/adapters.rs index e367ed907b..45a417cefb 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/adapters.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/adapters.rs @@ -9,6 +9,7 @@ use pyo3::{pyfunction, wrap_pyfunction}; 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::ErrorId; use crate::bindings::_helpers::records::registry; @@ -19,10 +20,10 @@ fn cli_adapter_error(py: Python<'_>, error: &PyErr) -> PyResult { let value: Bound<'_, PyAny> = error.value(py).clone().into_any(); let keywords: Bound<'_, PyDict> = PyDict::new(py); keywords.set_item("code", ADAPTER_CONFIGURATION_CODE)?; - keywords.set_item("help", value.getattr("help")?)?; + keywords.set_item("help", native_field(&value, "help")?)?; let raised: PyErr = PyErr::from_value( registry::error(py, ErrorId::CliUser)? - .call((value.getattr("message")?,), Some(&keywords))?, + .call((native_field(&value, "message")?,), Some(&keywords))?, ); raised.set_cause(py, Some(error.clone_ref(py))); Ok(raised) 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 d1ae0ad2a2..f2b6c612c4 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/connection.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/connection.rs @@ -20,6 +20,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::{ }; 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; @@ -74,7 +75,7 @@ pub(crate) fn with_dbt_profile<'py>( ("adapter_name", adapter_name.clone()), ( "project_config", - discovered_inputs.getattr("project_config")?, + native_field(discovered_inputs, "project_config")?, ), ("cli_vars", cli_vars.clone()), ], @@ -82,11 +83,11 @@ pub(crate) fn with_dbt_profile<'py>( if profile.is_none() { return Ok(config); } - for warning in profile.getattr("warnings")?.try_iter()? { + for warning in native_field(&profile, "warnings")?.try_iter()? { warn(py, &format!("Warning: {}", warning?.str()?))?; } let merged: Bound<'py, PyDict> = PyDict::new(py); - merged.update(&profile.getattr("connection")?.cast_into()?)?; + merged.update(&native_field(&profile, "connection")?.cast_into()?)?; for (key, value) in raw_config.iter() { let routing: bool = key .extract::() @@ -198,10 +199,10 @@ fn require_configured_connection( selected_target: &Bound<'_, PyAny>, ) -> PyResult<()> { let py: Python<'_> = project_dir.py(); - let project_config: Bound<'_, PyAny> = discovered_inputs.getattr("project_config")?; - let local_config: Bound<'_, PyAny> = discovered_inputs.getattr("local_config")?; - if project_config.getattr("connection")?.is_truthy()? - || local_config.getattr("connection")?.is_truthy()? + let project_config: Bound<'_, PyAny> = native_field(discovered_inputs, "project_config")?; + let local_config: Bound<'_, PyAny> = native_field(discovered_inputs, "local_config")?; + if native_field(&project_config, "connection")?.is_truthy()? + || native_field(&local_config, "connection")?.is_truthy()? { return Ok(()); } @@ -228,8 +229,8 @@ fn require_configured_connection( ("target_name", target_name.clone()), ], )?; - if !target_config.getattr("connection_name")?.is_none() - || target_config.getattr("connection")?.is_truthy()? + if !native_field(&target_config, "connection_name")?.is_none() + || native_field(&target_config, "connection")?.is_truthy()? { return Ok(()); } @@ -263,9 +264,12 @@ pub(crate) fn project_connection_config<'py>( &[ ( "project_config", - discovered_inputs.getattr("project_config")?, + native_field(discovered_inputs, "project_config")?, + ), + ( + "local_config", + native_field(discovered_inputs, "local_config")?, ), - ("local_config", discovered_inputs.getattr("local_config")?), ], )?; connection_config( 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 cb825263ab..e8b1dee75f 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/invocation.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/invocation.rs @@ -6,6 +6,7 @@ use pyo3::types::PyBool; use crate::bindings::_helpers::command_runtime::adapters::resolve_cli_adapter; use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; 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, RecordId}; use crate::bindings::_helpers::records::registry; @@ -33,8 +34,11 @@ pub(crate) fn resolve_adapter_connection_context<'py>( let adapter_name: Bound<'py, PyAny> = call_with( ®istry::native(py, NativeAttr::ResolveEffectiveAdapterName)?, &[ - ("project_config", discovered.getattr("project_config")?), - ("local_config", discovered.getattr("local_config")?), + ( + "project_config", + native_field(discovered, "project_config")?, + ), + ("local_config", native_field(discovered, "local_config")?), ], )?; let adapter: Bound<'py, PyAny> = resolve_cli_adapter(&adapter_name, project_dir)?; @@ -63,7 +67,7 @@ fn invocation_context<'py>( require_connection: bool, ) -> PyResult> { let py: Python<'py> = request.py(); - let requested: Bound<'py, PyAny> = request.getattr("project_dir")?; + let requested: Bound<'py, PyAny> = native_field(request, "project_dir")?; let project_dir: Bound<'py, PyAny> = if requested.is_none() { imported(py, "pathlib", "Path")?.call_method0("cwd")? } else { @@ -77,14 +81,15 @@ fn invocation_context<'py>( &discovered, &project_dir, ( - request.getattr("selected_target")?, - request.getattr("cli_vars")?, + native_field(request, "selected_target")?, + native_field(request, "cli_vars")?, ), require_connection, )?; - let machine_output: bool = request.getattr("json_output")?.is_truthy()?; - let use_color: bool = - !request.getattr("no_color")?.is_truthy()? && !machine_output && stdout_supports_color(py)?; + let machine_output: bool = native_field(request, "json_output")?.is_truthy()?; + let use_color: bool = !native_field(request, "no_color")?.is_truthy()? + && !machine_output + && stdout_supports_color(py)?; let stream: Bound<'py, PyAny> = py.import("sys")? .getattr(if machine_output { "stderr" } else { "stdout" })?; @@ -99,11 +104,14 @@ fn context_fields<'py>( Ok(vec![ ("effective_project_dir", project_dir.clone()), ("discovered_inputs", discovered.clone()), - ("adapter_name", adapter_context.getattr("adapter_name")?), - ("adapter", adapter_context.getattr("adapter")?), + ( + "adapter_name", + native_field(adapter_context, "adapter_name")?, + ), + ("adapter", native_field(adapter_context, "adapter")?), ( "connection_config", - adapter_context.getattr("connection_config")?, + native_field(adapter_context, "connection_config")?, ), ("use_color", boolean(py, *use_color)), ("progress_stream", stream.clone()), @@ -134,7 +142,10 @@ pub(crate) fn resolve_reported_command_invocation<'py>( call_with( ®istry::native(py, NativeAttr::ConnectionProgressReporter)?, &[ - ("adapter_name", adapter_context.getattr("adapter_name")?), + ( + "adapter_name", + native_field(adapter_context, "adapter_name")?, + ), ("stream", stream.clone()), ("use_color", boolean(py, *use_color)), ], diff --git a/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/preview_target.rs b/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/preview_target.rs index a57cd9990e..91992b5df3 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/preview_target.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/preview_target.rs @@ -7,6 +7,7 @@ use pyo3::{pyfunction, wrap_pyfunction}; use sqlbuild_config::command_connection::main::check_preview_target::check_preview_target; use crate::bindings::_helpers::command_runtime::runtime_errors::raised; +use crate::bindings::_helpers::records::fields::field as native_field; fn target_names(config: &Bound<'_, PyAny>) -> PyResult> { config @@ -28,8 +29,12 @@ pub(crate) fn validate_preview_target( let Some(as_target) = as_target else { return Ok(()); }; - let mut configured: Vec = target_names(&discovered_inputs.getattr("project_config")?)?; - configured.extend(target_names(&discovered_inputs.getattr("local_config")?)?); + let mut configured: Vec = + target_names(&native_field(&discovered_inputs, "project_config")?)?; + configured.extend(target_names(&native_field( + &discovered_inputs, + "local_config", + )?)?); check_preview_target(&as_target, &command_name, &configured).map_err(|error| raised(py, error)) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/runtime_errors.rs b/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/runtime_errors.rs index 37e5a2be94..e872c84cb1 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/runtime_errors.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/runtime_errors.rs @@ -4,6 +4,7 @@ use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyErr, PyResult, Python}; use pyo3::types::{PyDict, PyDictMethods}; use sqlbuild_config::command_connection::errors::CommandConnectionError; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::ErrorId; use crate::bindings::_helpers::records::registry; @@ -42,7 +43,7 @@ pub(crate) fn expected_resolution_error(py: Python<'_>, error: &PyErr) -> PyResu pub(crate) fn resolution_message(py: Python<'_>, error: &PyErr) -> PyResult { let value: Bound<'_, PyAny> = error.value(py).clone().into_any(); if error.is_instance(py, ®istry::error(py, ErrorId::CliUser)?) { - return value.getattr("message")?.str()?.extract(); + return native_field(&value, "message")?.str()?.extract(); } value.str()?.extract() } 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 2808e6538f..4a5d80d177 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/warehouses.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/warehouses.rs @@ -27,6 +27,7 @@ use crate::bindings::_helpers::discovery::configuration::discover_project_config 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; @@ -89,9 +90,9 @@ fn optional_text(value: &Bound<'_, PyAny>) -> PyResult> { pub(crate) fn command_scope(py: Python<'_>) -> PyResult> { let scope: Bound<'_, PyAny> = current_command_warehouse_scope(py)?; Ok(CommandScope { - group: scope.getattr("group")?, - cli_warehouse: optional_text(&scope.getattr("cli_warehouse")?)?, - cli_vars: scope.getattr("cli_vars")?, + group: native_field(&scope, "group")?, + cli_warehouse: optional_text(&native_field(&scope, "cli_warehouse")?)?, + cli_vars: native_field(&scope, "cli_vars")?, }) } @@ -125,7 +126,7 @@ fn group_value(target: &Bound<'_, PyAny>, group: &Bound<'_, PyAny>) -> PyResult< } else { QUERY_GROUP }; - optional_text(&target.getattr("warehouses")?.getattr(attribute)?) + optional_text(&native_field(target, "warehouses")?.getattr(attribute)?) } /// The active target's authored, unexpanded warehouse for one command group. @@ -134,8 +135,9 @@ pub(crate) fn find_authored<'py>( group: &Bound<'py, PyAny>, ) -> PyResult>> { let py: Python<'py> = group.py(); - let project_config: Bound<'py, PyAny> = inputs.discovered_inputs.getattr("project_config")?; - let local_config: Bound<'py, PyAny> = inputs.discovered_inputs.getattr("local_config")?; + 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)?, &[ @@ -154,7 +156,7 @@ pub(crate) fn find_authored<'py>( .getattr("targets")? .call_method1("get", (&target_name,))?; let target_name: String = target_name.extract()?; - let group_name: String = group.getattr("value")?.extract()?; + let group_name: String = native_field(group, "value")?.extract()?; Ok(authored_warehouse( (&target_name, &group_name), group_value(&local_target, group)?, @@ -287,8 +289,11 @@ fn validate_cli_warehouse( let adapter_name: Bound<'_, PyAny> = call_with( ®istry::native(py, NativeAttr::ResolveEffectiveAdapterName)?, &[ - ("project_config", discovered.getattr("project_config")?), - ("local_config", discovered.getattr("local_config")?), + ( + "project_config", + native_field(&discovered, "project_config")?, + ), + ("local_config", native_field(&discovered, "local_config")?), ], )?; if !session_warehouse_support(py, &adapter_name, &project_dir)? { 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 39430e2020..1e664b7d8f 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 @@ -6,6 +6,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; @@ -66,7 +67,7 @@ pub(crate) fn resolve_cursor_input_roles<'py>( model: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = model.py(); - let values: Bound<'py, PyAny> = model.getattr("config")?.getattr("values")?; + let values: Bound<'py, PyAny> = native_field(model, "config")?.getattr("values")?; let authored: Bound<'py, PyAny> = values.call_method1("get", (CURSOR_INPUTS_KEY,))?; let strategy: Bound<'py, PyAny> = values.call_method1("get", ("microbatch_strategy",))?; let rolling: bool = strategy.eq(ROLLING_WINDOW_STRATEGY)?; 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 725d880c7f..ac40f36b3c 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 @@ -5,6 +5,7 @@ use pyo3::pyfunction; use pyo3::types::PyTuple; 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::{ErrorId, NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::render::declaration_values::declaration_scope_resolver; @@ -56,19 +57,19 @@ pub(crate) fn build_declaration_scope<'py>( compile_cache_dir.unwrap_or_else(|| py.None().into_bound(py)), )], )?; - let test_files: Bound<'py, PyAny> = discovered_inputs.getattr("test_files")?; - let scenario_files: Bound<'py, PyAny> = discovered_inputs.getattr("scenario_files")?; + 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, - sql_lexical_syntax.getattr("native_mapping")?, + native_field(sql_lexical_syntax, "native_mapping")?, ))?; let native: Bound<'py, PyAny> = Bound::new( py, open_native_scope_index(py, discovered_inputs, loaded_macros, None)?, )? .into_any(); - let validated: Result<(), PyErr> = validate_scope_index(&native.getattr("index")?); + let validated: Result<(), PyErr> = validate_scope_index(&native_field(&native, "index")?); if let Err(error) = validated { if !error.is_instance(py, ®istry::error(py, ErrorId::ScopeValidation)?) { return Err(error); @@ -83,7 +84,7 @@ pub(crate) fn build_declaration_scope<'py>( && (test_files.is_truthy()? || scenario_files.is_truthy()?) { let faults: Bound<'py, PyAny> = call_with( - &native.getattr("grant_relationships")?, + &native_field(&native, "grant_relationships")?, &[ ("discovered_inputs", discovered_inputs.clone()), ("syntax", sql_lexical_syntax.clone()), @@ -122,11 +123,11 @@ fn indexed_macro_metadata<'py>(loaded_macros: &Bound<'py, PyAny>) -> PyResult> = Vec::new(); for pair in loaded_macros.call_method0("items")?.try_iter()? { let (key, loaded): (Bound<'py, PyAny>, Bound<'py, PyAny>) = pair?.extract()?; - let function: Bound<'py, PyAny> = loaded.getattr("function")?; + let function: Bound<'py, PyAny> = native_field(&loaded, "function")?; let code: Bound<'py, PyAny> = imported(py, "builtins", "getattr")?.call1((&function, "__code__", py.None()))?; let first_line: Bound<'py, PyAny> = if code.is_instance(&code_type)? { - code.getattr("co_firstlineno")? + native_field(&code, "co_firstlineno")? } else { py.None().into_bound(py) }; @@ -142,10 +143,10 @@ fn indexed_macro_metadata<'py>(loaded_macros: &Bound<'py, PyAny>) -> PyResult( ) -> PyResult> { let py: Python<'py> = scope.py(); let current: Bound<'py, PyTuple> = indexed_macro_metadata(loaded_macros)?; - let indexed: Bound<'py, PyTuple> = indexed_macro_metadata(&scope.getattr("loaded_macros")?)?; + let indexed: Bound<'py, PyTuple> = + indexed_macro_metadata(&native_field(scope, "loaded_macros")?)?; if current.as_any().ne(indexed.as_any())? { return Ok(py.None().into_bound(py)); } @@ -172,9 +174,9 @@ pub(crate) fn rebind_declaration_scope<'py>( discovered_inputs, loaded_macros, ( - scope.getattr("index")?, - scope.getattr("resolver")?.getattr("lookup")?, - scope.getattr("sql_test_scans")?, + native_field(scope, "index")?, + native_field(scope, "resolver")?.getattr("lookup")?, + native_field(scope, "sql_test_scans")?, ), ) } 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 174e0e20b9..464ffc2611 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 @@ -13,6 +13,7 @@ use crate::bindings::_helpers::contracts::spec_resolution::{ use crate::bindings::_helpers::model_config::template_data::{ expanded_effective_vars, expanded_template_data, run_id, }; +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; @@ -63,15 +64,15 @@ pub(crate) fn build_effective_settings<'py>( let names: Vec> = imported(py, "dataclasses", "fields")? .call1((&settings_class,))? .try_iter()? - .map(|field| field?.getattr("name")) + .map(|field| native_field(&field?, "name")) .collect::>()?; let values: Bound<'py, PyDict> = registry::native(py, NativeAttr::EffectiveConfigSettings)? .call1(( ( - project_config.getattr("settings")?, - local_config.getattr("settings")?, + native_field(project_config, "settings")?, + native_field(local_config, "settings")?, ), - local_config.getattr("setting_overrides")?, + native_field(local_config, "setting_overrides")?, PyList::new(py, names)?, ))? .downcast_into()?; @@ -99,8 +100,8 @@ fn target_selection<'py>( selected: Option>, ) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> { let py: Python<'py> = discovered.py(); - let project_config: Bound<'py, PyAny> = discovered.getattr("project_config")?; - let local_config: Bound<'py, PyAny> = discovered.getattr("local_config")?; + let project_config: Bound<'py, PyAny> = native_field(discovered, "project_config")?; + let local_config: Bound<'py, PyAny> = native_field(discovered, "local_config")?; let selected: Bound<'py, PyAny> = selected.unwrap_or_else(|| py.None().into_bound(py)); let Some(name) = resolve_target_name(py, &project_config, &local_config, &selected)? else { return Ok((py.None().into_bound(py), py.None().into_bound(py))); @@ -123,8 +124,8 @@ fn discovered_vars<'py>( _ => PyDict::new(py).into_any(), }; build_effective_vars( - &discovered.getattr("project_config")?, - &discovered.getattr("local_config")?, + &native_field(discovered, "project_config")?, + &native_field(discovered, "local_config")?, target_config, &cli_vars, ) @@ -152,8 +153,8 @@ pub(crate) fn build_effective_connection_config<'py>( let effective_vars: Bound<'py, PyAny> = discovered_vars(discovered_inputs, &target_config, cli_vars)?; build_effective_connection( - &discovered_inputs.getattr("project_config")?, - &discovered_inputs.getattr("local_config")?, + &native_field(discovered_inputs, "project_config")?, + &native_field(discovered_inputs, "local_config")?, &target_config, &effective_vars, ) @@ -182,8 +183,8 @@ pub(crate) fn build_effective_settings_config<'py>( discovered_inputs: &Bound<'py, PyAny>, ) -> PyResult> { build_effective_settings( - &discovered_inputs.getattr("project_config")?, - &discovered_inputs.getattr("local_config")?, + &native_field(discovered_inputs, "project_config")?, + &native_field(discovered_inputs, "local_config")?, ) } @@ -193,8 +194,8 @@ pub(crate) fn project_vars<'py>( target_config: &Bound<'py, PyAny>, ) -> PyResult> { build_effective_vars( - &discovered.getattr("project_config")?, - &discovered.getattr("local_config")?, + &native_field(discovered, "project_config")?, + &native_field(discovered, "local_config")?, target_config, PyDict::new(discovered.py()).as_any(), ) 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 6f2fcef8ae..1b98edf359 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 @@ -8,6 +8,7 @@ use crate::bindings::_helpers::compile_attachment::storage_policies::inherited_s 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::unicode_environment::unicode_environment; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; @@ -19,9 +20,9 @@ pub(crate) fn native_model_config_session<'py>( validator: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = inputs.py(); - let project_config: Bound<'py, PyAny> = inputs.getattr("project_config")?; - let target: Bound<'py, PyAny> = inputs.getattr("target_config")?; - let declared: Bound<'py, PyAny> = project_config.getattr("materialization_defaults")?; + let project_config: Bound<'py, PyAny> = native_field(inputs, "project_config")?; + let target: Bound<'py, PyAny> = native_field(inputs, "target_config")?; + let declared: Bound<'py, PyAny> = native_field(&project_config, "materialization_defaults")?; let materialization_defaults: Bound<'py, PyAny> = if declared.is_truthy()? { declared } else { @@ -41,20 +42,30 @@ pub(crate) fn native_model_config_session<'py>( let target_namespace: Bound<'py, PyAny> = if target.is_none() { target.clone() } else { - PyTuple::new(py, [target.getattr("database")?, target.getattr("schema")?])?.into_any() + PyTuple::new( + py, + [ + native_field(&target, "database")?, + native_field(&target, "schema")?, + ], + )? + .into_any() }; let builder: Bound<'py, PyAny> = registry::native(py, NativeAttr::NativeModelConfigBuilder)? .call1(( ( - project_config.getattr("defaults")?, - project_config.getattr("path_defaults")?, + native_field(&project_config, "defaults")?, + native_field(&project_config, "path_defaults")?, hook_entries, ), - (inputs.getattr("effective_vars")?, unicode_environment(py)?), + ( + native_field(inputs, "effective_vars")?, + unicode_environment(py)?, + ), ( ( - inputs.getattr("effective_target_name")?, - inputs.getattr("run_id")?, + native_field(inputs, "effective_target_name")?, + native_field(inputs, "run_id")?, ), target_namespace, ), @@ -70,7 +81,10 @@ pub(crate) fn native_model_config_session<'py>( call_with( ®istry::class(py, RecordId::NativeModelConfigSession)?, &[ - ("path_defaults", project_config.getattr("path_defaults")?), + ( + "path_defaults", + native_field(&project_config, "path_defaults")?, + ), ("builder", builder), ("validator", validator.clone()), ("inherited_storage", inherited.into_any()), 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 2f0c7060a1..f50de01af2 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 @@ -22,6 +22,7 @@ use crate::bindings::_helpers::render::macro_expansion_result::macro_expansion_c 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::registry; use sqlbuild_core::constants::PROJECT_CONFIG_FILENAME; @@ -60,8 +61,8 @@ fn constant_rendering<'py>(context: &Bound<'py, PyAny>) -> PyResult(context: &Bound<'py, PyAny>) -> PyResult dict( py, vec![ - ("loaded_macros", context.getattr("loaded_macros")?), - ("macro_context", context.getattr("macro_context")?), + ("loaded_macros", native_field(context, "loaded_macros")?), + ("macro_context", native_field(context, "macro_context")?), ( "bridge", host::entry(py, UserCodeHost::ActiveMacroBridge)?.call0()?, ), ( "scoped", - pyo3::types::PyBool::new(py, !context.getattr("declaration_resolver")?.is_none()) - .to_owned() - .into_any(), + pyo3::types::PyBool::new( + py, + !native_field(context, "declaration_resolver")?.is_none(), + ) + .to_owned() + .into_any(), ), ( "declaration_context", @@ -118,8 +122,8 @@ fn sql_hook_inputs<'py>( "python_hook_entry", registry::class(py, RecordId::PythonHookEntry)?, ), - ("hook_files", discovered.getattr("sql_hook_files")?), - ("effective_vars", context.getattr("effective_vars")?), + ("hook_files", native_field(discovered, "sql_hook_files")?), + ("effective_vars", native_field(context, "effective_vars")?), ("environment", unicode_environment(py)?), ( "render_project_var", @@ -168,22 +172,22 @@ pub(crate) fn build_model_inputs<'py>( let no_sql_validation: bool = arguments.flag("no_sql_validation")?; 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> = context.getattr("effective_settings")?; + 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, - settings.getattr("microbatch_concurrency")?, + native_field(&settings, "microbatch_concurrency")?, ), )?; if !external.is_none() { call_with( - &external.getattr("validate_model_names")?, + &native_field(&external, "validate_model_names")?, &[("known_model_names", validator.call_method0("model_names")?)], )?; } - let project_dir: Bound<'py, PyAny> = discovered_inputs.getattr("project_dir")?; + let project_dir: Bound<'py, PyAny> = native_field(discovered_inputs, "project_dir")?; let root: Bound<'py, PyAny> = if project_dir.is_truthy()? { project_dir } else { @@ -195,18 +199,18 @@ pub(crate) fn build_model_inputs<'py>( &[ ( "project_config", - discovered_inputs.getattr("project_config")?, + native_field(discovered_inputs, "project_config")?, ), - ("target_config", context.getattr("target_config")?), - ("effective_vars", context.getattr("effective_vars")?), + ("target_config", native_field(context, "target_config")?), + ("effective_vars", native_field(context, "effective_vars")?), ( "effective_target_name", - context.getattr("effective_target_name")?, + native_field(context, "effective_target_name")?, ), - ("run_id", context.getattr("run_id")?), + ("run_id", native_field(context, "run_id")?), ( "microbatch_concurrency", - settings.getattr("microbatch_concurrency")?, + native_field(&settings, "microbatch_concurrency")?, ), ], )?; @@ -214,7 +218,7 @@ pub(crate) fn build_model_inputs<'py>( let provider_names: Vec> = discovered_inputs .getattr("providers")? .try_iter()? - .map(|provider| provider?.getattr("name")) + .map(|provider| native_field(&provider?, "name")) .collect::>()?; let host_inputs: Bound<'py, PyDict> = dict( py, @@ -241,7 +245,7 @@ pub(crate) fn build_model_inputs<'py>( ), ( "hook_functions", - discovered_inputs.getattr("hook_functions")?, + native_field(discovered_inputs, "hook_functions")?, ), ( "provider_names", @@ -250,23 +254,23 @@ pub(crate) fn build_model_inputs<'py>( ], )?; let classes: Bound<'py, PyDict> = model_loop_classes( - &discovered_inputs.getattr("audit_factories")?, + &native_field(discovered_inputs, "audit_factories")?, context, &external, &host_inputs, )?; - let syntax: Bound<'py, PyAny> = context.getattr("sql_lexical_syntax")?; + 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), - syntax.getattr("native_mapping")?, + native_field(&syntax, "native_mapping")?, reserved_cursor_markers(), ( ( - settings.getattr("sql_analysis")?, + native_field(&settings, "sql_analysis")?, no_sql_validation, defer_model_sql_validation, - settings.getattr("require_sql_analysis")?, + native_field(&settings, "require_sql_analysis")?, ), project_config_path.str()?, ), 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 ca31f38e93..7c4ed3d9c3 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 @@ -11,6 +11,7 @@ use crate::bindings::_helpers::contracts::spec_resolution::resolve_effective_ada 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; use pyo3::IntoPyObjectExt; @@ -23,7 +24,7 @@ fn declaration_scope<'py>( ) -> PyResult> { let py: Python<'py> = discovered.py(); let loaded_macros: Bound<'py, PyAny> = host::entry(py, UserCodeHost::LoadProjectMacros)? - .call1((discovered.getattr("macro_files")?,))?; + .call1((native_field(discovered, "macro_files")?,))?; if !static_scope.is_none() { let rebound: Bound<'py, PyAny> = rebind_declaration_scope(&static_scope, &discovered.clone(), &loaded_macros.clone()) @@ -50,22 +51,25 @@ fn local_declarations<'py>(discovered: &Bound<'py, PyAny>) -> PyResult = PyDict::new(py); for declaration in declarations.try_iter()? { let declaration: Bound<'py, PyAny> = declaration?; - named.set_item(declaration.getattr("name")?, declaration)?; + named.set_item(native_field(&declaration, "name")?, declaration)?; } Ok(named.into_any()) }; let local: Bound<'py, PyDict> = PyDict::new(py); - for model_file in discovered.getattr("model_files")?.try_iter()? { + for model_file in native_field(discovered, "model_files")?.try_iter()? { let model_file: Bound<'py, PyAny> = model_file?; local.set_item( - model_file.getattr("file_path")?, + native_field(&model_file, "file_path")?, call_with( &context, &[ - ("enums", by_name(model_file.getattr("enum_declarations")?)?), + ( + "enums", + by_name(native_field(&model_file, "enum_declarations")?)?, + ), ( "constants", - by_name(model_file.getattr("constant_declarations")?)?, + by_name(native_field(&model_file, "constant_declarations")?)?, ), ], )?, @@ -163,8 +167,8 @@ pub(crate) fn sql_expansion_inputs(sources: ExpansionSources<'_>) -> PyResult = discovered.getattr("project_config")?; - let local_config: Bound<'_, PyAny> = discovered.getattr("local_config")?; + let project_config: Bound<'_, PyAny> = native_field(&discovered, "project_config")?; + let local_config: Bound<'_, PyAny> = native_field(&discovered, "local_config")?; let cli_vars: Bound<'_, PyAny> = sources .cli_vars .unwrap_or_else(|| PyDict::new(py).into_any()); @@ -182,7 +186,7 @@ pub(crate) fn sql_expansion_inputs(sources: ExpansionSources<'_>) -> PyResult, Bound<'_, PyAny>) = @@ -204,7 +208,10 @@ pub(crate) fn sql_expansion_inputs(sources: ExpansionSources<'_>) -> PyResult) -> PyResult( let mut policy: Option<(Bound<'py, PyAny>, &str)> = None; if let Some(entry) = entry { if !target.is_none() { - policy = present(target.getattr("time_travel_retention")?).map(|p| (p, "TARGET")); + policy = present(native_field(target, "time_travel_retention")?).map(|p| (p, "TARGET")); } - if let Some(found) = present(entry.getattr("time_travel_retention")?) { + if let Some(found) = present(native_field(entry, "time_travel_retention")?) { policy = Some((found, "MATERIALIZATION")); } if !target.is_none() @@ -60,8 +61,8 @@ fn retention<'py>( call_with( &resolved, &[ - ("desired_days", policy.getattr("desired_days")?), - ("unmanaged", policy.getattr("unmanaged")?), + ("desired_days", native_field(&policy, "desired_days")?), + ("unmanaged", native_field(&policy, "unmanaged")?), ("source", source(py, class, member)?), ], ) @@ -77,9 +78,9 @@ fn table_type<'py>( let mut declared: Option<(Bound<'py, PyAny>, &str)> = None; if let Some(entry) = entry { if !target.is_none() { - declared = present(target.getattr("default_table_type")?).map(|v| (v, "TARGET")); + declared = present(native_field(target, "default_table_type")?).map(|v| (v, "TARGET")); } - if let Some(found) = present(entry.getattr("table_type")?) { + if let Some(found) = present(native_field(entry, "table_type")?) { declared = Some((found, "MATERIALIZATION")); } } 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 21c49a298a..e1aa18f7a2 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_attachment/target.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_attachment/target.rs @@ -16,6 +16,7 @@ use crate::bindings::_helpers::frontier::constants::{ COMPILER_CACHE_DIRECTORY_NAME, RETIRED_CACHE_NAMESPACE_SUFFIXES, }; use crate::bindings::_helpers::model_config::template_data::{early_model_templates, run_id}; +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::IntoPyObjectExt; const DATABASE_KEY: &str = "database"; @@ -27,8 +28,8 @@ fn resolved_target<'py>( selected_target: &Bound<'py, PyAny>, ) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> { let py: Python<'py> = discovered.py(); - let project_config: Bound<'py, PyAny> = discovered.getattr("project_config")?; - let local_config: Bound<'py, PyAny> = discovered.getattr("local_config")?; + let project_config: Bound<'py, PyAny> = native_field(discovered, "project_config")?; + let local_config: Bound<'py, PyAny> = native_field(discovered, "local_config")?; let target_name: Bound<'py, PyAny> = resolve_target_name( py, &project_config.clone(), @@ -56,7 +57,7 @@ pub(crate) fn build_compile_target_context<'py>( let py: Python<'py> = discovered_inputs.py(); let (target_name, target_config) = resolved_target(discovered_inputs, selected_target)?; let cache_dir: Bound<'py, PyAny> = compile_cache_root( - &discovered_inputs.getattr("project_dir")?, + &native_field(discovered_inputs, "project_dir")?, &target_config, no_cache, )?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_command/prepared_artifacts.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_command/prepared_artifacts.rs index 6b80fbf6ae..c0251b1fef 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_command/prepared_artifacts.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_command/prepared_artifacts.rs @@ -15,6 +15,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::{ KeywordArguments, call_with, imported, keywords, }; use crate::bindings::_helpers::compile_outputs::artifacts::staged_artifact_files; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::NativeAttr; use crate::bindings::_helpers::records::registry; @@ -46,7 +47,7 @@ pub(crate) struct PreparedCompileArtifacts { } fn owner_lock_path<'py>(staging_dir: &Bound<'py, PyAny>) -> PyResult> { - let name: String = staging_dir.getattr("name")?.extract()?; + let name: String = native_field(staging_dir, "name")?.extract()?; staging_dir.call_method1("with_name", (format!("{name}{LOCK_SUFFIX}"),)) } @@ -61,7 +62,7 @@ fn owns_staging_directory(staging_dir: &Bound<'_, PyAny>) -> PyResult { )?), ); match read { - Ok(text) => text.eq(staging_dir.getattr("name")?), + Ok(text) => text.eq(native_field(staging_dir, "name")?), Err(error) if error.is_instance_of::(py) => Ok(false), Err(error) => Err(error), } @@ -71,7 +72,7 @@ fn owns_staging_directory(staging_dir: &Bound<'_, PyAny>) -> PyResult { fn release_and_unlink(lock: &Bound<'_, PyAny>) -> PyResult<()> { let py: Python<'_> = lock.py(); let unlinked: PyResult> = imported(py, "pathlib", "Path")? - .call1((lock.getattr("lock_file")?,))? + .call1((native_field(lock, "lock_file")?,))? .call_method( "unlink", (), @@ -116,7 +117,7 @@ fn remove_abandoned_directory(directory: &Bound<'_, PyAny>) -> PyResult { py, "skipped abandoned staging cleanup in %s: %s", ( - directory.getattr("parent")?, + native_field(directory, "parent")?, error.value(py).clone().into_any(), ), )?; @@ -164,7 +165,7 @@ fn submit_in_context<'py>( ) -> PyResult> { let py: Python<'py> = executor.py(); let context: Bound<'py, PyAny> = imported(py, "contextvars", "copy_context")?.call0()?; - executor.call_method1("submit", (context.getattr("run")?, task)) + executor.call_method1("submit", (native_field(&context, "run")?, task)) } fn is_os_error(py: Python<'_>, error: &pyo3::PyErr) -> bool { @@ -185,10 +186,10 @@ impl PreparedCompileArtifacts { /// Whether staging pays off: uncached, mid-sized projects whose memory stays within budget. fn stages_artifacts(slf: &Bound<'_, Self>, project: &Bound<'_, PyAny>) -> PyResult { - let models: usize = project.getattr("models")?.len()?; - let minimum: usize = slf.getattr("min_prepared_models")?.extract()?; - let maximum: usize = slf.getattr("max_prepared_models")?.extract()?; - Ok(project.getattr("compile_cache_dir")?.is_none() + let models: usize = native_field(project, "models")?.len()?; + let minimum: usize = native_field(slf, "min_prepared_models")?.extract()?; + let maximum: usize = native_field(slf, "max_prepared_models")?.extract()?; + Ok(native_field(project, "compile_cache_dir")?.is_none() && minimum <= models && models <= maximum) } @@ -327,7 +328,7 @@ impl PreparedCompileArtifacts { return Ok(None); }; Ok(Some(( - planned.bind(py).getattr("result")?, + native_field(planned.bind(py), "result")?, [ project.bind(py).clone(), adapter.bind(py).clone(), @@ -453,7 +454,7 @@ impl PreparedCompileArtifacts { .call1((target_dir.call_method1("glob", (format!("{STAGING_PREFIX}*"),))?,))?; for directory in directories.try_iter()? { let directory: Bound<'_, PyAny> = directory?; - if directory.getattr("suffix")?.eq(LOCK_SUFFIX)? { + if native_field(&directory, "suffix")?.eq(LOCK_SUFFIX)? { continue; } if !remove_abandoned_directory(&directory)? { @@ -469,7 +470,7 @@ impl PreparedCompileArtifacts { let _ = staging_dir.call_method0("mkdir")?; let _ = staging_dir.div(OWNER_MARKER)?.call_method( "write_text", - (staging_dir.getattr("name")?,), + (native_field(staging_dir, "name")?,), Some(&keywords( py, &[("encoding", "utf-8".into_bound_py_any(py)?)], @@ -504,7 +505,7 @@ impl PreparedCompileArtifacts { { return Ok(()); } - if project.getattr("sql_tests")?.is_truthy()? { + if native_field(project, "sql_tests")?.is_truthy()? { Self::start_test_planning(slf, project, adapter, target_dir)?; } Ok(()) @@ -542,7 +543,7 @@ impl PreparedCompileArtifacts { let (prepared, expected_files): (Bound<'py, PyAny>, Bound<'py, PyAny>) = result.extract()?; if !owns_staging_directory(&staging_dir)? { - let name: String = staging_dir.getattr("name")?.extract()?; + let name: String = native_field(&staging_dir, "name")?.extract()?; return Err(staging_changed( py, &format!( @@ -582,7 +583,7 @@ impl PreparedCompileArtifacts { return Ok(None); }; match Self::staged_result(&future)? { - Some(result) => Ok(Some(result.get_item(0)?.getattr("diagnostics")?)), + Some(result) => Ok(Some(native_field(&result.get_item(0)?, "diagnostics")?)), None => Ok(None), } } 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 dab6864f69..79af87bdf9 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_command/report.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_command/report.rs @@ -22,6 +22,7 @@ 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::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; @@ -71,7 +72,7 @@ impl<'py> ReportInputs<'py> { let graph: Bound<'py, PyAny> = arguments.value("graph")?; let lineage_mode: Bound<'py, PyAny> = arguments.value("lineage_mode")?; Ok(Self { - project: graph.getattr("project")?, + project: native_field(&graph, "project")?, graph, written: arguments.value("written")?, manifest: arguments.flag("manifest")?, @@ -103,8 +104,8 @@ impl<'py> ReportInputs<'py> { let names: Bound<'py, PySet> = PySet::empty(self.py())?; for key in self.selected_keys.try_iter()? { let key: Bound<'py, PyAny> = key?; - if key.getattr("resource_type")?.eq(resource_type)? { - names.add(key.getattr("name")?)?; + if native_field(&key, "resource_type")?.eq(resource_type)? { + names.add(native_field(&key, "name")?)?; } } Ok(Some(names)) @@ -122,7 +123,7 @@ impl<'py> ReportInputs<'py> { }; let mut selected: Vec> = Vec::new(); for model in models { - if names.contains(model.getattr("name")?)? { + if names.contains(native_field(&model, "name")?)? { selected.push(model); } } @@ -145,7 +146,7 @@ impl<'py> ReportInputs<'py> { return Ok(count); } for key in self.selected_keys.try_iter()? { - let kind: String = key?.getattr("resource_type")?.str()?.to_string(); + let kind: String = native_field(&key?, "resource_type")?.str()?.to_string(); count += usize::from(types.contains(&kind.as_str())); } Ok(count) @@ -154,7 +155,7 @@ impl<'py> ReportInputs<'py> { fn severity_count(&self, severity: &str) -> PyResult { let mut count: usize = 0; for diagnostic in &self.diagnostics { - count += usize::from(diagnostic.getattr("severity")?.eq(severity)?); + count += usize::from(native_field(diagnostic, "severity")?.eq(severity)?); } Ok(count) } @@ -197,7 +198,7 @@ impl<'py> ReportInputs<'py> { .written .getattr("target_dir")? .div(COMPILED_DIRECTORY)?; - let parts: Bound<'py, PyTuple> = path.getattr("parts")?.downcast_into()?; + let parts: Bound<'py, PyTuple> = native_field(&path, "parts")?.downcast_into()?; for (index, part) in parts.iter().enumerate() { if part.eq(TARGET_DIRECTORY)? { let kept: Bound<'py, PyTuple> = parts.get_slice(index, parts.len()); @@ -221,17 +222,20 @@ fn optional_number(value: &Bound<'_, PyAny>) -> PyResult> { /// Each authored file's text by path: models with SQL, then sources, then SQL tests. fn source_texts<'py>(project: &Bound<'py, PyAny>) -> PyResult> { let texts: Bound<'py, PyDict> = PyDict::new(project.py()); - for model in project.getattr("models")?.try_iter()? { + for model in native_field(project, "models")?.try_iter()? { let model: Bound<'py, PyAny> = model?; - let authored: Bound<'py, PyAny> = model.getattr("authored_sql")?; + let authored: Bound<'py, PyAny> = native_field(&model, "authored_sql")?; if authored.is_truthy()? { - texts.set_item(model.getattr("relative_path")?, authored)?; + texts.set_item(native_field(&model, "relative_path")?, authored)?; } } 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)?; - texts.set_item(file.getattr("relative_path")?, file.getattr("contents")?)?; + texts.set_item( + native_field(&file, "relative_path")?, + native_field(&file, "contents")?, + )?; } } Ok(texts) @@ -242,13 +246,13 @@ fn location_text( location: &Bound<'_, PyAny>, texts: &Bound<'_, PyDict>, ) -> PyResult { - let path: Bound<'_, PyAny> = location.getattr("path")?; + let path: Bound<'_, PyAny> = native_field(location, "path")?; Ok(LocationText { path: reader.text(&path)?, - line: location.getattr("line")?.extract()?, - column: location.getattr("column")?.extract()?, - end_line: optional_number(&location.getattr("end_line")?)?, - end_column: optional_number(&location.getattr("end_column")?)?, + line: native_field(location, "line")?.extract()?, + column: native_field(location, "column")?.extract()?, + end_line: optional_number(&native_field(location, "end_line")?)?, + end_column: optional_number(&native_field(location, "end_column")?)?, source: texts .get_item(path)? .map(|text| reader.text(&text)) @@ -266,32 +270,32 @@ fn diagnostic_text( texts: &Bound<'_, PyDict>, occurrences: usize, ) -> PyResult { - let location: Bound<'_, PyAny> = diagnostic.getattr("location")?; + let location: Bound<'_, PyAny> = native_field(diagnostic, "location")?; let mut related: Vec = Vec::new(); - for item in diagnostic.getattr("related_locations")?.try_iter()? { + for item in native_field(diagnostic, "related_locations")?.try_iter()? { let item: Bound<'_, PyAny> = item?; related.push(RelatedText { - label: reader.text(&item.getattr("label")?)?, - location: location_text(reader, &item.getattr("location")?, texts)?, - message: reader.optional(&item.getattr("message")?)?, + label: reader.text(&native_field(&item, "label")?)?, + location: location_text(reader, &native_field(&item, "location")?, texts)?, + message: reader.optional(&native_field(&item, "message")?)?, }); } Ok(DiagnosticText { - severity: reader.text(&diagnostic.getattr("severity")?)?, - code: reader.text(&diagnostic.getattr("code")?)?, - message: reader.text(&diagnostic.getattr("message")?)?, - resource_type: reader.optional(&diagnostic.getattr("resource_type")?)?, - resource_name: reader.optional(&diagnostic.getattr("resource_name")?)?, + severity: reader.text(&native_field(diagnostic, "severity")?)?, + code: reader.text(&native_field(diagnostic, "code")?)?, + message: reader.text(&native_field(diagnostic, "message")?)?, + resource_type: reader.optional(&native_field(diagnostic, "resource_type")?)?, + resource_name: reader.optional(&native_field(diagnostic, "resource_name")?)?, location: if location.is_none() { None } else { Some(location_text(reader, &location, texts)?) }, - path: reader.optional(&diagnostic.getattr("path")?)?, + path: reader.optional(&native_field(diagnostic, "path")?)?, related, - notes: texts_of(reader, &diagnostic.getattr("notes")?)?, - affected_rules: texts_of(reader, &diagnostic.getattr("affected_rules")?)?, - help: reader.optional(&diagnostic.getattr("help")?)?, + notes: texts_of(reader, &native_field(diagnostic, "notes")?)?, + affected_rules: texts_of(reader, &native_field(diagnostic, "affected_rules")?)?, + help: reader.optional(&native_field(diagnostic, "help")?)?, occurrences, }) } @@ -308,7 +312,7 @@ fn shown_diagnostics( let texts: Bound<'_, PyDict> = source_texts(&inputs.project)?; let warning_counts: Bound<'_, PyDict> = PyDict::new(py); for diagnostic in &inputs.diagnostics { - if diagnostic.getattr("severity")?.eq(WARNING_SEVERITY)? { + if native_field(diagnostic, "severity")?.eq(WARNING_SEVERITY)? { let count: usize = warning_counts .get_item(diagnostic)? .map_or(Ok(0), |count| count.extract())?; @@ -321,7 +325,7 @@ fn shown_diagnostics( if shown_warnings.contains(diagnostic)? { continue; } - if diagnostic.getattr("severity")?.eq(WARNING_SEVERITY)? { + if native_field(diagnostic, "severity")?.eq(WARNING_SEVERITY)? { shown_warnings.set_item(diagnostic, true)?; } let occurrences: usize = warning_counts @@ -336,10 +340,10 @@ fn shown_diagnostics( fn error_models<'py>(inputs: &ReportInputs<'py>) -> PyResult> { let names: Bound<'py, PySet> = PySet::empty(inputs.py())?; for diagnostic in &inputs.diagnostics { - let name: Bound<'_, PyAny> = diagnostic.getattr("resource_name")?; - if diagnostic.getattr("is_error")?.is_truthy()? + let name: Bound<'_, PyAny> = native_field(diagnostic, "resource_name")?; + if native_field(diagnostic, "is_error")?.is_truthy()? && !name.is_none() - && !diagnostic.getattr("resource_type")?.eq(SQL_TEST_TYPE)? + && !native_field(diagnostic, "resource_type")?.eq(SQL_TEST_TYPE)? { names.add(name)?; } @@ -361,11 +365,11 @@ pub(crate) fn format_compile_text<'py>( let failed: Bound<'py, PySet> = error_models(&inputs)?; let mut models: Vec = Vec::new(); for model in inputs.selected_models()? { - let name: Bound<'py, PyAny> = model.getattr("name")?; + let name: Bound<'py, PyAny> = native_field(&model, "name")?; models.push(ModelLine { failed: failed.contains(&name)?, name: reader.text(&name)?, - column_count: model.getattr("output_column_count")?.extract()?, + column_count: native_field(&model, "output_column_count")?.extract()?, }); } let lineage: LineageNote = if !inputs.lineage.is_none() && inputs.lineage_mode == RICH_LINEAGE { @@ -409,7 +413,7 @@ pub(crate) fn json_semantic_notice<'py>( let py: Python<'py> = graph.py(); let reader: SurrogateText = SurrogateText::default(); let notice: Option = ReportInputs { - project: graph.getattr("project")?, + project: native_field(graph, "project")?, graph: graph.clone(), written: py.None().into_bound(py), manifest: false, @@ -456,9 +460,9 @@ fn location_json<'py>(location: &Bound<'py, PyAny>) -> PyResult = dict( py, vec![ - ("path", location.getattr("path")?.str()?.into_any()), - ("line", location.getattr("line")?), - ("column", location.getattr("column")?), + ("path", native_field(location, "path")?.str()?.into_any()), + ("line", native_field(location, "line")?), + ("column", native_field(location, "column")?), ], )?; for key in ["end_line", "end_column"] { @@ -476,16 +480,19 @@ fn diagnostic_json<'py>(diagnostic: &Bound<'py, PyAny>) -> PyResult = dict( py, vec![ - ("phase", diagnostic.getattr("phase")?.str()?.into_any()), + ( + "phase", + native_field(diagnostic, "phase")?.str()?.into_any(), + ), ( "severity", - diagnostic.getattr("severity")?.str()?.into_any(), + native_field(diagnostic, "severity")?.str()?.into_any(), ), - ("code", diagnostic.getattr("code")?), - ("message", diagnostic.getattr("message")?), + ("code", native_field(diagnostic, "code")?), + ("message", native_field(diagnostic, "message")?), ], )?; - let resource_type: Bound<'py, PyAny> = diagnostic.getattr("resource_type")?; + let resource_type: Bound<'py, PyAny> = native_field(diagnostic, "resource_type")?; if !resource_type.is_none() { payload.set_item("resource_type", resource_type.str()?)?; } @@ -495,7 +502,7 @@ fn diagnostic_json<'py>(diagnostic: &Bound<'py, PyAny>) -> PyResult = diagnostic.getattr("path")?; + let path: Bound<'py, PyAny> = native_field(diagnostic, "path")?; if !path.is_none() { payload.set_item("path", path.str()?)?; } @@ -505,11 +512,11 @@ fn diagnostic_json<'py>(diagnostic: &Bound<'py, PyAny>) -> PyResult = diagnostic.getattr("location")?; + let location: Bound<'py, PyAny> = native_field(diagnostic, "location")?; if !location.is_none() { payload.set_item("location", location_json(&location)?)?; } - let related: Bound<'py, PyAny> = diagnostic.getattr("related_locations")?; + let related: Bound<'py, PyAny> = native_field(diagnostic, "related_locations")?; if related.is_truthy()? { let mut items: Vec> = Vec::new(); for item in related.try_iter()? { @@ -517,14 +524,14 @@ fn diagnostic_json<'py>(diagnostic: &Bound<'py, PyAny>) -> PyResult = dict( py, vec![ - ("label", item.getattr("label")?), + ("label", native_field(&item, "label")?), ( "location", - location_json(&item.getattr("location")?)?.into_any(), + location_json(&native_field(&item, "location")?)?.into_any(), ), ], )?; - let message: Bound<'py, PyAny> = item.getattr("message")?; + let message: Bound<'py, PyAny> = native_field(&item, "message")?; if !message.is_none() { entry.set_item("message", message)?; } @@ -532,7 +539,7 @@ fn diagnostic_json<'py>(diagnostic: &Bound<'py, PyAny>) -> PyResult = diagnostic.getattr("help")?; + let help: Bound<'py, PyAny> = native_field(diagnostic, "help")?; if !help.is_none() { payload.set_item("help", help)?; } @@ -550,7 +557,8 @@ fn diagnostic_json<'py>(diagnostic: &Bound<'py, PyAny>) -> PyResult, resource: &Bound<'_, PyAny>) -> PyResult<()> { - let name: Bound<'_, PyAny> = resource.getattr("destination")?.getattr("qualified_name")?; + let name: Bound<'_, PyAny> = + native_field(resource, "destination")?.getattr("qualified_name")?; if !name.is_none() { item.set_item("qualified_name", name)?; } @@ -562,7 +570,7 @@ fn lineage_summary<'py>( model: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = model.py(); - let name: Bound<'py, PyAny> = model.getattr("name")?; + let name: Bound<'py, PyAny> = native_field(model, "name")?; if lineage.is_none() || !lineage.call_method1("has_model", (&name,))?.is_truthy()? { return dict(py, vec![("available", any(py, false)?)]); } @@ -570,7 +578,7 @@ fn lineage_summary<'py>( py, vec![ ("available", any(py, true)?), - ("column_count", model.getattr("output_column_count")?), + ("column_count", native_field(model, "output_column_count")?), ( "edge_count", lineage.call_method1("edge_count_targeting", (&name,))?, @@ -588,28 +596,31 @@ fn model_resource<'py>( model: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = model.py(); - let key: Bound<'py, PyAny> = model.getattr("key")?; + let key: Bound<'py, PyAny> = native_field(model, "key")?; let mut depends_on: Vec> = Vec::new(); for upstream in inputs .graph .getattr("native")? .call_method1( "upstream", - ((key.getattr("resource_type")?.str()?, key.getattr("name")?),), + (( + native_field(&key, "resource_type")?.str()?, + native_field(&key, "name")?, + ),), )? .try_iter()? { let (kind, name): (Bound<'py, PyAny>, Bound<'py, PyAny>) = upstream?.extract()?; depends_on.push(dict(py, vec![("resource_type", kind), ("name", name)])?); } - let values: Bound<'py, PyAny> = model.getattr("config")?.getattr("values")?; + let values: Bound<'py, PyAny> = native_field(model, "config")?.getattr("values")?; let item: Bound<'py, PyDict> = dict( py, vec![ - ("name", model.getattr("name")?), + ("name", native_field(model, "name")?), ( "relative_path", - model.getattr("relative_path")?.str()?.into_any(), + native_field(model, "relative_path")?.str()?.into_any(), ), ( "materialized", @@ -618,13 +629,13 @@ fn model_resource<'py>( .str()? .into_any(), ), - ("column_count", model.getattr("output_column_count")?), + ("column_count", native_field(model, "output_column_count")?), ("depends_on", PyList::new(py, depends_on)?.into_any()), ( "lineage", lineage_summary(&inputs.lineage, model)?.into_any(), ), - ("query_sql", model.getattr("query_sql")?), + ("query_sql", native_field(model, "query_sql")?), ], )?; qualified_name(&item, model)?; @@ -643,40 +654,43 @@ fn model_resource<'py>( fn hook_definitions<'py>(project: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = project.py(); let mut definitions: Vec> = Vec::new(); - for hook in project.getattr("sql_hook_files")?.try_iter()? { + for hook in native_field(project, "sql_hook_files")?.try_iter()? { let hook: Bound<'py, PyAny> = hook?; definitions.push(dict( py, vec![ ("type", any(py, "sql")?), - ("name", hook.getattr("name")?), + ("name", native_field(&hook, "name")?), ( "relative_path", - hook.getattr("relative_path")?.call_method0("as_posix")?, + native_field(&hook, "relative_path")?.call_method0("as_posix")?, ), - ("description", hook.getattr("description")?), - ("sql", hook.getattr("sql_body")?), + ("description", native_field(&hook, "description")?), + ("sql", native_field(&hook, "sql_body")?), ], )?); } - let functions: Bound<'py, PyAny> = project.getattr("hook_functions")?; + let functions: Bound<'py, PyAny> = native_field(project, "hook_functions")?; let identities: Bound<'py, PyAny> = host::entry(py, UserCodeHost::BuildHookIdentities)?.call1((&functions,))?; for hook in functions.try_iter()? { let hook: Bound<'py, PyAny> = hook?; - let identity: Bound<'py, PyAny> = identities.get_item(hook.getattr("name")?)?; + let identity: Bound<'py, PyAny> = identities.get_item(native_field(&hook, "name")?)?; definitions.push(dict( py, vec![ ("type", any(py, "python")?), - ("name", hook.getattr("name")?), + ("name", native_field(&hook, "name")?), ( "relative_path", - hook.getattr("relative_path")?.call_method0("as_posix")?, + native_field(&hook, "relative_path")?.call_method0("as_posix")?, ), - ("description", hook.getattr("description")?), - ("definition_hash", identity.getattr("definition_hash")?), - ("version_hash", identity.getattr("version_hash")?), + ("description", native_field(&hook, "description")?), + ( + "definition_hash", + native_field(&identity, "definition_hash")?, + ), + ("version_hash", native_field(&identity, "version_hash")?), ], )?); } @@ -685,28 +699,31 @@ fn hook_definitions<'py>(project: &Bound<'py, PyAny>) -> PyResult(function: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = function.py(); - let return_columns: Bound<'py, PyAny> = function.getattr("return_columns")?; + let return_columns: Bound<'py, PyAny> = native_field(function, "return_columns")?; let mut columns: Vec> = Vec::new(); for column in return_columns.try_iter()? { let column: Bound<'py, PyAny> = column?; columns.push(dict( py, vec![ - ("name", column.getattr("name")?), - ("type", column.getattr("type")?), + ("name", native_field(&column, "name")?), + ("type", native_field(&column, "type")?), ], )?); } let item: Bound<'py, PyDict> = dict( py, vec![ - ("name", function.getattr("name")?), + ("name", native_field(function, "name")?), ( "relative_path", - function.getattr("relative_path")?.str()?.into_any(), + native_field(function, "relative_path")?.str()?.into_any(), + ), + ( + "language", + native_field(function, "language")?.getattr("value")?, ), - ("language", function.getattr("language")?.getattr("value")?), - ("description", function.getattr("description")?), + ("description", native_field(function, "description")?), ( "return_kind", any( @@ -718,7 +735,7 @@ fn function_resource<'py>(function: &Bound<'py, PyAny>) -> PyResult(audit: &Bound<'py, PyAny>) -> PyResult let item: Bound<'py, PyDict> = dict( py, vec![ - ("name", audit.getattr("name")?), + ("name", native_field(audit, "name")?), ( "relative_path", audit @@ -742,14 +759,17 @@ fn audit_resource<'py>(audit: &Bound<'py, PyAny>) -> PyResult ), ], )?; - let kind: Bound<'py, PyAny> = audit.getattr("attached_target_kind")?; - let target: Bound<'py, PyAny> = audit.getattr("attached_target_name")?; + let kind: Bound<'py, PyAny> = native_field(audit, "attached_target_kind")?; + let target: Bound<'py, PyAny> = native_field(audit, "attached_target_name")?; if !kind.is_none() && !target.is_none() { let attached: Bound<'py, PyDict> = dict( py, - vec![("resource_type", kind.getattr("value")?), ("name", target)], + vec![ + ("resource_type", native_field(&kind, "value")?), + ("name", target), + ], )?; - let column: Bound<'py, PyAny> = audit.getattr("attached_column_name")?; + let column: Bound<'py, PyAny> = native_field(audit, "attached_column_name")?; if !column.is_none() { attached.set_item("column_name", column)?; } @@ -760,7 +780,7 @@ fn audit_resource<'py>(audit: &Bound<'py, PyAny>) -> PyResult fn test_resource<'py>(test: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = test.py(); - let payload: Bound<'py, PyAny> = test.getattr("payload")?; + let payload: Bound<'py, PyAny> = native_field(test, "payload")?; let expected_models: Bound<'py, PyList> = if payload.is_instance(®istry::class(py, RecordId::CompiledModelSqlTestPayload)?)? { PyList::new( @@ -773,16 +793,17 @@ fn test_resource<'py>(test: &Bound<'py, PyAny>) -> PyResult> } else { PyList::empty(py) }; - let test_file_path: Bound<'py, PyAny> = test.getattr("test_file")?.getattr("relative_path")?; + let test_file_path: Bound<'py, PyAny> = + native_field(test, "test_file")?.getattr("relative_path")?; let item: Bound<'py, PyDict> = dict( py, vec![ - ("name", test.getattr("name")?), + ("name", native_field(test, "name")?), ("relative_path", test_file_path.str()?.into_any()), ("expected_models", expected_models.into_any()), ], )?; - let case_name: Bound<'py, PyAny> = test.getattr("case_name")?; + let case_name: Bound<'py, PyAny> = native_field(test, "case_name")?; if case_name.is_none() { return Ok(item); } @@ -793,24 +814,27 @@ fn test_resource<'py>(test: &Bound<'py, PyAny>) -> PyResult> let types: Bound<'py, PyDict> = PyDict::new(py); let mut parameter_schema: Vec> = Vec::new(); for parameter in &schema { - let kind: Bound<'py, PyAny> = parameter.getattr("value_type")?.getattr("value")?; - types.set_item(parameter.getattr("name")?, &kind)?; + let kind: Bound<'py, PyAny> = native_field(parameter, "value_type")?.getattr("value")?; + types.set_item(native_field(parameter, "name")?, &kind)?; } for parameter in &schema { parameter_schema.push(dict( py, vec![ - ("name", parameter.getattr("name")?), - ("type", parameter.getattr("value_type")?.getattr("value")?), - ("nullable", parameter.getattr("nullable")?), + ("name", native_field(parameter, "name")?), + ( + "type", + native_field(parameter, "value_type")?.getattr("value")?, + ), + ("nullable", native_field(parameter, "nullable")?), ], )?); } let mut parameters: Vec> = Vec::new(); - for pair in test.getattr("parameter_values")?.try_iter()? { + for pair in native_field(test, "parameter_values")?.try_iter()? { let (name, value): (Bound<'py, PyAny>, Bound<'py, PyAny>) = pair?.extract()?; - let inner: Bound<'py, PyAny> = value.getattr("value")?; - let shown: Bound<'py, PyAny> = if value.getattr("kind")?.eq(DECIMAL_KIND)? { + let inner: Bound<'py, PyAny> = native_field(&value, "value")?; + let shown: Bound<'py, PyAny> = if native_field(&value, "kind")?.eq(DECIMAL_KIND)? { inner.str()?.into_any() } else { inner @@ -821,7 +845,7 @@ fn test_resource<'py>(test: &Bound<'py, PyAny>) -> PyResult> vec![("name", name), ("type", kind), ("value", shown)], )?); } - let source_path: Bound<'py, PyAny> = test.getattr("source_path")?; + let source_path: Bound<'py, PyAny> = native_field(test, "source_path")?; let source_path: Bound<'py, PyAny> = if source_path.is_truthy()? { source_path } else { @@ -829,11 +853,11 @@ fn test_resource<'py>(test: &Bound<'py, PyAny>) -> PyResult> }; for (key, value) in [ ("source_path", source_path.call_method0("as_posix")?), - ("block_index", test.getattr("block_index")?), - ("parent_name", test.getattr("parent_name")?), + ("block_index", native_field(test, "block_index")?), + ("parent_name", native_field(test, "parent_name")?), ("case_name", case_name), - ("case_index", test.getattr("case_index")?), - ("case_fingerprint", test.getattr("case_fingerprint")?), + ("case_index", native_field(test, "case_index")?), + ("case_fingerprint", native_field(test, "case_fingerprint")?), ( "parameter_schema", PyList::new(py, parameter_schema)?.into_any(), @@ -864,7 +888,7 @@ fn source_resource<'py>(source: &Bound<'py, PyAny>) -> PyResult(source: &Bound<'py, PyAny>) -> PyResult(seed: &Bound<'py, PyAny>) -> PyResult> let item: Bound<'py, PyDict> = dict( py, vec![ - ("name", seed.getattr("name")?), + ("name", native_field(seed, "name")?), ( "relative_path", - seed.getattr("seed_file")? + native_field(seed, "seed_file")? .getattr("relative_path")? .str()? .into_any(), ), ( "column_count", - any(py, seed.getattr("schema_entry")?.getattr("columns")?.len()?)?, + any( + py, + native_field(seed, "schema_entry")? + .getattr("columns")? + .len()?, + )?, ), ], )?; @@ -921,7 +952,7 @@ pub(crate) fn format_compile_json( let mut has_errors: bool = false; let mut diagnostics: Vec> = Vec::new(); for diagnostic in &inputs.diagnostics { - has_errors = has_errors || diagnostic.getattr("is_error")?.is_truthy()?; + 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() }; @@ -1033,9 +1064,9 @@ pub(crate) fn format_compile_error_json<'py>( (location.clone(), location.clone(), location.clone()) } else { ( - location.getattr("path")?, - location.getattr("line")?, - location.getattr("column")?, + native_field(&location, "path")?, + native_field(&location, "line")?, + native_field(&location, "column")?, ) }; let diagnostic: Bound<'py, PyAny> = call_with( 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 134f294821..10966ebdcc 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 @@ -38,6 +38,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::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; @@ -143,7 +144,7 @@ fn launch_payload<'py>( Bound<'py, PyAny>, Bound<'py, PyAny>, ) = native.call_method0("launch_identity")?.extract()?; - let project_dir: Bound<'py, PyAny> = attempt.getattr("project_dir")?; + let project_dir: Bound<'py, PyAny> = native_field(attempt, "project_dir")?; let resolved: Bound<'py, PyAny> = project_dir.call_method0("resolve")?; let capture: Bound<'py, PyAny> = if py .import("sys")? @@ -157,7 +158,7 @@ fn launch_payload<'py>( capture.set_item("root", logs.str()?)?; capture.set_item("command", COMPILE_COMMAND_NAME)?; capture.set_item("project_dir", resolved.str()?)?; - capture.set_item("target", attempt.getattr("selected_target")?)?; + capture.set_item("target", native_field(attempt, "selected_target")?)?; capture.into_any() } else { py.None().into_bound(py) @@ -184,7 +185,7 @@ fn launch_payload<'py>( "startup_inputs", host::entry(py, FacadeHost::StartupInputs)?.call0()?, )?; - payload.set_item("json_output", attempt.getattr("json_output")?)?; + payload.set_item("json_output", native_field(attempt, "json_output")?)?; payload.set_item("rules", reuse_rules(py, false)?)?; payload.set_item("capture", capture)?; payload.set_item("hit_message", PyList::new(py, REUSE_HIT_MESSAGE)?)?; @@ -197,7 +198,7 @@ fn write_record( native: &Bound<'_, PyAny>, path: &Bound<'_, PyAny>, ) -> PyResult<()> { - let payload: Bound<'_, PyDict> = launch_payload(attempt, native, path.getattr("stem")?)?; + let payload: Bound<'_, PyDict> = launch_payload(attempt, native, native_field(path, "stem")?)?; let text: String = plain_json(payload.as_any())?; let path: PathBuf = path.extract()?; write_record_file(&path, &text).map_err(PyErr::from) @@ -207,7 +208,7 @@ fn write_record( pub(crate) fn write_launch_record(attempt: &Bound<'_, PyAny>) -> PyResult<()> { let py: Python<'_> = attempt.py(); let path: Bound<'_, PyAny> = launch_record_path(py)?; - let native: Bound<'_, PyAny> = attempt.getattr("native")?; + let native: Bound<'_, PyAny> = native_field(attempt, "native")?; if path.is_none() || native.is_none() { return Ok(()); } @@ -215,7 +216,7 @@ pub(crate) fn write_launch_record(attempt: &Bound<'_, PyAny>) -> PyResult<()> { if environment.contains(INVOCATION_CONTEXT_ENV)? { return Ok(()); } - let mut directory: Bound<'_, PyAny> = attempt.getattr("project_dir")?; + let mut directory: Bound<'_, PyAny> = native_field(attempt, "project_dir")?; for part in LAUNCH_RECORD_DIRECTORY { directory = directory.call_method1("__truediv__", (part,))?; } @@ -269,7 +270,7 @@ fn reuse_check<'py>( cache_directory(&imported(py, "pathlib", "Path")?.call1((project_dir,))?)? .call_method1("__truediv__", (NATIVE_REUSE_DIRECTORY_NAME,))?, )?; - check.set_item("selected_target", request.getattr("selected_target")?)?; + check.set_item("selected_target", native_field(request, "selected_target")?)?; check.set_item( "invocation", invocation_identity(request, project_dir, use_color)?, @@ -280,7 +281,7 @@ fn reuse_check<'py>( "bypass_requested", flag("no_cache")? || flag("manifest")? || flag("profiling")? || flag("debug")?, )?; - check.set_item("json_output", request.getattr("json_output")?)?; + check.set_item("json_output", native_field(request, "json_output")?)?; check.set_item("rules", reuse_rules(py, true)?)?; Ok(check) } @@ -374,7 +375,7 @@ fn replayed<'py>( fn attempt_compile_reuse<'py>(request: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = request.py(); let started: f64 = monotonic(py)?; - let requested: Bound<'py, PyAny> = request.getattr("project_dir")?; + let requested: Bound<'py, PyAny> = native_field(request, "project_dir")?; let project_dir: Bound<'py, PyAny> = imported(py, "os.path", "abspath")?.call1((if requested.is_none() { imported(py, "os", "getcwd")?.call0()? @@ -384,14 +385,14 @@ fn attempt_compile_reuse<'py>(request: &Bound<'py, PyAny>) -> PyResult = registry::native(py, NativeAttr::CheckCompileReuse)? .call1((reuse_check(request, &project_dir)?,))?; let checked: String = native.call_method0("outcome")?.extract()?; - let json_output: Bound<'py, PyAny> = request.getattr("json_output")?; + let json_output: Bound<'py, PyAny> = native_field(request, "json_output")?; let mut base: Vec<(&'static str, Bound<'py, PyAny>)> = vec![ ( "project_dir", imported(py, "pathlib", "Path")?.call1((&project_dir,))?, ), ("started", started.into_bound_py_any(py)?), - ("selected_target", request.getattr("selected_target")?), + ("selected_target", native_field(request, "selected_target")?), ("json_output", json_output.clone()), ]; if checked == CHECKED_BYPASS { @@ -423,7 +424,7 @@ fn attempt_compile_reuse<'py>(request: &Bound<'py, PyAny>) -> PyResult = replayed(&native, base, (started, json_output.is_truthy()?))?; - if attempt.getattr("outcome")?.eq(outcome(py, HIT)?)? { + if native_field(&attempt, "outcome")?.eq(outcome(py, HIT)?)? { write_launch_record(&attempt)?; } Ok(attempt) @@ -432,7 +433,7 @@ fn attempt_compile_reuse<'py>(request: &Bound<'py, PyAny>) -> PyResult(request: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = request.py(); - let flags: Bound<'py, PyAny> = request.getattr("profile_flags")?; + let flags: Bound<'py, PyAny> = native_field(request, "profile_flags")?; let mut profiling: bool = false; for name in [ "skip_discovery_sql_analysis", @@ -476,8 +477,8 @@ fn reuse_request<'py>(request: &Bound<'py, PyAny>) -> PyResult 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> = attempt.getattr("exit_code")?; - if attempt.getattr("outcome")?.eq(outcome(py, HIT)?)? && !exit_code.is_none() { + 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)) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_command/reuse_recording.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_command/reuse_recording.rs index 558889c3e9..5fb160c2eb 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_command/reuse_recording.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_command/reuse_recording.rs @@ -21,6 +21,7 @@ use crate::bindings::_helpers::compile_outputs::reuse::artifact_digest; 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::fields::field as native_field; const MISS_OUTCOME: &str = "miss"; const HIT_OUTCOME: &str = "hit"; @@ -261,7 +262,7 @@ pub(crate) fn compile_reuse_timings<'py>( attempt: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = attempt.py(); - let outcome: String = attempt.getattr("outcome")?.str()?.to_string(); + let outcome: String = native_field(attempt, "outcome")?.str()?.to_string(); let timings: Bound<'py, PyDict> = PyDict::new(py); for (name, counted) in [ (REUSE_HIT_TIMING, HIT_OUTCOME), @@ -270,7 +271,7 @@ pub(crate) fn compile_reuse_timings<'py>( ] { timings.set_item(name, i64::from(outcome == counted))?; } - timings.set_item(REUSE_CHECK_TIMING, attempt.getattr("check_ms")?)?; + timings.set_item(REUSE_CHECK_TIMING, native_field(attempt, "check_ms")?)?; Ok(timings) } @@ -352,18 +353,18 @@ fn record<'py>(facts: &CompileFacts<'py>) -> PyResult { &host::entry(py, FacadeHost::ProviderSettingsInputs)?, &[( "settings_classes", - facts.input_reads.getattr("settings_classes")?, + native_field(&facts.input_reads, "settings_classes")?, )], )?; - let unsupported: Bound<'_, PyAny> = settings.getattr("unsupported_reason")?; + let unsupported: Bound<'_, PyAny> = native_field(&settings, "unsupported_reason")?; if !unsupported.is_none() { let _ = reuse_logger(py)?.call_method1( "debug", ("Compile reuse is off for this project: %s", &unsupported), )?; } - let read_run_id: Bound<'_, PyAny> = facts.input_reads.getattr("read_run_id")?; - let stdout: Bound<'_, PyAny> = facts.output.getattr("stdout")?; + let read_run_id: Bound<'_, PyAny> = native_field(&facts.input_reads, "read_run_id")?; + let stdout: Bound<'_, PyAny> = native_field(&facts.output, "stdout")?; let eligible: bool = !stdout.is_none() && facts.compile_cache_enabled && !read_run_id.is_truthy()? @@ -378,14 +379,14 @@ fn record<'py>(facts: &CompileFacts<'py>) -> PyResult { let list = |value: Bound<'py, PyAny>| -> PyResult> { Ok(PyList::new(py, value.try_iter()?.collect::>>()?)?.into_any()) }; - for item in settings.getattr("inputs")?.try_iter()? { + for item in native_field(&settings, "inputs")?.try_iter()? { let item: Bound<'_, PyAny> = item?; rows.append(( - item.getattr("case_sensitive")?, - list(item.getattr("names")?)?, - list(item.getattr("prefixes")?)?, - list(item.getattr("env_files")?)?, - list(item.getattr("secrets_dirs")?)?, + native_field(&item, "case_sensitive")?, + list(native_field(&item, "names")?)?, + list(native_field(&item, "prefixes")?)?, + list(native_field(&item, "env_files")?)?, + list(native_field(&item, "secrets_dirs")?)?, ))?; } rows.into_any() @@ -433,7 +434,10 @@ fn record<'py>(facts: &CompileFacts<'py>) -> PyResult { .into_any(), ), ("module_paths", module_paths(py)?.into_any()), - ("artifacts", facts.artifact_writes.getattr("artifacts")?), + ( + "artifacts", + native_field(&facts.artifact_writes, "artifacts")?, + ), ( "artifacts_written", facts.artifacts_written.into_bound_py_any(py)?, @@ -488,9 +492,12 @@ pub(crate) fn write_reusable_compile( ), )?; let attempt: Bound<'_, PyAny> = arguments.value("attempt")?; - let entry_path: Bound<'_, PyAny> = attempt.getattr("entry_path")?; - let native: Bound<'_, PyAny> = attempt.getattr("native")?; - if !attempt.getattr("outcome")?.eq(MISS_OUTCOME)? || entry_path.is_none() || native.is_none() { + let entry_path: Bound<'_, PyAny> = native_field(&attempt, "entry_path")?; + let native: Bound<'_, PyAny> = native_field(&attempt, "native")?; + if !native_field(&attempt, "outcome")?.eq(MISS_OUTCOME)? + || entry_path.is_none() + || native.is_none() + { return Ok(()); } let facts: CompileFacts<'_> = CompileFacts { 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 4a0af16010..bc53cdfc21 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 @@ -6,6 +6,7 @@ use pyo3::{IntoPyObjectExt, pyfunction, wrap_pyfunction}; use sqlbuild_cli::replay::main::output_file_identities::output_file_identities; 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; @@ -28,10 +29,10 @@ const REQUEST_CHOICES: [&str; 9] = [ pub(crate) fn interpreter_values(py: Python<'_>) -> PyResult<[String; 4]> { let sys: Bound<'_, PyModule> = py.import("sys")?; Ok([ - sys.getattr("version")?.extract()?, - sys.getattr("executable")?.extract()?, - sys.getattr("platform")?.extract()?, - sys.getattr("implementation")? + native_field(&sys, "version")?.extract()?, + native_field(&sys, "executable")?.extract()?, + native_field(&sys, "platform")?.extract()?, + native_field(&sys, "implementation")? .getattr("cache_tag")? .str()? .extract()?, @@ -73,7 +74,7 @@ pub(crate) fn invocation_identity<'py>( let py: Python<'py> = request.py(); let payload: Bound<'py, PyDict> = PyDict::new(py); payload.set_item("project_dir", project_dir)?; - let requested: Bound<'py, PyAny> = request.getattr("project_dir")?; + let requested: Bound<'py, PyAny> = native_field(request, "project_dir")?; let requested: Bound<'py, PyAny> = if requested.is_none() { requested } else { @@ -85,8 +86,8 @@ pub(crate) fn invocation_identity<'py>( } payload.set_item("cwd", py.import("os")?.getattr("getcwd")?.call0()?)?; payload.set_item("use_color", use_color)?; - payload.set_item("select", listed(request.getattr("select")?)?)?; - payload.set_item("exclude", listed(request.getattr("exclude")?)?)?; + payload.set_item("select", listed(native_field(request, "select")?)?)?; + payload.set_item("exclude", listed(native_field(request, "exclude")?)?)?; payload.set_item("sys_path", listed(py.import("sys")?.getattr("path")?)?)?; let options: Bound<'py, PyDict> = keywords( py, 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 60946e133f..9bd1f35ce2 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; @@ -70,7 +71,7 @@ fn path_class(py: Python<'_>) -> PyResult> { /// `relative_path` under `models/`, unless it already starts there. fn model_output_path<'py>(relative_path: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = relative_path.py(); - let parts: Bound<'py, PyTuple> = relative_path.getattr("parts")?.downcast_into()?; + let parts: Bound<'py, PyTuple> = native_field(relative_path, "parts")?.downcast_into()?; if !parts.is_empty() && parts.get_item(0)?.eq(MODELS_DIR)? { return path_class(py)?.call1(parts); } @@ -83,8 +84,8 @@ fn function_output_path<'py>( language: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = relative_path.py(); - let parts: Bound<'py, PyTuple> = relative_path.getattr("parts")?.downcast_into()?; - let language_dir: Bound<'py, PyAny> = language.getattr("value")?; + let parts: Bound<'py, PyTuple> = native_field(relative_path, "parts")?.downcast_into()?; + let language_dir: Bound<'py, PyAny> = native_field(language, "value")?; let path: Bound<'py, PyAny> = if parts.len() >= LANGUAGE_DIR_DEPTH && parts.get_item(0)?.eq(FUNCTIONS_DIR)? && parts.get_item(1)?.eq(&language_dir)? @@ -102,9 +103,9 @@ fn function_output_path<'py>( /// The audit's folder and file name: generic audits under their target, others singular. fn audit_output_path<'py>(audit: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = audit.py(); - let target: Bound<'py, PyAny> = audit.getattr("attached_target_name")?; - let column: Bound<'py, PyAny> = audit.getattr("attached_column_name")?; - let name: String = audit.getattr("name")?.str()?.to_string(); + let target: Bound<'py, PyAny> = native_field(audit, "attached_target_name")?; + let column: Bound<'py, PyAny> = native_field(audit, "attached_column_name")?; + let name: String = native_field(audit, "name")?.str()?.to_string(); let folder: Bound<'py, PyAny> = if target.is_none() { path_class(py)?.call1((SINGULAR_DIR,))? } else { @@ -123,7 +124,7 @@ fn function_sql(adapter: &Bound<'_, PyAny>, function: &Bound<'_, PyAny>) -> PyRe let py: Python<'_> = adapter.py(); let mut arguments: Vec<(&str, Bound<'_, PyAny>)> = vec![( "destination", - function.getattr("destination")?.getattr("qualified_name")?, + native_field(function, "destination")?.getattr("qualified_name")?, )]; for field in [ "arguments", @@ -230,15 +231,18 @@ fn write_planned_target<'py>( let mut batch: ArtifactBatch = ArtifactBatch::new(remove_stale_files, None); let mut managed: Vec> = Vec::new(); let compiled: Bound<'py, PyAny> = joined(target_dir, &[COMPILED_DIR])?; - for entry in plan_output.getattr("model_entries")?.try_iter()? { + for entry in native_field(plan_output, "model_entries")?.try_iter()? { let entry: Bound<'py, PyAny> = entry?; let path: Bound<'py, PyAny> = - compiled.div(model_output_path(&entry.getattr("relative_path")?)?)?; - batch.queue_sql(&path, &entry.getattr("resolved_sql")?.extract::()?)?; + compiled.div(model_output_path(&native_field(&entry, "relative_path")?)?)?; + batch.queue_sql( + &path, + &native_field(&entry, "resolved_sql")?.extract::()?, + )?; managed.push(path); } batch.flush(py)?; - for entry in plan_output.getattr("function_entries")?.try_iter()? { + for entry in native_field(plan_output, "function_entries")?.try_iter()? { let entry: Bound<'py, PyAny> = entry?; if entry .getattr("destination")? @@ -249,21 +253,24 @@ fn write_planned_target<'py>( } let sql: String = function_sql(adapter, &entry)?; let path: Bound<'py, PyAny> = compiled.div(function_output_path( - &entry.getattr("relative_path")?, - &entry.getattr("language")?, + &native_field(&entry, "relative_path")?, + &native_field(&entry, "language")?, )?)?; batch.queue_sql(&path, &sql)?; managed.push(path); } batch.flush(py)?; - for entry in plan_output.getattr("audit_entries")?.try_iter()? { + for entry in native_field(plan_output, "audit_entries")?.try_iter()? { let entry: Bound<'py, PyAny> = entry?; let path: Bound<'py, PyAny> = compiled.div(AUDITS_DIR)?.div(audit_output_path(&entry)?)?; - batch.queue_sql(&path, &entry.getattr("resolved_sql")?.extract::()?)?; + batch.queue_sql( + &path, + &native_field(&entry, "resolved_sql")?.extract::()?, + )?; managed.push(path); } batch.flush(py)?; - for entry in plan_output.getattr("test_entries")?.try_iter()? { + for entry in native_field(plan_output, "test_entries")?.try_iter()? { let entry: Bound<'py, PyAny> = entry?; let path: Bound<'py, PyAny> = compiled .div(TESTS_DIR)? @@ -361,7 +368,7 @@ fn planned_artifacts<'py>( ("adapter", adapter.clone()), ( "sql_analysis_enabled", - project.getattr("settings")?.getattr("sql_analysis")?, + native_field(project, "settings")?.getattr("sql_analysis")?, ), ], )? @@ -371,7 +378,7 @@ fn planned_artifacts<'py>( let metadata: Bound<'py, PyDict> = PyDict::new(py); metadata.set_item("item_count", artifacts.len())?; let _ = call_with( - &entered.getattr("completed")?, + &native_field(entered, "completed")?, &[("metadata", metadata.into_any())], )?; Ok(artifacts) @@ -387,9 +394,9 @@ fn plan_tests<'py>( let py: Python<'py> = project.py(); let tests_root: Bound<'py, PyAny> = joined(target_dir, &[COMPILED_DIR, TESTS_DIR])?; let tests_root_path: PathBuf = tests_root.extract()?; - let cache_dir: Bound<'py, PyAny> = project.getattr("compile_cache_dir")?; + let cache_dir: Bound<'py, PyAny> = native_field(project, "compile_cache_dir")?; let cached: HashMap = read_cache(&cache_dir)?; - let tests: Bound<'py, PyAny> = project.getattr("sql_tests")?; + let tests: Bound<'py, PyAny> = native_field(project, "sql_tests")?; let caching: Option<(IdentityContext<'py>, Bound<'py, PyAny>)> = if cache_dir.is_none() { None } else { @@ -412,7 +419,7 @@ fn plan_tests<'py>( }; let record_key: String = artifact_record_key(&test)?; let chain: Bound<'py, PyAny> = - chains.call_method1("get", (test.getattr("key")?, &empty))?; + chains.call_method1("get", (native_field(&test, "key")?, &empty))?; let identity: String = artifact_identity(&test, &chain, context)?; let reusable: Option = cached .get(&record_key) @@ -474,7 +481,7 @@ fn artifact_diagnostics<'py>( ) -> PyResult>> { let py: Python<'py> = test.py(); let mut diagnostics: Vec> = Vec::new(); - for message in artifact.getattr("error_messages")?.try_iter()? { + for message in native_field(artifact, "error_messages")?.try_iter()? { diagnostics.push(call_with( ®istry::class(py, RecordId::CompilerDiagnostic)?, &[ @@ -486,8 +493,8 @@ fn artifact_diagnostics<'py>( ), ("message", message?), ("resource_type", "sql_test".into_bound_py_any(py)?), - ("resource_name", test.getattr("name")?), - ("path", test.getattr("source_path")?), + ("resource_name", native_field(test, "name")?), + ("path", native_field(test, "source_path")?), ], )?); } @@ -518,9 +525,9 @@ fn write_static_tests<'py>( let artifact: &Bound<'py, PyAny> = artifact.bind(py); let path: Bound<'py, PyAny> = tests_root.div(compiled_sql_test_path( test.bind(py), - &artifact.getattr("model_names")?, + &native_field(artifact, "model_names")?, )?)?; - batch.queue_sql(&path, &artifact.getattr("sql")?.extract::()?)?; + batch.queue_sql(&path, &native_field(artifact, "sql")?.extract::()?)?; managed.push(path.clone()); test_paths.push(path); } @@ -541,7 +548,7 @@ fn write_static_tests<'py>( } } record_compile_timing("cache_publication_ms", || { - write_cache(&project.getattr("compile_cache_dir")?, &records) + write_cache(&native_field(project, "compile_cache_dir")?, &records) })?; Ok((managed, diagnostics)) } @@ -558,15 +565,18 @@ pub(crate) fn write_static_target<'py>( let mut batch: ArtifactBatch = ArtifactBatch::new(remove_stale_files, prepared.recorder); let mut managed: Vec> = Vec::new(); let compiled: Bound<'py, PyAny> = joined(target_dir, &[COMPILED_DIR])?; - for model in project.getattr("models")?.try_iter()? { + for model in native_field(project, "models")?.try_iter()? { let model: Bound<'py, PyAny> = model?; let path: Bound<'py, PyAny> = - compiled.div(model_output_path(&model.getattr("relative_path")?)?)?; - batch.queue_sql(&path, &model.getattr("query_sql")?.extract::()?)?; + compiled.div(model_output_path(&native_field(&model, "relative_path")?)?)?; + batch.queue_sql( + &path, + &native_field(&model, "query_sql")?.extract::()?, + )?; managed.push(path); } batch.flush(py)?; - for function in project.getattr("functions")?.try_iter()? { + for function in native_field(project, "functions")?.try_iter()? { let function: Bound<'py, PyAny> = function?; if function .getattr("destination")? @@ -577,17 +587,20 @@ pub(crate) fn write_static_target<'py>( } let sql: String = function_sql(adapter, &function)?; let path: Bound<'py, PyAny> = compiled.div(function_output_path( - &function.getattr("relative_path")?, - &function.getattr("language")?, + &native_field(&function, "relative_path")?, + &native_field(&function, "language")?, )?)?; batch.queue_sql(&path, &sql)?; managed.push(path); } batch.flush(py)?; - for audit in project.getattr("audits")?.try_iter()? { + for audit in native_field(project, "audits")?.try_iter()? { let audit: Bound<'py, PyAny> = audit?; let path: Bound<'py, PyAny> = compiled.div(AUDITS_DIR)?.div(audit_output_path(&audit)?)?; - batch.queue_sql(&path, &audit.getattr("sql_body")?.extract::()?)?; + batch.queue_sql( + &path, + &native_field(&audit, "sql_body")?.extract::()?, + )?; managed.push(path); } batch.flush(py)?; @@ -635,7 +648,8 @@ pub(crate) fn publish_staged_target<'py>( recorder: ArtifactRecorder<'_>, ) -> PyResult> { let py: Python<'py> = target_dir.py(); - let staged_dir: Bound<'py, PyAny> = joined(&prepared.getattr("target_dir")?, &[COMPILED_DIR])?; + let staged_dir: Bound<'py, PyAny> = + joined(&native_field(prepared, "target_dir")?, &[COMPILED_DIR])?; let compiled_dir: Bound<'py, PyAny> = joined(target_dir, &[COMPILED_DIR])?; let existed: bool = compiled_dir.call_method0("is_dir")?.is_truthy()?; let expected: Vec = py @@ -673,13 +687,13 @@ pub(crate) fn publish_staged_target<'py>( written_target( target_dir, [ - prepared.getattr("model_count")?.extract()?, - prepared.getattr("seed_count")?.extract()?, - prepared.getattr("function_count")?.extract()?, - prepared.getattr("audit_count")?.extract()?, - prepared.getattr("test_count")?.extract()?, + native_field(prepared, "model_count")?.extract()?, + native_field(prepared, "seed_count")?.extract()?, + native_field(prepared, "function_count")?.extract()?, + native_field(prepared, "audit_count")?.extract()?, + native_field(prepared, "test_count")?.extract()?, ], - prepared.getattr("diagnostics")?, + native_field(prepared, "diagnostics")?, ) } 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 4e8502b286..2c83c893ca 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 @@ -18,6 +18,7 @@ use crate::bindings::_helpers::analysis::sql_test_comparisons::single_comparison use crate::bindings::_helpers::compile_driver::python_calls::imported; 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 RUN_DIR: &str = "run"; const TESTS_DIR: &str = "tests"; @@ -43,9 +44,9 @@ 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()?, - posix(&value.getattr("source_path")?)?, - value.getattr("block_index")?.extract()?, - value.getattr("case_name")?.extract()?, + posix(&native_field(value, "source_path")?)?, + native_field(value, "block_index")?.extract()?, + native_field(value, "case_name")?.extract()?, )) } @@ -63,8 +64,8 @@ fn path_facts(test: &Bound<'_, PyAny>, model_names: Vec) -> PyResult(entry: &Bound<'py, PyAny>) -> PyResult> { let mut model_names: Vec = Vec::new(); - for step in entry.getattr("chain")?.try_iter()? { - model_names.push(step?.getattr("model_name")?.extract()?); + for step in native_field(entry, "chain")?.try_iter()? { + model_names.push(native_field(&step?, "model_name")?.extract()?); } path_object( entry.py(), @@ -88,8 +89,7 @@ fn write_text(path: &Bound<'_, PyAny>, text: &str) -> PyResult<()> { let mkdir: Bound<'_, PyDict> = PyDict::new(py); mkdir.set_item("parents", true)?; mkdir.set_item("exist_ok", true)?; - path.getattr("parent")? - .call_method("mkdir", (), Some(&mkdir))?; + native_field(path, "parent")?.call_method("mkdir", (), Some(&mkdir))?; let options: Bound<'_, PyDict> = PyDict::new(py); options.set_item("encoding", "utf-8")?; path.call_method("write_text", (text,), Some(&options))?; @@ -100,10 +100,10 @@ fn sql_statements(result: &Bound<'_, PyAny>) -> PyResult> { let sql_kind: Bound<'_, PyAny> = host::entry(result.py(), AdapterHost::LifeCycleEventKind)?.getattr("SQL")?; let mut statements: Vec = Vec::new(); - for event in result.getattr("lifecycle_events")?.try_iter()? { + for event in native_field(result, "lifecycle_events")?.try_iter()? { let event: Bound<'_, PyAny> = event?; - if event.getattr("kind")?.eq(&sql_kind)? { - statements.push(event.getattr("content")?.extract()?); + if native_field(&event, "kind")?.eq(&sql_kind)? { + statements.push(native_field(&event, "content")?.extract()?); } } Ok(statements) @@ -116,7 +116,7 @@ fn entry_paths( let mut paths: HashMap = HashMap::new(); for entry in entries.try_iter()? { let entry: Bound<'_, PyAny> = entry?; - paths.insert(entry.getattr("name")?.extract()?, path_of(&entry)?); + paths.insert(native_field(&entry, "name")?.extract()?, path_of(&entry)?); } Ok(paths) } @@ -135,11 +135,12 @@ pub(crate) fn write_runtime_target( result: &Bound<'_, PyAny>, ) -> PyResult<()> { let run_dir: Bound<'_, PyAny> = target_dir.div(RUN_DIR)?; - let models: HashMap = entry_paths(&plan.getattr("model_entries")?, |entry| { - relative_posix(entry).map(|path| model_run_path(&path)) - })?; + let models: HashMap = + entry_paths(&native_field(plan, "model_entries")?, |entry| { + relative_posix(entry).map(|path| model_run_path(&path)) + })?; let functions: HashMap = - entry_paths(&plan.getattr("function_entries")?, |entry| { + entry_paths(&native_field(plan, "function_entries")?, |entry| { Ok(function_run_path( &relative_posix(entry)?, &entry @@ -178,7 +179,7 @@ pub(crate) fn write_test_runtime_target( executed.insert(test_key(&result?, "test_name")?); } let tests_dir: Bound<'_, PyAny> = target_dir.div(RUN_DIR)?.div(TESTS_DIR)?; - for entry in plan.getattr("test_entries")?.try_iter()? { + for entry in native_field(plan, "test_entries")?.try_iter()? { let entry: Bound<'_, PyAny> = entry?; if !executed.contains(&test_key(&entry, "name")?) { continue; @@ -193,10 +194,10 @@ pub(crate) fn write_test_runtime_target( } fn check_status(result: &Bound<'_, PyAny>) -> PyResult<&'static str> { - if result.getattr("passed")?.is_truthy()? { + if native_field(result, "passed")?.is_truthy()? { return Ok("pass"); } - if result.getattr("warned")?.is_truthy()? { + if native_field(result, "warned")?.is_truthy()? { return Ok("warn"); } Ok("fail") @@ -211,18 +212,21 @@ pub(crate) fn write_python_check_runtime_target( let checks: Bound<'_, PyList> = PyList::empty(py); for result in results.try_iter()? { let result: Bound<'_, PyAny> = result?; - let name: Bound<'_, PyAny> = result.getattr("node_name")?; + let name: Bound<'_, PyAny> = native_field(&result, "node_name")?; let check: Bound<'_, PyDict> = PyDict::new(py); check.set_item("kind", PYTHON_CHECK_KIND)?; check.set_item("name", &name)?; check.set_item("display_name", &name)?; check.set_item("check_id", format!("{PYTHON_CHECK_KIND}:{}", name.str()?))?; check.set_item("status", check_status(&result)?)?; - check.set_item("passed", result.getattr("passed")?)?; - check.set_item("severity", result.getattr("severity")?.getattr("value")?)?; - check.set_item("message", result.getattr("message")?)?; - check.set_item("error_message", result.getattr("error_message")?)?; - check.set_item("metadata", result.getattr("metadata")?)?; + check.set_item("passed", native_field(&result, "passed")?)?; + check.set_item( + "severity", + native_field(&result, "severity")?.getattr("value")?, + )?; + check.set_item("message", native_field(&result, "message")?)?; + check.set_item("error_message", native_field(&result, "error_message")?)?; + check.set_item("metadata", native_field(&result, "metadata")?)?; checks.append(check)?; } if checks.is_empty() { 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 5ac9ebb48a..85c03a5899 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::sql_test_glue::constants::SQL_TEST_CTE_SCAN_ALGORITHM; @@ -65,7 +66,7 @@ fn members_by_value<'py>(enum_class: &Bound<'py, PyAny>) -> PyResult = PyDict::new(enum_class.py()); for member in enum_class.try_iter()? { let member: Bound<'py, PyAny> = member?; - members.set_item(member.getattr("value")?, member)?; + members.set_item(native_field(&member, "value")?, member)?; } Ok(members) } @@ -244,7 +245,7 @@ fn test_block_row(block: Bound<'_, PyAny>) -> PyResult { block.clone().unbind(), attr(&block, "name")?, attr(&block, "sql_body")?, - attr(&block.getattr("mode")?, "value")?, + attr(&native_field(&block, "mode")?, "value")?, items(&block, "parameters")? .iter() .map(|parameter| attr(parameter, "name")) @@ -354,7 +355,10 @@ fn sql_test_settings<'py>( registry::member(py, RecordId::UsageKind, "RUNTIME")?, )?; settings.set_item("render_parameter", parameter_renderer(value_renderer)?)?; - settings.set_item("adapter_name", value_renderer.getattr("adapter_name")?)?; + settings.set_item( + "adapter_name", + native_field(value_renderer, "adapter_name")?, + )?; settings.set_item( "source_location", registry::class(py, RecordId::SourceLocation)?, @@ -362,7 +366,10 @@ fn sql_test_settings<'py>( settings.set_item("scan_store", scan_cache.call_method0("opened")?)?; settings.set_item( "scan_key", - (SQL_TEST_CTE_SCAN_ALGORITHM, syntax.getattr("cache_key")?), + ( + SQL_TEST_CTE_SCAN_ALGORITHM, + native_field(syntax, "cache_key")?, + ), )?; Ok(settings) } @@ -376,7 +383,7 @@ fn audit_block_row(block: Bound<'_, PyAny>) -> PyResult { attr(&block, "name")?, attr(&block, "sql_body")?, attr(&block, "evidence_sql")?, - block.getattr("evaluation_mode")?.eq(measurement)?, + native_field(&block, "evaluation_mode")?.eq(measurement)?, attr(&block, "header_values")?, )) } @@ -389,7 +396,7 @@ fn audit_file_row(file: Bound<'_, PyAny>) -> PyResult { text(&file, "relative_path")?, stem(&file, "file_path")?, file.clone().unbind(), - file.getattr("declaration_kind")?.is(&generic), + native_field(&file, "declaration_kind")?.is(&generic), attr(&file, "contents")?, items(&file, "blocks")? .into_iter() @@ -436,7 +443,7 @@ fn audit_settings<'py>( effective_settings: &Bound<'py, PyAny>, ) -> PyResult> { let settings: Bound<'py, PyDict> = PyDict::new(py); - let severity: Bound<'py, PyAny> = effective_settings.getattr("default_audit_severity")?; + let severity: Bound<'py, PyAny> = native_field(effective_settings, "default_audit_severity")?; settings.set_item( "default_severity", if severity.is_none() { @@ -447,7 +454,7 @@ fn audit_settings<'py>( )?; settings.set_item( "default_run_scope", - effective_settings.getattr("default_audit_run_scope")?, + native_field(effective_settings, "default_audit_run_scope")?, )?; let builtins: Vec = BUILTIN_AUDITS .iter() @@ -477,7 +484,7 @@ fn audit_settings<'py>( fn loader_rows<'py>(discovered: &Bound<'py, PyAny>) -> PyResult> { let mut rows: HashMap = HashMap::new(); for loader in items(discovered, "loader_functions")? { - let relative_path: Bound<'py, PyAny> = loader.getattr("relative_path")?; + let relative_path: Bound<'py, PyAny> = native_field(&loader, "relative_path")?; rows.insert( attr(&loader, "name")?, ( @@ -529,9 +536,9 @@ fn diagnostic_settings<'py>( PyTuple::new( py, [ - scenario.getattr("name")?, + native_field(scenario, "name")?, text(scenario, "relative_path")?.into_bound_py_any(py)?, - scenario.getattr("contents")?, + native_field(scenario, "contents")?, scenario .getattr("header_values")? .call_method1("get", (DESCRIPTION_KEY,))?, @@ -546,9 +553,9 @@ fn diagnostic_settings<'py>( PyTuple::new( py, [ - hook.getattr("name")?, + native_field(hook, "name")?, text(hook, "relative_path")?.into_bound_py_any(py)?, - hook.getattr("contents")?, + native_field(hook, "contents")?, hook.getattr(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 1a953d6662..acca204868 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 @@ -37,6 +37,7 @@ use crate::bindings::_helpers::compiled_project::source_loop::{ use crate::bindings::_helpers::model_config::authored_nodes::PyNode; use crate::bindings::_helpers::model_config::header_metadata::thresholds; use crate::bindings::_helpers::model_config::model_validator::NativeModelValidator; +use crate::bindings::_helpers::records::fields::field as native_field; use sqlbuild_model_config::errors::ConfigError; use sqlbuild_model_config::header_metadata::main::parse_measurement_policy::parse_measurement_policy; @@ -288,7 +289,7 @@ impl<'py> AuditScope<'_, 'py> { ); return Err(compile_error(self.context.classes, &message, None)); } - let severity: &'static str = if thresholds.getattr("error")?.is_none() { + let severity: &'static str = if native_field(&thresholds, "error")?.is_none() { "warn" } else { "error" @@ -352,7 +353,7 @@ impl<'py> AuditScope<'_, 'py> { ) -> PyResult { let py: Python<'py> = self.context.host.py(); let (owner_file, column) = attachment; - let definition_name: String = instance.getattr("definition_name")?.extract()?; + let definition_name: String = native_field(instance, "definition_name")?.extract()?; let Some(definition) = definitions.get(&definition_name) else { let message: String = format!("{owner_file} references unknown generic audit '{definition_name}'"); @@ -369,9 +370,9 @@ impl<'py> AuditScope<'_, 'py> { &consumer, (owner.kind, &owner.name, &owner.path), )?; - let thresholds: Bound<'py, PyAny> = instance.getattr("thresholds")?; - let minimum_samples: Bound<'py, PyAny> = instance.getattr("minimum_samples")?; - let instance_severity: Option = instance.getattr("severity")?.extract()?; + let thresholds: Bound<'py, PyAny> = native_field(instance, "thresholds")?; + let minimum_samples: Bound<'py, PyAny> = native_field(instance, "minimum_samples")?; + let instance_severity: Option = native_field(instance, "severity")?.extract()?; let label: String = format!("{owner_file} audit '{definition_name}'"); let measurement: bool = definition.block.4; if measurement { @@ -407,12 +408,13 @@ impl<'py> AuditScope<'_, 'py> { sql_body: definition.block.2.clone(), evidence_sql: definition.block.3.clone(), implicit_arguments: implicit, - explicit_arguments: argument_pairs(instance.getattr("arguments")?.downcast()?)?, + explicit_arguments: argument_pairs(native_field(instance, "arguments")?.downcast()?)?, has_thresholds: !thresholds.is_none(), - threshold_error: !thresholds.is_none() && !thresholds.getattr("error")?.is_none(), + threshold_error: !thresholds.is_none() + && !native_field(&thresholds, "error")?.is_none(), instance_severity, default_severity: self.settings.default_severity.clone(), - instance_run_scope: instance.getattr("run_scope")?.extract()?, + instance_run_scope: native_field(instance, "run_scope")?.extract()?, default_run_scope: self.settings.default_run_scope.clone(), }); let rendered = match rendering { @@ -438,7 +440,7 @@ impl<'py> AuditScope<'_, 'py> { .policies .map_err(|message| compile_error(self.context.classes, &message, None))?; let run_scope: String = match source { - PolicySource::Instance => instance.getattr("run_scope")?.extract()?, + PolicySource::Instance => native_field(instance, "run_scope")?.extract()?, PolicySource::Default => self.settings.default_run_scope.clone().unwrap_or_default(), PolicySource::Fallback => "delta_and_final".to_owned(), }; @@ -461,7 +463,7 @@ impl<'py> AuditScope<'_, 'py> { evidence_sql: expanded.evidence_sql, thresholds: thresholds.unbind(), minimum_samples: minimum_samples.unbind(), - evidence_limit: instance.getattr("evidence_limit")?.unbind(), + evidence_limit: native_field(instance, "evidence_limit")?.unbind(), references: expanded.references, target: Some(AttachedTarget { kind: owner.kind.to_owned(), @@ -472,8 +474,8 @@ impl<'py> AuditScope<'_, 'py> { run_scope, always_run: instance.getattr(ALWAYS_RUN_KEY)?.is_truthy()?, usages: PyTuple::new(py, usages)?.unbind(), - name: instance.getattr("name")?.extract()?, - description: instance.getattr("description")?.extract()?, + name: native_field(instance, "name")?.extract()?, + description: native_field(instance, "description")?.extract()?, }) } @@ -684,7 +686,7 @@ pub(crate) fn model_owner<'py>( continue; } let column_owner: &str = fact.location_path.as_deref().unwrap_or(&path); - for audit in columns.get_item(index)?.getattr("audits")?.try_iter()? { + for audit in native_field(&columns.get_item(index)?, "audits")?.try_iter()? { let audit: Bound<'py, PyAny> = audit?; let owner_file: String = location_path(&audit)?.unwrap_or_else(|| column_owner.to_owned()); @@ -721,8 +723,8 @@ pub(crate) fn resource_owner<'py>( } for column in columns.try_iter()? { let column: Bound<'py, PyAny> = column?; - let column_name: String = column.getattr("name")?.extract()?; - for audit in column.getattr("audits")?.try_iter()? { + let column_name: String = native_field(&column, "name")?.extract()?; + for audit in native_field(&column, "audits")?.try_iter()? { instances.push((owner_file.clone(), Some(column_name.clone()), audit?)); } } @@ -742,15 +744,15 @@ pub(crate) fn resource_owner<'py>( /// `_explicit_audit_column_name`: the instance's `column` argument when it is a string. fn explicit_column(audit: &Bound<'_, PyAny>) -> PyResult> { - let arguments: Bound<'_, PyDict> = audit.getattr("arguments")?.downcast_into()?; + let arguments: Bound<'_, PyDict> = native_field(audit, "arguments")?.downcast_into()?; header_text(&arguments, COLUMN_ARGUMENT) } /// The path of a spec's authored location, if it has one. fn location_path(spec: &Bound<'_, PyAny>) -> PyResult> { - let location: Bound<'_, PyAny> = spec.getattr("location")?; + let location: Bound<'_, PyAny> = native_field(spec, "location")?; if location.is_none() { return Ok(None); } - Ok(Some(location.getattr("path")?.str()?.to_string())) + Ok(Some(native_field(&location, "path")?.str()?.to_string())) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/audit_views.rs b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/audit_views.rs index 0abb35a97b..cd78b11337 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/audit_views.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/audit_views.rs @@ -232,7 +232,7 @@ macro_rules! audit_getters { } #[getter] - fn attached_target_name(&self, py: Python<'_>) -> PyResult> { + pub(crate) fn attached_target_name(&self, py: Python<'_>) -> PyResult> { read_audit(&self.owner, self.index, py, |audit, _| { Ok(audit.attached.target.as_ref().map(|target| target.name.clone())) }) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/compiled_model_analysis.rs b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/compiled_model_analysis.rs index af04b0ec9c..516f4988e8 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/compiled_model_analysis.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/compiled_model_analysis.rs @@ -25,6 +25,7 @@ use crate::bindings::_helpers::compiled_project::model_input_view::ModelInputRec use crate::bindings::_helpers::record_behaviours::lineage_facts::{ confidence_member, transform_kind_member, }; +use crate::bindings::_helpers::records::fields::field as native_field; const COMPILE_PHASE: &str = "compile"; const MODEL_RESOURCE: &str = "model"; @@ -156,8 +157,8 @@ fn located( } let cleaned: Cow<'_, str> = resolved_cleaned_sql(outcome).map_err(compiler_error)?; let model_file: &Bound<'_, PyAny> = input.model_file.bind(py); - let authored: String = model_file.getattr("contents")?.extract()?; - let query: String = model_file.getattr("query_sql")?.extract()?; + let authored: String = native_field(model_file, "contents")?.extract()?; + let query: String = native_field(model_file, "query_sql")?.extract()?; let expansion = &input.expansion; let passes: Vec> = vec![ expansion.declaration_spans.clone(), 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 c4e94a2f27..f5960d374e 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 @@ -5,6 +5,7 @@ use pyo3::sync::PyOnceLock; use pyo3::types::{PyDict, PyDictMethods, PyFrozenSet, PyString, PyTuple, PyTupleMethods}; use pyo3::{FromPyObject, pyclass, pymethods}; +use sqlbuild_analysis::assembly::analysis_session::main::contract_proof_json::contract_proof_json; use sqlbuild_analysis::assembly::analysis_session::models::LineageRow; use sqlbuild_analysis::compiled_project::main::model_facts::model_facts; use sqlbuild_analysis::compiled_project::models::{ @@ -21,6 +22,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; use crate::bindings::_helpers::rules::relation_keys::RelationKeyRow; const DATABASE_KEY: &str = "database"; @@ -30,9 +32,25 @@ const VALUES_ATTRIBUTE: &str = "values"; const LOGICAL_SCHEMA_ATTRIBUTE: &str = "logical_schema"; const LOGICAL_DATABASE_ATTRIBUTE: &str = "logical_database"; const MODEL_RESOURCE: &str = "model"; +const SQL_ANALYSIS_KEY: &str = "sql_analysis"; const TAGS_KEY: &str = "tags"; const NULLABLE: &str = "nullable"; +/// A model's SQL rule inputs; `excluded` is `None` when its config values could not be read natively. +pub(crate) struct SqlRuleInputs { + pub(crate) posix_path: String, + pub(crate) excluded: Option, + pub(crate) key: Option, +} + +/// What a model's SQL rule cache key reads. +pub(crate) struct SqlRuleKey { + pub(crate) authored_sql: String, + pub(crate) query_sql: String, + /// The canonical JSON text of the model's dynamic pivot proof. + pub(crate) contract: Option, +} + /// A schema's entry audits, column specs and each audited column's index and name. pub(crate) type AuditedSchema = (Py, Py, Vec<(usize, String)>); @@ -369,6 +387,41 @@ impl NativeCompiledModel { }) } + /// What SQL rules select the model by, plus its cache key inputs when `keyed`. + pub(crate) fn sql_rule_inputs(&self, py: Python<'_>, keyed: bool) -> PyResult { + read_input_with_owner(&self.owner, self.index, py, |owner, record| { + let facts: &CompiledModelFacts = + model_facts(owner.facts(), &record.name).ok_or_else(|| { + pyo3::exceptions::PyRuntimeError::new_err("the model was not assembled") + })?; + let excluded: Option = match &facts.config.encoding_error { + Some(_) => None, + None => Some( + facts.config.values.get(SQL_ANALYSIS_KEY) + == Some(&serde_json::Value::Bool(false)), + ), + }; + let key = keyed + .then(|| -> PyResult { + Ok(SqlRuleKey { + authored_sql: facts.authored_sql.clone(), + query_sql: facts.query_sql.clone(), + contract: compiled_of(record)? + .analysis + .proof + .as_ref() + .map(contract_proof_json), + }) + }) + .transpose()?; + Ok(SqlRuleInputs { + posix_path: facts.relative_path.clone(), + excluded, + key, + }) + }) + } + /// The model's authored file text and expanded query, from its recorded facts. pub(crate) fn sql_texts(&self, py: Python<'_>) -> PyResult<(String, String)> { let name: String = self.name(py)?; @@ -702,7 +755,7 @@ impl NativeCompiledModel { let classes: InputViewClasses<'_> = view_classes(&self.owner, py)?; self.read(py, |record, py| { let name: String = model_name(record); - let path: Bound<'_, PyAny> = record.model_file.bind(py).getattr("relative_path")?; + let path: Bound<'_, PyAny> = native_field(record.model_file.bind(py), "relative_path")?; compiled_of(record)? .analysis .binding_tuple(py, &classes.compiled, (&name, &path)) 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 e784fe5ac3..6208bb7ec8 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,5 +1,6 @@ //! Map offsets in expanded SQL back onto the authored text that produced them. +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}; @@ -22,10 +23,10 @@ fn map_expanded_offset<'py>( for span in pass?.try_iter()? { let span: Bound<'py, PyAny> = span?; pass_spans.push(SplicedSpan { - source_start: span.getattr("source_start")?.extract()?, - source_end: span.getattr("source_end")?.extract()?, - output_start: span.getattr("output_start")?.extract()?, - output_end: span.getattr("output_end")?.extract()?, + source_start: native_field(&span, "source_start")?.extract()?, + source_end: native_field(&span, "source_end")?.extract()?, + output_start: native_field(&span, "output_start")?.extract()?, + output_end: native_field(&span, "output_end")?.extract()?, }); } spans.push(pass_spans); 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 5610e7f072..acb029a4bb 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,6 +7,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; @@ -47,8 +48,11 @@ pub(crate) fn native_config_classes<'py>( dict( py, vec![ - ("builder", session.getattr("builder")?), - ("inherited_storage", session.getattr("inherited_storage")?), + ("builder", native_field(session, "builder")?), + ( + "inherited_storage", + native_field(session, "inherited_storage")?, + ), ("table_backed", PyTuple::new(py, table_backed)?.into_any()), ( "compile_model_config", @@ -87,16 +91,20 @@ fn named_records<'py>(resolver: &Bound<'py, PyAny>) -> PyResult>()?; - let declarations: Bound<'py, PyAny> = resolver.getattr("lookup")?.getattr("declarations")?; + let declarations: Bound<'py, PyAny> = + native_field(resolver, "lookup")?.getattr("declarations")?; for pair in declarations.call_method0("items")?.try_iter()? { let (identity, records_of): (Bound<'py, PyAny>, Bound<'py, PyAny>) = pair?.extract()?; - let kind: Bound<'py, PyAny> = identity.getattr("kind")?; + let kind: Bound<'py, PyAny> = native_field(&identity, "kind")?; if kinds.iter().any(|named| kind.is(named)) - && identity.getattr("owner")?.is_none() + && native_field(&identity, "owner")?.is_none() && records_of.is_truthy()? { records.set_item( - (kind.getattr("value")?, identity.getattr("name")?), + ( + native_field(&kind, "value")?, + native_field(&identity, "name")?, + ), records_of.get_item(0)?, )?; } @@ -114,14 +122,14 @@ 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 declarations: Bound<'py, PyAny> = - resolver.getattr("projection")?.getattr("declarations")?; + native_field(resolver, "projection")?.getattr("declarations")?; for pair in declarations.call_method0("items")?.try_iter()? { let (identity, value): (Bound<'py, PyAny>, Bound<'py, PyAny>) = pair?.extract()?; - if identity.getattr("kind")?.is(&enum_kind) - && identity.getattr("owner")?.is_none() + if native_field(&identity, "kind")?.is(&enum_kind) + && native_field(&identity, "owner")?.is_none() && value.is_instance(&enum_class)? { - enums.set_item(identity.getattr("name")?, value)?; + enums.set_item(native_field(&identity, "name")?, value)?; } } Ok(enums) @@ -132,7 +140,7 @@ fn schema_enums<'py>(resolver: &Bound<'py, PyAny>) -> PyResult(function: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = function.py(); let inspect: Bound<'py, PyAny> = py.import("inspect")?.into_any(); - let empty: Bound<'py, PyAny> = inspect.getattr("Parameter")?.getattr("empty")?; + let empty: Bound<'py, PyAny> = native_field(&inspect, "Parameter")?.getattr("empty")?; let mut parameters: Vec<(String, i64, bool)> = Vec::new(); for parameter in inspect .call_method1("signature", (function,))? @@ -142,12 +150,12 @@ fn hook_signature<'py>(function: &Bound<'py, PyAny>) -> PyResult = parameter?; parameters.push(( - parameter.getattr("name")?.extract()?, + native_field(¶meter, "name")?.extract()?, parameter .getattr("kind")? .call_method0("__int__")? .extract()?, - !parameter.getattr("default")?.is(&empty), + !native_field(¶meter, "default")?.is(&empty), )); } PyList::new(py, parameters) @@ -159,22 +167,24 @@ fn visibility_classes<'py>( visible_declarations: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = context.py(); - let resolver: Bound<'py, PyAny> = context.getattr("declaration_resolver")?; + let resolver: Bound<'py, PyAny> = native_field(context, "declaration_resolver")?; let (projects, resource_specific) = if resolver.is_none() { (false, PyFrozenSet::empty(py)?) } else { - let lookup: Bound<'py, PyAny> = resolver.getattr("lookup")?; - let declarations: Bound<'py, PyAny> = lookup.getattr("index")?.getattr("declarations")?; + 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 mut owners: Vec> = Vec::new(); for declaration in declarations.try_iter()? { let declaration: Bound<'py, PyAny> = declaration?; - let owner: Bound<'py, PyAny> = declaration.getattr("identity")?.getattr("owner")?; - if declaration.getattr("scope")?.is(&private) && !owner.is_none() { + let owner: Bound<'py, PyAny> = + native_field(&declaration, "identity")?.getattr("owner")?; + if native_field(&declaration, "scope")?.is(&private) && !owner.is_none() { owners.push(owner); } } - for resource in lookup.getattr("grants_by_resource")?.try_iter()? { + for resource in native_field(&lookup, "grants_by_resource")?.try_iter()? { owners.push(resource?); } (declarations.is_truthy()?, PyFrozenSet::new(py, &owners)?) @@ -185,8 +195,11 @@ fn visibility_classes<'py>( ("resolver", resolver), ("projects", projects.into_bound_py_any(py)?), ("resource_specific", resource_specific.into_any()), - ("public_enums", context.getattr("public_enums")?), - ("public_constants", context.getattr("public_constants")?), + ("public_enums", native_field(context, "public_enums")?), + ( + "public_constants", + native_field(context, "public_constants")?, + ), ("visible_declarations", visible_declarations.clone()), ( "resource_identity", @@ -355,7 +368,7 @@ pub(crate) fn model_loop_classes<'py>( ) -> PyResult> { let py: Python<'py> = context.py(); let host = |key: &str| -> PyResult> { host_inputs.as_any().get_item(key) }; - let resolver: Bound<'py, PyAny> = context.getattr("declaration_resolver")?; + let resolver: Bound<'py, PyAny> = native_field(context, "declaration_resolver")?; let mut provider_names: Vec = host("provider_names")? .try_iter()? .map(|name| name?.extract()) @@ -418,7 +431,10 @@ pub(crate) fn model_loop_classes<'py>( "contract_enforced", registry::member(py, RecordId::ContractPolicy, "ENFORCED")?, ), - ("model_schemas", context.getattr("public_model_schemas")?), + ( + "model_schemas", + native_field(context, "public_model_schemas")?, + ), ("named_records", named_records(&resolver)?.into_any()), ("schema_enums", schema_enums(&resolver)?.into_any()), ( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_input_view.rs b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_input_view.rs index ef2cfcc0f7..327d5a0fa0 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_input_view.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/model_input_view.rs @@ -12,6 +12,7 @@ use crate::bindings::_helpers::compiled_project::compiled_model_view::{ use crate::bindings::_helpers::compiled_project::model_loop_host::ReferenceRow; use crate::bindings::_helpers::compiled_project::project::NativeCompiledProject; use crate::bindings::_helpers::compiled_project::schema_facts::python_column_facts; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::render::declaration_expansion::SpanRow; use crate::bindings::_helpers::rules::relation_keys::{RelationKeyRow, relation_key_row}; @@ -378,7 +379,7 @@ pub(crate) fn model_relation_key_row( record: &ModelInputRecord, py: Python<'_>, ) -> PyResult { - let values: Bound<'_, PyAny> = record.config.bind(py).getattr("values")?; + let values: Bound<'_, PyAny> = native_field(record.config.bind(py), "values")?; let declared = match (&record.schema, owner.input_schema_fact(record)) { (Some(schema), Some(fact)) => { Some((fact.columns.as_slice(), schema.audits.bind(py).as_any())) @@ -470,14 +471,14 @@ pub(crate) fn attached_schema<'py>( })) }); } - let entry: Bound<'py, PyAny> = model_input.getattr("schema_entry")?; + let entry: Bound<'py, PyAny> = native_field(model_input, "schema_entry")?; if entry.is_none() { return Ok(None); } Ok(Some(AttachedSchema { - name: entry.getattr("name")?.extract()?, - dynamic_columns: entry.getattr("dynamic_columns")?, - type_enforcement: entry.getattr("type_enforcement")?.is_truthy()?, + name: native_field(&entry, "name")?.extract()?, + dynamic_columns: native_field(&entry, "dynamic_columns")?, + type_enforcement: native_field(&entry, "type_enforcement")?.is_truthy()?, })) } @@ -492,11 +493,11 @@ pub(crate) fn attached_columns(model_input: &Bound<'_, PyAny>) -> PyResult = model_input.getattr("schema_entry")?; + let entry: Bound<'_, PyAny> = native_field(model_input, "schema_entry")?; if entry.is_none() { return Ok(Vec::new()); } - python_column_facts(&entry.getattr("columns")?, None) + python_column_facts(&native_field(&entry, "columns")?, None) } /// The name of `model_input`, its file stem; a native input answers from its record. @@ -531,9 +532,9 @@ pub(crate) fn input_reference_rows(model_input: &Bound<'_, PyAny>) -> PyResult) -> PyResult { Ok(( - reference.getattr("ref_kind")?.str()?.extract()?, - reference.getattr("ref_name")?.extract()?, - reference.getattr("ref_package")?.extract()?, - reference.getattr("call_argument_count")?.extract()?, + native_field(reference, "ref_kind")?.str()?.extract()?, + native_field(reference, "ref_name")?.extract()?, + native_field(reference, "ref_package")?.extract()?, + native_field(reference, "call_argument_count")?.extract()?, )) } 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 52967e92af..92bc8e9392 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 @@ -58,6 +58,7 @@ use crate::bindings::_helpers::discovery::model_file_record::NativeModelFile; use crate::bindings::_helpers::model_config::config_errors::raised_config_exception; use crate::bindings::_helpers::model_config::header_metadata::ContractClasses; use crate::bindings::_helpers::model_config::model_validator::NativeModelValidator; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::render::declaration_expansion::{ DeclarationExpansion, ExpandedDeclarations, ExpansionClasses, ExpansionConsumer, SpanRow, expand_declarations, @@ -239,13 +240,13 @@ fn index_sql_hook_definitions(loop_classes: &Bound<'_, PyDict>) -> PyResult<()> let definitions: Bound<'_, PyDict> = PyDict::new(py); for hook_file in hooks.as_any().get_item(HOOK_FILES_KEY)?.try_iter()? { let hook_file: Bound<'_, PyAny> = hook_file?; - let name: Bound<'_, PyAny> = hook_file.getattr("name")?; + let name: Bound<'_, PyAny> = native_field(&hook_file, "name")?; if let Some(existing) = definitions.get_item(&name)? { let message: String = format!( "Duplicate SQL hook name '{}' found in {} and {}", name.str()?, - existing.getattr("relative_path")?.str()?, - hook_file.getattr("relative_path")?.str()? + native_field(&existing, "relative_path")?.str()?, + native_field(&hook_file, "relative_path")?.str()? ); let error: Bound<'_, PyAny> = loop_classes .as_any() 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 fbf83c25e3..1c48783a98 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 @@ -15,6 +15,7 @@ use crate::bindings::_helpers::compiled_project::model_loop::{ }; use crate::bindings::_helpers::compiled_project::model_loop_host::ReferenceRow; use crate::bindings::_helpers::compiled_project::model_loop_inputs::reference_row; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::render::macro_expansion::{ ExpansionOutput, NativeMacroExpansion, expanded, }; @@ -192,7 +193,7 @@ fn located( } let relocated = (|| -> PyResult> { let value: Bound<'_, PyAny> = error.value(py).clone().into_any(); - let offset: usize = value.getattr("offset")?.extract()?; + let offset: usize = native_field(&value, "offset")?.extract()?; let passes: Vec> = vec![model.declaration_spans.to_vec()]; let Some(authored) = authored_model_offset(model.contents, model.authored_query, (sql, offset), &passes) 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 ae87cef386..fb31778a67 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 @@ -9,6 +9,7 @@ use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; use pyo3::types::{PyDict, PyDictMethods, PyTuple}; use crate::bindings::_helpers::compiled_project::model_loop::{LoopContext, VisibleDeclarations}; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::scopes::declaration_contexts::NativeDeclarationContexts; const NAME_FIELD: &str = "name"; @@ -189,7 +190,7 @@ impl<'py> ModelVisibility<'py> { ) -> PyResult> { let resolver: &Bound<'py, PyAny> = &context.classes.visibility.resolver; let contexts: Bound<'py, NativeDeclarationContexts> = - resolver.getattr("native_contexts")?.downcast_into()?; + native_field(resolver, "native_contexts")?.downcast_into()?; let (resolved, _) = contexts.get().resolved_context(resolver, file_path, None)?; Ok(resolved) } @@ -211,7 +212,7 @@ fn rebound<'py>( } let resolver: &Bound<'py, PyAny> = &context.classes.visibility.resolver; let contexts: Bound<'py, NativeDeclarationContexts> = - resolver.getattr("native_contexts")?.downcast_into()?; + 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)?)?; 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 ab58515a7e..a663b6fbd2 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/project.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/project.rs @@ -56,6 +56,7 @@ use crate::bindings::_helpers::compiled_project::source_views::{ AttachedSource, NativeCompiledSource, NativeSourceInput, SourceNamespace, SourceRecord, }; use crate::bindings::_helpers::compiled_project::values::{object_value, plain_value}; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::render::declaration_values::ConstantRendering; pub(crate) const CONTRACT_KEY: &str = "contract"; @@ -210,8 +211,8 @@ impl NativeCompiledProject { .to_string(), ), ( - &schema_entry.getattr("audits")?, - &schema_entry.getattr("columns")?, + &native_field(schema_entry, "audits")?, + &native_field(schema_entry, "columns")?, ), )?); } 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 61c1012b24..88ff02b850 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 @@ -250,9 +250,13 @@ impl NativeProjectView { )) } - /// The project field custom-rule facts read by `name`, or `None` for any other field. + /// The project field custom-rule facts and rules runs read by `name`, or `None` for any other. pub(crate) fn fact_field(&self, py: Python<'_>, name: &str) -> Option> { let field: &Py = match name { + "settings" => &self.fields.compiler.settings, + "sql_scenarios" => &self.fields.compiler.sql_scenarios, + "scope_index" => &self.fields.compiler.scope_index, + "diagnostics" => &self.fields.compiler.diagnostics, "sources" => &self.fields.compiler.sources, "seeds" => &self.fields.compiler.seeds, "functions" => &self.fields.compiler.functions, 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 480081c418..e751dac524 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/records.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/records.rs @@ -14,6 +14,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; @@ -47,7 +48,7 @@ fn record<'py>(value: &Bound<'py, PyAny>, class: RecordId) -> PyResult = field?.getattr("name")?; + let name: Bound<'py, PyAny> = native_field(&field?, "name")?; fields.set_item( &name, value.getattr(name.downcast::()?)?, @@ -154,7 +155,7 @@ pub(crate) fn with_type_recovery<'py>( poisoned: &Bound<'py, PyFrozenSet>, ) -> PyResult> { let py: Python<'py> = model.py(); - let name: String = model.getattr("name")?.extract()?; + let name: String = native_field(model, "name")?.extract()?; let mut unchecked: Vec = Vec::new(); for pair in poisoned.iter() { let (model_name, column): (String, String) = pair.extract()?; @@ -165,14 +166,14 @@ pub(crate) fn with_type_recovery<'py>( if !model.is_instance(®istry::class(py, RecordId::CompiledModel)?)? { return model.call_method1("with_type_recovery", (diagnostics, unchecked)); } - let inferred: Bound<'py, PyAny> = model.getattr("inferred_columns")?; + let inferred: Bound<'py, PyAny> = native_field(model, "inferred_columns")?; let columns: Bound<'py, PyAny> = if inferred.is_none() { inferred } else { let mut columns: Vec> = Vec::new(); for column in inferred.try_iter()? { let column: Bound<'py, PyAny> = column?; - let key = (name.as_str(), column.getattr("name")?); + let key = (name.as_str(), native_field(&column, "name")?); columns.push(if poisoned.contains(key)? { replace(&column, &[("type", py.None().into_bound(py))])? } else { @@ -259,8 +260,8 @@ pub(crate) fn compile_inputs_view<'py>( #[pyfunction] pub(crate) fn plain_typed_value<'py>(value: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = value.py(); - let kind: String = value.getattr("kind")?.getattr("value")?.extract()?; - let inner: Bound<'py, PyAny> = value.getattr("value")?; + let kind: String = native_field(value, "kind")?.getattr("value")?.extract()?; + let inner: Bound<'py, PyAny> = native_field(value, "value")?; if kind == DECIMAL_KIND { return Ok(inner.str()?.into_any()); } @@ -290,13 +291,16 @@ fn enum_row<'py>(declaration: &Bound<'py, PyAny>) -> PyResult = member?; - Ok((member.getattr("name")?, member.getattr("value")?)) + Ok(( + native_field(&member, "name")?, + native_field(&member, "value")?, + )) }) .collect::>()?; PyTuple::new( py, [ - declaration.getattr("name")?, + native_field(declaration, "name")?, declaration .getattr("relative_path")? .call_method0("as_posix")?, @@ -311,23 +315,23 @@ fn enum_row<'py>(declaration: &Bound<'py, PyAny>) -> PyResult(declaration: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = declaration.py(); - let render_as: Bound<'py, PyAny> = declaration.getattr("render_as")?; + let render_as: Bound<'py, PyAny> = native_field(declaration, "render_as")?; PyTuple::new( py, [ - declaration.getattr("name")?, + native_field(declaration, "name")?, declaration .getattr("relative_path")? .call_method0("as_posix")?, PyList::empty(py).into_any(), - plain_typed_value(&declaration.getattr("value")?)?, + plain_typed_value(&native_field(declaration, "value")?)?, declaration .getattr("logical_type")? .getattr("display_name")?, if render_as.is_none() { render_as } else { - render_as.getattr("value")? + native_field(&render_as, "value")? }, ], ) @@ -361,13 +365,13 @@ fn family_types<'py>( .try_iter()? .map(|family| { let family: Bound<'py, PyAny> = family?; - let name: Bound<'py, PyAny> = family.getattr("name")?; + let name: Bound<'py, PyAny> = native_field(&family, "name")?; let name: String = if casefold { name.call_method0("casefold")?.extract()? } else { name.extract()? }; - Ok((name, family.getattr("inferred_type")?.extract()?)) + Ok((name, native_field(&family, "inferred_type")?.extract()?)) }) .collect() } @@ -393,22 +397,22 @@ pub(crate) fn record_model_analyses<'py>( if model.is_instance_of::() { continue; } - let inferred: Bound<'py, PyAny> = model.getattr("inferred_rows")?; + let inferred: Bound<'py, PyAny> = native_field(model, "inferred_rows")?; let rows: Vec<(String, Option, Bound<'py, PyAny>)> = if inferred.is_none() { Vec::new() } else { inferred.extract()? }; - let proof: Bound<'py, PyAny> = model.getattr("dynamic_column_contract")?; + let proof: Bound<'py, PyAny> = native_field(model, "dynamic_column_contract")?; let proof = if proof.is_none() { None } else { - let families: Bound<'py, PyAny> = proof.getattr("families")?; + let families: Bound<'py, PyAny> = native_field(&proof, "families")?; Some(( - proof.getattr("output_proven")?.extract()?, - proof.getattr("bare_dynamic_pivot")?.extract()?, + native_field(&proof, "output_proven")?.extract()?, + native_field(&proof, "bare_dynamic_pivot")?.extract()?, family_types(&families, true)?, - proof.getattr("failure_reason")?.extract()?, + native_field(&proof, "failure_reason")?.extract()?, family_types(&families, false)?, )) }; @@ -420,14 +424,14 @@ pub(crate) fn record_model_analyses<'py>( rows.iter() .map(|(_, _, nullability)| nullability.eq(&nullable)) .collect::>()?, - model.getattr("fast_lineage_has_star")?.is_truthy()?, + native_field(model, "fast_lineage_has_star")?.is_truthy()?, model .getattr("unchecked_output_columns")? .try_iter()? .map(|column| column?.extract()) .collect::>()?, ); - let name: String = model.getattr("name")?.extract()?; + let name: String = native_field(model, "name")?.extract()?; transferred += usize::from( project .borrow_mut() @@ -444,13 +448,13 @@ fn hand_built_project<'py>( let py: Python<'py> = project.py(); let native: Bound<'py, NativeCompiledProject> = Bound::new(py, NativeCompiledProject::default())?; - let models: Bound<'py, PyAny> = project.getattr("models")?; + let models: Bound<'py, PyAny> = native_field(project, "models")?; for model in models.try_iter()? { let model: Bound<'py, PyAny> = model?; let mut facts: CompiledModelFacts = model_facts(model_row(&model)?.as_any().extract()?)?; facts.schema = entry_schema_fact( - &model.getattr("schema_entry")?, - &model.getattr("relative_path")?, + &native_field(&model, "schema_entry")?, + &native_field(&model, "relative_path")?, )?; native.borrow_mut().retain(facts); } @@ -463,7 +467,10 @@ fn hand_built_project<'py>( pub(crate) fn compiled_project_facts<'py>( project: &Bound<'py, PyAny>, ) -> PyResult> { - let retained: Bound<'py, PyAny> = project.getattr("native_project")?; + if let Ok(view) = project.downcast::() { + return Ok(view.get().owner().bind(project.py()).clone()); + } + let retained: Bound<'py, PyAny> = native_field(project, "native_project")?; if !retained.is_none() { return Ok(retained.downcast_into()?); } @@ -472,8 +479,8 @@ pub(crate) fn compiled_project_facts<'py>( fn model_row<'py>(model: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = model.py(); - let relative_path: Bound<'py, PyAny> = model.getattr("relative_path")?; - let config: Bound<'py, PyAny> = model.getattr("config")?; + let relative_path: Bound<'py, PyAny> = native_field(model, "relative_path")?; + let config: Bound<'py, PyAny> = native_field(model, "config")?; let references: Vec> = model .getattr("references")? .try_iter()? @@ -482,16 +489,16 @@ fn model_row<'py>(model: &Bound<'py, PyAny>) -> PyResult> { PyTuple::new( py, [ - reference.getattr("ref_kind")?.str()?.into_any(), - reference.getattr("ref_name")?, - reference.getattr("ref_package")?, + native_field(&reference, "ref_kind")?.str()?.into_any(), + native_field(&reference, "ref_name")?, + native_field(&reference, "ref_package")?, ], ) }) .collect::>()?; let (enums, constants) = declaration_rows( - &model.getattr("enum_declarations")?, - &model.getattr("constant_declarations")?, + &native_field(model, "enum_declarations")?, + &native_field(model, "constant_declarations")?, )?; PyTuple::new( py, @@ -499,18 +506,18 @@ fn model_row<'py>(model: &Bound<'py, PyAny>) -> PyResult> { PyTuple::new( py, [ - model.getattr("name")?, + native_field(model, "name")?, relative_path.call_method0("as_posix")?, - model.getattr("query_sql")?, - model.getattr("authored_query_sql")?, - model.getattr("authored_sql")?, + native_field(model, "query_sql")?, + native_field(model, "authored_query_sql")?, + native_field(model, "authored_sql")?, ], )? .into_any(), PyTuple::new( py, [ - config.getattr("values")?, + native_field(&config, "values")?, PyList::new( py, config @@ -519,7 +526,7 @@ fn model_row<'py>(model: &Bound<'py, PyAny>) -> PyResult> { .collect::>>()?, )? .into_any(), - config.getattr("layer_schema")?, + native_field(&config, "layer_schema")?, ], )? .into_any(), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/resource_diagnostics.rs b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/resource_diagnostics.rs index 81546134f1..cb13b82114 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/resource_diagnostics.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/resource_diagnostics.rs @@ -22,6 +22,7 @@ use sqlbuild_analysis::resource_diagnostics::main::yaml_help::yaml_help; use sqlbuild_analysis::resource_diagnostics::models::{CursorModel, HeaderHelp}; use crate::bindings::_helpers::compiled_project::project::NativeCompiledProject; +use crate::bindings::_helpers::records::fields::field as native_field; const MISSING_DESCRIPTION_CODE: &str = "P010"; const CURSOR_WITHOUT_INPUTS_CODE: &str = "P011"; @@ -252,8 +253,10 @@ fn missing_descriptions<'py>( for record in project.inputs() { let model_file: &Bound<'py, PyAny> = record.model_file.bind(py); let name: String = record.name.clone(); - let path: String = model_file.getattr("relative_path")?.str()?.extract()?; - let contents: String = model_file.getattr("contents")?.extract()?; + let path: String = native_field(model_file, "relative_path")? + .str()? + .extract()?; + let contents: String = native_field(model_file, "contents")?.extract()?; let entry: String = format!("description \"What one row of {name} represents\""); let help: String = header_help(&HeaderHelp { kind: "model", @@ -301,10 +304,12 @@ fn missing_descriptions<'py>( let (_, _, entry, schema_file) = seed.parts(); let entry: &Bound<'py, PyAny> = entry.bind(py); let schema_file: &Bound<'py, PyAny> = schema_file.bind(py); - let name: String = entry.getattr("name")?.extract()?; - let description: Option = entry.getattr("description")?.extract()?; - let path: String = schema_file.getattr("relative_path")?.str()?.extract()?; - let contents_value = schema_file.getattr("contents")?; + let name: String = native_field(entry, "name")?.extract()?; + let description: Option = native_field(entry, "description")?.extract()?; + let path: String = native_field(schema_file, "relative_path")? + .str()? + .extract()?; + let contents_value = native_field(schema_file, "contents")?; let contents = contents_value.downcast::()?.to_str()?; found.push(diagnostics.described( ("seed", &name, Some("seed")), @@ -364,10 +369,12 @@ fn source_description<'py>( let attached = source.attached(); let name: &str = &attached.name; let source_file: &Bound<'py, PyAny> = attached.source_file.bind(py); - let path: String = source_file.getattr("relative_path")?.str()?.extract()?; - let contents_value = source_file.getattr("contents")?; + let path: String = native_field(source_file, "relative_path")? + .str()? + .extract()?; + let contents_value = native_field(source_file, "contents")?; let contents = contents_value.downcast::()?.to_str()?; - let loader_name: Option = attached.entry.bind(py).getattr("loader")?.extract()?; + let loader_name: Option = native_field(attached.entry.bind(py), "loader")?.extract()?; let loader: Option<(&String, &LoaderRow)> = loader_name .as_ref() .and_then(|loader| diagnostics.settings.loaders.get_key_value(loader)); @@ -408,8 +415,10 @@ fn function_description<'py>( let attached = &function.attached; let name: &str = &attached.name; let function_file: &Bound<'py, PyAny> = attached.function_file.bind(py); - let path: String = function_file.getattr("relative_path")?.str()?.extract()?; - let contents: String = function_file.getattr("contents")?.extract()?; + let path: String = native_field(function_file, "relative_path")? + .str()? + .extract()?; + let contents: String = native_field(function_file, "contents")?.extract()?; let resource_type: &str = if attached.return_columns.is_empty() { "udf" } else { @@ -471,7 +480,7 @@ fn cursor_without_inputs<'py>( let mut errors: Vec> = Vec::new(); for record in project.inputs() { let values: Bound<'py, PyDict> = - record.config.bind(py).getattr("values")?.downcast_into()?; + native_field(record.config.bind(py), "values")?.downcast_into()?; let kinds: Vec<&str> = record .references .iter() @@ -496,8 +505,10 @@ fn cursor_without_inputs<'py>( ) else { continue; }; - let path: String = model_file.getattr("relative_path")?.str()?.extract()?; - let contents: String = model_file.getattr("contents")?.extract()?; + let path: String = native_field(model_file, "relative_path")? + .str()? + .extract()?; + let contents: String = native_field(model_file, "contents")?.extract()?; let span: Option<(usize, usize)> = model_header_entry_span(&contents, CURSOR_KEY).map_err(PyValueError::new_err)?; let start: TextPosition = text_position(&contents, span.map_or(0, |span| span.0)); 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 e7e0122c61..61d293301b 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 @@ -21,6 +21,7 @@ use crate::bindings::_helpers::model_config::config_templates::{ PythonHost, Stop, TemplateSources, expanded, template_error, }; use crate::bindings::_helpers::project_assembly::resources::syntax_error_message; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::render::declaration_expansion::{ DeclarationExpansion, ExpandedDeclarations, ExpansionClasses, ExpansionConsumer, expand_declarations, @@ -53,21 +54,22 @@ pub(crate) fn attached_sources<'py>( let mut attached: Vec = Vec::with_capacity(rows.len()); for (file_path, source_file, entry) in rows { let entry: Bound<'py, PyAny> = entry.into_bound(py); - let name: String = entry.getattr("name")?.extract()?; + let name: String = native_field(&entry, "name")?.extract()?; let consumer: Bound<'py, PyAny> = context .classes .visibility .resource_identity .call1((&source_kind, &name))?; let declarations: Bound<'py, PyAny> = scoped_declarations(context, &file_path, &consumer)?; - let (expression, usages) = match entry.getattr("expression")?.extract::>()? { - Some(sql) => { - let expansion: AuthoredExpansion<'py> = - expanded_authored_sql(context, (&sql, &file_path), &declarations)?; - (Some(expansion.sql), expansion.usages) - } - None => (None, PyTuple::empty(py)), - }; + let (expression, usages) = + match native_field(&entry, "expression")?.extract::>()? { + Some(sql) => { + let expansion: AuthoredExpansion<'py> = + expanded_authored_sql(context, (&sql, &file_path), &declarations)?; + (Some(expansion.sql), expansion.usages) + } + None => (None, PyTuple::empty(py)), + }; let label = |field: &str| format!("source {name} {field}"); let database: Option = template_text(context, &entry, "database", &label("database"))?; @@ -76,15 +78,15 @@ pub(crate) fn attached_sources<'py>( let description: Option = template_text(context, &entry, "description", &label("description"))?; let meta: Bound<'py, PyAny> = - template_value(context, &entry.getattr("meta")?, &label("meta"))?; + template_value(context, &native_field(&entry, "meta")?, &label("meta"))?; let mut columns: Vec> = Vec::new(); - for column in entry.getattr("columns")?.try_iter()? { + for column in native_field(&entry, "columns")?.try_iter()? { columns.push(expanded_column(context, &name, &column?)?); } let mut audits: Vec> = Vec::new(); - for audit in entry.getattr("audits")?.try_iter()? { + for audit in native_field(&entry, "audits")?.try_iter()? { let audit: Bound<'py, PyAny> = audit?; - let definition: String = audit.getattr("definition_name")?.extract()?; + let definition: String = native_field(&audit, "definition_name")?.extract()?; audits.push(expanded_audit( context, &audit, @@ -218,7 +220,7 @@ pub(crate) fn expanded_authored_sql<'py>( return Err(compile_error(context.classes, &message, None)); } let interpolated_sql: String = interpolated_sql.unwrap_or_else(|| sql.to_owned()); - let consumer: Bound<'py, PyAny> = declarations.getattr("consumer")?; + let consumer: Bound<'py, PyAny> = native_field(declarations, "consumer")?; let (references, stop) = declaration_scan(&interpolated_sql); let mut usages: Vec> = Vec::new(); let mut argument_references: Vec = Vec::new(); @@ -352,7 +354,7 @@ fn expanded_column( column: &Bound<'_, PyAny>, ) -> PyResult> { let py: Python<'_> = column.py(); - let column_name: String = column.getattr("name")?.str()?.extract()?; + let column_name: String = native_field(column, "name")?.str()?.extract()?; let label = |field: &str| format!("source {source} column {column_name} {field}"); let changes: Bound<'_, PyDict> = PyDict::new(py); changes.set_item( @@ -365,12 +367,12 @@ fn expanded_column( )?; changes.set_item( "meta", - template_value(context, &column.getattr("meta")?, &label("meta"))?, + template_value(context, &native_field(column, "meta")?, &label("meta"))?, )?; let mut audits: Vec> = Vec::new(); - for audit in column.getattr("audits")?.try_iter()? { + for audit in native_field(column, "audits")?.try_iter()? { let audit: Bound<'_, PyAny> = audit?; - let definition: String = audit.getattr("definition_name")?.extract()?; + let definition: String = native_field(&audit, "definition_name")?.extract()?; audits.push(expanded_audit( context, &audit, @@ -392,7 +394,7 @@ fn expanded_audit( "arguments", template_value( context, - &audit.getattr("arguments")?, + &native_field(audit, "arguments")?, &format!("{label} arguments"), )?, )?; 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 6451ce7dd2..f21ce5a178 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 @@ -11,6 +11,7 @@ use crate::bindings::_helpers::compiled_project::model_input_view::{ InputViewClasses, view_classes, }; use crate::bindings::_helpers::compiled_project::project::NativeCompiledProject; +use crate::bindings::_helpers::records::fields::field as native_field; const SOURCE_RESOURCE: &str = "source"; const DATACLASSES_MODULE: &str = "dataclasses"; @@ -183,7 +184,7 @@ impl NativeSourceInput { let attached: &AttachedSource = &source.attached; Ok(( attached.name.clone(), - !attached.entry.bind(py).getattr("loader")?.is_none(), + !native_field(attached.entry.bind(py), "loader")?.is_none(), attached.database.clone(), attached.schema.clone(), )) 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 947a084832..c37367dc7e 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 @@ -47,6 +47,7 @@ use crate::bindings::_helpers::compiled_project::source_loop::{ AuthoredExpansion, expanded_authored_sql, scoped_declarations, }; use crate::bindings::_helpers::compiled_project::source_views::replaced; +use crate::bindings::_helpers::records::fields::field as native_field; const MODEL_MODE: &str = "model"; const MACRO_MODE: &str = "macro"; @@ -552,8 +553,8 @@ impl<'py> TestScope<'_, 'py> { unique.set_item( self.usage( resource, - &record.getattr("declaration")?, - &record.getattr("through")?, + &native_field(&record, "declaration")?, + &native_field(&record, "through")?, )?, py.None(), )?; @@ -567,7 +568,7 @@ impl<'py> TestScope<'_, 'py> { let Some(record) = macro_records.get_item(&name)? else { continue; }; - let identity: Bound<'py, PyAny> = record.getattr("identity")?; + let identity: Bound<'py, PyAny> = native_field(&record, "identity")?; let visible: Vec> = match macro_visibility.get_item(&name)? { Some(visibility) => test_visibility(&visibility)?, None => Vec::new(), @@ -580,7 +581,7 @@ impl<'py> TestScope<'_, 'py> { } for record in visible { unique.set_item( - self.usage(resource, &identity, &record.getattr("through")?)?, + self.usage(resource, &identity, &native_field(&record, "through")?)?, py.None(), )?; } @@ -866,12 +867,12 @@ impl<'py> TestScope<'_, 'py> { fields.set_item("declaration_usages", &test.usages)?; fields.set_item("parent_name", test_name)?; let block: &Bound<'py, PyAny> = test_block.bind(py); - fields.set_item("parameter_schema", block.getattr("parameters")?)?; + fields.set_item("parameter_schema", native_field(block, "parameters")?)?; if let Some((case, case_name, _)) = test.case { let case: &Bound<'py, PyAny> = case.bind(py); fields.set_item("case_name", case_name)?; - fields.set_item("case_index", case.getattr("case_index")?)?; - fields.set_item("parameter_values", case.getattr("values")?)?; + fields.set_item("case_index", native_field(case, "case_index")?)?; + fields.set_item("parameter_values", native_field(case, "values")?)?; } Ok(Some( self.settings @@ -911,7 +912,7 @@ impl<'py> TestScope<'_, 'py> { ); let (line, column) = (located.line, located.column); let arguments: Bound<'py, PyDict> = PyDict::new(py); - arguments.set_item("path", file.3.bind(py).getattr("relative_path")?)?; + arguments.set_item("path", native_field(file.3.bind(py), "relative_path")?)?; arguments.set_item("line", line)?; arguments.set_item("column", column)?; arguments.set_item("end_line", located.end_line)?; @@ -987,7 +988,7 @@ fn test_visibility<'py>(records: &Bound<'py, PyAny>) -> PyResult> = records.try_iter()?.collect::>()?; let mut granted: Vec> = Vec::new(); for record in &all { - if !record.getattr("through")?.is_none() { + if !native_field(record, "through")?.is_none() { granted.push(record.clone()); } } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compute_logs/conversion.rs b/crates/sqlbuild-python/src/bindings/_helpers/compute_logs/conversion.rs index 03e5e15825..3ff64702d5 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compute_logs/conversion.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compute_logs/conversion.rs @@ -12,6 +12,7 @@ use sqlbuild_cli::compute_logs::models::{ 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::fields::field as native_field; const EPOCH_TEXT: &str = "1970-01-01T00:00:00+00:00"; const UTC_OFFSET: &str = "+00:00"; @@ -68,7 +69,7 @@ pub(crate) fn capture_id(value: &Bound<'_, PyAny>) -> PyResult { pub(crate) fn stream_index(stream: &Bound<'_, PyAny>) -> PyResult { let py = stream.py(); if stream.is_instance(&host::entry(py, ExecutorHost::ComputeLogStream)?)? { - let value: String = stream.getattr("value")?.extract()?; + let value: String = native_field(stream, "value")?.extract()?; if let Some(index) = STREAM_VALUES.iter().position(|name| *name == value) { return Ok(index); } @@ -84,7 +85,7 @@ fn instant(value: &Bound<'_, PyAny>) -> PyResult { let py = value.py(); let text: String = value.call_method0("isoformat")?.extract()?; let text = text.replace(UTC_OFFSET, UTC_SUFFIX); - if value.getattr("tzinfo")?.is_none() || value.call_method0("utcoffset")?.is_none() { + if native_field(value, "tzinfo")?.is_none() || value.call_method0("utcoffset")?.is_none() { return Ok(CaptureInstant { micros: 0, aware: false, @@ -118,8 +119,8 @@ fn count(py: Python<'_>, value: &Bound<'_, PyAny>) -> PyResult { /// A Python `CaptureMetadata` or `FinalCaptureMetadata` as a native capture document. pub(crate) fn document(metadata: &Bound<'_, PyAny>) -> PyResult { let py = metadata.py(); - let invocation_id = capture_id(&metadata.getattr("invocation_id")?)?; - let version = metadata.getattr("format_version")?; + let invocation_id = capture_id(&native_field(metadata, "invocation_id")?)?; + let version = native_field(metadata, "format_version")?; let format_version: i64 = if is_exact_int(&version) { version.extract().unwrap_or(0) } else { @@ -128,11 +129,11 @@ pub(crate) fn document(metadata: &Bound<'_, PyAny>) -> PyResult let completion = if metadata.is_instance(&host::entry(py, ExecutorHost::FinalCaptureMetadata)?)? { Some(CaptureCompletion { - completed_at: instant(&metadata.getattr("completed_at")?)?, - exit_code: count(py, &metadata.getattr("exit_code")?)?, - stdout_bytes: count(py, &metadata.getattr("stdout_bytes")?)?, - stderr_bytes: count(py, &metadata.getattr("stderr_bytes")?)?, - diagnostics_bytes: count(py, &metadata.getattr("diagnostics_bytes")?)?, + completed_at: instant(&native_field(metadata, "completed_at")?)?, + exit_code: count(py, &native_field(metadata, "exit_code")?)?, + stdout_bytes: count(py, &native_field(metadata, "stdout_bytes")?)?, + stderr_bytes: count(py, &native_field(metadata, "stderr_bytes")?)?, + diagnostics_bytes: count(py, &native_field(metadata, "diagnostics_bytes")?)?, }) } else { None @@ -140,12 +141,12 @@ pub(crate) fn document(metadata: &Bound<'_, PyAny>) -> PyResult Ok(CaptureDocument { format_version, invocation_id, - command: metadata.getattr("command")?.extract()?, - project_dir: metadata.getattr("project_dir")?.extract()?, - started_at: instant(&metadata.getattr("started_at")?)?, - capture_date: metadata.getattr("capture_date")?.extract()?, - target: metadata.getattr("target")?.extract()?, - run_id: metadata.getattr("run_id")?.extract()?, + command: native_field(metadata, "command")?.extract()?, + project_dir: native_field(metadata, "project_dir")?.extract()?, + started_at: instant(&native_field(metadata, "started_at")?)?, + capture_date: native_field(metadata, "capture_date")?.extract()?, + target: native_field(metadata, "target")?.extract()?, + run_id: native_field(metadata, "run_id")?.extract()?, completion, }) } 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 c32675ba79..cadaab4d69 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/contract_command/command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/contract_command/command.rs @@ -23,6 +23,7 @@ use crate::bindings::_helpers::command_runtime::invocation::resolve_adapter_conn 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::presentation_text::terminal::stdout_supports_color; +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; @@ -62,7 +63,7 @@ fn selected_of<'py>( let mut kept: Vec> = Vec::new(); for resource in resources.try_iter()? { let resource: Bound<'py, PyAny> = resource?; - if selected.contains(resource.getattr("key")?)? { + if selected.contains(native_field(&resource, "key")?)? { kept.push(resource); } } @@ -83,10 +84,13 @@ fn select_resources<'py>( ®istry::native(py, NativeAttr::BuildCompiledProject)?, &[ ("discovered_inputs", discovered.clone()), - ("adapter", context.getattr("adapter")?), - ("selected_target", request.getattr("from_target")?), - ("cli_vars", request.getattr("cli_vars")?), - ("resolved_connection", context.getattr("connection_config")?), + ("adapter", native_field(context, "adapter")?), + ("selected_target", native_field(request, "from_target")?), + ("cli_vars", native_field(request, "cli_vars")?), + ( + "resolved_connection", + native_field(context, "connection_config")?, + ), ("analysis_selection", analysis), ], )?; @@ -98,13 +102,13 @@ fn select_resources<'py>( ®istry::native(py, NativeAttr::SelectProjectGraph)?, &[ ("graph", graph.clone()), - ("select", request.getattr("select")?), - ("exclude", request.getattr("exclude")?), + ("select", native_field(request, "select")?), + ("exclude", native_field(request, "exclude")?), ], )?; - let compiled: Bound<'py, PyAny> = graph.getattr("project")?; - let models: Bound<'py, PyTuple> = selected_of(compiled.getattr("models")?, &selected)?; - let sources: Bound<'py, PyTuple> = selected_of(compiled.getattr("sources")?, &selected)?; + 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)?; if models.is_empty() && sources.is_empty() { return Err(user_error( py, @@ -158,11 +162,11 @@ fn compare<'py>( progress: &Progress<'py>, ) -> PyResult> { let py: Python<'py> = request.py(); - let adapter: Bound<'py, PyAny> = context.getattr("adapter")?; + let adapter: Bound<'py, PyAny> = native_field(context, "adapter")?; let reporter: Bound<'py, PyAny> = call_with( ®istry::native(py, NativeAttr::ConnectionProgressReporter)?, &[ - ("adapter_name", context.getattr("adapter_name")?), + ("adapter_name", native_field(context, "adapter_name")?), ("stream", progress.stream.clone()), ( "use_color", @@ -173,7 +177,7 @@ fn compare<'py>( let connection_started: Instant = Instant::now(); reporter.call_method1("on_connection_start", (1,))?; let connection: Bound<'py, PyAny> = - match adapter.call_method1("connect", (context.getattr("connection_config")?,)) { + match adapter.call_method1("connect", (native_field(context, "connection_config")?,)) { Ok(connection) => connection, Err(error) => { if exception(py, &error) { @@ -185,7 +189,7 @@ fn compare<'py>( let compared: PyResult> = (|| { connection_timing(&reporter, "on_connection_complete", connection_started)?; let count: usize = selection.models.len() + selection.sources.len(); - let target: String = request.getattr("from_target")?.str()?.to_string(); + let target: String = native_field(request, "from_target")?.str()?.to_string(); let status: Bound<'py, PyAny> = progress.status()?; let started: Instant = Instant::now(); status.call_method1( @@ -252,10 +256,10 @@ fn write_updates<'py>( status.call_method1("start", ("Updating repository contracts...",))?; let write: ContractWrite<'py> = ContractWrite { project_dir: project_dir.clone(), - overwrite: request.getattr("overwrite")?, - adapter: context.getattr("adapter")?, - resolved_connection: context.getattr("connection_config")?, - cli_vars: request.getattr("cli_vars")?, + overwrite: native_field(request, "overwrite")?, + adapter: native_field(context, "adapter")?, + resolved_connection: native_field(context, "connection_config")?, + cli_vars: native_field(request, "cli_vars")?, }; let updated: Bound<'py, PyAny> = match write_contracts(&write, graph, &result) { Ok(updated) => updated, @@ -271,7 +275,7 @@ fn write_updates<'py>( return Err(error); } }; - let written: usize = updated.getattr("written_paths")?.len()?; + let written: usize = native_field(&updated, "written_paths")?.len()?; let options: Bound<'py, PyDict> = PyDict::new(py); options.set_item( "message", @@ -286,11 +290,11 @@ fn write_updates<'py>( fn finding_text(finding: Bound<'_, PyAny>) -> PyResult { Ok(ContractFindingText { - kind: finding.getattr("kind")?.str()?.to_string(), - column: optional_text(finding.getattr("column_name")?)?, - declared_type: optional_text(finding.getattr("declared_type")?)?, - physical_type: optional_text(finding.getattr("physical_type")?)?, - message: finding.getattr("message")?.str()?.to_string(), + kind: native_field(&finding, "kind")?.str()?.to_string(), + column: optional_text(native_field(&finding, "column_name")?)?, + declared_type: optional_text(native_field(&finding, "declared_type")?)?, + physical_type: optional_text(native_field(&finding, "physical_type")?)?, + message: native_field(&finding, "message")?.str()?.to_string(), }) } @@ -303,8 +307,8 @@ fn resource_text(item: Bound<'_, PyAny>) -> PyResult { } } Ok(ContractResourceText { - resource_type: item.getattr("resource_type")?.str()?.to_string(), - resource_name: item.getattr("resource_name")?.str()?.to_string(), + resource_type: native_field(&item, "resource_type")?.str()?.to_string(), + resource_name: native_field(&item, "resource_name")?.str()?.to_string(), relation: parts.join("."), findings: item .getattr("findings")? @@ -316,7 +320,7 @@ fn resource_text(item: Bound<'_, PyAny>) -> PyResult { fn report(result: &Bound<'_, PyAny>) -> PyResult { Ok(ContractReport { - from_target: result.getattr("from_target")?.str()?.to_string(), + from_target: native_field(result, "from_target")?.str()?.to_string(), resources: result .getattr("evidence")? .try_iter()? @@ -332,12 +336,13 @@ fn report(result: &Bound<'_, PyAny>) -> PyResult { fn progress_for<'py>(request: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = request.py(); - let json_output: bool = request.getattr("json_output")?.is_truthy()?; + let json_output: bool = native_field(request, "json_output")?.is_truthy()?; let stream: Bound<'py, PyAny> = py.import("sys")? .getattr(if json_output { "stderr" } else { "stdout" })?; - let use_color: bool = - !request.getattr("no_color")?.is_truthy()? && !json_output && stdout_supports_color(py)?; + let use_color: bool = !native_field(request, "no_color")?.is_truthy()? + && !json_output + && stdout_supports_color(py)?; Ok(Progress { stream, use_color }) } @@ -345,17 +350,17 @@ fn progress_for<'py>(request: &Bound<'py, PyAny>) -> PyResult> { #[pyfunction] #[pyo3(signature = (*, request))] fn run_contract_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { - let action: String = request.getattr("action")?.str()?.to_string(); + let action: String = native_field(request, "action")?.str()?.to_string(); let generate_write: bool = - action == GENERATE_ACTION && request.getattr("write")?.is_truthy()?; - if request.getattr("overwrite")?.is_truthy()? && !generate_write { + action == GENERATE_ACTION && native_field(request, "write")?.is_truthy()?; + if native_field(request, "overwrite")?.is_truthy()? && !generate_write { return Err(user_error( py, "contract --overwrite requires generate --write", "C471", )); } - let requested_dir: Bound<'_, PyAny> = request.getattr("project_dir")?; + let requested_dir: Bound<'_, PyAny> = native_field(request, "project_dir")?; let project_dir: Bound<'_, PyAny> = if requested_dir.is_truthy()? { requested_dir } else { @@ -368,12 +373,12 @@ fn run_contract_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult< call_with( ®istry::native(py, NativeAttr::RejectUnitTestSelectors)?, &[ - ("select", request.getattr("select")?), - ("exclude", request.getattr("exclude")?), + ("select", native_field(request, "select")?), + ("exclude", native_field(request, "exclude")?), ("discovered_inputs", discovered.clone()), ], )?; - let from_target: Bound<'_, PyAny> = request.getattr("from_target")?; + let from_target: Bound<'_, PyAny> = native_field(request, "from_target")?; if !discovered .getattr("project_config")? .getattr("targets")? @@ -388,7 +393,7 @@ fn run_contract_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult< let context: Bound<'_, PyAny> = resolve_adapter_connection_context( &discovered, &project_dir, - (py.None().into_bound(py), request.getattr("cli_vars")?), + (py.None().into_bound(py), native_field(request, "cli_vars")?), true, )?; let selection: Selection<'_> = select_resources(request, &discovered, &context)?; @@ -407,7 +412,7 @@ fn run_contract_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult< )?; } let report: ContractReport = report(&result)?; - let rendered: String = if request.getattr("json_output")?.is_truthy()? { + let rendered: String = if native_field(request, "json_output")?.is_truthy()? { render_contract_json(&report) .map_err(|error| PyValueError::new_err(format!("{error:?}")))? } else { 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 f563b0dd6a..2233529140 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/contract_command/write.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/contract_command/write.rs @@ -5,6 +5,7 @@ use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; use pyo3::types::{PyDict, PyDictMethods, PyList, PyListMethods, PyTuple}; 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, RecordId}; use crate::bindings::_helpers::records::registry; @@ -27,24 +28,24 @@ pub(crate) fn write_contracts<'py>( result: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = graph.py(); - let project: Bound<'py, PyAny> = graph.getattr("project")?; - let models: Bound<'py, PyDict> = by_name(&project.getattr("models")?)?; - let sources: Bound<'py, PyDict> = by_name(&project.getattr("sources")?)?; + 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> = imported(py, COMPILE_TYPES_MODULE, "CompiledResourceType")?.getattr("MODEL")?; let updates: Bound<'py, PyDict> = PyDict::new(py); let updated_evidence: Bound<'py, PyList> = PyList::empty(py); - for evidence in result.getattr("evidence")?.try_iter()? { + for evidence in native_field(result, "evidence")?.try_iter()? { let evidence: Bound<'py, PyAny> = evidence?; - let physical_columns: Bound<'py, PyAny> = evidence.getattr("physical_columns")?; - let findings: Bound<'py, PyAny> = evidence.getattr("findings")?; + let physical_columns: Bound<'py, PyAny> = native_field(&evidence, "physical_columns")?; + let findings: Bound<'py, PyAny> = native_field(&evidence, "findings")?; if physical_columns.is_none() || !findings.is_truthy()? { updated_evidence.append(evidence)?; continue; } - let name: Bound<'py, PyAny> = evidence.getattr("resource_name")?; + let name: Bound<'py, PyAny> = native_field(&evidence, "resource_name")?; let (path, edited): (Bound<'py, PyAny>, Bound<'py, PyAny>) = - if evidence.getattr("resource_type")?.eq(&model_type)? { + 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)?, @@ -55,13 +56,15 @@ pub(crate) fn write_contracts<'py>( ], )?; ( - write.project_dir.div(model.getattr("relative_path")?)?, + write + .project_dir + .div(native_field(&model, "relative_path")?)?, edited, ) } else { let source: Bound<'py, PyAny> = sources.as_any().get_item(&name)?; let path: Bound<'py, PyAny> = - source.getattr("source_file")?.getattr("file_path")?; + native_field(&source, "source_file")?.getattr("file_path")?; let edited: Bound<'py, PyAny> = call_with( ®istry::native(py, NativeAttr::EditContractSource)?, &[ @@ -143,7 +146,7 @@ fn applied<'py>( &[ ("discovered_inputs", discovered), ("adapter", write.adapter.clone()), - ("selected_target", result.getattr("from_target")?), + ("selected_target", native_field(result, "from_target")?), ("resolved_connection", write.resolved_connection.clone()), ("cli_vars", write.cli_vars.clone()), ("analysis_selection", selection), @@ -173,7 +176,7 @@ fn by_name<'py>(items: &Bound<'py, PyAny>) -> PyResult> { let named: Bound<'py, PyDict> = PyDict::new(items.py()); for item in items.try_iter()? { let item: Bound<'py, PyAny> = item?; - named.set_item(item.getattr("name")?, &item)?; + named.set_item(native_field(&item, "name")?, &item)?; } Ok(named) } 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 79b95cfef9..66a5399376 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/contracts/contract_diagnostics.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/contracts/contract_diagnostics.rs @@ -11,6 +11,7 @@ use crate::bindings::_helpers::compiled_project::project::NativeCompiledProject; use crate::bindings::_helpers::contracts::model_contracts::{ ConflictRow, DiagnosticRow, evaluate_project_model_contracts, project_promotion_conflicts, }; +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::sqltext::model_headers::{ColumnLocation, location_rows}; @@ -47,7 +48,7 @@ fn output_column_location<'py>( let recorded: Bound<'py, PyAny> = model .getattr("output_column_locations")? .call_method1("get", (column_name,))?; - let authored: Option = model.getattr("authored_sql")?.extract()?; + let authored: Option = native_field(model, "authored_sql")?.extract()?; let authored: String = authored.unwrap_or_default(); if !recorded.is_none() || authored.is_empty() { return Ok(Some(recorded).filter(|location| !location.is_none())); @@ -67,7 +68,7 @@ fn output_column_location<'py>( let location: Bound<'py, PyAny> = keyword_call( ®istry::class(py, RecordId::SourceLocation)?, vec![ - ("path", model.getattr("relative_path")?), + ("path", native_field(model, "relative_path")?), ("line", line.into_pyobject(py)?.into_any()), ("column", column.into_pyobject(py)?.into_any()), ("end_line", end_line.into_pyobject(py)?.into_any()), @@ -83,7 +84,7 @@ fn diagnostic_location<'py>( (declared_index, output_column): (Option, Option<&str>), ) -> PyResult> { let py: Python<'py> = model.py(); - let schema_entry: Bound<'py, PyAny> = model.getattr("schema_entry")?; + let schema_entry: Bound<'py, PyAny> = native_field(model, "schema_entry")?; if let Some(index) = declared_index && !schema_entry.is_none() { @@ -137,9 +138,9 @@ fn contract_diagnostic<'py>( ("code", code.into_pyobject(py)?.into_any()), ("message", message.into_pyobject(py)?.into_any()), ("resource_type", model_resource_type(py)?), - ("resource_name", model.getattr("name")?), + ("resource_name", native_field(model, "name")?), ("column_name", column_name.into_pyobject(py)?.into_any()), - ("path", model.getattr("relative_path")?), + ("path", native_field(model, "relative_path")?), ( "location", diagnostic_location(model, (declared_index, output_column.as_deref()))?, @@ -186,9 +187,9 @@ fn evaluate_model_contracts<'py>( &native, )?; let mut diagnostics: Vec> = Vec::new(); - for model in project.getattr("models")?.try_iter()? { + for model in native_field(project, "models")?.try_iter()? { let model: Bound<'py, PyAny> = model?; - let name: String = model.getattr("name")?.extract()?; + let name: String = native_field(&model, "name")?.extract()?; let Some((_, rows)) = outcomes.iter().rev().find(|(owner, _)| *owner == name) else { continue; }; 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 18679760d8..72fff61518 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/contracts/effective_config.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/contracts/effective_config.rs @@ -2,6 +2,7 @@ use std::hash::{Hash, Hasher}; +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::prelude::{ Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, PyStringMethods, Python, }; @@ -186,12 +187,12 @@ pub(crate) fn effective_config_target_name<'py>( local_config: &Bound<'py, PyAny>, selected: &Bound<'py, PyAny>, ) -> PyResult>> { - let mut defined: Vec> = keys(&project_config.getattr("targets")?)?; - defined.extend(keys(&local_config.getattr("targets")?)?); + let mut defined: Vec> = keys(&native_field(project_config, "targets")?)?; + defined.extend(keys(&native_field(local_config, "targets")?)?); let choice: TargetChoice> = TargetChoice { selected: optional_key(selected)?, - local: optional_key(&local_config.getattr("target")?)?, - default: optional_key(&project_config.getattr("default_target")?)?, + local: optional_key(&native_field(local_config, "target")?)?, + default: optional_key(&native_field(project_config, "default_target")?)?, defined, }; Ok(match effective_target_name(choice) { @@ -228,10 +229,10 @@ pub(crate) fn effective_config_target_fields<'py>( target: name, selected: is_set(&merged.fields[connection_name]) || is_set(&merged.fields[field_position("connection")]) - || project_config.getattr("connection")?.is_truthy()? - || local_config.getattr("connection")?.is_truthy()?, - project_names: keys(&project_config.getattr("connections")?)?, - local_names: keys(&local_config.getattr("connections")?)?, + || native_field(&project_config, "connection")?.is_truthy()? + || native_field(&local_config, "connection")?.is_truthy()?, + project_names: keys(&native_field(&project_config, "connections")?)?, + local_names: keys(&native_field(&local_config, "connections")?)?, }; match default_connection_name(choice) { Ok(Some(only)) => { @@ -261,13 +262,13 @@ fn effective_config_vars<'py>( cli_vars: &Bound<'py, PyAny>, ) -> PyResult> { let (project_config, local_config) = configs; - let project: Pairs<'py> = pairs(&project_config.getattr("vars")?)?; + let project: Pairs<'py> = pairs(&native_field(&project_config, "vars")?)?; let target: Pairs<'py> = if target_config.is_none() { Vec::new() } else { - pairs(&target_config.getattr("vars")?)? + pairs(&native_field(target_config, "vars")?)? }; - let local: Pairs<'py> = pairs(&local_config.getattr("vars")?)?; + let local: Pairs<'py> = pairs(&native_field(&local_config, "vars")?)?; let cli: Pairs<'py> = pairs(cli_vars)?; dictionary(py, effective_vars(&[&project, &target, &local, &cli])) } @@ -292,16 +293,16 @@ fn effective_config_connection<'py>( None } else { Some(( - optional_key(&target_config.getattr("connection_name")?)?, - pairs(&target_config.getattr("connection")?)?, + optional_key(&native_field(target_config, "connection_name")?)?, + pairs(&native_field(target_config, "connection")?)?, )) }; let layers: ConnectionLayers, Value<'py>> = ConnectionLayers { - project: pairs(&project_config.getattr("connection")?)?, + project: pairs(&native_field(project_config, "connection")?)?, target, - project_named: named(&project_config.getattr("connections")?)?, - local_named: named(&local_config.getattr("connections")?)?, - local: pairs(&local_config.getattr("connection")?)?, + project_named: named(&native_field(project_config, "connections")?)?, + local_named: named(&native_field(local_config, "connections")?)?, + local: pairs(&native_field(local_config, "connection")?)?, }; Ok(match effective_connection(layers) { Ok(connection) => (Some(dictionary(py, connection)?), None), 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 731a22f154..8be885312e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/contracts/spec_resolution.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/contracts/spec_resolution.rs @@ -16,6 +16,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -112,9 +113,9 @@ pub(crate) fn resolve_effective_adapter_name<'py>( project_config: &Bound<'py, PyAny>, local_config: &Bound<'py, PyAny>, ) -> PyResult> { - let local: Bound<'py, PyAny> = local_config.getattr("adapter")?; + let local: Bound<'py, PyAny> = native_field(local_config, "adapter")?; if local.is_none() { - return project_config.getattr("adapter"); + return native_field(project_config, "adapter"); } Ok(local) } @@ -147,8 +148,8 @@ fn resolve_effective_scenario_config<'py>( local_config: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = project_config.py(); - let project: Bound<'py, PyAny> = project_config.getattr("scenario")?; - let local: Bound<'py, PyAny> = local_config.getattr("scenario")?; + let project: Bound<'py, PyAny> = native_field(project_config, "scenario")?; + let local: Bound<'py, PyAny> = native_field(local_config, "scenario")?; let overrides: Bound<'py, PyDict> = PyDict::new(py); for item in project .getattr("local_type_overrides")? @@ -168,8 +169,8 @@ fn resolve_effective_scenario_config<'py>( .call_method1("setdefault", (dialect, PyDict::new(py)))? .call_method1("update", (rules,))?; } - let project_limits: Bound<'py, PyAny> = project.getattr("snapshot_limits")?; - let local_limits: Bound<'py, PyAny> = local.getattr("snapshot_limits")?; + let project_limits: Bound<'py, PyAny> = native_field(&project, "snapshot_limits")?; + let local_limits: Bound<'py, PyAny> = native_field(&local, "snapshot_limits")?; let limits: Vec<(&str, Bound<'py, PyAny>)> = SNAPSHOT_LIMITS .iter() .map(|field| { @@ -188,8 +189,8 @@ fn resolve_effective_scenario_config<'py>( ( "run_namespace", override_or( - local.getattr("run_namespace")?, - project.getattr("run_namespace")?, + native_field(&local, "run_namespace")?, + native_field(&project, "run_namespace")?, ), ), ("local_type_overrides", overrides.into_any()), @@ -212,7 +213,7 @@ fn scenario_local_type_overrides_for_dialect<'py>( sql_analysis_dialect: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = scenario_config.py(); - let rules: Bound<'py, PyAny> = scenario_config.getattr("local_type_overrides")?; + let rules: Bound<'py, PyAny> = native_field(scenario_config, "local_type_overrides")?; let overrides: Bound<'py, PyDict> = PyDict::new(py); overrides.call_method1( "update", @@ -286,7 +287,7 @@ fn loader_parts<'py>( /// Whether a family's optional `name_pattern` accepts `column_name`. fn family_accepts(family: &Bound<'_, PyAny>, column_name: &str) -> PyResult { - let pattern: Option = family.getattr("name_pattern")?.extract()?; + let pattern: Option = native_field(family, "name_pattern")?.extract()?; let Some(pattern) = pattern else { return Ok(true); }; 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 6bde63f1ba..e424f1c74e 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; use pyo3::prelude::{ @@ -84,7 +85,7 @@ pub(crate) fn raw_value( return sequence(value.downcast::()?.iter(), classes).map(RawSqlValue::List); } if value.is_instance(&classes.authored_set)? { - let items = value.getattr("values")?; + let items = native_field(value, "values")?; return sequence( items.try_iter()?.collect::>>()?.into_iter(), classes, @@ -93,7 +94,7 @@ pub(crate) fn raw_value( } if value.is_instance(&classes.value_call)? { let mut arguments: Vec<(String, RawSqlValue)> = Vec::new(); - for argument in value.getattr("arguments")?.try_iter()? { + for argument in native_field(value, "arguments")?.try_iter()? { let (name, item): (String, Bound<'_, PyAny>) = argument?.extract()?; arguments.push((name, raw_value(&item, classes)?)); } @@ -201,9 +202,9 @@ fn normalize_sql_value<'py>( _ => registry::class(py, RecordId::SqlValueLimits)?.call0()?, }; let limits = SqlValueLimits { - max_depth: applied.getattr("max_depth")?.extract()?, - max_elements: applied.getattr("max_elements")?.extract()?, - max_size: applied.getattr("max_size")?.extract()?, + max_depth: native_field(&applied, "max_depth")?.extract()?, + max_elements: native_field(&applied, "max_elements")?.extract()?, + max_size: native_field(&applied, "max_size")?.extract()?, }; match normalized(&raw, context, explicit_type, limits) { Ok(value) => sql_value(&value, &classes), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/contracts/state_literals.rs b/crates/sqlbuild-python/src/bindings/_helpers/contracts/state_literals.rs index cd02eb1c20..9d4ad26874 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/contracts/state_literals.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/contracts/state_literals.rs @@ -15,6 +15,7 @@ const BOOLEAN_TYPE: &str = "boolean"; const TIMESTAMP_TYPE: &str = "timestamp"; 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 sqlbuild_state::freshness::constants::TEXT_TIMESTAMP_STATE_VALUE_TYPE as TEXT_TIMESTAMP_TYPE; const DATE_TYPE: &str = "date"; const JSON_TYPE: &str = "json"; @@ -62,8 +63,8 @@ pub(crate) fn render_state_sql_literal( TIMESTAMP_TYPE | TEXT_TIMESTAMP_TYPE => timestamp_literal(value, declared_type), DATE_TYPE => { let datetime = py.import(DATETIME_MODULE)?; - if value.is_instance(&datetime.getattr("datetime")?)? - || !value.is_instance(&datetime.getattr("date")?)? + if value.is_instance(&native_field(&datetime, "datetime")?)? + || !value.is_instance(&native_field(&datetime, "date")?)? { return Err(rendering_error( py, @@ -83,15 +84,15 @@ pub(crate) fn render_state_sql_literal( fn timestamp_literal(value: &Bound<'_, PyAny>, declared_type: &str) -> PyResult { let py: Python<'_> = value.py(); let datetime = py.import(DATETIME_MODULE)?; - if !value.is_instance(&datetime.getattr("datetime")?)? { + if !value.is_instance(&native_field(&datetime, "datetime")?)? { return Err(rendering_error( py, "state timestamp columns require datetime values", )); } - let utc: Bound<'_, PyAny> = datetime.getattr("UTC")?; + let utc: Bound<'_, PyAny> = native_field(&datetime, "UTC")?; let normalized: Bound<'_, PyAny> = - if value.getattr("tzinfo")?.is_none() || value.call_method0("utcoffset")?.is_none() { + if native_field(value, "tzinfo")?.is_none() || value.call_method0("utcoffset")?.is_none() { let arguments: Bound<'_, PyDict> = PyDict::new(py); arguments.set_item("tzinfo", &utc)?; value.call_method("replace", (), Some(&arguments))? 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 5192cf7e9f..86b1ce0cac 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cost_command/bounds.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cost_command/bounds.rs @@ -12,6 +12,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; @@ -84,7 +85,7 @@ pub(crate) fn parse_bound<'py>( if value.chars().count() == ISO_DATE_LENGTH { return Ok(parsed); } - if parsed.getattr("tzinfo")?.is_none() { + if native_field(&parsed, "tzinfo")?.is_none() { return Err(cost_error( py, &CostCommandError::new( @@ -122,7 +123,7 @@ pub(crate) fn parse_since<'py>(py: Python<'py>, value: &str) -> PyResult { impl<'py> CostRequest<'py> { fn read(request: &Bound<'py, PyAny>) -> PyResult { let py: Python<'py> = request.py(); - let requested: Bound<'py, PyAny> = request.getattr("project_dir")?; - let order: Option = request.getattr("order")?.extract()?; + let requested: Bound<'py, PyAny> = native_field(request, "project_dir")?; + let order: Option = native_field(request, "order")?.extract()?; Ok(Self { request: request.clone(), project_dir: if requested.is_none() { @@ -45,14 +46,14 @@ impl<'py> CostRequest<'py> { } else { requested }, - selector: request.getattr("selector")?.extract()?, - limit: request.getattr("limit")?.extract()?, - no_limit: request.getattr("no_limit")?.is_truthy()?, + selector: native_field(request, "selector")?.extract()?, + limit: native_field(request, "limit")?.extract()?, + no_limit: native_field(request, "no_limit")?.is_truthy()?, descending: order .filter(|order| !order.is_empty()) .is_none_or(|order| order == DESCENDING_ORDER), - since: request.getattr("since")?.extract()?, - until: request.getattr("until")?.extract()?, + since: native_field(request, "since")?.extract()?, + until: native_field(request, "until")?.extract()?, }) } @@ -78,8 +79,7 @@ impl<'py> CostRequest<'py> { let options: Bound<'py, PyDict> = PyDict::new(py); options.set_item("parents", true)?; options.set_item("exist_ok", true)?; - path.getattr("parent")? - .call_method("mkdir", (), Some(&options))?; + native_field(&path, "parent")?.call_method("mkdir", (), Some(&options))?; let encoding: Bound<'py, PyDict> = PyDict::new(py); encoding.set_item("encoding", "utf-8")?; path.call_method("write_text", (format!("{payload}\n"),), Some(&encoding))?; @@ -116,7 +116,7 @@ fn in_window( since: Option<&Bound<'_, PyAny>>, (until, until_is_date): (Option<&Bound<'_, PyAny>>, bool), ) -> PyResult { - let completed: Bound<'_, PyAny> = record.getattr("completed_at")?; + let completed: Bound<'_, PyAny> = native_field(record, "completed_at")?; if let Some(since) = since && !completed.ge(since)? { @@ -224,7 +224,7 @@ fn resolve_run<'py>(request: &CostRequest<'py>) -> PyResult> { } let run_ids: Vec = records .iter() - .map(|record| record.getattr("run_id")?.extract()) + .map(|record| native_field(record, "run_id")?.extract()) .collect::>()?; let index: usize = select_cost_run(&run_ids, &request.selector).map_err(|error| cost_error(py, &error))?; @@ -240,7 +240,8 @@ fn with_resources<'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((record.getattr("cost")?,), Some(&cost_changes))?; + let cost: Bound<'py, PyAny> = + replace.call((native_field(record, "cost")?,), Some(&cost_changes))?; let changes: Bound<'py, PyDict> = PyDict::new(py); changes.set_item("cost", cost)?; replace.call((record,), Some(&changes)) 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 c4e0174cab..4d0feb89e9 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cost_command/refresh.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cost_command/refresh.rs @@ -14,15 +14,18 @@ use crate::bindings::_helpers::compile_driver::python_calls::{call_with, importe 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; fn coverage(summary: &Bound<'_, PyAny>) -> PyResult { Ok(CostCoverage { - status: summary.getattr("status")?.getattr("value")?.extract()?, - expected_statement_count: summary.getattr("expected_statement_count")?.extract()?, - observed_statement_count: summary.getattr("observed_statement_count")?.extract()?, - query_count: summary.getattr("query_count")?.extract()?, + status: native_field(summary, "status")? + .getattr("value")? + .extract()?, + expected_statement_count: native_field(summary, "expected_statement_count")?.extract()?, + observed_statement_count: native_field(summary, "observed_statement_count")?.extract()?, + query_count: native_field(summary, "query_count")?.extract()?, limitations: summary .getattr("limitations")? .try_iter()? @@ -69,7 +72,7 @@ fn target_database<'py>( ) -> PyResult> { let py: Python<'py> = target_config.py(); if !target_config.is_none() { - let database: Bound<'py, PyAny> = target_config.getattr("database")?; + let database: Bound<'py, PyAny> = native_field(target_config, "database")?; if !database.is_none() { return Ok(database); } @@ -82,11 +85,11 @@ fn target_database<'py>( } fn refresh(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult<()> { - let selector: Bound<'_, PyAny> = request.getattr("selector")?; + let selector: Bound<'_, PyAny> = native_field(request, "selector")?; if selector.extract::()? == HISTORY_SELECTOR { return Ok(()); } - let requested: Bound<'_, PyAny> = request.getattr("project_dir")?; + let requested: Bound<'_, PyAny> = native_field(request, "project_dir")?; let project_dir: Bound<'_, PyAny> = if requested.is_none() { imported(py, "pathlib", "Path")?.call_method0("cwd")? } else { @@ -96,12 +99,14 @@ fn refresh(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult<()> { if record.is_none() { return Ok(()); } - let current: Bound<'_, PyAny> = record.getattr("cost")?; - let status: String = current.getattr("status")?.getattr("value")?.extract()?; + let current: Bound<'_, PyAny> = native_field(&record, "cost")?; + let status: String = native_field(¤t, "status")? + .getattr("value")? + .extract()?; if !REFRESHABLE_STATUSES.contains(&status.as_str()) { return Ok(()); } - let target_name: Bound<'_, PyAny> = record.getattr("target_name")?; + let target_name: Bound<'_, PyAny> = native_field(&record, "target_name")?; let discovered: Bound<'_, PyAny> = call_with( ®istry::native(py, NativeAttr::DiscoverProjectInputs)?, &[("project_dir", project_dir.clone())], @@ -116,8 +121,11 @@ fn refresh(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult<()> { let adapter_name: Bound<'_, PyAny> = call_with( ®istry::native(py, NativeAttr::ResolveEffectiveAdapterName)?, &[ - ("project_config", discovered.getattr("project_config")?), - ("local_config", discovered.getattr("local_config")?), + ( + "project_config", + native_field(&discovered, "project_config")?, + ), + ("local_config", native_field(&discovered, "local_config")?), ], )?; let adapter: Bound<'_, PyAny> = resolve_cli_adapter(&adapter_name, &project_dir)?; @@ -140,14 +148,14 @@ fn refresh(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult<()> { ("selected_target", target_name), ], )?; - let run_id: Bound<'_, PyAny> = record.getattr("run_id")?; + let run_id: Bound<'_, PyAny> = native_field(&record, "run_id")?; let ledger: Bound<'_, PyAny> = project_dir .call_method1("__truediv__", ("target",))? .call_method1("__truediv__", ("executions",))? .call_method1("__truediv__", (&run_id,))? .call_method1("__truediv__", ("statements.jsonl",))?; let summary: Bound<'_, PyAny> = call_with( - &adapter.getattr("collect_run_cost")?, + &native_field(&adapter, "collect_run_cost")?, &[ ("connection_config", connection_config.clone()), ( @@ -155,11 +163,11 @@ fn refresh(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult<()> { target_database(&target_config, &connection_config)?, ), ("run_id", run_id), - ("started_at", record.getattr("started_at")?), - ("completed_at", record.getattr("completed_at")?), + ("started_at", native_field(&record, "started_at")?), + ("completed_at", native_field(&record, "completed_at")?), ("statement_ledger_path", ledger), - ("usd_per_credit", current.getattr("usd_per_credit")?), - ("rate_source", current.getattr("rate_source")?), + ("usd_per_credit", native_field(¤t, "usd_per_credit")?), + ("rate_source", native_field(¤t, "rate_source")?), ], )?; if accepts_cost_refresh(&coverage(¤t)?, &coverage(&summary)?) != Some(true) { 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 3d33b10470..0727f3360e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cost_command/sorting.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cost_command/sorting.rs @@ -6,6 +6,7 @@ use pyo3::types::{PyCFunction, PyDict, PyDictMethods, PyInt, PyList, PyTuple, Py use sqlbuild_core::cost_command::constants::{HISTORY_METRIC_SORT_FIELDS, MISSING_METRIC_STATUSES}; use crate::bindings::_helpers::compile_driver::python_calls::imported; +use crate::bindings::_helpers::records::fields::field as native_field; type SortKey = fn(&Bound<'_, PyAny>, &str) -> PyResult>; @@ -45,13 +46,14 @@ fn history_value(record: &Bound<'_, PyAny>, field: &str) -> PyResult> "credits" => attribute(record, "cost.estimated_compute_credits"), "duration" => Ok(record .getattr("completed_at")? - .sub(record.getattr("started_at")?)? + .sub(native_field(record, "started_at")?)? .unbind()), - "models" => Ok( - PyInt::new(py, record.getattr("cost")?.getattr("resources")?.len()?) - .into_any() - .unbind(), - ), + "models" => Ok(PyInt::new( + py, + native_field(record, "cost")?.getattr("resources")?.len()?, + ) + .into_any() + .unbind()), "status" => attribute(record, "cost.status.value"), _ => Err(PyKeyError::new_err(field.to_owned())), } 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 2c5d62f342..cd5c5e490b 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cost_report/degradation.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cost_report/degradation.rs @@ -17,6 +17,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; @@ -161,7 +162,7 @@ fn cost_record<'py>( fields.push(("cost", summary)); fields.push(( "had_executable_work", - finalization.getattr("had_executable_work")?, + native_field(finalization, "had_executable_work")?, )); call_with(®istry::class(py, RecordId::CostRunRecord)?, &fields) } @@ -176,7 +177,7 @@ fn summary<'py>( &host::entry(py, ExecutorHost::RunCostSummary)?, &[ ("status", status), - ("usd_per_credit", config.getattr("usd_per_credit")?), + ("usd_per_credit", native_field(config, "usd_per_credit")?), ("rate_source", PyString::new(py, rate_source).into_any()), ("message", message), ], @@ -189,9 +190,9 @@ fn persist<'py>( stage: &str, ) -> PyResult<()> { persist_cost_run( - finalization.getattr("project_dir")?.extract()?, + native_field(finalization, "project_dir")?.extract()?, record, - &finalization.getattr("output_stream")?, + &native_field(finalization, "output_stream")?, stage, ) } @@ -212,7 +213,7 @@ fn collection_failure<'py>( )?; let error_type: String = error_type(py, error)?; warn_logging_to( - &finalization.getattr("output_stream")?, + &native_field(finalization, "output_stream")?, &format!("Cost telemetry failed during {stage} ({error_type}); build result unchanged."), COLLECTION_LOGGER_NAME, )?; @@ -237,8 +238,8 @@ fn collected_summary<'py>( (config, rate_source): (&Bound<'py, PyAny>, &str), ) -> PyResult> { let py: Python<'py> = finalization.py(); - let run_id: Bound<'py, PyAny> = finalization.getattr("run_id")?; - let mut ledger: Bound<'py, PyAny> = finalization.getattr("project_dir")?; + let run_id: Bound<'py, PyAny> = native_field(finalization, "run_id")?; + let mut ledger: Bound<'py, PyAny> = native_field(finalization, "project_dir")?; for part in STATEMENT_LEDGER_PATH { ledger = if part.is_empty() { ledger.div(&run_id)? @@ -247,18 +248,21 @@ fn collected_summary<'py>( }; } call_with( - &adapter.getattr("collect_run_cost")?, + &native_field(adapter, "collect_run_cost")?, &[ ( "connection_config", - finalization.getattr("connection_config")?, + native_field(finalization, "connection_config")?, + ), + ( + "target_database", + native_field(finalization, "target_database")?, ), - ("target_database", finalization.getattr("target_database")?), ("run_id", run_id), - ("started_at", finalization.getattr("started_at")?), - ("completed_at", finalization.getattr("completed_at")?), + ("started_at", native_field(finalization, "started_at")?), + ("completed_at", native_field(finalization, "completed_at")?), ("statement_ledger_path", ledger), - ("usd_per_credit", config.getattr("usd_per_credit")?), + ("usd_per_credit", native_field(config, "usd_per_credit")?), ("rate_source", PyString::new(py, rate_source).into_any()), ], ) @@ -270,12 +274,12 @@ pub(crate) fn finalize_build_cost<'py>( finalization: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = finalization.py(); - let adapter: Bound<'py, PyAny> = finalization.getattr("adapter")?; + let adapter: Bound<'py, PyAny> = native_field(finalization, "adapter")?; if !cost_aware(&adapter)? { return Ok(py.None().into_bound(py)); } - let config: Bound<'py, PyAny> = finalization.getattr("config")?; - let rate_source: String = if config.getattr("usd_per_credit_is_default")?.is_truthy()? { + let config: Bound<'py, PyAny> = native_field(finalization, "config")?; + let rate_source: String = if native_field(&config, "usd_per_credit_is_default")?.is_truthy()? { DEFAULT_RATE_SOURCE.to_owned() } else { CONFIGURED_RATE_SOURCE.to_owned() @@ -290,7 +294,7 @@ pub(crate) fn finalize_build_cost<'py>( if capability.eq(host::entry(py, ExecutorHost::CostCapability)?.getattr("NONE")?)? { return Ok(py.None().into_bound(py)); } - let cost: Bound<'py, PyAny> = if finalization.getattr("collect")?.is_truthy()? { + let cost: Bound<'py, PyAny> = if native_field(finalization, "collect")?.is_truthy()? { match collected_summary(finalization, &adapter, rate) { Ok(summary) => summary, Err(error) => return collection_failure(finalization, rate, "collection", &error), @@ -298,20 +302,20 @@ pub(crate) fn finalize_build_cost<'py>( } else { summary( py, - finalization.getattr("cost_status")?, + native_field(finalization, "cost_status")?, rate, - finalization.getattr("cost_message")?, + native_field(finalization, "cost_message")?, )? }; let record: Bound<'py, PyAny> = cost_record(finalization, cost)?; persist(finalization, &record, "artifact persistence")?; - if !finalization.getattr("render")?.is_truthy()? { + if !native_field(finalization, "render")?.is_truthy()? { return Ok(record); } write_build_cost_breakdown( &record, - &finalization.getattr("output_stream")?, - finalization.getattr("use_color")?.is_truthy()?, + &native_field(finalization, "output_stream")?, + native_field(finalization, "use_color")?.is_truthy()?, )?; Ok(record) } 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 0b276d5592..f1358d228e 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; @@ -49,7 +50,7 @@ fn resource(value: &Bound<'_, PyAny>) -> PyResult { warehouse_name: text(value, "warehouse_name")?, warehouse_size: text(value, "warehouse_size")?, warehouse_type: optional_text(value, "warehouse_type")?, - cluster_number: value.getattr("cluster_number")?.extract()?, + cluster_number: native_field(value, "cluster_number")?.extract()?, query_count: integer(value, "query_count")?, attributed_seconds: decimal(value, "attributed_seconds")?, bytes_scanned: integer(value, "bytes_scanned")?, @@ -60,15 +61,15 @@ fn resource(value: &Bound<'_, PyAny>) -> PyResult { fn summary(value: &Bound<'_, PyAny>) -> PyResult { let mut resources: Vec = Vec::new(); - for item in value.getattr("resources")?.try_iter()? { + for item in native_field(value, "resources")?.try_iter()? { resources.push(resource(&item?)?); } let mut limitations: Vec = Vec::new(); - for item in value.getattr("limitations")?.try_iter()? { + for item in native_field(value, "limitations")?.try_iter()? { limitations.push(item?.extract()?); } Ok(CostSummary { - status: value.getattr("status")?.getattr("value")?.extract()?, + status: native_field(value, "status")?.getattr("value")?.extract()?, usd_per_credit: decimal(value, "usd_per_credit")?, rate_source: text(value, "rate_source")?, resources, @@ -97,9 +98,9 @@ pub(crate) fn cost_run(record: &Bound<'_, PyAny>) -> PyResult { build_status: text(record, "build_status")?, started_at: timestamp(record, "started_at")?, completed_at: timestamp(record, "completed_at")?, - cost: summary(&record.getattr("cost")?)?, + cost: summary(&native_field(record, "cost")?)?, version: integer(record, "version")?, - had_executable_work: record.getattr("had_executable_work")?.extract()?, + had_executable_work: native_field(record, "had_executable_work")?.extract()?, }) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cost_report/store.rs b/crates/sqlbuild-python/src/bindings/_helpers/cost_report/store.rs index bf599f24ad..2679840a9f 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cost_report/store.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cost_report/store.rs @@ -12,6 +12,7 @@ use sqlbuild_core::cost_report::main::read_cost_artifact::read_cost_artifact; use sqlbuild_core::cost_report::models::CostRun; use crate::bindings::_helpers::cost_report::records::artifact_error; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::result_output::integration_values::json_value; const ARTIFACT_NAME: &str = "run.json"; @@ -52,7 +53,7 @@ fn load(py: Python<'_>, path: &Path) -> PyResult = py.import("json")?; let payload: Bound<'_, PyAny> = match json.call_method1("loads", (text,)) { Ok(payload) => payload, - Err(error) if error.is_instance(py, &json.getattr("JSONDecodeError")?) => { + Err(error) if error.is_instance(py, &native_field(&json, "JSONDecodeError")?) => { return Ok(Err(invalid("cost run artifact is not valid JSON"))); } Err(error) => return Err(error), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cost_telemetry/allocation.rs b/crates/sqlbuild-python/src/bindings/_helpers/cost_telemetry/allocation.rs index 6eb15f25a6..eaec7b0afa 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cost_telemetry/allocation.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cost_telemetry/allocation.rs @@ -15,6 +15,7 @@ use crate::bindings::_helpers::cost_telemetry::values::{ }; 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; @@ -24,19 +25,19 @@ const PENDING_MESSAGE: &str = "Snowflake query history is not complete yet."; pub(crate) fn cost_query(observation: &Bound<'_, PyAny>, run_id: &str) -> PyResult { let text = |name: &str| -> PyResult> { observation.getattr(name)?.extract() }; Ok(CostQuery { - query_id: observation.getattr("query_id")?.extract()?, + query_id: native_field(observation, "query_id")?.extract()?, warehouse: WarehouseKey { - name: observation.getattr("warehouse_name")?.extract()?, - size: observation.getattr("warehouse_size")?.extract()?, + name: native_field(observation, "warehouse_name")?.extract()?, + size: native_field(observation, "warehouse_size")?.extract()?, kind: text("warehouse_type")?, - cluster: observation.getattr("cluster_number")?.extract()?, + cluster: native_field(observation, "cluster_number")?.extract()?, }, - started_micros: instant_micros(&observation.getattr("started_at")?)?, - completed_micros: instant_micros(&observation.getattr("completed_at")?)?, + started_micros: instant_micros(&native_field(observation, "started_at")?)?, + completed_micros: instant_micros(&native_field(observation, "completed_at")?)?, in_run: text("run_id")?.as_deref() == Some(run_id), resource_type: text("resource_type")?, resource_name: text("resource_name")?, - bytes_scanned: observation.getattr("bytes_scanned")?.extract()?, + bytes_scanned: native_field(observation, "bytes_scanned")?.extract()?, }) } 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 fbc922604d..a45f185962 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cost_telemetry/context.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cost_telemetry/context.rs @@ -11,6 +11,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::{ use crate::bindings::_helpers::cost_telemetry::values::process_object; 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; const CONTEXT_VARIABLE_NAME: &str = "sqlbuild_cost_resource_context"; const DEFAULT_PHASE: &str = "execute"; @@ -161,14 +162,14 @@ impl CostContext { let context: Bound<'_, PyAny> = call_with( &host::entry(py, ExecutorHost::CostResourceContext)?, &[ - ("run_id", current.getattr("run_id")?), + ("run_id", native_field(¤t, "run_id")?), ("resource_type", arguments.value("resource_type")?), ("resource_name", arguments.value("resource_name")?), - ("ledger_path", current.getattr("ledger_path")?), + ("ledger_path", native_field(¤t, "ledger_path")?), ( "phase", if phase.is_none() { - current.getattr("phase")? + native_field(¤t, "phase")? } else { phase }, @@ -176,7 +177,7 @@ impl CostContext { ("attempt", attempt), ( "on_statement_complete", - current.getattr("on_statement_complete")?, + native_field(¤t, "on_statement_complete")?, ), ], )?; 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 04eb73e589..45d4cd2a1a 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cost_telemetry/ledger.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cost_telemetry/ledger.rs @@ -21,6 +21,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::{ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; @@ -61,25 +62,30 @@ fn ledger_line(arguments: &KeywordArguments<'_>, context: &Bound<'_, PyAny>) -> } else { Some(error.get_type().name()?.extract()?) }; - let attempt: String = context.getattr("attempt")?.extract::()?.to_string(); + let attempt: String = native_field(context, "attempt")? + .extract::()? + .to_string(); let isoformat = |name: &str| -> PyResult { text(&arguments.value(name)?.call_method0("isoformat")?) }; let mut fields: Vec<(String, JsonValue)> = vec![ - ("run_id".to_owned(), text(&context.getattr("run_id")?)?), + ( + "run_id".to_owned(), + text(&native_field(context, "run_id")?)?, + ), ( "statement_id".to_owned(), text(&arguments.value("statement_id")?)?, ), ( "resource_type".to_owned(), - text(&context.getattr("resource_type")?)?, + text(&native_field(context, "resource_type")?)?, ), ( "resource_name".to_owned(), - text(&context.getattr("resource_name")?)?, + text(&native_field(context, "resource_name")?)?, ), - ("phase".to_owned(), text(&context.getattr("phase")?)?), + ("phase".to_owned(), text(&native_field(context, "phase")?)?), ( "attempt".to_owned(), JsonValue::Integer(JsonInteger::parse(&attempt).ok_or_else(|| { @@ -139,7 +145,7 @@ fn record_statement(py: Python<'_>, options: Option>) -> PyRes ), )?; let context: Bound<'_, PyAny> = arguments.value("context")?; - let path: Bound<'_, PyAny> = context.getattr("ledger_path")?; + let path: Bound<'_, PyAny> = native_field(&context, "ledger_path")?; if path.is_none() { return Ok(()); } @@ -153,7 +159,7 @@ fn record_statement(py: Python<'_>, options: Option>) -> PyRes match appended { Ok(()) => Ok(()), Err(error) if error.is_instance_of::(py) => { - mark_failure(&context.getattr("run_id")?, error.value(py).as_any()) + mark_failure(&native_field(&context, "run_id")?, error.value(py).as_any()) } Err(error) => Err(error), } 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 36713b0271..c08efd5777 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cost_telemetry/snowflake.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cost_telemetry/snowflake.rs @@ -22,6 +22,7 @@ use crate::bindings::_helpers::cost_telemetry::allocation::{ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; @@ -441,7 +442,7 @@ fn ledger_attributed<'py>( ) -> PyResult> { let py: Python<'py> = observation.py(); let entry: Option> = - pyo3::types::PyDictMethods::get_item(ledger, observation.getattr("query_id")?)?; + pyo3::types::PyDictMethods::get_item(ledger, native_field(observation, "query_id")?)?; let field = |name: &str| -> PyResult> { match &entry { Some(entry) => entry.getattr(name), @@ -478,7 +479,7 @@ fn collect<'py>( let failure: Option = consume_ledger_failure(py, &run_id)?; let by_query_id: Bound<'py, PyDict> = PyDict::new(py); for entry in &ledger { - let id: Bound<'py, PyAny> = entry.getattr("query_id")?; + let id: Bound<'py, PyAny> = native_field(entry, "query_id")?; if !id.is_none() { by_query_id.set_item(id, entry)?; } @@ -487,7 +488,7 @@ fn collect<'py>( let mut classification: Vec = Vec::new(); for (id, entry) in pyo3::types::PyDictMethods::iter(&by_query_id) { let id: String = id.extract()?; - if !entry.getattr("resource_type")?.eq(HOOK_RESOURCE_TYPE)? { + if !native_field(&entry, "resource_type")?.eq(HOOK_RESOURCE_TYPE)? { classification.push(id.clone()); } expected_ids.insert(id); diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cursor/conversion.rs b/crates/sqlbuild-python/src/bindings/_helpers/cursor/conversion.rs index d28ff6345e..51349afb9f 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cursor/conversion.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cursor/conversion.rs @@ -20,6 +20,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; @@ -78,7 +79,7 @@ impl<'py> CursorSession<'py> { } fn timestamp(&self, inner: &Bound<'py, PyAny>, handle: usize) -> PyResult { - let tzinfo: Bound<'py, PyAny> = inner.getattr("tzinfo")?; + let tzinfo: Bound<'py, PyAny> = native_field(inner, "tzinfo")?; let offset: ZoneOffset = if tzinfo.is_none() { ZoneOffset::Naive } else if tzinfo @@ -93,7 +94,7 @@ impl<'py> CursorSession<'py> { }; Ok(Timestamp { local: local_date_time(inner)?, - fold: inner.getattr("fold")?.extract::()? != 0, + fold: native_field(inner, "fold")?.extract::()? != 0, offset, template: Some(handle), source_text: None, @@ -102,7 +103,7 @@ impl<'py> CursorSession<'py> { /// A typed cursor value object as a scalar remembering the object. pub(crate) fn scalar(&self, value: &Bound<'py, PyAny>) -> PyResult { - let inner: Bound<'py, PyAny> = value.getattr("value")?; + let inner: Bound<'py, PyAny> = native_field(value, "value")?; let handle: usize = self.inputs.borrow().len(); let typed: ScalarValue = if value.is_instance(&self.integer_class)? { ScalarValue::Integer(inner.extract()?) @@ -110,7 +111,7 @@ impl<'py> CursorSession<'py> { ScalarValue::Date(civil_date(&inner)?) } else { let mut timestamp: Timestamp = self.timestamp(&inner, handle)?; - let source_text: Bound<'py, PyAny> = value.getattr("source_text")?; + let source_text: Bound<'py, PyAny> = native_field(value, "source_text")?; timestamp.source_text = (!source_text.is_none()) .then(|| source_text.extract()) .transpose()?; @@ -141,9 +142,9 @@ impl<'py> CursorSession<'py> { /// An `AlignedInterval` as an interval. pub(crate) fn interval(&self, interval: &Bound<'py, PyAny>) -> PyResult { Ok(Interval { - start: self.scalar(&interval.getattr("start")?)?, - end: self.scalar(&interval.getattr("end")?)?, - grain: self.grain(&interval.getattr("grain")?)?, + start: self.scalar(&native_field(interval, "start")?)?, + end: self.scalar(&native_field(interval, "end")?)?, + grain: self.grain(&native_field(interval, "grain")?)?, }) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cursor/durations.rs b/crates/sqlbuild-python/src/bindings/_helpers/cursor/durations.rs index 1fbc70322f..ef510ee59f 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cursor/durations.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cursor/durations.rs @@ -13,6 +13,7 @@ use crate::bindings::_helpers::cursor::conversion::CursorSession; use crate::bindings::_helpers::cursor::values::{ DATETIME_MODULE, civil_date, local_date_time, replaced, }; +use crate::bindings::_helpers::records::fields::field as native_field; const MONTHS_PER_YEAR: i64 = 12; @@ -64,7 +65,7 @@ pub(crate) fn shifted_moment<'py>( let typed: Moment = if is_timestamp { Moment::Timestamp { local: local_date_time(moment)?, - fold: moment.getattr("fold")?.extract::()? != 0, + fold: native_field(moment, "fold")?.extract::()? != 0, } } else { Moment::Date(civil_date(moment)?) @@ -73,7 +74,7 @@ pub(crate) fn shifted_moment<'py>( match shifted { Moment::Timestamp { local, fold } => replaced(moment, &local, fold), Moment::Date(date) => call_with( - &moment.getattr("replace")?, + &native_field(moment, "replace")?, &[ ("year", date.year.into_pyobject(py)?.into_any()), ("month", date.month.into_pyobject(py)?.into_any()), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cursor/intervals.rs b/crates/sqlbuild-python/src/bindings/_helpers/cursor/intervals.rs index fadaa324d8..a3d30b79ec 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cursor/intervals.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cursor/intervals.rs @@ -22,6 +22,7 @@ use crate::bindings::_helpers::cursor::conversion::CursorSession; use crate::bindings::_helpers::cursor::scalars::as_scalar; 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; const RAW_INTERVAL_CURSOR_TYPES: [&str; 2] = ["timestamp", "integer"]; @@ -121,8 +122,8 @@ fn merge_raw_intervals<'py>( for interval in intervals.try_iter()? { let interval: Bound<'py, PyAny> = interval?; parsed.push(RawInterval { - start: as_scalar(&session, &interval.getattr("start")?, cursor_type)?, - end: as_scalar(&session, &interval.getattr("end")?, cursor_type)?, + start: as_scalar(&session, &native_field(&interval, "start")?, cursor_type)?, + end: as_scalar(&session, &native_field(&interval, "end")?, cursor_type)?, interval, }); } @@ -153,8 +154,8 @@ fn merge_raw_intervals<'py>( last.interval = call_with( &class, &[ - ("start", last.interval.getattr("start")?), - ("end", next.interval.getattr("end")?), + ("start", native_field(&last.interval, "start")?), + ("end", native_field(&next.interval, "end")?), ], )?; last.end = next.end; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cursor/overrides.rs b/crates/sqlbuild-python/src/bindings/_helpers/cursor/overrides.rs index a36a0be5fd..d3178e8364 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cursor/overrides.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cursor/overrides.rs @@ -8,6 +8,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; @@ -26,12 +27,12 @@ pub(crate) fn cursor_timestamp<'py>(value: &Bound<'py, PyAny>) -> PyResult = imported(py, DATETIME_MODULE, "datetime")?; if instance_of(value, RecordId::DateValue)? { - let midnight: Bound<'py, PyAny> = datetime.getattr("min")?.call_method0("time")?; - return datetime.call_method1("combine", (value.getattr("value")?, midnight)); + let midnight: Bound<'py, PyAny> = native_field(&datetime, "min")?.call_method0("time")?; + return datetime.call_method1("combine", (native_field(value, "value")?, midnight)); } if instance_of(value, RecordId::IntegerValue)? { return Err(algebra_error(py, INTEGER_AS_TIMESTAMP)); @@ -46,7 +47,7 @@ pub(crate) fn cursor_integer<'py>(value: &Bound<'py, PyAny>) -> PyResult, error: &PyErr) -> PyResult { /// `value.astimezone(UTC)` for an aware `datetime`, else `value`. fn utc_normalized<'py>(value: &Bound<'py, PyAny>) -> PyResult> { - if value.getattr("tzinfo")?.is_none() { + if native_field(value, "tzinfo")?.is_none() { return Ok(value.clone()); } value.call_method1( @@ -228,7 +229,7 @@ pub(crate) fn render_value(value: &Bound<'_, PyAny>) -> PyResult { #[pyo3(signature = (*, sentinel))] pub(crate) fn sentinel_to_token(sentinel: &Bound<'_, PyAny>) -> PyResult { if sentinel.is_instance(&host::entry(sentinel.py(), ExecutorHost::BoundSentinel)?)? { - return sentinel.getattr("value")?.extract(); + return native_field(sentinel, "value")?.extract(); } render_value(sentinel) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cursor/values.rs b/crates/sqlbuild-python/src/bindings/_helpers/cursor/values.rs index 4c7e219b8b..0724d362da 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/cursor/values.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/cursor/values.rs @@ -5,6 +5,7 @@ use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; use sqlbuild_cursor::algebra::models::{CivilDate, LocalDateTime}; use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; +use crate::bindings::_helpers::records::fields::field as native_field; pub(crate) const DATETIME_MODULE: &str = "datetime"; const MICROS_PER_SECOND: i64 = 1_000_000; @@ -13,9 +14,9 @@ const SECONDS_PER_DAY: i64 = 86_400; /// The year, month and day of a `date` or `datetime`. pub(crate) fn civil_date(value: &Bound<'_, PyAny>) -> PyResult { Ok(CivilDate { - year: value.getattr("year")?.extract()?, - month: value.getattr("month")?.extract()?, - day: value.getattr("day")?.extract()?, + year: native_field(value, "year")?.extract()?, + month: native_field(value, "month")?.extract()?, + day: native_field(value, "day")?.extract()?, }) } @@ -23,18 +24,18 @@ pub(crate) fn civil_date(value: &Bound<'_, PyAny>) -> PyResult { pub(crate) fn local_date_time(value: &Bound<'_, PyAny>) -> PyResult { Ok(LocalDateTime { date: civil_date(value)?, - hour: value.getattr("hour")?.extract()?, - minute: value.getattr("minute")?.extract()?, - second: value.getattr("second")?.extract()?, - microsecond: value.getattr("microsecond")?.extract()?, + hour: native_field(value, "hour")?.extract()?, + minute: native_field(value, "minute")?.extract()?, + second: native_field(value, "second")?.extract()?, + microsecond: native_field(value, "microsecond")?.extract()?, }) } /// A `timedelta` as whole microseconds. pub(crate) fn timedelta_micros(delta: &Bound<'_, PyAny>) -> PyResult { - let days: i64 = delta.getattr("days")?.extract()?; - let seconds: i64 = delta.getattr("seconds")?.extract()?; - let microseconds: i64 = delta.getattr("microseconds")?.extract()?; + let days: i64 = native_field(delta, "days")?.extract()?; + let seconds: i64 = native_field(delta, "seconds")?.extract()?; + let microseconds: i64 = native_field(delta, "microseconds")?.extract()?; Ok((days * SECONDS_PER_DAY + seconds) * MICROS_PER_SECOND + microseconds) } @@ -46,7 +47,7 @@ pub(crate) fn replaced<'py>( ) -> PyResult> { let py: Python<'py> = template.py(); call_with( - &template.getattr("replace")?, + &native_field(template, "replace")?, &[ ("year", local.date.year.into_pyobject(py)?.into_any()), ("month", local.date.month.into_pyobject(py)?.into_any()), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/dag_artifact/artifact.rs b/crates/sqlbuild-python/src/bindings/_helpers/dag_artifact/artifact.rs index 5a25b40ee2..a4a36ea427 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dag_artifact/artifact.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dag_artifact/artifact.rs @@ -11,6 +11,7 @@ use sqlbuild_core::json::errors::JsonEmitError; use crate::bindings::_helpers::boundary::panics::compiler_guard; use crate::bindings::_helpers::dag_artifact::rows::{project_input, python_graph_input}; use crate::bindings::_helpers::graph::project_graph::NativeProjectGraph; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::ErrorId; use crate::bindings::_helpers::records::registry; @@ -38,12 +39,12 @@ pub(crate) fn build_dag_json( ) -> PyResult { compiler_guard(|| { let py = graph.py(); - let project: DagProjectInput = project_input(&graph.getattr("project")?)?; + let project: DagProjectInput = project_input(&native_field(graph, "project")?)?; let python: Option = python_graph .filter(|python_graph| !python_graph.is_none()) .map(python_graph_input) .transpose()?; - let native: Bound<'_, PyAny> = graph.getattr("native")?; + let native: Bound<'_, PyAny> = native_field(graph, "native")?; let native: PyRef<'_, NativeProjectGraph> = native.extract()?; build(native.graph(), &project, python.as_ref(), project_name) .map_err(|error| python_error(error, py)) 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 1cb78168a9..3ef6e485fe 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dag_artifact/rows.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dag_artifact/rows.rs @@ -14,6 +14,7 @@ use sqlbuild_analysis::dag_artifact::types::DagValue; use sqlbuild_analysis::dag_artifact::types::DagColumnLineage; use crate::bindings::_helpers::python_values::values::python_value; +use crate::bindings::_helpers::records::fields::field as native_field; const AUDIT_SEVERITY_MODULE: &str = "sqlbuild.compiler.auditing.types"; const DECIMAL_KIND: &str = "decimal"; @@ -44,7 +45,7 @@ fn enum_value(value: &Bound<'_, PyAny>, name: &str) -> PyResult> if member.is_none() { return Ok(None); } - member.getattr("value")?.extract().map(Some) + native_field(&member, "value")?.extract().map(Some) } fn texts(value: &Bound<'_, PyAny>, name: &str) -> PyResult> { @@ -384,7 +385,7 @@ fn check_severity(node: &Bound<'_, PyAny>) -> PyResult> { if check.is_none() { return Ok(None); } - let severity: Bound<'_, PyAny> = check.getattr("severity")?.getattr("value")?; + let severity: Bound<'_, PyAny> = native_field(&check, "severity")?.getattr("value")?; node.py() .import(AUDIT_SEVERITY_MODULE)? .getattr("AuditSeverity")? diff --git a/crates/sqlbuild-python/src/bindings/_helpers/dag_artifact/selection.rs b/crates/sqlbuild-python/src/bindings/_helpers/dag_artifact/selection.rs index 041d164ad9..e03fbe078c 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dag_artifact/selection.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dag_artifact/selection.rs @@ -19,6 +19,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::imported; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::native_host::third_party::ThirdPartyHost; use crate::bindings::_helpers::planner_selection::carrier::Carrier; +use crate::bindings::_helpers::records::fields::field as native_field; const PATHLIB_MODULE: &str = "pathlib"; const JSON_MODULE: &str = "json"; @@ -156,8 +157,8 @@ fn load_sqlbuild_dag<'py>( let json: Bound<'py, PyModule> = py.import(JSON_MODULE)?; let loaded: Bound<'py, PyAny> = match json.call_method1("loads", (content,)) { Ok(loaded) => loaded, - Err(error) if error.is_instance(py, &json.getattr("JSONDecodeError")?) => { - let message: String = error.value(py).getattr("msg")?.extract()?; + Err(error) if error.is_instance(py, &native_field(&json, "JSONDecodeError")?) => { + let message: String = native_field(error.value(py), "msg")?.extract()?; return Err(raised( error_class, format!( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/dagster_invocation/arguments.rs b/crates/sqlbuild-python/src/bindings/_helpers/dagster_invocation/arguments.rs index ea438177fd..f3a7722a4c 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dagster_invocation/arguments.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dagster_invocation/arguments.rs @@ -20,6 +20,7 @@ use crate::bindings::_helpers::dagster_invocation::access::{ }; use crate::bindings::_helpers::dagster_invocation::results::{dagster_check_name, sorted_nodes}; use crate::bindings::_helpers::native_host::third_party::ThirdPartyHost; +use crate::bindings::_helpers::records::fields::field as native_field; const SELECT_FILE_FLAG: &str = "--select-file"; const EVENT_OUTPUT_FLAG: &str = "--event-output"; @@ -49,7 +50,7 @@ fn temporary_file<'py>( handle.call_method1("write", (format!("{line}\n"),))?; } handle.call_method0("close")?; - imported(py, "pathlib", "Path")?.call1((handle.getattr("name")?,)) + imported(py, "pathlib", "Path")?.call1((native_field(&handle, "name")?,)) } fn is_scenario_test(args: &[String]) -> bool { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/dagster_invocation/failures.rs b/crates/sqlbuild-python/src/bindings/_helpers/dagster_invocation/failures.rs index 66b048599c..b4f7ed9dc4 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dagster_invocation/failures.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dagster_invocation/failures.rs @@ -12,6 +12,7 @@ use crate::bindings::_helpers::dagster_invocation::access::{ constant_contains, get, items, text_of, }; use crate::bindings::_helpers::native_host::third_party::ThirdPartyHost; +use crate::bindings::_helpers::records::fields::field as native_field; const LIVE_FAILURE_MESSAGE: &str = "SQLBuild resource attempt failed; see final execution diagnostics"; @@ -146,8 +147,8 @@ pub(crate) fn dagster_live_failure<'py>( command: Vec, ) -> PyResult<(Vec, Bound<'py, PyDict>)> { let py: Python<'py> = asset.py(); - let kind: String = asset.getattr("kind")?.extract()?; - let name: String = asset.getattr("name")?.extract()?; + let kind: String = native_field(&asset, "kind")?.extract()?; + let name: String = native_field(&asset, "name")?.extract()?; let label: String = format!("{kind}:{name}"); let or_unknown = |value: Bound<'py, PyAny>| -> PyResult { if value.is_truthy()? { @@ -156,8 +157,8 @@ pub(crate) fn dagster_live_failure<'py>( Ok(UNKNOWN.to_owned()) } }; - let phase: String = or_unknown(asset.getattr("failed_phase")?)?; - let code: String = or_unknown(envelope.getattr("error_code")?)?; + let phase: String = or_unknown(native_field(&asset, "failed_phase")?)?; + let code: String = or_unknown(native_field(&envelope, "error_code")?)?; let metadata: Bound<'py, PyDict> = PyDict::new(py); metadata.set_item("sqlbuild_asset", &label)?; metadata.set_item("sqlbuild_asset_kind", &kind)?; @@ -167,12 +168,15 @@ pub(crate) fn dagster_live_failure<'py>( metadata.set_item("sqlbuild_error_message", LIVE_FAILURE_MESSAGE)?; metadata.set_item("sqlbuild_command", command.join(" "))?; for (key, value) in [ - ("sqlbuild_target", asset.getattr("target")?), + ("sqlbuild_target", native_field(&asset, "target")?), ( "sqlbuild_staging_relation", - asset.getattr("staging_relation")?, + native_field(&asset, "staging_relation")?, + ), + ( + "sqlbuild_duration_ms", + native_field(&envelope, "duration_ms")?, ), - ("sqlbuild_duration_ms", envelope.getattr("duration_ms")?), ] { if !value.is_none() { metadata.set_item(key, value)?; 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 add0bd57a2..7c5293cce2 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dagster_invocation/results.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dagster_invocation/results.rs @@ -16,6 +16,7 @@ use crate::bindings::_helpers::dagster_invocation::access::{ text_of, value_or, }; use crate::bindings::_helpers::native_host::third_party::ThirdPartyHost; +use crate::bindings::_helpers::records::fields::field as native_field; const CHECK_KIND_FALLBACK: &str = "check"; const CHECK_NAME_JOIN: &str = "__"; @@ -88,7 +89,7 @@ pub(crate) fn dagster_check_name(check: &Bound<'_, PyAny>) -> PyResult { fn metadata_value<'py>(value: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = value.py(); if value.is_instance(&imported(py, "enum", "Enum")?)? { - return value.getattr("value"); + return native_field(value, "value"); } if value.is_instance(&imported(py, "collections.abc", "Mapping")?)? { let plain: Bound<'py, PyDict> = PyDict::new(py); @@ -505,17 +506,22 @@ pub(crate) fn dagster_integration_results<'py>( let nodes = nodes_by_id(&dag)?; let source_kind: String = constant(py, ThirdPartyHost::SourceNodeKind)?.extract()?; let clone_command: String = constant(py, ThirdPartyHost::CloneCommand)?.extract()?; - let envelope_command: String = envelope.getattr("command")?.str()?.extract()?; + let envelope_command: String = native_field(&envelope, "command")?.str()?.extract()?; let mut plans: Vec> = Vec::new(); - let asset: Bound<'py, PyAny> = envelope.getattr("asset")?; - if !asset.is_none() && completed(py, &asset.getattr("status")?.str()?.extract::()?)? { - let resource_id: String = envelope.getattr("resource_id")?.str()?.extract()?; + let asset: Bound<'py, PyAny> = native_field(&envelope, "asset")?; + if !asset.is_none() + && completed( + py, + &native_field(&asset, "status")?.str()?.extract::()?, + )? + { + let resource_id: String = native_field(&envelope, "resource_id")?.str()?.extract()?; let node: Option<&Bound<'py, PyAny>> = node_with_id(&nodes, &resource_id); let asset_facts: Bound<'py, PyAny> = asset.call_method0("as_dict")?; let mut projected: Vec<(String, Bound<'py, PyAny>)> = Vec::new(); if let Some(node) = node { projected.push((text_of(node, "id")?, asset_facts.clone())); - let loader: Bound<'py, PyAny> = asset.getattr("loader")?; + let loader: Bound<'py, PyAny> = native_field(&asset, "loader")?; let loader_facts: Option<(String, Bound<'py, PyDict>)> = if text_of(node, "kind")? == source_kind && !loader.is_none() { loader_result( @@ -597,7 +603,7 @@ pub(crate) fn dagster_integration_results<'py>( ], )?; let mut seen: HashSet = HashSet::new(); - for check in envelope.getattr("checks")?.try_iter()? { + for check in native_field(&envelope, "checks")?.try_iter()? { let facts: Bound<'py, PyDict> = check?.call_method0("as_dict")?.cast_into()?; let record: Bound<'py, PyDict> = facts.copy()?; for (name, value) in &check_fields { 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 eabf813bc3..3710ca73c8 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 @@ -14,6 +14,7 @@ use sqlbuild_core::dbt_command::models::ForwardedArg; use crate::bindings::_helpers::compile_driver::python_calls::call_with; use crate::bindings::_helpers::dbt_command::support::{color_enabled, flag, path, stream}; +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 sqlbuild_core::constants::PROJECT_CONFIG_FILENAME; @@ -89,8 +90,8 @@ pub(crate) fn ensure_twin_project<'py>( let callbacks: Bound<'py, PyAny> = call_with( ®istry::class(py, RecordId::DbtInitProgressCallbacks)?, &[ - ("start", progress.getattr("start")?), - ("complete", progress.getattr("complete")?), + ("start", native_field(&progress, "start")?), + ("complete", native_field(&progress, "complete")?), ], )?; let none: Bound<'py, PyAny> = py.None().into_bound(py); @@ -121,12 +122,12 @@ pub(crate) fn ensure_twin_project<'py>( ®istry::native(py, NativeAttr::BuildDbtInitProject)?, &[("request", request)], )?; - let project_file: String = result.getattr("project_file")?.str()?.to_string(); + let project_file: String = native_field(&result, "project_file")?.str()?.to_string(); progress_stream.call_method1( "write", (render_dbt_auto_init(&project_file, CliStyle { use_color }),), )?; - Ok((result.getattr("output_dir")?, forwarded)) + Ok((native_field(&result, "output_dir")?, forwarded)) } /// The arguments as a Python tuple. 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 0e86d93fec..3a35b84ba0 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dbt_command/command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dbt_command/command.rs @@ -11,6 +11,7 @@ use crate::bindings::_helpers::dbt_command::auto_init::{args_tuple, ensure_twin_ 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::runtime_config::validate_dbt_execution_args; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; @@ -70,7 +71,10 @@ fn run_plan(project_dir: &Bound<'_, PyAny>, args: &[String], no_color: bool) -> &[ ("project_dir", project_dir.clone()), ("args", args_tuple(py, &options.routed_args)?), - ("on_progress", progress.reporter.getattr("on_progress")?), + ( + "on_progress", + native_field(&progress.reporter, "on_progress")?, + ), ("progress_stream", progress.stream.clone()), ("use_color", flag(py, progress.use_color)), ], @@ -117,7 +121,10 @@ fn run_execution( ("command", command.clone()), ("project_dir", project_dir.clone()), ("args", args_tuple(py, &options.routed_args)?), - ("on_progress", progress.reporter.getattr("on_progress")?), + ( + "on_progress", + native_field(&progress.reporter, "on_progress")?, + ), ("progress_stream", progress.stream.clone()), ("dbt_stdout_stream", stream(py, "stdout")?), ("use_color", flag(py, progress.use_color)), @@ -153,7 +160,7 @@ fn run_dbt_command(py: Python<'_>, options: Option>) -> PyResu args, no_color, )?; - let command_name: String = command.getattr("value")?.extract()?; + let command_name: String = native_field(&command, "value")?.extract()?; match command_name.as_str() { PLAN_COMMAND => run_plan(&project_dir, &args, no_color), DEBUG_COMMAND => run_dbt_debug(&project_dir, &args, no_color), 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 ed3ffd5488..6b2c6b68a7 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dbt_command/init.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dbt_command/init.rs @@ -13,6 +13,7 @@ use sqlbuild_core::dbt_command::models::DbtInitSummary; use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; use crate::bindings::_helpers::dbt_command::support::{color_enabled, flag, path, stream}; +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; @@ -42,7 +43,7 @@ fn optional_path<'py>(value: Bound<'py, PyAny>) -> PyResult> { /// The dbt project directory: the option, else `.` when the working directory is a dbt project. fn dbt_project_dir<'py>(request: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = request.py(); - let given: Bound<'py, PyAny> = request.getattr("dbt_project_dir")?; + let given: Bound<'py, PyAny> = native_field(request, "dbt_project_dir")?; if !given.is_none() { return optional_path(given); } @@ -89,28 +90,28 @@ fn run_dbt_init_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult< let callbacks: Bound<'_, PyAny> = call_with( ®istry::class(py, RecordId::DbtInitProgressCallbacks)?, &[ - ("start", progress.getattr("start")?), - ("complete", progress.getattr("complete")?), + ("start", native_field(&progress, "start")?), + ("complete", native_field(&progress, "complete")?), ], )?; let init_request: Bound<'_, PyAny> = call_with( ®istry::class(py, RecordId::DbtInitRequest)?, &[ - ("cwd", request.getattr("cwd")?), + ("cwd", native_field(request, "cwd")?), ("dbt_project_dir", project_dir), ( "profiles_dir", - optional_path(request.getattr("profiles_dir")?)?, + optional_path(native_field(request, "profiles_dir")?)?, ), - ("profile_name", request.getattr("profile_name")?), - ("target_name", request.getattr("target_name")?), + ("profile_name", native_field(request, "profile_name")?), + ("target_name", native_field(request, "target_name")?), ( "sqb_output_dir", - optional_path(request.getattr("sqb_output_dir")?)?, + optional_path(native_field(request, "sqb_output_dir")?)?, ), - ("dry_run", request.getattr("dry_run")?), - ("overwrite", request.getattr("overwrite")?), - ("skip_dbt_debug", request.getattr("skip_dbt_debug")?), + ("dry_run", native_field(request, "dry_run")?), + ("overwrite", native_field(request, "overwrite")?), + ("skip_dbt_debug", native_field(request, "skip_dbt_debug")?), ("progress_callbacks", callbacks), ], )?; @@ -124,7 +125,7 @@ fn run_dbt_init_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult< )?; let style: CliStyle = CliStyle { use_color }; let stdout: Bound<'_, PyAny> = stream(py, "stdout")?; - if request.getattr("dry_run")?.is_truthy()? { + if native_field(request, "dry_run")?.is_truthy()? { stdout.call_method1( "write", (render_dbt_init_preview(&summary(&result)?, style),), @@ -135,7 +136,7 @@ fn run_dbt_init_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult< "write", (render_dbt_init_summary(&summary(&result)?, style),), )?; - for warning in result.getattr("warnings")?.try_iter()? { + for warning in native_field(&result, "warnings")?.try_iter()? { stream(py, "stderr")? .call_method1("write", (format!("Warning: {}\n", warning?.str()?),))?; } 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 435a07c977..fdb10dcd25 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 @@ -22,6 +22,7 @@ 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 sqlbuild_core::constants::PROJECT_CONFIG_FILENAME as PROJECT_FILE; @@ -43,19 +44,19 @@ struct InitPaths<'py> { #[pyo3(signature = (*, request))] fn build_dbt_init_project<'py>(request: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = request.py(); - let cwd: Bound<'py, PyAny> = request.getattr("cwd")?; + let cwd: Bound<'py, PyAny> = native_field(request, "cwd")?; let paths: InitPaths<'py> = init_paths(request)?; - let sqb_output_dir: Bound<'py, PyAny> = request.getattr("sqb_output_dir")?; + let sqb_output_dir: Bound<'py, PyAny> = native_field(request, "sqb_output_dir")?; let output_dir: Bound<'py, PyAny> = if sqb_output_dir.is_none() { joined( - &paths.dbt_project_dir.getattr("parent")?, + &native_field(&paths.dbt_project_dir, "parent")?, DEFAULT_OUTPUT_DIR, )? } else { resolved(&cwd, &sqb_output_dir)? }; let project_file: Bound<'py, PyAny> = joined(&output_dir, PROJECT_FILE)?; - if truthy(&project_file, "exists")? && !request.getattr("overwrite")?.is_truthy()? { + if truthy(&project_file, "exists")? && !native_field(request, "overwrite")?.is_truthy()? { return Err(profile_error( py, format!( @@ -73,7 +74,7 @@ fn build_dbt_init_project<'py>(request: &Bound<'py, PyAny>) -> PyResult = request.getattr("progress_callbacks")?; + let callbacks: Bound<'py, PyAny> = native_field(request, "progress_callbacks")?; progress(&callbacks, "start", "Inspecting dbt project and profile...")?; let metadata: DbtProjectMetadata = load_dbt_project_metadata(&paths.dbt_project_dir)?; let profile_name: Bound<'py, PyAny> = profile_name(request, &metadata)?; @@ -97,12 +98,12 @@ fn build_dbt_init_project<'py>(request: &Bound<'py, PyAny>) -> PyResult = selected.getattr("target_name")?; + 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", normalized.getattr("adapter")?), + ("adapter", native_field(&normalized, "adapter")?), ("target_name", target_name.clone()), ("profile_name", profile_name.clone()), ( @@ -114,8 +115,11 @@ fn build_dbt_init_project<'py>(request: &Bound<'py, PyAny>) -> PyResult(request: &Bound<'py, PyAny>) -> PyResult>()?; - if !request.getattr("skip_dbt_debug")?.is_truthy()? { + if !native_field(request, "skip_dbt_debug")?.is_truthy()? { progress(&callbacks, "start", "Running dbt debug...")?; if let Some(warning) = dbt_debug_warning(&paths, &target_name.extract::()?)? { warnings.push(PyString::new(py, &warning).into_any()); } progress(&callbacks, "complete", "Finished dbt debug.")?; } - let dry_run: Bound<'py, PyAny> = request.getattr("dry_run")?; + let dry_run: Bound<'py, PyAny> = native_field(request, "dry_run")?; if !dry_run.is_truthy()? { progress(&callbacks, "start", "Writing SQLBuild project config...")?; let keywords: Bound<'py, PyDict> = PyDict::new(py); @@ -148,7 +152,7 @@ fn build_dbt_init_project<'py>(request: &Bound<'py, PyAny>) -> PyResult) -> Result<(), PyErr> { } fn init_paths<'py>(request: &Bound<'py, PyAny>) -> PyResult> { - let cwd: Bound<'py, PyAny> = request.getattr("cwd")?; - let dbt_project_dir: Bound<'py, PyAny> = resolved(&cwd, &request.getattr("dbt_project_dir")?)?; - let profiles_dir: Bound<'py, PyAny> = - profiles_dir(&cwd, &dbt_project_dir, &request.getattr("profiles_dir")?)?; + let cwd: Bound<'py, PyAny> = native_field(request, "cwd")?; + let dbt_project_dir: Bound<'py, PyAny> = + resolved(&cwd, &native_field(request, "dbt_project_dir")?)?; + let profiles_dir: Bound<'py, PyAny> = profiles_dir( + &cwd, + &dbt_project_dir, + &native_field(request, "profiles_dir")?, + )?; Ok(InitPaths { dbt_project_dir, profiles_dir, @@ -185,7 +193,7 @@ fn profile_name<'py>( request: &Bound<'py, PyAny>, metadata: &DbtProjectMetadata, ) -> PyResult> { - let requested: Bound<'py, PyAny> = request.getattr("profile_name")?; + let requested: Bound<'py, PyAny> = native_field(request, "profile_name")?; if requested.is_truthy()? { return Ok(requested); } @@ -197,7 +205,7 @@ fn selected_output<'py>( request: &Bound<'py, PyAny>, profile_name: &Bound<'py, PyAny>, ) -> PyResult> { - let target_name: Option = request.getattr("target_name")?.extract()?; + let target_name: Option = native_field(request, "target_name")?.extract()?; load_selected_dbt_profile_output( &paths.profiles_dir, &profile_name.extract::()?, 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 7584104b63..d38960d760 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,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -59,7 +60,7 @@ pub(crate) fn option_values(options: &Bound<'_, PyAny>) -> PyResult) -> PyResult { let value = |name: &str| attribute_text(parsed, name); let flag = |name: &str| -> PyResult { parsed.getattr(name)?.is_truthy() }; Ok(DbtParsedFlags { - select: texts(&parsed.getattr("select")?)?, - exclude: texts(&parsed.getattr("exclude")?)?, + select: texts(&native_field(parsed, "select")?)?, + exclude: texts(&native_field(parsed, "exclude")?)?, vars: value("vars")?, threads: value("threads")?, full_refresh: flag("full_refresh")?, @@ -282,7 +283,7 @@ fn parsed_flags(parsed: &Bound<'_, PyAny>) -> PyResult { defer_to: value("defer_to")?, fail_fast: flag("fail_fast")?, hard_copy: flag("hard_copy")?, - dbt_passthrough: texts(&parsed.getattr("dbt_passthrough")?)?, + dbt_passthrough: texts(&native_field(parsed, "dbt_passthrough")?)?, }) } 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 fad85edd01..efd43a63a3 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 @@ -27,6 +27,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, 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; @@ -200,7 +201,7 @@ fn handle_line<'py>( if update.is_none() { return Ok(()); } - let message: Bound<'py, PyAny> = update.getattr("status_message")?; + let message: Bound<'py, PyAny> = native_field(&update, "status_message")?; if !message.is_none() && output.enable_status { let mut state = locked(py, shared)?; let status: Bound<'py, PyAny> = match state.status.take() { @@ -217,14 +218,14 @@ fn handle_line<'py>( }; state.status = Some(status.unbind()); } - let result: Bound<'py, PyAny> = update.getattr("result")?; - let row: Bound<'py, PyAny> = update.getattr("row")?; + let result: Bound<'py, PyAny> = native_field(&update, "result")?; + let row: Bound<'py, PyAny> = native_field(&update, "row")?; if result.is_none() || row.is_none() { return Ok(()); } close_status(py, shared)?; - let finished: bool = update.getattr("finished")?.is_truthy()?; - let unique_id: String = result.getattr("unique_id")?.extract()?; + let finished: bool = native_field(&update, "finished")?.is_truthy()?; + let unique_id: String = native_field(&result, "unique_id")?.extract()?; if finished { locked(py, shared)? .active_nodes @@ -240,7 +241,7 @@ fn handle_line<'py>( let mut state = locked(py, shared)?; if !finished { let started: f64 = shared.epoch.elapsed().as_secs_f64(); - let name: String = result.getattr("node_name")?.extract()?; + let name: String = native_field(&result, "node_name")?.extract()?; match state .active_nodes .iter_mut() 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 2c4559cdbe..eac5dec3c8 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/execution.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/execution.rs @@ -18,6 +18,7 @@ use sqlbuild_core::dbt_plan::models::DbtExecutedNode; use crate::bindings::_helpers::compile_driver::python_calls::KeywordArguments; use crate::bindings::_helpers::dbt_interop::cli::option_values; use crate::bindings::_helpers::dbt_interop::graph::{DbtCombinedGraph, texts}; +use crate::bindings::_helpers::records::fields::field as native_field; const MERGED_OPTIONS: (&[&str], &[&str]) = (&["command", "options", "routed_args", "plan"], &[]); @@ -35,13 +36,15 @@ fn build_merged_dbt_execution_argv<'py>( MERGED_OPTIONS, )?; let plan: Bound<'py, PyAny> = arguments.value("plan")?; - let required_terms: Vec = texts(&plan.getattr("dbt_required_selector_terms")?)?; - if !plan.getattr("dbt_selected_unique_ids")?.is_truthy()? && required_terms.is_empty() { + let required_terms: Vec = texts(&native_field(&plan, "dbt_required_selector_terms")?)?; + if !native_field(&plan, "dbt_selected_unique_ids")?.is_truthy()? && required_terms.is_empty() { return Ok(None); } let args: Vec = merged_dbt_execution_args(&texts(&arguments.value("routed_args")?)?, &required_terms); - let dbt_executable: String = plan.getattr("dbt_command_argv")?.get_item(0)?.extract()?; + let dbt_executable: String = native_field(&plan, "dbt_command_argv")? + .get_item(0)? + .extract()?; let command: DbtArgvCommand = DbtArgvCommand::Other { command: arguments.value("command")?.getattr("value")?.extract()?, args, @@ -65,10 +68,10 @@ fn executed_nodes(node_results: &Bound<'_, PyAny>) -> PyResult = result?; nodes.push(DbtExecutedNode { - unique_id: result.getattr("unique_id")?.extract()?, - resource_type: result.getattr("resource_type")?.extract()?, - status: result.getattr("status")?.extract()?, - execution_time: result.getattr("execution_time")?.extract()?, + unique_id: native_field(&result, "unique_id")?.extract()?, + resource_type: native_field(&result, "resource_type")?.extract()?, + status: native_field(&result, "status")?.extract()?, + execution_time: native_field(&result, "execution_time")?.extract()?, }); } Ok(nodes) @@ -83,7 +86,8 @@ fn build_failed_sqlbuild_model_names<'py>( node_results: &Bound<'py, PyAny>, ) -> PyResult> { let mut dependencies: HashMap> = HashMap::new(); - let lookup: Bound<'py, PyAny> = manifest.getattr("validation_depends_on_nodes_by_unique_id")?; + let lookup: Bound<'py, PyAny> = + native_field(manifest, "validation_depends_on_nodes_by_unique_id")?; for item in lookup.call_method0("items")?.try_iter()? { let (unique_id, depends_on): (String, Bound<'py, PyAny>) = item?.extract()?; dependencies.insert(unique_id, texts(&depends_on)?); 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 61ee6fb002..c7cf0daf34 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/graph.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/graph.rs @@ -21,6 +21,7 @@ use sqlbuild_core::dbt_graph::types::DbtSelectorResolver; use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; use crate::bindings::_helpers::dbt_interop::manifest::resolve_sqlbuild_model_dbt_refs; +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; @@ -109,15 +110,15 @@ fn graph_manifest(manifest: &Bound<'_, PyAny>) -> PyResult { { let model: Bound<'_, PyAny> = model?; models.push(DbtGraphManifestModel { - unique_id: model.getattr("unique_id")?.extract()?, - name: model.getattr("name")?.extract()?, - depends_on_nodes: texts(&model.getattr("depends_on_nodes")?)?, + unique_id: native_field(&model, "unique_id")?.extract()?, + name: native_field(&model, "name")?.extract()?, + depends_on_nodes: texts(&native_field(&model, "depends_on_nodes")?)?, }); } Ok(DbtGraphManifest { models, - source_ids: key_set(&manifest.getattr("sources_by_unique_id")?)?, - seed_ids: key_set(&manifest.getattr("seeds_by_unique_id")?)?, + source_ids: key_set(&native_field(manifest, "sources_by_unique_id")?)?, + seed_ids: key_set(&native_field(manifest, "seeds_by_unique_id")?)?, }) } @@ -132,22 +133,22 @@ fn project_models<'py>( { let (model, dbt_model): (Bound<'py, PyAny>, Bound<'py, PyAny>) = pair?.extract()?; refs_by_name - .entry(model.getattr("name")?.extract()?) + .entry(native_field(&model, "name")?.extract()?) .or_default() - .push(dbt_model.getattr("unique_id")?.extract()?); + .push(native_field(&dbt_model, "unique_id")?.extract()?); } let mut models: Vec = Vec::new(); - for model in project.getattr("models")?.try_iter()? { + for model in native_field(project, "models")?.try_iter()? { let model: Bound<'py, PyAny> = model?; - let name: String = model.getattr("name")?.extract()?; + let name: String = native_field(&model, "name")?.extract()?; let mut model_deps: Vec = Vec::new(); - for dependency in model.getattr("deps")?.try_iter()? { + for dependency in native_field(&model, "deps")?.try_iter()? { let dependency: Bound<'py, PyAny> = dependency?; if dependency .getattr("resource_type")? .eq(MODEL_RESOURCE_TYPE)? { - model_deps.push(dependency.getattr("name")?.extract()?); + model_deps.push(native_field(&dependency, "name")?.extract()?); } } let dbt_ref_unique_ids: Vec = refs_by_name.get(&name).cloned().unwrap_or_default(); @@ -196,9 +197,9 @@ impl DbtSelectorResolver for PlannerResolver<'_> { fn split(&self, term: &str) -> PyResult { let parsed: Bound<'_, PyAny> = self.split.call1((term,))?; Ok(DbtSelectorExpansion { - core: parsed.getattr("core")?.extract()?, - upstream: parsed.getattr("upstream")?.is_truthy()?, - downstream: parsed.getattr("downstream")?.is_truthy()?, + core: native_field(&parsed, "core")?.extract()?, + upstream: native_field(&parsed, "upstream")?.is_truthy()?, + downstream: native_field(&parsed, "downstream")?.is_truthy()?, }) } 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 666f734777..cea8e6a43b 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/manifest.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/manifest.rs @@ -22,6 +22,7 @@ use sqlbuild_config::dbt_manifest::models::{ use crate::bindings::_helpers::compile_driver::python_calls::{ KeywordArguments, call_with, imported, }; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -361,7 +362,7 @@ fn decoded<'py>(contents: &Bound<'py, PyAny>) -> PyResult> { match imported(py, "json", "loads")?.call1((contents,)) { Ok(value) => Ok(value), Err(error) if error.is_instance(py, &imported(py, "json", "JSONDecodeError")?) => { - let detail: String = error.value(py).getattr("msg")?.extract()?; + let detail: String = native_field(error.value(py), "msg")?.extract()?; Err(raised( py, ManifestError::invalid(&format!(" JSON: {detail}")), @@ -438,7 +439,7 @@ fn resolved_model<'py>( _ => { let packages: Vec = matches .iter() - .map(|model| model.getattr("package_name")?.extract()) + .map(|model| native_field(model, "package_name")?.extract()) .collect::>()?; Err(raised(py, ManifestError::ambiguous_model(name, packages))) } @@ -467,7 +468,7 @@ fn validate_compile_dbt_model_names( if dbt_manifest.is_none() { return Ok(()); } - let dbt_names: Bound<'_, PyAny> = dbt_manifest.getattr("models_by_name")?; + let dbt_names: Bound<'_, PyAny> = native_field(&dbt_manifest, "models_by_name")?; let mut shared: Vec = Vec::new(); for name in known_model_names.try_iter()? { let name: Bound<'_, PyAny> = name?; @@ -501,15 +502,15 @@ fn validate_compile_dbt_model_reference( model_relative_path: Bound<'_, PyAny>, dbt_manifest: Bound<'_, PyAny>, ) -> Result<(), PyErr> { - if !dbt_ref_kind(&reference.getattr("ref_kind")?)? { + if !dbt_ref_kind(&native_field(&reference, "ref_kind")?)? { return Ok(()); } - let name: String = reference.getattr("ref_name")?.extract()?; + let name: String = native_field(&reference, "ref_name")?.extract()?; if dbt_manifest.is_none() { let path: String = model_relative_path.str()?.extract()?; return Err(raised(py, ManifestError::missing_manifest(&path, &name))); } - let package: Option = optional_text(&reference.getattr("ref_package")?)?; + let package: Option = optional_text(&native_field(&reference, "ref_package")?)?; resolved_model(&dbt_manifest, &name, package.as_deref())?; Ok(()) } @@ -535,21 +536,21 @@ pub(crate) fn resolve_sqlbuild_model_dbt_refs<'py>( .map(|names| name_set(&names)) .transpose()?; let mut refs: Vec> = Vec::new(); - for model in project.getattr("models")?.try_iter()? { + for model in native_field(&project, "models")?.try_iter()? { let model: Bound<'py, PyAny> = model?; if let Some(selected) = &selected_model_names { - let name: String = model.getattr("name")?.extract()?; + let name: String = native_field(&model, "name")?.extract()?; if !selected.contains(&name) { continue; } } - for reference in model.getattr("references")?.try_iter()? { + for reference in native_field(&model, "references")?.try_iter()? { let reference: Bound<'py, PyAny> = reference?; - if !dbt_ref_kind(&reference.getattr("ref_kind")?)? { + if !dbt_ref_kind(&native_field(&reference, "ref_kind")?)? { continue; } - let name: String = reference.getattr("ref_name")?.extract()?; - let package: Option = optional_text(&reference.getattr("ref_package")?)?; + let name: String = native_field(&reference, "ref_name")?.extract()?; + let package: Option = optional_text(&native_field(&reference, "ref_package")?)?; let resolved: Bound<'py, PyAny> = resolved_model(&manifest, &name, package.as_deref())?; refs.push(PyTuple::new(py, [model.clone(), resolved])?); } @@ -576,7 +577,10 @@ fn resolve_dbt_reference_relation<'py>( } let name: String = arguments.value("ref_name")?.extract()?; let package: Option = optional_text(&arguments.value("ref_package")?)?; - resolved_model(&manifest, &name, package.as_deref())?.getattr("relation_name") + native_field( + &resolved_model(&manifest, &name, package.as_deref())?, + "relation_name", + ) } pub(crate) fn register(module: &Bound<'_, PyModule>) -> 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 017599488a..8489295842 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -46,20 +47,20 @@ fn listed<'py>( ) -> PyResult> { let py: Python<'py> = runner.py(); let result: Bound<'py, PyAny> = call_with( - &runner.getattr("ls")?, + &native_field(runner, "ls")?, &[ ("options", dbt_options.clone()), ("select", select), ("exclude", exclude.clone()), ], )?; - let command: Bound<'py, PyAny> = result.getattr("command")?; - if command.getattr("returncode")?.extract::()? == 0 { + let command: Bound<'py, PyAny> = native_field(&result, "command")?; + if native_field(&command, "returncode")?.extract::()? == 0 { return Ok(result); } let detail: Option = dbt_failure_detail( - &command.getattr("stderr")?.extract::()?, - &command.getattr("stdout")?.extract::()?, + &native_field(&command, "stderr")?.extract::()?, + &native_field(&command, "stdout")?.extract::()?, ); let help: Bound<'py, PyAny> = detail.map_or_else( || py.None().into_bound(py), @@ -72,14 +73,14 @@ fn listed<'py>( fn unique_ids<'py>(listing: &Bound<'py, PyAny>) -> PyResult> { let mut ids: Vec> = Vec::new(); - for node in listing.getattr("nodes")?.try_iter()? { - ids.push(node?.getattr("unique_id")?); + for node in native_field(listing, "nodes")?.try_iter()? { + ids.push(native_field(&node?, "unique_id")?); } PyTuple::new(listing.py(), ids) } fn command_kind(command: &Bound<'_, PyAny>) -> PyResult { - let value: String = command.getattr("value")?.extract()?; + let value: String = native_field(command, "value")?.extract()?; match value.as_str() { "plan" => Ok(DbtInteropCommandKind::Plan), "run" => Ok(DbtInteropCommandKind::Run), @@ -100,7 +101,7 @@ fn required_terms( arguments: &KeywordArguments<'_>, selection: &Bound<'_, PyAny>, ) -> PyResult> { - let required: Vec = texts(&selection.getattr("dbt_required_unique_ids")?)?; + let required: Vec = texts(&native_field(selection, "dbt_required_unique_ids")?)?; if required.is_empty() { return Ok(Vec::new()); } @@ -110,15 +111,15 @@ fn required_terms( py, arguments.value("project")?, arguments.value("manifest")?, - Some(selection.getattr("sqlbuild_model_names")?), + Some(native_field(selection, "sqlbuild_model_names")?), )? .try_iter()? { let (_model, dbt_model): (Bound<'_, PyAny>, Bound<'_, PyAny>) = pair?.extract()?; referenced.push(DbtReferencedModel { - unique_id: dbt_model.getattr("unique_id")?.extract()?, - fqn: texts(&dbt_model.getattr("fqn")?)?, - name: dbt_model.getattr("name")?.extract()?, + unique_id: native_field(&dbt_model, "unique_id")?.extract()?, + fqn: texts(&native_field(&dbt_model, "fqn")?)?, + name: native_field(&dbt_model, "name")?.extract()?, }); } Ok(dbt_required_selector_terms(&referenced, &required)) @@ -145,8 +146,8 @@ fn plan_dbt_interop_command<'py>( registry::class(py, RecordId::DbtInteropCommand)?.call1((arguments.value("command")?,))?; let command_args: Bound<'py, PyAny> = arguments.value("command_args")?; let (select, exclude) = ( - command_args.getattr("select")?, - command_args.getattr("exclude")?, + native_field(&command_args, "select")?, + native_field(&command_args, "exclude")?, ); let (runner, dbt_options) = ( arguments.value("dbt_runner")?, @@ -181,13 +182,13 @@ fn plan_dbt_interop_command<'py>( let required_selector_terms: Vec = required_terms(&arguments, &selection)?; let options: DbtCliOptionValues = option_values(&dbt_options)?; let (dbt_executable, sqlbuild_executable): (String, String) = ( - command_args.getattr("dbt_executable")?.extract()?, - command_args.getattr("sqlbuild_executable")?.extract()?, + native_field(&command_args, "dbt_executable")?.extract()?, + native_field(&command_args, "sqlbuild_executable")?.extract()?, ); let (dbt_command_args, sqlbuild_command_args, selected_model_names) = ( - texts(&command_args.getattr("dbt_command_args")?)?, - texts(&command_args.getattr("sqlbuild_command_args")?)?, - texts(&selection.getattr("sqlbuild_model_names")?)?, + texts(&native_field(&command_args, "dbt_command_args")?)?, + texts(&native_field(&command_args, "sqlbuild_command_args")?)?, + texts(&native_field(&selection, "sqlbuild_model_names")?)?, ); let argvs: DbtPlanArgvs = dbt_plan_argvs(&DbtPlanArgvRequest { command: command_kind(&command)?, @@ -209,7 +210,7 @@ fn plan_dbt_interop_command<'py>( "dbt_command_argv", PyTuple::new(py, argvs.dbt_command_argv)?.into_any(), ), - ("dbt_ls_nodes", full_listing.getattr("nodes")?), + ("dbt_ls_nodes", native_field(&full_listing, "nodes")?), ( "sqlbuild_command_argvs", argv_tuples(py, argvs.sqlbuild_command_argvs)?, 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 0045d597c7..efd3badb26 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/pipeline.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/pipeline.rs @@ -12,6 +12,7 @@ use crate::bindings::_helpers::build_command::support::{boolean, none, text}; use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; use crate::bindings::_helpers::dbt_interop::pipeline_prologue::progress; use crate::bindings::_helpers::dbt_interop::runtime_config::config_overrides; +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; @@ -50,25 +51,28 @@ fn prepare<'py>( py, NativeAttr::LoadCompiledDbtManifest, &[ - ("runner", invocation.getattr("runner")?), - ("dbt_options", invocation.getattr("dbt_options")?), + ("runner", native_field(&invocation, "runner")?), + ("dbt_options", native_field(&invocation, "dbt_options")?), ("full_refresh", boolean(py, false)), - ("on_progress", request.getattr("on_progress")?), + ("on_progress", native_field(request, "on_progress")?), ], )?; let compiled = native( py, NativeAttr::CompileDbtInteropProject, &[ - ("project_dir", request.getattr("project_dir")?), + ("project_dir", native_field(request, "project_dir")?), ( "discovered_inputs", - invocation.getattr("discovered_inputs")?, + native_field(&invocation, "discovered_inputs")?, ), ("manifest", manifest.clone()), - ("dbt_vars", invocation.getattr("dbt_vars")?), - ("no_sql_validation", request.getattr("no_sql_validation")?), - ("on_progress", request.getattr("on_progress")?), + ("dbt_vars", native_field(&invocation, "dbt_vars")?), + ( + "no_sql_validation", + native_field(request, "no_sql_validation")?, + ), + ("on_progress", native_field(request, "on_progress")?), ], )?; Ok((invocation, manifest, compiled)) @@ -84,15 +88,15 @@ fn resolve_plan<'py>( request.py(), NativeAttr::ResolveDbtInteropPlan, &[ - ("command", request.getattr("command")?), + ("command", native_field(request, "command")?), ("invocation", invocation.clone()), ("compiled", compiled.clone()), ("manifest", manifest.clone()), ( "sqlbuild_executable", - request.getattr("sqlbuild_executable")?, + native_field(request, "sqlbuild_executable")?, ), - ("on_progress", request.getattr("on_progress")?), + ("on_progress", native_field(request, "on_progress")?), ], ) } @@ -101,9 +105,9 @@ fn resolve_plan<'py>( #[pyfunction] fn execute_dbt_interop_from_project(request: &Bound<'_, PyAny>) -> PyResult { let py = request.py(); - let command = request.getattr("command")?; + let command = native_field(request, "command")?; let kind = registry::class(py, RecordId::DbtInteropCommand)?; - if !command.eq(kind.getattr("RUN")?)? && !command.eq(kind.getattr("BUILD")?)? { + 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!( "unsupported dbt interop execution command: {}", @@ -113,16 +117,16 @@ 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 = resolution.getattr("plan")?; + let plan = native_field(&resolution, "plan")?; let merged = native( py, NativeAttr::BuildMergedDbtExecutionArgv, &[ ("command", command), - ("options", invocation.getattr("dbt_options")?), + ("options", native_field(&invocation, "dbt_options")?), ( "routed_args", - invocation.getattr("routed")?.getattr("dbt_args")?, + native_field(&invocation, "routed")?.getattr("dbt_args")?, ), ("plan", plan.clone()), ], @@ -159,13 +163,13 @@ fn execute_dbt_interop_from_project(request: &Bound<'_, PyAny>) -> PyResult py, NativeAttr::BuildFailedSqlbuildModelNames, &[ - ("graph", resolution.getattr("graph")?), + ("graph", native_field(&resolution, "graph")?), ("manifest", manifest.clone()), - ("node_results", execution.getattr("node_results")?), + ("node_results", native_field(&execution, "node_results")?), ], )?; - let code: i64 = execution.getattr("returncode")?.extract()?; - if code != 0 && !execution.getattr("node_results")?.is_truthy()? { + let code: i64 = native_field(&execution, "returncode")?.extract()?; + if code != 0 && !native_field(&execution, "node_results")?.is_truthy()? { return Ok(code); } let output = native( @@ -181,7 +185,7 @@ fn execute_dbt_interop_from_project(request: &Bound<'_, PyAny>) -> PyResult ], )?; let skip = if output.is_none() { - if plan.getattr("sqlbuild_skip_reason")?.is_none() { + if native_field(&plan, "sqlbuild_skip_reason")?.is_none() { return Ok(code); } Some("No SQLBuild work selected.") @@ -276,7 +280,7 @@ fn plan_dbt_interop_from_project<'py>( py, NativeAttr::ConnectionProgressReporter, &[ - ("adapter_name", compiled.getattr("adapter_name")?), + ("adapter_name", native_field(&compiled, "adapter_name")?), ("stream", stream), ("use_color", args.value_or_false("use_color")?), ], @@ -288,16 +292,16 @@ fn plan_dbt_interop_from_project<'py>( py, NativeAttr::AttachSqlbuildPlanOutput, &[ - ("plan", resolution.getattr("plan")?), + ("plan", native_field(&resolution, "plan")?), ("project_dir", args.value("project_dir")?), ( "discovered_inputs", - invocation.getattr("discovered_inputs")?, + native_field(&invocation, "discovered_inputs")?, ), ("compiled", compiled), ( "sqlbuild_args", - invocation.getattr("effective_sqlbuild_args")?, + native_field(&invocation, "effective_sqlbuild_args")?, ), ("connection_progress", connection), ("on_progress", args.value("on_progress")?), @@ -336,11 +340,11 @@ fn debug_dbt_from_project(py: Python<'_>, options: Option>) -> ("project_root", args.value("project_dir")?), ( "config", - discovered.getattr("project_config")?.getattr("dbt")?, + native_field(&discovered, "project_config")?.getattr("dbt")?, ), ( "local_config", - discovered.getattr("local_config")?.getattr("dbt")?, + native_field(&discovered, "local_config")?.getattr("dbt")?, ), ("overrides", overrides), ("require_project_dir", boolean(py, true)), @@ -349,9 +353,9 @@ fn debug_dbt_from_project(py: Python<'_>, options: Option>) -> let dbt_options = call_with( ®istry::class(py, RecordId::DbtCliOptions)?, &[ - ("project_dir", resolved.getattr("project_dir")?), - ("profiles_dir", resolved.getattr("profiles_dir")?), - ("target", resolved.getattr("target")?), + ("project_dir", native_field(&resolved, "project_dir")?), + ("profiles_dir", native_field(&resolved, "profiles_dir")?), + ("target", native_field(&resolved, "target")?), ], )?; let mut runner = args.value("dbt_runner")?; @@ -363,7 +367,7 @@ fn debug_dbt_from_project(py: Python<'_>, options: Option>) -> } let argv: Vec = args.value("args")?.extract()?; let result = call_with( - &runner.getattr("debug")?, + &native_field(&runner, "debug")?, &[ ("options", dbt_options), ("args", PyTuple::new(py, dbt_debug_args(&argv))?.into_any()), @@ -371,11 +375,11 @@ fn debug_dbt_from_project(py: Python<'_>, options: Option>) -> )?; let stdout = args.value("stdout_stream")?; let stderr = args.value("stderr_stream")?; - stdout.call_method1("write", (result.getattr("stdout")?,))?; - stderr.call_method1("write", (result.getattr("stderr")?,))?; + stdout.call_method1("write", (native_field(&result, "stdout")?,))?; + stderr.call_method1("write", (native_field(&result, "stderr")?,))?; stdout.call_method0("flush")?; stderr.call_method0("flush")?; - result.getattr("returncode")?.extract() + native_field(&result, "returncode")?.extract() } pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> { 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 f3e96c7711..4bf18bb67a 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 @@ -10,6 +10,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::{ }; use crate::bindings::_helpers::dbt_interop::pipeline_prologue::progress; use crate::bindings::_helpers::dbt_interop::plan_output::{connection_config, connection_hooks}; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; @@ -34,14 +35,14 @@ fn environment<'py>(args: &KeywordArguments<'py>) -> PyResult> call_with( ®istry::class(request.py(), RecordId::DbtPlanEnvironment)?, &[ - ("project_dir", request.getattr("project_dir")?), + ("project_dir", native_field(&request, "project_dir")?), ( "discovered_inputs", args.value("invocation")?.getattr("discovered_inputs")?, ), - ("project", compiled.getattr("project")?), - ("adapter", compiled.getattr("adapter")?), - ("adapter_name", compiled.getattr("adapter_name")?), + ("project", native_field(&compiled, "project")?), + ("adapter", native_field(&compiled, "adapter")?), + ("adapter_name", native_field(&compiled, "adapter_name")?), ], ) } @@ -66,7 +67,7 @@ fn write_dbt_execution_plan_text( ) -> PyResult<()> { let args = KeywordArguments::read(py, "write_dbt_execution_plan_text", options, OPTIONS)?; let request = args.value("request")?; - if request.getattr("json_output")?.is_truthy()? { + if native_field(&request, "json_output")?.is_truthy()? { return Ok(()); } let plan = args.value("plan")?; @@ -85,7 +86,7 @@ fn write_dbt_execution_plan_text( )?), )? }; - let limit = if request.getattr("verbose")?.is_truthy()? { + let limit = if native_field(&request, "verbose")?.is_truthy()? { none(py) } else { pyo3::types::PyInt::new(py, super::constants::PLAN_DISPLAY_LIMIT).into_any() @@ -95,7 +96,7 @@ fn write_dbt_execution_plan_text( &[ ("plan", display), ("json_output", boolean(py, false)), - ("use_color", request.getattr("use_color")?), + ("use_color", native_field(&request, "use_color")?), ( "display_options", call_with( @@ -124,17 +125,23 @@ fn execute_dbt_without_state_tracking<'py>( call_with( ®istry::native(py, NativeAttr::ExecuteDbtCommands)?, &[ - ("runner", invocation.getattr("runner")?), - ("options", invocation.getattr("dbt_options")?), + ("runner", native_field(&invocation, "runner")?), + ("options", native_field(&invocation, "dbt_options")?), ("merged_argv", args.value("merged_dbt_argv")?), - ("progress_stream", invocation.getattr("output_stream")?), - ("stdout_stream", invocation.getattr("dbt_output_stream")?), - ("use_color", request.getattr("use_color")?), + ( + "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", request.getattr("on_progress")?), + ("on_progress", native_field(&request, "on_progress")?), ], ) } @@ -177,15 +184,15 @@ fn resolve_sqlbuild_execution_plan_output<'py>( OPTIONS, )?; let plan = args.value("plan")?; - if !plan.getattr("sqlbuild_skip_reason")?.is_none() { + if !native_field(&plan, "sqlbuild_skip_reason")?.is_none() { return Ok(none(py)); } - let selected = plan.getattr("selection")?.getattr("sqlbuild_model_names")?; + let selected = native_field(&plan, "selection")?.getattr("sqlbuild_model_names")?; if !selected.is_truthy()? { return Ok(none(py)); } let invocation = args.value("invocation")?; - let stream = invocation.getattr("output_stream")?; + let stream = native_field(&invocation, "output_stream")?; emit(&stream, "\n")?; let reporter = call_with( ®istry::native(py, NativeAttr::ConnectionProgressReporter)?, @@ -205,7 +212,7 @@ fn resolve_sqlbuild_execution_plan_output<'py>( ("selected_model_names", selected), ( "sqlbuild_args", - invocation.getattr("effective_sqlbuild_args")?, + native_field(&invocation, "effective_sqlbuild_args")?, ), ( "external_blocked_model_names", @@ -234,8 +241,11 @@ fn execution_limits<'py>( call_with( ®istry::native(py, NativeAttr::ResolveTargetConfig)?, &[ - ("project_config", discovered.getattr("project_config")?), - ("local_config", discovered.getattr("local_config")?), + ( + "project_config", + native_field(discovered, "project_config")?, + ), + ("local_config", native_field(discovered, "local_config")?), ("target_name", target), ], )? @@ -253,24 +263,27 @@ fn run_dbt_sqlbuild_work<'py>( let request = args.value("request")?; let invocation = args.value("invocation")?; let compiled = args.value("compiled")?; - let project = compiled.getattr("project")?; - let discovered = invocation.getattr("discovered_inputs")?; + let project = native_field(&compiled, "project")?; + let discovered = native_field(&invocation, "discovered_inputs")?; let context = call_with( ®istry::class(py, RecordId::DbtSqlbuildWorkContext)?, &[ ("plan_output", args.value("plan_output")?), ("connection_config", args.value("connection_config")?), - ("adapter", compiled.getattr("adapter")?), - ("adapter_name", compiled.getattr("adapter_name")?), - ("output_stream", invocation.getattr("output_stream")?), - ("use_color", request.getattr("use_color")?), + ("adapter", native_field(&compiled, "adapter")?), + ("adapter_name", native_field(&compiled, "adapter_name")?), + ("output_stream", native_field(&invocation, "output_stream")?), + ("use_color", native_field(&request, "use_color")?), ( "snapshots_config", - discovered.getattr("project_config")?.getattr("snapshots")?, + native_field(&discovered, "project_config")?.getattr("snapshots")?, ), ( "execution_limits", - execution_limits(&discovered, project.getattr("effective_target_name")?)?, + execution_limits( + &discovered, + native_field(&project, "effective_target_name")?, + )?, ), ], )?; @@ -278,11 +291,11 @@ fn run_dbt_sqlbuild_work<'py>( ®istry::native(py, NativeAttr::ExecuteDbtSqlbuildWork)?, &[ ("context", context), - ("command", request.getattr("command")?), + ("command", native_field(&request, "command")?), ("project", project), - ("project_dir", request.getattr("project_dir")?), - ("fail_fast", request.getattr("fail_fast")?), - ("verbose", request.getattr("verbose")?), + ("project_dir", native_field(&request, "project_dir")?), + ("fail_fast", native_field(&request, "fail_fast")?), + ("verbose", native_field(&request, "verbose")?), ], ) } 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 30c8a18552..a6c4a7b2ac 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 @@ -10,6 +10,7 @@ use crate::bindings::_helpers::dbt_interop::reference_resolver::DbtCompileRefere 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::registry; use pyo3::{pyfunction, wrap_pyfunction}; @@ -62,21 +63,21 @@ pub(crate) fn resolve_dbt_execution_invocation<'py>( request: &Bound<'py, PyAny>, ) -> PyResult> { let py = request.py(); - let mut executable = request.getattr("dbt_executable")?; + let mut executable = native_field(request, "dbt_executable")?; if !executable.is_truthy()? { executable = registry::native(py, NativeAttr::ResolveDbtExecutable)?.call0()?; } - let mut stream = request.getattr("progress_stream")?; + let mut stream = native_field(request, "progress_stream")?; if !stream.is_truthy()? { stream = py.import("sys")?.getattr("stdout")?; } - let command = request.getattr("command")?; - let parsed = parse_dbt_execution_args(&command, &request.getattr("args")?)?; + 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 project_dir = request.getattr("project_dir")?; + let project_dir = native_field(request, "project_dir")?; let discovered = call_with( ®istry::native(py, NativeAttr::DiscoverProjectInputs)?, &[("project_dir", project_dir.clone())], @@ -88,19 +89,19 @@ pub(crate) fn resolve_dbt_execution_invocation<'py>( .collect::>()?; let options = plan_options(&project_dir, &discovered, &dbt_args)?; let vars = vars_mapping( - &discovered.getattr("project_config")?.getattr("dbt")?, - &discovered.getattr("local_config")?.getattr("dbt")?, + &native_field(&discovered, "project_config")?.getattr("dbt")?, + &native_field(&discovered, "local_config")?.getattr("dbt")?, &dbt_args, )? .into_any(); - let mut runner = request.getattr("dbt_runner")?; + let mut runner = native_field(request, "dbt_runner")?; if !runner.is_truthy()? { runner = call_with( ®istry::native(py, NativeAttr::DbtRunner)?, &[("dbt_executable", executable.clone())], )?; } - let dbt_stream = request.getattr("dbt_stdout_stream")?; + let dbt_stream = native_field(request, "dbt_stdout_stream")?; call_with( ®istry::class(py, RecordId::DbtInteropInvocation)?, &[ @@ -114,7 +115,10 @@ pub(crate) fn resolve_dbt_execution_invocation<'py>( stream }, ), - ("effective_sqlbuild_args", routed.getattr("sqlbuild_args")?), + ( + "effective_sqlbuild_args", + native_field(&routed, "sqlbuild_args")?, + ), ("routed", routed), ("discovered_inputs", discovered), ("dbt_options", options), @@ -142,7 +146,7 @@ fn load_compiled_dbt_manifest<'py>( ("full_refresh", args.value("full_refresh")?), ], )?; - if result.getattr("returncode")?.extract::()? != 0 { + if native_field(&result, "returncode")?.extract::()? != 0 { let detail = registry::native(py, NativeAttr::DbtFailureDetail)?.call1((result,))?; let error = registry::error(py, ErrorId::DbtInteropRuntime)?.call( ("dbt compile failed",), @@ -182,8 +186,11 @@ fn compile_dbt_interop_project<'py>( let name = call_with( ®istry::native(py, NativeAttr::ResolveEffectiveAdapterName)?, &[ - ("project_config", discovered.getattr("project_config")?), - ("local_config", discovered.getattr("local_config")?), + ( + "project_config", + native_field(&discovered, "project_config")?, + ), + ("local_config", native_field(&discovered, "local_config")?), ], )?; let adapter = interop_adapter(&name, &args.value("project_dir")?)?; @@ -236,22 +243,28 @@ fn resolve_dbt_interop_plan<'py>( let start = Instant::now(); progress(&callback, "Resolving dbt and SQLBuild selection...")?; let invocation = args.value("invocation")?; - let routed = invocation.getattr("routed")?; + let routed = native_field(&invocation, "routed")?; let tuple = py.import("builtins")?.getattr("tuple")?; let command_args = call_with( ®istry::class(py, RecordId::DbtInteropCommandArgs)?, &[ - ("select", tuple.call1((routed.getattr("select")?,))?), - ("exclude", tuple.call1((routed.getattr("exclude")?,))?), + ("select", tuple.call1((native_field(&routed, "select")?,))?), + ( + "exclude", + tuple.call1((native_field(&routed, "exclude")?,))?, + ), ( "dbt_command_args", - tuple.call1((routed.getattr("dbt_args")?,))?, + tuple.call1((native_field(&routed, "dbt_args")?,))?, ), ( "sqlbuild_command_args", - tuple.call1((invocation.getattr("effective_sqlbuild_args")?,))?, + tuple.call1((native_field(&invocation, "effective_sqlbuild_args")?,))?, + ), + ( + "dbt_executable", + native_field(&invocation, "dbt_executable")?, ), - ("dbt_executable", invocation.getattr("dbt_executable")?), ("sqlbuild_executable", args.value("sqlbuild_executable")?), ], )?; @@ -262,8 +275,8 @@ fn resolve_dbt_interop_plan<'py>( ("project", project), ("manifest", args.value("manifest")?), ("graph", graph.clone()), - ("dbt_runner", invocation.getattr("runner")?), - ("dbt_options", invocation.getattr("dbt_options")?), + ("dbt_runner", native_field(&invocation, "runner")?), + ("dbt_options", native_field(&invocation, "dbt_options")?), ("command_args", command_args), ], )?; 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 90e993a695..07d12ad906 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/plan.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/plan.rs @@ -9,6 +9,7 @@ use sqlbuild_core::cli_text::main::format_dbt_interop_plan_json::format_dbt_inte use sqlbuild_core::cli_text::models::{DbtInteropPlanText, DbtLsNodeText, DisplayLimit}; use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_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; @@ -49,51 +50,51 @@ fn reason_value(value: &Bound<'_, PyAny>) -> PyResult> { if value.is_none() { return Ok(None); } - Ok(Some(value.getattr("value")?.extract()?)) + Ok(Some(native_field(value, "value")?.extract()?)) } fn ls_node(node: &Bound<'_, PyAny>) -> PyResult { Ok(DbtLsNodeText { - unique_id: node.getattr("unique_id")?.extract()?, - resource_type: optional_text(&node.getattr("resource_type")?)?, - name: optional_text(&node.getattr("name")?)?, - fqn: strings(&node.getattr("fqn")?)?, + unique_id: native_field(node, "unique_id")?.extract()?, + resource_type: optional_text(&native_field(node, "resource_type")?)?, + name: optional_text(&native_field(node, "name")?)?, + fqn: strings(&native_field(node, "fqn")?)?, }) } fn plan_text(plan: &Bound<'_, PyAny>) -> PyResult { - let selection: Bound<'_, PyAny> = plan.getattr("selection")?; - let anchor_ids: Bound<'_, PyAny> = selection.getattr("dbt_anchor_unique_ids_by_term")?; + let selection: Bound<'_, PyAny> = native_field(plan, "selection")?; + let anchor_ids: Bound<'_, PyAny> = native_field(&selection, "dbt_anchor_unique_ids_by_term")?; let empty: Bound<'_, PyTuple> = PyTuple::empty(plan.py()); let mut anchors: Vec<(String, Vec)> = Vec::new(); - for term in selection.getattr("dbt_anchor_terms")?.try_iter()? { + for term in native_field(&selection, "dbt_anchor_terms")?.try_iter()? { let term: Bound<'_, PyAny> = term?; let unique_ids: Vec = strings(&anchor_ids.call_method1("get", (&term, &empty))?)?; anchors.push((term.extract()?, unique_ids)); } let mut path_translations: Vec<(String, String)> = Vec::new(); - for pair in selection.getattr("path_translations")?.try_iter()? { + for pair in native_field(&selection, "path_translations")?.try_iter()? { path_translations.push(pair?.extract()?); } Ok(DbtInteropPlanText { - command: plan.getattr("command")?.getattr("value")?.extract()?, - dbt_argv: strings(&plan.getattr("dbt_command_argv")?)?, - supplemental_dbt_argvs: argvs(&plan.getattr("supplemental_dbt_command_argvs")?)?, + command: native_field(plan, "command")?.getattr("value")?.extract()?, + dbt_argv: strings(&native_field(plan, "dbt_command_argv")?)?, + supplemental_dbt_argvs: argvs(&native_field(plan, "supplemental_dbt_command_argvs")?)?, dbt_nodes: plan .getattr("dbt_selected_nodes")? .try_iter()? .map(|node| ls_node(&node?)) .collect::>()?, - dbt_selected_unique_ids: strings(&plan.getattr("dbt_selected_unique_ids")?)?, - dbt_required_unique_ids: strings(&selection.getattr("dbt_required_unique_ids")?)?, - dbt_required_selector_terms: strings(&plan.getattr("dbt_required_selector_terms")?)?, - dbt_skip_reason: reason_value(&plan.getattr("dbt_skip_reason")?)?, - sqlbuild_argvs: argvs(&plan.getattr("sqlbuild_command_argvs")?)?, - sqlbuild_models: strings(&selection.getattr("sqlbuild_model_names")?)?, - sqlbuild_skip_reason: reason_value(&plan.getattr("sqlbuild_skip_reason")?)?, + dbt_selected_unique_ids: strings(&native_field(plan, "dbt_selected_unique_ids")?)?, + dbt_required_unique_ids: strings(&native_field(&selection, "dbt_required_unique_ids")?)?, + dbt_required_selector_terms: strings(&native_field(plan, "dbt_required_selector_terms")?)?, + dbt_skip_reason: reason_value(&native_field(plan, "dbt_skip_reason")?)?, + sqlbuild_argvs: argvs(&native_field(plan, "sqlbuild_command_argvs")?)?, + sqlbuild_models: strings(&native_field(&selection, "sqlbuild_model_names")?)?, + sqlbuild_skip_reason: reason_value(&native_field(plan, "sqlbuild_skip_reason")?)?, anchors, path_translations, - warnings: strings(&plan.getattr("warnings")?)?, + warnings: strings(&native_field(plan, "warnings")?)?, }) } @@ -137,13 +138,13 @@ pub(crate) fn build_dbt_interop_plan<'py>( let nodes: Bound<'py, PyAny> = tuple_of(arguments.value("dbt_ls_nodes")?)?; let mut selected: Vec = Vec::new(); for node in nodes.try_iter()? { - selected.push(node?.getattr("unique_id")?.extract()?); + selected.push(native_field(&node?, "unique_id")?.extract()?); } selected.sort(); let selection: Bound<'py, PyAny> = arguments.value("selection")?; let dbt_has_work: bool = - !selected.is_empty() || selection.getattr("dbt_required_unique_ids")?.is_truthy()?; - let sqlbuild_has_work: bool = selection.getattr("sqlbuild_model_names")?.is_truthy()?; + !selected.is_empty() || native_field(&selection, "dbt_required_unique_ids")?.is_truthy()?; + let sqlbuild_has_work: bool = native_field(&selection, "sqlbuild_model_names")?.is_truthy()?; let warnings: Vec = dbt_interop_plan_warnings( strings(&empty_or(arguments.value("warnings")?))?, dbt_has_work || sqlbuild_has_work, @@ -193,8 +194,8 @@ pub(crate) fn build_dbt_interop_plan<'py>( fn display_limit(options: &Bound<'_, PyAny>) -> PyResult { Ok(DisplayLimit { - max_entries: options.getattr("max_entries_per_section")?.extract()?, - overflow_flag: options.getattr("overflow_flag")?.extract()?, + max_entries: native_field(options, "max_entries_per_section")?.extract()?, + overflow_flag: native_field(options, "overflow_flag")?.extract()?, }) } @@ -214,7 +215,7 @@ fn render_dbt_interop_plan(py: Python<'_>, options: Option>) - display = registry::class(py, RecordId::DisplayOptions)?.call0()?; } let use_color: bool = arguments.flag("use_color")?; - let plan_output: Bound<'_, PyAny> = plan.getattr("sqlbuild_plan_output")?; + let plan_output: Bound<'_, PyAny> = native_field(&plan, "sqlbuild_plan_output")?; let sqlbuild_plan: Option = if text.sqlbuild_skip_reason.is_some() || plan_output.is_none() { None 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 72ad4eeb3e..7af4501e51 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 @@ -14,6 +14,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::{ }; 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::{ErrorId, NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; @@ -35,14 +36,14 @@ pub(crate) fn connection_config<'py>( environment: &Bound<'py, PyAny>, ) -> PyResult> { let py = environment.py(); - let discovered = environment.getattr("discovered_inputs")?; + let discovered = native_field(environment, "discovered_inputs")?; let raw = call_with( ®istry::native(py, NativeAttr::BuildEffectiveConnectionConfig)?, &[("discovered_inputs", discovered.clone())], )?; let raw: Bound<'_, PyDict> = raw.cast_into()?; - let project_dir = environment.getattr("project_dir")?; - let adapter_name = environment.getattr("adapter_name")?; + let project_dir = native_field(environment, "project_dir")?; + let adapter_name = native_field(environment, "adapter_name")?; let config: Bound<'_, PyDict> = with_dbt_profile( &raw, raw.copy()?, @@ -58,9 +59,9 @@ pub(crate) fn connection_config<'py>( ®istry::native(py, NativeAttr::ApplyCommandWarehouse)?, &[ ("config", config), - ("adapter_name", environment.getattr("adapter_name")?), + ("adapter_name", native_field(environment, "adapter_name")?), ("discovered_inputs", discovered), - ("project_dir", environment.getattr("project_dir")?), + ("project_dir", native_field(environment, "project_dir")?), ("selected_target", none(py)), ], ) @@ -87,7 +88,7 @@ pub(crate) fn connection_hooks<'py>( if !reporter.is_none() { pairs.push(( "on_connection_start", - reporter.getattr("on_connection_start")?, + native_field(reporter, "on_connection_start")?, )); for name in ["on_connection_complete", "on_connection_error"] { pairs.push(( @@ -109,7 +110,10 @@ fn open_connection<'py>( hooks: &Bound<'py, PyAny>, ) -> PyResult> { let py = environment.py(); - let mut pairs = vec![("on_connection_start", hooks.getattr("on_connection_start")?)]; + let mut pairs = vec![( + "on_connection_start", + native_field(hooks, "on_connection_start")?, + )]; for name in ["on_connection_complete", "on_connection_error"] { let callback = hooks.getattr(name)?; pairs.push(( @@ -122,7 +126,7 @@ fn open_connection<'py>( )); } let relayed = call_with(&host::entry(py, ExecutorHost::ConnectionHooks)?, &pairs)?; - connection_with_hooks(&environment.getattr("adapter")?, &config, &relayed) + connection_with_hooks(&native_field(environment, "adapter")?, &config, &relayed) } /// Build the SQLBuild plan from current warehouse facts, closing the custom adapter connection. @@ -135,7 +139,7 @@ pub(crate) fn build_sqlbuild_plan_output<'py>( let arguments = KeywordArguments::read(py, "build_sqlbuild_plan_output", options, BUILD_OPTIONS)?; let request = arguments.value("request")?; - let selected = request.getattr("selected_model_names")?; + let selected = native_field(&request, "selected_model_names")?; if !selected.is_truthy()? { return Ok(none(py)); } @@ -143,16 +147,16 @@ pub(crate) fn build_sqlbuild_plan_output<'py>( let hooks = arguments.value("hooks")?; let connection = open_connection(&environment, connection_config(&environment)?, &hooks)?; let planned = (|| { - let args = request.getattr("sqlbuild_args")?; + let args = native_field(&request, "sqlbuild_args")?; let deferral = call_with( ®istry::native(py, NativeAttr::ResolveModelDeferralInputs)?, &[ - ("project", environment.getattr("project")?), + ("project", native_field(&environment, "project")?), ( "discovered_inputs", - environment.getattr("discovered_inputs")?, + native_field(&environment, "discovered_inputs")?, ), - ("adapter", environment.getattr("adapter")?), + ("adapter", native_field(&environment, "adapter")?), ("connection", connection.clone()), ("defer_to", flag_value(&args, "--defer-to")?), ], @@ -178,16 +182,16 @@ pub(crate) fn build_sqlbuild_plan_output<'py>( ("full_refresh", full_refresh.clone()), ( "external_blocked_model_names", - request.getattr("external_blocked_model_names")?, + native_field(&request, "external_blocked_model_names")?, ), ], )?; - let discovered = environment.getattr("discovered_inputs")?; + let discovered = native_field(&environment, "discovered_inputs")?; let result = call_with( ®istry::native(py, NativeAttr::BuildExecutionPlan)?, &[ - ("project", environment.getattr("project")?), - ("adapter", environment.getattr("adapter")?), + ("project", native_field(&environment, "project")?), + ("adapter", native_field(&environment, "adapter")?), ("connection", connection.clone()), ("selection", selection), ("overrides", overrides), @@ -196,9 +200,12 @@ pub(crate) fn build_sqlbuild_plan_output<'py>( "policies", registry::class(py, RecordId::PlannerPolicies)?.call0()?, ), - ("on_progress", hooks.getattr("on_progress")?), - ("project_config", discovered.getattr("project_config")?), - ("local_config", discovered.getattr("local_config")?), + ("on_progress", native_field(&hooks, "on_progress")?), + ( + "project_config", + native_field(&discovered, "project_config")?, + ), + ("local_config", native_field(&discovered, "local_config")?), ], ); match result { @@ -206,7 +213,7 @@ pub(crate) fn build_sqlbuild_plan_output<'py>( call_with( ®istry::native(py, NativeAttr::BuildDisplayOnlySqlbuildPlan)?, &[ - ("project", environment.getattr("project")?), + ("project", native_field(&environment, "project")?), ("selected_model_names", selected), ("full_refresh", full_refresh), ], @@ -236,9 +243,9 @@ fn attach_sqlbuild_plan_output<'py>( &[ ("project_dir", args.value("project_dir")?), ("discovered_inputs", args.value("discovered_inputs")?), - ("project", compiled.getattr("project")?), - ("adapter", compiled.getattr("adapter")?), - ("adapter_name", compiled.getattr("adapter_name")?), + ("project", native_field(&compiled, "project")?), + ("adapter", native_field(&compiled, "adapter")?), + ("adapter_name", native_field(&compiled, "adapter_name")?), ], )?; let request = call_with( @@ -246,7 +253,7 @@ fn attach_sqlbuild_plan_output<'py>( &[ ( "selected_model_names", - plan.getattr("selection")?.getattr("sqlbuild_model_names")?, + native_field(&plan, "selection")?.getattr("sqlbuild_model_names")?, ), ("sqlbuild_args", args.value("sqlbuild_args")?), ], @@ -280,11 +287,11 @@ fn attach_sqlbuild_plan_output<'py>( /// Prefer stderr for dbt subprocess failure details, retaining Python whitespace semantics. #[pyfunction] fn dbt_failure_detail<'py>(result: &Bound<'py, PyAny>) -> PyResult> { - let stderr = result.getattr("stderr")?; + let stderr = native_field(result, "stderr")?; let detail = if stderr.is_truthy()? { stderr } else { - result.getattr("stdout")? + native_field(result, "stdout")? } .call_method0("strip")?; if detail.is_truthy()? { 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 01dc966401..1bb253c083 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/profile.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/dbt_interop/profile.rs @@ -15,6 +15,7 @@ use sqlbuild_config::dbt_profile::models::{ use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; 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::RecordId; use crate::bindings::_helpers::records::registry; @@ -106,7 +107,7 @@ fn normalize_dbt_profile_connection<'py>( resolved: Bound<'py, PyAny>, ) -> PyResult> { let output: Bound<'py, PyDict> = PyDict::new(py); - output.update(&resolved.getattr("output")?.cast_into()?)?; + output.update(&native_field(&resolved, "output")?.cast_into()?)?; let mut entries: Vec = Vec::new(); for (key, value) in output.iter() { entries.push(ProfileEntry { 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 28be17377b..8ba22b6097 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 @@ -14,6 +14,7 @@ use crate::bindings::_helpers::dbt_interop::profile_load::{ }; 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; @@ -128,25 +129,25 @@ fn resolve_raw_dbt_profile_connection<'py>( if !is_profile { return Ok(py.None().into_bound(py)); } - let dbt_config: Bound<'py, PyAny> = project_config.getattr("dbt")?; + let dbt_config: Bound<'py, PyAny> = native_field(&project_config, "dbt")?; let dbt_project_dir: Bound<'py, PyAny> = resolved_path( &project_dir, or_config( optional_str(&raw_config, "project_dir")?, - &dbt_config.getattr("project_dir")?, + &native_field(&dbt_config, "project_dir")?, )?, )?; let profiles_dir: Bound<'py, PyAny> = resolved_path( &project_dir, or_config( optional_str(&raw_config, "profiles_dir")?, - &dbt_config.getattr("profiles_dir")?, + &native_field(&dbt_config, "profiles_dir")?, )?, )?; let profile_name: Option = optional_str(&raw_config, "profile")?; let target_name: Option = or_config( optional_str(&raw_config, "target")?, - &dbt_config.getattr("target")?, + &native_field(&dbt_config, "target")?, )?; let cli_vars: Bound<'py, PyAny> = if cli_vars.is_none() { PyDict::new(py).into_any() @@ -159,7 +160,7 @@ fn resolve_raw_dbt_profile_connection<'py>( (profile_name, target_name), cli_vars, )?; - let resolved_adapter: Bound<'py, PyAny> = resolved.getattr("adapter")?; + let resolved_adapter: Bound<'py, PyAny> = native_field(&resolved, "adapter")?; if !resolved_adapter.eq(&adapter_name)? { return Err(profile_error( py, 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 6eeea47c5c..f965dcd379 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 @@ -12,6 +12,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::{ }; 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::{ErrorId, NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; @@ -146,7 +147,7 @@ impl DbtCompileReferenceResolver { } names.add(model_name)?; } - for key in manifest.getattr("models_by_package_and_name")?.try_iter()? { + for key in native_field(&manifest, "models_by_package_and_name")?.try_iter()? { let key: Bound<'py, PyTuple> = key?.cast_into()?; let qualified: String = format!("{}__{}", key.get_item(0)?.str()?, key.get_item(1)?.str()?); @@ -336,7 +337,7 @@ fn manifest_contents<'py>( return Ok(py.None().into_bound(py)); } call_with( - &manifest.getattr("read_text")?, + &native_field(&manifest, "read_text")?, &[("encoding", PyString::new(py, "utf-8").into_any())], ) } 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 1d69871d7c..d620f39fcc 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; use crate::bindings::types::CompilerDetach; @@ -65,7 +66,7 @@ impl DbtRunner { (&self.dbt_executable, options), DbtArgvCommand::Compile { full_refresh }, )?; - self.run(command, &options.getattr("project_dir")?) + self.run(command, &native_field(options, "project_dir")?) } /// Run dbt deps. @@ -76,7 +77,7 @@ impl DbtRunner { (&self.dbt_executable, options), DbtArgvCommand::Deps, )?; - self.run(command, &options.getattr("project_dir")?) + self.run(command, &native_field(options, "project_dir")?) } /// Run dbt debug. @@ -92,7 +93,7 @@ impl DbtRunner { (&self.dbt_executable, options), DbtArgvCommand::Debug { args }, )?; - self.run(command, &options.getattr("project_dir")?) + self.run(command, &native_field(options, "project_dir")?) } /// Run dbt ls and parse selected nodes, reusing an earlier identical listing. @@ -121,8 +122,9 @@ impl DbtRunner { if let Some(cached) = self.cached_listing(py, &key)? { return Ok(cached); } - let result: Bound<'py, PyAny> = self.run(command, &options.getattr("project_dir")?)?; - let stdout: String = result.getattr("stdout")?.extract()?; + 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)?, &[ 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 d707c38bb0..50cb28f9f8 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 @@ -16,6 +16,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::call_with; 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; @@ -235,8 +236,8 @@ pub(crate) fn vars_mapping<'py>( ) -> PyResult> { let py: Python<'py> = project_config.py(); let values: Bound<'py, PyDict> = PyDict::new(py); - values.call_method1("update", (project_config.getattr("vars")?,))?; - values.call_method1("update", (local_config.getattr("vars")?,))?; + values.call_method1("update", (native_field(project_config, "vars")?,))?; + values.call_method1("update", (native_field(local_config, "vars")?,))?; values.update(cli_vars(py, dbt_flag_value(args, VARS_FLAG))?.as_mapping())?; Ok(values) } @@ -263,7 +264,7 @@ pub(crate) fn plan_options<'py>( .getattr("project_config")? .getattr("dbt")?; let local_config: Bound<'py, PyAny> = - discovered_inputs.getattr("local_config")?.getattr("dbt")?; + native_field(discovered_inputs, "local_config")?.getattr("dbt")?; let resolved: Bound<'py, PyAny> = call_with( ®istry::native(py, NativeAttr::ResolveDbtConfig)?, &[ @@ -286,10 +287,10 @@ pub(crate) fn plan_options<'py>( call_with( ®istry::class(py, RecordId::DbtCliOptions)?, &[ - ("project_dir", resolved.getattr("project_dir")?), - ("profiles_dir", resolved.getattr("profiles_dir")?), - ("target", resolved.getattr("target")?), - ("target_path", resolved.getattr("target_path")?), + ("project_dir", native_field(&resolved, "project_dir")?), + ("profiles_dir", native_field(&resolved, "profiles_dir")?), + ("target", native_field(&resolved, "target")?), + ("target_path", native_field(&resolved, "target_path")?), ("vars", vars), ( "state", @@ -303,11 +304,11 @@ pub(crate) fn plan_options<'py>( /// The manifest path `dbt compile` writes for these options. pub(crate) fn manifest_path<'py>(options: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = options.py(); - let project_dir: Bound<'py, PyAny> = options.getattr("project_dir")?; + let project_dir: Bound<'py, PyAny> = native_field(options, "project_dir")?; if project_dir.is_none() { return Err(config_error(py, "dbt project directory is not configured")); } - let mut target_path: Bound<'py, PyAny> = options.getattr("target_path")?; + let mut target_path: Bound<'py, PyAny> = native_field(options, "target_path")?; if !target_path.is_truthy()? { target_path = project_dir.div(DEFAULT_TARGET_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 704d2a8e74..cc27de688d 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 @@ -2,6 +2,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; use crate::bindings::_helpers::dbt_interop::runtime_results::{append_missing, result_ids}; +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 pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; @@ -65,7 +66,7 @@ fn expected_total<'py>( ], ); call_with( - &status.getattr("complete")?, + &native_field(&status, "complete")?, &[( "message", PyString::new( @@ -79,12 +80,12 @@ fn expected_total<'py>( )], )?; let result = result?; - if result.getattr("returncode")?.extract::()? != 0 { + if native_field(&result, "returncode")?.extract::()? != 0 { return Ok(py.None().into_bound(py)); } let nodes = call_with( ®istry::native(py, NativeAttr::ParseDbtLsJsonLines)?, - &[("stdout", result.getattr("stdout")?)], + &[("stdout", native_field(&result, "stdout")?)], )?; Ok(PyInt::new(py, nodes.len()?).into_any()) } @@ -168,8 +169,8 @@ fn execute_dbt_commands<'py>( expected_total(&arguments, &argv)? }; let dbt_options = arguments.value("options")?; - let mut target = dbt_options.getattr("target_path")?; - let project_dir = dbt_options.getattr("project_dir")?; + let mut target = native_field(&dbt_options, "target_path")?; + let project_dir = native_field(&dbt_options, "project_dir")?; if target.is_none() && !project_dir.is_none() { target = project_dir.call_method1("__truediv__", ("target",))?; } @@ -191,7 +192,7 @@ fn execute_dbt_commands<'py>( let mut filtered: Vec> = Vec::new(); for result in streamed.try_iter()? { let result = result?; - let id: String = result.getattr("unique_id")?.extract()?; + let id: String = native_field(&result, "unique_id")?.extract()?; if expected.as_ref().is_none_or(|ids| ids.contains(&id)) { filtered.push(result); } @@ -204,7 +205,7 @@ fn execute_dbt_commands<'py>( let mut appended: Vec> = Vec::new(); for result in results.try_iter()? { let result = result?; - if !streamed_ids.contains(&result.getattr("unique_id")?.extract::()?) { + if !streamed_ids.contains(&native_field(&result, "unique_id")?.extract::()?) { appended.push(result); } } @@ -214,7 +215,7 @@ fn execute_dbt_commands<'py>( ®istry::native(py, NativeAttr::RenderDbtNodeResult)?, &[ ("result", result.clone()), - ("use_color", style.getattr("use_color")?), + ("use_color", native_field(&style, "use_color")?), ( "display_index", PyInt::new(py, count + offset + 1).into_any(), 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 92af81bd67..75fdc61f63 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,6 +1,7 @@ //! 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; use pyo3::exceptions::PyOSError; @@ -13,7 +14,7 @@ use std::collections::HashSet; pub(crate) fn result_ids(results: &Bound<'_, PyAny>) -> PyResult> { results .try_iter()? - .map(|result| result?.getattr("unique_id")?.extract()) + .map(|result| native_field(&result?, "unique_id")?.extract()) .collect() } @@ -74,16 +75,16 @@ pub(crate) fn append_missing<'py>( let json = py.import("json")?; let payload = (|| { let text = call_with( - &path.getattr("read_text")?, + &native_field(&path, "read_text")?, &[("encoding", PyString::new(py, "utf-8").into_any())], )?; - json.getattr("loads")?.call1((text,)) + native_field(&json, "loads")?.call1((text,)) })(); let payload = match payload { Ok(payload) => payload, Err(error) if error.is_instance_of::(py) - || error.is_instance(py, &json.getattr("JSONDecodeError")?) => + || error.is_instance(py, &native_field(&json, "JSONDecodeError")?) => { return Ok(results.clone()); } 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 ab53114c3d..8c25401c45 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 @@ -14,6 +14,7 @@ use crate::bindings::_helpers::build_command::support::{ use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; 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, RecordId}; use crate::bindings::_helpers::records::registry; @@ -73,17 +74,26 @@ fn execute_dbt_sqlbuild_work<'py>( let args = KeywordArguments::read(py, "execute_dbt_sqlbuild_work", options, WORK_OPTIONS)?; let context = args.value("context")?; let project = args.value("project")?; - let plan = context.getattr("plan_output")?; - let output = context.getattr("output_stream")?; - let color = context.getattr("use_color")?; + 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", context.getattr("snapshots_config")?), - ("target_name", project.getattr("effective_target_name")?), - ("execution_limits", context.getattr("execution_limits")?), + ( + "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()])?, @@ -101,12 +111,15 @@ fn execute_dbt_sqlbuild_work<'py>( )?; let command = args.value("command")?; let kinds = registry::class(py, RecordId::DbtInteropCommand)?; - let display = if command.eq(kinds.getattr("RUN")?)? { + let display = if command.eq(native_field(&kinds, "RUN")?)? { "sqb build --no-tests --no-audits".to_owned() } else { - format!("sqb {}", command.getattr("value")?.extract::()?) + format!( + "sqb {}", + native_field(&command, "value")?.extract::()? + ) }; - let concurrency = project.getattr("settings")?.getattr("concurrency")?; + let concurrency = native_field(&project, "settings")?.getattr("concurrency")?; let header = call_with( ®istry::native(py, NativeAttr::FormatExecutionHeader)?, &[ @@ -134,17 +147,17 @@ fn execute_dbt_sqlbuild_work<'py>( let connection = call_with( ®istry::native(py, NativeAttr::ConnectionProgressReporter)?, &[ - ("adapter_name", context.getattr("adapter_name")?), + ("adapter_name", native_field(&context, "adapter_name")?), ("stream", output.clone()), ("blank_line_after_complete", boolean(py, true)), ("use_color", color.clone()), ], )?; - let build = command.eq(kinds.getattr("BUILD")?)?; + let build = command.eq(native_field(&kinds, "BUILD")?)?; let runtime = call_with( &host::entry(py, ExecutorHost::BuildRuntimeParams)?, &[ - ("run_id", project.getattr("run_id")?), + ("run_id", native_field(&project, "run_id")?), ("run_tests", boolean(py, build)), ("run_audits", boolean(py, build)), ("fail_fast", args.value("fail_fast")?), @@ -158,27 +171,33 @@ fn execute_dbt_sqlbuild_work<'py>( ( "on_node_start", keyword_relay( - callbacks.getattr("on_node_start")?, + native_field(&callbacks, "on_node_start")?, &["name", "resource_kind"], )?, ), - ("on_node_complete", callbacks.getattr("on_node_complete")?), - ("on_sub_progress", callbacks.getattr("on_sub_progress")?), + ( + "on_node_complete", + native_field(&callbacks, "on_node_complete")?, + ), + ( + "on_sub_progress", + native_field(&callbacks, "on_sub_progress")?, + ), ( "on_connection_start", - connection.getattr("on_connection_start")?, + native_field(&connection, "on_connection_start")?, ), ( "on_connection_complete", keyword_relay( - connection.getattr("on_connection_complete")?, + native_field(&connection, "on_connection_complete")?, &["connection_count", "elapsed_seconds"], )?, ), ( "on_connection_error", keyword_relay( - connection.getattr("on_connection_error")?, + native_field(&connection, "on_connection_error")?, &["connection_count", "elapsed_seconds"], )?, ), @@ -188,9 +207,12 @@ fn execute_dbt_sqlbuild_work<'py>( &host::entry(py, ExecutorHost::RunBuildPipeline)?, &[ ("plan", plan.clone()), - ("connection_config", context.getattr("connection_config")?), - ("adapter", context.getattr("adapter")?), - ("settings", project.getattr("settings")?), + ( + "connection_config", + native_field(&context, "connection_config")?, + ), + ("adapter", native_field(&context, "adapter")?), + ("settings", native_field(&project, "settings")?), ("runtime", runtime), ("callbacks", hooks), ], @@ -210,12 +232,12 @@ fn execute_dbt_sqlbuild_work<'py>( ®istry::native(py, NativeAttr::FormatBuildFooter)?, &[ ("result", result.clone()), - ("elapsed", callbacks.getattr("elapsed")?), + ("elapsed", native_field(&callbacks, "elapsed")?), ("use_color", color), ], )?; emit(&output, &format!("\n{}\n", footer.extract::()?))?; - Ok(i64::from(!result.getattr("status")?.eq( + 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 fd439a288e..364c3915cf 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 @@ -34,6 +34,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::{ KeywordArguments, call_with, imported, within, }; 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, RecordId}; use crate::bindings::_helpers::records::registry; @@ -112,7 +113,9 @@ fn warehouse_checks( .adapter .getattr("supports_session_warehouse")? .is_truthy()?; - let adapter_name: String = context.adapter.getattr("adapter_name")?.str()?.extract()?; + let adapter_name: String = native_field(&context.adapter, "adapter_name")? + .str()? + .extract()?; let inputs: WarehouseInputs<'_, '_> = WarehouseInputs { discovered_inputs: &context.discovered_inputs, selected_target: &context.selected_target, @@ -120,7 +123,7 @@ fn warehouse_checks( let mut checks: Vec = Vec::new(); for group in registry::class(py, RecordId::WarehouseGroup)?.try_iter()? { let group: Bound<'_, PyAny> = group?; - let group_name: String = group.getattr("value")?.str()?.extract()?; + let group_name: String = native_field(&group, "value")?.str()?.extract()?; if !supports_session_warehouse { if let Some(found) = find_authored(inputs, &group)? { checks.push(WarehouseCheck::NotUsed { @@ -154,11 +157,11 @@ fn warehouse_checks( fn providers(discovered_inputs: &Bound<'_, PyAny>) -> PyResult> { let mut providers: Vec = Vec::new(); - for provider in discovered_inputs.getattr("providers")?.try_iter()? { + for provider in native_field(discovered_inputs, "providers")?.try_iter()? { let provider: Bound<'_, PyAny> = provider?; providers.push(DebugProvider { - name: provider.getattr("name")?.str()?.extract()?, - relative_path: provider.getattr("relative_path")?.str()?.extract()?, + name: native_field(&provider, "name")?.str()?.extract()?, + relative_path: native_field(&provider, "relative_path")?.str()?.extract()?, class_name: provider .getattr("provider_class")? .getattr("__name__")? @@ -205,7 +208,7 @@ fn connection_check( Err(error) => return Ok(ConnectionCheck::ConnectFailed(check_failure(py, error)?)), }; let queried: PyResult> = call_with( - &adapter.getattr("query")?, + &native_field(adapter, "query")?, &[ ("connection", connection.clone()), ("sql", PyString::new(py, "SELECT 1").into_any()), @@ -266,9 +269,12 @@ fn build_report( &[ ( "project_config", - discovered_inputs.getattr("project_config")?, + native_field(&discovered_inputs, "project_config")?, + ), + ( + "local_config", + native_field(&discovered_inputs, "local_config")?, ), - ("local_config", discovered_inputs.getattr("local_config")?), ], )?; let adapter: Bound<'_, PyAny> = resolve_cli_adapter(&adapter_name, &project_dir_object)?; @@ -323,7 +329,7 @@ fn build_report( let connection_name: Option = if target_config.is_none() { None } else { - optional_string(&target_config.getattr("connection_name")?)? + optional_string(&native_field(&target_config, "connection_name")?)? }; let facts: DebugProjectFacts = DebugProjectFacts { project_file: path_text(py, &files.project_file)?, 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 e8e8571bbe..9b958d2487 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/diagnostics/console_records.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/diagnostics/console_records.rs @@ -1,5 +1,6 @@ //! The `--debug` stderr line of one Python log record. +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::exceptions::PyOverflowError; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult}; use pyo3::types::{PyInt, PyString}; @@ -72,8 +73,8 @@ fn format_diagnostics_console_record( None }; let text = DiagnosticRecordText { - level_name: record.getattr("levelname")?.extract()?, - logger_name: record.getattr("name")?.extract()?, + level_name: native_field(record, "levelname")?.extract()?, + logger_name: native_field(record, "name")?.extract()?, message: record.call_method0("getMessage")?.extract()?, subject, name: text_field(record, "sqlbuild_name")?, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/diagnostics/log_records.rs b/crates/sqlbuild-python/src/bindings/_helpers/diagnostics/log_records.rs index 9e207eb743..16f4745a8d 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/diagnostics/log_records.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/diagnostics/log_records.rs @@ -26,6 +26,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::imported; use crate::bindings::_helpers::diagnostics::redaction::redacted_value; 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; const PRIVATE_SQL_MARKER: ExecutorHost = ExecutorHost::SqlPrivateRecordMarker; @@ -164,8 +165,8 @@ fn route_diagnostic_log_record<'py>( let py: Python<'py> = record.py(); let values: Bound<'py, PyDict> = record_values(record)?; let facts = RecordFacts { - internal: internal_name(&record.getattr("name")?)?, - level: record.getattr("levelno")?.extract()?, + internal: internal_name(&native_field(record, "name")?)?, + level: native_field(record, "levelno")?.extract()?, has_sql_digest: values.contains(SQL_DIGEST_FIELD)?, has_sql_text: values.contains(SQL_TEXT_FIELD)?, }; @@ -184,7 +185,7 @@ fn route_diagnostic_log_record<'py>( if facts.has_sql_digest { REDACTED_SQL_LOG_MESSAGE.clone_into(&mut message); } - let exc_info: Bound<'py, PyAny> = record.getattr("exc_info")?; + let exc_info: Bound<'py, PyAny> = native_field(record, "exc_info")?; if !exc_info.is_none() { let formatted: PyResult = imported(py, "logging", "Formatter") .and_then(|formatter| formatter.call0()) @@ -271,7 +272,7 @@ fn retained<'py>( } else { extra_fields(record)? }; - return Ok((message, record.getattr("name")?, fields)); + return Ok((message, native_field(record, "name")?, fields)); } let sql_fields: Bound<'py, PyDict> = PyDict::new(py); for (key, value) in fields.iter() { @@ -298,12 +299,12 @@ fn compute_diagnostic_log_fields<'py>( invocation_id: &str, include_sql_text: bool, ) -> PyResult>> { - let name: Bound<'py, PyAny> = record.getattr("name")?; + let name: Bound<'py, PyAny> = native_field(record, "name")?; let Ok(name) = name.downcast::() else { return Ok(None); }; let values: Bound<'py, PyDict> = record_values(record)?; - let level: i64 = record.getattr("levelno")?.extract()?; + let level: i64 = native_field(record, "levelno")?.extract()?; let facts = RecordFacts { internal: internal_logger(name.to_str()?), level, 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 e9a96b89df..a147a603f4 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/diff_report/command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/diff_report/command.rs @@ -32,6 +32,7 @@ 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::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr}; use crate::bindings::_helpers::records::registry; @@ -75,7 +76,7 @@ fn resolve_invocation<'py>( &error, )); } - let project_dir: Bound<'py, PyAny> = match request.getattr("project_dir")? { + let project_dir: Bound<'py, PyAny> = match native_field(request, "project_dir")? { directory if !directory.is_none() => directory, _ => imported(py, "pathlib", "Path")?.call_method0("cwd")?, }; @@ -90,8 +91,8 @@ fn resolve_invocation<'py>( call( ®istry::native(py, NativeAttr::RejectUnitTestSelectors)?, &[ - ("select", &request.getattr("select")?), - ("exclude", &request.getattr("exclude")?), + ("select", &native_field(request, "select")?), + ("exclude", &native_field(request, "exclude")?), ("discovered_inputs", &discovered), ], )?; @@ -183,13 +184,13 @@ fn run_query_diff( Err(error) => return Err(query_execution_error(py, error)?), }; let models: Vec = diff_facts(&result)?; - let schema_only: bool = request.getattr("schema_only")?.is_truthy()?; + let schema_only: bool = native_field(request, "schema_only")?.is_truthy()?; let (outcome, exit_code) = diff_exit_code(&models, !schema_only); let published: PyResult<()> = publish_diff_output( request, ( - &preparation.getattr("left_label")?.extract::()?, - &preparation.getattr("right_label")?.extract::()?, + &native_field(&preparation, "left_label")?.extract::()?, + &native_field(&preparation, "right_label")?.extract::()?, ), ( preparation @@ -226,8 +227,11 @@ fn target_max_full_rows<'py>( let configured: Bound<'py, PyAny> = call( ®istry::native(py, NativeAttr::ResolveTargetConfig)?, &[ - ("project_config", &discovered.getattr("project_config")?), - ("local_config", &discovered.getattr("local_config")?), + ( + "project_config", + &native_field(discovered, "project_config")?, + ), + ("local_config", &native_field(discovered, "local_config")?), ("target_name", target), ], )? @@ -249,15 +253,18 @@ fn diff_to_target<'py>( from_target: &str, ) -> PyResult> { let py: Python<'py> = request.py(); - let to_name: Bound<'py, PyAny> = request.getattr("to_name")?; + let to_name: Bound<'py, PyAny> = native_field(request, "to_name")?; if !to_name.is_none() { return Ok(to_name); } let active: Bound<'py, PyAny> = call( ®istry::native(py, NativeAttr::ResolveTargetName)?, &[ - ("project_config", &discovered.getattr("project_config")?), - ("local_config", &discovered.getattr("local_config")?), + ( + "project_config", + &native_field(discovered, "project_config")?, + ), + ("local_config", &native_field(discovered, "local_config")?), ("selected_target", &none(py)), ], )?; @@ -287,13 +294,13 @@ fn diff_to_target<'py>( } fn side_facts(side: &Bound<'_, PyAny>) -> PyResult { - let detail: Bound<'_, PyAny> = side.getattr("detail")?; + let detail: Bound<'_, PyAny> = native_field(side, "detail")?; Ok(FullDiffSideFacts { - target: side.getattr("target")?.extract()?, - relation: side.getattr("relation")?.extract()?, - row_count: side.getattr("row_count")?.extract()?, - max_rows: side.getattr("max_rows")?.extract()?, - exceeds_limit: side.getattr("exceeds_limit")?.is_truthy()?, + target: native_field(side, "target")?.extract()?, + relation: native_field(side, "relation")?.extract()?, + row_count: native_field(side, "row_count")?.extract()?, + max_rows: native_field(side, "max_rows")?.extract()?, + exceeds_limit: native_field(side, "exceeds_limit")?.is_truthy()?, detail: if detail.is_none() { None } else { @@ -311,16 +318,16 @@ fn size_guard_error( ) -> PyResult { let value: Bound<'_, PyAny> = error.value(py).clone().into_any(); let mut blocked: Vec = Vec::new(); - for model in value.getattr("blocked")?.try_iter()? { + for model in native_field(&value, "blocked")?.try_iter()? { let model: Bound<'_, PyAny> = model?; blocked.push(FullDiffModelFacts { - name: model.getattr("name")?.extract()?, - has_cursor: model.getattr("has_cursor")?.is_truthy()?, - left: side_facts(&model.getattr("left")?)?, - right: side_facts(&model.getattr("right")?)?, + name: native_field(&model, "name")?.extract()?, + has_cursor: native_field(&model, "has_cursor")?.is_truthy()?, + left: side_facts(&native_field(&model, "left")?)?, + right: side_facts(&native_field(&model, "right")?)?, }); } - let message: String = value.getattr("message")?.extract()?; + let message: String = native_field(&value, "message")?.extract()?; let report: SizeGuardReport = size_guard_report(facts, targets, &message, &blocked); let details_text: String = dumps( &report.details, @@ -351,7 +358,8 @@ fn run_model_diff( }; let to_target: Bound<'_, PyAny> = diff_to_target(request, discovered, from_name)?; let to_name: String = to_target.extract()?; - let targets: Bound<'_, PyAny> = discovered.getattr("project_config")?.getattr("targets")?; + let targets: Bound<'_, PyAny> = + native_field(discovered, "project_config")?.getattr("targets")?; if !targets.contains(from_name)? { return Err(rejected( py, @@ -370,7 +378,7 @@ 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> = request.getattr("cli_vars")?; + let cli_vars: Bound<'_, PyAny> = native_field(request, "cli_vars")?; let connection_config: Bound<'_, PyAny> = call( ®istry::native(py, NativeAttr::ResolveTargetConnectionConfig)?, &[ @@ -395,9 +403,12 @@ fn run_model_diff( ("from_target", &from_target), ("to_target", &to_target), ("resolved_connection", &connection_config), - ("no_sql_validation", &request.getattr("no_sql_validation")?), - ("select", &request.getattr("select")?), - ("exclude", &request.getattr("exclude")?), + ( + "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), ], @@ -414,7 +425,7 @@ fn run_model_diff( ), )); } - let verbose: bool = request.getattr("verbose")?.is_truthy()?; + let verbose: bool = native_field(request, "verbose")?.is_truthy()?; let column_examples: i64 = effective_max_examples(facts.max_column_examples, verbose); let row_only_examples: i64 = effective_max_examples(facts.max_row_only_examples, verbose); let limits: Bound<'_, PyAny> = if facts.full || facts.schema_only || facts.bounded.is_some() { @@ -438,10 +449,10 @@ fn run_model_diff( }; let connection: Bound<'_, PyAny> = adapter.call_method1("connect", (&connection_config,))?; let extras: [(&str, Bound<'_, PyAny>); 5] = [ - ("bounded", request.getattr("bounded")?), + ("bounded", native_field(request, "bounded")?), ("max_column_examples", object(py, column_examples)?), ("max_row_only_examples", object(py, row_only_examples)?), - ("max_models", request.getattr("max_models")?), + ("max_models", native_field(request, "max_models")?), ("full_size_limits", limits), ]; let extra: Vec<(&str, &Bound<'_, PyAny>)> = 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 56bd8aeea2..aa39b587ca 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/diff_report/facts.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/diff_report/facts.rs @@ -14,6 +14,7 @@ use sqlbuild_core::json::main::python_float_repr::python_float_repr; use sqlbuild_core::json::models::{JsonInteger, JsonValue}; use crate::bindings::_helpers::compile_driver::python_calls::imported; +use crate::bindings::_helpers::records::fields::field as native_field; fn text_of(value: &Bound<'_, PyAny>) -> PyResult { Ok(value.str()?.to_string_lossy().into_owned()) @@ -131,40 +132,40 @@ fn coverage(value: Bound<'_, PyAny>) -> PyResult> { return Ok(None); } Ok(Some(DiffCoverageFacts { - row_count: value.getattr("row_count")?.extract()?, - minimum_cursor: optional_value(value.getattr("minimum_cursor")?)?, - maximum_cursor: optional_value(value.getattr("maximum_cursor")?)?, + row_count: native_field(&value, "row_count")?.extract()?, + minimum_cursor: optional_value(native_field(&value, "minimum_cursor")?)?, + maximum_cursor: optional_value(native_field(&value, "maximum_cursor")?)?, })) } fn column(value: &Bound<'_, PyAny>) -> PyResult { - let tolerance: Bound<'_, PyAny> = value.getattr("tolerance")?; + let tolerance: Bound<'_, PyAny> = native_field(value, "tolerance")?; Ok(DiffColumnFacts { - name: text_of(&value.getattr("name")?)?, - mismatched_count: value.getattr("mismatched_count")?.extract()?, + name: text_of(&native_field(value, "name")?)?, + mismatched_count: native_field(value, "mismatched_count")?.extract()?, tolerance: if tolerance.is_none() { None } else { Some(( - optional_text(tolerance.getattr("absolute")?)?, - optional_text(tolerance.getattr("relative")?)?, + optional_text(native_field(&tolerance, "absolute")?)?, + optional_text(native_field(&tolerance, "relative")?)?, )) }, }) } fn coverage_differs(rows: &Bound<'_, PyAny>) -> PyResult { - let left: Bound<'_, PyAny> = rows.getattr("left_coverage")?; - let right: Bound<'_, PyAny> = rows.getattr("right_coverage")?; + let left: Bound<'_, PyAny> = native_field(rows, "left_coverage")?; + let right: Bound<'_, PyAny> = native_field(rows, "right_coverage")?; if left.is_none() || right.is_none() { return Ok(false); } Ok(left .getattr("minimum_cursor")? - .ne(right.getattr("minimum_cursor")?)? + .ne(native_field(&right, "minimum_cursor")?)? || left .getattr("maximum_cursor")? - .ne(right.getattr("maximum_cursor")?)?) + .ne(native_field(&right, "maximum_cursor")?)?) } fn row_facts(rows: Bound<'_, PyAny>) -> PyResult> { @@ -173,10 +174,10 @@ fn row_facts(rows: Bound<'_, PyAny>) -> PyResult> { } let count = |name: &str| -> PyResult { rows.getattr(name)?.extract() }; let mut columns: Vec = Vec::new(); - for item in rows.getattr("column_results")?.try_iter()? { + for item in native_field(&rows, "column_results")?.try_iter()? { columns.push(column(&item?)?); } - let sampling: Bound<'_, PyAny> = rows.getattr("sampling")?; + let sampling: Bound<'_, PyAny> = native_field(&rows, "sampling")?; Ok(Some(DiffRowFacts { left_count: count("left_count")?, right_count: count("right_count")?, @@ -192,81 +193,81 @@ fn row_facts(rows: Bound<'_, PyAny>) -> PyResult> { None } else { Some(( - sampling.getattr("row_limit")?.extract()?, - sampling.getattr("seed")?.extract()?, + native_field(&sampling, "row_limit")?.extract()?, + native_field(&sampling, "seed")?.extract()?, )) }, - left_coverage: coverage(rows.getattr("left_coverage")?)?, - right_coverage: coverage(rows.getattr("right_coverage")?)?, - cursor_column: optional_text(rows.getattr("cursor_column")?)?, - cursor_start: optional_value(rows.getattr("cursor_start")?)?, - cursor_end: optional_value(rows.getattr("cursor_end")?)?, - exhaustive: rows.getattr("is_exhaustive")?.is_truthy()?, + left_coverage: coverage(native_field(&rows, "left_coverage")?)?, + right_coverage: coverage(native_field(&rows, "right_coverage")?)?, + cursor_column: optional_text(native_field(&rows, "cursor_column")?)?, + cursor_start: optional_value(native_field(&rows, "cursor_start")?)?, + cursor_end: optional_value(native_field(&rows, "cursor_end")?)?, + exhaustive: native_field(&rows, "is_exhaustive")?.is_truthy()?, coverage_differs: coverage_differs(&rows)?, })) } fn sample_row(value: &Bound<'_, PyAny>) -> PyResult { let mut changed_cells: Vec = Vec::new(); - for cell in value.getattr("changed_cells")?.try_iter()? { + for cell in native_field(value, "changed_cells")?.try_iter()? { let cell: Bound<'_, PyAny> = cell?; changed_cells.push(DiffChangedCell { - name: text_of(&cell.getattr("name")?)?, - left: diff_value(&cell.getattr("left_value")?)?, - right: diff_value(&cell.getattr("right_value")?)?, + name: text_of(&native_field(&cell, "name")?)?, + left: diff_value(&native_field(&cell, "left_value")?)?, + right: diff_value(&native_field(&cell, "right_value")?)?, }); } Ok(DiffSampleRow { - key_values: named_values(&value.getattr("key_values")?)?, + key_values: named_values(&native_field(value, "key_values")?)?, changed_cells, }) } fn model_facts(model: &Bound<'_, PyAny>) -> PyResult { - let schema: Bound<'_, PyAny> = model.getattr("schema_result")?; + 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()? { - names.push(text_of(&column?.getattr("name")?)?); + names.push(text_of(&native_field(&column?, "name")?)?); } Ok(names) }; let mut type_changes: Vec<(String, String, String)> = Vec::new(); - for pair in schema.getattr("type_changed_columns")?.try_iter()? { + for pair in native_field(&schema, "type_changed_columns")?.try_iter()? { let (left, right): (Bound<'_, PyAny>, Bound<'_, PyAny>) = pair?.extract()?; type_changes.push(( - text_of(&left.getattr("name")?)?, - text_of(&left.getattr("type")?)?, - text_of(&right.getattr("type")?)?, + text_of(&native_field(&left, "name")?)?, + text_of(&native_field(&left, "type")?)?, + text_of(&native_field(&right, "type")?)?, )); } let mut unequal_samples: Vec = Vec::new(); - for sample in model.getattr("unequal_row_samples")?.try_iter()? { + for sample in native_field(model, "unequal_row_samples")?.try_iter()? { unequal_samples.push(sample_row(&sample?)?); } Ok(DiffModelFacts { - name: text_of(&model.getattr("name")?)?, - left_relation: text_of(&model.getattr("left_relation")?)?, - right_relation: text_of(&model.getattr("right_relation")?)?, + name: text_of(&native_field(model, "name")?)?, + left_relation: text_of(&native_field(model, "left_relation")?)?, + right_relation: text_of(&native_field(model, "right_relation")?)?, added_columns: names("added_columns")?, removed_columns: names("removed_columns")?, type_changes, - unique_key: strings(&model.getattr("unique_key")?)?, - rows: row_facts(model.getattr("row_result")?)?, + unique_key: strings(&native_field(model, "unique_key")?)?, + rows: row_facts(native_field(model, "row_result")?)?, unequal_samples, - left_only_keys: key_samples(&model.getattr("left_only_key_samples")?)?, - right_only_keys: key_samples(&model.getattr("right_only_key_samples")?)?, - bounded_fallback: model.getattr("bounded_fallback")?.is_truthy()?, - excluded_columns: strings(&model.getattr("excluded_columns")?)?, - input_kind: text_of(&model.getattr("input_kind")?)?, - unkeyed: model.getattr("unkeyed")?.is_truthy()?, + left_only_keys: key_samples(&native_field(model, "left_only_key_samples")?)?, + right_only_keys: key_samples(&native_field(model, "right_only_key_samples")?)?, + bounded_fallback: native_field(model, "bounded_fallback")?.is_truthy()?, + excluded_columns: strings(&native_field(model, "excluded_columns")?)?, + input_kind: text_of(&native_field(model, "input_kind")?)?, + unkeyed: native_field(model, "unkeyed")?.is_truthy()?, }) } /// Every model result of a `DiffExecutionResult`. pub(crate) fn diff_facts(result: &Bound<'_, PyAny>) -> PyResult> { let mut models: Vec = Vec::new(); - for model in result.getattr("model_results")?.try_iter()? { + for model in native_field(result, "model_results")?.try_iter()? { models.push(model_facts(&model?)?); } Ok(models) 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 7c20b69051..71d42879e0 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/diff_report/query.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/diff_report/query.rs @@ -22,6 +22,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; use crate::bindings::types::CompilerDetach; @@ -56,8 +57,11 @@ pub(crate) fn project_adapter<'py>( let name: Bound<'py, PyAny> = call( ®istry::native(py, NativeAttr::ResolveEffectiveAdapterName)?, &[ - ("project_config", &discovered.getattr("project_config")?), - ("local_config", &discovered.getattr("local_config")?), + ( + "project_config", + &native_field(discovered, "project_config")?, + ), + ("local_config", &native_field(discovered, "local_config")?), ], )?; let adapter: Bound<'py, PyAny> = resolve_cli_adapter(&name, project_dir)?; @@ -167,8 +171,8 @@ pub(crate) fn prepare_query_diff<'py>( ) -> PyResult> { let py: Python<'py> = request.py(); let (adapter_name, adapter) = project_adapter(discovered, project_dir)?; - let selected_target: Bound<'py, PyAny> = request.getattr("selected_target")?; - let cli_vars: Bound<'py, PyAny> = request.getattr("cli_vars")?; + 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)?, &[ @@ -216,14 +220,14 @@ pub(crate) fn prepare_query_diff<'py>( } let left_sql: String = query_input( py, - request.getattr("left_query")?, - request.getattr("left_query_file")?, + native_field(request, "left_query")?, + native_field(request, "left_query_file")?, "left", )?; let right_sql: String = query_input( py, - request.getattr("right_query")?, - request.getattr("right_query_file")?, + native_field(request, "right_query")?, + native_field(request, "right_query_file")?, "right", )?; let left_sql: String = read_only_query(py, &left_sql, "left", &adapter)?; @@ -246,7 +250,7 @@ pub(crate) fn prepare_query_diff<'py>( }; Ok(text.trim().to_owned()) }; - let verbose: bool = request.getattr("verbose")?.is_truthy()?; + let verbose: bool = native_field(request, "verbose")?.is_truthy()?; let examples = |name: &str| -> PyResult> { object( py, @@ -343,29 +347,31 @@ pub(crate) fn execute_query_diff<'py>( preparation: &Bound<'py, PyAny>, ) -> PyResult<(Bound<'py, PyAny>, PhaseSeconds)> { let py: Python<'py> = request.py(); - let adapter: Bound<'py, PyAny> = preparation.getattr("adapter")?; + let adapter: Bound<'py, PyAny> = native_field(preparation, "adapter")?; let total_started: Instant = Instant::now(); let mut phases: Vec<(String, f64)> = Vec::new(); progress_line(py, "Query diff connection START")?; let connection_started: Instant = Instant::now(); - let connection: Bound<'py, PyAny> = - match adapter.call_method1("connect", (preparation.getattr("connection_config")?,)) { - Ok(connection) => connection, - Err(error) => { - progress_line( - py, - &format!( - "Query diff connection ERROR ({:.2}s)", - seconds(connection_started) - ), - )?; - progress_line( - py, - &format!("Query diff total ERROR ({:.2}s)", seconds(total_started)), - )?; - return Err(error); - } - }; + let connection: Bound<'py, PyAny> = match adapter.call_method1( + "connect", + (native_field(preparation, "connection_config")?,), + ) { + Ok(connection) => connection, + Err(error) => { + progress_line( + py, + &format!( + "Query diff connection ERROR ({:.2}s)", + seconds(connection_started) + ), + )?; + progress_line( + py, + &format!("Query diff total ERROR ({:.2}s)", seconds(total_started)), + )?; + return Err(error); + } + }; phases.push(("connection".to_owned(), seconds(connection_started))); progress_line( py, @@ -374,13 +380,13 @@ pub(crate) fn execute_query_diff<'py>( let lifecycle: Bound<'py, PyAny> = host::entry(py, ExecutorHost::QueryDiffArtifactLifecycle)?; let artifact = |side: &str| -> PyResult> { call( - &lifecycle.getattr("build")?, + &native_field(&lifecycle, "build")?, &[ ("adapter", &adapter), - ("run_id", &preparation.getattr("run_id")?), + ("run_id", &native_field(preparation, "run_id")?), ("side", &object(py, side)?), - ("database", &preparation.getattr("database")?), - ("schema", &preparation.getattr("schema")?), + ("database", &native_field(preparation, "database")?), + ("schema", &native_field(preparation, "schema")?), ], ) }; @@ -398,7 +404,7 @@ pub(crate) fn execute_query_diff<'py>( let mut cleanup_errors: Vec = Vec::new(); for item in created.iter().rev() { if let Err(error) = call( - &lifecycle.getattr("cleanup")?, + &native_field(&lifecycle, "cleanup")?, &[ ("adapter", &adapter), ("connection", &connection), @@ -468,7 +474,7 @@ fn compare_artifacts<'py>( (created, phases): (&mut Vec>, &mut Vec<(String, f64)>), ) -> PyResult> { let py: Python<'py> = request.py(); - let adapter: Bound<'py, PyAny> = preparation.getattr("adapter")?; + let adapter: Bound<'py, PyAny> = native_field(preparation, "adapter")?; phases.push(( "reconciliation".to_owned(), reconcile(preparation, connection, lifecycle)?, @@ -479,7 +485,7 @@ fn compare_artifacts<'py>( let started: Instant = Instant::now(); for (artifact, sql) in [(left, "left_sql"), (right, "right_sql")] { if let Err(error) = call( - &lifecycle.getattr("materialize")?, + &native_field(lifecycle, "materialize")?, &[ ("adapter", &adapter), ("connection", connection), @@ -505,7 +511,7 @@ fn compare_artifacts<'py>( py, &format!( "Query diff materialization OK ({}; expires {}; {materialization:.2}s)", - preparation.getattr("schema")?.str()?, + native_field(preparation, "schema")?.str()?, expires_at.call_method0("isoformat")?.str()? ), )?; @@ -513,17 +519,17 @@ fn compare_artifacts<'py>( let started: Instant = Instant::now(); let name: String = format!( "{} vs {}", - preparation.getattr("left_label")?.str()?, - preparation.getattr("right_label")?.str()? + native_field(preparation, "left_label")?.str()?, + native_field(preparation, "right_label")?.str()? ); let pairs: [(&str, Bound<'py, PyAny>); 3] = [ ( "max_column_examples", - preparation.getattr("effective_max_column_examples")?, + native_field(preparation, "effective_max_column_examples")?, ), ( "max_row_only_examples", - preparation.getattr("effective_max_row_only_examples")?, + native_field(preparation, "effective_max_row_only_examples")?, ), ("comparison_name", object(py, name)?), ]; @@ -536,8 +542,8 @@ fn compare_artifacts<'py>( &[ ("adapter", &adapter), ("connection", connection), - ("left_relation", &left.getattr("relation")?), - ("right_relation", &right.getattr("relation")?), + ("left_relation", &native_field(left, "relation")?), + ("right_relation", &native_field(right, "relation")?), ("options", &options), ], ) @@ -573,12 +579,12 @@ fn reconcile( let now: Bound<'_, PyAny> = imported(py, "datetime", "datetime")? .call_method1("now", (imported(py, "datetime", "UTC")?,))?; let cleaned: PyResult> = call( - &lifecycle.getattr("cleanup_expired")?, + &native_field(lifecycle, "cleanup_expired")?, &[ - ("adapter", &preparation.getattr("adapter")?), + ("adapter", &native_field(preparation, "adapter")?), ("connection", connection), - ("database", &preparation.getattr("database")?), - ("schema", &preparation.getattr("schema")?), + ("database", &native_field(preparation, "database")?), + ("schema", &native_field(preparation, "schema")?), ("now", &now), ], ); @@ -601,15 +607,15 @@ fn reconcile( py, &format!( "Query diff reconciliation OK ({} expired; {elapsed:.2}s)", - result.getattr("cleaned")?.len()? + native_field(&result, "cleaned")?.len()? ), )?; - for relation in result.getattr("untracked")?.try_iter()? { + for relation in native_field(&result, "untracked")?.try_iter()? { progress_line( py, &format!( "Query diff reconciliation WARNING reserved-name relation lacks matching ownership evidence: {}", - relation?.getattr("name")?.str()? + native_field(&relation?, "name")?.str()? ), )?; } @@ -651,14 +657,14 @@ fn inspect_inputs<'py>( (left, right): (&Bound<'py, PyAny>, &Bound<'py, PyAny>), ) -> PyResult> { let py: Python<'py> = request.py(); - let adapter: Bound<'py, PyAny> = preparation.getattr("adapter")?; + let adapter: Bound<'py, PyAny> = native_field(preparation, "adapter")?; let location = |artifact: &Bound<'py, PyAny>| -> PyResult> { PyTuple::new( py, [ - artifact.getattr("database")?, - artifact.getattr("schema")?, - artifact.getattr("name")?, + native_field(artifact, "database")?, + native_field(artifact, "schema")?, + native_field(artifact, "name")?, ], ) }; @@ -673,11 +679,11 @@ fn inspect_inputs<'py>( )?; for artifact in [left, right] { let exists: Bound<'py, PyAny> = call( - &lookup.getattr("exists")?, + &native_field(&lookup, "exists")?, &[ - ("database", &artifact.getattr("database")?), - ("schema", &artifact.getattr("schema")?), - ("name", &artifact.getattr("name")?), + ("database", &native_field(artifact, "database")?), + ("schema", &native_field(artifact, "schema")?), + ("name", &native_field(artifact, "name")?), ], )?; if exists.is_truthy()? { @@ -685,7 +691,7 @@ fn inspect_inputs<'py>( py, format!( "query-diff artifact already exists: {}", - artifact.getattr("relation")?.str()? + native_field(artifact, "relation")?.str()? ), "C245", Some("wait for its TTL cleanup or investigate the reserved-name relation"), @@ -693,7 +699,7 @@ fn inspect_inputs<'py>( } } let ttl: Bound<'py, PyAny> = registry::class(py, RecordId::Duration)? - .call_method1("parse", (preparation.getattr("artifact_ttl")?,))?; + .call_method1("parse", (native_field(preparation, "artifact_ttl")?,))?; if ttl.is_none() { return Err(failure( py, @@ -702,10 +708,10 @@ fn inspect_inputs<'py>( None, )); } - let max_columns: Option = request.getattr("max_columns")?.extract()?; + let max_columns: Option = native_field(request, "max_columns")?.extract()?; for (sql, label) in [("left_sql", "left_label"), ("right_sql", "right_label")] { let columns: Vec = call( - &adapter.getattr("query_column_names")?, + &native_field(&adapter, "query_column_names")?, &[ ("connection", connection), ("sql", &preparation.getattr(sql)?), 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 6f861c78b7..088a0352a2 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/diff_report/registration.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/diff_report/registration.rs @@ -20,6 +20,7 @@ use crate::bindings::_helpers::diff_report::facts::{diff_facts, json_of}; use crate::bindings::_helpers::diff_report::tolerances; use crate::bindings::_helpers::frontier::encoder::emit_error; use crate::bindings::_helpers::presentation_text::terminal::stdout_supports_color; +use crate::bindings::_helpers::records::fields::field as native_field; use sqlbuild_cli::diff_report::types::PhaseSeconds; const DEFAULT_MAX_VALUE_LENGTH: usize = 160; @@ -137,10 +138,10 @@ fn render_diff_error_json(py: Python<'_>, options: Option>) -> } fn mode_label(request: &Bound<'_, PyAny>) -> PyResult { - if request.getattr("schema_only")?.is_truthy()? { + if native_field(request, "schema_only")?.is_truthy()? { return Ok("schema-only".to_owned()); } - let bounded: Bound<'_, PyAny> = request.getattr("bounded")?; + let bounded: Bound<'_, PyAny> = native_field(request, "bounded")?; if bounded.is_truthy()? { return Ok(format!("bounded {}", bounded.str()?)); } @@ -160,16 +161,16 @@ pub(crate) fn publish_diff_output( (models, phase_seconds, outcome): (&[DiffModelFacts], Option, Option), ) -> PyResult<()> { let py: Python<'_> = request.py(); - let max_value_length: Bound<'_, PyAny> = request.getattr("max_value_length")?; + let max_value_length: Bound<'_, PyAny> = native_field(request, "max_value_length")?; let examples: DiffExampleOptions = DiffExampleOptions { - max_value_length: if request.getattr("full_example_values")?.is_truthy()? { + max_value_length: if native_field(request, "full_example_values")?.is_truthy()? { None } else if max_value_length.is_truthy()? { Some(max_value_length.extract()?) } else { Some(DEFAULT_MAX_VALUE_LENGTH) }, - suppress_values: request.getattr("suppress_example_values")?.is_truthy()?, + suppress_values: native_field(request, "suppress_example_values")?.is_truthy()?, }; let document: DiffDocumentOptions = DiffDocumentOptions { from_label: from_label.to_owned(), @@ -178,13 +179,12 @@ pub(crate) fn publish_diff_output( outcome, phase_seconds, }; - let path: Bound<'_, PyAny> = request.getattr("json_output_path")?; + let path: Bound<'_, PyAny> = native_field(request, "json_output_path")?; if !path.is_none() { let keywords: Bound<'_, PyDict> = PyDict::new(py); keywords.set_item("parents", true)?; keywords.set_item("exist_ok", true)?; - path.getattr("parent")? - .call_method("mkdir", (), Some(&keywords))?; + native_field(&path, "parent")?.call_method("mkdir", (), Some(&keywords))?; let encoding: Bound<'_, PyDict> = PyDict::new(py); encoding.set_item("encoding", "utf-8")?; path.call_method( @@ -193,10 +193,11 @@ pub(crate) fn publish_diff_output( Some(&encoding), )?; } - if request.getattr("json_output")?.is_truthy()? { + if native_field(request, "json_output")?.is_truthy()? { return write_stdout(py, &json_text(models, &document)?); } - let use_color: bool = !request.getattr("no_color")?.is_truthy()? && stdout_supports_color(py)?; + let use_color: bool = + !native_field(request, "no_color")?.is_truthy()? && stdout_supports_color(py)?; write_stdout( py, &render_diff_text( @@ -205,7 +206,7 @@ pub(crate) fn publish_diff_output( &DiffTextOptions { mode_label: mode_label(request)?, use_color, - verbose: request.getattr("verbose")?.is_truthy()?, + verbose: native_field(request, "verbose")?.is_truthy()?, max_column_examples: usize::try_from(max_column_examples).unwrap_or_default(), max_row_only_examples: usize::try_from(max_row_only_examples).unwrap_or_default(), }, 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 36d8f85712..103090db23 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/diff_report/support.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/diff_report/support.rs @@ -9,6 +9,7 @@ use sqlbuild_cli::diff_report::models::DiffRequestFacts; use crate::bindings::_helpers::compile_driver::python_calls::imported; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::native_host::models::HostId; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::ErrorId; use crate::bindings::_helpers::records::registry; @@ -52,10 +53,10 @@ pub(crate) fn cli_error(py: Python<'_>, class: ErrorId, error: &DiffUserError) - /// The `message`, `code` and `help` of a raised `CliUserError`. pub(crate) fn user_error_parts(error: &Bound<'_, PyAny>) -> PyResult { - let help: Bound<'_, PyAny> = error.getattr("help")?; + let help: Bound<'_, PyAny> = native_field(error, "help")?; Ok(DiffUserError { - message: error.getattr("message")?.extract()?, - code: error.getattr("code")?.str()?.extract()?, + message: native_field(error, "message")?.extract()?, + code: native_field(error, "code")?.str()?.extract()?, help: if help.is_none() { None } else { 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 ddac90a6c2..e84bc01802 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/diff_report/tolerances.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/diff_report/tolerances.rs @@ -11,6 +11,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::call_with; 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::fields::field as native_field; const DECIMAL_MODULE: &str = "decimal"; const ABSOLUTE_KEY: &str = "absolute"; @@ -185,7 +186,7 @@ fn optional_decimal<'py>( }; let py: Python<'py> = raw.py(); let decimal_module: Bound<'py, PyModule> = py.import(DECIMAL_MODULE)?; - let decimal_class: Bound<'py, PyAny> = decimal_module.getattr("Decimal")?; + let decimal_class: Bound<'py, PyAny> = native_field(&decimal_module, "Decimal")?; let numeric: bool = !raw.is_instance_of::() && (raw.is_instance_of::() || raw.is_instance_of::() @@ -196,7 +197,9 @@ fn optional_decimal<'py>( } match decimal_class.call1((raw.str()?,)) { Ok(value) => Ok(Some(value)), - Err(error) if error.is_instance(py, &decimal_module.getattr("InvalidOperation")?) => { + Err(error) + if error.is_instance(py, &native_field(&decimal_module, "InvalidOperation")?) => + { let replacement: PyErr = input_error(py, format!("{label} must be numeric"), "X405"); replacement.set_cause(py, Some(error)); Err(replacement) @@ -273,7 +276,7 @@ fn resolve_row_diff_sampling<'py>( let request = Request::new("resolve_row_diff_sampling", options)?; let py: Python<'py> = request.py(); let override_values: Bound<'py, PyAny> = request.get("override")?; - if override_values.getattr("exhaustive")?.is_truthy()? { + if native_field(&override_values, "exhaustive")?.is_truthy()? { return Ok(None); } let label: String = request.get("label")?.extract()?; @@ -336,7 +339,7 @@ fn merge_row_diff_tolerances<'py>( &[ ("by_type", merged("by_type")?), ("by_column", merged("by_column")?), - ("sampling", base.getattr("sampling")?), + ("sampling", native_field(&base, "sampling")?), ], ) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/discovery/guidance.rs b/crates/sqlbuild-python/src/bindings/_helpers/discovery/guidance.rs index 0cd7da5cd8..7343bc751e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/discovery/guidance.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/discovery/guidance.rs @@ -12,6 +12,7 @@ use sqlbuild_config::project::main::sql_analysis_off_guidance::sql_analysis_off_ use sqlbuild_config::project::models::{SettingSite, SettingText}; use crate::bindings::_helpers::compile_driver::python_calls::KeywordArguments; +use crate::bindings::_helpers::records::fields::field as native_field; const SQL_ANALYSIS_KEY: &str = "sql_analysis"; /// The note and help for turning SQL analysis on, or `None` when it is effectively on. @@ -19,14 +20,14 @@ pub(crate) fn sql_analysis_guidance( discovered_inputs: &Bound<'_, PyAny>, purpose: &str, ) -> PyResult> { - let local: Bound<'_, PyAny> = discovered_inputs.getattr("local_config")?; + let local: Bound<'_, PyAny> = native_field(discovered_inputs, "local_config")?; let local_override: bool = local .getattr("setting_overrides")? .contains(SQL_ANALYSIS_KEY)?; let owner: Bound<'_, PyAny> = if local_override { local } else { - discovered_inputs.getattr("project_config")? + native_field(discovered_inputs, "project_config")? }; if owner .getattr("settings")? 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 7ee298031a..db3314521e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/discovery/hook_entries.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/discovery/hook_entries.rs @@ -4,6 +4,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; @@ -55,10 +56,10 @@ pub(crate) fn hook_entries<'py>( hooks.append(sql_hook(&entry, sql_fields)?)?; } else if entry.is_instance(&python_class)? { let hook: Bound<'py, PyDict> = PyDict::new(py); - let name: Bound<'py, PyAny> = entry.getattr("name")?; + let name: Bound<'py, PyAny> = native_field(&entry, "name")?; hook.set_item("type", "python")?; hook.set_item("name", &name)?; - hook.set_item("kwargs", entry.getattr("kwargs")?)?; + hook.set_item("kwargs", native_field(&entry, "kwargs")?)?; if let Some(fields) = python_hook_fields && let Some(extra) = fields.get_item(&name)? { 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 72f5ae26dd..8134adb1b4 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/discovery/imported_modules.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/discovery/imported_modules.rs @@ -9,6 +9,7 @@ use crate::bindings::_helpers::discovery::locations::project_relative_path; 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; use pyo3::IntoPyObjectExt; @@ -88,7 +89,7 @@ impl<'py> ProjectDir<'py> { return Ok(None); } let cached: Bound<'py, PyAny> = call_with( - &scope.getattr("cached_extensions")?, + &native_field(&scope, "cached_extensions")?, &[("project_dir", self.path.clone())], )?; Ok((!cached.is_none()).then_some(cached)) @@ -147,7 +148,7 @@ pub(crate) fn provider_by_name<'py>(providers: &Bound<'py, PyAny>) -> PyResult = PyDict::new(providers.py()); for provider in providers.try_iter()? { let provider: Bound<'py, PyAny> = provider?; - by_name.set_item(provider.getattr("name")?, provider)?; + by_name.set_item(native_field(&provider, "name")?, provider)?; } Ok(by_name) } 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 4f4232d00a..16b9f904bb 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/discovery/imported_providers.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/discovery/imported_providers.rs @@ -13,6 +13,7 @@ use crate::bindings::_helpers::discovery::imported_modules::{ }; 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::registry; @@ -48,7 +49,7 @@ fn provider_name<'py>( relative_path: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = provider_class.py(); - let explicit: Bound<'py, PyAny> = provider_class.getattr("provider_name")?; + let explicit: Bound<'py, PyAny> = native_field(provider_class, "provider_name")?; if !explicit.is_none() { reject_invalid_identity(&explicit, relative_path)?; } @@ -142,7 +143,7 @@ pub(crate) fn discover_provider_classes<'py>( if let Some(cached) = project.cached_extensions()? { let providers: Bound<'py, PyAny> = cached.get_item(0)?; for provider in providers.try_iter()? { - record_read.call1((provider?.getattr("provider_class")?,))?; + record_read.call1((native_field(&provider?, "provider_class")?,))?; } return Ok(providers); } @@ -236,7 +237,7 @@ pub(crate) fn discover_event_exporter_declarations<'py>( if read.is_none() { continue; } - let name: String = read.getattr("name")?.extract()?; + let name: String = native_field(&read, "name")?.extract()?; if let Some((_, existing)) = seen.iter().find(|(listed, _)| *listed == name) { return Err(discovery_error( py, @@ -253,7 +254,7 @@ pub(crate) fn discover_event_exporter_declarations<'py>( &shim(py, UserCodeHost::ValidateEventExporterSignature)?, &[ ("function", function.clone()), - ("exporter_name", read.getattr("name")?), + ("exporter_name", native_field(&read, "name")?), ("relative_path", relative_path.clone()), ], )?; @@ -264,10 +265,10 @@ pub(crate) fn discover_event_exporter_declarations<'py>( &[ ("file_path", file_path.clone()), ("relative_path", relative_path), - ("name", read.getattr("name")?), + ("name", native_field(&read, "name")?), ("function", function.clone()), - ("event_kinds", read.getattr("event_kinds")?), - ("min_severity", read.getattr("min_severity")?), + ("event_kinds", native_field(&read, "event_kinds")?), + ("min_severity", native_field(&read, "min_severity")?), ], )?); } @@ -281,10 +282,10 @@ fn exporter_provider_usages<'py>( (project, providers): (&ProjectDir<'py>, &Bound<'py, PyDict>), ) -> PyResult> { let py: Python<'py> = project.py(); - let function: Bound<'py, PyAny> = declaration.getattr("function")?; - let name: String = declaration.getattr("name")?.extract()?; + let function: Bound<'py, PyAny> = native_field(declaration, "function")?; + let name: String = native_field(declaration, "name")?.extract()?; let relative_path: String = project - .relative(&declaration.getattr("file_path")?)? + .relative(&native_field(declaration, "file_path")?)? .str()? .extract()?; let empty: Bound<'py, PyAny> = imported(py, "inspect", "Parameter")?.getattr("empty")?; @@ -304,7 +305,7 @@ fn exporter_provider_usages<'py>( ), )); }; - let provider_class: Bound<'py, PyAny> = provider.getattr("provider_class")?; + let provider_class: Bound<'py, PyAny> = native_field(&provider, "provider_class")?; if !annotation.is(&empty) && !annotation.is(&provider_class) { return Err(discovery_error( py, 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 9974b14861..5a113f57be 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 @@ -16,6 +16,7 @@ use crate::bindings::_helpers::discovery::imported_providers; use crate::bindings::_helpers::discovery::python_hook_files::discover_python_hook_files; 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; @@ -123,7 +124,7 @@ impl<'py> Buckets<'py> { let mut pairs: Vec<(&str, Bound<'py, PyAny>)> = vec![ ("file_path", file_path.clone()), ("relative_path", project.relative(file_path)?), - ("name", read.getattr("name")?), + ("name", native_field(&read, "name")?), ("function", function.clone()), ]; for field in kind.fields { @@ -147,7 +148,7 @@ impl<'py> Buckets<'py> { let Some(read) = definition(&function, READ_AUDIT_FACTORY)? else { continue; }; - let name: String = read.getattr("name")?.extract()?; + let name: String = native_field(&read, "name")?.extract()?; let relative: Bound<'py, PyAny> = project.relative(&imported_module.file_path)?; let relative_text: String = relative.str()?.extract()?; let kinds: Vec<&str> = node_definition_names(&function)?; @@ -181,7 +182,7 @@ impl<'py> Buckets<'py> { py, RecordId::DiscoveredAuditFactory, &[ - ("name", read.getattr("name")?), + ("name", native_field(&read, "name")?), ("function", function.clone()), ("file_path", imported_module.file_path.clone()), ("relative_path", relative), @@ -206,7 +207,7 @@ impl<'py> Buckets<'py> { let Some(read) = definition(&function, READ_FACTORY)? else { continue; }; - let name: String = read.getattr("name")?.extract()?; + let name: String = native_field(&read, "name")?.extract()?; let result: Bound<'py, PyAny> = call_factory(&function, ("Factory", &name, &location))?; for (index, generated) in factory_functions(&result, (&name, &location))? .into_iter() @@ -356,7 +357,7 @@ fn factory_functions<'py>( || result.is_instance_of::(); let collection: bool = result.is_instance_of::() || result.is_instance_of::() - || result.is_instance(&builtins.getattr("set")?)?; + || result.is_instance(&native_field(&builtins, "set")?)?; if excluded || !collection { return Err(discovery_error( py, @@ -464,7 +465,7 @@ pub(crate) fn discover_hook_functions<'py>( let Some(read) = definition(&function, READ_HOOK)? else { continue; }; - let name: String = read.getattr("name")?.extract()?; + let name: String = native_field(&read, "name")?.extract()?; if let Some((_, existing)) = seen.iter().find(|(listed, _)| *listed == name) { return Err(discovery_error( py, @@ -479,10 +480,10 @@ pub(crate) fn discover_hook_functions<'py>( seen.push((name, file_path.clone())); let arguments: Bound<'py, PyDict> = hook_file.copy()?; for (field, value) in [ - ("name", read.getattr("name")?), + ("name", native_field(&read, "name")?), ("function", function.clone()), - ("description", read.getattr("description")?), - ("reads", read.getattr("reads")?), + ("description", native_field(&read, "description")?), + ("reads", native_field(&read, "reads")?), ("provider_usages", provider_usages(&function, &providers)?), ] { arguments.set_item(field, value)?; @@ -531,7 +532,7 @@ pub(crate) fn discover_materialization_files<'py>( &[ ("file_path", file_path.clone()), ("relative_path", project.relative(&file_path)?), - ("name", file_path.getattr("stem")?), + ("name", native_field(&file_path, "stem")?), ("provider_usages", usages), ], )?); diff --git a/crates/sqlbuild-python/src/bindings/_helpers/discovery/locations.rs b/crates/sqlbuild-python/src/bindings/_helpers/discovery/locations.rs index f0fd7178d0..8fe5e383e3 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/discovery/locations.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/discovery/locations.rs @@ -1,5 +1,6 @@ //! Authored locations Python consumers read: YAML entry lines and project-relative paths. +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult}; use pyo3::types::{PyTuple, PyTupleMethods}; use pyo3::{pyfunction, wrap_pyfunction}; @@ -21,8 +22,8 @@ pub(crate) fn project_relative_path<'py>( path: &Bound<'py, PyAny>, project_dir: &Bound<'py, PyAny>, ) -> PyResult> { - let root_parts: Bound<'py, PyTuple> = project_dir.getattr("parts")?.cast_into()?; - let path_parts: Bound<'py, PyTuple> = path.getattr("parts")?.cast_into()?; + let root_parts: Bound<'py, PyTuple> = native_field(project_dir, "parts")?.cast_into()?; + let path_parts: Bound<'py, PyTuple> = native_field(path, "parts")?.cast_into()?; let same_anchor: bool = path.call_method0("is_absolute")?.is_truthy()? == project_dir.call_method0("is_absolute")?.is_truthy()?; if !same_anchor 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 a95fe3ec57..cfaaca1bb7 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 @@ -232,7 +232,12 @@ impl NativeModelFile { } /// This file with edited contents and query, as `dataclasses.replace` would make it. - fn with_text(&self, py: Python<'_>, contents: Py, query_sql: Py) -> Self { + pub(crate) fn with_text( + &self, + py: Python<'_>, + contents: Py, + query_sql: Py, + ) -> Self { Self { file_path: self.file_path.clone_ref(py), relative_path: self.relative_path.clone_ref(py), 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 c540e99aa9..0bc9b73ea8 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/discovery/project_facts.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/discovery/project_facts.rs @@ -1,5 +1,6 @@ //! The facts cross-file validation reads, taken from Python's `DiscoveredProjectInputs`. +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::FromPyObject; use pyo3::exceptions::{PyTypeError, PyValueError}; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; @@ -41,7 +42,7 @@ pub(crate) fn project_facts( each(inputs, "enum_files", declarations_fact)?; declaration_files.extend(each(inputs, "constant_files", declarations_fact)?); let mut model_schemas: Vec = Vec::new(); - for schema in inputs.getattr("model_schema_files")?.try_iter()? { + for schema in native_field(inputs, "model_schema_files")?.try_iter()? { model_schemas.extend(each(&schema?, "declarations", named)?); } Ok(ProjectFacts { @@ -95,13 +96,13 @@ fn optional_text(owner: &Bound<'_, PyAny>, name: &str) -> PyResult, name: &str) -> PyResult { - owner.getattr(name)?.getattr("stem")?.extract() + native_field(&owner.getattr(name)?, "stem")?.extract() } /// `(resource.name, str(resource.relative_path))`. fn named(resource: &Bound<'_, PyAny>) -> PyResult { Ok(NamedPath { - name: resource.getattr("name")?.extract()?, + name: native_field(resource, "name")?.extract()?, path: text(resource, "relative_path")?, }) } @@ -123,7 +124,7 @@ fn relative_stem_named(file: &Bound<'_, PyAny>) -> PyResult { } fn enum_value(owner: &Bound<'_, PyAny>, name: &str) -> PyResult { - owner.getattr(name)?.getattr("value")?.extract() + native_field(&owner.getattr(name)?, "value")?.extract() } fn model_fact(model: &Bound<'_, PyAny>) -> PyResult { @@ -143,17 +144,17 @@ fn seed_file_fact(seed: &Bound<'_, PyAny>) -> PyResult { } pub(crate) fn column_name(column: &Bound<'_, PyAny>) -> PyResult { - column.getattr("name")?.extract() + native_field(column, "name")?.extract() } fn seed_entry_fact(py: Python<'_>, entry: &Bound<'_, PyAny>) -> PyResult { - let settings: Bound<'_, PyAny> = entry.getattr("csv_settings")?; + let settings: Bound<'_, PyAny> = native_field(entry, "csv_settings")?; let mut csv: CsvSettings = CsvSettings { delimiter: optional_text(&settings, "delimiter")?, quotechar: optional_text(&settings, "quotechar")?, escapechar: optional_text(&settings, "escapechar")?, - doublequote: settings.getattr("doublequote")?.extract()?, - skipinitialspace: settings.getattr("skipinitialspace")?.extract()?, + doublequote: native_field(&settings, "doublequote")?.extract()?, + skipinitialspace: native_field(&settings, "skipinitialspace")?.extract()?, dialect_error: None, }; csv.dialect_error = csv_dialect_error(py, &csv)?; @@ -173,17 +174,17 @@ fn schema_fact(py: Python<'_>, schema: &Bound<'_, PyAny>) -> PyResult) -> PyResult { - let integration: Bound<'_, PyAny> = entry.getattr("integration_loader")?; + let integration: Bound<'_, PyAny> = native_field(entry, "integration_loader")?; let integration_loader: Option = if integration.is_none() { None } else { Some(IntegrationLoaderFact { - kind: integration.getattr("kind")?.extract()?, + kind: native_field(&integration, "kind")?.extract()?, }) }; Ok(SourceFact { name: column_name(entry)?, - managed: entry.getattr("managed")?.extract()?, + managed: native_field(entry, "managed")?.extract()?, loader: optional_text(entry, "loader")?, integration_loader, }) 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 82bb91c3cc..f2246ac55a 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/discovery/project_validation.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/discovery/project_validation.rs @@ -10,6 +10,7 @@ use sqlbuild_discovery::project_validation::models::{ProjectFacts, ValidationFai use crate::bindings::_helpers::discovery::project_facts::{NodeRow, project_facts}; use crate::bindings::_helpers::discovery::raised::{failure_error, read_error}; +use crate::bindings::_helpers::records::fields::field as native_field; const CSV_MODULE: &str = "csv"; const BUILTINS_MODULE: &str = "builtins"; @@ -42,7 +43,7 @@ fn validate_discovered_project_inputs( failure, } => match py .import(PATHLIB_MODULE) - .and_then(|module| module.getattr("Path")) + .and_then(|module| native_field(&module, "Path")) .and_then(|path| path.call1((absolute_path,))) { Ok(file_path) => read_error(failure, &file_path), @@ -50,7 +51,7 @@ fn validate_discovered_project_inputs( }, ValidationFailure::Csv(message) => raised( py.import(CSV_MODULE) - .and_then(|module| module.getattr("Error")) + .and_then(|module| native_field(&module, "Error")) .and_then(|class| class.call1((message,))), ), ValidationFailure::CsvDialect(error) => { 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 261782f5b7..0e218d3d75 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 @@ -22,6 +22,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; use crate::bindings::models::NativeProjectTree; @@ -83,13 +84,13 @@ fn keyword_values<'py>( let py: Python<'py> = call.py(); let literal_eval: Bound<'py, PyAny> = py.import(AST_MODULE)?.getattr("literal_eval")?; let values: Bound<'py, PyDict> = PyDict::new(py); - let nodes: Bound<'py, PyAny> = call.getattr("keywords")?; + let nodes: Bound<'py, PyAny> = native_field(call, "keywords")?; for (keyword, node) in function.keywords.iter().zip(nodes.try_iter()?) { let Some(name) = &keyword.name else { let detail: String = format!("{UDF_STATEMENT} must not use **kwargs expansion"); return Err(parse_error(source, &detail, None)); }; - match literal_eval.call1((node?.getattr("value")?,)) { + match literal_eval.call1((native_field(&node?, "value")?,)) { Ok(value) => values.set_item(name, value)?, Err(error) if error.is_instance_of::(py) @@ -138,7 +139,7 @@ fn warehouse_body<'py>( ) -> PyResult> { let py: Python<'py> = module.py(); let ast: Bound<'py, PyModule> = py.import(AST_MODULE)?; - let statements: Bound<'py, PyList> = module.getattr("body")?.downcast_into::()?; + let statements: Bound<'py, PyList> = native_field(module, "body")?.downcast_into::()?; let body: Bound<'py, PyList> = PyList::empty(py); for (index, statement) in statements.iter().enumerate() { if function.removed_statements.contains(&index) { @@ -146,8 +147,9 @@ fn warehouse_body<'py>( } if index == function.statement_index { let decorators: Bound<'py, PyList> = PyList::empty(py); - for (position, decorator) in - statement.getattr("decorator_list")?.try_iter()?.enumerate() + for (position, decorator) in native_field(&statement, "decorator_list")? + .try_iter()? + .enumerate() { if !function.removed_decorators.contains(&position) { decorators.append(decorator?)?; @@ -160,8 +162,8 @@ fn warehouse_body<'py>( let keywords: Bound<'py, PyDict> = PyDict::new(py); keywords.set_item("body", body)?; keywords.set_item("type_ignores", PyList::empty(py))?; - let cleaned: Bound<'py, PyAny> = ast.getattr("Module")?.call((), Some(&keywords))?; - ast.getattr("unparse")? + let cleaned: Bound<'py, PyAny> = native_field(&ast, "Module")?.call((), Some(&keywords))?; + native_field(&ast, "unparse")? .call1((cleaned,))? .call_method0("strip") } @@ -189,13 +191,14 @@ fn parsed_function<'py>( UdfShape::Single(function) => function, }; let statement: Bound<'py, PyAny> = - module.getattr("body")?.get_item(function.statement_index)?; + native_field(&module, "body")?.get_item(function.statement_index)?; let decorator: usize = function .removed_decorators .first() .copied() .ok_or_else(|| compiler_error("a @udf(...) function without its decorator"))?; - let call: Bound<'py, PyAny> = statement.getattr("decorator_list")?.get_item(decorator)?; + let call: Bound<'py, PyAny> = + native_field(&statement, "decorator_list")?.get_item(decorator)?; let values: Bound<'py, PyDict> = keyword_values(source, &function, &call)?; if function.has_positional_arguments { let detail: String = format!("{UDF_STATEMENT} must use keyword arguments only"); diff --git a/crates/sqlbuild-python/src/bindings/_helpers/discovery/raised.rs b/crates/sqlbuild-python/src/bindings/_helpers/discovery/raised.rs index f126bcd051..ab2421b739 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/discovery/raised.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/discovery/raised.rs @@ -10,6 +10,7 @@ use sqlbuild_discovery::models::{DiscoveryFailure, ReadFailure, StageFailure}; use sqlbuild_discovery::tree::models::ProjectTree; use crate::bindings::_helpers::discovery::project_files::path_object; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::ErrorId; use crate::bindings::_helpers::records::registry; @@ -74,7 +75,7 @@ pub(crate) fn read_error(failure: ReadFailure, file_path: &Bound<'_, PyAny>) -> start, end, reason, - } => builtins.getattr("UnicodeDecodeError")?.call1(( + } => native_field(&builtins, "UnicodeDecodeError")?.call1(( DECODE_ENCODING, PyBytes::new(py, &bytes), start, @@ -86,7 +87,7 @@ pub(crate) fn read_error(failure: ReadFailure, file_path: &Bound<'_, PyAny>) -> winerror, message, } => { - let os_error = builtins.getattr("OSError")?; + let os_error = native_field(&builtins, "OSError")?; let path: String = file_path.str()?.to_string(); match (winerror, errno) { (Some(winerror), _) => os_error.call1((py.None(), message, path, winerror)), 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 d18a1db2d6..63f9f7ee77 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/discovery/scope_snapshot.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/discovery/scope_snapshot.rs @@ -24,6 +24,7 @@ use crate::bindings::_helpers::discovery::sql_test_records::{ }; use crate::bindings::_helpers::discovery::yaml_records::discover_source_files; +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; @@ -49,7 +50,7 @@ impl<'py> Faults<'py> { /// A fresh fault list and its `append`, the `on_fault` a discoverer reports to. fn collector(&self) -> PyResult<(Bound<'py, PyList>, Bound<'py, PyAny>)> { let faults: Bound<'py, PyList> = PyList::empty(self.py()); - let append: Bound<'py, PyAny> = faults.getattr("append")?; + let append: Bound<'py, PyAny> = native_field(&faults, "append")?; Ok((faults, append)) } @@ -113,7 +114,7 @@ fn configs<'py>( (&list, Some(PROJECT_CONFIG_FILENAME)), load_project_config_file(py, project_dir).map(|config| config.into_bound(py)), || { - let name: Bound<'py, PyAny> = project_dir.getattr("name")?; + let name: Bound<'py, PyAny> = native_field(project_dir, "name")?; let name: Bound<'py, PyAny> = if name.is_truthy()? { name } else { @@ -143,7 +144,7 @@ fn category<'py>( list: &Bound<'py, PyList>, discover: impl FnOnce(Bound<'py, PyAny>) -> PyResult>, ) -> PyResult> { - let append: Bound<'py, PyAny> = list.getattr("append")?; + let append: Bound<'py, PyAny> = native_field(list, "append")?; tolerated( faults, (list, None), 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 3e0f807641..bc691b6d4f 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 @@ -27,6 +27,7 @@ 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::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; @@ -112,8 +113,8 @@ fn test_blocks<'py>( .iter() .map(|block| { Ok(( - block.getattr("test_index")?.extract()?, - block.getattr("name")?.extract()?, + native_field(block, "test_index")?.extract()?, + native_field(block, "name")?.extract()?, )) }) .collect::>()?; @@ -189,7 +190,7 @@ fn scenario_file<'py>( ("header_values", header_values.into_any()), ("sql_body", file.sql_body.into_bound_py_any(py)?), ("sql_body_span", file.body_span.into_bound_py_any(py)?), - ("name", file_path.getattr("stem")?), + ("name", native_field(file_path, "stem")?), ( "ownership_root", ownership_root(py, SQL_SCENARIOS_OWNERSHIP_ROOT)?, 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 1158baef24..80863e67d3 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/event_exporting/dispatcher.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/event_exporting/dispatcher.rs @@ -44,6 +44,7 @@ use crate::bindings::_helpers::native_threads::constants::OUTPUT_SUPPRESSED_ATTR use crate::bindings::_helpers::native_threads::exit_signal::{ interpreter_exiting, signal_interpreter_exit, }; +use crate::bindings::_helpers::records::fields::field as native_field; /// One bound lifecycle-event sink. pub(crate) struct Exporter { @@ -180,7 +181,7 @@ fn retire_output_lane(py: Python<'_>, retired: Option>) -> PyResult<() fn join(thread: &Bound<'_, PyAny>, seconds: f64) -> PyResult<()> { if thread.call_method0("is_alive")?.is_truthy()? { call_with( - &thread.getattr("join")?, + &native_field(thread, "join")?, &[("timeout", PyFloat::new(thread.py(), seconds).into_any())], )?; } @@ -211,12 +212,12 @@ fn read_exporters( .try_iter()? .map(|kind| kind.and_then(|kind| kind.extract::())) .collect::>()?, - min_severity: severity_from(&exporter.getattr("min_severity")?)?, + min_severity: severity_from(&native_field(&exporter, "min_severity")?)?, }); bound.push(Arc::new(Exporter { - name: exporter.getattr("name")?.extract()?, - function: exporter.getattr("function")?.unbind(), - provider_arguments: exporter.getattr("provider_arguments")?.unbind(), + name: native_field(&exporter, "name")?.extract()?, + function: native_field(&exporter, "function")?.unbind(), + provider_arguments: native_field(&exporter, "provider_arguments")?.unbind(), })); } Ok((bound, filters)) @@ -227,8 +228,8 @@ fn read_output_sinks(sinks: &Bound<'_, PyAny>) -> PyResult> { for sink in sinks.try_iter()? { let sink: Bound<'_, PyAny> = sink?; bound.push(OutputSink { - function: sink.getattr("function")?.unbind(), - provider_arguments: sink.getattr("provider_arguments")?.unbind(), + function: native_field(&sink, "function")?.unbind(), + provider_arguments: native_field(&sink, "provider_arguments")?.unbind(), streams: sink .getattr("streams")? .try_iter()? @@ -548,7 +549,7 @@ impl Shared { ),), )?; let empty: Bound<'_, PyAny> = imported(py, "queue", "Empty")?; - let get: Bound<'_, PyAny> = outcome.getattr("get")?; + let get: Bound<'_, PyAny> = native_field(&outcome, "get")?; loop { match call_with( &get, @@ -724,7 +725,7 @@ impl EventExporterDispatcher { /// Filter and enqueue eligible exporter attempts without waiting. fn enqueue(&self, py: Python<'_>, event: &Bound<'_, PyAny>) -> PyResult<()> { - let event_type: String = event.getattr("event_type")?.extract()?; + let event_type: String = native_field(event, "event_type")?.extract()?; let policy: ExportPolicy = export_policy(py, &event_type)?; let shared = &self.shared; let _held = shared.hold(py)?; @@ -821,7 +822,7 @@ impl EventExporterDispatcher { .getattr("record_type")? .eq(COMMAND_OUTPUT_RECORD_TYPE)?; let stream: Option = if is_output { - Some(record.getattr("stream")?.extract()?) + Some(native_field(&record, "stream")?.extract()?) } else { None }; @@ -878,7 +879,7 @@ impl EventExporterDispatcher { } let thread: &Bound<'_, PyAny> = self.thread.bind(py); call_with( - &thread.getattr("join")?, + &native_field(thread, "join")?, &[( "timeout", PyFloat::new(py, remaining_seconds(deadline) + JOIN_GRACE_SECONDS).into_any(), 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 adcf228f24..9179f5318a 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/event_exporting/scope.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/event_exporting/scope.rs @@ -18,6 +18,7 @@ use crate::bindings::_helpers::event_exporting::models::BoundEventExporter; 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; static CURRENT_SCOPE: PyOnceLock> = PyOnceLock::new(); @@ -66,11 +67,11 @@ fn provider_arguments<'py>( declaration: &Bound<'py, PyAny>, ) -> PyResult> { let arguments: Bound<'py, PyDict> = PyDict::new(session.py()); - for usage in declaration.getattr("provider_usages")?.try_iter()? { + for usage in native_field(declaration, "provider_usages")?.try_iter()? { let usage: Bound<'py, PyAny> = usage?; arguments.set_item( - usage.getattr("parameter_name")?, - session.call_method1("get", (usage.getattr("provider_name")?,))?, + native_field(&usage, "parameter_name")?, + session.call_method1("get", (native_field(&usage, "provider_name")?,))?, )?; } Ok(arguments) @@ -90,14 +91,14 @@ fn bindings<'py>( Bound::new( py, BoundEventExporter::create( - exporter.getattr("name")?.extract()?, + native_field(&exporter, "name")?.extract()?, ( - exporter.getattr("function")?.unbind(), + native_field(&exporter, "function")?.unbind(), arguments.into_any().unbind(), ), ( - Some(&exporter.getattr("event_kinds")?), - Some(&exporter.getattr("min_severity")?), + Some(&native_field(&exporter, "event_kinds")?), + Some(&native_field(&exporter, "min_severity")?), ), )?, )? @@ -112,10 +113,10 @@ fn bindings<'py>( sinks.push(call_with( &sink_class, &[ - ("name", sink.getattr("name")?), - ("function", sink.getattr("function")?), + ("name", native_field(&sink, "name")?), + ("function", native_field(&sink, "function")?), ("provider_arguments", arguments.into_any()), - ("streams", sink.getattr("streams")?), + ("streams", native_field(&sink, "streams")?), ], )?); } @@ -175,7 +176,7 @@ impl EventExporterCommandScope { Err(error) => { session.call_method0("release")?; call_with( - &session.getattr("close")?, + &native_field(&session, "close")?, &[("force", PyBool::new(py, true).to_owned().into_any())], )?; return Err(error); @@ -264,9 +265,9 @@ impl EventExporterCommandScope { py, None, ( - discovered_inputs.getattr("providers")?, - discovered_inputs.getattr("event_exporters")?, - discovered_inputs.getattr("command_output_sinks")?, + native_field(discovered_inputs, "providers")?, + native_field(discovered_inputs, "event_exporters")?, + native_field(discovered_inputs, "command_output_sinks")?, ), ) } @@ -372,7 +373,7 @@ impl ProviderSessionCloser { let session: &Bound<'_, PyAny> = self.session.bind(py); let released = session.call_method0("release"); call_with( - &session.getattr("close")?, + &native_field(session, "close")?, &[("force", PyBool::new(py, true).to_owned().into_any())], )?; released?; @@ -449,7 +450,7 @@ fn cached_event_exporter_extensions<'py>( return Ok(scope); } call_with( - &scope.getattr("cached_extensions")?, + &native_field(&scope, "cached_extensions")?, &[("project_dir", project_dir.clone())], ) } 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 9dcb146d8e..f56f2678ba 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/execution_history/conversion.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/execution_history/conversion.rs @@ -16,6 +16,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::{call_with, importe use crate::bindings::_helpers::cursor::values::{DATETIME_MODULE, local_date_time}; 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; const UTC_SUFFIX: &str = "Z"; const UTC_OFFSET: &str = "+00:00"; @@ -125,7 +126,7 @@ pub(crate) fn now(py: Python<'_>) -> PyResult { /// Whether a `datetime`'s `tzinfo` is exactly `UTC`. pub(crate) fn utc_zone(value: &Bound<'_, PyAny>) -> PyResult { - Ok(value.getattr("tzinfo")?.is(&utc(value.py())?)) + Ok(native_field(value, "tzinfo")?.is(&utc(value.py())?)) } /// `datetime.fromisoformat(text.replace("Z", "+00:00"))`, or `None` on `ValueError`. @@ -157,25 +158,25 @@ fn texts(value: &Bound<'_, PyAny>) -> PyResult> { /// An `EventFilter`'s fields. pub(crate) fn event_filter_spec(value: &Bound<'_, PyAny>) -> PyResult { - let family: Bound<'_, PyAny> = value.getattr("family")?; + let family: Bound<'_, PyAny> = native_field(value, "family")?; Ok(EventFilterSpec { - invocation_id: optional_text(&value.getattr("invocation_id")?)?, - run_id: optional_text(&value.getattr("run_id")?)?, - producer: optional_text(&value.getattr("producer")?)?, - event_types: texts(&value.getattr("event_types")?)?, + invocation_id: optional_text(&native_field(value, "invocation_id")?)?, + run_id: optional_text(&native_field(value, "run_id")?)?, + producer: optional_text(&native_field(value, "producer")?)?, + event_types: texts(&native_field(value, "event_types")?)?, family: if family.is_none() { None } else { - Some(family.getattr("value")?.extract()?) + Some(native_field(&family, "value")?.extract()?) }, - occurred_at_start: filter_time(&value.getattr("occurred_at_start")?)?, - occurred_at_end: filter_time(&value.getattr("occurred_at_end")?)?, + occurred_at_start: filter_time(&native_field(value, "occurred_at_start")?)?, + occurred_at_end: filter_time(&native_field(value, "occurred_at_end")?)?, }) } /// A `RunStatus` member's status. fn run_status(value: &Bound<'_, PyAny>) -> PyResult { - let text: String = value.getattr("value")?.extract()?; + let text: String = native_field(value, "value")?.extract()?; parsed_status(value.py(), &text) } @@ -199,10 +200,10 @@ pub(crate) fn run_filter_spec(value: &Bound<'_, PyAny>) -> PyResult>()?; Ok(RunFilterSpec { - invocation_id: optional_text(&value.getattr("invocation_id")?)?, + invocation_id: optional_text(&native_field(value, "invocation_id")?)?, statuses, - created_at_start: filter_time(&value.getattr("created_at_start")?)?, - created_at_end: filter_time(&value.getattr("created_at_end")?)?, + created_at_start: filter_time(&native_field(value, "created_at_start")?)?, + created_at_end: filter_time(&native_field(value, "created_at_end")?)?, }) } @@ -214,13 +215,13 @@ pub(crate) fn known_event(event: &Bound<'_, PyAny>) -> PyResult { /// The `event_id` a known event carries or an opaque envelope holds as a string. pub(crate) fn event_id_of(event: &Bound<'_, PyAny>) -> PyResult> { if known_event(event)? { - return text_of(&event.getattr("event_id")?); + return text_of(&native_field(event, "event_id")?); } text_of(&raw_field(event, "event_id")?) } fn raw_field<'py>(event: &Bound<'py, PyAny>, name: &str) -> PyResult> { - event.getattr("raw")?.call_method1("get", (name,)) + native_field(event, "raw")?.call_method1("get", (name,)) } /// `lifecycle_event_to_json(event)`. @@ -257,11 +258,11 @@ fn opaque_occurred_at(event: &Bound<'_, PyAny>) -> PyResult> { pub(crate) fn envelope_of(event: &Bound<'_, PyAny>) -> PyResult { let mut envelope = EventEnvelope::default(); if known_event(event)? { - envelope.schema_version = event.getattr("schema_version")?.extract()?; + 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)?)?; } - envelope.occurred_at = Some(utc_time(&event.getattr("occurred_at")?)?); + envelope.occurred_at = Some(utc_time(&native_field(event, "occurred_at")?)?); return Ok(envelope); } envelope.schema_version = @@ -275,23 +276,23 @@ pub(crate) fn envelope_of(event: &Bound<'_, PyAny>) -> PyResult { /// A `StoredEvent` as projection reads it. pub(crate) fn projection_fact(stored: &Bound<'_, PyAny>) -> PyResult { - let event: Bound<'_, PyAny> = stored.getattr("event")?; + let event: Bound<'_, PyAny> = native_field(stored, "event")?; let mut run: Option = None; if known_event(&event)? { - let run_id: Bound<'_, PyAny> = event.getattr("run_id")?; + let run_id: Bound<'_, PyAny> = native_field(&event, "run_id")?; if !run_id.is_none() { run = Some(RunFact { run_id: run_id.extract()?, - invocation_id: event.getattr("invocation_id")?.extract()?, - event_type: event.getattr("event_type")?.extract()?, - occurred_at: utc_time(&event.getattr("occurred_at")?)?, + invocation_id: native_field(&event, "invocation_id")?.extract()?, + event_type: native_field(&event, "event_type")?.extract()?, + occurred_at: utc_time(&native_field(&event, "occurred_at")?)?, }); } } Ok(ProjectionFact { - storage_order: stored.getattr("storage_order")?.extract()?, - cursor: stored.getattr("cursor")?.extract()?, - received_at: utc_time(&stored.getattr("received_at")?)?, + storage_order: native_field(stored, "storage_order")?.extract()?, + cursor: native_field(stored, "cursor")?.extract()?, + received_at: utc_time(&native_field(stored, "received_at")?)?, run, }) } @@ -306,16 +307,16 @@ pub(crate) fn run_row(record: &Bound<'_, PyAny>) -> PyResult { utc_time(&value).map(Some) }; Ok(RunRow { - run_id: record.getattr("run_id")?.extract()?, - invocation_id: record.getattr("invocation_id")?.extract()?, - created_at: utc_time(&record.getattr("created_at")?)?, - status: run_status(&record.getattr("status")?)?, - is_complete: record.getattr("is_complete")?.is_truthy()?, - last_event_cursor: record.getattr("last_event_cursor")?.extract()?, - last_storage_order: record.getattr("last_storage_order")?.extract()?, - command: optional_text(&record.getattr("command")?)?, - target: optional_text(&record.getattr("target")?)?, - environment: optional_text(&record.getattr("environment")?)?, + run_id: native_field(record, "run_id")?.extract()?, + invocation_id: native_field(record, "invocation_id")?.extract()?, + created_at: utc_time(&native_field(record, "created_at")?)?, + status: run_status(&native_field(record, "status")?)?, + is_complete: native_field(record, "is_complete")?.is_truthy()?, + last_event_cursor: native_field(record, "last_event_cursor")?.extract()?, + last_storage_order: native_field(record, "last_storage_order")?.extract()?, + command: optional_text(&native_field(record, "command")?)?, + target: optional_text(&native_field(record, "target")?)?, + environment: optional_text(&native_field(record, "environment")?)?, started_at: optional_time("started_at")?, ended_at: optional_time("ended_at")?, }) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/execution_history/functions.rs b/crates/sqlbuild-python/src/bindings/_helpers/execution_history/functions.rs index 8f3bb313bd..6c63aece8d 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/execution_history/functions.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/execution_history/functions.rs @@ -17,6 +17,7 @@ use crate::bindings::_helpers::execution_history::conversion::{ checked, event_filter_spec, exact_int, projection_facts, run_filter_spec, run_records, run_rows, text_of, utc_zone, }; +use crate::bindings::_helpers::records::fields::field as native_field; /// Return the required stable ID from a known or opaque lifecycle event. #[pyfunction] @@ -69,9 +70,9 @@ fn append_and_project<'py>( #[pyfunction] fn validate_stored_history_event(record: &Bound<'_, PyAny>) -> Result<(), PyErr> { let py: Python<'_> = record.py(); - let storage_order: i64 = record.getattr("storage_order")?.extract()?; - let cursor: String = text_of(&record.getattr("cursor")?)?.unwrap_or_default(); - let received_at_is_utc: bool = utc_zone(&record.getattr("received_at")?)?; + let storage_order: i64 = native_field(record, "storage_order")?.extract()?; + let cursor: String = text_of(&native_field(record, "cursor")?)?.unwrap_or_default(); + let received_at_is_utc: bool = utc_zone(&native_field(record, "received_at")?)?; checked( py, validate_stored_event(storage_order, &cursor, received_at_is_utc), @@ -101,12 +102,12 @@ fn validate_history_run_filter(run_filter: &Bound<'_, PyAny>) -> Result<(), PyEr #[pyo3(signature = (*, page, is_event_page))] fn validate_history_page(page: &Bound<'_, PyAny>, is_event_page: bool) -> Result<(), PyErr> { let py: Python<'_> = page.py(); - let records: Bound<'_, PyAny> = page.getattr("records")?; + let records: Bound<'_, PyAny> = native_field(page, "records")?; let has_records: bool = records.is_truthy()?; - let next_cursor: Bound<'_, PyAny> = page.getattr("next_cursor")?; + let next_cursor: Bound<'_, PyAny> = native_field(page, "next_cursor")?; let next_text: Option = text_of(&next_cursor)?; let last_text: Option = if is_event_page && has_records { - text_of(&records.get_item(-1)?.getattr("cursor")?)? + text_of(&native_field(&records.get_item(-1)?, "cursor")?)? } else { None }; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/execution_history/postgres.rs b/crates/sqlbuild-python/src/bindings/_helpers/execution_history/postgres.rs index 7482d7d61b..b32426de73 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/execution_history/postgres.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/execution_history/postgres.rs @@ -51,6 +51,7 @@ use crate::bindings::_helpers::execution_history::statements::{ }; 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; const PSYCOPG_MODULE: &str = "psycopg"; const PSYCOPG_ROWS_MODULE: &str = "psycopg.rows"; @@ -167,7 +168,7 @@ impl PostgresExecutionHistory { failure_message: &str, ) -> PyResult { let py: Python<'py> = slf.py(); - let connection: Bound<'py, PyAny> = slf.getattr("_connection")?; + let connection: Bound<'py, PyAny> = native_field(slf, "_connection")?; let timeout: String = self .statement_timeout_ms .get() @@ -198,7 +199,7 @@ impl PostgresExecutionHistory { if history_error_of_kind(py, &error, STORAGE_ERROR)? || !driver_failure(py, &error)? { return Err(error); } - let sqlstate: Option = text_of(&error.value(py).getattr("sqlstate")?)?; + let sqlstate: Option = text_of(&native_field(error.value(py), "sqlstate")?)?; if retries_transaction(sqlstate.as_deref(), attempts, max_retries) { attempts += 1; continue; @@ -210,7 +211,7 @@ impl PostgresExecutionHistory { /// `query` through a fresh cursor, with driver failures reported as read failures. fn read_rows<'py>(slf: &Bound<'py, Self>, query: &Query) -> PyResult>> { let py: Python<'py> = slf.py(); - let connection: Bound<'py, PyAny> = slf.getattr("_connection")?; + let connection: Bound<'py, PyAny> = native_field(slf, "_connection")?; let rows = within(&connection.call_method0("cursor")?, |cursor| { execute_query(cursor, query)?; fetch_all(cursor) @@ -229,7 +230,7 @@ impl PostgresExecutionHistory { if let Some(cursor) = cursor { return body(cursor); } - let owned: Bound<'py, PyAny> = slf.getattr("_connection")?.call_method0("cursor")?; + let owned: Bound<'py, PyAny> = native_field(slf, "_connection")?.call_method0("cursor")?; let result: PyResult = body(&owned); let closed = owned.call_method0("close"); let value: T = result?; @@ -346,7 +347,7 @@ impl PostgresExecutionHistory { events: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = slf.py(); - slf.getattr("_append_events")?.call( + native_field(slf, "_append_events")?.call( (), Some(&keywords( py, @@ -361,7 +362,7 @@ impl PostgresExecutionHistory { projected: &Bound<'py, PyAny>, ) -> PyResult<()> { let py: Python<'py> = slf.py(); - slf.getattr("_replace_projection")?.call( + native_field(slf, "_replace_projection")?.call( (), Some(&keywords( py, @@ -528,7 +529,7 @@ impl PostgresExecutionHistory { this.closed.store(false, Ordering::SeqCst); let mut connection: Option> = None; let opened: PyResult<()> = (|| { - let opened: Bound<'_, PyAny> = psycopg.getattr("connect")?.call( + let opened: Bound<'_, PyAny> = native_field(&psycopg, "connect")?.call( (dsn,), Some(&keywords( py, @@ -665,7 +666,7 @@ impl PostgresExecutionHistory { let page: Vec> = rows.into_iter().take(limit as usize).collect(); let records: Vec> = Self::stored_events(slf, &page)?; let next_cursor: Bound<'py, PyAny> = match records.last() { - Some(record) => record.getattr("cursor")?, + Some(record) => native_field(record, "cursor")?, None => py.None().into_bound(py), }; call_with( @@ -850,7 +851,7 @@ impl PostgresExecutionHistory { let py: Python<'_> = slf.py(); slf.get().ensure_open(py)?; let healthy = (|| -> PyResult { - let connection: Bound<'_, PyAny> = slf.getattr("_connection")?; + let connection: Bound<'_, PyAny> = native_field(slf, "_connection")?; let row = within(&connection.call_method0("cursor")?, |cursor| { execute_plain(cursor, POSTGRES_PING)?; fetch_one(cursor) @@ -886,7 +887,7 @@ impl PostgresExecutionHistory { if this.closed.load(Ordering::SeqCst) { return Ok(()); } - slf.getattr("_connection")?.call_method0("close")?; + native_field(slf, "_connection")?.call_method0("close")?; this.closed.store(true, Ordering::SeqCst); Ok(()) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/execution_history/sqlite.rs b/crates/sqlbuild-python/src/bindings/_helpers/execution_history/sqlite.rs index 503d21f626..3eb9cdf6ee 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/execution_history/sqlite.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/execution_history/sqlite.rs @@ -52,6 +52,7 @@ use crate::bindings::_helpers::execution_history::statements::{ }; 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; const SQLITE_MODULE: &str = "sqlite3"; const PATHLIB_MODULE: &str = "pathlib"; @@ -223,7 +224,7 @@ impl SqliteExecutionHistory { let py: Python<'py> = slf.py(); self.with_lock(py, || { self.ensure_open(py)?; - let connection: Bound<'py, PyAny> = slf.getattr("_connection")?; + let connection: Bound<'py, PyAny> = native_field(slf, "_connection")?; execute_plain(&connection, SQLITE_BEGIN)?; let value: T = match body(&connection) { Ok(value) => value, @@ -234,7 +235,7 @@ impl SqliteExecutionHistory { }; if let Err(error) = execute_plain(&connection, SQLITE_COMMIT) { if sqlite_failure(py, &error)? - && connection.getattr("in_transaction")?.is_truthy()? + && native_field(&connection, "in_transaction")?.is_truthy()? { let _ = execute_plain(&connection, SQLITE_ROLLBACK); } @@ -348,7 +349,7 @@ impl SqliteExecutionHistory { None => { let query = event_insert_query(Dialect::Sqlite, event, &now(py)?); let cursor: Bound<'py, PyAny> = execute_query(connection, &query)?; - let storage_id: Bound<'py, PyAny> = cursor.getattr("lastrowid")?; + let storage_id: Bound<'py, PyAny> = native_field(&cursor, "lastrowid")?; connection .call_method1("execute", (SQLITE_EVENT_BY_STORAGE_ID, (storage_id,)))? .call_method0("fetchone")? @@ -373,7 +374,7 @@ impl SqliteExecutionHistory { fn migrate_in(&self, slf: &Bound<'_, Self>) -> PyResult<()> { let py: Python<'_> = slf.py(); - let connection: Bound<'_, PyAny> = slf.getattr("_connection")?; + let connection: Bound<'_, PyAny> = native_field(slf, "_connection")?; let current: i64 = Self::metadata_version(&connection)?; if !checked(py, schema_migration_needed(Dialect::Sqlite, current))? { return Ok(()); @@ -495,7 +496,7 @@ impl SqliteExecutionHistory { let opened: PyResult<()> = (|| { prepare_history_directory(&database)?; let sqlite = py.import(SQLITE_MODULE)?; - let opened: Bound<'_, PyAny> = sqlite.getattr("connect")?.call( + let opened: Bound<'_, PyAny> = native_field(&sqlite, "connect")?.call( (database.as_str(),), Some( &crate::bindings::_helpers::compile_driver::python_calls::keywords( @@ -515,7 +516,7 @@ impl SqliteExecutionHistory { connection = Some(opened.clone()); slf.setattr("_connection", &opened)?; this.with_lock(py, || { - opened.setattr("row_factory", sqlite.getattr("Row")?)?; + opened.setattr("row_factory", native_field(&sqlite, "Row")?)?; execute_plain(&opened, &format!("PRAGMA busy_timeout = {busy_timeout_ms}"))?; execute_plain(&opened, SQLITE_FOREIGN_KEYS)?; if database != MEMORY_PATH { @@ -613,7 +614,7 @@ impl SqliteExecutionHistory { let limit: Option = limit.map_or(Some(DEFAULT_PAGE_LIMIT), exact_int); checked(py, validate_page_limit(limit))?; let limit: i64 = limit.unwrap_or(DEFAULT_PAGE_LIMIT); - let connection: Bound<'py, PyAny> = slf.getattr("_connection")?; + let connection: Bound<'py, PyAny> = native_field(slf, "_connection")?; let after: i64 = Self::event_position(&connection, after_cursor)?; let spec = event_filter_spec(event_filter)?; let query = event_page_query(Dialect::Sqlite, after, &spec, limit); @@ -632,7 +633,7 @@ impl SqliteExecutionHistory { } } let next_cursor: Bound<'py, PyAny> = match records.last() { - Some(record) => record.getattr("cursor")?, + Some(record) => native_field(record, "cursor")?, None => py.None().into_bound(py), }; crate::bindings::_helpers::compile_driver::python_calls::call_with( @@ -655,7 +656,7 @@ impl SqliteExecutionHistory { let this: &Self = slf.get(); this.with_lock(py, || { this.ensure_open(py)?; - let connection: Bound<'py, PyAny> = slf.getattr("_connection")?; + let connection: Bound<'py, PyAny> = native_field(slf, "_connection")?; let row = fetch_one(&connection.call_method1("execute", (SQLITE_RUN_BY_ID, (run_id,)))?)?; match row { @@ -683,7 +684,7 @@ impl SqliteExecutionHistory { let limit: Option = limit.map_or(Some(DEFAULT_PAGE_LIMIT), exact_int); checked(py, validate_page_limit(limit))?; let limit: i64 = limit.unwrap_or(DEFAULT_PAGE_LIMIT); - let connection: Bound<'py, PyAny> = slf.getattr("_connection")?; + let connection: Bound<'py, PyAny> = native_field(slf, "_connection")?; let none = py.None().into_bound(py); let after = Self::run_cursor_key(&connection, after_cursor.unwrap_or(&none))?; let spec = run_filter_spec(run_filter)?; @@ -727,7 +728,7 @@ impl SqliteExecutionHistory { this.with_lock(py, || { this.ensure_open(py)?; this.project_calls.fetch_add(1, Ordering::SeqCst); - let connection: Bound<'py, PyAny> = slf.getattr("_connection")?; + let connection: Bound<'py, PyAny> = native_field(slf, "_connection")?; let current: Vec = Self::read_all_runs(&connection)?; let facts = projection_facts(stored_events)?; let projected: Vec = checked(py, project_runs(facts, current))?; @@ -805,7 +806,7 @@ impl SqliteExecutionHistory { let this: &Self = slf.get(); this.with_lock(py, || { this.ensure_open(py)?; - let connection: Bound<'_, PyAny> = slf.getattr("_connection")?; + let connection: Bound<'_, PyAny> = native_field(slf, "_connection")?; let Some(result) = fetch_one(&execute_plain(&connection, SQLITE_QUICK_CHECK)?)? else { return Ok(false); }; @@ -823,7 +824,7 @@ impl SqliteExecutionHistory { let this: &Self = slf.get(); this.with_lock(py, || { this.ensure_open(py)?; - Self::metadata_version(&slf.getattr("_connection")?) + Self::metadata_version(&native_field(slf, "_connection")?) }) } @@ -882,7 +883,7 @@ impl SqliteExecutionHistory { fn _read_all_events<'py>(slf: &Bound<'py, Self>) -> PyResult> { let py: Python<'py> = slf.py(); - let connection: Bound<'py, PyAny> = slf.getattr("_connection")?; + let connection: Bound<'py, PyAny> = native_field(slf, "_connection")?; let rows: Vec> = execute_plain(&connection, SQLITE_ALL_EVENTS)? .try_iter()? .collect::>()?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/exported_constants/registration.rs b/crates/sqlbuild-python/src/bindings/_helpers/exported_constants/registration.rs index bd788edf76..a718006bcd 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/exported_constants/registration.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/exported_constants/registration.rs @@ -7,6 +7,7 @@ use crate::bindings::_helpers::cursor::conversion::GRAIN_NAMES; use crate::bindings::_helpers::exported_constants::constants::{ EXPORTED_INTEGERS, EXPORTED_TEXT_SETS, EXPORTED_TEXT_TUPLES, EXPORTED_TEXTS, }; +use crate::bindings::_helpers::records::fields::field as native_field; /// Registers after the native enums, since `GRAIN_ORDER` is keyed by `CursorGrain` members. pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> { @@ -23,7 +24,7 @@ pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> { for (name, values) in EXPORTED_TEXT_SETS { module.add(*name, PyFrozenSet::new(py, values.iter())?)?; } - let grain = module.getattr("CursorGrain")?; + let grain = native_field(module, "CursorGrain")?; let order = PyDict::new(py); for (name, rank) in GRAIN_NAMES { order.set_item(grain.call1((name,))?, rank as usize)?; 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 398a2e3074..3987efa485 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; use pyo3::exceptions::{PyUnicodeDecodeError, PyValueError}; @@ -62,11 +63,11 @@ pub(crate) fn select_latest_fingerprints<'py>( node_names: Vec, filtered_node_types: Vec, ) -> PyResult> { - let schema: String = fingerprint_set.getattr("schema")?.extract()?; + let schema: String = native_field(fingerprint_set, "schema")?.extract()?; if node_names.is_empty() && filtered_node_types.is_empty() { return indexed_set(py, &schema, &[]); } - let by_identity: Bound<'py, PyAny> = fingerprint_set.getattr("fingerprints_by_identity")?; + let by_identity: Bound<'py, PyAny> = native_field(fingerprint_set, "fingerprints_by_identity")?; let latest: Vec> = if by_identity.is_truthy()? { by_identity .call_method0("values")? @@ -113,8 +114,8 @@ fn indexed_set<'py>( fn identity_of(fingerprint: &Bound<'_, PyAny>) -> PyResult<(String, String)> { Ok(( - fingerprint.getattr("node_type")?.extract()?, - fingerprint.getattr("node_name")?.extract()?, + native_field(fingerprint, "node_type")?.extract()?, + native_field(fingerprint, "node_name")?.extract()?, )) } 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 10890995c1..f722831ae8 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/fingerprints/storage_io.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/fingerprints/storage_io.rs @@ -22,6 +22,7 @@ use crate::bindings::_helpers::fingerprints::storage_sql::{ 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; @@ -83,10 +84,10 @@ fn create_sql( let py: Python<'_> = render_framework_type.py(); let framework: Bound<'_, PyAny> = host::entry(py, AdapterHost::FrameworkType)?; let string_type: String = render_framework_type - .call1((framework.getattr("STRING")?,))? + .call1((native_field(&framework, "STRING")?,))? .extract()?; let timestamp_type: String = render_framework_type - .call1((framework.getattr("TIMESTAMP")?,))? + .call1((native_field(&framework, "TIMESTAMP")?,))? .extract()?; Ok(create_table_sql( &qualified_table_name(location)?, @@ -369,8 +370,8 @@ fn write_fingerprint<'py>(request: Option<&Bound<'py, PyDict>>) -> PyResult<()> )?); execute_with_retry(&writer, &sqls, &|error| { let mut details: Vec> = vec![ - fingerprint.getattr("node_type")?, - fingerprint.getattr("node_name")?, + native_field(&fingerprint, "node_type")?, + native_field(&fingerprint, "node_name")?, ]; details.extend(error_details(py, error)?); Ok((SINGLE_RETRY_MESSAGE, details)) @@ -385,8 +386,8 @@ fn batch_warning<'py>(batch: &[Bound<'py, PyAny>], error: &PyErr) -> PyResult = first.py(); let mut details: Vec> = vec![ batch.len().into_pyobject(py)?.into_any(), - first.getattr("node_type")?, - first.getattr("node_name")?, + native_field(first, "node_type")?, + native_field(first, "node_name")?, ]; details.extend(error_details(py, error)?); Ok((BATCH_RETRY_MESSAGE, details)) 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 962b281e0b..2ebc2de696 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/fingerprints/storage_sql.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/fingerprints/storage_sql.rs @@ -8,6 +8,7 @@ use sqlbuild_analysis::fingerprint_storage::main::insert_sql::insert_sql; use sqlbuild_analysis::fingerprint_storage::models::StoredFingerprint; use crate::bindings::_helpers::fingerprints::latest_rows::fingerprint_input_error; +use crate::bindings::_helpers::records::fields::field as native_field; /// One `INSERT` of a single fingerprint. pub(crate) fn fingerprint_insert_sql( @@ -46,17 +47,17 @@ pub(crate) fn fingerprint_insert_batches<'py>( fn stored_fingerprint(fingerprint: &Bound<'_, PyAny>) -> PyResult { Ok(StoredFingerprint { - node_type: fingerprint.getattr("node_type")?.extract()?, - node_name: fingerprint.getattr("node_name")?.extract()?, - target_database: fingerprint.getattr("target_database")?.extract()?, - target_schema: fingerprint.getattr("target_schema")?.extract()?, - target_name: fingerprint.getattr("target_name")?.extract()?, + node_type: native_field(fingerprint, "node_type")?.extract()?, + node_name: native_field(fingerprint, "node_name")?.extract()?, + target_database: native_field(fingerprint, "target_database")?.extract()?, + target_schema: native_field(fingerprint, "target_schema")?.extract()?, + target_name: native_field(fingerprint, "target_name")?.extract()?, run_id: formatted(fingerprint, "run_id")?, definition_hash: formatted(fingerprint, "definition_hash")?, version_hash: formatted(fingerprint, "version_hash")?, schema_fingerprint: formatted(fingerprint, "schema_fingerprint")?, - definition: fingerprint.getattr("definition")?.extract()?, - metadata_json: fingerprint.getattr("metadata_json")?.extract()?, + definition: native_field(fingerprint, "definition")?.extract()?, + metadata_json: native_field(fingerprint, "metadata_json")?.extract()?, ts: fingerprint .getattr("ts")? .call_method0("isoformat")? 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 18734a6846..33a3fde9bc 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/format_command/command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/format_command/command.rs @@ -26,6 +26,7 @@ use crate::bindings::_helpers::format_command::run::{FormatOutcome, FormatReques use crate::bindings::_helpers::format_command::selection::{LintInputs, resolve_lint_inputs}; use crate::bindings::_helpers::frontier::encoder::emit_error; 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; @@ -96,9 +97,10 @@ fn resolve_inputs<'py>( selected, discovered, }) => { - resolved.has_fixture_candidates = discovered.getattr("test_files")?.is_truthy()?; + resolved.has_fixture_candidates = + native_field(&discovered, "test_files")?.is_truthy()?; let adapter_dialect: Bound<'py, PyAny> = - adapter.getattr("sql_analysis_dialect_name")?; + native_field(&adapter, "sql_analysis_dialect_name")?; resolved.dialect = if adapter_dialect.is_truthy()? { adapter_dialect.extract()? } else { @@ -131,14 +133,14 @@ fn fixture_inputs<'py>(resolved: &Resolved<'py>) -> PyResult = discovered.getattr("local_config")?; + let local: Bound<'py, PyAny> = native_field(discovered, "local_config")?; let settings_owner: Bound<'py, PyAny> = if local .getattr("setting_overrides")? .contains(SQL_ANALYSIS_SETTING_KEY)? { local } else { - discovered.getattr("project_config")? + native_field(discovered, "project_config")? }; let enabled: bool = settings_owner .getattr("settings")? @@ -194,7 +196,7 @@ fn complete_status(status: &Bound<'_, PyAny>, report: &LintReport, elapsed: f64) status.call_method1("error", (message,))?; } else { call_with( - &status.getattr("complete")?, + &native_field(status, "complete")?, &[("message", PyString::new(status.py(), &message).into_any())], )?; } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/format_command/report.rs b/crates/sqlbuild-python/src/bindings/_helpers/format_command/report.rs index 73d0f2b5b8..037dab8908 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/format_command/report.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/format_command/report.rs @@ -9,6 +9,7 @@ use sqlbuild_core::format_command::models::{LintFinding, LintReport, RuleFixLine use sqlbuild_core::text::main::python_splitlines::python_splitlines; use crate::bindings::_helpers::format_command::run::FormatOutcome; +use crate::bindings::_helpers::records::fields::field as native_field; const FAULT_SEVERITY: &str = "fault"; const APPLIED_STATUS: &str = "applied"; @@ -48,20 +49,20 @@ fn finding( contents: &Bound<'_, PyDict>, root: &Path, ) -> PyResult { - let path: Bound<'_, PyAny> = violation.getattr("file_path")?; - let line: i64 = violation.getattr("line")?.extract()?; - let remediation: Bound<'_, PyAny> = violation.getattr("remediation")?; + let path: Bound<'_, PyAny> = native_field(violation, "file_path")?; + let line: i64 = native_field(violation, "line")?.extract()?; + let remediation: Bound<'_, PyAny> = native_field(violation, "remediation")?; Ok(LintFinding { file: text(&path)?, display_path: display_path(&path.extract::()?, root), line, - column: violation.getattr("column")?.extract()?, - end_line: violation.getattr("end_line")?.extract()?, - end_column: violation.getattr("end_column")?.extract()?, - code: violation.getattr("code")?.extract()?, - message: violation.getattr("message")?.extract()?, - fault: text(&violation.getattr("severity")?)? == FAULT_SEVERITY, - engine: violation.getattr("engine")?.extract()?, + column: native_field(violation, "column")?.extract()?, + end_line: native_field(violation, "end_line")?.extract()?, + end_column: native_field(violation, "end_column")?.extract()?, + code: native_field(violation, "code")?.extract()?, + message: native_field(violation, "message")?.extract()?, + fault: text(&native_field(violation, "severity")?)? == FAULT_SEVERITY, + engine: native_field(violation, "engine")?.extract()?, remediation: if remediation.is_none() { None } else { @@ -72,14 +73,14 @@ fn finding( } fn rule_fix(report: &Bound<'_, PyAny>, declined: &BTreeSet) -> PyResult { - let path: Bound<'_, PyAny> = report.getattr("file_path")?; - let status: String = report.getattr("status")?.extract()?; - let reason: String = report.getattr("reason")?.extract()?; + let path: Bound<'_, PyAny> = native_field(report, "file_path")?; + let status: String = native_field(report, "status")?.extract()?; + let reason: String = native_field(report, "reason")?.extract()?; let refused: bool = status == APPLIED_STATUS && declined.contains(&path.extract::()?); Ok(RuleFixLine { file: text(&path)?, - code: report.getattr("code")?.extract()?, - line: report.getattr("line")?.extract()?, + code: native_field(report, "code")?.extract()?, + line: native_field(report, "line")?.extract()?, status: if refused { REFUSED_STATUS.to_owned() } else { 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 728eb2b891..9677ca951b 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/format_command/run.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/format_command/run.rs @@ -13,6 +13,7 @@ use crate::bindings::_helpers::discovery::description_inputs::externally_describ 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::registry; use crate::bindings::_helpers::rules::dsl_headers::{header_violations, rules_config}; @@ -176,7 +177,7 @@ fn lint_final_contents<'py>( } fn code_is(violation: &Bound<'_, PyAny>, code: &str) -> PyResult { - Ok(violation.getattr("code")?.extract::()? == code) + Ok(native_field(violation, "code")?.extract::()? == code) } /// Drop `description-present` faults for models described outside their header. @@ -250,9 +251,10 @@ pub(crate) fn run_format<'py>(request: &FormatRequest<'_, 'py>) -> PyResult
= request.project_dir.py(); let fixture_inputs: Option<(Bound<'py, PyAny>, Bound<'py, PyAny>)> = match (request.discovered, request.adapter) { - (Some(discovered), Some(adapter)) => { - Some((discovered.clone(), adapter.getattr("sql_lexical_syntax")?)) - } + (Some(discovered), Some(adapter)) => Some(( + discovered.clone(), + native_field(adapter, "sql_lexical_syntax")?, + )), (Some(_), None) => { return Err(registry::error(py, ErrorId::Lint)? .call1(("fixture-null formatting requires the project adapter",)) @@ -283,7 +285,7 @@ pub(crate) fn run_format<'py>(request: &FormatRequest<'_, 'py>) -> PyResult = format_faults .iter() - .map(|fault| fault.getattr("file_path")?.extract()) + .map(|fault| native_field(&fault, "file_path")?.extract()) .collect::>()?; let updated: Bound<'py, PyDict> = PyDict::new(py); for (path, contents) in merged.iter() { @@ -302,7 +304,7 @@ pub(crate) fn run_format<'py>(request: &FormatRequest<'_, 'py>) -> PyResult> = drop_resolved_description_faults(request.project_dir, violations)?; let final_contents: Bound<'py, PyDict> = overlaid(&files, &updated)?; - let allow_suppressions: bool = request.config.getattr("allow_suppressions")?.extract()?; + let allow_suppressions: bool = native_field(request.config, "allow_suppressions")?.extract()?; let violations: Vec> = apply_lint_suppressions(py, violations, final_contents.clone(), allow_suppressions)?; Ok(FormatOutcome { 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 65ca9574ce..202e716c67 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/format_command/selection.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/format_command/selection.rs @@ -16,6 +16,7 @@ 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::python_calls::{call_with, 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::registry; @@ -75,8 +76,11 @@ fn discovered_adapter<'py>( let adapter_name: Bound<'py, PyAny> = call_with( ®istry::native(py, NativeAttr::ResolveEffectiveAdapterName)?, &[ - ("project_config", discovered.getattr("project_config")?), - ("local_config", discovered.getattr("local_config")?), + ( + "project_config", + native_field(discovered, "project_config")?, + ), + ("local_config", native_field(discovered, "local_config")?), ], )?; resolve_cli_adapter(&adapter_name, project_dir) @@ -89,15 +93,15 @@ fn selectors<'py>(py: Python<'py>, values: &[String]) -> PyResult) -> PyResult> { let mut pairs: Vec<(String, PathBuf)> = Vec::new(); - for model_file in scope.discovered.getattr("model_files")?.try_iter()? { + for model_file in native_field(scope.discovered, "model_files")?.try_iter()? { let model_file: Bound<'_, PyAny> = model_file?; - let relative_path: Bound<'_, PyAny> = model_file.getattr("relative_path")?; + let relative_path: Bound<'_, PyAny> = native_field(&model_file, "relative_path")?; let configured: Bound<'_, PyAny> = model_file .getattr("header_values")? .call_method1("get", ("name",))?; let name: String = match configured.downcast::() { Ok(text) if !text.to_str()?.is_empty() => text.to_str()?.to_owned(), - _ => relative_path.getattr("stem")?.extract()?, + _ => native_field(&relative_path, "stem")?.extract()?, }; let path: PathBuf = scope.directory.join(relative_path.extract::()?); pairs.push((name, resolved(&path))); @@ -171,15 +175,18 @@ fn model_paths( let mut names: BTreeSet = BTreeSet::new(); for key in keys.try_iter()? { let key: Bound<'_, PyAny> = key?; - if key.getattr("resource_type")?.eq(&model_type)? { - names.insert(key.getattr("name")?.extract()?); + if native_field(&key, "resource_type")?.eq(&model_type)? { + names.insert(native_field(&key, "name")?.extract()?); } } let mut paths: BTreeSet = BTreeSet::new(); - for model in graph.getattr("project")?.getattr("models")?.try_iter()? { + for model in native_field(&graph, "project")? + .getattr("models")? + .try_iter()? + { let model: Bound<'_, PyAny> = model?; - if names.contains(&model.getattr("name")?.extract::()?) { - let relative: PathBuf = model.getattr("relative_path")?.extract()?; + if names.contains(&native_field(&model, "name")?.extract::()?) { + let relative: PathBuf = native_field(&model, "relative_path")?.extract()?; paths.insert(resolved(&scope.directory.join(relative))); } } 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 15349b3765..67a8d50ee1 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/freshness/freshness_command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/freshness/freshness_command.rs @@ -32,6 +32,7 @@ use crate::bindings::_helpers::result_output::documents::write_execution_json_ou const COMPILE_TYPES_MODULE: &str = "sqlbuild.compiler.compile.types"; 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::ErrorId; use crate::bindings::_helpers::records::registry; use sqlbuild_core::constants::SOURCE_FRESHNESS_TABLE_NAME as STATE_TABLE_NAME; @@ -81,15 +82,15 @@ fn source_names_for_keys( let py: Python<'_> = graph.py(); let source_type: Bound<'_, PyAny> = imported(py, COMPILE_TYPES_MODULE, "CompiledResourceType")?.getattr("SOURCE")?; - let upstream_deps: Bound<'_, PyAny> = graph.getattr("upstream_deps")?; + let upstream_deps: Bound<'_, PyAny> = native_field(graph, "upstream_deps")?; for key in keys.try_iter()? { let key: Bound<'_, PyAny> = key?; let upstream: Bound<'_, PyAny> = expand_project_upstream_keys(key.clone(), &upstream_deps)?.into_any(); for candidate in std::iter::once(Ok(key.clone())).chain(upstream.try_iter()?) { let candidate: Bound<'_, PyAny> = candidate?; - if candidate.getattr("resource_type")?.eq(&source_type)? { - names.insert(candidate.getattr("name")?.extract()?); + if native_field(&candidate, "resource_type")?.eq(&source_type)? { + names.insert(native_field(&candidate, "name")?.extract()?); } } } @@ -113,7 +114,7 @@ fn selected_source_names( let selected_keys: Bound<'_, PyAny> = if select.is_truthy()? { graph_keys(graph, select)? } else { - graph.getattr("all_keys")?.call_method0("values")? + native_field(graph, "all_keys")?.call_method0("values")? }; let selected: BTreeSet = source_names_for_keys(graph, &selected_keys)?; let excluded: BTreeSet = if exclude.is_truthy()? { @@ -135,12 +136,12 @@ fn previous_records<'py>( let mut schemas: BTreeSet = BTreeSet::new(); for collection in ["models", "seeds", "functions"] { for item in project.getattr(collection)?.try_iter()? { - let destination: Bound<'py, PyAny> = item?.getattr("destination")?; - let destination_database: Bound<'py, PyAny> = destination.getattr("database")?; + 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() { database = Some(destination_database); } - let schema: Bound<'py, PyAny> = destination.getattr("schema")?; + let schema: Bound<'py, PyAny> = native_field(&destination, "schema")?; if !schema.is_none() { schemas.insert(schema.extract()?); } @@ -173,9 +174,9 @@ fn previous_records<'py>( )?; let read: StateRead<'py> = StateRead { connection: connection.clone(), - execute: adapter.getattr("execute")?, - render_qualified_name: adapter.getattr("render_qualified_name")?, - render_read_latest_sql: adapter.getattr("render_read_latest_source_freshness_sql")?, + execute: native_field(adapter, "execute")?, + render_qualified_name: native_field(adapter, "render_qualified_name")?, + render_read_latest_sql: native_field(adapter, "render_read_latest_source_freshness_sql")?, }; let records: Bound<'py, PyDict> = PyDict::new(py); for schema in &schemas { @@ -210,16 +211,19 @@ fn observe( &[ ("discovered_inputs", discovered), ("project_dir", project_dir), - ("selected_target", &request.getattr("selected_target")?), - ("cli_vars", &request.getattr("cli_vars")?), + ( + "selected_target", + &native_field(request, "selected_target")?, + ), + ("cli_vars", &native_field(request, "cli_vars")?), ], )?), )?; let connection: Bound<'_, PyAny> = adapter.call_method1("connect", (connection_config,))?; let observed = (|| { - let project: Bound<'_, PyAny> = graph.getattr("project")?; + let project: Bound<'_, PyAny> = native_field(graph, "project")?; let by_identity: Option> = - if request.getattr("compare_state")?.extract::()? { + if native_field(request, "compare_state")?.extract::()? { Some(previous_records(adapter, &connection, &project)?.into_any()) } else { None @@ -227,7 +231,7 @@ fn observe( let sources: Vec> = project .getattr("sources")? .try_iter()? - .map(|source| source?.getattr("source_entry")) + .map(|source| native_field(&source?, "source_entry")) .collect::>()?; let observed_at: Bound<'_, PyAny> = imported(py, "datetime", "datetime")? .call_method1("now", (imported(py, "datetime", "UTC")?,))?; @@ -240,7 +244,7 @@ fn observe( observe_freshness( &observer, (&PyTuple::new(py, sources)?.into_any(), names, &[]), - (project.getattr("run_id")?.extract()?, &state), + (native_field(&project, "run_id")?.extract()?, &state), ) })(); let closed: PyResult> = adapter.call_method1("close", (&connection,)); @@ -266,26 +270,26 @@ fn write_json_output( #[pyfunction] #[pyo3(signature = (*, request))] fn run_freshness_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { - let fail_on_stale: bool = request.getattr("fail_on_stale")?.extract()?; - let fail_on_error: bool = request.getattr("fail_on_error")?.extract()?; - if fail_on_stale && !request.getattr("compare_state")?.extract::()? { + 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::()? { return Err(user_error( py, (STALE_REQUIRES_STATE_MESSAGE, STALE_REQUIRES_STATE_CODE), )); } - let project_dir: Bound<'_, PyAny> = match request.getattr("project_dir")? { + 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")?, }; let discovered: Bound<'_, PyAny> = discover_project_inputs(py, Some(keywords(py, &[("project_dir", &project_dir)])?))?; - let select: Bound<'_, PyAny> = request.getattr("select")?; - let exclude: Bound<'_, PyAny> = request.getattr("exclude")?; + let select: Bound<'_, PyAny> = native_field(request, "select")?; + let exclude: Bound<'_, PyAny> = native_field(request, "exclude")?; reject_unit_test_selectors(&select, &exclude, &discovered)?; let adapter_name: Bound<'_, PyAny> = resolve_effective_adapter_name( - &discovered.getattr("project_config")?, - &discovered.getattr("local_config")?, + &native_field(&discovered, "project_config")?, + &native_field(&discovered, "local_config")?, )?; let adapter: Bound<'_, PyAny> = resolve_cli_adapter(&adapter_name, &project_dir)?; let graph: Bound<'_, PyAny> = build_project_graph( @@ -295,9 +299,15 @@ fn run_freshness_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult &[ ("discovered_inputs", &discovered), ("adapter", &adapter), - ("selected_target", &request.getattr("selected_target")?), - ("no_sql_validation", &request.getattr("no_sql_validation")?), - ("cli_vars", &request.getattr("cli_vars")?), + ( + "selected_target", + &native_field(request, "selected_target")?, + ), + ( + "no_sql_validation", + &native_field(request, "no_sql_validation")?, + ), + ("cli_vars", &native_field(request, "cli_vars")?), ], )?), )?; @@ -314,15 +324,15 @@ fn run_freshness_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult }; let payload: String = freshness_json(&results).map_err(|error| PyValueError::new_err(format!("{error:?}")))?; - let json_output_path: Bound<'_, PyAny> = request.getattr("json_output_path")?; - if request.getattr("json_output")?.extract::()? { + let json_output_path: Bound<'_, PyAny> = native_field(request, "json_output_path")?; + if native_field(request, "json_output")?.extract::()? { write_json_output(py, &payload, (true, &json_output_path))?; } else { if !json_output_path.is_none() { write_json_output(py, &payload, (false, &json_output_path))?; } let use_color: bool = - !request.getattr("no_color")?.extract::()? && stdout_supports_color(py)?; + !native_field(request, "no_color")?.extract::()? && stdout_supports_color(py)?; write(py, &render_freshness_text(&results, CliStyle { use_color }))?; } Ok(freshness_exit_code( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/freshness/freshness_observe.rs b/crates/sqlbuild-python/src/bindings/_helpers/freshness/freshness_observe.rs index 8baefb7939..cbcdec8f8c 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/freshness/freshness_observe.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/freshness/freshness_observe.rs @@ -17,6 +17,7 @@ use crate::bindings::_helpers::freshness::observation::{ }; 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::state_rows::freshness::{age_policy, equivalent}; use sqlbuild_core::freshness_text::main::freshness_status::freshness_status; use sqlbuild_core::freshness_text::models::{ @@ -56,9 +57,9 @@ fn target_result( message: Option, ) -> PyResult { let mut result: FreshnessSourceResult = FreshnessSourceResult::new(name, status); - result.target_database = optional_text(source.getattr("database")?)?; - result.target_schema = optional_text(source.getattr("schema")?)?; - result.target_name = optional_text(source.getattr("table")?)?; + result.target_database = optional_text(native_field(source, "database")?)?; + result.target_schema = optional_text(native_field(source, "schema")?)?; + result.target_name = optional_text(native_field(source, "table")?)?; result.message = message; Ok(result) } @@ -70,7 +71,8 @@ fn previous_record<'py>( let Some(lookup) = &state.by_identity else { return Ok(None); }; - let found: Bound<'py, PyAny> = lookup.call_method1("get", (record.getattr("identity")?,))?; + let found: Bound<'py, PyAny> = + lookup.call_method1("get", (native_field(record, "identity")?,))?; Ok(found.is_truthy()?.then_some(found)) } @@ -85,19 +87,19 @@ fn observed_result( source, &PyString::new(py, OBSERVATION_RUN_ID).into_any(), )?; - let freshness: Bound<'_, PyAny> = source.getattr("freshness")?; + let freshness: Bound<'_, PyAny> = native_field(source, "freshness")?; let (lag_tolerance, policy) = if freshness.is_none() { (py.None().into_bound(py), py.None().into_bound(py)) } else { ( - freshness.getattr("lag_tolerance")?, - freshness.getattr("age_policy")?, + native_field(&freshness, "lag_tolerance")?, + native_field(&freshness, "age_policy")?, ) }; let age_status: Option> = age_policy( &policy, - &observation.getattr("data_version")?, - &observation.getattr("observed_at")?, + &native_field(observation, "data_version")?, + &native_field(observation, "observed_at")?, )?; let previous: Option> = previous_record(state, ¤t)?; let comparison: StateComparison = match &previous { @@ -109,7 +111,7 @@ fn observed_result( StateComparison::Equivalent { same_hash: previous .getattr("data_version_hash")? - .eq(current.getattr("data_version_hash")?)?, + .eq(native_field(¤t, "data_version_hash")?)?, } } else { StateComparison::Different @@ -118,23 +120,23 @@ fn observed_result( }; let mut result: FreshnessSourceResult = FreshnessSourceResult::new(name, freshness_status(comparison)); - result.strategy = optional_text(current.getattr("strategy")?)?; - result.value_kind = optional_text(current.getattr("value_kind")?)?; - result.current_data_version = optional_text(current.getattr("data_version")?)?; + result.strategy = optional_text(native_field(¤t, "strategy")?)?; + result.value_kind = optional_text(native_field(¤t, "value_kind")?)?; + result.current_data_version = optional_text(native_field(¤t, "data_version")?)?; result.previous_data_version = match &previous { - Some(previous) => optional_text(previous.getattr("data_version")?)?, + Some(previous) => optional_text(native_field(previous, "data_version")?)?, None => None, }; result.lag_tolerance = optional_text(lag_tolerance)?; - result.target_database = optional_text(current.getattr("target_database")?)?; - result.target_schema = optional_text(current.getattr("target_schema")?)?; - result.target_name = optional_text(current.getattr("target_name")?)?; + result.target_database = optional_text(native_field(¤t, "target_database")?)?; + result.target_schema = optional_text(native_field(¤t, "target_schema")?)?; + result.target_name = optional_text(native_field(¤t, "target_name")?)?; if comparison == StateComparison::Missing { result.message = Some(MISSING_STATE_MESSAGE.to_owned()); } result.age_status = match age_status { None => None, - Some(status) => Some(status.getattr("value")?.extract()?), + Some(status) => Some(native_field(&status, "value")?.extract()?), }; Ok(result) } @@ -166,9 +168,10 @@ fn adapter_observation<'py>( )?)); } }; - let unknown: Bound<'py, PyAny> = batch.getattr("unknown")?.call_method1("get", (name,))?; + let unknown: Bound<'py, PyAny> = + native_field(batch, "unknown")?.call_method1("get", (name,))?; if !unknown.is_none() { - let reason: String = unknown.getattr("reason")?.str()?.extract()?; + let reason: String = native_field(&unknown, "reason")?.str()?.extract()?; let status: FreshnessStatus = if reason == UNAVAILABLE_REASON { FreshnessStatus::Unknown } else { @@ -177,7 +180,7 @@ fn adapter_observation<'py>( return Ok(Err(target_result( source, (name, status), - optional_text(unknown.getattr("message")?)?, + optional_text(native_field(&unknown, "message")?)?, )?)); } let observation: Bound<'py, PyAny> = batch @@ -219,7 +222,7 @@ fn observe_sources( let mut positions: HashMap = HashMap::new(); let mut adapter_sources: Vec> = Vec::new(); for source in sources { - let name: String = source.getattr("name")?.extract()?; + let name: String = native_field(source, "name")?.extract()?; let Some(observed_source) = observation_source(&observer.adapter, source)? else { results.push(target_result( source, @@ -267,7 +270,7 @@ pub(crate) fn observe_freshness( let mut chosen: Vec> = Vec::new(); for source in sources.try_iter()? { let source: Bound<'_, PyAny> = source?; - let name: String = source.getattr("name")?.extract()?; + let name: String = native_field(&source, "name")?.extract()?; if (selected.is_empty() || selected.contains(name.as_str())) && !excluded.contains(name.as_str()) { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/freshness/observation.rs b/crates/sqlbuild-python/src/bindings/_helpers/freshness/observation.rs index 92dcadea18..d287a8768b 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/freshness/observation.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/freshness/observation.rs @@ -15,6 +15,7 @@ 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::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; @@ -40,11 +41,13 @@ fn observation_error(py: Python<'_>, message: &str) -> pyo3::PyErr { /// The configured strategy value of a source, or `None` without freshness config. pub(crate) fn strategy(source: &Bound<'_, PyAny>) -> PyResult> { - let freshness: Bound<'_, PyAny> = source.getattr("freshness")?; + let freshness: Bound<'_, PyAny> = native_field(source, "freshness")?; if freshness.is_none() { return Ok(None); } - Ok(Some(freshness.getattr("strategy")?.str()?.extract()?)) + Ok(Some( + native_field(&freshness, "strategy")?.str()?.extract()?, + )) } /// The source as observed: itself with freshness config, an adapter-metadata copy, or none. @@ -53,11 +56,11 @@ pub(crate) fn observation_source<'py>( source: &Bound<'py, PyAny>, ) -> PyResult>> { let py: Python<'py> = source.py(); - if !source.getattr("freshness")?.is_none() { + if !native_field(source, "freshness")?.is_none() { return Ok(Some(source.clone())); } - if !source.getattr("expression")?.is_none() - || source.getattr("table")?.is_none() + if !native_field(source, "expression")?.is_none() + || native_field(source, "table")?.is_none() || !adapter .call_method0("supports_table_freshness_metadata")? .is_truthy()? @@ -159,8 +162,8 @@ fn observe_configured_inner<'py>( strategy: Option<&str>, ) -> PyResult> { let py: Python<'py> = source.py(); - let name: Bound<'py, PyAny> = source.getattr("name")?; - let config: Bound<'py, PyAny> = source.getattr("freshness")?; + let name: Bound<'py, PyAny> = native_field(source, "name")?; + let config: Bound<'py, PyAny> = native_field(source, "freshness")?; let Some(strategy) = strategy else { return Err(observation_error( py, @@ -173,9 +176,9 @@ fn observe_configured_inner<'py>( if strategy == ADAPTER_STRATEGY { return observe_adapter_source(observer, source); } - let value_kind: Bound<'py, PyAny> = config.getattr("value_kind")?; + let value_kind: Bound<'py, PyAny> = native_field(&config, "value_kind")?; let sql: Bound<'py, PyAny> = if strategy == COLUMN_STRATEGY { - let column: Bound<'py, PyAny> = config.getattr("column")?; + let column: Bound<'py, PyAny> = native_field(&config, "column")?; if column.is_none() || value_kind.is_none() { return Err(observation_error( py, @@ -185,7 +188,7 @@ fn observe_configured_inner<'py>( ), )); } - let filter: Bound<'py, PyAny> = config.getattr("filter")?; + let filter: Bound<'py, PyAny> = native_field(&config, "filter")?; let where_sql: String = if filter.is_none() { String::new() } else { @@ -201,7 +204,7 @@ fn observe_configured_inner<'py>( ("source_relation", text(py, &relation)), ( "source_is_subquery", - PyBool::new(py, !source.getattr("expression")?.is_none()) + PyBool::new(py, !native_field(source, "expression")?.is_none()) .to_owned() .into_any(), ), @@ -209,7 +212,7 @@ fn observe_configured_inner<'py>( ], )? } else { - let query: Bound<'py, PyAny> = config.getattr("query")?; + let query: Bound<'py, PyAny> = native_field(&config, "query")?; if query.is_none() || value_kind.is_none() { return Err(observation_error( py, @@ -225,7 +228,7 @@ fn observe_configured_inner<'py>( presumed_utc_value(&single_data_version(observer, &name, sql)?)?; observation( &name, - (config.getattr("strategy")?, value_kind), + (native_field(&config, "strategy")?, value_kind), (data_version, observer.observed_at.clone()), ) } @@ -238,7 +241,7 @@ fn single_data_version<'py>( let py: Python<'py> = source_name.py(); let name: String = source_name.str()?.extract()?; let result: Bound<'py, PyAny> = match call_with( - &observer.adapter.getattr("query")?, + &native_field(&observer.adapter, "query")?, &[ ("connection", observer.connection.clone()), ("sql", sql), @@ -260,8 +263,8 @@ fn single_data_version<'py>( )); } }; - let columns: usize = result.getattr("columns")?.len()?; - let rows: Bound<'py, PyAny> = result.getattr("rows")?; + let columns: usize = native_field(&result, "columns")?.len()?; + let rows: Bound<'py, PyAny> = native_field(&result, "rows")?; let row_count: usize = rows.len()?; let first: Option> = if columns == 1 && row_count == 1 { Some(rows.get_item(0)?.get_item(0)?) @@ -289,13 +292,13 @@ fn unsupported_message( { return Ok(Some(format!( "adapter '{}' does not support table freshness metadata", - adapter.getattr("adapter_name")?.str()? + native_field(adapter, "adapter_name")?.str()? ))); } - if !source.getattr("expression")?.is_none() || source.getattr("table")?.is_none() { + if !native_field(source, "expression")?.is_none() || native_field(source, "table")?.is_none() { return Ok(Some(format!( "source '{}' adapter freshness requires a physical table source", - source.getattr("name")?.str()? + native_field(source, "name")?.str()? ))); } Ok(None) @@ -329,8 +332,8 @@ fn metadata_unknown<'py>( let name: String = source_name.str()?.extract()?; let unavailable: Bound<'py, PyAny> = host::entry(py, AdapterHost::TableFreshnessStatus)?.getattr("UNAVAILABLE")?; - if metadata.getattr("status")?.eq(unavailable)? { - let message: Bound<'py, PyAny> = metadata.getattr("message")?; + if native_field(metadata, "status")?.eq(unavailable)? { + let message: Bound<'py, PyAny> = native_field(metadata, "message")?; let message: String = if message.is_truthy()? { message.str()?.extract()? } else { @@ -338,7 +341,7 @@ fn metadata_unknown<'py>( }; return unknown(source_name, ("UNAVAILABLE", text(py, &message))).map(Some); } - if metadata.getattr("data_version")?.is_none() { + if native_field(metadata, "data_version")?.is_none() { let message: String = format!("source '{name}' freshness data_version is null"); return unknown(source_name, ("UNAVAILABLE", text(py, &message))).map(Some); } @@ -351,7 +354,7 @@ fn metadata_observation<'py>( fallback_observed_at: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = source_name.py(); - let observed_at: Bound<'py, PyAny> = metadata.getattr("observed_at")?; + let observed_at: Bound<'py, PyAny> = native_field(metadata, "observed_at")?; let observed_at: Bound<'py, PyAny> = if observed_at.is_none() { fallback_observed_at.clone() } else { @@ -362,10 +365,10 @@ fn metadata_observation<'py>( ( adapter_strategy(py)?, registry::class(py, RecordId::SourceFreshnessValueKind)? - .call1((metadata.getattr("value_kind")?,))?, + .call1((native_field(metadata, "value_kind")?,))?, ), ( - presumed_utc_value(&metadata.getattr("data_version")?)?, + presumed_utc_value(&native_field(metadata, "data_version")?)?, observed_at, ), ) @@ -380,19 +383,21 @@ fn observe_adapter_source<'py>( return Err(observation_error(py, &message)); } let metadata: Bound<'py, PyAny> = call_with( - &observer.adapter.getattr("get_table_freshness_metadata")?, + &native_field(&observer.adapter, "get_table_freshness_metadata")?, &[ ("connection", observer.connection.clone()), - ("database", source.getattr("database")?), - ("schema", source.getattr("schema")?), - ("name", source.getattr("table")?), + ("database", native_field(source, "database")?), + ("schema", native_field(source, "schema")?), + ("name", native_field(source, "table")?), ], )?; - let name: Bound<'py, PyAny> = source.getattr("name")?; + let name: Bound<'py, PyAny> = native_field(source, "name")?; if let Some(unknown) = metadata_unknown(&name, &metadata)? { return Err(observation_error( py, - &unknown.getattr("message")?.str()?.extract::()?, + &native_field(&unknown, "message")? + .str()? + .extract::()?, )); } metadata_observation(&name, &metadata, &observer.observed_at) @@ -418,9 +423,9 @@ pub(crate) fn observe_adapter_batch<'py>( let batch: Bound<'py, PyAny> = observe_adapter_batch_inner(observer, sources)?; let metadata: Bound<'py, PyDict> = PyDict::new(py); metadata.set_item("item_count", sources.len())?; - metadata.set_item("unknown_count", batch.getattr("unknown")?.len()?)?; + metadata.set_item("unknown_count", native_field(&batch, "unknown")?.len()?)?; call_with( - &entered.getattr("completed")?, + &native_field(entered, "completed")?, &[("metadata", metadata.into_any())], )?; Ok(batch) @@ -438,7 +443,7 @@ fn observe_adapter_batch_inner<'py>( let requests: Bound<'py, PyDict> = PyDict::new(py); let request_class: Bound<'py, PyAny> = host::entry(py, AdapterHost::TableFreshnessRequest)?; for source in sources { - let name: Bound<'py, PyAny> = source.getattr("name")?; + let name: Bound<'py, PyAny> = native_field(source, "name")?; if let Some(message) = unsupported_message(&observer.adapter, source)? { unknown_sources .set_item(&name, unknown(&name, ("UNAVAILABLE", text(py, &message)))?)?; @@ -449,9 +454,9 @@ fn observe_adapter_batch_inner<'py>( call_with( &request_class, &[ - ("database", source.getattr("database")?), - ("schema", source.getattr("schema")?), - ("name", source.getattr("table")?.str()?.into_any()), + ("database", native_field(source, "database")?), + ("schema", native_field(source, "schema")?), + ("name", native_field(source, "table")?.str()?.into_any()), ], )?, )?; @@ -464,7 +469,7 @@ fn observe_adapter_batch_inner<'py>( unique.set_item(request, py.None())?; } let metadata_by_request: Bound<'py, PyAny> = call_with( - &observer.adapter.getattr("get_tables_freshness_metadata")?, + &native_field(&observer.adapter, "get_tables_freshness_metadata")?, &[ ("connection", observer.connection.clone()), ("requests", PyTuple::new(py, unique.keys())?.into_any()), @@ -474,7 +479,7 @@ fn observe_adapter_batch_inner<'py>( let observations: Bound<'py, PyDict> = PyDict::new(py); for (name, request) in requests.iter() { let missing: Bound<'py, PyAny> = call_with( - &metadata_class.getattr("unavailable")?, + &native_field(&metadata_class, "unavailable")?, &[( "message", text( @@ -513,11 +518,11 @@ pub(crate) fn observation_record<'py>( run_id: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = observation.py(); - let source_name: Bound<'py, PyAny> = observation.getattr("source_name")?; - let strategy: Bound<'py, PyAny> = observation.getattr("strategy")?; - let value_kind: Bound<'py, PyAny> = observation.getattr("value_kind")?; + let source_name: Bound<'py, PyAny> = native_field(observation, "source_name")?; + let strategy: Bound<'py, PyAny> = native_field(observation, "strategy")?; + let value_kind: Bound<'py, PyAny> = native_field(observation, "value_kind")?; let data_version: Bound<'py, PyAny> = - normalize(&observation.getattr("data_version")?, &value_kind)?; + normalize(&native_field(observation, "data_version")?, &value_kind)?; let data_version_hash: String = version_hash( &source_name.extract::()?, &strategy.extract::()?, @@ -528,15 +533,15 @@ pub(crate) fn observation_record<'py>( ®istry::class(py, RecordId::SourceFreshnessRecord)?, &[ ("source_name", source_name), - ("target_database", source.getattr("database")?), - ("target_schema", source.getattr("schema")?), - ("target_name", source.getattr("table")?), + ("target_database", native_field(source, "database")?), + ("target_schema", native_field(source, "schema")?), + ("target_name", native_field(source, "table")?), ("run_id", run_id.clone()), - ("strategy", strategy.getattr("value")?), - ("value_kind", value_kind.getattr("value")?), + ("strategy", native_field(&strategy, "value")?), + ("value_kind", native_field(&value_kind, "value")?), ("data_version", data_version), ("data_version_hash", text(py, &data_version_hash)), - ("observed_at", observation.getattr("observed_at")?), + ("observed_at", native_field(observation, "observed_at")?), ], ) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/freshness/planning.rs b/crates/sqlbuild-python/src/bindings/_helpers/freshness/planning.rs index 5f7d89cc16..0d27260653 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/freshness/planning.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/freshness/planning.rs @@ -19,6 +19,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::state_rows::freshness::{age_policy, equivalent}; @@ -31,7 +32,7 @@ const NO_CONFIG_MESSAGE: &str = "no freshness config and adapter metadata unavai fn handled_message<'py>(py: Python<'py>, error: &PyErr) -> PyResult>> { let value: Bound<'py, PyAny> = error.value(py).clone().into_any(); if value.is_instance(&host::entry(py, AdapterHost::AdapterUserError)?)? { - return value.getattr("message").map(Some); + return native_field(&value, "message").map(Some); } if value.is_instance(®istry::native( py, @@ -55,9 +56,9 @@ fn previous_records<'py>( let py: Python<'py> = adapter.py(); let read: StateRead<'py> = StateRead { connection: connection.clone(), - execute: adapter.getattr("execute")?, + execute: native_field(adapter, "execute")?, render_qualified_name: arguments.value("render_qualified_name")?, - render_read_latest_sql: adapter.getattr("render_read_latest_source_freshness_sql")?, + render_read_latest_sql: native_field(adapter, "render_read_latest_source_freshness_sql")?, }; let database: Bound<'py, PyAny> = arguments.value("state_database")?; let exists_by_schema: Bound<'py, PyAny> = arguments.value("state_table_exists_by_schema")?; @@ -69,12 +70,15 @@ fn previous_records<'py>( .is_truthy()?; let previous: Bound<'py, PyAny> = read_latest(&read, (&database, &schema), exists, source_names)?; - for (identity, candidate) in previous.getattr("records")?.downcast::()?.iter() { + for (identity, candidate) in native_field(&previous, "records")? + .downcast::()? + .iter() + { let replace: bool = match merged.get_item(&identity)? { None => true, Some(current) => candidate .getattr("observed_at")? - .gt(current.getattr("observed_at")?)?, + .gt(native_field(¤t, "observed_at")?)?, }; if replace { merged.set_item(identity, candidate)?; @@ -89,7 +93,7 @@ fn sorted_by_identity<'py>( records: impl Iterator>, ) -> PyResult>> { let mut keyed: Vec<(String, Bound<'py, PyAny>)> = records - .map(|record| Ok((record.getattr("identity")?.str()?.extract()?, record))) + .map(|record| Ok((native_field(&record, "identity")?.str()?.extract()?, record))) .collect::>()?; keyed.sort_by(|left, right| left.0.cmp(&right.0)); Ok(keyed.into_iter().map(|(_, record)| record).collect()) @@ -123,7 +127,7 @@ fn adapter_batch<'py>( }; let unknown_sources: Bound<'py, PyDict> = PyDict::new(py); for source in sources { - let name: Bound<'py, PyAny> = source.getattr("name")?; + let name: Bound<'py, PyAny> = native_field(source, "name")?; unknown_sources.set_item(&name, unknown(&name, ("ERROR", message.clone()))?)?; } call_with( @@ -172,7 +176,7 @@ pub(crate) fn build_direct_source_freshness_planning_result<'py>( if arguments.value("filter_previous_to_sources")?.is_truthy()? { let mut names: Vec = sources .iter() - .map(|source| source.getattr("name")?.extract()) + .map(|source| native_field(source, "name")?.extract()) .collect::>()?; names.sort(); Some(names) @@ -188,10 +192,10 @@ pub(crate) fn build_direct_source_freshness_planning_result<'py>( let observed_sources: Bound<'py, PyDict> = PyDict::new(py); let mut adapter_sources: Vec> = Vec::new(); for source in &sources { - if source.getattr("managed")?.is_truthy()? { + if native_field(source, "managed")?.is_truthy()? { continue; } - let name: Bound<'py, PyAny> = source.getattr("name")?; + let name: Bound<'py, PyAny> = native_field(source, "name")?; let Some(observed) = observation_source(&observer.adapter, source)? else { unknown_sources.set_item( &name, @@ -205,8 +209,8 @@ pub(crate) fn build_direct_source_freshness_planning_result<'py>( } } let batch: Bound<'py, PyAny> = adapter_batch(&observer, &adapter_sources)?; - unknown_sources.update(batch.getattr("unknown")?.downcast()?)?; - let observations: Bound<'py, PyAny> = batch.getattr("observations")?; + unknown_sources.update(native_field(&batch, "unknown")?.downcast()?)?; + let observations: Bound<'py, PyAny> = native_field(&batch, "observations")?; let mut observed_records: Vec> = Vec::new(); let changed: Bound<'py, PySet> = PySet::empty(py)?; let unchanged: Bound<'py, PySet> = PySet::empty(py)?; @@ -238,22 +242,22 @@ pub(crate) fn build_direct_source_freshness_planning_result<'py>( }; let record: Bound<'py, PyAny> = observation_record(&observation, &source, &run_id)?; observed_records.push(record.clone()); - let freshness: Bound<'py, PyAny> = source.getattr("freshness")?; + let freshness: Bound<'py, PyAny> = native_field(&source, "freshness")?; let (policy, lag_tolerance) = if freshness.is_none() { (py.None().into_bound(py), None) } else { ( - freshness.getattr("age_policy")?, + native_field(&freshness, "age_policy")?, freshness .getattr("lag_tolerance")? .extract::>()?, ) }; - let identity: Bound<'py, PyAny> = record.getattr("identity")?; + let identity: Bound<'py, PyAny> = native_field(&record, "identity")?; if let Some(status) = age_policy( &policy, - &observation.getattr("data_version")?, - &observation.getattr("observed_at")?, + &native_field(&observation, "data_version")?, + &native_field(&observation, "observed_at")?, )? { age_statuses.set_item(&identity, status)?; } @@ -323,16 +327,16 @@ pub(crate) fn build_direct_source_freshness_propagation_result<'py>( scope: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = scope.py(); - let downstream_deps: Bound<'py, PyAny> = scope.getattr("downstream_deps")?; + let downstream_deps: Bound<'py, PyAny> = native_field(scope, "downstream_deps")?; let changed: Bound<'py, PyDict> = PyDict::new(py); let unknown_sources: Bound<'py, PyDict> = PyDict::new(py); let errors: Bound<'py, PyDict> = PyDict::new(py); let stale: Bound<'py, PySet> = PySet::empty(py)?; let blocked: Bound<'py, PySet> = PySet::empty(py)?; - for identity in source_freshness.getattr("changed_identities")?.try_iter()? { + for identity in native_field(source_freshness, "changed_identities")?.try_iter()? { let identity: Bound<'py, PyAny> = identity?; let models: Bound<'py, PyFrozenSet> = - downstream_models(&identity.getattr("source_name")?, &downstream_deps)?; + downstream_models(&native_field(&identity, "source_name")?, &downstream_deps)?; stale.call_method1("update", (&models,))?; changed.set_item(identity, models)?; } @@ -356,7 +360,7 @@ pub(crate) fn build_direct_source_freshness_propagation_result<'py>( continue; } let models: Bound<'py, PyFrozenSet> = - downstream_models(&identity.getattr("source_name")?, &downstream_deps)?; + downstream_models(&native_field(&identity, "source_name")?, &downstream_deps)?; blocked.call_method1("update", (&models,))?; errors.set_item(identity, models)?; } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/freshness/publication.rs b/crates/sqlbuild-python/src/bindings/_helpers/freshness/publication.rs index 2b0d374a70..1d7d91d50b 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/freshness/publication.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/freshness/publication.rs @@ -11,6 +11,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::{ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; @@ -26,12 +27,12 @@ fn successful_model_names<'py>(result: &Bound<'py, PyAny>) -> PyResult = host::entry(py, ExecutorHost::ExecutionStatus)?.getattr("SUCCESS")?; let mut names: Vec> = Vec::new(); - for model_result in result.getattr("model_results")?.try_iter()? { + for model_result in native_field(result, "model_results")?.try_iter()? { let model_result: Bound<'py, PyAny> = model_result?; - let status: Bound<'py, PyAny> = model_result.getattr("status")?; + let status: Bound<'py, PyAny> = native_field(&model_result, "status")?; let value: Bound<'py, PyAny> = status.getattr_opt("value")?.unwrap_or(status); if value.eq(&success)? { - names.push(model_result.getattr("model_name")?); + names.push(native_field(&model_result, "model_name")?); } } Ok(names) @@ -50,13 +51,13 @@ fn collect_record<'py>( let Some(entry) = entries_by_name.get_item(model_name?)? else { continue; }; - let destination: Bound<'py, PyAny> = entry.getattr("destination")?; - let schema: Bound<'py, PyAny> = destination.getattr("schema")?; + let destination: Bound<'py, PyAny> = native_field(&entry, "destination")?; + let schema: Bound<'py, PyAny> = native_field(&destination, "schema")?; if schema.is_none() { continue; } let location: (Bound<'py, PyAny>, Bound<'py, PyAny>) = - (destination.getattr("database")?, schema); + (native_field(&destination, "database")?, schema); if contains_location(&recorded, &location)? { continue; } @@ -104,14 +105,14 @@ fn eligible_records<'py>( run_id: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = plan.py(); - let freshness: Bound<'py, PyAny> = plan.getattr("source_freshness")?; - if freshness.is_none() || freshness.getattr("propagation")?.is_none() { + let freshness: Bound<'py, PyAny> = native_field(plan, "source_freshness")?; + if freshness.is_none() || native_field(&freshness, "propagation")?.is_none() { return Ok(Vec::new()); } let entries_by_name: Bound<'py, PyDict> = PyDict::new(py); - for entry in plan.getattr("model_entries")?.try_iter()? { + for entry in native_field(plan, "model_entries")?.try_iter()? { let entry: Bound<'py, PyAny> = entry?; - entries_by_name.set_item(entry.getattr("name")?, entry)?; + entries_by_name.set_item(native_field(&entry, "name")?, entry)?; } if entries_by_name.is_empty() { return Ok(Vec::new()); @@ -120,9 +121,9 @@ fn eligible_records<'py>( let successful: Bound<'py, PyFrozenSet> = PyFrozenSet::new(py, successful_model_names(result)?)?; let observed: Bound<'py, PyDict> = PyDict::new(py); - for record in freshness.getattr("observed_records")?.try_iter()? { + for record in native_field(&freshness, "observed_records")?.try_iter()? { let record: Bound<'py, PyAny> = record?; - observed.set_item(record.getattr("identity")?, 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); @@ -157,23 +158,23 @@ fn renderers<'py>(adapter: &Bound<'py, PyAny>) -> PyResult> { &[ ( "render_qualified_name", - adapter.getattr("render_qualified_name")?, + native_field(adapter, "render_qualified_name")?, ), ( "render_framework_type", - adapter.getattr("render_framework_type")?, + native_field(adapter, "render_framework_type")?, ), ( "render_insert_records_sql", - adapter.getattr("render_insert_source_freshness_records_sql")?, + native_field(adapter, "render_insert_source_freshness_records_sql")?, ), ( "render_create_table_sql", - adapter.getattr("render_create_source_freshness_table_sql")?, + native_field(adapter, "render_create_source_freshness_table_sql")?, ), ( "render_create_index_sqls", - adapter.getattr("render_create_source_freshness_index_sqls")?, + native_field(adapter, "render_create_source_freshness_index_sqls")?, ), ], ) @@ -190,7 +191,7 @@ fn write_records<'py>( for ((database, schema), records) in grouped { let mut by_source: Vec<(Bound<'py, PyAny>, Vec>)> = Vec::new(); for record in records { - let source_name: Bound<'py, PyAny> = record.getattr("source_name")?; + let source_name: Bound<'py, PyAny> = native_field(record, "source_name")?; let mut position: Option = None; for (index, (known, _)) in by_source.iter().enumerate() { if known.eq(&source_name)? { @@ -205,7 +206,7 @@ fn write_records<'py>( } for (source_name, source_records) in by_source { let scope: Bound<'py, PyAny> = call_with( - &cost_context.getattr("resource_scope")?, + &native_field(&cost_context, "resource_scope")?, &[ ("resource_type", PyString::new(py, "source").into_any()), ("resource_name", source_name), @@ -222,7 +223,7 @@ fn write_records<'py>( py, &[ ("connection", connection.clone()), - ("execute", adapter.getattr("execute")?), + ("execute", native_field(adapter, "execute")?), ("database", database.clone()), ("schema", schema.clone()), ("records", PyTuple::new(py, &source_records)?.into_any()), 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 854795f961..d01d1d9175 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/freshness/state_io.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/freshness/state_io.rs @@ -15,6 +15,7 @@ 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::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; @@ -171,7 +172,7 @@ freshness table to regenerate source freshness state." let records: Bound<'py, PyDict> = PyDict::new(py); for row in result.call_method0("fetchall")?.try_iter()? { let record: Bound<'py, PyAny> = row_record(&row?)?; - records.set_item(record.getattr("identity")?, record)?; + records.set_item(native_field(&record, "identity")?, record)?; } call_with( ®istry::class(py, RecordId::SourceFreshnessSet)?, @@ -293,18 +294,18 @@ pub(crate) fn write_source_freshness_records<'py>( let location = || -> Vec<(&str, Bound<'py, PyAny>)> { vec![("database", database.clone()), ("schema", schema.clone())] }; - let render_create: Bound<'py, PyAny> = renderers.getattr("render_create_table_sql")?; + let render_create: Bound<'py, PyAny> = native_field(renderers, "render_create_table_sql")?; let create_sql: Bound<'py, PyAny> = if render_create.is_none() { create_table_sql( (database, schema), - &renderers.getattr("render_qualified_name")?, - &renderers.getattr("render_framework_type")?, + &native_field(renderers, "render_qualified_name")?, + &native_field(renderers, "render_framework_type")?, )? } else { call_with(&render_create, &location())? }; execute_sql((execute, connection), create_sql)?; - let render_indexes: Bound<'py, PyAny> = renderers.getattr("render_create_index_sqls")?; + let render_indexes: Bound<'py, PyAny> = native_field(renderers, "render_create_index_sqls")?; if !render_indexes.is_none() { for index_sql in call_with(&render_indexes, &location())?.try_iter()? { execute_sql((execute, connection), index_sql?)?; @@ -312,8 +313,10 @@ pub(crate) fn write_source_freshness_records<'py>( } let mut insert: Vec<(&str, Bound<'py, PyAny>)> = location(); insert.push(("records", records.clone())); - let insert_sql: Bound<'py, PyAny> = - call_with(&renderers.getattr("render_insert_records_sql")?, &insert)?; + let insert_sql: Bound<'py, PyAny> = call_with( + &native_field(renderers, "render_insert_records_sql")?, + &insert, + )?; execute_sql((execute, connection), insert_sql) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/frontier/encoder.rs b/crates/sqlbuild-python/src/bindings/_helpers/frontier/encoder.rs index 7ce3002cfb..ddb868ca9f 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/frontier/encoder.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/frontier/encoder.rs @@ -23,6 +23,7 @@ use sqlbuild_core::json::models::{JsonInteger, JsonValue}; use crate::bindings::_helpers::compile_driver::python_calls::imported; use crate::bindings::_helpers::frontier::python_types::PythonTypes; +use crate::bindings::_helpers::records::fields::field as native_field; /// An instance's attributes, by name in first-set order. type Attributes<'py> = Vec<(String, Bound<'py, PyAny>)>; @@ -157,7 +158,7 @@ impl<'py> CaptureEncoder<'py> { fn encode_value(&mut self, value: &Bound<'py, PyAny>) -> PyResult { if value.is_instance(&self.types.enumeration)? { - let name: String = value.getattr("name")?.str()?.to_str()?.to_owned(); + let name: String = native_field(value, "name")?.str()?.to_str()?.to_owned(); return Ok(tagged( "__enum__", JsonValue::String(format!( @@ -271,12 +272,15 @@ impl<'py> CaptureEncoder<'py> { return Ok(JsonValue::Object(vec![ ( "__partial__".to_owned(), - self.encode(&value.getattr("func")?)?, + self.encode(&native_field(value, "func")?)?, + ), + ( + "args".to_owned(), + self.encode(&native_field(value, "args")?)?, ), - ("args".to_owned(), self.encode(&value.getattr("args")?)?), ( "keywords".to_owned(), - self.encode(&value.getattr("keywords")?)?, + self.encode(&native_field(value, "keywords")?)?, ), ])); } @@ -300,7 +304,7 @@ impl<'py> CaptureEncoder<'py> { let mut entries: Vec<(String, JsonValue)> = vec![("__type__".to_owned(), JsonValue::String(type_name))]; for field in self.types.fields.call1((value,))?.try_iter()? { - let name: String = field?.getattr("name")?.extract()?; + let name: String = native_field(&field?, "name")?.extract()?; if omitted.contains(&name.as_str()) { continue; } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/frontier/python_types.rs b/crates/sqlbuild-python/src/bindings/_helpers/frontier/python_types.rs index 4cfe429e65..9f2f2cb712 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/frontier/python_types.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/frontier/python_types.rs @@ -7,6 +7,7 @@ use sqlbuild_core::json::models::{JsonInteger, JsonValue}; use crate::bindings::_helpers::compile_driver::python_calls::imported; use crate::bindings::_helpers::frontier::encoder::qualified_name; +use crate::bindings::_helpers::records::fields::field as native_field; /// The Python classes and functions the encoder checks values against. pub(crate) struct PythonTypes<'py> { @@ -117,7 +118,7 @@ impl<'py> PythonTypes<'py> { if value.is_instance(&self.pattern)? { return Ok(Some(text( "__pattern__", - value.getattr("pattern")?.str()?.to_str()?.to_owned(), + native_field(value, "pattern")?.str()?.to_str()?.to_owned(), ))); } if value.is_instance(&self.module)? { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/frontier/stage_capture.rs b/crates/sqlbuild-python/src/bindings/_helpers/frontier/stage_capture.rs index ab4f013520..1ca54501a7 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/frontier/stage_capture.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/frontier/stage_capture.rs @@ -12,6 +12,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::{ call_with, imported, keywords, within, }; use crate::bindings::_helpers::frontier::encoder::{CaptureEncoder, emit_error}; +use crate::bindings::_helpers::records::fields::field as native_field; pub(crate) const CAPTURE_DIR_ENV_VAR: &str = "SQLBUILD_COMPILER_STAGE_CAPTURE_DIR"; const CAPTURE_SUFFIX: &str = ".json"; @@ -58,7 +59,7 @@ pub(crate) fn write_stage_capture( return Ok(()); } let number: u64 = capture_number(&directory)?; - let stage_name: String = stage.getattr("value")?.str()?.extract()?; + let stage_name: String = native_field(stage, "value")?.str()?.extract()?; let path: Bound<'_, PyAny> = imported(py, "pathlib", "Path")? .call1((directory,))? .call_method1( @@ -90,7 +91,7 @@ pub(crate) fn write_plan_capture(plan_output: &Bound<'_, PyAny>) -> PyResult<()> fn write_capture_file(path: &Bound<'_, PyAny>, value: &Bound<'_, PyAny>) -> PyResult<()> { let py: Python<'_> = path.py(); call_with( - &path.getattr("parent")?.getattr("mkdir")?, + &native_field(path, "parent")?.getattr("mkdir")?, &[ ( "parents", 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 603a7870aa..a439a58255 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/graph/execution_gates.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/graph/execution_gates.rs @@ -8,6 +8,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; @@ -92,17 +93,17 @@ fn model_python_hook_names<'py>( model: &Bound<'py, PyAny>, ) -> PyResult> { let entry_type: Bound<'_, PyAny> = registry::class(py, RecordId::PythonHookEntry)?; - let values: Bound<'_, PyAny> = model.getattr("config")?.getattr("values")?; + let values: Bound<'_, PyAny> = native_field(model, "config")?.getattr("values")?; let mut names: Vec = Vec::new(); for hook_key in MODEL_HOOK_KEYS { let entries: Bound<'_, PyAny> = values.call_method1("get", (hook_key,))?; if entries.is_instance(&entry_type)? { - names.push(entries.getattr("name")?.extract()?); + names.push(native_field(&entries, "name")?.extract()?); } else if entries.is_instance_of::() || entries.is_instance_of::() { for entry in entries.try_iter()? { let entry: Bound<'_, PyAny> = entry?; if entry.is_instance(&entry_type)? { - names.push(entry.getattr("name")?.extract()?); + names.push(native_field(&entry, "name")?.extract()?); } } } @@ -125,12 +126,12 @@ pub(crate) fn sql_ref_key<'py>( let resource_type: Bound<'py, PyAny> = py .import(COMPILE_TYPES_MODULE)? .getattr("CompiledResourceType")? - .call1((reference.getattr("kind")?.getattr("value")?,))?; + .call1((native_field(reference, "kind")?.getattr("value")?,))?; keyword_call( ®istry::class(py, RecordId::CompiledObjectKey)?, &[ ("resource_type", resource_type), - ("name", reference.getattr("name")?), + ("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 2f4b5e27eb..704ef6c721 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/graph/keys.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/graph/keys.rs @@ -1,5 +1,6 @@ //! `CompiledObjectKey`s for native `(resource type, name)` pairs, converted once per pair. +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}; @@ -55,7 +56,7 @@ impl<'py> KeyCache<'py> { /// The native `(resource type, name)` pair of a `CompiledObjectKey`. pub(crate) fn native_key(key: &Bound<'_, PyAny>) -> PyResult<(String, String)> { Ok(( - key.getattr("resource_type")?.str()?.extract()?, - key.getattr("name")?.extract()?, + native_field(key, "resource_type")?.str()?.extract()?, + native_field(key, "name")?.extract()?, )) } 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 d08a2c659b..3127c88639 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/graph/python_lifecycle.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/graph/python_lifecycle.rs @@ -2,6 +2,7 @@ use std::collections::{BTreeSet, HashMap}; +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}; @@ -65,11 +66,12 @@ pub(crate) fn build_python_sql_run_lifecycle<'py>( } fn lifecycle_node(node: &Bound<'_, PyAny>) -> PyResult { - let loader: Bound<'_, PyAny> = node.getattr("loader")?; + let loader: Bound<'_, PyAny> = native_field(node, "loader")?; let external_loader: bool = !loader.is_none() - && loader.getattr("connection_mode")?.extract::()? == EXTERNAL_CONNECTION_MODE; + && native_field(&loader, "connection_mode")?.extract::()? + == EXTERNAL_CONNECTION_MODE; Ok(LifecycleNode { - kind: node.getattr("kind")?.extract()?, + kind: native_field(node, "kind")?.extract()?, external_loader, }) } 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 e011134253..a07b3878b6 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,6 +13,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; @@ -43,7 +44,7 @@ fn optional_value<'py>(value: &Bound<'py, PyAny>) -> PyResult> if value.is_none() { return Ok(value.clone()); } - value.getattr("value") + native_field(value, "value") } /// The kind-specific decorator configuration and metadata record of one discovered function. @@ -124,7 +125,7 @@ fn python_node<'py>( let identity: Bound<'py, PyAny> = call_with( &vocabulary.identity, &[ - ("kind", kind_member.getattr("value")?), + ("kind", native_field(&kind_member, "value")?), ("name", get("name")?), ("function", get("function")?), ("file_path", get("file_path")?), @@ -168,10 +169,10 @@ fn upstream_names( ) -> PyResult>> { let by_key: Bound<'_, PyDict> = PyDict::new(py); for node in nodes { - by_key.set_item(node.getattr("function")?, node.getattr("name")?)?; + by_key.set_item(native_field(node, "function")?, native_field(node, "name")?)?; } for node in nodes { - let name: Bound<'_, PyAny> = node.getattr("name")?; + let name: Bound<'_, PyAny> = native_field(node, "name")?; by_key.set_item( PyTuple::new(py, [PyString::new(py, NAME_KEY).into_any(), name.clone()])?, name, @@ -182,7 +183,7 @@ fn upstream_names( .iter() .map(|node| { let mut names: Vec = Vec::new(); - for dependency in node.getattr("depends_on")?.try_iter()? { + for dependency in native_field(node, "depends_on")?.try_iter()? { let dependency: Bound<'_, PyAny> = dependency?; if dependency.is_instance(sql_ref)? { continue; @@ -199,8 +200,8 @@ fn upstream_names( fn inventory_node(node: &Bound<'_, PyAny>, upstream: Vec) -> PyResult { Ok(InventoryNode { - name: node.getattr("name")?.extract()?, - tags: node.getattr("tags")?.extract()?, + name: native_field(node, "name")?.extract()?, + tags: native_field(node, "tags")?.extract()?, relative_path: node .getattr("relative_path")? .call_method0("as_posix")? @@ -215,9 +216,9 @@ pub(crate) fn python_node_graph<'py>( ) -> PyResult> { let py: Python<'py> = discovered_inputs.py(); let providers: Bound<'py, PyDict> = PyDict::new(py); - for provider in discovered_inputs.getattr("providers")?.try_iter()? { + for provider in native_field(discovered_inputs, "providers")?.try_iter()? { let provider: Bound<'py, PyAny> = provider?; - providers.set_item(provider.getattr("name")?, provider)?; + providers.set_item(native_field(&provider, "name")?, provider)?; } let vocabulary: Vocabulary<'py> = Vocabulary { node: registry::class(py, RecordId::DiscoveredPythonNode)?, @@ -291,8 +292,8 @@ fn graph_record<'py>( let by_name: Bound<'py, PyDict> = PyDict::new(py); let by_selector: Bound<'py, PyDict> = PyDict::new(py); for node in nodes { - let name: Bound<'py, PyAny> = node.getattr("name")?; - let kind: String = node.getattr("kind")?.getattr("value")?.extract()?; + let name: Bound<'py, PyAny> = native_field(node, "name")?; + let kind: String = native_field(node, "kind")?.getattr("value")?.extract()?; by_name.set_item(&name, node)?; by_selector.set_item(format!("{kind}:{}", name.str()?), node)?; } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/graph/python_selection.rs b/crates/sqlbuild-python/src/bindings/_helpers/graph/python_selection.rs index 851997d86b..a0761289a9 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/graph/python_selection.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/graph/python_selection.rs @@ -18,6 +18,7 @@ use sqlbuild_analysis::python_selection::models::{ use crate::bindings::_helpers::boundary::panics::compiler_guard; use crate::bindings::_helpers::graph::project_graph::NativeProjectGraph; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::ErrorId; use crate::bindings::_helpers::records::registry; @@ -63,16 +64,18 @@ fn name_rows(mapping: &Bound<'_, PyAny>) -> PyResult { fn selection_node(node: &Bound<'_, PyAny>) -> PyResult { let mut sql_deps: Vec = Vec::new(); - for reference in node.getattr("sql_deps")?.try_iter()? { + for reference in native_field(node, "sql_deps")?.try_iter()? { let reference: Bound<'_, PyAny> = reference?; sql_deps.push(( - reference.getattr("kind")?.getattr("value")?.extract()?, - reference.getattr("name")?.extract()?, + native_field(&reference, "kind")? + .getattr("value")? + .extract()?, + native_field(&reference, "name")?.extract()?, )); } Ok(PythonSelectionNode { - name: node.getattr("name")?.extract()?, - kind: node.getattr("kind")?.getattr("value")?.extract()?, + name: native_field(node, "name")?.extract()?, + kind: native_field(node, "kind")?.getattr("value")?.extract()?, sql_deps, }) } @@ -85,9 +88,9 @@ fn selection_graph(graph: &Bound<'_, PyAny>) -> PyResult { .try_iter()? .map(|node| selection_node(&node?)) .collect::>()?, - upstream: name_rows(&graph.getattr("upstream_deps")?)?, - downstream: name_rows(&graph.getattr("downstream_deps")?)?, - tags: name_rows(&graph.getattr("tag_index")?)?, + upstream: name_rows(&native_field(graph, "upstream_deps")?)?, + downstream: name_rows(&native_field(graph, "downstream_deps")?)?, + tags: name_rows(&native_field(graph, "tag_index")?)?, paths: graph .getattr("path_index")? .call_method0("items")? @@ -106,7 +109,7 @@ fn project_sources(project_graph: &Bound<'_, PyAny>) -> PyResult = source?; Ok(( - source.getattr("name")?.extract()?, + native_field(&source, "name")?.extract()?, source .getattr("source_entry")? .getattr("loader")? @@ -165,7 +168,8 @@ pub(crate) fn resolve_python_sql_run_selection( exclude: Vec, ) -> PyResult<(Vec, Vec)> { let py: Python<'_> = project_graph.py(); - let native: Bound<'_, NativeProjectGraph> = project_graph.getattr("native")?.downcast_into()?; + let native: Bound<'_, NativeProjectGraph> = + native_field(project_graph, "native")?.downcast_into()?; let sources: Vec<(String, Option)> = project_sources(project_graph)?; let nodes: PythonSelectionGraph = selection_graph(python_graph)?; let selection: RunSelection = compiler_guard(|| { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/graph/views.rs b/crates/sqlbuild-python/src/bindings/_helpers/graph/views.rs index b3d16357ad..af037455e5 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/graph/views.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/graph/views.rs @@ -12,6 +12,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; @@ -102,7 +103,8 @@ pub(crate) fn project_lineage_views<'py>( py: Python<'py>, project: &Bound<'py, PyAny>, ) -> PyResult> { - let graph: PyRef<'py, NativeProjectGraph> = project.getattr("lineage_graph")?.extract()?; + let graph: PyRef<'py, NativeProjectGraph> = + native_field(project, "lineage_graph")?.extract()?; graph_views(py, graph.graph()) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/hand_built/compiled_model.rs b/crates/sqlbuild-python/src/bindings/_helpers/hand_built/compiled_model.rs index 556a280393..f05a66d68b 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/hand_built/compiled_model.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/hand_built/compiled_model.rs @@ -2,6 +2,7 @@ use crate::bindings::_helpers::native_host::facade::FacadeHost; use crate::bindings::_helpers::native_host::host; +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 pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; @@ -17,8 +18,8 @@ pub(crate) fn compiled_model<'py>( macro_deps: &Object<'py>, ) -> PyResult> { let py: Python<'py> = model_input.py(); - let model_file: Object<'py> = model_input.getattr("model_file")?; - let model_name: Object<'py> = model_file.getattr("file_path")?.getattr("stem")?; + let model_file: Object<'py> = native_field(model_input, "model_file")?; + let model_name: Object<'py> = native_field(&model_file, "file_path")?.getattr("stem")?; let none: Object<'py> = py.None().into_bound(py); let mut inferred: Object<'py> = none.clone(); let mut lineage: Object<'py> = none.clone(); @@ -27,8 +28,8 @@ pub(crate) fn compiled_model<'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((analysis.getattr("standalone_proof")?,))?; - let session: Object<'py> = analysis.getattr("session")?; + .call1((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()?; let arguments: Bound<'py, PyDict> = PyDict::new(py); @@ -37,21 +38,21 @@ pub(crate) fn compiled_model<'py>( arguments.set_item("placeholders", &none)?; let outcome: Object<'py> = registry::native(py, NativeAttr::SessionOutcomeAnalysis)?.call((), Some(&arguments))?; - let result: Object<'py> = outcome.getattr("polyglot_analysis")?; - if result.getattr("analysis_succeeded")?.is_truthy()? { - inferred = result.getattr("columns")?; - lineage = result.getattr("lineage_columns")?; - has_star = result.getattr("has_star")?; - star_resolved = result.getattr("star_resolved")?; + let result: Object<'py> = native_field(&outcome, "polyglot_analysis")?; + if native_field(&result, "analysis_succeeded")?.is_truthy()? { + inferred = native_field(&result, "columns")?; + lineage = native_field(&result, "lineage_columns")?; + has_star = native_field(&result, "has_star")?; + star_resolved = native_field(&result, "star_resolved")?; } else { - inferred = fallback_columns(&analysis.getattr("fallback")?)?; + inferred = fallback_columns(&native_field(analysis, "fallback")?)?; } - let session_proof: Object<'py> = outcome.getattr("dynamic_column_contract")?; + let session_proof: Object<'py> = native_field(&outcome, "dynamic_column_contract")?; if session_proof.is_truthy()? { proof = session_proof; } - validated = result.getattr("binding_validated")?; - let cleaned: Object<'py> = outcome.getattr("cleaned_sql")?; + validated = native_field(&result, "binding_validated")?; + let cleaned: Object<'py> = native_field(&outcome, "cleaned_sql")?; let cleaned: Object<'py> = if cleaned.is_truthy()? { cleaned } else { @@ -59,12 +60,12 @@ pub(crate) fn compiled_model<'py>( }; diagnostics = located( model_input, - &result.getattr("binding_diagnostics")?, + &native_field(&result, "binding_diagnostics")?, &cleaned, )?; } - if !proof.is_none() && proof.getattr("output_proven")?.is_truthy()? { - inferred = proof.getattr("fixed_columns")?; + if !proof.is_none() && native_field(&proof, "output_proven")?.is_truthy()? { + inferred = native_field(&proof, "fixed_columns")?; has_star = true_object(py); } let resource_type: Object<'py> = host::entry(py, FacadeHost::CompiledResourceType)?; @@ -80,11 +81,11 @@ pub(crate) fn compiled_model<'py>( ], )?); } - let config: Object<'py> = model_input.getattr("config")?; + let config: Object<'py> = native_field(model_input, "config")?; let key: Object<'py> = call( &key_class, &[ - ("resource_type", resource_type.getattr("MODEL")?), + ("resource_type", native_field(&resource_type, "MODEL")?), ("name", model_name.clone()), ], )?; @@ -94,42 +95,45 @@ pub(crate) fn compiled_model<'py>( ("key", key), ("deps", PyTuple::new(py, dependency_keys)?.into_any()), ("name", model_name.clone()), - ("relative_path", model_file.getattr("relative_path")?), - ("query_sql", model_input.getattr("query_sql")?), + ("relative_path", native_field(&model_file, "relative_path")?), + ("query_sql", native_field(model_input, "query_sql")?), ("config", config.clone()), ("destination", destination(&config, &model_name)?), - ("references", model_input.getattr("references")?), - ("schema_entry", model_input.getattr("schema_entry")?), + ("references", native_field(model_input, "references")?), + ("schema_entry", native_field(model_input, "schema_entry")?), ("inferred_columns", inferred), ("fast_lineage_columns", lineage), ("fast_lineage_has_star", has_star), ("fast_lineage_star_resolved", star_resolved), - ("authored_sql", model_file.getattr("contents")?), - ("authored_query_sql", model_file.getattr("query_sql")?), + ("authored_sql", native_field(&model_file, "contents")?), + ( + "authored_query_sql", + native_field(&model_file, "query_sql")?, + ), ( "output_column_locations", - model_file.getattr("output_column_locations")?, + native_field(&model_file, "output_column_locations")?, ), ( "extract_implicit_alias_columns", - model_file.getattr("extract_implicit_alias_columns")?, + native_field(&model_file, "extract_implicit_alias_columns")?, ), ("macro_deps", macro_deps.clone()), ( "enum_declarations", - model_input.getattr("enum_declarations")?, + native_field(model_input, "enum_declarations")?, ), ( "constant_declarations", - model_input.getattr("constant_declarations")?, + native_field(model_input, "constant_declarations")?, ), - ("enum_columns", model_input.getattr("enum_columns")?), + ("enum_columns", native_field(model_input, "enum_columns")?), ("binding_diagnostics", diagnostics), ("binding_validated", validated), ("dynamic_column_contract", proof), ( "rejected_sql_analysis_opt_out", - model_input.getattr("rejected_sql_analysis_opt_out")?, + native_field(model_input, "rejected_sql_analysis_opt_out")?, ), ], ) @@ -146,7 +150,7 @@ fn true_object(py: Python<'_>) -> Object<'_> { /// A model's configured physical and logical destination. fn destination<'py>(config: &Object<'py>, model_name: &Object<'py>) -> PyResult> { let py: Python<'py> = config.py(); - let values: Object<'py> = config.getattr("values")?; + let values: Object<'py> = native_field(config, "values")?; let text = |key: &str| -> PyResult>> { let value: Object<'py> = values.call_method1("get", (key,))?; Ok(value.is_instance_of::().then_some(value)) @@ -162,8 +166,11 @@ fn destination<'py>(config: &Object<'py>, model_name: &Object<'py>) -> PyResult< ("schema", text("schema")?.unwrap_or_else(|| none.clone())), ("name", text("alias")?.unwrap_or_else(|| model_name.clone())), ("qualified_name", none.clone()), - ("logical_schema", config.getattr("logical_schema")?), - ("logical_database", config.getattr("logical_database")?), + ("logical_schema", native_field(config, "logical_schema")?), + ( + "logical_database", + native_field(config, "logical_database")?, + ), ], ) } @@ -201,8 +208,8 @@ fn located<'py>( if !diagnostics.is_truthy()? { return Ok(PyTuple::empty(py).into_any()); } - let model_file: Object<'py> = model_input.getattr("model_file")?; - let path: Object<'py> = model_file.getattr("relative_path")?; + let model_file: Object<'py> = native_field(model_input, "model_file")?; + let path: Object<'py> = native_field(&model_file, "relative_path")?; let mut rows: Vec> = Vec::new(); for item in diagnostics.try_iter()? { let item: Object<'py> = item?; @@ -217,18 +224,18 @@ fn located<'py>( let texts: Object<'py> = PyTuple::new( py, [ - model_file.getattr("contents")?, - model_file.getattr("query_sql")?, + native_field(&model_file, "contents")?, + native_field(&model_file, "query_sql")?, cleaned.clone(), ], )? .into_any(); - let expansion: Object<'py> = model_input.getattr("sql_expansion")?; + let expansion: Object<'py> = native_field(model_input, "sql_expansion")?; let expansion_row: Object<'py> = if expansion.is_none() { expansion } else { let mut passes: Vec> = Vec::new(); - for spans in expansion.getattr("passes")?.try_iter()? { + for spans in native_field(&expansion, "passes")?.try_iter()? { let mut rows: Vec> = Vec::new(); for span in spans?.try_iter()? { let span: Object<'py> = span?; @@ -244,7 +251,7 @@ fn located<'py>( PyTuple::new( py, [ - expansion.getattr("expanded_sql")?, + native_field(&expansion, "expanded_sql")?, PyList::new(py, passes)?.into_any(), ], )? @@ -256,7 +263,7 @@ fn located<'py>( 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> = host::entry(py, FacadeHost::CompiledResourceType)?.getattr("MODEL")?; - let name: Object<'py> = model_file.getattr("file_path")?.getattr("stem")?; + let name: Object<'py> = native_field(&model_file, "file_path")?.getattr("stem")?; let mut built: Vec> = Vec::new(); for row in located.try_iter()? { let (code, message, severity, line, column, span): ( 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 2e3bc7a43a..3323d9c927 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/init_command/command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/init_command/command.rs @@ -16,6 +16,7 @@ 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::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::registry; @@ -48,7 +49,8 @@ fn run_init_command(py: Python<'_>, project_dir: Option>) -> Py { return Err(exists_error(py)); } - let project_name: String = init_project_name(&base_dir.getattr("name")?.extract::()?); + let project_name: String = + 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( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/init_command/scaffold.rs b/crates/sqlbuild-python/src/bindings/_helpers/init_command/scaffold.rs index 5aaaaab000..8e969b0c1c 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/init_command/scaffold.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/init_command/scaffold.rs @@ -4,6 +4,7 @@ use std::fs::{self, OpenOptions}; use std::io::{self, ErrorKind}; use std::path::Path; +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyErr, PyResult, Python}; use sqlbuild_core::constants::PROJECT_CONFIG_FILENAME as PROJECT_FILE; use sqlbuild_core::init_command::constants::{GITKEEP_FILE, SCAFFOLD_DIRECTORIES}; @@ -17,7 +18,7 @@ fn os_error(py: Python<'_>, error: &io::Error, path: &Path) -> PyErr { .strip_suffix(&format!(" (os error {errno})")) .unwrap_or(&described); py.import("builtins").and_then(|builtins| { - builtins.getattr("OSError")?.call1(( + native_field(&builtins, "OSError")?.call1(( errno, strerror, path.to_string_lossy().into_owned(), @@ -26,7 +27,7 @@ fn os_error(py: Python<'_>, error: &io::Error, path: &Path) -> PyErr { } None => py .import("builtins") - .and_then(|builtins| builtins.getattr("OSError")?.call1((error.to_string(),))), + .and_then(|builtins| native_field(&builtins, "OSError")?.call1((error.to_string(),))), }; match raised { Ok(raised) => PyErr::from_value(raised), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_classification.rs b/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_classification.rs index 2e7bc07f8d..9308ed6926 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_classification.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_classification.rs @@ -26,6 +26,7 @@ use crate::bindings::_helpers::janitor::janitor_keys::{ }; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::planner_scenario::artifacts::is_scenario_artifact_physical_name; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::relation_naming::fitting::shared_error; /// Protection rules for one plan. @@ -458,7 +459,10 @@ pub(crate) fn plan_archives<'py>( archive_candidates.push(record); if same_run_expiry { same_run.push(archived_relation( - (&archive_key, candidate.relation.getattr("relation_type")?), + ( + &archive_key, + native_field(&candidate.relation, "relation_type")?, + ), (&archived_at, &expires_at), Some(&candidate.key), )?); @@ -502,7 +506,7 @@ pub(crate) fn plan_archives<'py>( let parsed_at: Bound<'py, PyAny> = utc_datetime(py, parsed.archived_at)?; let parsed_expiry: Bound<'py, PyAny> = parsed_at.add(&retention)?; let archived: Bound<'py, PyAny> = archived_relation( - (&key, relation.getattr("relation_type")?), + (&key, native_field(relation, "relation_type")?), (&parsed_at, &parsed_expiry), None, )?; 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 97cacbf43f..35c5a7f9f0 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -77,31 +78,31 @@ fn flush(py: Python<'_>) -> PyResult<()> { /// Discover the project and resolve the target and output context. fn invocation<'py>(py: Python<'py>, request: &Bound<'py, PyAny>) -> PyResult> { - let project_dir: Bound<'py, PyAny> = match request.getattr("project_dir")? { + let project_dir: Bound<'py, PyAny> = match native_field(request, "project_dir")? { directory if !directory.is_none() => directory, _ => py.import("pathlib")?.getattr("Path")?.call_method0("cwd")?, }; let discovered: Bound<'py, PyAny> = discover_project_inputs(py, Some(keywords(py, &[("project_dir", &project_dir)])?))?; - let as_target: Bound<'py, PyAny> = request.getattr("as_target")?; + let as_target: Bound<'py, PyAny> = native_field(request, "as_target")?; validate_preview_target( py, discovered.clone(), as_target.extract::>()?, COMMAND_NAME.to_owned(), )?; - let selected_target: Bound<'py, PyAny> = request.getattr("selected_target")?; + let selected_target: Bound<'py, PyAny> = native_field(request, "selected_target")?; let active_target: Bound<'py, PyAny> = object( py, resolve_target_name( py, - &discovered.getattr("project_config")?, - &discovered.getattr("local_config")?, + &native_field(&discovered, "project_config")?, + &native_field(&discovered, "local_config")?, &selected_target, )?, )?; let use_color: bool = - !request.getattr("no_color")?.extract::()? && stdout_supports_color(py)?; + !native_field(request, "no_color")?.extract::()? && stdout_supports_color(py)?; let options: Bound<'py, PyDict> = keywords( py, &[ @@ -123,16 +124,16 @@ fn settings<'py>( janitor: &Bound<'py, PyAny>, ) -> PyResult> { let retention_days: Bound<'py, PyAny> = resolve_retention_days( - request.getattr("retention_days")?.extract()?, - janitor.getattr("retention_days")?.extract()?, + native_field(request, "retention_days")?.extract()?, + native_field(janitor, "retention_days")?.extract()?, ) .into_pyobject(py)? .into_any(); validate_janitor_settings(retention_days.extract()?, 0) .map_err(|error| settings_error(py, error))?; - let requested: Bound<'py, PyAny> = request.getattr("direct_state_history_versions")?; + let requested: Bound<'py, PyAny> = native_field(request, "direct_state_history_versions")?; let history: Bound<'py, PyAny> = if requested.is_none() { - janitor.getattr("direct_state_history_versions")? + native_field(janitor, "direct_state_history_versions")? } else { requested }; @@ -144,11 +145,11 @@ fn settings<'py>( ("direct_state_history_versions", &history), ( "archive_retention_days", - &janitor.getattr("archive_retention_days")?, + &native_field(janitor, "archive_retention_days")?, ), ( "drop_old_name_views", - &request.getattr("drop_old_name_views")?, + &native_field(request, "drop_old_name_views")?, ), ], )?; @@ -177,16 +178,16 @@ fn clean( (compiled, connection): (&JanitorCompiled<'_>, &Bound<'_, PyAny>), ) -> PyResult { let style: CliStyle = CliStyle { - use_color: invocation.getattr("use_color")?.extract()?, + use_color: native_field(invocation, "use_color")?.extract()?, }; let planning: Bound<'_, PyAny> = build_janitor_execution_plan((invocation, settings), compiled, connection)?; let plan: JanitorPlanText = janitor_plan_text(&planning)?; write(py, &render_janitor_plan(&plan, style))?; let blocked: i32 = i32::from(!plan.blocked_schemas.is_empty()); - let as_target: Option = invocation.getattr("as_target")?.extract()?; + let as_target: Option = native_field(invocation, "as_target")?.extract()?; if let Some(as_target) = as_target { - let active: Option = invocation.getattr("active_target_name")?.extract()?; + let active: Option = native_field(invocation, "active_target_name")?.extract()?; write( py, &janitor_preview_complete_text(&as_target, active.as_deref(), style), @@ -199,7 +200,7 @@ fn clean( if !janitor_plan_has_work(&plan) { return Ok(0); } - if !request.getattr("auto_approve")?.extract::()? && !confirmed(py, &plan)? { + if !native_field(request, "auto_approve")?.extract::()? && !confirmed(py, &plan)? { write(py, "Janitor cancelled.\n")?; return Ok(1); } @@ -217,20 +218,20 @@ fn clean( fn run_janitor_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { let invocation: Bound<'_, PyAny> = invocation(py, request)?; let style: CliStyle = CliStyle { - use_color: invocation.getattr("use_color")?.extract()?, + use_color: native_field(&invocation, "use_color")?.extract()?, }; let janitor: Bound<'_, PyAny> = invocation .getattr("discovered_inputs")? .getattr("project_config")? .getattr("janitor")?; - if !janitor.getattr("enabled")?.is_truthy()? { + if !native_field(&janitor, "enabled")?.is_truthy()? { write(py, &janitor_disabled_text(style))?; return Ok(0); } let settings: Bound<'_, PyAny> = settings(py, request, &janitor)?; - let as_target: Option = invocation.getattr("as_target")?.extract()?; + let as_target: Option = native_field(&invocation, "as_target")?.extract()?; if let Some(as_target) = &as_target { - let active: Option = invocation.getattr("active_target_name")?.extract()?; + let active: Option = native_field(&invocation, "active_target_name")?.extract()?; write( py, &janitor_preview_start_text(as_target, active.as_deref()), 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 f428c2e97f..12feaa347e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_facts.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_facts.rs @@ -21,6 +21,7 @@ const EXECUTION_LOGGER: &str = "sqlbuild.execution"; 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::fields::field as native_field; use sqlbuild_core::constants::FINGERPRINT_TABLE_NAME; use sqlbuild_core::constants::SOURCE_FRESHNESS_TABLE_NAME; @@ -92,7 +93,7 @@ fn destination_keys<'py>( let mut keys: Vec<(Key, Bound<'py, PyAny>)> = Vec::new(); for node in project.getattr(field)?.try_iter()? { let node: Bound<'py, PyAny> = node?; - keys.push((Key::of(&node.getattr("destination")?)?, node)); + keys.push((Key::of(&native_field(&node, "destination")?)?, node)); } Ok(keys) } @@ -130,10 +131,10 @@ fn source_schemas( (default_database, default_schema): (Option, Option), ) -> PyResult)>> { let mut schemas: Vec<(SchemaKey, BTreeSet)> = Vec::new(); - for source in project.getattr("sources")?.try_iter()? { + for source in native_field(project, "sources")?.try_iter()? { let source: Bound<'_, PyAny> = source?; - let entry: Bound<'_, PyAny> = source.getattr("source_entry")?; - if !entry.getattr("expression")?.is_none() { + let entry: Bound<'_, PyAny> = native_field(&source, "source_entry")?; + if !native_field(&entry, "expression")?.is_none() { continue; } let database: Option = entry @@ -144,7 +145,7 @@ fn source_schemas( .getattr("schema")? .extract::>()? .or_else(|| default_schema.clone()); - let name: String = source.getattr("name")?.extract()?; + let name: String = native_field(&source, "name")?.extract()?; let key: SchemaKey = (database, schema); match schemas.iter_mut().find(|(listed, _)| *listed == key) { Some((_, names)) => { @@ -193,8 +194,8 @@ fn list_target_schema_relations<'py>( for relation in listed.try_iter()? { let relation: Bound<'py, PyAny> = relation?; let folded: SchemaKey = folded_schema(&( - relation.getattr("database")?.extract()?, - relation.getattr("schema")?.extract()?, + native_field(&relation, "database")?.extract()?, + native_field(&relation, "schema")?.extract()?, )); let Some(target) = target_schemas .iter() @@ -254,7 +255,7 @@ fn lookup_exists( ) -> PyResult { let py: Python<'_> = lookup.py(); call_with( - &lookup.getattr("exists")?, + &native_field(lookup, "exists")?, &[ ("database", optional(py, database)?), ("schema", text(py, schema)), @@ -297,7 +298,7 @@ fn tracked_relation_keys( py, &[ ("connection", warehouse.connection.clone()), - ("execute", warehouse.adapter.getattr("execute")?), + ("execute", native_field(&warehouse.adapter, "execute")?), ( "table_exists", PyBool::new(py, exists).to_owned().into_any(), @@ -306,7 +307,7 @@ fn tracked_relation_keys( ("schema", text(py, schema)), ( "render_qualified_name", - warehouse.adapter.getattr("render_qualified_name")?, + native_field(&warehouse.adapter, "render_qualified_name")?, ), ( "render_read_latest_sql", @@ -348,14 +349,16 @@ fn tracked_relation_keys( .try_iter()? { let fingerprint: Bound<'_, PyAny> = fingerprint?; - let target_schema: Option = fingerprint.getattr("target_schema")?.extract()?; - let target_name: Option = fingerprint.getattr("target_name")?.extract()?; + let target_schema: Option = + native_field(&fingerprint, "target_schema")?.extract()?; + let target_name: Option = + native_field(&fingerprint, "target_name")?.extract()?; let (Some(target_schema), Some(target_name)) = (target_schema, target_name) else { continue; }; tracked.insert( Key { - database: fingerprint.getattr("target_database")?.extract()?, + database: native_field(&fingerprint, "target_database")?.extract()?, schema: Some(target_schema), name: target_name, } @@ -476,7 +479,7 @@ pub(crate) fn query_diff_artifact_candidates<'py>( let mut skipped: Vec> = Vec::new(); for (database, schema) in sorted { let inspection: Bound<'py, PyAny> = call_with( - &lifecycle.getattr("inspect")?, + &native_field(&lifecycle, "inspect")?, &[ ("adapter", warehouse.adapter.clone()), ("connection", warehouse.connection.clone()), @@ -485,7 +488,7 @@ pub(crate) fn query_diff_artifact_candidates<'py>( ("now", now.clone()), ], )?; - for artifact in inspection.getattr("expired")?.try_iter()? { + for artifact in native_field(&inspection, "expired")?.try_iter()? { let key: Key = Key::of(&artifact?)?; candidates.push(record( py, @@ -493,7 +496,7 @@ pub(crate) fn query_diff_artifact_candidates<'py>( &[("key", key.to_python(py)?)], )?); } - for relation in inspection.getattr("untracked")?.try_iter()? { + for relation in native_field(&inspection, "untracked")?.try_iter()? { let relation: Bound<'py, PyAny> = relation?; skipped.push(skipped_relation( &Key::of(&relation)?, 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 e60fdff466..31d2ba4bfd 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_keys.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_keys.rs @@ -9,6 +9,7 @@ use sqlbuild_analysis::janitor_planning::models::ArchiveTimestamp; 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::fields::field as native_field; const DATETIME_MODULE: &str = "datetime"; @@ -29,9 +30,9 @@ impl Key { /// The key of an object with `database`, `schema` and `name` attributes. pub(crate) fn of(item: &Bound<'_, PyAny>) -> PyResult { Ok(Self { - database: item.getattr("database")?.extract()?, - schema: item.getattr("schema")?.extract()?, - name: item.getattr("name")?.extract()?, + database: native_field(item, "database")?.extract()?, + schema: native_field(item, "schema")?.extract()?, + name: native_field(item, "name")?.extract()?, }) } @@ -166,8 +167,8 @@ pub(crate) fn days<'py>(py: Python<'py>, days: i64) -> PyResult(value: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = value.py(); - if value.getattr("tzinfo")?.is_none() { - return call_with(&value.getattr("replace")?, &[("tzinfo", utc(py)?)]); + if native_field(value, "tzinfo")?.is_none() { + return call_with(&native_field(value, "replace")?, &[("tzinfo", utc(py)?)]); } value.call_method1("astimezone", (utc(py)?,)) } @@ -195,7 +196,7 @@ pub(crate) fn relation_age_timestamp<'py>( pub(crate) fn archive_timestamp<'py>(value: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = value.py(); call_with( - &aware_utc(value)?.getattr("replace")?, + &native_field(&aware_utc(value)?, "replace")?, &[("microsecond", 0_i64.into_pyobject(py)?.into_any())], ) } @@ -203,12 +204,12 @@ pub(crate) fn archive_timestamp<'py>(value: &Bound<'py, PyAny>) -> PyResult) -> PyResult { Ok(ArchiveTimestamp { - year: value.getattr("year")?.extract()?, - month: value.getattr("month")?.extract()?, - day: value.getattr("day")?.extract()?, - hour: value.getattr("hour")?.extract()?, - minute: value.getattr("minute")?.extract()?, - second: value.getattr("second")?.extract()?, + year: native_field(value, "year")?.extract()?, + month: native_field(value, "month")?.extract()?, + day: native_field(value, "day")?.extract()?, + hour: native_field(value, "hour")?.extract()?, + minute: native_field(value, "minute")?.extract()?, + second: native_field(value, "second")?.extract()?, }) } 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 fbc0549abc..d2110ed1d0 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 @@ -23,6 +23,7 @@ use crate::bindings::_helpers::state_rows::migration_io::{ const IS_COMPATIBILITY_VIEW: ExecutorHost = ExecutorHost::IsCompatibilityView; 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; @@ -43,7 +44,7 @@ fn status<'py>( history: &Bound<'py, PyAny>, now: &Bound<'py, PyAny>, ) -> PyResult> { - call_with(&history.getattr("status")?, &[("now", now.clone())]) + call_with(&native_field(history, "status")?, &[("now", now.clone())]) } fn status_is(py: Python<'_>, value: &Bound<'_, PyAny>, member: &str) -> PyResult { @@ -87,13 +88,13 @@ fn read_histories<'py>( py, &[ ("connection", warehouse.connection.clone()), - ("execute", warehouse.adapter.getattr("execute")?), + ("execute", native_field(&warehouse.adapter, "execute")?), ("database", optional(py, database.as_deref())?), ("schema", text(py, &schema)), ("stored_columns", stored_columns), ( "render_qualified_name", - warehouse.adapter.getattr("render_qualified_name")?, + native_field(&warehouse.adapter, "render_qualified_name")?, ), ], )?))?; @@ -105,7 +106,7 @@ fn read_histories<'py>( } fn old_key(history: &Bound<'_, PyAny>) -> PyResult { - Key::of(&history.getattr("old")?) + Key::of(&native_field(history, "old")?) } /// Each history's listed relation at its old name, listing only schemas not already scanned. @@ -200,11 +201,11 @@ fn view<'py>( claimed_by: Option>, ) -> PyResult> { let py: Python<'py> = history.py(); - let created: Bound<'py, PyAny> = history.getattr("created")?; + let created: Bound<'py, PyAny> = native_field(history, "created")?; let expires_at: Bound<'py, PyAny> = if created.is_none() { py.None().into_bound(py) } else { - created.getattr("expires_at")? + native_field(&created, "expires_at")? }; let mut pairs: Vec<(&str, Bound<'py, PyAny>)> = vec![ ("key", key.to_python(py)?), @@ -257,12 +258,13 @@ pub(crate) fn plan_old_name_views<'py>( (early_drops, now): (&[String], &Bound<'py, PyAny>), ) -> PyResult> { let py: Python<'py> = warehouse.py(); - let target_name: Option = project.getattr("effective_target_name")?.extract()?; + let target_name: Option = native_field(project, "effective_target_name")?.extract()?; let destinations: Bound<'py, PyDict> = project_destinations(project)?; let mut histories: Vec> = Vec::new(); for history in read_histories(warehouse, managed, relations)? { - let recorded: Option = - history.getattr("move")?.getattr("target_name")?.extract()?; + let recorded: Option = native_field(&history, "move")? + .getattr("target_name")? + .extract()?; if recorded.is_some() && recorded != target_name { continue; } 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 9aaeeeac51..f505a54ff6 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_phases.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_phases.rs @@ -16,6 +16,7 @@ use crate::bindings::_helpers::janitor::janitor_planning::{ }; 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::{ErrorId, NativeAttr}; use crate::bindings::_helpers::records::registry; @@ -42,7 +43,7 @@ fn stdout(py: Python<'_>) -> PyResult> { } fn use_color(invocation: &Bound<'_, PyAny>) -> PyResult { - invocation.getattr("use_color")?.extract() + native_field(invocation, "use_color")?.extract() } /// `OperationLifecycle(operation_kind=..., operation_name=...)`. @@ -59,10 +60,10 @@ pub(crate) fn compile_janitor_project<'py>( ) -> PyResult> { let py: Python<'py> = invocation.py(); let context: Bound<'py, PyAny> = resolve_adapter_connection_context( - &invocation.getattr("discovered_inputs")?, - &invocation.getattr("effective_project_dir")?, + &native_field(invocation, "discovered_inputs")?, + &native_field(invocation, "effective_project_dir")?, ( - invocation.getattr("selected_target")?, + native_field(invocation, "selected_target")?, py.None().into_bound(py), ), true, @@ -80,9 +81,9 @@ pub(crate) fn compile_janitor_project<'py>( ), ], )?; - let as_target: Bound<'py, PyAny> = invocation.getattr("as_target")?; + let as_target: Bound<'py, PyAny> = native_field(invocation, "as_target")?; let target: Bound<'py, PyAny> = if as_target.is_none() { - invocation.getattr("selected_target")? + native_field(invocation, "selected_target")? } else { as_target }; @@ -96,11 +97,14 @@ pub(crate) fn compile_janitor_project<'py>( &[ ( "discovered_inputs", - invocation.getattr("discovered_inputs")?, + native_field(invocation, "discovered_inputs")?, ), - ("adapter", context.getattr("adapter")?), + ("adapter", native_field(&context, "adapter")?), ("selected_target", target), - ("resolved_connection", context.getattr("connection_config")?), + ( + "resolved_connection", + native_field(&context, "connection_config")?, + ), ], )?), )?; @@ -114,10 +118,10 @@ pub(crate) fn compile_janitor_project<'py>( Ok(project) })?; Ok(JanitorCompiled { - adapter_name: context.getattr("adapter_name")?, - adapter: context.getattr("adapter")?, + adapter_name: native_field(&context, "adapter_name")?, + adapter: native_field(&context, "adapter")?, project, - connection_config: context.getattr("connection_config")?, + connection_config: native_field(&context, "connection_config")?, }) } @@ -180,7 +184,9 @@ pub(crate) fn close_janitor_warehouse( } fn candidate_count(plan: &Bound<'_, PyAny>) -> PyResult { - let mut count: usize = plan.getattr("old_name_views")?.getattr("drops")?.len()?; + let mut count: usize = native_field(plan, "old_name_views")? + .getattr("drops")? + .len()?; for field in CANDIDATE_FIELDS { count += plan.getattr(field)?.len()?; } @@ -228,9 +234,10 @@ pub(crate) fn build_janitor_execution_plan<'py>( }, ), &JanitorSettings { - retention_days: settings.getattr("retention_days")?.extract()?, - delete_tracked_only: janitor.getattr("delete_tracked_only")?.is_truthy()?, - exclude_patterns: janitor.getattr("exclude_patterns")?.extract()?, + retention_days: native_field(settings, "retention_days")?.extract()?, + delete_tracked_only: native_field(&janitor, "delete_tracked_only")? + .is_truthy()?, + exclude_patterns: native_field(&janitor, "exclude_patterns")?.extract()?, direct_mode: true, state_history_versions: settings .getattr("direct_state_history_versions")? 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 7b3b8d4f3b..7735ab251f 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_plan.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_plan.rs @@ -1,5 +1,6 @@ //! Display facts of the executor's janitor plan and execution result. +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult}; use pyo3::{pyfunction, wrap_pyfunction}; use sqlbuild_core::cli_text::models::CliStyle; @@ -18,7 +19,7 @@ fn display_name(item: &Bound<'_, PyAny>) -> PyResult { } fn key_name(item: &Bound<'_, PyAny>) -> PyResult { - display_name(&item.getattr("key")?) + display_name(&native_field(item, "key")?) } /// `format(value, pattern)`, as an f-string format spec renders a datetime. @@ -36,7 +37,9 @@ fn age_days(value: &Bound<'_, PyAny>, planned_at: &Bound<'_, PyAny>) -> PyResult if value.is_none() || planned_at.is_none() { return Ok(None); } - Ok(Some(planned_at.sub(value)?.getattr("days")?.extract()?)) + Ok(Some( + native_field(&planned_at.sub(value)?, "days")?.extract()?, + )) } fn names(items: &Bound<'_, PyAny>) -> PyResult> { @@ -47,13 +50,13 @@ fn archived( item: &Bound<'_, PyAny>, planned_at: &Bound<'_, PyAny>, ) -> PyResult { - let archived_at: Bound<'_, PyAny> = item.getattr("archived_at")?; + let archived_at: Bound<'_, PyAny> = native_field(item, "archived_at")?; Ok(ArchivedRelationText { name: key_name(item)?, - archived_in_run: !item.getattr("original_key")?.is_none(), + archived_in_run: !native_field(item, "original_key")?.is_none(), archived_at: formatted(&archived_at, TIMESTAMP_FORMAT)?, age_days: age_days(&archived_at, planned_at)?, - expires_at: formatted(&item.getattr("expires_at")?, TIMESTAMP_FORMAT)?, + expires_at: formatted(&native_field(item, "expires_at")?, TIMESTAMP_FORMAT)?, }) } @@ -71,11 +74,11 @@ fn old_name_views(views: &Bound<'_, PyAny>) -> PyResult> { let mut read: Vec = Vec::new(); for view in views.try_iter()? { let view: Bound<'_, PyAny> = view?; - let expires_at: Bound<'_, PyAny> = view.getattr("expires_at")?; - let drop_reason: Bound<'_, PyAny> = view.getattr("drop_reason")?; + let expires_at: Bound<'_, PyAny> = native_field(&view, "expires_at")?; + let drop_reason: Bound<'_, PyAny> = native_field(&view, "drop_reason")?; read.push(OldNameViewText { name: key_name(&view)?, - destination_model: view.getattr("destination_model")?.str()?.extract()?, + destination_model: native_field(&view, "destination_model")?.str()?.extract()?, expires_on: if expires_at.is_none() { None } else { @@ -86,8 +89,8 @@ fn old_name_views(views: &Bound<'_, PyAny>) -> PyResult> { } else { Some(drop_reason.str()?.extract()?) }, - occupied: view.getattr("occupied")?.is_truthy()?, - claimed_by: view.getattr("claimed_by")?.extract()?, + occupied: native_field(&view, "occupied")?.is_truthy()?, + claimed_by: native_field(&view, "claimed_by")?.extract()?, }); } Ok(read) @@ -99,8 +102,8 @@ fn source_schemas(schemas: &Bound<'_, PyAny>, blocked: bool) -> PyResult = schema?; let (suppressed_candidates, suppressed_archive_deletions) = if blocked { ( - names(&schema.getattr("suppressed_candidates")?)?, - names(&schema.getattr("suppressed_archive_deletions")?)?, + names(&native_field(&schema, "suppressed_candidates")?)?, + names(&native_field(&schema, "suppressed_archive_deletions")?)?, ) } else { (Vec::new(), Vec::new()) @@ -121,58 +124,58 @@ fn source_schemas(schemas: &Bound<'_, PyAny>, blocked: bool) -> PyResult) -> PyResult { - let planned_at: Bound<'_, PyAny> = plan.getattr("planned_at")?; + let planned_at: Bound<'_, PyAny> = native_field(plan, "planned_at")?; let mut archive_candidates: Vec = Vec::new(); - for candidate in plan.getattr("archive_candidates")?.try_iter()? { + for candidate in native_field(plan, "archive_candidates")?.try_iter()? { let candidate: Bound<'_, PyAny> = candidate?; - let expires_at: Bound<'_, PyAny> = candidate.getattr("expires_at")?; + let expires_at: Bound<'_, PyAny> = native_field(&candidate, "expires_at")?; archive_candidates.push(ArchiveCandidateText { name: key_name(&candidate)?, - archive_name: display_name(&candidate.getattr("archive_key")?)?, - age_days: age_days(&candidate.getattr("age_timestamp")?, &planned_at)?, - expires_now: expires_at.le(candidate.getattr("archived_at")?)?, + archive_name: display_name(&native_field(&candidate, "archive_key")?)?, + age_days: age_days(&native_field(&candidate, "age_timestamp")?, &planned_at)?, + expires_now: expires_at.le(native_field(&candidate, "archived_at")?)?, expires_at: formatted(&expires_at, TIMESTAMP_FORMAT)?, }); } let mut state_prunes: Vec<(String, i64)> = Vec::new(); - for candidate in plan.getattr("direct_state_prune_candidates")?.try_iter()? { + for candidate in native_field(plan, "direct_state_prune_candidates")?.try_iter()? { let candidate: Bound<'_, PyAny> = candidate?; state_prunes.push(( display_name(&candidate)?, - candidate.getattr("retain_versions")?.extract()?, + native_field(&candidate, "retain_versions")?.extract()?, )); } let mut skipped_relations: Vec<(String, String)> = Vec::new(); - for skipped in plan.getattr("skipped_relations")?.try_iter()? { + for skipped in native_field(plan, "skipped_relations")?.try_iter()? { let skipped: Bound<'_, PyAny> = skipped?; skipped_relations.push(( key_name(&skipped)?, - skipped.getattr("reason")?.str()?.extract()?, + native_field(&skipped, "reason")?.str()?.extract()?, )); } - let views: Bound<'_, PyAny> = plan.getattr("old_name_views")?; + let views: Bound<'_, PyAny> = native_field(plan, "old_name_views")?; Ok(JanitorPlanText { - target_name: plan.getattr("target_name")?.extract()?, - retention_days: plan.getattr("retention_days")?.extract()?, - direct_mode: plan.getattr("direct_mode")?.is_truthy()?, - archive_retention_days: plan.getattr("archive_retention_days")?.extract()?, - age_metadata_supported: plan.getattr("age_metadata_supported")?.is_truthy()?, - scanned_schema_count: plan.getattr("scanned_schema_count")?.extract()?, - candidates: names(&plan.getattr("candidates")?)?, + target_name: native_field(plan, "target_name")?.extract()?, + retention_days: native_field(plan, "retention_days")?.extract()?, + direct_mode: native_field(plan, "direct_mode")?.is_truthy()?, + archive_retention_days: native_field(plan, "archive_retention_days")?.extract()?, + age_metadata_supported: native_field(plan, "age_metadata_supported")?.is_truthy()?, + scanned_schema_count: native_field(plan, "scanned_schema_count")?.extract()?, + candidates: names(&native_field(plan, "candidates")?)?, archive_candidates, archive_deletions: archived_list( - &plan.getattr("archive_deletion_candidates")?, + &native_field(plan, "archive_deletion_candidates")?, &planned_at, )?, - retained_archives: archived_list(&plan.getattr("retained_archives")?, &planned_at)?, - old_name_drops: old_name_views(&views.getattr("drops")?)?, - old_name_live: old_name_views(&views.getattr("live")?)?, - old_name_missing: old_name_views(&views.getattr("missing")?)?, - query_artifacts: names(&plan.getattr("query_diff_artifact_candidates")?)?, + retained_archives: archived_list(&native_field(plan, "retained_archives")?, &planned_at)?, + old_name_drops: old_name_views(&native_field(&views, "drops")?)?, + old_name_live: old_name_views(&native_field(&views, "live")?)?, + old_name_missing: old_name_views(&native_field(&views, "missing")?)?, + query_artifacts: names(&native_field(plan, "query_diff_artifact_candidates")?)?, state_prunes, skipped_relations, - skipped_schemas: source_schemas(&plan.getattr("skipped_schemas")?, false)?, - blocked_schemas: source_schemas(&plan.getattr("blocked_schemas")?, true)?, + skipped_schemas: source_schemas(&native_field(plan, "skipped_schemas")?, false)?, + blocked_schemas: source_schemas(&native_field(plan, "blocked_schemas")?, true)?, }) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_planning.rs b/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_planning.rs index 7ce8973aa3..32fabe8565 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_planning.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/janitor/janitor_planning.rs @@ -27,6 +27,7 @@ use crate::bindings::_helpers::janitor::janitor_old_names::{ }; 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::relation_naming::fitting::shared_error; const LIFECYCLE: ExecutorHost = ExecutorHost::OperationLifecycle; @@ -129,7 +130,10 @@ fn artifact_only_plan<'py>( py, ExecutorHost::JanitorPlan, &[ - ("target_name", project.getattr("effective_target_name")?), + ( + "target_name", + native_field(project, "effective_target_name")?, + ), ("retention_days", integer(py, settings.retention_days)?), ("direct_mode", bool_value(py, settings.direct_mode)), ( @@ -174,7 +178,7 @@ pub(crate) fn build_plan<'py>( .extract::>()? { push_artifact_schema( - project.getattr("effective_target_database")?.extract()?, + native_field(project, "effective_target_database")?.extract()?, schema, ); } @@ -266,7 +270,10 @@ pub(crate) fn build_plan<'py>( py, ExecutorHost::JanitorPlan, &[ - ("target_name", project.getattr("effective_target_name")?), + ( + "target_name", + native_field(project, "effective_target_name")?, + ), ("retention_days", integer(py, settings.retention_days)?), ("direct_mode", bool_value(py, settings.direct_mode)), ( 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 5864156618..1666b67c37 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/lifecycle_events/envelopes.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/lifecycle_events/envelopes.rs @@ -23,6 +23,7 @@ use crate::bindings::_helpers::lifecycle_events::validation::{ frozen_json, known_event_check, optional_text_check, required_text_check, schema_version_check, timestamp_check, }; +use crate::bindings::_helpers::records::fields::field as native_field; const EVENT_REQUIRED_TEXT: [&str; 5] = [ "event_id", @@ -107,9 +108,9 @@ fn lifecycle_event_check(event: &Bound<'_, PyAny>) -> PyResult<()> { for field in EVENT_REQUIRED_TEXT { required_text_check(&event.getattr(field)?, field)?; } - let schema_version: Bound<'_, PyAny> = event.getattr("schema_version")?; + let schema_version: Bound<'_, PyAny> = native_field(event, "schema_version")?; schema_version_check(&schema_version)?; - let sequence: Bound<'_, PyAny> = event.getattr("invocation_sequence")?; + let sequence: Bound<'_, PyAny> = native_field(event, "invocation_sequence")?; let sequence: SequenceFact = if sequence.is_none() { SequenceFact::Absent } else if nonnegative_int(&sequence)? { @@ -117,13 +118,13 @@ fn lifecycle_event_check(event: &Bound<'_, PyAny>) -> PyResult<()> { } else { SequenceFact::Invalid }; - let has_external_context: bool = event.getattr("external_context")?.is_truthy()?; + let has_external_context: bool = native_field(event, "external_context")?.is_truthy()?; if let Some(problem) = lifecycle_version_problem(saturated(&schema_version)?, sequence, has_external_context) { return Err(validation_error(py, &problem)); } - timestamp_check(&event.getattr("occurred_at")?)?; + timestamp_check(&native_field(event, "occurred_at")?)?; for field in EVENT_OPTIONAL_TEXT { optional_text_check(&event.getattr(field)?, field)?; } @@ -147,7 +148,7 @@ fn invocation_sequence_check(value: &Bound<'_, PyAny>) -> PyResult<()> { #[pyo3(signature = (*, record))] fn diagnostic_log_check(record: &Bound<'_, PyAny>) -> PyResult<()> { let py: Python<'_> = record.py(); - let schema_version: Bound<'_, PyAny> = record.getattr("schema_version")?; + let schema_version: Bound<'_, PyAny> = native_field(record, "schema_version")?; schema_version_check(&schema_version)?; if let Some(problem) = diagnostic_version_problem(saturated(&schema_version)?) { return Err(validation_error(py, &problem)); @@ -155,11 +156,11 @@ fn diagnostic_log_check(record: &Bound<'_, PyAny>) -> PyResult<()> { for field in DIAGNOSTIC_REQUIRED_TEXT { required_text_check(&record.getattr(field)?, field)?; } - let severity: String = record.getattr("severity")?.extract()?; + let severity: String = native_field(record, "severity")?.extract()?; if let Some(problem) = diagnostic_severity_problem(&severity) { return Err(validation_error(py, problem)); } - timestamp_check(&record.getattr("occurred_at")?)?; + timestamp_check(&native_field(record, "occurred_at")?)?; for field in DIAGNOSTIC_OPTIONAL_TEXT { optional_text_check(&record.getattr(field)?, field)?; } 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 9c609ba121..cbde594342 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/lifecycle_events/validation.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/lifecycle_events/validation.rs @@ -10,6 +10,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::imported; use crate::bindings::_helpers::lifecycle_events::payloads::{ PathSegment, frozen, object_entries, validation_error, }; +use crate::bindings::_helpers::records::fields::field as native_field; const CORRELATION_FIELDS: [&str; 5] = [ "run_id", @@ -74,7 +75,7 @@ pub(crate) fn optional_text_check(value: &Bound<'_, PyAny>, field_name: &str) -> pub(crate) fn timestamp_check(value: &Bound<'_, PyAny>) -> PyResult<()> { let py = value.py(); let datetime: Bound<'_, PyAny> = imported(py, "datetime", "datetime")?; - if !value.is_instance(&datetime)? || value.getattr("tzinfo")?.is_none() { + if !value.is_instance(&datetime)? || native_field(value, "tzinfo")?.is_none() { return Err(validation_error( py, "occurred_at must be a timezone-aware UTC datetime", @@ -97,10 +98,10 @@ pub(crate) fn known_event_check(event: &Bound<'_, PyAny>) -> PyResult<()> { correlations.push((field, !event.getattr(field)?.is_none())); } let facts = KnownEventFacts { - schema_version: event.getattr("schema_version")?.extract()?, - event_type: event.getattr("event_type")?.extract()?, + schema_version: native_field(event, "schema_version")?.extract()?, + event_type: native_field(event, "event_type")?.extract()?, correlations, - payload: object_entries(&event.getattr("payload")?)?, + payload: object_entries(&native_field(event, "payload")?)?, }; validate_known_event(&facts).map_err(|message| validation_error(event.py(), &message)) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/lineage/column_lineage_graph.rs b/crates/sqlbuild-python/src/bindings/_helpers/lineage/column_lineage_graph.rs index f7ec90bca9..2bea132814 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/lineage/column_lineage_graph.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/lineage/column_lineage_graph.rs @@ -14,6 +14,7 @@ use crate::bindings::_helpers::lineage::lineage_objects::LineageClasses; use crate::bindings::_helpers::record_behaviours::lineage_facts::{ confidence_member, transform_kind_member, }; +use crate::bindings::_helpers::records::fields::field as native_field; /// One compact model's facts and root-star flag. struct CompactModel { @@ -116,7 +117,7 @@ impl ProjectColumnLineage { } for item in models.call_method0("items")?.try_iter()? { let (name, model): (String, Bound<'_, PyAny>) = item?.extract()?; - let _ = edge_counts.insert(name, upstream_count(&model.getattr("columns")?)?); + let _ = edge_counts.insert(name, upstream_count(&native_field(&model, "columns")?)?); } Ok(Self { models: models.clone().unbind(), @@ -189,7 +190,7 @@ impl ProjectColumnLineage { if model.is_none() { return Ok(false); } - model.getattr("has_star")?.extract() + native_field(&model, "has_star")?.extract() } /// Return a target model's direct edge count without expanding edge objects. @@ -253,8 +254,8 @@ impl ProjectColumnLineage { } fn __repr__(slf: &Bound<'_, Self>) -> PyResult { - let models: Bound<'_, PyString> = slf.getattr("models")?.repr()?; - let edges: Bound<'_, PyString> = slf.getattr("edges")?.repr()?; + let models: Bound<'_, PyString> = native_field(slf, "models")?.repr()?; + let edges: Bound<'_, PyString> = native_field(slf, "edges")?.repr()?; Ok(format!( "ProjectColumnLineage(models={models}, edges={edges})" )) @@ -265,8 +266,8 @@ impl ProjectColumnLineage { if !other.is_instance_of::() { return Ok(py.NotImplemented()); } - let equal: bool = slf.getattr("models")?.eq(other.getattr("models")?)? - && slf.getattr("edges")?.eq(other.getattr("edges")?)?; + let equal: bool = native_field(slf, "models")?.eq(native_field(other, "models")?)? + && native_field(slf, "edges")?.eq(native_field(other, "edges")?)?; Ok(pyo3::types::PyBool::new(py, equal) .to_owned() .into_any() @@ -292,7 +293,7 @@ impl ProjectColumnLineage { if model.is_none() { continue; } - builder.push_columns(name, &model.getattr("columns")?)?; + builder.push_columns(name, &native_field(&model, "columns")?)?; } self.records = Some(EdgeRecords::new(builder.keys, builder.parts)); Ok(()) @@ -401,11 +402,11 @@ impl RecordBuilder { return self.push_columns(model_name, columns); } let py: Python<'_> = columns.py(); - let pool: Bound<'_, PyAny> = columns.getattr("string_pool")?; - let remapped: bool = columns.getattr("resource_names")?.is_truthy()? - || columns.getattr("resource_name_indexes")?.is_truthy()?; + let pool: Bound<'_, PyAny> = native_field(columns, "string_pool")?; + let remapped: bool = native_field(columns, "resource_names")?.is_truthy()? + || native_field(columns, "resource_name_indexes")?.is_truthy()?; let mut members = CodeMembers::default(); - for row in columns.getattr("rows")?.try_iter()? { + for row in native_field(columns, "rows")?.try_iter()? { let (name_index, transform_code, confidence_code, sources): ( Bound<'_, PyAny>, i64, @@ -447,16 +448,16 @@ impl RecordBuilder { fn push_columns(&mut self, model_name: &str, columns: &Bound<'_, PyAny>) -> PyResult<()> { for column in columns.try_iter()? { let column: Bound<'_, PyAny> = column?; - let output_column: String = column.getattr("output_column")?.extract()?; - let transform: Bound<'_, PyAny> = column.getattr("transform_kind")?; - let confidence: Bound<'_, PyAny> = column.getattr("confidence")?; - for source in column.getattr("upstream_columns")?.try_iter()? { + let output_column: String = native_field(&column, "output_column")?.extract()?; + let transform: Bound<'_, PyAny> = native_field(&column, "transform_kind")?; + let confidence: Bound<'_, PyAny> = native_field(&column, "confidence")?; + for source in native_field(&column, "upstream_columns")?.try_iter()? { let source: Bound<'_, PyAny> = source?; - let resource_type: Bound<'_, PyAny> = source.getattr("resource_type")?; + let resource_type: Bound<'_, PyAny> = native_field(&source, "resource_type")?; self.keys.push(EdgeKey { source_type: text(&resource_type)?, - source_name: source.getattr("resource_name")?.extract()?, - source_column: source.getattr("column_name")?.extract()?, + source_name: native_field(&source, "resource_name")?.extract()?, + source_column: native_field(&source, "column_name")?.extract()?, target_model: model_name.to_owned(), target_column: output_column.clone(), transform: text(&transform)?, @@ -512,7 +513,7 @@ fn fact_edge_count(classes: &LineageClasses<'_>, columns: &Bound<'_, PyAny>) -> return upstream_count(columns); } let mut count: usize = 0; - for row in columns.getattr("rows")?.try_iter()? { + for row in native_field(columns, "rows")?.try_iter()? { count += row?.get_item(3)?.len()?; } Ok(count) @@ -521,7 +522,7 @@ fn fact_edge_count(classes: &LineageClasses<'_>, columns: &Bound<'_, PyAny>) -> fn upstream_count(columns: &Bound<'_, PyAny>) -> PyResult { let mut count: usize = 0; for column in columns.try_iter()? { - count += column?.getattr("upstream_columns")?.len()?; + count += native_field(&column?, "upstream_columns")?.len()?; } Ok(count) } @@ -538,21 +539,21 @@ fn model_from_facts<'py>( for column in columns.try_iter()? { let column: Bound<'py, PyAny> = column?; let mut sources: Vec> = Vec::new(); - for source in column.getattr("upstream_columns")?.try_iter()? { + for source in native_field(&column, "upstream_columns")?.try_iter()? { let source: Bound<'py, PyAny> = source?; sources.push(classes.lineage_source.call1(( - source.getattr("resource_type")?, - source.getattr("resource_name")?, - source.getattr("column_name")?, + native_field(&source, "resource_type")?, + native_field(&source, "resource_name")?, + native_field(&source, "column_name")?, ))?); } lineage_columns.push(classes.column_lineage.call1(( - column.getattr("output_column")?, - column.getattr("transform_kind")?, + native_field(&column, "output_column")?, + native_field(&column, "transform_kind")?, py.None(), PyTuple::new(py, sources)?, &classes.unknown_nullability, - column.getattr("confidence")?, + native_field(&column, "confidence")?, ))?); } classes.model(model_name, lineage_columns, has_star) 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 1e08cd4c4f..70e23da755 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/lineage/lineage_cache.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/lineage/lineage_cache.rs @@ -4,6 +4,7 @@ use std::collections::{HashMap, HashSet}; use std::hash::Hash; use std::path::{Path, PathBuf}; +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}; @@ -96,9 +97,9 @@ fn skips_cache(py: Python<'_>, error: &PyErr) -> PyResult { /// Python's `sqlbuild` distribution version, as the cache key records it. fn sqlbuild_version(py: Python<'_>) -> PyResult { let metadata: Bound<'_, PyModule> = py.import("importlib.metadata")?; - match metadata.getattr("version")?.call1(("sqlbuild",)) { + match native_field(&metadata, "version")?.call1(("sqlbuild",)) { Ok(version) => version.extract(), - Err(error) if error.is_instance(py, &metadata.getattr("PackageNotFoundError")?) => { + Err(error) if error.is_instance(py, &native_field(&metadata, "PackageNotFoundError")?) => { Ok(UNKNOWN_VERSION.to_owned()) } Err(error) => Err(error), 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 ccbb57ac9f..3b4b82ff5f 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/lineage/lineage_command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/lineage/lineage_command.rs @@ -38,6 +38,7 @@ use crate::bindings::_helpers::lineage::lineage_cache::{ }; use crate::bindings::_helpers::lineage::lineage_index::{graph_index, index_views, lineage_keys}; 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, RecordId}; use crate::bindings::_helpers::records::registry; @@ -146,8 +147,8 @@ fn reject_unit_tests( let options: Bound<'_, PyDict> = keywords( py, &[ - ("select", &request.getattr("select")?), - ("exclude", &request.getattr("exclude")?), + ("select", &native_field(request, "select")?), + ("exclude", &native_field(request, "exclude")?), ("discovered_inputs", discovered), ], )?; @@ -165,7 +166,7 @@ fn prepare_source<'py>( let fingerprint: Option = if compiled { None } else { - lineage_fingerprint(project_dir, &request.getattr("cli_vars")?)? + lineage_fingerprint(project_dir, &native_field(request, "cli_vars")?)? }; if let Some(fingerprint) = &fingerprint && let Some(index) = read_lineage_cache(py, &directory, fingerprint)? @@ -181,19 +182,23 @@ fn prepare_source<'py>( Some(&keywords( py, &[ - ("project_config", &discovered.getattr("project_config")?), - ("local_config", &discovered.getattr("local_config")?), + ( + "project_config", + &native_field(&discovered, "project_config")?, + ), + ("local_config", &native_field(&discovered, "local_config")?), ], )?), )?; let adapter: Bound<'py, PyAny> = resolve_cli_adapter(&adapter_name, project_dir)?; - let no_validation: bool = request.getattr("no_sql_validation")?.extract::()? || !compiled; + let no_validation: bool = + native_field(request, "no_sql_validation")?.extract::()? || !compiled; let options: Bound<'py, PyDict> = keywords( py, &[ ("discovered_inputs", &discovered), ("adapter", &adapter), - ("cli_vars", &request.getattr("cli_vars")?), + ("cli_vars", &native_field(request, "cli_vars")?), ], )?; options.set_item("no_sql_validation", no_validation)?; @@ -225,15 +230,17 @@ fn compiled_graph_error(py: Python<'_>, message: &str, help: bool) -> PyErr { } fn semantic_use(value: &Bound<'_, PyAny>) -> PyResult { - let source: Bound<'_, PyAny> = value.getattr("source")?; + let source: Bound<'_, PyAny> = native_field(value, "source")?; Ok(SemanticUse { - consumer_model: value.getattr("consumer_model")?.str()?.extract()?, - context: value.getattr("context")?.str()?.extract()?, - expression: value.getattr("expression_sql")?.str()?.extract()?, + consumer_model: native_field(value, "consumer_model")?.str()?.extract()?, + context: native_field(value, "context")?.str()?.extract()?, + expression: native_field(value, "expression_sql")?.str()?.extract()?, source: lineage_column(&source)?, - confidence: value.getattr("confidence")?.getattr("value")?.extract()?, - line: value.getattr("line")?.extract()?, - column: value.getattr("column")?.extract()?, + confidence: native_field(value, "confidence")? + .getattr("value")? + .extract()?, + line: native_field(value, "line")?.extract()?, + column: native_field(value, "column")?.extract()?, }) } @@ -255,14 +262,14 @@ fn semantic_uses( false, )); }; - let targets: Vec = strings(&request.getattr("targets")?)?; + 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", &request.getattr("select")?), - ("exclude", &request.getattr("exclude")?), + ("select", &native_field(request, "select")?), + ("exclude", &native_field(request, "exclude")?), ], )?; let keys: Vec = lineage_keys( @@ -284,7 +291,7 @@ fn semantic_uses( }; let uses: Bound<'_, PyAny> = registry::native(py, NativeAttr::BuildDirectSemanticUses)?.call1(( - graph.getattr("project")?, + native_field(graph, "project")?, adapter.call_method0("sql_analysis_dialect")?, PyFrozenSet::new(py, model_names)?, ))?; @@ -295,18 +302,18 @@ fn semantic_uses( fn lineage_column(column: &Bound<'_, PyAny>) -> PyResult { Ok(LineageColumn { - resource_type: column.getattr("resource_type")?.str()?.extract()?, - resource_name: column.getattr("resource_name")?.extract()?, - column_name: column.getattr("column_name")?.extract()?, + resource_type: native_field(column, "resource_type")?.str()?.extract()?, + resource_name: native_field(column, "resource_name")?.extract()?, + column_name: native_field(column, "column_name")?.extract()?, }) } fn column_edge(edge: &Bound<'_, PyAny>) -> PyResult { Ok(LineageColumnEdge { - source: lineage_column(&edge.getattr("source")?)?, - target: lineage_column(&edge.getattr("target")?)?, - transform: edge.getattr("transform_kind")?.str()?.extract()?, - confidence: edge.getattr("confidence")?.str()?.extract()?, + source: lineage_column(&native_field(edge, "source")?)?, + target: lineage_column(&native_field(edge, "target")?)?, + transform: native_field(edge, "transform_kind")?.str()?.extract()?, + confidence: native_field(edge, "confidence")?.str()?.extract()?, }) } @@ -374,7 +381,7 @@ fn column_lineage<'py>( (&message, rich && count >= RICH_STATUS_MODEL_THRESHOLD), || { registry::native(py, NativeAttr::BuildProjectColumnLineage)?.call1(( - graph.getattr("project")?, + native_field(graph, "project")?, py.None(), mode, PyFrozenSet::new(py, &candidates.model_names)?, @@ -435,7 +442,7 @@ fn column_trace( }, trace: traced_edges(&lineage, (&key.name, column_name), (direction, depth))?, direction: direction.to_owned(), - mode: mode.getattr("value")?.extract()?, + mode: native_field(mode, "value")?.extract()?, max_depth: depth, analyzed_model_count: candidates.model_names.len(), truncated: candidates.truncated, @@ -448,7 +455,7 @@ fn selector_views<'py>( source: &LineageSource<'py>, ) -> PyResult> { match source { - LineageSource::Compiled { graph, .. } => graph.getattr("lineage_views"), + LineageSource::Compiled { graph, .. } => native_field(graph, "lineage_views"), LineageSource::Cached(index) => index_views(py, index), } } @@ -493,8 +500,8 @@ fn selector_anchors( for part in token.split(SELECTOR_INTERSECTION_MARKER) { let parsed: Bound<'_, PyAny> = parse.call1((part,))?; if parsed.is_instance(&path_class)? { - let start: LineageKey = named(parsed.getattr("start_name")?.extract()?)?; - let end: LineageKey = named(parsed.getattr("end_name")?.extract()?)?; + let start: LineageKey = named(native_field(&parsed, "start_name")?.extract()?)?; + let end: LineageKey = named(native_field(&parsed, "end_name")?.extract()?)?; anchors .retained .extend(lineage_path_nodes(index, &start, &end)); @@ -502,7 +509,7 @@ fn selector_anchors( anchors.downstream.push(end); continue; } - let kind: String = parsed.getattr("kind")?.str()?.extract()?; + let kind: String = native_field(&parsed, "kind")?.str()?.extract()?; let set_kind: bool = SET_SELECTOR_KINDS.contains(&kind.as_str()); if set_kind && require_clear_anchors { return Err(user_error( @@ -517,8 +524,8 @@ fn selector_anchors( view_options(views, &["all_keys", "tag_index", "path_index"])?; options.set_item("parsed", &parsed)?; let matched: Vec = lineage_keys(&matcher.call((), Some(&options))?)?; - let upstream: bool = parsed.getattr("upstream")?.extract()?; - let downstream: bool = parsed.getattr("downstream")?.extract()?; + let upstream: bool = native_field(&parsed, "upstream")?.extract()?; + let downstream: bool = native_field(&parsed, "downstream")?.extract()?; if set_kind || upstream { anchors.upstream.extend(matched.iter().cloned()); } @@ -542,8 +549,8 @@ fn selector_rendering( (depth, style): (Option, CliStyle), ) -> PyResult { let views: Bound<'_, PyAny> = selector_views(py, source)?; - let select: Bound<'_, PyAny> = request.getattr("select")?; - let exclude: Bound<'_, PyAny> = request.getattr("exclude")?; + let select: Bound<'_, PyAny> = native_field(request, "select")?; + let exclude: Bound<'_, PyAny> = native_field(request, "exclude")?; let options: Bound<'_, PyDict> = view_options(&views, &GRAPH_VIEW_NAMES)?; options.set_item("select", &select)?; options.set_item("exclude", &exclude)?; @@ -551,8 +558,8 @@ fn selector_rendering( ®istry::native(py, NativeAttr::ResolveProjectSelectors)?.call((), Some(&options))?, )?; let index: &LineageIndex = source.index(); - let direction: Option = request.getattr("direction")?.extract()?; - let output_format: String = request.getattr("output_format")?.extract()?; + let direction: Option = native_field(request, "direction")?.extract()?; + let output_format: String = native_field(request, "output_format")?.extract()?; let graph = if let Some(direction) = direction { let options: Bound<'_, PyDict> = view_options(&views, &GRAPH_VIEW_NAMES)?; options.set_item("selectors", &exclude)?; @@ -580,7 +587,8 @@ fn selector_failure( let planner_error: Bound<'_, PyAny> = registry::error(py, ErrorId::PlannerInput)?; if !matches!(source, LineageSource::Cached(_)) || !error.is_instance(py, &planner_error) - || error.value(py).getattr("code")?.extract::()? != UNKNOWN_SELECTOR_ERROR_CODE + || native_field(error.value(py), "code")?.extract::()? + != UNKNOWN_SELECTOR_ERROR_CODE { return Ok(()); } @@ -598,8 +606,8 @@ fn rendering( (source, project_dir): (&LineageSource<'_>, &Bound<'_, PyAny>), (depth, style): (Option, CliStyle), ) -> PyResult { - let targets: Vec = strings(&request.getattr("targets")?)?; - let output_format: String = request.getattr("output_format")?.extract()?; + let targets: Vec = strings(&native_field(request, "targets")?)?; + let output_format: String = native_field(request, "output_format")?.extract()?; if targets.is_empty() { return selector_rendering(py, request, source, (depth, style)) .map_err(|error| selector_failure(py, request, (source, project_dir), error)); @@ -613,7 +621,7 @@ fn rendering( && let Some(trace) = column_trace( py, source, - (target, &request.getattr("lineage_mode")?), + (target, &native_field(request, "lineage_mode")?), (&direction, depth), )? { @@ -626,9 +634,9 @@ fn rendering( fn request_shape(request: &Bound<'_, PyAny>) -> PyResult { Ok(LineageRequestShape { - targets: strings(&request.getattr("targets")?)?, - select: strings(&request.getattr("select")?)?, - exclude: strings(&request.getattr("exclude")?)?, + targets: strings(&native_field(request, "targets")?)?, + select: strings(&native_field(request, "select")?)?, + exclude: strings(&native_field(request, "exclude")?)?, }) } @@ -638,13 +646,14 @@ fn request_shape(request: &Bound<'_, PyAny>) -> PyResult { 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 = parse_lineage_depth(&request.getattr("depth")?.extract::()?) - .map_err(|error| user_error(py, error))?; - let project_dir: Bound<'_, PyAny> = match request.getattr("project_dir")? { + let depth: Option = + parse_lineage_depth(&native_field(request, "depth")?.extract::()?) + .map_err(|error| user_error(py, error))?; + 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")?, }; - let include_uses: bool = request.getattr("include_uses")?.extract()?; + let include_uses: bool = native_field(request, "include_uses")?.extract()?; let compiled: bool = include_uses || shape .targets 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 42a2a7e533..f6c960c321 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/lineage/lineage_index.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/lineage/lineage_index.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; +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}; @@ -11,8 +12,8 @@ use sqlbuild_analysis::lineage_command::models::{LineageIndex, LineageKey, Linea /// A Python `CompiledObjectKey` as a lineage key. pub(crate) fn lineage_key(key: &Bound<'_, PyAny>) -> PyResult { Ok(LineageKey { - resource_type: key.getattr("resource_type")?.str()?.extract()?, - name: key.getattr("name")?.extract()?, + resource_type: native_field(key, "resource_type")?.str()?.extract()?, + name: native_field(key, "name")?.extract()?, }) } @@ -95,30 +96,36 @@ fn project_nodes(project: &Bound<'_, PyAny>) -> PyResult = model?; add(LineageNode { - key: lineage_key(&model.getattr("key")?)?, - relative_path: optional_text(model.getattr("relative_path")?)?, + key: lineage_key(&native_field(&model, "key")?)?, + relative_path: optional_text(native_field(&model, "relative_path")?)?, qualified_name: optional_text( - model.getattr("destination")?.getattr("qualified_name")?, + native_field(&model, "destination")?.getattr("qualified_name")?, )?, }); } - for seed in project.getattr("seeds")?.try_iter()? { + for seed in native_field(project, "seeds")?.try_iter()? { let seed: Bound<'_, PyAny> = seed?; add(LineageNode { - key: lineage_key(&seed.getattr("key")?)?, - relative_path: optional_text(seed.getattr("seed_file")?.getattr("relative_path")?)?, - qualified_name: optional_text(seed.getattr("destination")?.getattr("qualified_name")?)?, + key: lineage_key(&native_field(&seed, "key")?)?, + relative_path: optional_text( + native_field(&seed, "seed_file")?.getattr("relative_path")?, + )?, + qualified_name: optional_text( + native_field(&seed, "destination")?.getattr("qualified_name")?, + )?, }); } - for source in project.getattr("sources")?.try_iter()? { + for source in native_field(project, "sources")?.try_iter()? { let source: Bound<'_, PyAny> = source?; add(LineageNode { - key: lineage_key(&source.getattr("key")?)?, - relative_path: optional_text(source.getattr("source_file")?.getattr("relative_path")?)?, - qualified_name: source_relation_name(&source.getattr("source_entry")?)?, + key: lineage_key(&native_field(&source, "key")?)?, + relative_path: optional_text( + native_field(&source, "source_file")?.getattr("relative_path")?, + )?, + qualified_name: source_relation_name(&native_field(&source, "source_entry")?)?, }); } Ok(nodes) @@ -126,7 +133,7 @@ fn project_nodes(project: &Bound<'_, PyAny>) -> PyResult) -> PyResult { - let views: Bound<'_, PyAny> = graph.getattr("lineage_views")?; + let views: Bound<'_, PyAny> = native_field(graph, "lineage_views")?; let mut all_keys: Vec<(String, LineageKey)> = Vec::new(); for item in views .getattr("all_keys")? @@ -154,7 +161,8 @@ pub(crate) fn graph_index(graph: &Bound<'_, PyAny>) -> PyResult { let (key, folder): (Bound<'_, PyAny>, String) = item?.extract()?; paths.push((lineage_key(&key)?, folder)); } - let mut nodes: HashMap = project_nodes(&graph.getattr("project")?)?; + let mut nodes: HashMap = + project_nodes(&native_field(graph, "project")?)?; for (_, key) in &all_keys { nodes.entry(key.clone()).or_insert_with(|| LineageNode { key: key.clone(), @@ -164,8 +172,8 @@ pub(crate) fn graph_index(graph: &Bound<'_, PyAny>) -> PyResult { } Ok(LineageIndex { nodes, - upstream: ordered_edges(&views.getattr("upstream_deps")?)?, - downstream: ordered_edges(&views.getattr("downstream_deps")?)?, + upstream: ordered_edges(&native_field(&views, "upstream_deps")?)?, + downstream: ordered_edges(&native_field(&views, "downstream_deps")?)?, tags, paths, all_keys, 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 a66b3e90f2..0afc432355 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/lineage/lineage_objects.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/lineage/lineage_objects.rs @@ -2,6 +2,7 @@ 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 pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; @@ -51,8 +52,8 @@ impl<'py> LineageClasses<'py> { transform_kind: registry::class(py, RecordId::ColumnTransformKind)?, confidence: registry::class(py, RecordId::ColumnLineageConfidence)?, analysis_dialect: registry::class(py, RecordId::PolyglotAnalysisDialect)?, - model_resource: resource_type.getattr("MODEL")?, - unknown_nullability: nullability.getattr("UNKNOWN")?, + model_resource: native_field(&resource_type, "MODEL")?, + unknown_nullability: native_field(&nullability, "UNKNOWN")?, nullability, resource_type, }) 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 7439f16ab5..3cd6139974 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/lineage/project_lineage.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/lineage/project_lineage.rs @@ -23,6 +23,7 @@ use crate::bindings::_helpers::lineage::lineage_objects::{LineageClasses, log_li use crate::bindings::_helpers::lineage::project_schema::{ fast_schema, inferred_names, rich_schema, }; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::ErrorId; use crate::bindings::_helpers::records::registry; use crate::bindings::models::ProjectCatalog; @@ -72,14 +73,14 @@ fn build_direct_semantic_uses<'py>( let classes: LineageClasses<'py> = LineageClasses::new(py)?; let schema = rich_schema(project)?; let mut models: Vec<(String, String, String)> = Vec::new(); - for model in project.getattr("models")?.try_iter()? { + for model in native_field(project, "models")?.try_iter()? { let model: Bound<'py, PyAny> = model?; - let name: Bound<'py, PyAny> = model.getattr("name")?; + let name: Bound<'py, PyAny> = native_field(&model, "name")?; if model_names.contains(&name)? { models.push(( name.extract()?, - model.getattr("query_sql")?.extract()?, - model.getattr("authored_sql")?.extract()?, + native_field(&model, "query_sql")?.extract()?, + native_field(&model, "authored_sql")?.extract()?, )); } } @@ -123,11 +124,11 @@ fn selected_models<'py>( model_names: Option<&Bound<'py, PyAny>>, ) -> PyResult>> { let mut models: Vec> = Vec::new(); - for model in project.getattr("models")?.try_iter()? { + for model in native_field(project, "models")?.try_iter()? { let model: Bound<'py, PyAny> = model?; let selected: bool = match model_names { None => true, - Some(names) => names.contains(model.getattr("name")?)?, + Some(names) => names.contains(native_field(&model, "name")?)?, }; if selected { models.push(model); @@ -154,22 +155,22 @@ fn fast_lineage<'py>( let mut work: Vec> = Vec::with_capacity(models.len()); let mut requests: Vec = Vec::new(); for model in models { - let facts: Bound<'py, PyAny> = model.getattr("fast_lineage_columns")?; + let facts: Bound<'py, PyAny> = native_field(model, "fast_lineage_columns")?; let (has_star, star_resolved): (bool, bool) = star_flags(model)?; let expands_star: bool = has_star && !star_resolved; if facts.is_none() { requests.push(FastLineageModel::Parse { - query_sql: model.getattr("query_sql")?.extract()?, + query_sql: native_field(model, "query_sql")?.extract()?, inferred_columns: inferred_names(model)?, }); } else if expands_star { requests.push(FastLineageModel::StarExpansion { - query_sql: model.getattr("query_sql")?.extract()?, + query_sql: native_field(model, "query_sql")?.extract()?, existing_columns: output_columns(classes, &facts)?, }); } work.push(FastModel { - name: model.getattr("name")?.extract()?, + name: native_field(model, "name")?.extract()?, facts: (!facts.is_none()).then_some(facts), has_star, expands_star, @@ -178,11 +179,11 @@ fn fast_lineage<'py>( let outcomes: Vec = if requests.is_empty() { Vec::new() } else { - let binding_catalog: Bound<'py, PyAny> = project.getattr("binding_catalog")?; + let binding_catalog: Bound<'py, PyAny> = native_field(project, "binding_catalog")?; let catalog: Option> = if binding_catalog.is_none() { None } else { - binding_catalog.getattr("native")?.extract()? + native_field(&binding_catalog, "native")?.extract()? }; let request = FastLineageRequest { dialect: dialect.map(|name| name.extract()).transpose()?, @@ -251,7 +252,7 @@ fn fast_lineage<'py>( /// The output column names of compact facts or plain column facts. fn output_columns(classes: &LineageClasses<'_>, facts: &Bound<'_, PyAny>) -> PyResult> { if facts.is_instance(&classes.compact_facts)? { - let pool: Bound<'_, PyAny> = facts.getattr("string_pool")?; + let pool: Bound<'_, PyAny> = native_field(facts, "string_pool")?; return facts .getattr("rows")? .try_iter()? @@ -260,7 +261,7 @@ fn output_columns(classes: &LineageClasses<'_>, facts: &Bound<'_, PyAny>) -> PyR } facts .try_iter()? - .map(|column| column?.getattr("output_column")?.extract()) + .map(|column| native_field(&column?, "output_column")?.extract()) .collect() } @@ -288,7 +289,7 @@ fn rich_lineage<'py>( schema: rich_schema(project)?, models: models .iter() - .map(|model| model.getattr("query_sql")?.extract()) + .map(|model| native_field(model, "query_sql")?.extract()) .collect::>()?, }; py.compiler_detach(|| build_rich_lineage(&request)) @@ -297,7 +298,7 @@ fn rich_lineage<'py>( let built: Bound<'py, PyDict> = PyDict::new(py); let mut edges: Vec> = Vec::new(); for (model, outcome) in models.iter().zip(outcomes) { - let name: String = model.getattr("name")?.extract()?; + let name: String = native_field(model, "name")?.extract()?; match outcome { RichLineageOutcome::Built { columns, has_star } => { let mut lineage_columns: Vec> = Vec::with_capacity(columns.len()); @@ -318,21 +319,21 @@ fn rich_lineage<'py>( } for item in built.iter() { let (target_name, result): (Bound<'py, PyAny>, Bound<'py, PyAny>) = item; - for column in result.getattr("columns")?.try_iter()? { + for column in native_field(&result, "columns")?.try_iter()? { let column: Bound<'py, PyAny> = column?; let target: Bound<'py, PyAny> = classes.qualified_column.call1(( &classes.model_resource, &target_name, - column.getattr("output_column")?, + native_field(&column, "output_column")?, ))?; - let transform: Bound<'py, PyAny> = column.getattr("transform_kind")?; - let confidence: Bound<'py, PyAny> = column.getattr("confidence")?; - for upstream in column.getattr("upstream_columns")?.try_iter()? { + let transform: Bound<'py, PyAny> = native_field(&column, "transform_kind")?; + let confidence: Bound<'py, PyAny> = native_field(&column, "confidence")?; + for upstream in native_field(&column, "upstream_columns")?.try_iter()? { let upstream: Bound<'py, PyAny> = upstream?; let source: Bound<'py, PyAny> = classes.qualified_column.call1(( - upstream.getattr("resource_type")?, - upstream.getattr("resource_name")?, - upstream.getattr("column_name")?, + native_field(&upstream, "resource_type")?, + native_field(&upstream, "resource_name")?, + native_field(&upstream, "column_name")?, ))?; edges.push(classes.edge(source, &target, &transform, &confidence)?); } 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 052ea85341..623b015e46 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/lineage/project_schema.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/lineage/project_schema.rs @@ -6,6 +6,7 @@ use sqlbuild_analysis::lineage::models::{ }; use crate::bindings::_helpers::analysis::model_reads::{declared_rows, inferred_rows}; +use crate::bindings::_helpers::records::fields::field as native_field; type TypedColumns = Vec<(String, Option)>; @@ -57,11 +58,11 @@ pub(crate) fn inferred_names(model: &Bound<'_, PyAny>) -> PyResult> fn project_columns(project: &Bound<'_, PyAny>) -> PyResult> { let mut resources: Vec = Vec::new(); - for model in project.getattr("models")?.try_iter()? { + for model in native_field(project, "models")?.try_iter()? { let model: Bound<'_, PyAny> = model?; resources.push(ProjectColumns { resource_type: LineageResourceType::Model, - name: model.getattr("name")?.extract()?, + name: native_field(&model, "name")?.extract()?, typed: inferred_columns(&model)?, schema: schema_columns(&model)?, }); @@ -73,16 +74,16 @@ fn project_columns(project: &Bound<'_, PyAny>) -> PyResult> for resource in project.getattr(attribute)?.try_iter()? { let resource: Bound<'_, PyAny> = resource?; let mut typed: TypedColumns = Vec::new(); - for column in resource.getattr(entry)?.getattr("columns")?.try_iter()? { + for column in native_field(&resource.getattr(entry)?, "columns")?.try_iter()? { let column: Bound<'_, PyAny> = column?; typed.push(( - column.getattr("name")?.extract()?, - column.getattr("type")?.extract()?, + native_field(&column, "name")?.extract()?, + native_field(&column, "type")?.extract()?, )); } resources.push(ProjectColumns { resource_type, - name: resource.getattr("name")?.extract()?, + name: native_field(&resource, "name")?.extract()?, typed, schema: Vec::new(), }); 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 0a81034f77..cd6d231201 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/load/ingestr_command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/load/ingestr_command.rs @@ -19,6 +19,7 @@ use crate::bindings::_helpers::boundary::keyword_request::Request; use crate::bindings::_helpers::model_config::template_data::expanded_template_data; 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; const INGESTR_ERROR: ThirdPartyHost = ThirdPartyHost::IngestrIntegrationError; const INGESTR_SOURCE_CONFIG: ThirdPartyHost = ThirdPartyHost::IngestrSourceConfig; @@ -45,15 +46,15 @@ fn ingestr_error(py: Python<'_>, message: String) -> PyErr { /// The source's ingestr config, raising when the source declares no valid one. fn ingestr_config<'py>(source_entry: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = source_entry.py(); - let name: String = source_entry.getattr("name")?.str()?.extract()?; - let loader: Bound<'py, PyAny> = source_entry.getattr("integration_loader")?; - if loader.is_none() || !loader.getattr("kind")?.eq(host::entry(py, INGESTR_KIND)?)? { + let name: String = native_field(source_entry, "name")?.str()?.extract()?; + let loader: Bound<'py, PyAny> = native_field(source_entry, "integration_loader")?; + if loader.is_none() || !native_field(&loader, "kind")?.eq(host::entry(py, INGESTR_KIND)?)? { return Err(ingestr_error( py, format!("Source '{name}' does not define ingestr config"), )); } - let config: Bound<'py, PyAny> = loader.getattr("config")?; + let config: Bound<'py, PyAny> = native_field(&loader, "config")?; if !config.is_instance(&host::entry(py, INGESTR_SOURCE_CONFIG)?)? { return Err(ingestr_error( py, @@ -177,7 +178,7 @@ fn build_ingestr_command<'py>( &connection(request.get("connection_config")?.downcast()?)?, ) .map_err(|error| ingestr_error(py, error.message))?; - let schema: Option = source_entry.getattr("schema")?.extract()?; + let schema: Option = native_field(&source_entry, "schema")?.extract()?; let destination_name: String = request.get("destination_name")?.extract()?; let destination: IngestrDestination = IngestrDestination { uri, 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 8b3b0fb10c..cec1b2e845 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/load/load_command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/load/load_command.rs @@ -30,6 +30,7 @@ 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::{ErrorId, NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; @@ -70,7 +71,7 @@ fn write(stream: &Bound<'_, PyAny>, value: &str) -> PyResult<()> { } fn invocation<'py>(py: Python<'py>, request: &Bound<'py, PyAny>) -> PyResult> { - let project_dir: Bound<'py, PyAny> = match request.getattr("project_dir")? { + let project_dir: Bound<'py, PyAny> = match native_field(request, "project_dir")? { directory if !directory.is_none() => directory, _ => py.import("pathlib")?.getattr("Path")?.call_method0("cwd")?, }; @@ -78,8 +79,8 @@ fn invocation<'py>(py: Python<'py>, request: &Bound<'py, PyAny>) -> PyResult = request.getattr("select")?; - let exclude: Bound<'py, PyAny> = request.getattr("exclude")?; + 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)?, &[ @@ -88,7 +89,7 @@ fn invocation<'py>(py: Python<'py>, request: &Bound<'py, PyAny>) -> PyResult = request.getattr("selected_target")?; + let selected_target: Bound<'py, PyAny> = native_field(request, "selected_target")?; let target_config: Bound<'py, PyAny> = call_with( ®istry::native(py, NativeAttr::BuildEffectiveTargetConfig)?, &[ @@ -101,7 +102,7 @@ fn invocation<'py>(py: Python<'py>, request: &Bound<'py, PyAny>) -> PyResult = LoadProject::new(&discovered, &target_config)?; @@ -113,8 +114,8 @@ fn invocation<'py>(py: Python<'py>, request: &Bound<'py, PyAny>) -> PyResult> = project.reference_entries(&selected, &defaults)?; - let machine_output: bool = request.getattr("json_output")?.extract()?; - let use_color: bool = !request.getattr("no_color")?.extract::()? + let machine_output: bool = native_field(request, "json_output")?.extract()?; + let use_color: bool = !native_field(request, "no_color")?.extract::()? && !machine_output && stdout_supports_color(py)?; let stream: Bound<'py, PyAny> = @@ -139,11 +140,11 @@ fn user_error(py: Python<'_>, message: String) -> PyErr { } fn target_name<'py>(source: &Bound<'py, PyAny>) -> PyResult> { - let table: Bound<'_, PyAny> = source.getattr("table")?; + let table: Bound<'_, PyAny> = native_field(source, "table")?; if table.is_truthy()? { Ok(table) } else { - source.getattr("name") + native_field(source, "name") } } @@ -159,7 +160,7 @@ fn check_reference_targets( let selected_names: Vec = invocation .selected .iter() - .map(|source| source.getattr("name")?.extract()) + .map(|source| native_field(source, "name")?.extract()) .collect::>()?; let connection: Bound<'_, PyAny> = adapter.call_method1("connect", (connection_config,))?; let checked: PyResult<()> = (|| { @@ -170,8 +171,8 @@ fn check_reference_targets( PyTuple::new( py, [ - source.getattr("database")?, - source.getattr("schema")?, + native_field(source, "database")?, + native_field(source, "schema")?, target_name(source)?, ], ) @@ -188,10 +189,10 @@ fn check_reference_targets( for source in &invocation.references { let target: Bound<'_, PyAny> = target_name(source)?; let exists: bool = call_with( - &lookup.getattr("exists")?, + &native_field(&lookup, "exists")?, &[ - ("database", source.getattr("database")?), - ("schema", source.getattr("schema")?), + ("database", native_field(source, "database")?), + ("schema", native_field(source, "schema")?), ("name", target.clone()), ], )? @@ -203,7 +204,7 @@ fn check_reference_targets( "Selected source load requires intermediate loader '{}', but its target \ relation '{}' does not exist; use +{} to refresh upstream ingress \ dependencies", - source.getattr("name")?.str()?, + native_field(source, "name")?.str()?, target.str()?, selected_names.join(", ") ), @@ -240,8 +241,8 @@ fn project_relations<'py>( .chain(&invocation.references) .chain(&invocation.selected) { - if source.getattr("expression")?.is_none() { - sources.set_item(source.getattr("name")?, source)?; + if native_field(source, "expression")?.is_none() { + sources.set_item(native_field(source, "name")?, source)?; } } let reference_class: Bound<'py, PyAny> = host::entry(py, UserCodeHost::SqlResourceRef)?; @@ -270,8 +271,11 @@ fn prepare<'py>( let adapter_name: Bound<'py, PyAny> = call_with( ®istry::native(py, NativeAttr::ResolveEffectiveAdapterName)?, &[ - ("project_config", discovered.getattr("project_config")?), - ("local_config", discovered.getattr("local_config")?), + ( + "project_config", + native_field(discovered, "project_config")?, + ), + ("local_config", native_field(discovered, "local_config")?), ], )?; let adapter: Bound<'py, PyAny> = resolve_cli_adapter(&adapter_name, &invocation.project_dir)?; @@ -280,8 +284,8 @@ fn prepare<'py>( &[ ("discovered_inputs", discovered.clone()), ("project_dir", invocation.project_dir.clone()), - ("selected_target", request.getattr("selected_target")?), - ("cli_vars", request.getattr("cli_vars")?), + ("selected_target", native_field(request, "selected_target")?), + ("cli_vars", native_field(request, "cli_vars")?), ], )?; check_reference_targets(invocation, (&adapter, &connection_config))?; @@ -289,12 +293,12 @@ fn prepare<'py>( ®istry::native(py, NativeAttr::BuildEffectiveRuntimeConfig)?, &[ ("discovered_inputs", discovered.clone()), - ("selected_target", request.getattr("selected_target")?), - ("cli_vars", request.getattr("cli_vars")?), + ("selected_target", native_field(request, "selected_target")?), + ("cli_vars", native_field(request, "cli_vars")?), ], )? .extract()?; - let requested: Bound<'py, PyAny> = request.getattr("concurrency")?; + let requested: Bound<'py, PyAny> = native_field(request, "concurrency")?; let concurrency: i64 = if requested.is_none() { call_with( ®istry::native(py, NativeAttr::BuildEffectiveSettingsConfig)?, @@ -307,7 +311,10 @@ fn prepare<'py>( }; let typed: Bound<'py, PyAny> = call_with( ®istry::native(py, NativeAttr::TypeCursorOverrides)?, - &[("cursor_overrides", request.getattr("cursor_overrides")?)], + &[( + "cursor_overrides", + native_field(request, "cursor_overrides")?, + )], )?; let cursor_overrides: Bound<'py, PyAny> = if typed.is_truthy()? { typed @@ -316,7 +323,10 @@ fn prepare<'py>( }; let provider_session: Bound<'py, PyAny> = call_with( &host::entry(py, UserCodeHost::BuildProviderSession)?, - &[("discovered_providers", discovered.getattr("providers")?)], + &[( + "discovered_providers", + native_field(discovered, "providers")?, + )], )?; Ok(Preparation { adapter_name: adapter_name.extract()?, @@ -351,22 +361,22 @@ fn runtime_params<'py>( ), ("target", target_name.clone()), ("vars", effective_vars.clone()), - ("is_reload", request.getattr("reload")?), + ("is_reload", native_field(request, "reload")?), ( "start_cursor_ts", - cursor_timestamp(&overrides.getattr("start_ts")?)?, + cursor_timestamp(&native_field(overrides, "start_ts")?)?, ), ( "end_cursor_ts", - cursor_timestamp(&overrides.getattr("end_ts")?)?, + cursor_timestamp(&native_field(overrides, "end_ts")?)?, ), ( "start_cursor_int", - cursor_integer(&overrides.getattr("start_int")?)?, + cursor_integer(&native_field(overrides, "start_int")?)?, ), ( "end_cursor_int", - cursor_integer(&overrides.getattr("end_int")?)?, + cursor_integer(&native_field(overrides, "end_int")?)?, ), ( "use_color", @@ -376,7 +386,7 @@ fn runtime_params<'py>( ), ( "providers", - preparation.provider_session.getattr("providers")?, + native_field(&preparation.provider_session, "providers")?, ), ("project_relations", preparation.project_relations.clone()), ], @@ -395,7 +405,7 @@ fn execute<'py>( registry::native(py, NativeAttr::ExecutionEventWriter)?.call0()?; let order: Bound<'py, PyDict> = PyDict::new(py); for (index, source) in invocation.selected.iter().enumerate() { - order.set_item(source.getattr("name")?, index + 1)?; + order.set_item(native_field(source, "name")?, index + 1)?; } let reporter: Bound<'py, LoadProgressReporter> = registry::native(py, NativeAttr::LoadProgressReporter)? @@ -440,18 +450,24 @@ fn execute<'py>( let callbacks: Bound<'py, PyAny> = call_with( &host::entry(py, ExecutorHost::LoadCallbacks)?, &[ - ("on_load_start", reporter.getattr("on_start")?), - ("on_load_progress", reporter.getattr("on_progress")?), - ("on_load_complete", relay.getattr("on_load_complete")?), + ("on_load_start", native_field(&reporter, "on_start")?), + ("on_load_progress", native_field(&reporter, "on_progress")?), + ( + "on_load_complete", + native_field(&relay, "on_load_complete")?, + ), ( "on_connection_start", - connection.getattr("on_connection_start")?, + native_field(&connection, "on_connection_start")?, ), ( "on_connection_complete", - relay.getattr("on_connection_complete")?, + native_field(&relay, "on_connection_complete")?, + ), + ( + "on_connection_error", + native_field(&relay, "on_connection_error")?, ), - ("on_connection_error", relay.getattr("on_connection_error")?), ], )?; call_with( @@ -467,7 +483,7 @@ fn execute<'py>( ), ( "loader_functions", - invocation.discovered.getattr("loader_functions")?, + native_field(&invocation.discovered, "loader_functions")?, ), ("connection_config", preparation.connection_config.clone()), ("adapter", preparation.adapter.clone()), @@ -498,8 +514,11 @@ fn write_json(request: &Bound<'_, PyAny>, results: &[Bound<'_, PyAny>]) -> PyRes ®istry::native(py, NativeAttr::WriteExecutionJsonOutput)?, &[ ("payload", payload), - ("json_output", request.getattr("json_output")?), - ("json_output_path", request.getattr("json_output_path")?), + ("json_output", native_field(request, "json_output")?), + ( + "json_output_path", + native_field(request, "json_output_path")?, + ), ], )?; Ok(()) @@ -534,7 +553,7 @@ fn run_load_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult .iter() .map(|source| { Ok(( - source.getattr("name")?.extract()?, + native_field(source, "name")?.extract()?, loader_node_flag(source)?, )) }) 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 703f54293a..ea751efde9 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/load/load_progress.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/load/load_progress.rs @@ -15,6 +15,7 @@ use crate::bindings::_helpers::build_progress::terminal::start_spinner; 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; @@ -38,16 +39,16 @@ pub(crate) fn load_result_text(result: &Bound<'_, PyAny>) -> PyResult = host::entry(py, AdapterHost::LifeCycleEventKind)?.getattr("LOG")?; let mut log_messages: Vec = Vec::new(); - for event in result.getattr("lifecycle_events")?.try_iter()? { + for event in native_field(result, "lifecycle_events")?.try_iter()? { let event: Bound<'_, PyAny> = event?; - if event.getattr("kind")?.eq(&log_kind)? { - log_messages.push(event.getattr("content")?.extract()?); + if native_field(&event, "kind")?.eq(&log_kind)? { + log_messages.push(native_field(&event, "content")?.extract()?); } } - let duration: Bound<'_, PyAny> = result.getattr("duration_ms")?; + let duration: Bound<'_, PyAny> = native_field(result, "duration_ms")?; Ok(LoadResultText { - source_name: result.getattr("source_name")?.extract()?, - loader_name: result.getattr("loader_name")?.str()?.extract()?, + source_name: native_field(result, "source_name")?.extract()?, + loader_name: native_field(result, "loader_name")?.str()?.extract()?, resource_kind: result .getattr("resource_kind")? .getattr("value")? @@ -58,15 +59,15 @@ pub(crate) fn load_result_text(result: &Bound<'_, PyAny>) -> PyResult()?, ), - rows_loaded: result.getattr("rows_loaded")?.extract()?, + rows_loaded: native_field(result, "rows_loaded")?.extract()?, duration_ms: if duration.is_none() { None } else { Some(duration.extract()?) }, log_messages, - error_message: optional_text(result.getattr("error_message")?)?, - warning_messages: result.getattr("warning_messages")?.extract()?, + error_message: optional_text(native_field(result, "error_message")?)?, + warning_messages: native_field(result, "warning_messages")?.extract()?, }) } @@ -212,7 +213,7 @@ impl LoadProgressReporter { Ok(Self { is_tty: stream.call_method0("isatty")?.is_truthy()?, stream: stream.unbind(), - write_lock: lines.getattr("lock")?.unbind(), + write_lock: native_field(&lines, "lock")?.unbind(), lines: lines.unbind(), style: CliStyle { use_color: arguments.value("use_color")?.is_truthy()?, @@ -237,7 +238,7 @@ impl LoadProgressReporter { { let mut state = reporter.state(); state.active = Some(ActiveSource { - name: source.getattr("name")?.extract()?, + name: native_field(source, "name")?.extract()?, resource_kind: if loader_node { "loader" } else { "source" }.to_owned(), }); state.message = String::new(); @@ -266,7 +267,7 @@ impl LoadProgressReporter { if !self.is_tty { return Ok(()); } - let name: String = source.getattr("name")?.extract()?; + let name: String = native_field(source, "name")?.extract()?; { let mut state = self.state(); match &state.active { 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 e9ef6cf49d..26feb7a965 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/load/load_selection.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/load/load_selection.rs @@ -17,6 +17,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::{ }; 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::{ErrorId, NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; @@ -46,8 +47,8 @@ pub(crate) fn environment_sources<'py>( 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 discovered.getattr("source_files")?.try_iter()? { - for source in source_file?.getattr("source_entries")?.try_iter()? { + 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"] { @@ -74,11 +75,11 @@ impl<'py> LoadProject<'py> { let sources: Vec> = environment_sources(discovered, target_config)?; let mut graph: LoadGraph = LoadGraph::default(); for source in &sources { - let loader: Bound<'py, PyAny> = source.getattr("loader")?; + let loader: Bound<'py, PyAny> = native_field(source, "loader")?; if !loader.is_none() { graph .sources - .push((source.getattr("name")?.extract()?, loader.extract()?)); + .push((native_field(source, "name")?.extract()?, loader.extract()?)); } } let functions: Vec> = discovered @@ -87,13 +88,16 @@ impl<'py> LoadProject<'py> { .collect::>()?; let names_by_function: Bound<'py, PyDict> = PyDict::new(py); for function in &functions { - names_by_function.set_item(function.getattr("function")?, function.getattr("name")?)?; + names_by_function.set_item( + native_field(function, "function")?, + native_field(function, "name")?, + )?; } let mut loaders: HashMap> = HashMap::new(); for function in functions { - let name: String = function.getattr("name")?.extract()?; + let name: String = native_field(&function, "name")?.extract()?; let mut dependencies: Vec = Vec::new(); - for dependency in function.getattr("depends_on")?.try_iter()? { + for dependency in native_field(&function, "depends_on")?.try_iter()? { if let Some(dependency) = names_by_function.get_item(dependency?)? { dependencies.push(dependency.extract()?); } @@ -116,7 +120,7 @@ impl<'py> LoadProject<'py> { ) -> PyResult> { let loader: &Bound<'py, PyAny> = &self.loaders[name]; let py: Python<'py> = loader.py(); - let destination: Bound<'py, PyAny> = loader.getattr("destination")?; + let destination: Bound<'py, PyAny> = native_field(loader, "destination")?; let destination: Bound<'py, PyAny> = if destination.is_none() { PyString::new(py, &format!("{LOADER_TABLE_PREFIX}{name}")).into_any() } else { @@ -137,16 +141,16 @@ impl<'py> LoadProject<'py> { ®istry::class(py, RecordId::SourceEntry)?, &[ ("name", name_value.clone()), - ("database", parts.getattr("database")?), - ("schema", parts.getattr("schema")?), - ("table", parts.getattr("table")?), + ("database", native_field(&parts, "database")?), + ("schema", native_field(&parts, "schema")?), + ("table", native_field(&parts, "table")?), ("loader", name_value), - ("write_strategy", loader.getattr("write_strategy")?), - ("cursor_column", loader.getattr("cursor_column")?), - ("unique_key", loader.getattr("unique_key")?), - ("contract", loader.getattr("contract")?), + ("write_strategy", native_field(loader, "write_strategy")?), + ("cursor_column", native_field(loader, "cursor_column")?), + ("unique_key", native_field(loader, "unique_key")?), + ("contract", native_field(loader, "contract")?), ("meta", meta.into_any()), - ("columns", loader.getattr("columns")?), + ("columns", native_field(loader, "columns")?), ], ) } @@ -162,8 +166,8 @@ impl<'py> LoadProject<'py> { .map_err(|error| unknown_selector(py, &error))?; let mut managed: HashMap> = HashMap::new(); for source in &self.sources { - if !source.getattr("loader")?.is_none() { - managed.insert(source.getattr("name")?.extract()?, source); + if !native_field(source, "loader")?.is_none() { + managed.insert(native_field(source, "name")?.extract()?, source); } } entries @@ -185,8 +189,8 @@ impl<'py> LoadProject<'py> { .iter() .map(|source| { Ok(( - source.getattr("name")?.extract()?, - source.getattr("loader")?.extract()?, + native_field(source, "name")?.extract()?, + native_field(source, "loader")?.extract()?, )) }) .collect::>()?; @@ -264,8 +268,11 @@ pub(crate) fn loader_defaults<'py>( let adapter_name: Bound<'py, PyAny> = call_with( ®istry::native(py, NativeAttr::ResolveEffectiveAdapterName)?, &[ - ("project_config", discovered.getattr("project_config")?), - ("local_config", discovered.getattr("local_config")?), + ( + "project_config", + native_field(discovered, "project_config")?, + ), + ("local_config", native_field(discovered, "local_config")?), ], )?; let duckdb: Bound<'py, PyAny> = 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 3a257f062e..3431f09483 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 @@ -17,6 +17,7 @@ use sqlbuild_core::json::models::{JsonInteger, JsonValue}; use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, imported}; use crate::bindings::_helpers::frontier::encoder::emit_error; +use crate::bindings::_helpers::records::fields::field as native_field; const VALUE_RENDERER_FIELD: &str = "_value_renderer"; /// Nesting at which a value counts as too deep to key, as Python's recursion limit stops it. @@ -109,7 +110,7 @@ impl<'py> CanonicalTypes<'py> { return Ok(JsonValue::Array(vec![ JsonValue::String("enum".to_owned()), JsonValue::String(type_name(&kind)?), - self.canonical(&value.getattr("value")?, depth + 1)?, + self.canonical(&native_field(value, "value")?, depth + 1)?, ])); } if value.is_none() { @@ -201,7 +202,7 @@ impl<'py> CanonicalTypes<'py> { if self.is_dataclass.call1((value,))?.is_truthy()? && !value.is_instance(&self.type_type)? { let mut fields: Vec = Vec::new(); for field in self.fields.call1((value,))?.try_iter()? { - let name: Bound<'py, PyAny> = field?.getattr("name")?; + let name: Bound<'py, PyAny> = native_field(&field?, "name")?; let item: Bound<'py, PyAny> = value.getattr(name.downcast::()?)?; fields.push(JsonValue::Array(vec![ text(name.downcast::()?)?, @@ -230,7 +231,7 @@ fn macro_store_token<'py>( let loaded: Bound<'py, PyAny> = arguments.value("loaded")?; let keyed: PyResult> = (|| { let types: CanonicalTypes<'py> = CanonicalTypes::load(py)?; - let name: JsonValue = text(loaded.getattr("name")?.downcast::()?)?; + let name: JsonValue = text(native_field(&loaded, "name")?.downcast::()?)?; let path: JsonValue = text( loaded .getattr("relative_path")? @@ -238,7 +239,7 @@ fn macro_store_token<'py>( .downcast::()?, )?; let identity: JsonValue = types.canonical(&arguments.value("identity")?, 0)?; - let raw_source: String = loaded.getattr("raw_source")?.extract()?; + let raw_source: String = native_field(&loaded, "raw_source")?.extract()?; macro_key((name, path), identity, &raw_source).map_err(emit_error) })(); match keyed { @@ -269,8 +270,8 @@ fn context_store_token<'py>( } else { let sorted = imported(py, "builtins", "sorted")?; let mut parts: Vec = vec![ - types.canonical(&declarations.getattr("constants")?, 0)?, - types.canonical(&declarations.getattr("enums")?, 0)?, + types.canonical(&native_field(&declarations, "constants")?, 0)?, + types.canonical(&native_field(&declarations, "enums")?, 0)?, ]; for name in ["inaccessible_constants", "inaccessible_enums"] { let mut items: Vec = Vec::new(); @@ -296,7 +297,7 @@ fn canonical_context<'py>( ) -> PyResult { let mut fields: Vec = Vec::new(); for field in types.fields.call1((macro_context,))?.try_iter()? { - let name: Bound<'py, PyAny> = field?.getattr("name")?; + let name: Bound<'py, PyAny> = native_field(&field?, "name")?; let name_text: String = name.extract()?; let value: Bound<'py, PyAny> = macro_context.getattr(name_text.as_str())?; let canonical: JsonValue = if name_text == VALUE_RENDERER_FIELD { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/manifest/builder.rs b/crates/sqlbuild-python/src/bindings/_helpers/manifest/builder.rs index 4f88fb792e..fc23adf7b1 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/manifest/builder.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/manifest/builder.rs @@ -21,6 +21,7 @@ use crate::bindings::_helpers::manifest::rows::{ }; 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; use crate::bindings::_helpers::records::registry; @@ -90,7 +91,7 @@ pub(crate) fn build_manifest(options: Option<&Bound<'_, PyDict>>) -> PyResult = host::entry(py, UserCodeHost::BuildHookIdentities)? - .call1((project.getattr("hook_functions")?,))?; + .call1((native_field(&project, "hook_functions")?,))?; compiler_guard(|| { let python_hooks = python_hooks(&project, &hook_identities)?; let ProjectRows { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/manifest/rows.rs b/crates/sqlbuild-python/src/bindings/_helpers/manifest/rows.rs index 217ea3d27d..5e352f28b4 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/manifest/rows.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/manifest/rows.rs @@ -13,6 +13,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; @@ -42,7 +43,7 @@ fn enum_value(value: &Bound<'_, PyAny>, name: &str) -> PyResult> if member.is_none() { return Ok(None); } - member.getattr("value")?.extract().map(Some) + native_field(&member, "value")?.extract().map(Some) } fn items<'py>(value: &Bound<'py, PyAny>, name: &str) -> PyResult>> { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/microbatches/conversion.rs b/crates/sqlbuild-python/src/bindings/_helpers/microbatches/conversion.rs index e9181b2e41..b8dfa3816d 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/microbatches/conversion.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/microbatches/conversion.rs @@ -12,6 +12,7 @@ use crate::bindings::_helpers::cursor::conversion::CursorSession; use crate::bindings::_helpers::cursor::scalars::as_scalar; 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; /// `MicrobatchStateError(message)`. pub(crate) fn state_error(py: Python<'_>, message: &str) -> PyErr { @@ -79,8 +80,8 @@ impl<'py> BoundParser<'py> { /// The bounds of an object with `start` and `end` attributes. pub(crate) fn interval(&mut self, interval: &Bound<'py, PyAny>) -> PyResult { Ok(BoundInterval { - start: self.bound(&interval.getattr("start")?)?, - end: self.bound(&interval.getattr("end")?)?, + start: self.bound(&native_field(interval, "start")?)?, + end: self.bound(&native_field(interval, "end")?)?, }) } } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/microbatches/projection.rs b/crates/sqlbuild-python/src/bindings/_helpers/microbatches/projection.rs index f4f62ee23b..fe5c70c1bd 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/microbatches/projection.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/microbatches/projection.rs @@ -18,6 +18,7 @@ use crate::bindings::_helpers::microbatches::conversion::{ }; 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; const COMPLETION_RECORD_TYPES: [&str; 2] = ["partition_completion", "synthetic_completion"]; const ORDINARY_COMPLETION: &str = "partition_completion"; @@ -62,11 +63,17 @@ fn python_coverage<'py>( PyString::new(py, &segment.interval.start).into_any(), ), ("end", PyString::new(py, &segment.interval.end).into_any()), - ("fingerprint_status", event.getattr("fingerprint_status")?), - ("model_version_hash", event.getattr("model_version_hash")?), - ("record_type", event.getattr("record_type")?), - ("completion_type", event.getattr("completion_type")?), - ("event_id", event.getattr("event_id")?), + ( + "fingerprint_status", + native_field(event, "fingerprint_status")?, + ), + ( + "model_version_hash", + native_field(event, "model_version_hash")?, + ), + ("record_type", native_field(event, "record_type")?), + ("completion_type", native_field(event, "completion_type")?), + ("event_id", native_field(event, "event_id")?), ], )?); } @@ -111,10 +118,10 @@ fn coverage<'py>( let mut completions: Vec = Vec::new(); for event in events.try_iter()? { let event: Bound<'py, PyAny> = event?; - let record_type: String = enum_text(&event.getattr("record_type")?)?; + let record_type: String = enum_text(&native_field(&event, "record_type")?)?; let (start, end) = ( - event.getattr("partition_start")?, - event.getattr("partition_end")?, + native_field(&event, "partition_start")?, + native_field(&event, "partition_end")?, ); if !COMPLETION_RECORD_TYPES.contains(&record_type.as_str()) || start.is_none() @@ -128,10 +135,10 @@ fn coverage<'py>( end: parser.bound(&end)?, }, ordinary: record_type == ORDINARY_COMPLETION, - unknown_fingerprint: enum_text(&event.getattr("fingerprint_status")?)? + unknown_fingerprint: enum_text(&native_field(&event, "fingerprint_status")?)? == UNKNOWN_FINGERPRINT, - created: timestamp(&event.getattr("created_at")?)?, - event_id: event.getattr("event_id")?.extract()?, + created: timestamp(&native_field(&event, "created_at")?)?, + event_id: native_field(&event, "event_id")?.extract()?, }); completion_events.push(event); } @@ -164,7 +171,7 @@ fn replay<'py>(request: Option<&Bound<'py, PyDict>>) -> PyResult = request.get("requirement")?; let current: String = request.get("current_model_version_hash")?.extract()?; let required: Option = - optional_text(&requirement.getattr("required_model_version_hash")?)?; + optional_text(&native_field(&requirement, "required_model_version_hash")?)?; let versions = ReplayVersions { required: required.as_deref(), current: ¤t, @@ -184,9 +191,9 @@ fn replay<'py>(request: Option<&Bound<'py, PyDict>>) -> PyResult = piece?; pieces.push(CoveragePiece { interval: parser.interval(&piece)?, - unknown_fingerprint: enum_text(&piece.getattr("fingerprint_status")?)? + unknown_fingerprint: enum_text(&native_field(&piece, "fingerprint_status")?)? == UNKNOWN_FINGERPRINT, - model_version_hash: optional_text(&piece.getattr("model_version_hash")?)?, + model_version_hash: optional_text(&native_field(&piece, "model_version_hash")?)?, }); } } @@ -225,20 +232,21 @@ fn latest<'py>( let objects: Vec> = events.try_iter()?.collect::>()?; let mut facts: Vec = Vec::with_capacity(objects.len()); for event in &objects { - let is_requirement: bool = enum_text(&event.getattr("record_type")?)? == REPLAY_REQUIREMENT; + let is_requirement: bool = + enum_text(&native_field(event, "record_type")?)? == REPLAY_REQUIREMENT; let required: Option = - optional_text(&event.getattr("required_model_version_hash")?)?; + optional_text(&native_field(event, "required_model_version_hash")?)?; let matching: bool = is_requirement && required.as_deref() == Some(current_model_version_hash); facts.push(RequirementFact { is_requirement, required_model_version_hash: required, created: if matching { - timestamp(&event.getattr("created_at")?)? + timestamp(&native_field(event, "created_at")?)? } else { 0.0 }, - event_id: event.getattr("event_id")?.extract()?, + event_id: native_field(event, "event_id")?.extract()?, }); } Ok(latest_requirement(&facts, current_model_version_hash).map(|index| objects[index].clone())) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/microbatches/statements.rs b/crates/sqlbuild-python/src/bindings/_helpers/microbatches/statements.rs index c801ce5f78..ea9060f12e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/microbatches/statements.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/microbatches/statements.rs @@ -20,6 +20,7 @@ use crate::bindings::_helpers::microbatches::conversion::{optional_text, state_e 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::fields::field as native_field; const MISSING_SCHEMA: &str = "microbatch history requires a target schema"; const EMPTY_INSERT: &str = "bulk microbatch event insert requires at least one event"; @@ -91,13 +92,13 @@ fn scope_table( scope: &Bound<'_, PyAny>, ) -> PyResult { let py: Python<'_> = scope.py(); - let schema: Bound<'_, PyAny> = scope.getattr("target_schema")?; + let schema: Bound<'_, PyAny> = native_field(scope, "target_schema")?; if schema.is_none() { return Err(state_error(py, MISSING_SCHEMA)); } qualified_table( render_qualified_name, - (&scope.getattr("target_database")?, &schema), + (&native_field(scope, "target_database")?, &schema), )? .ok_or_else(|| state_error(py, MISSING_SCHEMA)) } @@ -112,7 +113,7 @@ fn events_table<'py>( let Some(first) = listed.first() else { return Err(state_error(events.py(), empty)); }; - let table: String = scope_table(render_qualified_name, &first.getattr("scope")?)?; + let table: String = scope_table(render_qualified_name, &native_field(first, "scope")?)?; Ok((listed, table)) } @@ -160,14 +161,14 @@ fn read_scope( let layout: Layout = Layout::new(py)?; let wildcard: String = host::entry(py, ExecutorHost::MicrobatchGenerationWildcard)?.extract()?; - let generation: Bound<'_, PyAny> = scope.getattr("physical_generation_id")?; + let generation: Bound<'_, PyAny> = native_field(scope, "physical_generation_id")?; let generation: Option = if generation.extract::()? == wildcard { None } else { Some(layout.literal("physical_generation_id", &generation)?) }; - let scope_kind: String = layout.literal("scope_kind", &scope.getattr("scope_kind")?)?; - let scope_key: String = layout.literal("scope_key", &scope.getattr("scope_key")?)?; + let scope_kind: String = layout.literal("scope_kind", &native_field(scope, "scope_kind")?)?; + let scope_key: String = layout.literal("scope_key", &native_field(scope, "scope_key")?)?; Ok(read_scope_sql( &table, &ScopeRead { @@ -193,10 +194,10 @@ fn create_table(request: Option<&Bound<'_, PyDict>>) -> PyResult { let render_framework_type: Bound<'_, PyAny> = request.get("render_framework_type")?; let framework: Bound<'_, PyAny> = host::entry(py, AdapterHost::FrameworkType)?; let text_type: String = render_framework_type - .call1((framework.getattr("STRING")?,))? + .call1((native_field(&framework, "STRING")?,))? .extract()?; let timestamp_type: String = render_framework_type - .call1((framework.getattr("TIMESTAMP")?,))? + .call1((native_field(&framework, "TIMESTAMP")?,))? .extract()?; let layout: Layout = Layout::new(py)?; let columns: Vec = layout diff --git a/crates/sqlbuild-python/src/bindings/_helpers/model_config/authored_nodes.rs b/crates/sqlbuild-python/src/bindings/_helpers/model_config/authored_nodes.rs index 1c6cae106a..b3f393fb4f 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/model_config/authored_nodes.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/model_config/authored_nodes.rs @@ -1,5 +1,6 @@ //! Python objects read in place as authored header values. +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::prelude::{Bound, PyAny, PyAnyMethods}; use pyo3::types::{ PyBool, PyBoolMethods, PyDict, PyDictMethods, PyFloat, PyInt, PyList, PyListMethods, PyString, @@ -113,7 +114,7 @@ impl AuthoredNode for PyNode<'_> { fn is_date_like(&self) -> bool { let py = self.0.py(); py.import("datetime") - .and_then(|module| module.getattr("date")) + .and_then(|module| native_field(&module, "date")) .and_then(|date| self.0.is_instance(&date)) .unwrap_or(false) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/model_config/contract_classes.rs b/crates/sqlbuild-python/src/bindings/_helpers/model_config/contract_classes.rs index 2ff6218008..292d31795e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/model_config/contract_classes.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/model_config/contract_classes.rs @@ -7,6 +7,7 @@ use pyo3::types::{PyDict, PyDictMethods}; use crate::bindings::_helpers::native_host::facade::FacadeHost; 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::RecordId; use crate::bindings::_helpers::records::registry; @@ -104,7 +105,7 @@ fn members_by_value<'py>(enumeration: &Bound<'py, PyAny>) -> PyResult = PyDict::new(enumeration.py()); for member in enumeration.try_iter()? { let member: Bound<'py, PyAny> = member?; - members.set_item(member.getattr("value")?, member)?; + members.set_item(native_field(&member, "value")?, member)?; } Ok(members) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/model_config/described_models.rs b/crates/sqlbuild-python/src/bindings/_helpers/model_config/described_models.rs index ee11b901b1..83850b5aa7 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/model_config/described_models.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/model_config/described_models.rs @@ -10,6 +10,7 @@ use crate::bindings::_helpers::model_config::model_config_builder::{ layered_values, raised_config_error, }; use crate::bindings::_helpers::model_config::project_defaults::project_defaults_mapping; +use crate::bindings::_helpers::records::fields::field as native_field; const DESCRIPTION_KEY: &str = "description"; const MODEL_SCHEMA_KEY: &str = "model_schema"; @@ -29,20 +30,22 @@ fn externally_described_models<'py>( model_schemas: Bound<'py, PyDict>, ) -> PyResult>> { let py = inputs.py(); - let project_config: Bound<'py, PyAny> = inputs.getattr("project_config")?; + let project_config: Bound<'py, PyAny> = native_field(&inputs, "project_config")?; let defaults: Bound<'py, PyDict> = - project_defaults_mapping(&project_config.getattr("defaults")?)?; + project_defaults_mapping(&native_field(&project_config, "defaults")?)?; let path_defaults: Bound<'py, PyDict> = - project_config.getattr("path_defaults")?.downcast_into()?; + native_field(&project_config, "path_defaults")?.downcast_into()?; let path_keys: Vec = path_defaults .keys() .iter() .map(|key| key.extract::()) .collect::>()?; let mut described: Vec> = Vec::new(); - for model_file in inputs.getattr("model_files")?.try_iter()? { + for model_file in native_field(&inputs, "model_files")?.try_iter()? { let model_file: Bound<'py, PyAny> = model_file?; - let relative_path: String = model_file.getattr("relative_path")?.str()?.extract()?; + let relative_path: String = native_field(&model_file, "relative_path")? + .str()? + .extract()?; let mut values: Bound<'py, PyDict> = defaults.copy()?; match select_path_default(&relative_path, &path_keys) { PathDefaultChoice::Selected(Some(key)) => { @@ -53,7 +56,8 @@ fn externally_described_models<'py>( PathDefaultChoice::Selected(None) => {} PathDefaultChoice::Conflict(error) => return Err(raised_config_error(py, error)), } - let header: Bound<'py, PyDict> = model_file.getattr("header_values")?.downcast_into()?; + let header: Bound<'py, PyDict> = + native_field(&model_file, "header_values")?.downcast_into()?; values = layered_values(py, &values, &header)?; let schema_description: Option> = match values.get_item(MODEL_SCHEMA_KEY)? { @@ -65,7 +69,7 @@ fn externally_described_models<'py>( }; if non_empty_text(values.get_item(DESCRIPTION_KEY)?)? || non_empty_text(schema_description)? { - described.push(model_file.getattr("file_path")?); + described.push(native_field(&model_file, "file_path")?); } } Ok(described) 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 fab7173244..090d0d39e2 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 @@ -16,6 +16,7 @@ use crate::bindings::_helpers::boundary::panics::compiler_guard; use crate::bindings::_helpers::discovery::project_facts::{column_name, each, stem}; use crate::bindings::_helpers::model_config::authored_nodes::PyNode; use crate::bindings::_helpers::model_config::config_errors::native_config_error; +use crate::bindings::_helpers::records::fields::field as native_field; const FILE_PATH_ATTRIBUTE: &str = "file_path"; const RETURNS_KEY: &str = "returns"; @@ -60,16 +61,16 @@ fn discovered_names(inputs: &Bound<'_, PyAny>) -> PyResult { .into_iter() .collect(); let mut seeds: HashSet = HashSet::new(); - for schema in inputs.getattr("schema_files")?.try_iter()? { + for schema in native_field(inputs, "schema_files")?.try_iter()? { seeds.extend(each(&schema?, "seed_entries", column_name)?); } let mut sources: HashSet = HashSet::new(); - for file in inputs.getattr("source_files")?.try_iter()? { + for file in native_field(inputs, "source_files")?.try_iter()? { sources.extend(each(&file?, "source_entries", column_name)?); } let mut functions: HashSet = HashSet::new(); let mut table_functions: HashSet = HashSet::new(); - for file in inputs.getattr("sql_function_files")?.try_iter()? { + for file in native_field(inputs, "sql_function_files")?.try_iter()? { let file: Bound<'_, PyAny> = file?; let name: String = model_stem(&file)?; if returns_table(&file)? { @@ -83,7 +84,7 @@ fn discovered_names(inputs: &Bound<'_, PyAny>) -> PyResult { /// Whether a SQL function's header `returns` is a mapping whose only key is `table`. fn returns_table(file: &Bound<'_, PyAny>) -> PyResult { - let headers: Bound<'_, PyAny> = file.getattr("header_values")?; + let headers: Bound<'_, PyAny> = native_field(file, "header_values")?; let returns: Bound<'_, PyAny> = headers.call_method1("get", (RETURNS_KEY,))?; let Ok(returns) = returns.downcast::() else { return Ok(false); @@ -100,7 +101,7 @@ impl NativeModelValidator { let names: ResourceNames = discovered_names(&inputs)?; let custom_materializations: HashSet = each(&inputs, "materialization_files", |file| { - file.getattr("name")?.extract() + native_field(file, "name")?.extract() })? .into_iter() .collect(); diff --git a/crates/sqlbuild-python/src/bindings/_helpers/output_capture/dispatcher.rs b/crates/sqlbuild-python/src/bindings/_helpers/output_capture/dispatcher.rs index 9d0ce25de1..523a936c90 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/output_capture/dispatcher.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/output_capture/dispatcher.rs @@ -30,6 +30,7 @@ use crate::bindings::_helpers::native_threads::exit_signal::signal_interpreter_e use crate::bindings::_helpers::output_capture::values::{ freeze, object, output_error, pass_text, python_text, unencodable, }; +use crate::bindings::_helpers::records::fields::field as native_field; const THREAD_NAME: &str = "sqlbuild-output-exporter"; /// Attribute naming a capture thread's worker, for the exit hook. @@ -147,7 +148,7 @@ fn stop_output_capture_threads(py: Python<'_>) -> PyResult<()> { for thread in threads { if thread.call_method0("is_alive")?.is_truthy()? { call_with( - &thread.getattr("join")?, + &native_field(&thread, "join")?, &[("timeout", object(py, EXIT_JOIN_SECONDS)?)], )?; } @@ -471,7 +472,7 @@ impl OutputCaptureDispatcher { { return Ok(()); } - let stream = match stream.getattr("value")?.extract::()?.as_str() { + let stream = match native_field(stream, "value")?.extract::()?.as_str() { "stderr" => OutputStream::Stderr, _ => OutputStream::Stdout, }; @@ -518,7 +519,7 @@ impl OutputCaptureDispatcher { } let thread = self.thread.bind(py); call_with( - &thread.getattr("join")?, + &native_field(thread, "join")?, &[( "timeout", object(py, shared.shutdown_timeout_seconds.max(0.0))?, 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 bb6c15c7bb..8249be09a4 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/output_capture/records.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/output_capture/records.rs @@ -21,6 +21,7 @@ use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::output_capture::values::{ freeze, json_value, object, output_error, plain_int, validation_error, }; +use crate::bindings::_helpers::records::fields::field as native_field; const ENVELOPE_FIELDS: [&str; 16] = [ "record_id", @@ -68,7 +69,7 @@ fn optional_text(value: &Bound<'_, PyAny>) -> PyResult> { fn utc_aware(value: &Bound<'_, PyAny>) -> PyResult { let py = value.py(); if !value.is_instance(&imported(py, "datetime", "datetime")?)? - || value.getattr("tzinfo")?.is_none() + || native_field(value, "tzinfo")?.is_none() { return Ok(false); } @@ -117,7 +118,10 @@ pub(crate) fn freeze_command_output_record<'py>( let py = record.py(); validate_output_record(&record_shape(record)?) .map_err(|message| validation_error(py, &message))?; - let frozen = freeze(&record.getattr("external_context")?, "external_context")?; + let frozen = freeze( + &native_field(record, "external_context")?, + "external_context", + )?; if !frozen.is_instance(&imported(py, "collections.abc", "Mapping")?)? { return Err(validation_error( py, @@ -140,7 +144,7 @@ pub(crate) fn command_output_to_json(record: &Bound<'_, PyAny>) -> PyResult JsonValue::String(occurred_at.replace("+00:00", "Z")), - "stream" | "priority" => json_value(&field(name)?.getattr("value")?)?, + "stream" | "priority" => json_value(&native_field(&field(name)?, "value")?)?, _ => json_value(&field(name)?)?, }; entries.push((name.to_owned(), value)); @@ -260,7 +264,7 @@ pub(crate) fn command_output_from_json<'py>( validation_error(py, "command output contains invalid field values") } })?; - if !field("record_id")?.eq(record.getattr("record_id")?)? { + if !field("record_id")?.eq(native_field(&record, "record_id")?)? { return Err(validation_error( py, "command output record_id does not match its identity", 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 2db6d6e932..ed3a623375 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/plan_command/plan.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/plan_command/plan.rs @@ -13,6 +13,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::{call_with, importe use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; 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, RecordId}; use crate::bindings::_helpers::records::registry; @@ -57,7 +58,7 @@ struct PlanInvocation<'py> { fn resolve_invocation<'py>(request: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = request.py(); - let requested_dir: Bound<'py, PyAny> = request.getattr("project_dir")?; + let requested_dir: Bound<'py, PyAny> = native_field(request, "project_dir")?; let project_dir: Bound<'py, PyAny> = if requested_dir.is_none() { imported(py, "pathlib", "Path")?.call_method0("cwd")? } else { @@ -72,28 +73,29 @@ fn resolve_invocation<'py>(request: &Bound<'py, PyAny>) -> PyResult = PyDict::new(py); preview.set_item("discovered_inputs", &discovered)?; - preview.set_item("as_target", request.getattr("as_target")?)?; + 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))?; let context: Bound<'py, PyAny> = resolve_adapter_connection_context( &discovered, &project_dir, ( - request.getattr("selected_target")?, - request.getattr("cli_vars")?, + native_field(request, "selected_target")?, + native_field(request, "cli_vars")?, ), true, )?; - let json_output: bool = request.getattr("json_output")?.is_truthy()?; - let use_color: bool = - !request.getattr("no_color")?.is_truthy()? && !json_output && stdout_supports_color(py)?; + let json_output: bool = native_field(request, "json_output")?.is_truthy()?; + let use_color: bool = !native_field(request, "no_color")?.is_truthy()? + && !json_output + && stdout_supports_color(py)?; let stream: Bound<'py, PyAny> = 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", context.getattr("adapter_name")?), + ("adapter_name", native_field(&context, "adapter_name")?), ("stream", stream.clone()), ("use_color", flag(py, use_color)), ], @@ -105,7 +107,7 @@ fn resolve_invocation<'py>(request: &Bound<'py, PyAny>) -> PyResult = request.getattr("load_sources")?; + 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)?, @@ -136,23 +138,23 @@ fn connection_hooks<'py>(invocation: &PlanInvocation<'py>) -> PyResult( ("discovered_inputs", invocation.discovered.clone()), ], )?; - let as_target: Bound<'py, PyAny> = request.getattr("as_target")?; + let as_target: Bound<'py, PyAny> = native_field(request, "as_target")?; if !as_target.is_none() { invocation.stream.call_method1( "write", @@ -187,7 +189,7 @@ fn compile_pipeline<'py>( options.set_item( "selected_target", if as_target.is_none() { - request.getattr("selected_target")? + native_field(request, "selected_target")? } else { as_target }, @@ -198,12 +200,12 @@ fn compile_pipeline<'py>( options.set_item("auto_load_sources", &invocation.load_sources)?; options.set_item( "connection_config", - invocation.context.getattr("connection_config")?, + native_field(&invocation.context, "connection_config")?, )?; options.set_item("external_sql_reference_resolver", resolver)?; options.set_item( "resolve_python_run_selectors", - request.getattr("include_python")?, + native_field(request, "include_python")?, )?; options.set_item("record_migration_fingerprints", false)?; let pipeline_options: Bound<'py, PyAny> = @@ -212,7 +214,7 @@ fn compile_pipeline<'py>( ®istry::native(py, NativeAttr::RunCompilePipeline)?, &[ ("discovered_inputs", invocation.discovered.clone()), - ("adapter", invocation.context.getattr("adapter")?), + ("adapter", native_field(&invocation.context, "adapter")?), ("options", pipeline_options), ("hooks", connection_hooks(invocation)?), ], @@ -225,9 +227,9 @@ fn write_output( pipeline: &Bound<'_, PyAny>, ) -> PyResult<()> { let py: Python<'_> = request.py(); - let plan: Bound<'_, PyAny> = pipeline.getattr("plan_output")?; - let python_entries: Bound<'_, PyAny> = pipeline.getattr("python_plan_entries")?; - if request.getattr("json_output")?.is_truthy()? { + 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)], @@ -238,7 +240,7 @@ fn write_output( let display: Bound<'_, PyDict> = PyDict::new(py); display.set_item( "max_entries_per_section", - if request.getattr("verbose")?.is_truthy()? { + if native_field(request, "verbose")?.is_truthy()? { None } else { Some(DEFAULT_SECTION_ENTRIES) @@ -248,7 +250,7 @@ fn write_output( ®istry::native(py, NativeAttr::FormatPlan)?, &[ ("plan", plan), - ("full_refresh", request.getattr("full_refresh")?), + ("full_refresh", native_field(request, "full_refresh")?), ("use_color", flag(py, invocation.use_color)), ( "display_options", @@ -269,11 +271,11 @@ fn write_output( fn run_plan_command(request: &Bound<'_, PyAny>) -> PyResult { let py: Python<'_> = request.py(); let invocation: PlanInvocation<'_> = resolve_invocation(request)?; - if !request.getattr("json_output")?.is_truthy()? { + if !native_field(request, "json_output")?.is_truthy()? { invocation.stream.call_method1("write", ("\n",))?; invocation.stream.call_method0("flush")?; } - let verbose: bool = request.getattr("verbose")?.is_truthy()?; + let verbose: bool = native_field(request, "verbose")?.is_truthy()?; let pipeline: Bound<'_, PyAny> = with_verbose_inspection(py, verbose, || compile_pipeline(request, &invocation))?; write_output(request, &invocation, &pipeline)?; 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 3057151148..b3bfdd4ee5 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/plan_command/rules.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/plan_command/rules.rs @@ -8,6 +8,7 @@ use pyo3::wrap_pyfunction; use crate::bindings::_helpers::command_runtime::adapters::resolve_cli_adapter; 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, RecordId}; use crate::bindings::_helpers::records::registry; @@ -37,7 +38,7 @@ fn print_line(py: Python<'_>, text: &str, stderr: bool) -> PyResult<()> { fn rule_codes(catalogue: &Bound<'_, PyAny>) -> PyResult> { let mut codes: Vec = Vec::new(); for rule in catalogue.try_iter()? { - codes.push(rule?.getattr("code")?.extract()?); + codes.push(native_field(&rule?, "code")?.extract()?); } Ok(codes) } @@ -48,13 +49,13 @@ fn run_skills( project_dir: &Bound<'_, PyAny>, ) -> PyResult { let py: Python<'_> = request.py(); - let check: bool = request.getattr("skills_check")?.is_truthy()?; + 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", request.getattr("skills_check")?), + ("check", native_field(request, "skills_check")?), ], )? .is_truthy()?; @@ -82,8 +83,8 @@ fn list_rules(catalogue: &Bound<'_, PyAny>, json_output: bool) -> PyResult let rule: Bound<'_, PyAny> = rule?; lines.push(format!( "{} {}", - rule.getattr("code")?.str()?, - rule.getattr("message")?.str()? + native_field(&rule, "code")?.str()?, + native_field(&rule, "message")?.str()? )); } print_line(py, &lines.join("\n"), false)?; @@ -96,13 +97,13 @@ fn list_rules(catalogue: &Bound<'_, PyAny>, json_output: bool) -> PyResult for field in ["code", "family", "message", "custom"] { item.set_item(field, rule.getattr(field)?)?; } - let subject: Bound<'_, PyAny> = rule.getattr("subject")?; + let subject: Bound<'_, PyAny> = native_field(&rule, "subject")?; item.set_item( "subject", if subject.is_none() { subject } else { - subject.getattr("value")? + native_field(&subject, "value")? }, )?; rules.append(item)?; @@ -126,7 +127,7 @@ fn show_rule( let py: Python<'_> = catalogue.py(); for rule in catalogue.try_iter()? { let rule: Bound<'_, PyAny> = rule?; - if rule.getattr("code")?.extract::()? == selector { + if native_field(&rule, "code")?.extract::()? == selector { let text: String = call_with( ®istry::native(py, NativeAttr::FormatRule)?, &[("rule", rule), ("config", config.clone())], @@ -157,8 +158,8 @@ fn rule_inputs<'py>( ®istry::native(py, NativeAttr::DiscoverProjectInputs)?, &[("project_dir", project_dir.clone())], )?; - let select: Bound<'py, PyAny> = request.getattr("select")?; - let exclude: Bound<'py, PyAny> = request.getattr("exclude")?; + 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)?, &[ @@ -170,8 +171,11 @@ fn rule_inputs<'py>( let adapter_name: Bound<'py, PyAny> = call_with( ®istry::native(py, NativeAttr::ResolveEffectiveAdapterName)?, &[ - ("project_config", discovered.getattr("project_config")?), - ("local_config", discovered.getattr("local_config")?), + ( + "project_config", + native_field(&discovered, "project_config")?, + ), + ("local_config", native_field(&discovered, "local_config")?), ], )?; let adapter: Bound<'py, PyAny> = resolve_cli_adapter(&adapter_name, project_dir)?; @@ -223,8 +227,8 @@ fn run_rule_selection( graph, selected, } = rule_inputs(request, project_dir)?; - let narrowed: bool = - request.getattr("select")?.is_truthy()? || request.getattr("exclude")?.is_truthy()?; + let narrowed: bool = native_field(request, "select")?.is_truthy()? + || native_field(request, "exclude")?.is_truthy()?; let replaced: Bound<'_, PyDict> = PyDict::new(py); replaced.set_item("select", PyTuple::new(py, [selector])?)?; replaced.set_item("ignore", PyTuple::empty(py))?; @@ -259,7 +263,7 @@ fn run_rule_selection( )?; let note: Bound<'_, PyAny> = call_with( ®istry::native(py, NativeAttr::FormatSkippedTypeProofRules)?, - &[("codes", result.getattr("skipped_type_proof_rules")?)], + &[("codes", native_field(&result, "skipped_type_proof_rules")?)], )?; if !note.is_none() { print_line(py, &format!("note: {}", note.str()?), true)?; @@ -275,12 +279,12 @@ fn run_rule_selection( "result", registry::class(py, RecordId::RulesResult)?.call((), Some(&fields))?, ), - ("json_output", request.getattr("json_output")?), + ("json_output", native_field(request, "json_output")?), ], )? .extract()?; print_line(py, &text, false)?; - let findings: usize = result.getattr("findings")?.len()?; + let findings: usize = native_field(&result, "findings")?.len()?; if findings > 0 { print_line( py, @@ -298,7 +302,7 @@ fn run_rule_selection( #[pyo3(signature = (*, request))] fn run_rules_command(request: &Bound<'_, PyAny>) -> PyResult { let py: Python<'_> = request.py(); - let requested_dir: Bound<'_, PyAny> = request.getattr("project_dir")?; + let requested_dir: Bound<'_, PyAny> = native_field(request, "project_dir")?; let project_dir: Bound<'_, PyAny> = if requested_dir.is_none() { imported(py, "pathlib", "Path")?.call_method0("cwd")? } else { @@ -309,7 +313,7 @@ fn run_rules_command(request: &Bound<'_, PyAny>) -> PyResult { ®istry::native(py, NativeAttr::LoadRulesConfig)?, &[("project_dir", project_dir.clone())], )?; - let action: String = request.getattr("action")?.extract()?; + let action: String = native_field(request, "action")?.extract()?; if action == SKILLS_ACTION { return run_skills(request, &config, &project_dir); } @@ -321,9 +325,12 @@ fn run_rules_command(request: &Bound<'_, PyAny>) -> PyResult { ], )?; if action == LIST_ACTION { - return list_rules(&catalogue, request.getattr("json_output")?.is_truthy()?); + return list_rules( + &catalogue, + native_field(request, "json_output")?.is_truthy()?, + ); } - let selector: Option = request.getattr("rule_selector")?.extract()?; + let selector: Option = native_field(request, "rule_selector")?.extract()?; let Some(selector) = selector else { print_line(py, "A rule code or family prefix is required", false)?; return Ok(USAGE_EXIT_CODE); 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 f3d929fdb1..38561eada9 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,6 +9,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; @@ -54,8 +55,8 @@ fn grain_rank(grain: &str) -> PyResult { /// The coarsest timestamp grain declared across a model and its inputs. fn effective_grain(entry: &Bound<'_, PyAny>) -> PyResult> { - let declared: Option = optional_text(&entry.getattr("cursor_grain")?)?; - if !entry.getattr("cursor_type")?.eq(TIMESTAMP_CURSOR)? { + let declared: Option = optional_text(&native_field(entry, "cursor_grain")?)?; + if !native_field(entry, "cursor_type")?.eq(TIMESTAMP_CURSOR)? { return Ok(declared); } let mut effective: String = declared.unwrap_or_else(|| SECOND_GRAIN.to_owned()); @@ -65,8 +66,8 @@ fn effective_grain(entry: &Bound<'_, PyAny>) -> PyResult> { { return Ok(Some(effective)); } - for relation in entry.getattr("cursor_input_relations")?.try_iter()? { - let relation_grain: String = optional_text(&relation?.getattr("cursor_grain")?)? + for relation in native_field(entry, "cursor_input_relations")?.try_iter()? { + let relation_grain: String = optional_text(&native_field(&relation?, "cursor_grain")?)? .unwrap_or_else(|| SECOND_GRAIN.to_owned()); if grain_rank(&relation_grain)? > grain_rank(&effective)? { effective = relation_grain; @@ -82,7 +83,7 @@ fn count_batches( cursor_type: &str, ) -> PyResult> { let py: Python<'_> = bounds.py(); - let (start, end) = (bounds.getattr("start")?, bounds.getattr("end")?); + 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,))?; @@ -116,8 +117,8 @@ fn count_batches( return Ok(None); } let (start, end): (i128, i128) = ( - start.getattr("value")?.extract()?, - end.getattr("value")?.extract()?, + native_field(&start, "value")?.extract()?, + native_field(&end, "value")?.extract()?, ); let Ok(size) = batch_size.trim().replace('_', "").parse::() else { return Ok(None); @@ -135,50 +136,52 @@ pub(crate) fn cursor_details<'py>( entry: &Bound<'py, PyAny>, ) -> PyResult>> { let py: Python<'py> = entry.py(); - if entry.getattr("cursor_column")?.is_none() { + if native_field(entry, "cursor_column")?.is_none() { return Ok(None); } - let requested_start: Option = optional_text(&entry.getattr("start_cursor_override")?)?; - let requested_end: Option = optional_text(&entry.getattr("end_cursor_override")?)?; + let requested_start: Option = + optional_text(&native_field(entry, "start_cursor_override")?)?; + let requested_end: Option = + optional_text(&native_field(entry, "end_cursor_override")?)?; let mut any_runtime: bool = false; - for relation in entry.getattr("cursor_input_relations")?.try_iter()? { - any_runtime = any_runtime || relation?.getattr("is_runtime_owned")?.is_truthy()?; + for relation in native_field(entry, "cursor_input_relations")?.try_iter()? { + any_runtime = any_runtime || native_field(&relation?, "is_runtime_owned")?.is_truthy()?; } let runtime_owned: bool = any_runtime && !(requested_start.is_some() && requested_end.is_some()); - let microbatch_range: Bound<'py, PyAny> = entry.getattr("microbatch_range")?; + let microbatch_range: Bound<'py, PyAny> = native_field(entry, "microbatch_range")?; let mut resolved_bounds: Option> = if microbatch_range.is_truthy()? { Some(microbatch_range) } else { - let bounds: Bound<'py, PyAny> = entry.getattr("cursor_bounds")?; + let bounds: Bound<'py, PyAny> = native_field(entry, "cursor_bounds")?; (!bounds.is_none()).then_some(bounds) }; let sentinel: Bound<'py, PyAny> = host::entry(py, ExecutorHost::BoundSentinel)?; if let Some(bounds) = &resolved_bounds - && (bounds.getattr("start")?.is_instance(&sentinel)? - || bounds.getattr("end")?.is_instance(&sentinel)?) + && (native_field(bounds, "start")?.is_instance(&sentinel)? + || native_field(bounds, "end")?.is_instance(&sentinel)?) { resolved_bounds = None; } let effective_grain: Option = effective_grain(entry)?; - let declared_batch_size: Option = optional_text(&entry.getattr("batch_size")?)?; + let declared_batch_size: Option = optional_text(&native_field(entry, "batch_size")?)?; let effective_batch_size: Option = match (&declared_batch_size, &effective_grain) { (Some(batch_size), Some(grain)) => Some(resolve_effective_microbatch_batch_size( py, batch_size, grain, )?), (batch_size, _) => batch_size.clone(), }; - let cursor_type: Option = optional_text(&entry.getattr("cursor_type")?)?; + let cursor_type: Option = optional_text(&native_field(entry, "cursor_type")?)?; let planned_batch_count: Option = match (&resolved_bounds, &effective_batch_size, &cursor_type) { (Some(bounds), Some(batch_size), Some(cursor_type)) - if entry.getattr("incremental_mode")?.eq(MICROBATCH_MODE)? => + if native_field(entry, "incremental_mode")?.eq(MICROBATCH_MODE)? => { count_batches(bounds, batch_size, cursor_type)? } _ => None, }; - let empty_inputs: Bound<'py, PyAny> = entry.getattr("empty_cursor_inputs")?; + let empty_inputs: Bound<'py, PyAny> = native_field(entry, "empty_cursor_inputs")?; let resolution_status: &'static str = if empty_inputs.is_truthy()? { NO_INPUT_ROWS } else if resolved_bounds.is_some() { @@ -194,35 +197,35 @@ pub(crate) fn cursor_details<'py>( runtime_owned, resolution_status, resolved_bounds, - declared_grain: optional_text(&entry.getattr("cursor_grain")?)?, + declared_grain: optional_text(&native_field(entry, "cursor_grain")?)?, effective_grain, declared_batch_size, effective_batch_size, planned_batch_count, empty_inputs, - waiting_on_empty_inputs: entry.getattr("waiting_on_empty_inputs")?.is_truthy()?, + waiting_on_empty_inputs: native_field(entry, "waiting_on_empty_inputs")?.is_truthy()?, })) } /// `runtime_cursor_inputs_clause`: the runtime-owned cursor inputs this run brings up to date. pub(crate) fn runtime_inputs_clause(entry: &Bound<'_, PyAny>) -> PyResult { let mut labels: Vec = Vec::new(); - for relation in entry.getattr("cursor_input_relations")?.try_iter()? { + for relation in native_field(entry, "cursor_input_relations")?.try_iter()? { let relation: Bound<'_, PyAny> = relation?; - if !relation.getattr("is_runtime_owned")?.is_truthy()? { + if !native_field(&relation, "is_runtime_owned")?.is_truthy()? { continue; } - let input_name: Bound<'_, PyAny> = relation.getattr("input_name")?; + let input_name: Bound<'_, PyAny> = native_field(&relation, "input_name")?; let name: String = if input_name.is_truthy()? { text(&input_name)? } else { - text(&relation.getattr("relation")?)? + text(&native_field(&relation, "relation")?)? }; - let kind: Bound<'_, PyAny> = relation.getattr("input_kind")?; + let kind: Bound<'_, PyAny> = native_field(&relation, "input_kind")?; let label: String = if kind.is_none() { name } else { - format!("{} {name}", text(&kind.getattr("value")?)?) + format!("{} {name}", text(&native_field(&kind, "value")?)?) }; if !labels.contains(&label) { labels.push(label); 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 389dbc19b4..29d39adac2 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/plan_output/display.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/plan_output/display.rs @@ -23,6 +23,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; @@ -31,14 +32,14 @@ const DATETIME_MODULE: &str = "datetime"; fn label_facts(entry: &Bound<'_, PyAny>) -> PyResult { let field = |name: &str| -> PyResult> { optional_text(&entry.getattr(name)?) }; Ok(ModelLabelFacts { - materialization_type: text(&entry.getattr("materialization_type")?)?, - action: text(&entry.getattr("action")?)?, + materialization_type: text(&native_field(entry, "materialization_type")?)?, + action: text(&native_field(entry, "action")?)?, custom_materialization_name: field("custom_materialization_name")?, incremental_strategy: field("incremental_strategy")?, incremental_mode: field("incremental_mode")?, cursor_type: field("cursor_type")?, snapshot_strategy: field("snapshot_strategy")?, - historical: !entry.getattr("observed_at_column")?.is_none(), + historical: !native_field(entry, "observed_at_column")?.is_none(), historical_input: field("historical_input")?, }) } @@ -184,7 +185,7 @@ fn model_execution_annotation(entry: &Bound<'_, PyAny>) -> PyResult { fn materialization_type_display<'py>( resource_kind: &Bound<'py, PyAny>, ) -> PyResult> { - resource_kind.getattr("value") + native_field(resource_kind, "value") } fn names(input_names: &Bound<'_, PyAny>) -> PyResult> { @@ -224,18 +225,18 @@ 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> = value.getattr("value")?; + let inner: Bound<'py, PyAny> = native_field(value, "value")?; if value.is_instance(®istry::class(py, RecordId::TimestampValue)?)? { return Ok(inner); } let datetime: Bound<'py, PyAny> = imported(py, DATETIME_MODULE, "datetime")?; - let midnight: Bound<'py, PyAny> = datetime.getattr("min")?.call_method0("time")?; + let midnight: Bound<'py, PyAny> = native_field(&datetime, "min")?.call_method0("time")?; datetime.call_method1("combine", (inner, midnight)) } fn presume_utc<'py>(value: Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = value.py(); - if !value.getattr("tzinfo")?.is_none() { + if !native_field(&value, "tzinfo")?.is_none() { return Ok(value); } let options: Bound<'py, PyDict> = PyDict::new(py); @@ -257,7 +258,7 @@ pub(crate) fn aligned_timestamp_bounds<'py>( let py: Python<'py> = start.py(); let mut start_at: Bound<'py, PyAny> = as_datetime(start)?; let mut end_at: Bound<'py, PyAny> = as_datetime(end)?; - if start_at.getattr("tzinfo")?.is_none() != end_at.getattr("tzinfo")?.is_none() { + if native_field(&start_at, "tzinfo")?.is_none() != native_field(&end_at, "tzinfo")?.is_none() { start_at = presume_utc(start_at)?; end_at = presume_utc(end_at)?; } 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 f9bba2e78e..9b5ec90e53 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/plan_output/migrations.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/plan_output/migrations.rs @@ -4,6 +4,7 @@ use crate::bindings::_helpers::plan_output::plan_text::display_limit; use crate::bindings::_helpers::plan_output::values::{ iso, items, optional_text, relation_name, text, texts, }; +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult}; use pyo3::{pyfunction, wrap_pyfunction}; use sqlbuild_core::cli_text::models::{ @@ -21,71 +22,71 @@ fn label(value: &Bound<'_, PyAny>) -> PyResult> { if value.is_none() { return Ok(None); } - text(&value.getattr("label")?).map(Some) + text(&native_field(value, "label")?).map(Some) } fn decision(entry: &Bound<'_, PyAny>) -> PyResult { - let value = entry.getattr("decision")?; + let value = native_field(entry, "decision")?; Ok(MigrationDecisionText { value: text(&value)?, - label: text(&value.getattr("label")?)?, - blocks_build: entry.getattr("blocks_build")?.extract()?, - origin_missing: entry.getattr("origin_missing")?.extract()?, + label: text(&native_field(&value, "label")?)?, + blocks_build: native_field(entry, "blocks_build")?.extract()?, + origin_missing: native_field(entry, "origin_missing")?.extract()?, ..MigrationDecisionText::default() }) } fn migration(entry: &Bound<'_, PyAny>, materialization: Option) -> PyResult { let mut decision = decision(entry)?; - let value = entry.getattr("decision")?; - decision.decision_blocks_build = value.getattr("blocks_build")?.extract()?; - decision.checks_compatibility = value.getattr("checks_compatibility")?.extract()?; + let value = native_field(entry, "decision")?; + decision.decision_blocks_build = native_field(&value, "blocks_build")?.extract()?; + decision.checks_compatibility = native_field(&value, "checks_compatibility")?.extract()?; Ok(MigrationText { - model_name: text(&entry.getattr("model_name")?)?, - origin: text(&relation_name(&entry.getattr("origin")?)?)?, - destination: text(&relation_name(&entry.getattr("destination")?)?)?, + model_name: text(&native_field(entry, "model_name")?)?, + origin: text(&relation_name(&native_field(entry, "origin")?)?)?, + destination: text(&relation_name(&native_field(entry, "destination")?)?)?, decision, - compatibility: text(&entry.getattr("compatibility")?.getattr("value")?)?, - discovery: text(&entry.getattr("discovery")?.getattr("value")?)?, - completed_at: optional_text(&iso(&entry.getattr("completed_at")?)?)?, - target_label: text(&entry.getattr("target_label")?)?, - transfer: label(&entry.getattr("transfer")?)?, - transfer_fallback: label(&entry.getattr("transfer_fallback")?)?, - storage_transition: optional_text(&entry.getattr("storage_transition")?)?, - promotion: label(&entry.getattr("promotion")?)?, - findings: texts(&entry.getattr("compatibility_findings")?)?, + compatibility: text(&native_field(entry, "compatibility")?.getattr("value")?)?, + discovery: text(&native_field(entry, "discovery")?.getattr("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")?)?, + transfer_fallback: label(&native_field(entry, "transfer_fallback")?)?, + storage_transition: optional_text(&native_field(entry, "storage_transition")?)?, + promotion: label(&native_field(entry, "promotion")?)?, + findings: texts(&native_field(entry, "compatibility_findings")?)?, materialization, }) } fn old_name(entry: &Bound<'_, PyAny>) -> PyResult { - let expires = entry.getattr("expires_at")?; + let expires = native_field(entry, "expires_at")?; let expires_date = if expires.is_none() { None } else { Some(expires.call_method1("strftime", ("%Y-%m-%d",))?.extract()?) }; Ok(OldNameText { - model_name: text(&entry.getattr("model_name")?)?, - origin: text(&relation_name(&entry.getattr("origin")?)?)?, - destination: text(&relation_name(&entry.getattr("destination")?)?)?, - action: text(&entry.getattr("action")?)?, + model_name: text(&native_field(entry, "model_name")?)?, + origin: text(&relation_name(&native_field(entry, "origin")?)?)?, + destination: text(&relation_name(&native_field(entry, "destination")?)?)?, + action: text(&native_field(entry, "action")?)?, expires_date, - column_aliases: entry.getattr("column_aliases")?.extract()?, - grants_supported: entry.getattr("grants_supported")?.extract()?, - grants_copied: optional_text(&entry.getattr("grants_copied")?)?, - reason: optional_text(&entry.getattr("reason")?)?, + column_aliases: native_field(entry, "column_aliases")?.extract()?, + grants_supported: native_field(entry, "grants_supported")?.extract()?, + grants_copied: optional_text(&native_field(entry, "grants_copied")?)?, + reason: optional_text(&native_field(entry, "reason")?)?, }) } fn column(entry: &Bound<'_, PyAny>) -> PyResult { Ok(ColumnMigrationText { - model_name: text(&entry.getattr("model_name")?)?, - origin_column: text(&entry.getattr("origin_column")?)?, - destination_column: text(&entry.getattr("destination_column")?)?, + model_name: text(&native_field(entry, "model_name")?)?, + origin_column: text(&native_field(entry, "origin_column")?)?, + destination_column: text(&native_field(entry, "destination_column")?)?, decision: decision(entry)?, - discovery: text(&entry.getattr("discovery")?.getattr("value")?)?, - completed_at: optional_text(&iso(&entry.getattr("completed_at")?)?)?, + discovery: text(&native_field(entry, "discovery")?.getattr("value")?)?, + completed_at: optional_text(&iso(&native_field(entry, "completed_at")?)?)?, }) } @@ -96,24 +97,24 @@ pub(crate) fn migration_lines( display_options: &Bound<'_, PyAny>, ) -> PyResult> { let mut materializations: BTreeMap = BTreeMap::new(); - for entry in items(&plan.getattr("model_entries")?)? { + for entry in items(&native_field(plan, "model_entries")?)? { materializations.insert( - text(&entry.getattr("name")?)?, - text(&entry.getattr("materialization_type")?)?, + text(&native_field(&entry, "name")?)?, + text(&native_field(&entry, "materialization_type")?)?, ); } let mut migrations: Vec = Vec::new(); - for entry in items(&plan.getattr("migration_entries")?)? { - let name = text(&entry.getattr("model_name")?)?; + for entry in items(&native_field(plan, "migration_entries")?)? { + let name = text(&native_field(&entry, "model_name")?)?; migrations.push(migration(&entry, materializations.get(&name).cloned())?); } let report = MigrationTextReport { migrations, - old_names: items(&plan.getattr("old_name_view_entries")?)? + old_names: items(&native_field(plan, "old_name_view_entries")?)? .iter() .map(old_name) .collect::>()?, - columns: items(&plan.getattr("column_migration_entries")?)? + columns: items(&native_field(plan, "column_migration_entries")?)? .iter() .map(column) .collect::>()?, @@ -145,19 +146,19 @@ pub(crate) fn pre_build_lines( display_options: &Bound<'_, PyAny>, ) -> PyResult> { let mut table_types: Vec = Vec::new(); - for entry in items(&plan.getattr("table_type_entries")?)? { + for entry in items(&native_field(plan, "table_type_entries")?)? { table_types.push(TableTypeText { - model_name: text(&entry.getattr("model_name")?)?, - desired_type: text(&entry.getattr("desired_type")?)?, - actual_type: optional_text(&entry.getattr("actual_type")?)? + model_name: text(&native_field(&entry, "model_name")?)?, + desired_type: text(&native_field(&entry, "desired_type")?)?, + actual_type: optional_text(&native_field(&entry, "actual_type")?)? .filter(|actual| !actual.is_empty()), - source: text(&entry.getattr("source")?)?, - irreversible_warning: optional_text(&entry.getattr("irreversible_warning")?)?, + source: text(&native_field(&entry, "source")?)?, + irreversible_warning: optional_text(&native_field(&entry, "irreversible_warning")?)?, }); } let mut retention: Vec = Vec::new(); - for entry in items(&plan.getattr("retention_entries")?)? { - let request = entry.getattr("request")?; + for entry in items(&native_field(plan, "retention_entries")?)? { + let request = native_field(&entry, "request")?; let mut parts: Vec = Vec::new(); for field in ["database", "schema", "name"] { let part = request.getattr(field)?; @@ -167,13 +168,13 @@ pub(crate) fn pre_build_lines( } retention.push(RetentionText { scope: parts.join("."), - desired_days: request.getattr("desired_days")?.extract()?, - actual_days: days(&entry.getattr("actual_days")?)?, - effective_days: days(&entry.getattr("effective_days")?)?, - source: text(&entry.getattr("source")?)?, - direction: text(&entry.getattr("direction")?.getattr("value")?)?, - phase: text(&entry.getattr("phase")?.getattr("value")?)?, - irreversible_warning: optional_text(&entry.getattr("irreversible_warning")?)?, + desired_days: native_field(&request, "desired_days")?.extract()?, + 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")?)?, + irreversible_warning: optional_text(&native_field(&entry, "irreversible_warning")?)?, }); } let work = PreBuildWorkText { @@ -203,7 +204,7 @@ fn render_old_name(entry: &Bound<'_, PyAny>, style: &Bound<'_, PyAny>) -> PyResu Ok(old_name_rows::old_name_rows( &old_name(entry)?, CliStyle { - use_color: style.getattr("use_color")?.extract()?, + use_color: native_field(style, "use_color")?.extract()?, }, )) } 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 123d07fb5e..f9144957f4 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 @@ -17,6 +17,7 @@ use crate::bindings::_helpers::plan_output::safety::{future_cursor_safety, maxim use crate::bindings::_helpers::plan_output::values::{ any, dict, enum_value, iso, items, list, mapped, optional_enum_value, relation_name, text, }; +use crate::bindings::_helpers::records::fields::field as native_field; const MICROBATCH_MODE: &str = "microbatch"; const RENAMED_REASON: &str = "renamed"; @@ -32,8 +33,8 @@ fn token_bounds<'py>(bounds: &Bound<'py, PyAny>) -> PyResult> Ok(dict( bounds.py(), vec![ - ("start", token(bounds.getattr("start")?)?), - ("end", token(bounds.getattr("end")?)?), + ("start", token(native_field(bounds, "start")?)?), + ("end", token(native_field(bounds, "end")?)?), ], )? .into_any()) @@ -84,8 +85,8 @@ fn cursor_json<'py>(py: Python<'py>, details: &CursorDetails<'py>) -> PyResult) -> PyResult<&'static str> { - let expected: Bound<'_, PyAny> = entry.getattr("fingerprint_version_hash")?; - let built: Bound<'_, PyAny> = entry.getattr("previous_version_hash")?; + let expected: Bound<'_, PyAny> = native_field(entry, "fingerprint_version_hash")?; + let built: Bound<'_, PyAny> = native_field(entry, "previous_version_hash")?; Ok(if expected.is_none() { "unknown" } else if built.is_none() { @@ -99,14 +100,14 @@ fn identity_status(entry: &Bound<'_, PyAny>) -> PyResult<&'static str> { /// The safety evidence of the microbatch range when it has some, else of the cursor bounds. fn evidence<'py>(entry: &Bound<'py, PyAny>, field: &str) -> PyResult> { - let microbatch_range: Bound<'py, PyAny> = entry.getattr("microbatch_range")?; + 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)?; if !found.is_none() { return Ok(found); } } - let bounds: Bound<'py, PyAny> = entry.getattr("cursor_bounds")?; + let bounds: Bound<'py, PyAny> = native_field(entry, "cursor_bounds")?; if bounds.is_none() { return Ok(bounds); } @@ -119,23 +120,26 @@ fn microbatch_state<'py>(entry: &Bound<'py, PyAny>) -> PyResult = entry.getattr("microbatch_limit")?; + let limit: Bound<'py, PyAny> = native_field(entry, "microbatch_limit")?; if !limit.is_none() { state.set_item("limit", limit)?; - state.set_item("count", entry.getattr("microbatch_limit_count")?)?; + state.set_item("count", native_field(entry, "microbatch_limit_count")?)?; state.set_item( "action", - optional_enum_value(&entry.getattr("microbatch_limit_action")?)?, + optional_enum_value(&native_field(entry, "microbatch_limit_action")?)?, )?; - state.set_item("warning", entry.getattr("microbatch_limit_warning")?)?; + state.set_item("warning", native_field(entry, "microbatch_limit_warning")?)?; } Ok(state.into_any()) } @@ -145,10 +149,10 @@ fn model_json<'py>(entry: &Bound<'py, PyAny>) -> PyResult> { let model: Bound<'py, PyDict> = dict( py, vec![ - ("name", entry.getattr("name")?), + ("name", native_field(entry, "name")?), ( "relative_path", - entry.getattr("relative_path")?.str()?.into_any(), + native_field(entry, "relative_path")?.str()?.into_any(), ), ( "materialization_type", @@ -158,15 +162,15 @@ fn model_json<'py>(entry: &Bound<'py, PyAny>) -> PyResult> { ("reason", enum_value(entry, "reason")?), ( "expected_version_hash", - entry.getattr("fingerprint_version_hash")?, + native_field(entry, "fingerprint_version_hash")?, ), ( "built_version_hash", - entry.getattr("previous_version_hash")?, + native_field(entry, "previous_version_hash")?, ), ( "built_version_present", - any(py, !entry.getattr("previous_version_hash")?.is_none())?, + any(py, !native_field(entry, "previous_version_hash")?.is_none())?, ), ("identity_status", any(py, identity_status(entry)?)?), ], @@ -177,7 +181,7 @@ fn model_json<'py>(entry: &Bound<'py, PyAny>) -> PyResult> { model.set_item(key, value)?; } } - if entry.getattr("incremental_mode")?.eq(MICROBATCH_MODE)? { + if native_field(entry, "incremental_mode")?.eq(MICROBATCH_MODE)? { model.set_item("microbatch_state", microbatch_state(entry)?)?; } for key in ["cursor_column", "cursor_type"] { @@ -186,7 +190,7 @@ fn model_json<'py>(entry: &Bound<'py, PyAny>) -> PyResult> { model.set_item(key, value)?; } } - let bounds: Bound<'py, PyAny> = entry.getattr("cursor_bounds")?; + let bounds: Bound<'py, PyAny> = native_field(entry, "cursor_bounds")?; if !bounds.is_none() { model.set_item("cursor_bounds", token_bounds(&bounds)?)?; } @@ -202,31 +206,32 @@ fn model_json<'py>(entry: &Bound<'py, PyAny>) -> PyResult> { if let Some(details) = cursor_details(entry)? { model.set_item("cursor", cursor_json(py, &details)?)?; } - let backfill: Bound<'py, PyAny> = entry.getattr("backfill")?; + let backfill: Bound<'py, PyAny> = native_field(entry, "backfill")?; model.set_item( "backfill", dict( py, vec![ ("action", enum_value(&backfill, "action")?), - ("duration", backfill.getattr("duration")?), + ("duration", native_field(&backfill, "duration")?), ], )?, )?; - if entry.getattr("reason")?.eq(RENAMED_REASON)? - && entry.getattr("query_changed")?.is_truthy()? + if native_field(entry, "reason")?.eq(RENAMED_REASON)? + && native_field(entry, "query_changed")?.is_truthy()? { model.set_item("query_changed", true)?; } - let changed_functions: Bound<'py, PyAny> = entry.getattr("changed_functions")?; + let changed_functions: Bound<'py, PyAny> = native_field(entry, "changed_functions")?; if changed_functions.is_truthy()? { model.set_item("changed_functions", list(&changed_functions)?)?; } - let qualified: Bound<'py, PyAny> = entry.getattr("destination")?.getattr("qualified_name")?; + let qualified: Bound<'py, PyAny> = + native_field(entry, "destination")?.getattr("qualified_name")?; if !qualified.is_none() { model.set_item("qualified_name", qualified)?; } - let hints: Bound<'py, PyAny> = entry.getattr("column_rename_hints")?; + let hints: Bound<'py, PyAny> = native_field(entry, "column_rename_hints")?; if hints.is_truthy()? { model.set_item( "column_rename_hints", @@ -234,10 +239,13 @@ fn model_json<'py>(entry: &Bound<'py, PyAny>) -> PyResult> { Ok(dict( py, vec![ - ("column", hint.getattr("added_column")?), - ("candidates", list(&hint.getattr("candidate_columns")?)?), - ("identical", hint.getattr("identical")?), - ("hint", hint.getattr("message")?), + ("column", native_field(hint, "added_column")?), + ( + "candidates", + list(&native_field(hint, "candidate_columns")?)?, + ), + ("identical", native_field(hint, "identical")?), + ("hint", native_field(hint, "message")?), ], )? .into_any()) @@ -251,7 +259,8 @@ fn with_qualified_name<'py>( item: Bound<'py, PyDict>, entry: &Bound<'py, PyAny>, ) -> PyResult> { - let qualified: Bound<'py, PyAny> = entry.getattr("destination")?.getattr("qualified_name")?; + let qualified: Bound<'py, PyAny> = + native_field(entry, "destination")?.getattr("qualified_name")?; if !qualified.is_none() { item.set_item("qualified_name", qualified)?; } @@ -264,7 +273,7 @@ fn seed_json<'py>(entry: &Bound<'py, PyAny>) -> PyResult> { dict( py, vec![ - ("name", entry.getattr("name")?), + ("name", native_field(entry, "name")?), ("reason", enum_value(entry, "reason")?), ], )?, @@ -277,22 +286,22 @@ fn source_load_json<'py>(entry: &Bound<'py, PyAny>) -> PyResult = dict( py, vec![ - ("name", entry.getattr("name")?), - ("loader", entry.getattr("loader")?), + ("name", native_field(entry, "name")?), + ("loader", native_field(entry, "loader")?), ("kind", enum_value(entry, "resource_kind")?), - ("target", entry.getattr("destination")?), - ("is_reload", entry.getattr("is_reload")?), + ("target", native_field(entry, "destination")?), + ("is_reload", native_field(entry, "is_reload")?), ], )?; - let strategy: Bound<'py, PyAny> = entry.getattr("write_strategy")?; + let strategy: Bound<'py, PyAny> = native_field(entry, "write_strategy")?; if !strategy.is_none() { - load.set_item("write_strategy", strategy.getattr("value")?)?; + load.set_item("write_strategy", native_field(&strategy, "value")?)?; } - let cursor: Bound<'py, PyAny> = entry.getattr("cursor_column")?; + let cursor: Bound<'py, PyAny> = native_field(entry, "cursor_column")?; if !cursor.is_none() { load.set_item("cursor_column", cursor)?; } - let unique_key: Bound<'py, PyAny> = entry.getattr("unique_key")?; + let unique_key: Bound<'py, PyAny> = native_field(entry, "unique_key")?; if unique_key.is_truthy()? { load.set_item("unique_key", unique_key)?; } @@ -301,15 +310,15 @@ fn source_load_json<'py>(entry: &Bound<'py, PyAny>) -> PyResult(entry: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = entry.py(); - let columns: Bound<'py, PyAny> = entry.getattr("return_columns")?; + let columns: Bound<'py, PyAny> = native_field(entry, "return_columns")?; with_qualified_name( dict( py, vec![ - ("name", entry.getattr("name")?), + ("name", native_field(entry, "name")?), ( "relative_path", - entry.getattr("relative_path")?.str()?.into_any(), + native_field(entry, "relative_path")?.str()?.into_any(), ), ("language", enum_value(entry, "language")?), ( @@ -323,15 +332,15 @@ fn function_json<'py>(entry: &Bound<'py, PyAny>) -> PyResult> }, )?, ), - ("returns", entry.getattr("returns")?), + ("returns", native_field(entry, "returns")?), ( "return_columns", mapped(&columns, |column| { Ok(dict( py, vec![ - ("name", column.getattr("name")?), - ("type", column.getattr("type")?), + ("name", native_field(column, "name")?), + ("type", native_field(column, "type")?), ], )? .into_any()) @@ -349,7 +358,7 @@ fn warning_json<'py>(warning: &Bound<'py, PyAny>) -> PyResult> py, vec![ ("severity", enum_value(warning, "severity")?), - ("message", warning.getattr("message")?), + ("message", native_field(warning, "message")?), ], )?; for key in ["model_name", "code"] { @@ -388,19 +397,19 @@ pub(crate) fn unified_diff<'py>( fn identity_diff<'py>(entry: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = entry.py(); let result: Bound<'py, PyDict> = PyDict::new(py); - if !entry.getattr("identity_status")?.eq(CHANGED_IDENTITY)? { + if !native_field(entry, "identity_status")?.eq(CHANGED_IDENTITY)? { return Ok(result); } let pairs = [ ( "source_diff", - definition_source_text(&entry.getattr("previous_definition_json")?)?, - definition_source_text(&entry.getattr("current_definition_json")?)?, + definition_source_text(&native_field(entry, "previous_definition_json")?)?, + definition_source_text(&native_field(entry, "current_definition_json")?)?, ), ( "dependency_diff", - dependency_source_text(&entry.getattr("previous_metadata_json")?)?, - dependency_source_text(&entry.getattr("current_metadata_json")?)?, + dependency_source_text(&native_field(entry, "previous_metadata_json")?)?, + dependency_source_text(&native_field(entry, "current_metadata_json")?)?, ), ]; for (key, previous, current) in pairs { @@ -421,7 +430,7 @@ fn python_entry_json<'py>(entry: &Bound<'py, PyAny>) -> PyResult = dict( py, vec![ - ("name", entry.getattr("name")?), + ("name", native_field(entry, "name")?), ("kind", enum_value(entry, "kind")?), ("phase", enum_value(entry, "phase")?), ("identity_status", enum_value(entry, "identity_status")?), @@ -448,31 +457,31 @@ fn providers_json<'py>( ) -> PyResult> { let py: Python<'py> = plan.py(); let mut usages: Vec> = Vec::new(); - for usage in items(&plan.getattr("provider_usages")?)? { + for usage in items(&native_field(plan, "provider_usages")?)? { usages.push(( - text(&usage.getattr("provider_name")?)?, + text(&native_field(&usage, "provider_name")?)?, ( - text(&usage.getattr("consumer_kind")?)?, - text(&usage.getattr("consumer_name")?)?, - text(&usage.getattr("parameter_name")?)?, + text(&native_field(&usage, "consumer_kind")?)?, + text(&native_field(&usage, "consumer_name")?)?, + text(&native_field(&usage, "parameter_name")?)?, ), - usage.getattr("annotation_class_name")?, - usage.getattr("annotation_module")?, + native_field(&usage, "annotation_class_name")?, + native_field(&usage, "annotation_module")?, )); } for entry in python_entries { let kind: String = text(&enum_value(entry, "kind")?)?; - let name: String = text(&entry.getattr("name")?)?; - for usage in items(&entry.getattr("provider_usages")?)? { + let name: String = text(&native_field(entry, "name")?)?; + for usage in items(&native_field(entry, "provider_usages")?)? { usages.push(( - text(&usage.getattr("provider_name")?)?, + text(&native_field(&usage, "provider_name")?)?, ( kind.clone(), name.clone(), - text(&usage.getattr("parameter_name")?)?, + text(&native_field(&usage, "parameter_name")?)?, ), - usage.getattr("annotation_class_name")?, - usage.getattr("annotation_module")?, + native_field(&usage, "annotation_class_name")?, + native_field(&usage, "annotation_module")?, )); } } @@ -517,19 +526,22 @@ fn old_name_json<'py>(entry: &Bound<'py, PyAny>) -> PyResult> Ok(dict( py, vec![ - ("model", entry.getattr("model_name")?), + ("model", native_field(entry, "model_name")?), ("action", enum_value(entry, "action")?), - ("view", relation_name(&entry.getattr("origin")?)?), - ("reads", relation_name(&entry.getattr("destination")?)?), - ("expires_at", iso(&entry.getattr("expires_at")?)?), + ("view", relation_name(&native_field(entry, "origin")?)?), + ( + "reads", + relation_name(&native_field(entry, "destination")?)?, + ), + ("expires_at", iso(&native_field(entry, "expires_at")?)?), ( "column_aliases", py.import("builtins")? .getattr("dict")? - .call1((entry.getattr("column_aliases")?,))?, + .call1((native_field(entry, "column_aliases")?,))?, ), - ("grants_copied", entry.getattr("grants_copied")?), - ("reason", entry.getattr("reason")?), + ("grants_copied", native_field(entry, "grants_copied")?), + ("reason", native_field(entry, "reason")?), ], )? .into_any()) @@ -538,23 +550,24 @@ fn old_name_json<'py>(entry: &Bound<'py, PyAny>) -> PyResult> fn migrations_json<'py>(plan: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = plan.py(); let old_names: Bound<'py, PyDict> = PyDict::new(py); - for entry in items(&plan.getattr("old_name_view_entries")?)? { - old_names.set_item(entry.getattr("model_name")?, entry)?; + for entry in items(&native_field(plan, "old_name_view_entries")?)? { + old_names.set_item(native_field(&entry, "model_name")?, entry)?; } let materializations: Bound<'py, PyDict> = PyDict::new(py); - for entry in items(&plan.getattr("model_entries")?)? { + for entry in items(&native_field(plan, "model_entries")?)? { materializations.set_item( - entry.getattr("name")?, - entry.getattr("materialization_type")?.str()?, + native_field(&entry, "name")?, + native_field(&entry, "materialization_type")?.str()?, )?; } - mapped(&plan.getattr("migration_entries")?, |entry| { - let model: Bound<'py, PyAny> = entry.getattr("model_name")?; - let transfer: Bound<'py, PyAny> = entry.getattr("transfer")?; - let completed_at: Bound<'py, PyAny> = entry.getattr("completed_at")?; + mapped(&native_field(plan, "migration_entries")?, |entry| { + let model: Bound<'py, PyAny> = native_field(entry, "model_name")?; + let transfer: Bound<'py, PyAny> = native_field(entry, "transfer")?; + let completed_at: Bound<'py, PyAny> = native_field(entry, "completed_at")?; let transfer_value: Bound<'py, PyAny> = if !transfer.is_none() { - transfer.getattr("value")? - } else if !entry.getattr("decision")?.eq(RENAMED_DECISION)? || !completed_at.is_none() { + native_field(&transfer, "value")? + } else if !native_field(entry, "decision")?.eq(RENAMED_DECISION)? || !completed_at.is_none() + { py.None().into_bound(py) } else { let view: bool = materializations @@ -576,25 +589,31 @@ fn migrations_json<'py>(plan: &Bound<'py, PyAny>) -> PyResult> ("compatibility", enum_value(entry, "compatibility")?), ( "compatibility_findings", - list(&entry.getattr("compatibility_findings")?)?, + list(&native_field(entry, "compatibility_findings")?)?, ), - ("origin_model", entry.getattr("origin_model")?), - ("origin", relation_name(&entry.getattr("origin")?)?), + ("origin_model", native_field(entry, "origin_model")?), + ("origin", relation_name(&native_field(entry, "origin")?)?), ( "destination", - relation_name(&entry.getattr("destination")?)?, + relation_name(&native_field(entry, "destination")?)?, + ), + ("target", native_field(entry, "target_name")?), + ( + "origin_version_hash", + native_field(entry, "origin_version_hash")?, ), - ("target", entry.getattr("target_name")?), - ("origin_version_hash", entry.getattr("origin_version_hash")?), ("transfer", transfer_value), ( "transfer_fallback", - optional_enum_value(&entry.getattr("transfer_fallback")?)?, + optional_enum_value(&native_field(entry, "transfer_fallback")?)?, + ), + ( + "storage_transition", + native_field(entry, "storage_transition")?, ), - ("storage_transition", entry.getattr("storage_transition")?), ( "promotion", - optional_enum_value(&entry.getattr("promotion")?)?, + optional_enum_value(&native_field(entry, "promotion")?)?, ), ("completed_at", iso(&completed_at)?), ("old_name", old_name), @@ -606,19 +625,25 @@ fn migrations_json<'py>(plan: &Bound<'py, PyAny>) -> PyResult> fn column_migrations_json<'py>(plan: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = plan.py(); - mapped(&plan.getattr("column_migration_entries")?, |entry| { + mapped(&native_field(plan, "column_migration_entries")?, |entry| { Ok(dict( py, vec![ ("kind", any(py, "column_migration")?), - ("model", entry.getattr("model_name")?), - ("relation", relation_name(&entry.getattr("destination")?)?), - ("origin_column", entry.getattr("origin_column")?), - ("destination_column", entry.getattr("destination_column")?), + ("model", native_field(entry, "model_name")?), + ( + "relation", + relation_name(&native_field(entry, "destination")?)?, + ), + ("origin_column", native_field(entry, "origin_column")?), + ( + "destination_column", + native_field(entry, "destination_column")?, + ), ("discovery", enum_value(entry, "discovery")?), ("decision", enum_value(entry, "decision")?), - ("target", entry.getattr("target_name")?), - ("completed_at", iso(&entry.getattr("completed_at")?)?), + ("target", native_field(entry, "target_name")?), + ("completed_at", iso(&native_field(entry, "completed_at")?)?), ], )? .into_any()) @@ -627,28 +652,28 @@ fn column_migrations_json<'py>(plan: &Bound<'py, PyAny>) -> PyResult(plan: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = plan.py(); - mapped(&plan.getattr("retention_entries")?, |entry| { - let request: Bound<'py, PyAny> = entry.getattr("request")?; + mapped(&native_field(plan, "retention_entries")?, |entry| { + let request: Bound<'py, PyAny> = native_field(entry, "request")?; Ok(dict( py, vec![ ("scope", enum_value(&request, "scope")?), - ("database", request.getattr("database")?), - ("schema", request.getattr("schema")?), - ("name", request.getattr("name")?), - ("models", entry.getattr("model_names")?), - ("desired_days", request.getattr("desired_days")?), - ("actual_days", entry.getattr("actual_days")?), - ("effective_days", entry.getattr("effective_days")?), - ("source", entry.getattr("source")?), + ("database", native_field(&request, "database")?), + ("schema", native_field(&request, "schema")?), + ("name", native_field(&request, "name")?), + ("models", native_field(entry, "model_names")?), + ("desired_days", native_field(&request, "desired_days")?), + ("actual_days", native_field(entry, "actual_days")?), + ("effective_days", native_field(entry, "effective_days")?), + ("source", native_field(entry, "source")?), ("direction", enum_value(entry, "direction")?), ("phase", enum_value(entry, "phase")?), - ("statements", entry.getattr("statements")?), + ("statements", native_field(entry, "statements")?), ( "irreversible_warning", - entry.getattr("irreversible_warning")?, + native_field(entry, "irreversible_warning")?, ), - ("decrease_policy", entry.getattr("decrease_policy")?), + ("decrease_policy", native_field(entry, "decrease_policy")?), ], )? .into_any()) @@ -657,20 +682,20 @@ fn retention_json<'py>(plan: &Bound<'py, PyAny>) -> PyResult> fn table_type_json<'py>(plan: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = plan.py(); - mapped(&plan.getattr("table_type_entries")?, |entry| { + mapped(&native_field(plan, "table_type_entries")?, |entry| { Ok(dict( py, vec![ ("kind", any(py, "table_type_conversion")?), - ("model", entry.getattr("model_name")?), - ("desired_type", entry.getattr("desired_type")?), - ("actual_type", entry.getattr("actual_type")?), - ("source", entry.getattr("source")?), - ("downgrade", entry.getattr("downgrade")?), - ("downgrade_policy", entry.getattr("downgrade_policy")?), + ("model", native_field(entry, "model_name")?), + ("desired_type", native_field(entry, "desired_type")?), + ("actual_type", native_field(entry, "actual_type")?), + ("source", native_field(entry, "source")?), + ("downgrade", native_field(entry, "downgrade")?), + ("downgrade_policy", native_field(entry, "downgrade_policy")?), ( "irreversible_warning", - entry.getattr("irreversible_warning")?, + native_field(entry, "irreversible_warning")?, ), ], )? @@ -718,22 +743,28 @@ fn format_plan_json(py: Python<'_>, options: Option>) -> PyRes ("python_node_count", any(py, python_nodes.len())?), ( "models", - mapped(&plan.getattr("model_entries")?, model_json)?, + mapped(&native_field(&plan, "model_entries")?, model_json)?, + ), + ( + "seeds", + mapped(&native_field(&plan, "seed_entries")?, seed_json)?, ), - ("seeds", mapped(&plan.getattr("seed_entries")?, seed_json)?), ( "source_loads", - mapped(&plan.getattr("source_load_entries")?, source_load_json)?, + mapped( + &native_field(&plan, "source_load_entries")?, + source_load_json, + )?, ), ( "functions", - mapped(&plan.getattr("function_entries")?, function_json)?, + mapped(&native_field(&plan, "function_entries")?, function_json)?, ), ("python_nodes", PyList::new(py, python_nodes)?.into_any()), ("providers", providers_json(&plan, &python_entries)?), ( "warnings", - mapped(&plan.getattr("warnings")?, warning_json)?, + mapped(&native_field(&plan, "warnings")?, warning_json)?, ), ("retention", retention_json(&plan)?), ("table_type_conversions", table_type_json(&plan)?), @@ -741,11 +772,14 @@ fn format_plan_json(py: Python<'_>, options: Option>) -> PyRes ("column_migrations", column_migrations_json(&plan)?), ( "old_names", - mapped(&plan.getattr("old_name_view_entries")?, old_name_json)?, + mapped( + &native_field(&plan, "old_name_view_entries")?, + old_name_json, + )?, ), ], )?; - let metadata: Bound<'_, PyAny> = plan.getattr("metadata")?; + let metadata: Bound<'_, PyAny> = native_field(&plan, "metadata")?; if metadata.is_truthy()? { result.set_item("metadata", metadata)?; } 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 930041c4ef..a18c46709f 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,6 +27,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; @@ -68,16 +69,19 @@ fn token<'py>(bound: Bound<'py, PyAny>) -> PyResult> { /// `start → inclusive end` of resolved bounds, in the model's cursor display. fn cursor_range(entry: &Bound<'_, PyAny>, bounds: &Bound<'_, PyAny>) -> PyResult { let (cursor_type, cursor_grain) = ( - entry.getattr("cursor_type")?, - entry.getattr("cursor_grain")?, + native_field(entry, "cursor_type")?, + native_field(entry, "cursor_grain")?, ); let start: String = cursor_bound_display( - &token(bounds.getattr("start")?)?, + &token(native_field(bounds, "start")?)?, + &cursor_type, + &cursor_grain, + )?; + let end: String = inclusive_cursor_end( + &token(native_field(bounds, "end")?)?, &cursor_type, &cursor_grain, )?; - let end: String = - inclusive_cursor_end(&token(bounds.getattr("end")?)?, &cursor_type, &cursor_grain)?; Ok(format!("{start} \u{2192} {end}")) } @@ -97,9 +101,9 @@ fn cursor_text(entry: &Bound<'_, PyAny>, details: CursorDetails<'_>) -> PyResult .map(|bounds| cursor_range(entry, bounds)) .transpose()?; Ok(CursorPlanText { - column: optional_text(&entry.getattr("cursor_column")?)?.unwrap_or_default(), - cursor_type: optional_text(&entry.getattr("cursor_type")?)?, - microbatch: entry.getattr("incremental_mode")?.eq(MICROBATCH_MODE)?, + column: optional_text(&native_field(entry, "cursor_column")?)?.unwrap_or_default(), + cursor_type: optional_text(&native_field(entry, "cursor_type")?)?, + microbatch: native_field(entry, "incremental_mode")?.eq(MICROBATCH_MODE)?, requested_start: details.requested_start, requested_end: details.requested_end, empty_inputs_window, @@ -114,10 +118,11 @@ fn cursor_text(entry: &Bound<'_, PyAny>, details: CursorDetails<'_>) -> PyResult resolved: details.resolution_status == RESOLVED, deferred: details.resolution_status == DEFERRED, runtime_inputs_clause: runtime_inputs_clause(entry)?, - batch_concurrency: text(&entry.getattr("batch_concurrency")?)?, - unaccounted_partition_policy: optional_text( - &entry.getattr("unaccounted_partition_policy")?, - )?, + batch_concurrency: text(&native_field(entry, "batch_concurrency")?)?, + unaccounted_partition_policy: optional_text(&native_field( + entry, + "unaccounted_partition_policy", + )?)?, }) } @@ -127,7 +132,7 @@ fn config_json<'py>(metadata_json: &Bound<'py, PyAny>) -> PyResult = py.import("json")?; let payload: Bound<'_, PyAny> = match json.call_method1("loads", (metadata_json,)) { Ok(payload) => payload, - Err(error) if error.is_instance(py, &json.getattr("JSONDecodeError")?) => { + Err(error) if error.is_instance(py, &native_field(&json, "JSONDecodeError")?) => { return Ok(metadata_json.clone()); } Err(error) => return Err(error), @@ -142,16 +147,16 @@ fn config_json<'py>(metadata_json: &Bound<'py, PyAny>) -> PyResult, reason: &str) -> PyResult>> { - if !entry.getattr("config_changed")?.is_truthy()? && reason != CONFIG_CHANGED { + if !native_field(entry, "config_changed")?.is_truthy()? && reason != CONFIG_CHANGED { return Ok(None); } let (previous, current) = ( - entry.getattr("previous_metadata_json")?, - entry.getattr("fingerprint_metadata_json")?, + native_field(entry, "previous_metadata_json")?, + native_field(entry, "fingerprint_metadata_json")?, ); if previous.is_none() || current.is_none() { return Ok(None); @@ -164,9 +169,9 @@ fn config_diff(entry: &Bound<'_, PyAny>, reason: &str) -> PyResult) -> PyResult { - let action: String = text(&entry.getattr("action")?.getattr("value")?)?; - let reason: String = text(&entry.getattr("reason")?.getattr("value")?)?; - let name: String = text(&entry.getattr("name")?)?; + let action: String = text(&native_field(entry, "action")?.getattr("value")?)?; + let reason: String = text(&native_field(entry, "reason")?.getattr("value")?)?; + let name: String = text(&native_field(entry, "name")?)?; if action == SKIP_ACTION { return Ok(ModelPlanText { name, @@ -175,43 +180,46 @@ fn model_text(entry: &Bound<'_, PyAny>) -> PyResult { ..ModelPlanText::default() }); } - let backfill: Bound<'_, PyAny> = entry.getattr("backfill")?; + let backfill: Bound<'_, PyAny> = native_field(entry, "backfill")?; let mut schema_findings: Vec = Vec::new(); - for finding in items(&entry.getattr("schema_findings")?)? { + for finding in items(&native_field(entry, "schema_findings")?)? { schema_findings.push(SchemaFindingText { - kind: text(&finding.getattr("kind")?.getattr("value")?)?, - column_name: text(&finding.getattr("column_name")?)?, - expected_type: optional_text(&finding.getattr("expected_type")?)?, - actual_type: optional_text(&finding.getattr("actual_type")?)?, + kind: text(&native_field(&finding, "kind")?.getattr("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")?)?, }); } let mut rename_hints: Vec = Vec::new(); - for hint in items(&entry.getattr("column_rename_hints")?)? { + for hint in items(&native_field(entry, "column_rename_hints")?)? { rename_hints.push(RenameHintText { - added_column: text(&hint.getattr("added_column")?)?, - message: text(&hint.getattr("message")?)?, + added_column: text(&native_field(&hint, "added_column")?)?, + message: text(&native_field(&hint, "message")?)?, }); } - let previous_query: Bound<'_, PyAny> = entry.getattr("previous_query_sql")?; + let previous_query: Bound<'_, PyAny> = native_field(entry, "previous_query_sql")?; let query_diff_lines: Option> = if previous_query.is_none() - || (!entry.getattr("query_changed")?.is_truthy()? && reason != QUERY_CHANGED) + || (!native_field(entry, "query_changed")?.is_truthy()? && reason != QUERY_CHANGED) { None } else { Some(query_diff( &previous_query, - &entry.getattr("fingerprint_query_sql")?, + &native_field(entry, "fingerprint_query_sql")?, )?) }; Ok(ModelPlanText { materialization_label: model_materialization_label(entry)?, - custom_materialization_name: optional_text(&entry.getattr("custom_materialization_name")?)?, - backfill_action: text(&backfill.getattr("action")?.getattr("value")?)?, - backfill_duration: optional_text(&backfill.getattr("duration")?)?, + custom_materialization_name: optional_text(&native_field( + entry, + "custom_materialization_name", + )?)?, + backfill_action: text(&native_field(&backfill, "action")?.getattr("value")?)?, + backfill_duration: optional_text(&native_field(&backfill, "duration")?)?, cursor: cursor_details(entry)? .map(|details| cursor_text(entry, details)) .transpose()?, - changed_functions: texts(&entry.getattr("changed_functions")?)?, + changed_functions: texts(&native_field(entry, "changed_functions")?)?, schema_findings, rename_hints, config_diff: config_diff(entry, &reason)?, @@ -223,24 +231,24 @@ fn model_text(entry: &Bound<'_, PyAny>) -> PyResult { } fn function_text(entry: &Bound<'_, PyAny>) -> PyResult { - let reason: String = text(&entry.getattr("reason")?.getattr("value")?)?; - let previous: Bound<'_, PyAny> = entry.getattr("previous_query_sql")?; + let reason: String = text(&native_field(entry, "reason")?.getattr("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( &previous, - &entry.getattr("fingerprint_query_sql")?, + &native_field(entry, "fingerprint_query_sql")?, )?) } else { None }; Ok(FunctionPlanText { - name: text(&entry.getattr("name")?)?, - kind: if entry.getattr("return_columns")?.is_truthy()? { + name: text(&native_field(entry, "name")?)?, + kind: if native_field(entry, "return_columns")?.is_truthy()? { "table function".to_owned() } else { format!( "{} udf", - text(&entry.getattr("language")?.getattr("value")?)? + text(&native_field(entry, "language")?.getattr("value")?)? ) }, reason, @@ -249,35 +257,35 @@ fn function_text(entry: &Bound<'_, PyAny>) -> PyResult { } fn source_load_text(entry: &Bound<'_, PyAny>) -> PyResult { - let strategy: Bound<'_, PyAny> = entry.getattr("write_strategy")?; + let strategy: Bound<'_, PyAny> = native_field(entry, "write_strategy")?; Ok(SourceLoadPlanText { - name: text(&entry.getattr("name")?)?, - resource_kind: text(&entry.getattr("resource_kind")?.getattr("value")?)?, - is_reload: entry.getattr("is_reload")?.is_truthy()?, - integration_kind: optional_text(&entry.getattr("integration_kind")?)?, + name: text(&native_field(entry, "name")?)?, + resource_kind: text(&native_field(entry, "resource_kind")?.getattr("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() { None } else { - Some(text(&strategy.getattr("value")?)?) + Some(text(&native_field(&strategy, "value")?)?) }, - cursor_column: optional_text(&entry.getattr("cursor_column")?)?, - unique_key: texts(&entry.getattr("unique_key")?)?, + cursor_column: optional_text(&native_field(entry, "cursor_column")?)?, + unique_key: texts(&native_field(entry, "unique_key")?)?, }) } /// 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(&entry.getattr("identity_status")?.getattr("value")?)?; + let identity_status: String = text(&native_field(entry, "identity_status")?.getattr("value")?)?; let mut diffs: [Vec; 2] = [Vec::new(), Vec::new()]; if identity_status == CHANGED_IDENTITY { let sources = [ ( - definition_source_text(&entry.getattr("previous_definition_json")?)?, - definition_source_text(&entry.getattr("current_definition_json")?)?, + definition_source_text(&native_field(entry, "previous_definition_json")?)?, + definition_source_text(&native_field(entry, "current_definition_json")?)?, ), ( - dependency_source_text(&entry.getattr("previous_metadata_json")?)?, - dependency_source_text(&entry.getattr("current_metadata_json")?)?, + dependency_source_text(&native_field(entry, "previous_metadata_json")?)?, + dependency_source_text(&native_field(entry, "current_metadata_json")?)?, ), ]; for (diff, sources) in diffs.iter_mut().zip(sources) { @@ -294,9 +302,9 @@ fn python_text(entry: &Bound<'_, PyAny>) -> PyResult { } let [source_diff, dependency_diff] = diffs; Ok(PythonPlanText { - name: text(&entry.getattr("name")?)?, - kind: text(&entry.getattr("kind")?.getattr("value")?)?, - phase: text(&entry.getattr("phase")?.getattr("value")?)?, + name: text(&native_field(entry, "name")?)?, + kind: text(&native_field(entry, "kind")?.getattr("value")?)?, + phase: text(&native_field(entry, "phase")?.getattr("value")?)?, identity_status, source_diff, dependency_diff, @@ -308,27 +316,30 @@ fn provider_usages( python_entries: &[Bound<'_, PyAny>], ) -> PyResult> { let mut usages: Vec = Vec::new(); - for usage in items(&plan.getattr("provider_usages")?)? { + for usage in items(&native_field(plan, "provider_usages")?)? { usages.push(ProviderUsageText { - provider_name: text(&usage.getattr("provider_name")?)?, - consumer_kind: text(&usage.getattr("consumer_kind")?)?, - consumer_name: text(&usage.getattr("consumer_name")?)?, - parameter_name: text(&usage.getattr("parameter_name")?)?, - annotation_class_name: optional_text(&usage.getattr("annotation_class_name")?)?, + provider_name: text(&native_field(&usage, "provider_name")?)?, + consumer_kind: text(&native_field(&usage, "consumer_kind")?)?, + consumer_name: text(&native_field(&usage, "consumer_name")?)?, + parameter_name: text(&native_field(&usage, "parameter_name")?)?, + annotation_class_name: optional_text(&native_field(&usage, "annotation_class_name")?)?, }); } for entry in python_entries { let (kind, name) = ( - text(&entry.getattr("kind")?.getattr("value")?)?, - text(&entry.getattr("name")?)?, + text(&native_field(entry, "kind")?.getattr("value")?)?, + text(&native_field(entry, "name")?)?, ); - for usage in items(&entry.getattr("provider_usages")?)? { + for usage in items(&native_field(entry, "provider_usages")?)? { usages.push(ProviderUsageText { - provider_name: text(&usage.getattr("provider_name")?)?, + provider_name: text(&native_field(&usage, "provider_name")?)?, consumer_kind: kind.clone(), consumer_name: name.clone(), - parameter_name: text(&usage.getattr("parameter_name")?)?, - annotation_class_name: optional_text(&usage.getattr("annotation_class_name")?)?, + parameter_name: text(&native_field(&usage, "parameter_name")?)?, + annotation_class_name: optional_text(&native_field( + &usage, + "annotation_class_name", + )?)?, }); } } @@ -394,8 +405,8 @@ fn pruned_models(metadata: &Bound<'_, PyAny>) -> PyResult> { pub(crate) fn display_limit(options: &Bound<'_, PyAny>) -> PyResult { Ok(DisplayLimit { - max_entries: options.getattr("max_entries_per_section")?.extract()?, - overflow_flag: text(&options.getattr("overflow_flag")?)?, + max_entries: native_field(options, "max_entries_per_section")?.extract()?, + overflow_flag: text(&native_field(options, "overflow_flag")?)?, }) } @@ -422,20 +433,20 @@ fn report(arguments: &KeywordArguments<'_>) -> PyResult { Ok(default) } }; - let metadata: Bound<'_, PyAny> = plan.getattr("metadata")?; + let metadata: Bound<'_, PyAny> = native_field(&plan, "metadata")?; let mut warnings: Vec = Vec::new(); - for warning in items(&plan.getattr("warnings")?)? { + for warning in items(&native_field(&plan, "warnings")?)? { warnings.push(PlanWarningText { - severity: text(&warning.getattr("severity")?.getattr("value")?)?, - message: text(&warning.getattr("message")?)?, - model_name: optional_text(&warning.getattr("model_name")?)?, + severity: text(&native_field(&warning, "severity")?.getattr("value")?)?, + message: text(&native_field(&warning, "message")?)?, + model_name: optional_text(&native_field(&warning, "model_name")?)?, }); } let mut seeds: Vec = Vec::new(); - for seed in items(&plan.getattr("seed_entries")?)? { + for seed in items(&native_field(&plan, "seed_entries")?)? { seeds.push(SeedPlanText { - name: text(&seed.getattr("name")?)?, - reason: text(&seed.getattr("reason")?)?, + name: text(&native_field(&seed, "name")?)?, + reason: text(&native_field(&seed, "reason")?)?, }); } let pre_build: Vec = pre_build_lines(Vec::new(), &plan, &display_options)?; @@ -446,16 +457,16 @@ fn report(arguments: &KeywordArguments<'_>) -> PyResult { display: display_limit(&display_options)?, include_freshness_diagnostics: flag_or("include_direct_freshness_diagnostics", true)?, selection_diagnostics: selection_diagnostics(&metadata)?, - models: items(&plan.getattr("model_entries")?)? + models: items(&native_field(&plan, "model_entries")?)? .iter() .map(model_text) .collect::>()?, - functions: items(&plan.getattr("function_entries")?)? + functions: items(&native_field(&plan, "function_entries")?)? .iter() .map(function_text) .collect::>()?, seeds, - source_loads: items(&plan.getattr("source_load_entries")?)? + source_loads: items(&native_field(&plan, "source_load_entries")?)? .iter() .map(source_load_text) .collect::>()?, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/plan_output/python_identity.rs b/crates/sqlbuild-python/src/bindings/_helpers/plan_output/python_identity.rs index edc5b1481a..e0f46e3e48 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/plan_output/python_identity.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/plan_output/python_identity.rs @@ -1,5 +1,6 @@ //! The source texts a changed Python node's identity diffs compare. +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyResult, Python}; use pyo3::types::{PyDict, PyDictMethods, PyList, PyListMethods, PyString}; @@ -16,7 +17,9 @@ fn json_object<'py>(raw_json: &Bound<'py, PyAny>) -> PyResult = py.import("json")?; let payload: Bound<'py, PyAny> = match json.call_method1("loads", (raw_json,)) { Ok(payload) => payload, - Err(error) if error.is_instance(py, &json.getattr("JSONDecodeError")?) => return Ok(None), + Err(error) if error.is_instance(py, &native_field(&json, "JSONDecodeError")?) => { + return Ok(None); + } Err(error) => return Err(error), }; let Ok(payload) = payload.downcast_into::() else { 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 4df340d178..733d559750 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/plan_output/safety.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/plan_output/safety.rs @@ -6,6 +6,7 @@ use pyo3::{pyfunction, wrap_pyfunction}; use crate::bindings::_helpers::cursor::scalars::render_value; use crate::bindings::_helpers::plan_output::values::{dict, enum_value, items}; +use crate::bindings::_helpers::records::fields::field as native_field; /// `render(value=value)`. fn render<'py>(value: Bound<'py, PyAny>) -> PyResult> { @@ -42,7 +43,7 @@ pub(crate) fn future_cursor_safety<'py>( if evidence.is_none() { return Ok(evidence.clone()); } - let relation: Bound<'py, PyAny> = evidence.getattr("determining_relation")?; + let relation: Bound<'py, PyAny> = native_field(evidence, "determining_relation")?; let determining: Bound<'py, PyAny> = if relation.is_none() { relation } else { @@ -52,21 +53,21 @@ pub(crate) fn future_cursor_safety<'py>( ("relation", relation), ( "cursor_column", - evidence.getattr("determining_cursor_column")?, + native_field(evidence, "determining_cursor_column")?, ), ], )? .into_any() }; let mut inputs: Vec> = Vec::new(); - for item in items(&evidence.getattr("inputs")?)? { + for item in items(&native_field(evidence, "inputs")?)? { inputs.push(dict( py, vec![ - ("relation", item.getattr("relation")?), - ("cursor_column", item.getattr("cursor_column")?), - ("minimum", optional_render(item.getattr("minimum")?)?), - ("maximum", render(item.getattr("maximum")?)?), + ("relation", native_field(&item, "relation")?), + ("cursor_column", native_field(&item, "cursor_column")?), + ("minimum", optional_render(native_field(&item, "minimum")?)?), + ("maximum", render(native_field(&item, "maximum")?)?), ], )?); } @@ -74,10 +75,10 @@ pub(crate) fn future_cursor_safety<'py>( py, vec![ ("action", enum_value(evidence, "action")?), - ("max_distance", evidence.getattr("max_distance")?), + ("max_distance", native_field(evidence, "max_distance")?), ( "invocation_time", - render(evidence.getattr("invocation_time")?)?, + render(native_field(evidence, "invocation_time")?)?, ), ( "discovered_bounds", @@ -93,11 +94,11 @@ pub(crate) fn future_cursor_safety<'py>( ), ( "future_start_detected", - evidence.getattr("future_start_detected")?, + native_field(evidence, "future_start_detected")?, ), ( "future_end_detected", - evidence.getattr("future_end_detected")?, + native_field(evidence, "future_end_detected")?, ), ("determining_input", determining), ("inputs", pyo3::types::PyList::new(py, inputs)?.into_any()), @@ -118,34 +119,34 @@ pub(crate) fn maximum_start_safety<'py>( py, vec![ ("action", enum_value(evidence, "action")?), - ("max_ahead", evidence.getattr("max_ahead")?), + ("max_ahead", native_field(evidence, "max_ahead")?), ( "invocation_time", - render(evidence.getattr("invocation_time")?)?, + render(native_field(evidence, "invocation_time")?)?, ), ( "physical_target_max", - render(evidence.getattr("physical_target_max")?)?, + render(native_field(evidence, "physical_target_max")?)?, ), ( "highest_eligible_target_max", - optional_render(evidence.getattr("highest_eligible_target_max")?)?, + optional_render(native_field(evidence, "highest_eligible_target_max")?)?, ), ( "effective_start", - render(evidence.getattr("effective_start")?)?, + render(native_field(evidence, "effective_start")?)?, ), ( "maximum_allowed_start", - render(evidence.getattr("maximum_allowed_start")?)?, + render(native_field(evidence, "maximum_allowed_start")?)?, ), ( "input", dict( py, vec![ - ("relation", evidence.getattr("target_relation")?), - ("cursor_column", evidence.getattr("cursor_column")?), + ("relation", native_field(evidence, "target_relation")?), + ("cursor_column", native_field(evidence, "cursor_column")?), ], )? .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 e10c15f827..2c3b6725f5 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/plan_output/values.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/plan_output/values.rs @@ -1,5 +1,6 @@ //! Reading planner values by attribute and building the JSON documents' dictionaries. +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::IntoPyObjectExt; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; use pyo3::types::{PyDict, PyDictMethods, PyList}; @@ -32,7 +33,7 @@ pub(crate) fn enum_value<'py>( value: &Bound<'py, PyAny>, name: &str, ) -> PyResult> { - value.getattr(name)?.getattr("value") + native_field(&value.getattr(name)?, "value") } /// `value.value` when `value` is set, else `None`. @@ -40,7 +41,7 @@ pub(crate) fn optional_enum_value<'py>(value: &Bound<'py, PyAny>) -> PyResult( /// `value.qualified_name or value.name`. pub(crate) fn relation_name<'py>(value: &Bound<'py, PyAny>) -> PyResult> { - let qualified: Bound<'py, PyAny> = value.getattr("qualified_name")?; + let qualified: Bound<'py, PyAny> = native_field(value, "qualified_name")?; if qualified.is_truthy()? { return Ok(qualified); } - value.getattr("name") + native_field(value, "name") } /// `value.isoformat()` when 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 e6fbde744d..f587171c84 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_changes/actions.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_changes/actions.rs @@ -5,6 +5,7 @@ use pyo3::types::{PyDict, PyDictMethods}; use pyo3::{pyfunction, wrap_pyfunction}; 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::RecordId; use crate::bindings::_helpers::records::registry; @@ -23,21 +24,21 @@ pub(crate) fn resolve_model_actions<'py>( let model_kind: Bound<'py, PyAny> = imported(py, COMPILE_TYPES_MODULE, "CompiledResourceType")?.getattr("MODEL")?; let action_class: Bound<'py, PyAny> = registry::class(py, RecordId::ResolvedModelAction)?; - let selected: Bound<'py, PyAny> = scope.getattr("selected_keys")?; - let models_by_name: Bound<'py, PyAny> = scope.getattr("models_by_name")?; - let model_changes: Bound<'py, PyAny> = changes.getattr("models")?; + 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")?; let resolved: Bound<'py, PyDict> = PyDict::new(py); - for key in scope.getattr("execution_order")?.try_iter()? { + for key in native_field(scope, "execution_order")?.try_iter()? { let key: Bound<'py, PyAny> = key?; - if !selected.contains(&key)? || !key.getattr("resource_type")?.eq(&model_kind)? { + if !selected.contains(&key)? || !native_field(&key, "resource_type")?.eq(&model_kind)? { continue; } - let name: Bound<'py, PyAny> = key.getattr("name")?; + let name: Bound<'py, PyAny> = native_field(&key, "name")?; let change: Bound<'py, PyAny> = model_changes.call_method1("get", (&name,))?; if !models_by_name.contains(&name)? || change.is_none() { continue; } - let backfill: Bound<'py, PyAny> = change.getattr("backfill")?; + let backfill: Bound<'py, PyAny> = native_field(&change, "backfill")?; resolved.set_item( name, call_with(&action_class, &[("change", change), ("backfill", backfill)])?, 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 ac8e45266c..e301b367cd 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_changes/detect.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_changes/detect.rs @@ -43,6 +43,7 @@ use crate::bindings::_helpers::planner_identity::metadata::{ compiled_function_fingerprint_sql, function_local_hashes, model_declared_columns_hash, model_version_identity_metadata_json, }; +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; @@ -101,22 +102,23 @@ pub(crate) fn detect_changes<'py>( 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> = scope.getattr("selected_keys")?; - let recorded: Bound<'py, PyAny> = snapshot.getattr("fingerprints")?.getattr("functions")?; + 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()?; - let dialect: Option = snapshot.getattr("column_dialect")?.extract()?; - for function in project.getattr("functions")?.try_iter()? { + let dialect: Option = native_field(snapshot, "column_dialect")?.extract()?; + for function in native_field(project, "functions")?.try_iter()? { let function: Bound<'py, PyAny> = function?; - let name: Bound<'py, PyAny> = function.getattr("name")?; + let name: Bound<'py, PyAny> = native_field(&function, "name")?; let fingerprint_sql: String = compiled_function_fingerprint_sql(&function)?; let mut arguments: Vec<(&str, Bound<'py, PyAny>)> = vec![( "fingerprint_sql", PyString::new(py, &fingerprint_sql).into_any(), )]; - if selected.contains(function.getattr("key")?)? { + if selected.contains(native_field(&function, "key")?)? { let reason: &str = if full_refresh { "full_refresh" } else { @@ -129,7 +131,9 @@ pub(crate) fn detect_changes<'py>( &name.extract::()?, ( &fingerprint_sql, - &function.getattr("language")?.str()?.extract::()?, + &native_field(&function, "language")? + .str()? + .extract::()?, dialect.as_deref(), ), )? != fingerprint @@ -246,7 +250,7 @@ fn scope_model_changes<'py>( .extract()?, }; let function_local_hashes: Bound<'py, PyAny> = function_local_hashes( - &project.getattr("functions")?, + &native_field(project, "functions")?, project .getattr("sql_analysis_dialect")? .extract::>()? @@ -255,12 +259,12 @@ fn scope_model_changes<'py>( .into_any(); let hook_version_hashes: Bound<'py, PyDict> = PyDict::new(py); let identities: Bound<'py, PyAny> = host::entry(py, UserCodeHost::BuildHookIdentities)? - .call1((project.getattr("hook_functions")?,))?; + .call1((native_field(project, "hook_functions")?,))?; for item in identities.call_method0("items")?.try_iter()? { let item: Bound<'py, PyAny> = item?; hook_version_hashes.set_item( item.get_item(0)?, - item.get_item(1)?.getattr("version_hash")?, + native_field(&item.get_item(1)?, "version_hash")?, )?; } let pass = ModelPass { @@ -272,16 +276,16 @@ fn scope_model_changes<'py>( }; let model_kind: Bound<'py, PyAny> = imported(py, COMPILE_TYPES_MODULE, "CompiledResourceType")?.getattr("MODEL")?; - let selected: Bound<'py, PyAny> = scope.getattr("selected_keys")?; - let models_by_name: Bound<'py, PyAny> = scope.getattr("models_by_name")?; + 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); - for key in scope.getattr("execution_order")?.try_iter()? { + for key in native_field(scope, "execution_order")?.try_iter()? { let key: Bound<'py, PyAny> = key?; - if !selected.contains(&key)? || !key.getattr("resource_type")?.eq(&model_kind)? { + if !selected.contains(&key)? || !native_field(&key, "resource_type")?.eq(&model_kind)? { continue; } let model: Bound<'py, PyAny> = - models_by_name.call_method1("get", (key.getattr("name")?,))?; + models_by_name.call_method1("get", (native_field(&key, "name")?,))?; if model.is_none() { continue; } @@ -343,7 +347,7 @@ pub(crate) fn model_change<'py>( .getattr("models")? .call_method1("get", (&model_name,))?; let recorded: Option = recorded_fingerprint(&fingerprint)?; - let dialect: Bound<'py, PyAny> = snapshot.getattr("column_dialect")?; + let dialect: Bound<'py, PyAny> = native_field(snapshot, "column_dialect")?; let compiled_query_hash: Option = if pass.query_change_tracking && recorded.is_some() { Some(model_query_hash( py, @@ -363,7 +367,7 @@ pub(crate) fn model_change<'py>( (Some(hash), Some(recorded)) => *hash != recorded.definition_hash, _ => false, }; - if !relation_exists && snapshot.getattr("renamed_models")?.contains(&model_name)? { + if !relation_exists && native_field(snapshot, "renamed_models")?.contains(&model_name)? { result.query_changed = definition_changed; let backfill: Backfill = if definition_changed { replay_policy(model)? @@ -390,7 +394,7 @@ pub(crate) fn model_change<'py>( .as_ref() .map(|recorded| recorded.metadata_json.as_str()) .filter(|text| *text != EMPTY_FINGERPRINT_METADATA_JSON); - let renames: Bound<'py, PyAny> = snapshot.getattr("reference_renames")?; + let renames: Bound<'py, PyAny> = native_field(snapshot, "reference_renames")?; let origins: Option> = match (&compiled_query_hash, &recorded) { (Some(_), Some(recorded)) if definition_changed && renames.is_truthy()? => origin_names( (model_reads::query_sql(model)?.extract()?, &fingerprint), @@ -441,7 +445,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( - &model.getattr("config")?.getattr("values")?, + &native_field(model, "config")?.getattr("values")?, REPLAY_CONFIG_KEY, )? .map(|value| value.extract()) @@ -461,9 +465,9 @@ fn recorded_fingerprint(fingerprint: &Bound<'_, PyAny>) -> PyResult( if warehouse.is_none() { return Ok(Vec::new()); } - let schema_entry: Bound<'py, PyAny> = model.getattr("schema_entry")?; + let schema_entry: Bound<'py, PyAny> = native_field(model, "schema_entry")?; let mut yml: Vec<(String, String)> = Vec::new(); let mut type_enforcement: bool = false; let mut dynamic: Option> = None; if !schema_entry.is_none() { - for column in schema_entry.getattr("columns")?.try_iter()? { + for column in native_field(&schema_entry, "columns")?.try_iter()? { let column: Bound<'py, PyAny> = column?; - if let Some(kind) = column.getattr("type")?.extract::>()? { - yml.push((column.getattr("name")?.extract()?, kind)); + if let Some(kind) = native_field(&column, "type")?.extract::>()? { + yml.push((native_field(&column, "name")?.extract()?, kind)); } } type_enforcement = schema_entry .getattr("type_enforcement")? .extract::>()? .unwrap_or(false); - let families: Bound<'py, PyAny> = schema_entry.getattr("dynamic_columns")?; + let families: Bound<'py, PyAny> = native_field(&schema_entry, "dynamic_columns")?; if families.is_truthy()? { dynamic = Some(families); } @@ -583,8 +587,8 @@ fn column_pairs(columns: &Bound<'_, PyAny>) -> PyResult> { for column in columns.try_iter()? { let column: Bound<'_, PyAny> = column?; pairs.push(( - column.getattr("name")?.extract()?, - column.getattr("type")?.extract()?, + native_field(&column, "name")?.extract()?, + native_field(&column, "type")?.extract()?, )); } Ok(pairs) @@ -607,7 +611,7 @@ fn schema_findings_of<'py>( if matching.len()? != 1 { return Ok(None); } - matching.get_item(0)?.getattr("type")?.extract() + native_field(&matching.get_item(0)?, "type")?.extract() }; detect_schema_changes( columns, @@ -657,8 +661,8 @@ fn schema_changes<'py>( for column in inferred_columns.try_iter()? { let column: Bound<'py, PyAny> = column?; items.push(( - column.getattr("name")?.extract()?, - column.getattr("type")?.extract()?, + native_field(&column, "name")?.extract()?, + native_field(&column, "type")?.extract()?, )); } Some(items) 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 12d79a6396..2f7aa8f553 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_changes/renames.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_changes/renames.rs @@ -17,6 +17,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::{ KeywordArguments, call_with, imported, }; 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; @@ -32,14 +33,14 @@ pub(crate) fn origin_names( (recorded_hash, dialect): (&str, &Bound<'_, PyAny>), ) -> PyResult>> { let py: Python<'_> = fingerprint.py(); - let recorded_at: Bound<'_, PyAny> = utc(&fingerprint.getattr("ts")?)?; + let recorded_at: Bound<'_, PyAny> = utc(&native_field(fingerprint, "ts")?)?; let mut candidates: Vec = Vec::new(); for rename in renames.try_iter()? { let rename: Bound<'_, PyAny> = rename?; - let renamed_at: Bound<'_, PyAny> = rename.getattr("recorded_at")?; + let renamed_at: Bound<'_, PyAny> = native_field(&rename, "recorded_at")?; candidates.push(RenameCandidate { - new_name: rename.getattr("new_name")?.extract()?, - origin_name: rename.getattr("origin_name")?.extract()?, + new_name: native_field(&rename, "new_name")?.extract()?, + origin_name: native_field(&rename, "origin_name")?.extract()?, applies: renamed_at.is_none() || recorded_at.lt(utc(&renamed_at)?)?, }); } @@ -106,8 +107,8 @@ fn ref_call_name(text: &str) -> Option<(usize, &str)> { fn utc<'py>(value: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = value.py(); let zone: Bound<'py, PyAny> = imported(py, "datetime", "UTC")?; - if value.getattr("tzinfo")?.is_none() || value.call_method0("utcoffset")?.is_none() { - return call_with(&value.getattr("replace")?, &[("tzinfo", zone)]); + if native_field(value, "tzinfo")?.is_none() || value.call_method0("utcoffset")?.is_none() { + return call_with(&native_field(value, "replace")?, &[("tzinfo", zone)]); } value.call_method1("astimezone", (zone,)) } @@ -122,7 +123,7 @@ fn origin_reference_names_of<'py>( dialect: &Bound<'py, PyAny>, ) -> PyResult>> { let py: Python<'py> = recorded.py(); - let recorded_hash: String = recorded.getattr("definition_hash")?.extract()?; + let recorded_hash: String = native_field(recorded, "definition_hash")?.extract()?; let Some(mapping) = origin_names((query_sql, recorded), renames, (&recorded_hash, dialect))? else { return Ok(None); @@ -166,7 +167,7 @@ pub(crate) fn plan_reference_renames<'py>( if !changed_referencing_query(scope, snapshot)? { return Ok(PyTuple::empty(py)); } - let state_schemas: Bound<'py, PyAny> = snapshot.getattr("migration_state_schemas")?; + let state_schemas: Bound<'py, PyAny> = native_field(snapshot, "migration_state_schemas")?; let inspected: Bound<'py, PySet> = PySet::empty(py)?; for schema in schemas.try_iter()? { let schema: Bound<'py, PyAny> = schema?; @@ -175,7 +176,7 @@ pub(crate) fn plan_reference_renames<'py>( } } call_with( - &state.getattr("inspect_migration_events")?, + &native_field(&state, "inspect_migration_events")?, &[("schemas", inspected.into_any())], )?; let target_name: Bound<'py, PyAny> = runtime @@ -193,11 +194,11 @@ pub(crate) fn plan_reference_renames<'py>( )?); } } - for event in state.getattr("events")?.try_iter()? { + for event in native_field(&state, "events")?.try_iter()? { let event: Bound<'py, PyAny> = event?; - let event_target: Bound<'py, PyAny> = event.getattr("target_name")?; - let origin: Bound<'py, PyAny> = event.getattr("origin_model")?; - let destination: Bound<'py, PyAny> = event.getattr("destination_model")?; + let event_target: Bound<'py, PyAny> = native_field(&event, "target_name")?; + let origin: Bound<'py, PyAny> = native_field(&event, "origin_model")?; + let destination: Bound<'py, PyAny> = native_field(&event, "destination_model")?; if (event_target.is_none() || event_target.eq(&target_name)?) && !origin.is_none() && origin.ne(&destination)? @@ -207,7 +208,7 @@ pub(crate) fn plan_reference_renames<'py>( &[ ("new_name", destination), ("origin_name", origin), - ("recorded_at", event.getattr("created_at")?), + ("recorded_at", native_field(&event, "created_at")?), ], )?); } @@ -220,19 +221,20 @@ fn changed_referencing_query( snapshot: &Bound<'_, PyAny>, ) -> PyResult { let py: Python<'_> = scope.py(); - let models_by_name: Bound<'_, PyAny> = scope.getattr("models_by_name")?; + let models_by_name: Bound<'_, PyAny> = native_field(scope, "models_by_name")?; let mut selected: Vec> = Vec::new(); - for key in scope.getattr("selected_keys")?.try_iter()? { + for key in native_field(scope, "selected_keys")?.try_iter()? { let model: Bound<'_, PyAny> = - models_by_name.call_method1("get", (key?.getattr("name")?,))?; + models_by_name.call_method1("get", (native_field(&key?, "name")?,))?; if !model.is_none() { selected.push(model); } } - let recorded_models: Bound<'_, PyAny> = snapshot.getattr("fingerprints")?.getattr("models")?; + let recorded_models: Bound<'_, PyAny> = + native_field(snapshot, "fingerprints")?.getattr("models")?; let ref_kind: Bound<'_, PyAny> = registry::class(py, RecordId::SqlReferenceKind)?.getattr("REF")?; - let dialect: Option = snapshot.getattr("column_dialect")?.extract()?; + let dialect: Option = native_field(snapshot, "column_dialect")?.extract()?; for model in selected { let recorded: Bound<'_, PyAny> = recorded_models.call_method1("get", (model_reads::name_object(&model)?,))?; @@ -240,8 +242,8 @@ fn changed_referencing_query( continue; } let mut references_models: bool = false; - for reference in model.getattr("references")?.try_iter()? { - if reference?.getattr("ref_kind")?.eq(&ref_kind)? { + for reference in native_field(&model, "references")?.try_iter()? { + if native_field(&reference?, "ref_kind")?.eq(&ref_kind)? { references_models = true; break; } @@ -254,7 +256,7 @@ fn changed_referencing_query( &model_reads::query_sql(&model)?.extract::()?, dialect.as_deref(), ), - )? != recorded.getattr("definition_hash")?.extract::()? + )? != native_field(&recorded, "definition_hash")?.extract::()? { return Ok(true); } 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 3015e9551c..fa549ebded 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,6 +23,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; use pyo3::PyRef; @@ -108,16 +109,22 @@ fn clone_resolution<'py>( (adapter, connection): (&Bound<'py, PyAny>, &Bound<'py, PyAny>), ) -> PyResult> { let py = project.py(); - let model_locations = object(py, build_model_locations(&project.getattr("models")?)?)?; - let seed_locations = object(py, build_seed_locations(&project.getattr("seeds")?)?)?; + let model_locations = object( + py, + build_model_locations(&native_field(project, "models")?)?, + )?; + let seed_locations = object(py, build_seed_locations(&native_field(project, "seeds")?)?)?; let function_locations = object( py, - build_function_locations(&project.getattr("functions")?)?, + build_function_locations(&native_field(project, "functions")?)?, )?; let source_map = PyDict::new(py); - for source in source_project.getattr("sources")?.try_iter()? { + for source in native_field(source_project, "sources")?.try_iter()? { let source = source?; - source_map.set_item(source.getattr("name")?, source.getattr("source_entry")?)?; + source_map.set_item( + native_field(&source, "name")?, + native_field(&source, "source_entry")?, + )?; } let source_warehouse_columns = object( py, @@ -149,7 +156,7 @@ fn model_entry<'py>( &[ ("key", model_reads::model_key(model)?), ("name", model_reads::name_object(model)?), - ("relative_path", model.getattr("relative_path")?), + ("relative_path", native_field(model, "relative_path")?), ("materialization_type", materialization_type), ("action", planner_type(py, RecordId::PlanAction, action)?), ( @@ -171,9 +178,9 @@ fn clone_model_entries<'py>( ) -> PyResult> { let py = project.py(); let resolution = clone_resolution(project, source_project, (adapter, connection))?; - let selected = plan.getattr("selected_keys")?; + let selected = native_field(plan, "selected_keys")?; let entries = PyDict::new(py); - for model in project.getattr("models")?.try_iter()? { + for model in native_field(project, "models")?.try_iter()? { let model = model?; if !selected.contains(model_reads::model_key(&model)?)? || disabled_model(&model)? { continue; @@ -181,9 +188,9 @@ fn clone_model_entries<'py>( let materialization_type = materialization_of(&model)?; let resolved_sql = if materialization_type.eq(VIEW_MATERIALIZATION)? { let functions = PyDict::new(py); - for function in project.getattr("functions")?.try_iter()? { + for function in native_field(project, "functions")?.try_iter()? { let function = function?; - functions.set_item(function.getattr("name")?, function)?; + functions.set_item(native_field(&function, "name")?, function)?; } let context = Bound::new( py, @@ -232,7 +239,7 @@ fn clone_model_entries<'py>( model_entry(&model, materialization_type, resolved_sql)?, )?; } - in_order(&entries, &plan.getattr("execution_order")?) + in_order(&entries, &native_field(plan, "execution_order")?) } fn clone_source_entries<'py>( @@ -242,25 +249,25 @@ fn clone_source_entries<'py>( ) -> PyResult> { let py = project.py(); let entries = PyDict::new(py); - for source in project.getattr("sources")?.try_iter()? { + for source in native_field(project, "sources")?.try_iter()? { let source = source?; - let entry = source.getattr("source_entry")?; - if !selected.contains(source.getattr("key")?)? - || !entry.getattr("managed")?.is_truthy()? - || entry.getattr("loader")?.is_none() - || !entry.getattr("expression")?.is_none() + let entry = native_field(&source, "source_entry")?; + if !selected.contains(native_field(&source, "key")?)? + || !native_field(&entry, "managed")?.is_truthy()? + || native_field(&entry, "loader")?.is_none() + || !native_field(&entry, "expression")?.is_none() { continue; } - let table = entry.getattr("table")?; + let table = native_field(&entry, "table")?; let relation_name = if table.is_truthy()? { table } else { - entry.getattr("name")? + native_field(&entry, "name")? }; let kwargs = PyDict::new(py); - kwargs.set_item("database", entry.getattr("database")?)?; - kwargs.set_item("schema", entry.getattr("schema")?)?; + kwargs.set_item("database", native_field(&entry, "database")?)?; + kwargs.set_item("schema", native_field(&entry, "schema")?)?; kwargs.set_item("name", &relation_name)?; let qualified = adapter .getattr("render_qualified_name")? @@ -269,21 +276,21 @@ fn clone_source_entries<'py>( (), Some(&{ let location = PyDict::new(py); - location.set_item("database", entry.getattr("database")?)?; - location.set_item("schema", entry.getattr("schema")?)?; + location.set_item("database", native_field(&entry, "database")?)?; + location.set_item("schema", native_field(&entry, "schema")?)?; location.set_item("name", relation_name)?; location.set_item("qualified_name", qualified)?; location }), )?; entries.set_item( - source.getattr("key")?, + native_field(&source, "key")?, planner_model( py, RecordId::CloneSourcePlanEntry, &[ - ("key", source.getattr("key")?), - ("name", source.getattr("name")?), + ("key", native_field(&source, "key")?), + ("name", native_field(&source, "name")?), ("destination", destination), ], )?, @@ -299,11 +306,11 @@ fn clone_function_entries<'py>( ) -> PyResult> { let py = project.py(); let resolution = clone_resolution(project, source_project, (adapter, connection))?; - let selected = plan.getattr("selected_keys")?; + let selected = native_field(plan, "selected_keys")?; let entries = PyDict::new(py); - for function in project.getattr("functions")?.try_iter()? { + for function in native_field(project, "functions")?.try_iter()? { let function = function?; - if !selected.contains(function.getattr("key")?)? { + if !selected.contains(native_field(&function, "key")?)? { continue; } let body_sql = resolve_function_sql( @@ -353,9 +360,9 @@ fn clone_function_entries<'py>( ("packages", field("packages")?), ], )?; - entries.set_item(function.getattr("key")?, entry)?; + entries.set_item(native_field(&function, "key")?, entry)?; } - in_order(&entries, &plan.getattr("execution_order")?) + in_order(&entries, &native_field(plan, "execution_order")?) } fn seed_entry<'py>(seed: &Bound<'py, PyAny>) -> PyResult> { @@ -364,17 +371,17 @@ fn seed_entry<'py>(seed: &Bound<'py, PyAny>) -> PyResult> { py, RecordId::SeedPlanEntry, &[ - ("key", seed.getattr("key")?), - ("name", seed.getattr("name")?), - ("destination", seed.getattr("destination")?), + ("key", native_field(seed, "key")?), + ("name", native_field(seed, "name")?), + ("destination", native_field(seed, "destination")?), ( "file_path", - seed.getattr("seed_file")?.getattr("file_path")?, + native_field(seed, "seed_file")?.getattr("file_path")?, ), ("columns", PyTuple::empty(py).into_any()), ( "csv_settings", - seed.getattr("schema_entry")?.getattr("csv_settings")?, + native_field(seed, "schema_entry")?.getattr("csv_settings")?, ), ], ) @@ -387,7 +394,7 @@ fn seed_entries<'py>( wanted: &Bound<'py, PyAny>, ) -> PyResult> { let mut entries: Vec> = Vec::new(); - for seed in project.getattr("seeds")?.try_iter()? { + for seed in native_field(project, "seeds")?.try_iter()? { let seed = seed?; if wanted.contains(seed.getattr(field)?)? { entries.push(seed_entry(&seed)?); @@ -402,7 +409,7 @@ fn origin_model_entries<'py>( ) -> PyResult> { let py = project.py(); let mut entries: Vec> = Vec::new(); - for model in project.getattr("models")?.try_iter()? { + for model in native_field(project, "models")?.try_iter()? { let model = model?; if !selected_names.contains(model_reads::name_object(&model)?)? || disabled_model(&model)? { continue; @@ -416,7 +423,7 @@ fn origin_model_entries<'py>( fn names_of<'py>(entries: &Bound<'py, PyAny>) -> PyResult> { let mut names: Vec> = Vec::new(); for entry in entries.try_iter()? { - names.push(entry?.getattr("name")?); + names.push(native_field(&entry?, "name")?); } Ok(PyFrozenSet::new(entries.py(), names)?.into_any()) } @@ -429,12 +436,12 @@ fn origin_source_entries<'py>( let py = project.py(); let mut keys: Vec> = Vec::new(); let mut order: Vec> = Vec::new(); - for source in project.getattr("sources")?.try_iter()? { + for source in native_field(project, "sources")?.try_iter()? { let source = source?; - if selected_names.contains(source.getattr("name")?)? { - keys.push(source.getattr("key")?); + if selected_names.contains(native_field(&source, "name")?)? { + keys.push(native_field(&source, "key")?); } - order.push(source.getattr("key")?); + order.push(native_field(&source, "key")?); } clone_source_entries( project, @@ -481,7 +488,7 @@ fn run_clone_planning<'py>( (&adapter, &connection), &destination_sources, )?; - let seeds = seed_entries(&project, "key", &plan.getattr("selected_keys")?)?; + let seeds = seed_entries(&project, "key", &native_field(&plan, "selected_keys")?)?; let functions = clone_function_entries( (&project, &plan), (&adapter, &connection), @@ -490,13 +497,13 @@ fn run_clone_planning<'py>( let mut sources = PyTuple::empty(py).into_any(); if destination_sources .getattr("effective_target_name")? - .eq(project.getattr("effective_target_name")?)? + .eq(native_field(&project, "effective_target_name")?)? { sources = clone_source_entries( &project, ( - &plan.getattr("selected_keys")?, - &plan.getattr("execution_order")?, + &native_field(&plan, "selected_keys")?, + &native_field(&plan, "execution_order")?, ), &adapter, )?; @@ -512,18 +519,18 @@ fn run_clone_planning<'py>( let origin_by_name = PyDict::new(py); for entry in origin_sources.try_iter()? { let entry = entry?; - origin_by_name.set_item(entry.getattr("name")?, entry)?; + origin_by_name.set_item(native_field(&entry, "name")?, entry)?; } let mut kept_sources: Vec> = Vec::new(); let mut kept_origins: Vec> = Vec::new(); for entry in sources.try_iter()? { let entry = entry?; - let Some(origin) = origin_by_name.get_item(entry.getattr("name")?)? else { + let Some(origin) = origin_by_name.get_item(native_field(&entry, "name")?)? else { continue; }; if origin .getattr("destination")? - .ne(entry.getattr("destination")?)? + .ne(native_field(&entry, "destination")?)? { kept_origins.push(origin); kept_sources.push(entry); 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 041d7a9afd..a383c0794a 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 @@ -19,6 +19,7 @@ use crate::bindings::_helpers::planner_phases::scopes::resolve_planner_scopes; use crate::bindings::_helpers::planner_strategy::audit_entries::build_selected_audit_entries; use crate::bindings::_helpers::planner_strategy::model_entries::extract_seed_columns; use crate::bindings::_helpers::planner_strategy::strategy::materialization_of; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; use pyo3::types::{PyDict, PyDictMethods, PyFrozenSet, PyList, PyListMethods, PyString, PyTuple}; @@ -41,12 +42,12 @@ fn base_plan_output<'py>( parts: StaticPlanParts<'py>, ) -> PyResult> { let py = project.py(); - let selected_keys = scope.getattr("selected_keys")?; + let selected_keys = native_field(scope, "selected_keys")?; let execution_order = match parts.execution_order { Some(order) => order, None => { let mut order: Vec> = Vec::new(); - for key in scope.getattr("execution_order")?.try_iter()? { + for key in native_field(scope, "execution_order")?.try_iter()? { let key = key?; if selected_keys.contains(&key)? { order.push(key); @@ -59,24 +60,30 @@ fn base_plan_output<'py>( fields.extend(parts.fields); fields.extend([ ("selected_keys", selected_keys), - ("upstream_deps", scope.getattr("upstream_deps")?), - ("downstream_deps", scope.getattr("downstream_deps")?), - ("model_locations", relations.getattr("model_locations")?), - ("seed_locations", relations.getattr("seed_locations")?), + ("upstream_deps", native_field(scope, "upstream_deps")?), + ("downstream_deps", native_field(scope, "downstream_deps")?), + ( + "model_locations", + native_field(relations, "model_locations")?, + ), + ("seed_locations", native_field(relations, "seed_locations")?), ( "function_locations", - relations.getattr("function_locations")?, + native_field(relations, "function_locations")?, + ), + ("source_map", native_field(relations, "source_map")?), + ( + "source_read_map", + native_field(relations, "source_read_map")?, ), - ("source_map", relations.getattr("source_map")?), - ("source_read_map", relations.getattr("source_read_map")?), ( "python_source_read_map", - relations.getattr("python_source_read_map")?, + native_field(relations, "python_source_read_map")?, ), - ("hook_functions", project.getattr("hook_functions")?), + ("hook_functions", native_field(project, "hook_functions")?), ( "enforce_explicit_references", - project.getattr("enforce_explicit_references")?, + native_field(project, "enforce_explicit_references")?, ), ]); planner_model(py, RecordId::PlanOutput, &fields) @@ -148,11 +155,11 @@ fn build_audit_command_plan<'py>( ) -> PyResult> { let py = project.py(); let materializations = PyDict::new(py); - for model in project.getattr("models")?.try_iter()? { + for model in native_field(project, "models")?.try_iter()? { let model = model?; materializations.set_item( model_reads::name_object(&model)?, - materialization_of(&model)?.getattr("value")?, + native_field(&materialization_of(&model)?, "value")?, )?; } let audits = object( @@ -202,8 +209,8 @@ fn build_test_command_plan<'py>( let adapter = args.value("adapter")?; let scope = args.value("scope")?; let relations = args.value("relations")?; - let selected_keys = scope.getattr("selected_keys")?; - let selection = scope.getattr("sql_test_selection")?; + let selected_keys = native_field(&scope, "selected_keys")?; + let selection = native_field(&scope, "sql_test_selection")?; let selected = selected_test_entries( (&project, &adapter), &selected_keys, @@ -212,9 +219,9 @@ fn build_test_command_plan<'py>( )?; let (entries, warnings) = (selected.entries, selected.warnings); let mut case_names: BTreeSet = BTreeSet::new(); - for test in project.getattr("sql_tests")?.try_iter()? { + for test in native_field(&project, "sql_tests")?.try_iter()? { let test = test?; - let case_name = test.getattr("case_name")?; + let case_name = native_field(&test, "case_name")?; if !case_name.is_none() && test_selected(&test, &selected_keys, &selection)? { case_names.insert(case_name.extract()?); } @@ -222,16 +229,16 @@ fn build_test_command_plan<'py>( let mut test_keys: Vec> = Vec::new(); let mut function_keys: Vec> = Vec::new(); for entry in &entries { - test_keys.push(entry.getattr("key")?); - for key in entry.getattr("function_deps")?.try_iter()? { + test_keys.push(native_field(entry, "key")?); + for key in native_field(entry, "function_deps")?.try_iter()? { function_keys.push(key?); } } let function_keys = PyFrozenSet::new(py, function_keys)?; let mut functions: Vec> = Vec::new(); - for function in project.getattr("functions")?.try_iter()? { + for function in native_field(&project, "functions")?.try_iter()? { let function = function?; - if !function_keys.contains(function.getattr("key")?)? { + if !function_keys.contains(native_field(&function, "key")?)? { continue; } let fingerprint_sql = object(py, build_compiled_function_fingerprint_sql(&function)?)?; @@ -248,7 +255,7 @@ fn build_test_command_plan<'py>( } let scoped_keys = selected_keys.call_method1("__or__", (PyFrozenSet::new(py, test_keys)?,))?; let mut order: Vec> = Vec::new(); - for key in scope.getattr("execution_order")?.try_iter()? { + for key in native_field(&scope, "execution_order")?.try_iter()? { let key = key?; if scoped_keys.contains(&key)? { order.push(key); @@ -287,19 +294,19 @@ fn build_seed_command_plan<'py>( Some(fingerprints) if fingerprints.is_truthy()? => fingerprints, _ => PyDict::new(py).into_any(), }; - let selected_keys = scope.getattr("selected_keys")?; + let selected_keys = native_field(scope, "selected_keys")?; let mut entries: Vec> = Vec::new(); - for seed in project.getattr("seeds")?.try_iter()? { + for seed in native_field(project, "seeds")?.try_iter()? { let seed = seed?; - if !selected_keys.contains(seed.getattr("key")?)? { + if !selected_keys.contains(native_field(&seed, "key")?)? { continue; } let identity = object(py, build_seed_identity(&seed)?)?; let (version_hash, metadata_json) = (identity.get_item(0)?, identity.get_item(1)?); - let previous = existing.call_method1("get", (seed.getattr("name")?,))?; + let previous = existing.call_method1("get", (native_field(&seed, "name")?,))?; let reason = if previous.is_none() { "FIRST_RUN" - } else if previous.getattr("version_hash")?.ne(&version_hash)? { + } else if native_field(&previous, "version_hash")?.ne(&version_hash)? { "CONFIG_CHANGED" } else { "NO_CHANGE" @@ -308,17 +315,17 @@ fn build_seed_command_plan<'py>( py, RecordId::SeedPlanEntry, &[ - ("key", seed.getattr("key")?), - ("name", seed.getattr("name")?), - ("destination", seed.getattr("destination")?), + ("key", native_field(&seed, "key")?), + ("name", native_field(&seed, "name")?), + ("destination", native_field(&seed, "destination")?), ( "file_path", - seed.getattr("seed_file")?.getattr("file_path")?, + native_field(&seed, "seed_file")?.getattr("file_path")?, ), ("columns", object(py, extract_seed_columns(&seed)?)?), ( "csv_settings", - seed.getattr("schema_entry")?.getattr("csv_settings")?, + native_field(&seed, "schema_entry")?.getattr("csv_settings")?, ), ("fingerprint_definition", metadata_json.clone()), ("fingerprint_version_hash", version_hash), @@ -366,18 +373,18 @@ fn read_selected_seed_fingerprints<'py>( BTreeMap::new(); for seed in seeds.try_iter()? { let seed = seed?; - let destination = seed.getattr("destination")?; - let schema = destination.getattr("schema")?; + let destination = native_field(&seed, "destination")?; + let schema = native_field(&destination, "schema")?; if schema.is_none() { continue; } - let database: Option = destination.getattr("database")?.extract()?; + let database: Option = native_field(&destination, "database")?.extract()?; let schema: String = schema.extract()?; names_by_target .entry((database.clone().unwrap_or_default(), schema.clone())) .or_insert_with(|| ((database, schema), BTreeSet::new())) .1 - .insert(seed.getattr("name")?.extract()?); + .insert(native_field(&seed, "name")?.extract()?); } let fingerprints = PyDict::new(py); let mut relations_by_target: Vec> = Vec::new(); @@ -389,14 +396,14 @@ fn read_selected_seed_fingerprints<'py>( kwargs.set_item("database", database)?; kwargs.set_item("schemas", schemas)?; kwargs.set_item("names", names)?; - relations_by_target.push(adapter.getattr("list_relations")?.call((), Some(&kwargs))?); + relations_by_target.push(native_field(adapter, "list_relations")?.call((), Some(&kwargs))?); } for (((database, schema), names), relations) in names_by_target.values().zip(relations_by_target) { let mut table_exists = false; for relation in relations.try_iter()? { - let name: String = relation?.getattr("name")?.extract()?; + let name: String = native_field(&relation?, "name")?.extract()?; if name.to_lowercase() == table_name.to_lowercase() { table_exists = true; break; @@ -407,7 +414,7 @@ fn read_selected_seed_fingerprints<'py>( py, &[ ("connection", connection.clone()), - ("execute", adapter.getattr("execute")?), + ("execute", native_field(adapter, "execute")?), ("table_exists", boolean(py, table_exists)?), ( "database", @@ -418,11 +425,11 @@ fn read_selected_seed_fingerprints<'py>( ("schema", PyString::new(py, schema).into_any()), ( "render_qualified_name", - adapter.getattr("render_qualified_name")?, + native_field(adapter, "render_qualified_name")?, ), ( "render_read_latest_sql", - adapter.getattr("render_read_latest_fingerprints_sql")?, + native_field(adapter, "render_read_latest_fingerprints_sql")?, ), ("node_names", node_names.into_any()), ], @@ -437,7 +444,7 @@ fn read_selected_seed_fingerprints<'py>( )?; for item in items.iter() { let (name, fingerprint) = (item.get_item(0)?, item.get_item(1)?); - if fingerprint.getattr("node_type")?.eq(NODE_TYPE_SEED)? + if native_field(&fingerprint, "node_type")?.eq(NODE_TYPE_SEED)? && names.contains(&name.extract::()?) { fingerprints.set_item(name, fingerprint)?; 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 48509ee8c9..a7cbf6ed29 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 @@ -11,6 +11,7 @@ use crate::bindings::_helpers::planner_identity::graph::{ use crate::bindings::_helpers::planner_phases::full_refresh::resolve_model_full_refresh; use crate::bindings::_helpers::planner_phases::objects::object; use crate::bindings::_helpers::planner_strategy::strategy::materialization_of; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; use pyo3::types::{PyFrozenSet, PyTuple}; @@ -41,7 +42,7 @@ fn display_action( fn config<'py>(model: &Bound<'py, PyAny>, key: &str) -> PyResult> { Ok( - authored_config_str(&model.getattr("config")?.getattr("values")?, key)? + authored_config_str(&native_field(model, "config")?.getattr("values")?, key)? .unwrap_or_else(|| model.py().None().into_bound(model.py())), ) } @@ -69,7 +70,7 @@ fn display_entry<'py>( &[ ("key", model_reads::model_key(model)?), ("name", model_reads::name_object(model)?), - ("relative_path", model.getattr("relative_path")?), + ("relative_path", native_field(model, "relative_path")?), ( "action", planner_type( @@ -138,7 +139,7 @@ fn build_display_only_sqlbuild_plan<'py>( .collect::>>()?, )?; let mut entries: Vec> = Vec::new(); - for model in project.getattr("models")?.try_iter()? { + for model in native_field(project, "models")?.try_iter()? { let model = model?; if selected.contains(model_reads::name_object(&model)?)? { entries.push(display_entry(&model, full_refresh)?); @@ -146,7 +147,7 @@ fn build_display_only_sqlbuild_plan<'py>( } let keys: Vec> = entries .iter() - .map(|entry| entry.getattr("key")) + .map(|entry| native_field(entry, "key")) .collect::>()?; let upstream = build_execution_upstream_deps(project)?.into_any(); let downstream = build_downstream_deps(&upstream)?.into_any(); 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 4cf714f6d8..c1d782f835 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,6 +31,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; use pyo3::types::PyDict; @@ -50,18 +51,20 @@ fn scopes_and_cursor_overrides<'py>( &args.value("policies")?, )?; let selected = class_field(&scopes, |s: &PlannerScopeResolution| &s.selected_scope)?; - let models_by_name = selected.getattr("models_by_name")?; + let models_by_name = native_field(&selected, "models_by_name")?; let model_type = imported(py, COMPILE_TYPES, "CompiledResourceType")?.getattr("MODEL")?; let mut models: Vec> = Vec::new(); - for key in selected.getattr("selected_keys")?.try_iter()? { + for key in native_field(&selected, "selected_keys")?.try_iter()? { let key = key?; - let name = key.getattr("name")?; - if key.getattr("resource_type")?.eq(&model_type)? && models_by_name.contains(&name)? { + let name = native_field(&key, "name")?; + if native_field(&key, "resource_type")?.eq(&model_type)? + && models_by_name.contains(&name)? + { models.push(models_by_name.get_item(&name)?); } } let overrides = args.value("overrides")?; - let typed = type_cursor_overrides(&overrides.getattr("cursor_overrides")?, &models)?; + let typed = type_cursor_overrides(&native_field(&overrides, "cursor_overrides")?, &models)?; Ok(( scopes, replaced(&overrides, &[("cursor_overrides", typed)])?, @@ -85,13 +88,13 @@ fn change_results<'py>( &class_field(scopes, |s: &PlannerScopeResolution| &s.inspection_scope)?, snapshot, ), - overrides.getattr("full_refresh")?.is_truthy()?, + native_field(overrides, "full_refresh")?.is_truthy()?, ( - versions.getattr("model_version_hashes")?, - versions.getattr("model_metadata_jsons")?, + native_field(&versions, "model_version_hashes")?, + native_field(&versions, "model_metadata_jsons")?, ), )?; - if !policies.getattr("selection_diagnostics")?.is_truthy()? { + if !native_field(policies, "selection_diagnostics")?.is_truthy()? { let empty = native( py, RecordId::PlannerChangeResults, @@ -122,7 +125,7 @@ fn planned_output<'py>( let identities = build_planner_identity_context( &project, scopes, - policies.getattr("selection_diagnostics")?.extract()?, + native_field(&policies, "selection_diagnostics")?.extract()?, )?; check_selected_scope_buildability(runtime, scopes, &warehouse, &deferral)?; let snapshot = warehouse_field(&warehouse, |warehouse| &warehouse.snapshot)?; @@ -138,7 +141,7 @@ fn planned_output<'py>( (&project, &inspection_scope), (&args.value("adapter")?, &args.value("connection")?), &warehouse_field(&warehouse, |warehouse| &warehouse.inspection_relations)?, - &snapshot.getattr("source_freshness_state_schemas")?, + &native_field(&snapshot, "source_freshness_state_schemas")?, )?; let reconciliation = reconcile_execution_changes( (&inspection_scope, &snapshot), @@ -227,8 +230,8 @@ pub(crate) fn build_execution_plan<'py>( PlanningRequest { project: project.clone().unbind(), adapter: adapter.clone().unbind(), - selected_keys: inspection.getattr("selected_keys")?.unbind(), - sql_test_selection: inspection.getattr("sql_test_selection")?.unbind(), + 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")? 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 63889b31af..e9f5542fd0 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 @@ -42,6 +42,7 @@ 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::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; @@ -116,7 +117,7 @@ fn plan_model_from_change<'py>( )], )? } else { - request.change_result.getattr("backfill")? + native_field(&request.change_result, "backfill")? }; let cursor_column = config_str(model, "cursor")?; reject_unexposed_cursor_input_columns(model, &cursor_column, &reads.lookups)?; @@ -159,7 +160,7 @@ fn plan_model_from_change<'py>( build_cursor_input_relations((model, adapter), &cursor_column, &reads.lookups)?.into_any(); let mut any_runtime_owned = false; for relation in cursor_input_relations.try_iter()? { - if relation?.getattr("is_runtime_owned")?.is_truthy()? { + if native_field(&relation?, "is_runtime_owned")?.is_truthy()? { any_runtime_owned = true; break; } @@ -226,10 +227,10 @@ fn check_binding_diagnostics<'py>( let py = project.py(); let sql_test = imported(py, COMPILE_TYPES, "CompiledResourceType")?.getattr("SQL_TEST")?; let mut errors: Vec> = Vec::new(); - for diagnostic in project.getattr("diagnostics")?.try_iter()? { + for diagnostic in native_field(project, "diagnostics")?.try_iter()? { let diagnostic = diagnostic?; - if diagnostic.getattr("is_error")?.is_truthy()? - && diagnostic.getattr("resource_type")?.ne(&sql_test)? + if native_field(&diagnostic, "is_error")?.is_truthy()? + && native_field(&diagnostic, "resource_type")?.ne(&sql_test)? { errors.push(diagnostic); } @@ -256,7 +257,7 @@ fn check_binding_diagnostics<'py>( )?; for diagnostic in bound.try_iter()? { let diagnostic = diagnostic?; - if diagnostic.getattr("is_error")?.is_truthy()? { + if native_field(&diagnostic, "is_error")?.is_truthy()? { errors.push(diagnostic); } } @@ -275,9 +276,9 @@ fn check_binding_diagnostics<'py>( }; lines.push(format!( "error[{}]: {}\n --> {}:{}:{}", - diagnostic.getattr("code")?.str()?, - diagnostic.getattr("message")?.str()?, - diagnostic.getattr("path")?.str()?, + native_field(diagnostic, "code")?.str()?, + native_field(diagnostic, "message")?.str()?, + native_field(diagnostic, "path")?.str()?, position("line")?, position("column")?, )); @@ -286,14 +287,14 @@ fn check_binding_diagnostics<'py>( py, ErrorId::PlannerInput, &lines.join("\n"), - &[("code", first.getattr("code")?)], + &[("code", native_field(first, "code")?)], )) } fn names_of(entries: &[Bound<'_, PyAny>]) -> PyResult { Ok(entries .iter() - .map(|entry| Ok(entry.getattr("name")?.str()?.to_string())) + .map(|entry| Ok(native_field(entry, "name")?.str()?.to_string())) .collect::>>()? .join(", ")) } @@ -309,12 +310,14 @@ fn check_microbatch_concurrency<'py>( let create = planner_type(py, RecordId::PlanAction, "CREATE_TABLE")?; let mut configured: Vec> = Vec::new(); for entry in entries { - if entry.getattr("batch_concurrency")?.gt(1)? && entry.getattr("action")?.ne(&skip)? { + if native_field(entry, "batch_concurrency")?.gt(1)? + && native_field(entry, "action")?.ne(&skip)? + { configured.push(entry.clone()); } } - let settings = project.getattr("settings")?; - if !configured.is_empty() && !settings.getattr("microbatch_concurrency")?.is_truthy()? { + let settings = native_field(project, "settings")?; + if !configured.is_empty() && !native_field(&settings, "microbatch_concurrency")?.is_truthy()? { let guidance = object(py, microbatch_guidance())?; let prefix: &str = MICROBATCH_CONCURRENCY_MESSAGE; return Err(error_of( @@ -339,18 +342,18 @@ fn check_microbatch_concurrency<'py>( &format!( "adapter '{}' does not support concurrent microbatch delete/insert DML; set \ batch_concurrency = 1", - adapter.getattr("adapter_name")?.str()? + native_field(adapter, "adapter_name")?.str()? ), &[], )); } let mut concurrent: Vec> = Vec::new(); for entry in configured { - if entry.getattr("action")?.ne(&create)? { + if native_field(&entry, "action")?.ne(&create)? { concurrent.push(entry); } } - if concurrent.is_empty() || !settings.getattr("concurrency")?.gt(1)? { + if concurrent.is_empty() || !native_field(&settings, "concurrency")?.gt(1)? { return Ok(None); } let count = concurrent.len(); @@ -410,7 +413,7 @@ pub(crate) fn plan_entries<'py>( }; let build = inputs.cast::()?.get(); let relations_context = relations.cast::()?.get(); - let selected_keys = scope.getattr("selected_keys")?; + let selected_keys = native_field(&scope, "selected_keys")?; check_binding_diagnostics(&project, &adapter, &relations, &selected_keys)?; let freshness_blocked = build.source_freshness_blocked_model_names.bind(py).clone(); let external_blocked = build.external_blocked_model_names.bind(py).clone(); @@ -421,12 +424,12 @@ pub(crate) fn plan_entries<'py>( runtime_cursor_producer_names(&scope, &relations_context.source_map.bind(py).clone())?, )?; let functions_by_name = PyDict::new(py); - for function in project.getattr("functions")?.try_iter()? { + for function in native_field(&project, "functions")?.try_iter()? { let function = function?; - functions_by_name.set_item(function.getattr("name")?, function)?; + functions_by_name.set_item(native_field(&function, "name")?, function)?; } - let settings = project.getattr("settings")?; - let models_by_name = scope.getattr("models_by_name")?; + let settings = native_field(&project, "settings")?; + let models_by_name = native_field(&scope, "models_by_name")?; let context = Bound::new( py, ModelPlanContext { @@ -448,16 +451,16 @@ pub(crate) fn plan_entries<'py>( let reads = ContextReads::of( context, adapter.clone(), - project.getattr("external_sql_reference_resolver")?, + native_field(&project, "external_sql_reference_resolver")?, )?; - let sql_analysis_enabled = settings.getattr("sql_analysis")?.is_truthy()?; + 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 mut entries: Vec> = Vec::new(); let mut warnings: Vec> = Vec::new(); - for key in scope.getattr("execution_order")?.try_iter()? { + for key in native_field(&scope, "execution_order")?.try_iter()? { let key = key?; - let name = key.getattr("name")?; + let name = native_field(&key, "name")?; if !selected_keys.contains(&key)? || !models.contains(&name)? { continue; } @@ -479,11 +482,11 @@ pub(crate) fn plan_entries<'py>( sql_analysis_enabled, full_refresh: effective_full_refresh, overrides, - change_result: resolved.getattr("change")?, + change_result: native_field(&resolved, "change")?, protect_full_refresh: !freshness_blocked.contains(&name)? && !external_blocked.contains(&name)?, })?; - let entry_name = entry.getattr("name")?; + let entry_name = native_field(&entry, "name")?; if freshness_blocked.contains(&entry_name)? { entry = replaced( &entry, @@ -508,15 +511,15 @@ pub(crate) fn plan_entries<'py>( ], )?; } - if entry.getattr("incremental_mode")?.eq(µbatch)? { - let policy = entry.getattr("unaccounted_partition_policy")?; + if native_field(&entry, "incremental_mode")?.eq(µbatch)? { + let policy = native_field(&entry, "unaccounted_partition_policy")?; let policy = if policy.is_truthy()? { policy } else { - settings.getattr("microbatch_unaccounted_partition_policy")? + native_field(&settings, "microbatch_unaccounted_partition_policy")? }; entry = replaced(&entry, &[("unaccounted_partition_policy", policy)])?; - if entry.getattr("action")?.ne(&skip)? { + if native_field(&entry, "action")?.ne(&skip)? { let limited = apply_configured_microbatch_limits(&entry, &inputs)?; entry = limited.get_item(0)?; for warning in limited.get_item(1)?.try_iter()? { @@ -590,8 +593,8 @@ pub(crate) fn plan_model<'py>( sql_analysis_enabled: args.flag("sql_analysis_enabled")?, full_refresh: args.flag("full_refresh")?, overrides: ( - overrides.getattr("start_cursor_override")?, - overrides.getattr("end_cursor_override")?, + native_field(&overrides, "start_cursor_override")?, + native_field(&overrides, "end_cursor_override")?, ), change_result, protect_full_refresh: true, @@ -608,8 +611,8 @@ fn build_model_materializations<'py>( for entry in model_entries.try_iter()? { let entry = entry?; lookup.set_item( - entry.getattr("name")?, - entry.getattr("materialization_type")?, + native_field(&entry, "name")?, + native_field(&entry, "materialization_type")?, )?; } Ok(lookup) 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 e4b9d2fd82..0789fc370c 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,6 +14,7 @@ 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::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; @@ -58,7 +59,7 @@ fn by_name<'py>(items: &Bound<'py, PyAny>) -> PyResult> { let lookup = PyDict::new(items.py()); for item in items.try_iter()? { let item = item?; - lookup.set_item(item.getattr("name")?, item)?; + lookup.set_item(native_field(&item, "name")?, item)?; } Ok(lookup) } @@ -105,31 +106,31 @@ fn seed_entries<'py>( extras: &Bound<'py, PyAny>, ) -> PyResult>> { let py = project.py(); - let version_hashes = extras.getattr("seed_version_hashes")?; - let metadata_jsons = extras.getattr("seed_metadata_jsons")?; - let plan_reasons = extras.getattr("seed_plan_reasons")?; + let version_hashes = native_field(extras, "seed_version_hashes")?; + let metadata_jsons = native_field(extras, "seed_metadata_jsons")?; + let plan_reasons = native_field(extras, "seed_plan_reasons")?; let mut entries: Vec> = Vec::new(); - for seed in project.getattr("seeds")?.try_iter()? { + for seed in native_field(project, "seeds")?.try_iter()? { let seed = seed?; - if !selected_keys.contains(seed.getattr("key")?)? { + if !selected_keys.contains(native_field(&seed, "key")?)? { continue; } - let name = seed.getattr("name")?; + let name = native_field(&seed, "name")?; entries.push(planner_model( py, RecordId::SeedPlanEntry, &[ - ("key", seed.getattr("key")?), + ("key", native_field(&seed, "key")?), ("name", name.clone()), - ("destination", seed.getattr("destination")?), + ("destination", native_field(&seed, "destination")?), ( "file_path", - seed.getattr("seed_file")?.getattr("file_path")?, + native_field(&seed, "seed_file")?.getattr("file_path")?, ), ("columns", object(py, extract_seed_columns(&seed)?)?), ( "csv_settings", - seed.getattr("schema_entry")?.getattr("csv_settings")?, + native_field(&seed, "schema_entry")?.getattr("csv_settings")?, ), ( "fingerprint_definition", @@ -171,15 +172,18 @@ fn provider_usages<'py>( py, RecordId::PlanProviderUsage, &[ - ("provider_name", usage.getattr("provider_name")?), + ("provider_name", native_field(&usage, "provider_name")?), ("consumer_kind", string(py, consumer_kind)), ("consumer_name", consumer_name.clone()), - ("parameter_name", usage.getattr("parameter_name")?), + ("parameter_name", native_field(&usage, "parameter_name")?), ( "annotation_class_name", - usage.getattr("annotation_class_name")?, + native_field(&usage, "annotation_class_name")?, + ), + ( + "annotation_module", + native_field(&usage, "annotation_module")?, ), - ("annotation_module", usage.getattr("annotation_module")?), ], )?); } @@ -211,13 +215,13 @@ fn hook_provider_usages<'py>( if !hook.is_instance(&python_hook)? { continue; } - let Some(discovered) = hook_by_name.get_item(hook.getattr("name")?)? else { + let Some(discovered) = hook_by_name.get_item(native_field(&hook, "name")?)? else { continue; }; usages.extend(provider_usages( - &discovered.getattr("provider_usages")?, + &native_field(&discovered, "provider_usages")?, HOOK_CONSUMER, - &discovered.getattr("name")?, + &native_field(&discovered, "name")?, )?); } Ok(usages) @@ -231,23 +235,23 @@ fn plan_provider_usages<'py>( ) -> PyResult> { let py = project.py(); let mut usages: Vec> = Vec::new(); - let loader_by_name = by_name(&project.getattr("loader_functions")?)?; + let loader_by_name = by_name(&native_field(project, "loader_functions")?)?; for entry in source_load_entries.try_iter()? { - let Some(loader) = loader_by_name.get_item(entry?.getattr("loader")?)? else { + let Some(loader) = loader_by_name.get_item(native_field(&entry?, "loader")?)? else { continue; }; usages.extend(provider_usages( - &loader.getattr("provider_usages")?, + &native_field(&loader, "provider_usages")?, LOADER_CONSUMER, - &loader.getattr("name")?, + &native_field(&loader, "name")?, )?); } - let hook_by_name = by_name(&project.getattr("hook_functions")?)?; - let materialization_by_name = by_name(&project.getattr("materialization_files")?)?; + let hook_by_name = by_name(&native_field(project, "hook_functions")?)?; + let materialization_by_name = by_name(&native_field(project, "materialization_files")?)?; for entry in model_entries.try_iter()? { let entry = entry?; usages.extend(hook_provider_usages(&entry, &hook_by_name)?); - let custom = entry.getattr("custom_materialization_name")?; + let custom = native_field(&entry, "custom_materialization_name")?; if custom.is_none() { continue; } @@ -255,9 +259,9 @@ fn plan_provider_usages<'py>( continue; }; usages.extend(provider_usages( - &materialization.getattr("provider_usages")?, + &native_field(&materialization, "provider_usages")?, MATERIALIZATION_CONSUMER, - &materialization.getattr("name")?, + &native_field(&materialization, "name")?, )?); } tuple_of(py, usages) @@ -271,22 +275,23 @@ fn require_intermediate_loader_targets<'py>( source_load_entries: &Bound<'py, PyAny>, ) -> PyResult<()> { let py = project.py(); - let loader_functions = project.getattr("loader_functions")?; + let loader_functions = native_field(project, "loader_functions")?; let loader_by_name = by_name(&loader_functions)?; let selected_names = PyFrozenSet::new( py, source_load_entries .try_iter()? - .map(|entry| entry?.getattr("name")) + .map(|entry| native_field(&entry?, "name")) .collect::>>()?, )?; - let existing = snapshot.getattr("existing_relations")?; + let existing = native_field(snapshot, "existing_relations")?; for entry in source_load_entries.try_iter()? { - let source_entry = source_map.call_method1("get", (entry?.getattr("name")?,))?; - if source_entry.is_none() || source_entry.getattr("loader")?.is_none() { + let source_entry = source_map.call_method1("get", (native_field(&entry?, "name")?,))?; + if source_entry.is_none() || native_field(&source_entry, "loader")?.is_none() { continue; } - let Some(loader_function) = loader_by_name.get_item(source_entry.getattr("loader")?)? + let Some(loader_function) = + loader_by_name.get_item(native_field(&source_entry, "loader")?)? else { continue; }; @@ -297,19 +302,21 @@ fn require_intermediate_loader_targets<'py>( for dependency_name in dependencies.try_iter()? { let dependency_name = dependency_name?; let dependency = source_map.call_method1("get", (&dependency_name,))?; - if dependency.is_none() || selected_names.contains(dependency.getattr("name")?)? { + if dependency.is_none() + || selected_names.contains(native_field(&dependency, "name")?)? + { continue; } - let table = dependency.getattr("table")?; + let table = native_field(&dependency, "table")?; let target = if table.is_truthy()? { table } else { - dependency.getattr("name")? + native_field(&dependency, "name")? }; if existing.contains(&target)? { continue; } - let source_name = source_entry.getattr("name")?.str()?; + let source_name = native_field(&source_entry, "name")?.str()?; return Err(error_of( py, ErrorId::PlannerInput, @@ -346,20 +353,23 @@ pub(super) fn function_entry<'py>( &[ ("adapter", adapter.clone()), ("function", function.clone()), - ("model_locations", relations.getattr("model_locations")?), - ("seed_locations", relations.getattr("seed_locations")?), + ( + "model_locations", + native_field(relations, "model_locations")?, + ), + ("seed_locations", native_field(relations, "seed_locations")?), ( "function_locations", - relations.getattr("function_locations")?, + native_field(relations, "function_locations")?, ), - ("source_map", relations.getattr("source_read_map")?), + ("source_map", native_field(relations, "source_read_map")?), ( "source_warehouse_columns", - relations.getattr("source_warehouse_columns")?, + native_field(relations, "source_warehouse_columns")?, ), ( "star_exclude_keyword", - relations.getattr("star_exclude_keyword")?, + native_field(relations, "star_exclude_keyword")?, ), ], )?), @@ -397,18 +407,18 @@ fn function_entries<'py>( selected_keys: &Bound<'py, PyAny>, ) -> PyResult>> { let py = project.py(); - let functions = changes.getattr("functions")?; - let previous = snapshot.getattr("fingerprints")?.getattr("functions")?; + let functions = native_field(changes, "functions")?; + let previous = native_field(snapshot, "fingerprints")?.getattr("functions")?; let mut entries: Vec> = Vec::new(); - for function in project.getattr("functions")?.try_iter()? { + for function in native_field(project, "functions")?.try_iter()? { let function = function?; - if !selected_keys.contains(function.getattr("key")?)? { + if !selected_keys.contains(native_field(&function, "key")?)? { continue; } - let name = function.getattr("name")?; + let name = native_field(&function, "name")?; let change = functions.get_item(&name)?; let previous_query_sql = if previous.contains(&name)? { - previous.get_item(&name)?.getattr("definition")? + native_field(&previous.get_item(&name)?, "definition")? } else { none(py) }; @@ -417,9 +427,9 @@ fn function_entries<'py>( adapter, relations, ( - change.getattr("fingerprint_sql")?, + native_field(&change, "fingerprint_sql")?, previous_query_sql, - change.getattr("reason")?, + native_field(&change, "reason")?, ), )?); } @@ -432,17 +442,17 @@ pub(super) fn test_selected( selected_keys: &Bound<'_, PyAny>, selection: &Bound<'_, PyAny>, ) -> PyResult { - let parent = test.getattr("parent_name")?; + let parent = native_field(test, "parent_name")?; let name = if parent.is_truthy()? { parent } else { - test.getattr("name")? + native_field(test, "name")? }; - if selection.getattr("excluded_names")?.contains(&name)? { + if native_field(selection, "excluded_names")?.contains(&name)? { return Ok(false); } - Ok(selection.getattr("names")?.contains(&name)? - || scope_overlaps(&test.getattr("scope_deps")?, selected_keys)?) + Ok(native_field(selection, "names")?.contains(&name)? + || scope_overlaps(&native_field(test, "scope_deps")?, selected_keys)?) } /// Planned SQL test entries and the warnings planning them produced. @@ -460,12 +470,12 @@ pub(super) fn selected_test_entries<'py>( ) -> PyResult> { let py = project.py(); let mut selected: Vec> = Vec::new(); - for test in project.getattr("sql_tests")?.try_iter()? { + for test in native_field(project, "sql_tests")?.try_iter()? { let test = test?; if !test_selected(&test, selected_keys, selection)? { continue; } - if !case_name.is_none() && test.getattr("case_name")?.ne(case_name)? { + if !case_name.is_none() && native_field(&test, "case_name")?.ne(case_name)? { continue; } selected.push(test); @@ -475,7 +485,7 @@ pub(super) fn selected_test_entries<'py>( if selected.is_empty() { return Ok(SelectedTests { entries, warnings }); } - let analysis = project.getattr("settings")?.getattr("sql_analysis")?; + let analysis = native_field(project, "settings")?.getattr("sql_analysis")?; let fixture_context = if analysis.is_truthy()? { Bound::new(py, planning_context(project, None)?)?.into_any() } else { @@ -493,15 +503,15 @@ pub(super) fn selected_test_entries<'py>( let mut diagnostics: Vec = Vec::new(); for result in results.try_iter()? { let result = result?; - let entry = result.getattr("entry")?; + let entry = native_field(&result, "entry")?; if entry.is_none() { - for diagnostic in result.getattr("fixture_diagnostics")?.try_iter()? { + for diagnostic in native_field(&result, "fixture_diagnostics")?.try_iter()? { diagnostics.push(diagnostic?.str()?.to_string()); } continue; } entries.push(entry); - for warning in result.getattr("warnings")?.try_iter()? { + for warning in native_field(&result, "warnings")?.try_iter()? { warnings.push(warning?); } } @@ -559,9 +569,9 @@ pub(crate) fn plan_output_fields<'py>( } else { extras }; - let selected_keys = scope.getattr("selected_keys")?; - let execution_order = scope.getattr("execution_order")?; - let source_map = relations.getattr("source_map")?; + let selected_keys = native_field(&scope, "selected_keys")?; + let execution_order = native_field(&scope, "execution_order")?; + let source_map = native_field(&relations, "source_map")?; let seeds = seed_entries(&project, &snapshot, &selected_keys, &extras)?; let source_load_entries = object( py, @@ -582,8 +592,8 @@ pub(crate) fn plan_output_fields<'py>( let materializations = PyDict::new(py); for entry in &model_entries { materializations.set_item( - entry.getattr("name")?, - entry.getattr("materialization_type")?, + native_field(entry, "name")?, + native_field(entry, "materialization_type")?, )?; } let audit_entries = object( @@ -599,18 +609,18 @@ pub(crate) fn plan_output_fields<'py>( ], )?))?, )?; - let selection = scope.getattr("sql_test_selection")?; - let mut planned = extras.getattr("planned_sql_tests")?; + let selection = native_field(&scope, "sql_test_selection")?; + let mut planned = native_field(&extras, "planned_sql_tests")?; if planned.is_none() - || planned.getattr("selected_keys")?.ne(&selected_keys)? - || planned.getattr("sql_test_selection")?.ne(&selection)? + || native_field(&planned, "selected_keys")?.ne(&selected_keys)? + || native_field(&planned, "sql_test_selection")?.ne(&selection)? { planned = planned_sql_tests(&project, &adapter, &selected_keys, &selection)?; } - let test_entries = planned.getattr("entries")?; + let test_entries = native_field(&planned, "entries")?; let mut test_keys: Vec> = Vec::new(); for entry in test_entries.try_iter()? { - test_keys.push(entry?.getattr("key")?); + test_keys.push(native_field(&entry?, "key")?); } let scoped_keys = selected_keys.call_method1("__or__", (PyFrozenSet::new(py, test_keys)?,))?; let mut scoped_order: Vec> = Vec::new(); @@ -621,7 +631,7 @@ pub(crate) fn plan_output_fields<'py>( } } let mut warnings: Vec> = model_warnings; - for warning in planned.getattr("warnings")?.try_iter()? { + for warning in native_field(&planned, "warnings")?.try_iter()? { warnings.push(warning?); } let model_entries = tuple_of(py, model_entries)?; @@ -650,24 +660,33 @@ pub(crate) fn plan_output_fields<'py>( ), ("selected_keys", selected_keys), ("warnings", tuple_of(py, warnings)?), - ("upstream_deps", scope.getattr("upstream_deps")?), - ("downstream_deps", scope.getattr("downstream_deps")?), - ("model_locations", relations.getattr("model_locations")?), - ("seed_locations", relations.getattr("seed_locations")?), + ("upstream_deps", native_field(&scope, "upstream_deps")?), + ("downstream_deps", native_field(&scope, "downstream_deps")?), + ( + "model_locations", + native_field(&relations, "model_locations")?, + ), + ( + "seed_locations", + native_field(&relations, "seed_locations")?, + ), ( "function_locations", - relations.getattr("function_locations")?, + native_field(&relations, "function_locations")?, ), ("source_map", source_map), - ("source_read_map", relations.getattr("source_read_map")?), + ( + "source_read_map", + native_field(&relations, "source_read_map")?, + ), ( "python_source_read_map", - relations.getattr("python_source_read_map")?, + native_field(&relations, "python_source_read_map")?, ), - ("hook_functions", project.getattr("hook_functions")?), + ("hook_functions", native_field(&project, "hook_functions")?), ( "enforce_explicit_references", - project.getattr("enforce_explicit_references")?, + native_field(&project, "enforce_explicit_references")?, ), ( "provider_usages", @@ -675,7 +694,7 @@ pub(crate) fn plan_output_fields<'py>( ), ( "python_identity_fingerprints", - snapshot.getattr("fingerprints")?.getattr("python_nodes")?, + native_field(&snapshot, "fingerprints")?.getattr("python_nodes")?, ), ]) } @@ -736,7 +755,7 @@ fn sql_test_model_chain_names_by_key<'py>( let chains = resolve_sql_test_model_chains(project, tests)?; let by_key = PyDict::new(py); for (test, chain) in tests.try_iter()?.zip(chains.try_iter()?) { - by_key.set_item(test?.getattr("key")?, chain?)?; + by_key.set_item(native_field(&test?, "key")?, chain?)?; } Ok(by_key) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/relations.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/relations.rs index d67d2d00b0..f362d2cafc 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/relations.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_entries/relations.rs @@ -17,6 +17,7 @@ use crate::bindings::_helpers::planner_warehouse::semantic_sources::semantic_sou use crate::bindings::_helpers::planner_warehouse::source_columns::gather_source_inspection; use crate::bindings::_helpers::planner_warehouse::source_deferral::build_source_read_map; use crate::bindings::_helpers::planner_warehouse::source_deferral::with_declared_source_reads; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; use pyo3::types::{PyDict, PyDictMethods, PyFrozenSet, PySet, PySetMethods, PyTuple}; @@ -29,10 +30,10 @@ fn selected_audit_relation_keys<'py>( ) -> PyResult> { let py = project.py(); let keys = PySet::empty(py)?; - for audit in project.getattr("audits")?.try_iter()? { + for audit in native_field(project, "audits")?.try_iter()? { let audit = audit?; if audit_is_selected(&audit, selected_keys)? { - for dependency in audit.getattr("scope_deps")?.try_iter()? { + for dependency in native_field(&audit, "scope_deps")?.try_iter()? { keys.add(dependency?)?; } } @@ -95,14 +96,17 @@ pub(crate) fn planner_relations_context<'py>( } else { deferral }; - let selected_keys = scope.getattr("selected_keys")?; - let mut model_locations = object(py, build_model_locations(&project.getattr("models")?)?)?; - let mut seed_locations = object(py, build_seed_locations(&project.getattr("seeds")?)?)?; + let selected_keys = native_field(&scope, "selected_keys")?; + let mut model_locations = object( + py, + build_model_locations(&native_field(&project, "models")?)?, + )?; + let mut seed_locations = object(py, build_seed_locations(&native_field(&project, "seeds")?)?)?; let function_locations = object( py, - build_function_locations(&project.getattr("functions")?)?, + build_function_locations(&native_field(&project, "functions")?)?, )?; - let deferred_locations = deferral.getattr("deferred_locations")?; + let deferred_locations = native_field(&deferral, "deferred_locations")?; if !deferred_locations.is_none() { let applied = object( py, @@ -127,15 +131,15 @@ pub(crate) fn planner_relations_context<'py>( build_source_load_map( &project, &load_keys, - Some(&if scope.getattr("all_keys")?.is_truthy()? { - scope.getattr("upstream_deps")? + Some(&if native_field(&scope, "all_keys")?.is_truthy()? { + native_field(&scope, "upstream_deps")? } else { none(py) }) .filter(|value| !value.is_none()), )?, )?; - let deferral_enabled = deferral.getattr("source_deferral_enabled")?.is_truthy()?; + let deferral_enabled = native_field(&deferral, "source_deferral_enabled")?.is_truthy()?; let read_map = |map: &Bound<'py, PyAny>, names: Option>| -> PyResult> { @@ -148,7 +152,10 @@ pub(crate) fn planner_relations_context<'py>( ("selected_keys", selected_keys.clone()), ("project_config", project_config.clone()), ("local_config", local_config.clone()), - ("defer_sources_to", deferral.getattr("defer_sources_to")?), + ( + "defer_sources_to", + native_field(&deferral, "defer_sources_to")?, + ), ]; if let Some(names) = names { pairs.push(("read_source_names", names)); @@ -161,7 +168,7 @@ pub(crate) fn planner_relations_context<'py>( with_declared_source_reads( &project, source_map.downcast::()?, - &scope.getattr("python_read_source_names")?, + &native_field(&scope, "python_read_source_names")?, )?, )?; let python_names = PyFrozenSet::new( 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 6335cc5e60..2343090563 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_identity/graph.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_identity/graph.rs @@ -26,6 +26,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -67,7 +68,7 @@ impl<'py> KeyObjects<'py> { } fn function_resource(key: &Bound<'_, PyAny>) -> PyResult { - let kind: String = key.getattr("resource_type")?.str()?.extract()?; + let kind: String = native_field(key, "resource_type")?.str()?.extract()?; Ok(kind == UDF_RESOURCE || kind == TABLE_FN_RESOURCE) } @@ -89,7 +90,7 @@ pub(crate) fn build_execution_upstream_deps<'py>( for resource in project.getattr(collection)?.try_iter()? { let resource: Bound<'py, PyAny> = resource?; upstream.set_item( - resource.getattr("key")?, + native_field(&resource, "key")?, PyList::new( py, resource @@ -100,12 +101,12 @@ pub(crate) fn build_execution_upstream_deps<'py>( )?; } } - for test in project.getattr("sql_tests")?.try_iter()? { + for test in native_field(project, "sql_tests")?.try_iter()? { let test: Bound<'py, PyAny> = test?; - let test_key: Bound<'py, PyAny> = test.getattr("key")?; - let scope_deps: Bound<'py, PyAny> = test.getattr("scope_deps")?; + let test_key: Bound<'py, PyAny> = native_field(&test, "key")?; + let scope_deps: Bound<'py, PyAny> = native_field(&test, "scope_deps")?; let deps: Bound<'py, PyList> = PyList::empty(py); - if test.getattr("mode")?.str()?.extract::()? == TABLE_FN_TEST_MODE { + if native_field(&test, "mode")?.str()?.extract::()? == TABLE_FN_TEST_MODE { for scope_dep in scope_deps.try_iter()? { let scope_dep: Bound<'py, PyAny> = scope_dep?; if function_resource(&scope_dep)? { @@ -169,21 +170,21 @@ pub(crate) fn build_execution_edge_origins<'py>( ) -> PyResult> { let py: Python<'py> = project.py(); let origins: Bound<'py, PyDict> = PyDict::new(py); - for test in project.getattr("sql_tests")?.try_iter()? { + for test in native_field(project, "sql_tests")?.try_iter()? { let test: Bound<'py, PyAny> = test?; - let test_name: String = test.getattr("name")?.extract()?; - for target_key in test.getattr("scope_deps")?.try_iter()? { + let test_name: String = native_field(&test, "name")?.extract()?; + for target_key in native_field(&test, "scope_deps")?.try_iter()? { let target_key: Bound<'py, PyAny> = target_key?; - let target_name: String = target_key.getattr("name")?.extract()?; + let target_name: String = native_field(&target_key, "name")?.extract()?; origins.set_item( - (target_key, test.getattr("key")?), + (target_key, native_field(&test, "key")?), format!("SQL test '{test_name}' runs before '{target_name}'"), )?; } } let (gates, hooks): (Vec, Vec) = native_execution_gate_edges(project)?; let cache: KeyCache<'py> = KeyCache::new(py)?; - let lineage: Bound<'py, PyAny> = project.getattr("lineage_graph")?; + let lineage: Bound<'py, PyAny> = native_field(project, "lineage_graph")?; let is_lineage = |gated: &(String, String), read: &(String, String)| -> PyResult { lineage .call_method1("upstream", (gated.clone(),))? @@ -323,8 +324,8 @@ fn missing_rows<'py>( .map(|entry| { let entry: Bound<'py, PyAny> = entry?; Ok(MissingUpstream { - key: objects.key(entry.getattr("key")?)?, - required_by: objects.keys(&entry.getattr("required_by")?)?, + key: objects.key(native_field(&entry, "key")?)?, + required_by: objects.keys(&native_field(&entry, "required_by")?)?, }) }) .collect() @@ -365,7 +366,7 @@ pub(crate) fn check_buildability<'py>( } else { objects.keys(&satisfied_keys)?.into_iter().collect() }, - existing_relations: name_set(&snapshot.getattr("existing_relations")?)?, + existing_relations: name_set(&native_field(&snapshot, "existing_relations")?)?, deferred_relations: if deferred_relations.is_none() { None } else { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_identity/hashing.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_identity/hashing.rs index 07a5cc44d6..932af40c79 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_identity/hashing.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_identity/hashing.rs @@ -14,6 +14,7 @@ use sqlbuild_core::panics::main::is_compiler_panic::is_compiler_panic; use crate::bindings::_helpers::boundary::panics::compiler_error; 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::ErrorId; use crate::bindings::_helpers::records::registry; use crate::bindings::types::CompilerDetach; @@ -67,8 +68,8 @@ fn fingerprint_error(py: Python<'_>, subject: &str, message: String) -> PyErr { /// The graph key of a `CompiledObjectKey`-like value: its resource type text and name. pub(crate) fn graph_key(key: &Bound<'_, PyAny>) -> PyResult { Ok(GraphKey { - kind: key.getattr("resource_type")?.str()?.extract()?, - name: key.getattr("name")?.extract()?, + kind: native_field(key, "resource_type")?.str()?.extract()?, + name: native_field(key, "name")?.extract()?, }) } 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 5f93834e0a..2dfaca4bd6 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_identity/identities.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_identity/identities.rs @@ -24,6 +24,7 @@ use crate::bindings::_helpers::planner_identity::hashing::{graph_key, model_quer use crate::bindings::_helpers::planner_identity::metadata::{ function_local_hashes, model_version_identity_metadata_json, seed_identity, }; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; @@ -56,7 +57,7 @@ fn hook_version_hashes<'py>(hook_functions: &Bound<'py, PyAny>) -> PyResult, Bound<'py, PyAny>) = item?.extract()?; - hashes.set_item(name, identity.getattr("version_hash")?)?; + hashes.set_item(name, native_field(&identity, "version_hash")?)?; } Ok(hashes) } @@ -91,14 +92,14 @@ pub(crate) fn build_direct_model_version_identities<'py>( let mut graph = IdentityGraph::default(); for function in functions.try_iter()? { let function: Bound<'py, PyAny> = function?; - let name: String = function.getattr("name")?.extract()?; + let name: String = native_field(&function, "name")?.extract()?; let Some(function_hash) = function_hashes.get_item(&name)? else { continue; }; graph.nodes.insert( - graph_key(&function.getattr("key")?)?, + graph_key(&native_field(&function, "key")?)?, IdentityNode { - upstream_keys: graph_keys(&function.getattr("deps")?)?, + upstream_keys: graph_keys(&native_field(&function, "deps")?)?, local_hash: Some(function_hash.extract()?), }, ); @@ -110,7 +111,7 @@ pub(crate) fn build_direct_model_version_identities<'py>( for seed in seeds.try_iter()? { let seed: Bound<'py, PyAny> = seed?; let (seed_hash, metadata_json): (String, String) = seed_identity(&seed)?; - let name: Bound<'py, PyAny> = seed.getattr("name")?; + let name: Bound<'py, PyAny> = native_field(&seed, "name")?; seed_hashes.set_item(&name, &seed_hash)?; seed_metadata.set_item(&name, metadata_json)?; } @@ -138,8 +139,8 @@ pub(crate) fn build_direct_model_version_identities<'py>( } let model_metadata: Bound<'py, PyDict> = PyDict::new(py); let model_local: Bound<'py, PyDict> = PyDict::new(py); - let models_by_name: Bound<'py, PyAny> = scope.getattr("models_by_name")?; - for key in scope.getattr("execution_order")?.try_iter()? { + let models_by_name: Bound<'py, PyAny> = native_field(&scope, "models_by_name")?; + for key in native_field(&scope, "execution_order")?.try_iter()? { let key: Bound<'py, PyAny> = key?; let order_key: GraphKey = graph_key(&key)?; graph.execution_order.push(order_key.clone()); @@ -170,7 +171,7 @@ pub(crate) fn build_direct_model_version_identities<'py>( graph.nodes.insert( key_of(MODEL_RESOURCE, &name), IdentityNode { - upstream_keys: graph_keys(&model.getattr("deps")?)?, + upstream_keys: graph_keys(&native_field(&model, "deps")?)?, local_hash: Some(local_hash), }, ); @@ -218,10 +219,10 @@ pub(crate) fn with_honest_model_write_hashes<'py>( let snapshot: Bound<'py, PyAny> = arguments.value("snapshot")?; let changes: Bound<'py, PyAny> = arguments.value("changes")?; let identities: Bound<'py, PyAny> = arguments.value("version_identities")?; - let selected_python: Bound<'py, PyAny> = scope.getattr("selected_keys")?; + let selected_python: Bound<'py, PyAny> = native_field(&scope, "selected_keys")?; let selected: HashSet = graph_keys(&selected_python)?.into_iter().collect(); let function_hashes: Vec<(String, String)> = - string_pairs(&identities.getattr("function_local_hashes")?)?; + string_pairs(&native_field(&identities, "function_local_hashes")?)?; let mut graph = IdentityGraph::default(); let mut base_hashes: Vec<(GraphKey, String)> = Vec::new(); let mut function_deps: HashMap> = HashMap::new(); @@ -252,8 +253,9 @@ pub(crate) fn with_honest_model_write_hashes<'py>( base_hashes.push((function_key, function_hash.clone())); } } - let recorded_seeds: Bound<'py, PyAny> = snapshot.getattr("fingerprints")?.getattr("seeds")?; - for (name, seed_hash) in string_pairs(&identities.getattr("seed_version_hashes")?)? { + let recorded_seeds: Bound<'py, PyAny> = + native_field(&snapshot, "fingerprints")?.getattr("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( seed_key.clone(), @@ -266,11 +268,11 @@ pub(crate) fn with_honest_model_write_hashes<'py>( let write_hash: String = if selected.contains(&seed_key) || fingerprint.is_none() { seed_hash } else { - fingerprint.getattr("version_hash")?.extract()? + native_field(&fingerprint, "version_hash")?.extract()? }; base_hashes.push((seed_key, write_hash)); } - let local_hashes: Bound<'py, PyAny> = identities.getattr("model_local_hashes")?; + let local_hashes: Bound<'py, PyAny> = native_field(&identities, "model_local_hashes")?; for model in scope .getattr("models_by_name")? .call_method0("values")? @@ -281,15 +283,16 @@ pub(crate) fn with_honest_model_write_hashes<'py>( graph.nodes.insert( key_of(MODEL_RESOURCE, &name.extract::()?), IdentityNode { - upstream_keys: graph_keys(&model.getattr("deps")?)?, + upstream_keys: graph_keys(&native_field(&model, "deps")?)?, local_hash: local_hashes.call_method1("get", (&name,))?.extract()?, }, ); } - let recorded_models: Bound<'py, PyAny> = snapshot.getattr("fingerprints")?.getattr("models")?; + let recorded_models: Bound<'py, PyAny> = + native_field(&snapshot, "fingerprints")?.getattr("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> = fingerprint.getattr("version_hash")?; + let version_hash: Bound<'py, PyAny> = native_field(&fingerprint, "version_hash")?; if version_hash.is_truthy()? { base_hashes.push((key_of(MODEL_RESOURCE, &name), version_hash.extract()?)); } @@ -300,7 +303,7 @@ pub(crate) fn with_honest_model_write_hashes<'py>( base_hashes.push((key_of(MODEL_RESOURCE, &name), model_hash)); } } - graph.execution_order = graph_keys(&scope.getattr("execution_order")?)?; + graph.execution_order = graph_keys(&native_field(&scope, "execution_order")?)?; let write_hashes: HashMap = graph_write_identity_hashes(&graph, &selected, base_hashes) .into_iter() @@ -313,8 +316,8 @@ pub(crate) fn with_honest_model_write_hashes<'py>( .downcast::()? .as_mapping(), )?; - let models_by_name: Bound<'py, PyAny> = scope.getattr("models_by_name")?; - for key in scope.getattr("execution_order")?.try_iter()? { + let models_by_name: Bound<'py, PyAny> = native_field(&scope, "models_by_name")?; + for key in native_field(&scope, "execution_order")?.try_iter()? { let key: Bound<'py, PyAny> = key?; let order_key: GraphKey = graph_key(&key)?; if order_key.kind != MODEL_RESOURCE || !selected.contains(&order_key) { @@ -360,7 +363,7 @@ pub(crate) fn merge_recomputed_model_changes<'py>( ) -> PyResult> { let py: Python<'py> = resolved_actions.py(); let replace: Bound<'py, PyAny> = imported(py, DATACLASSES_MODULE, "replace")?; - let recomputed: Bound<'py, PyAny> = changes.getattr("models")?; + let recomputed: Bound<'py, PyAny> = native_field(changes, "models")?; let merged: Bound<'py, PyDict> = PyDict::new(py); for item in resolved_actions .getattr("models")? @@ -370,7 +373,7 @@ pub(crate) fn merge_recomputed_model_changes<'py>( let (name, resolved): (Bound<'py, PyAny>, Bound<'py, PyAny>) = item?.extract()?; let mut change: Bound<'py, PyAny> = recomputed.call_method1("get", (&name,))?; if change.is_none() { - change = resolved.getattr("change")?; + change = native_field(&resolved, "change")?; } merged.set_item(name, replace_with(&replace, &resolved, ("change", change))?)?; } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_identity/json_values.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_identity/json_values.rs index ee7f791b5d..0769b4e92f 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_identity/json_values.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_identity/json_values.rs @@ -7,6 +7,7 @@ use pyo3::types::{ PyTypeMethods, }; +use crate::bindings::_helpers::records::fields::field as native_field; use sqlbuild_core::json::main::dumps::dumps; use sqlbuild_core::json::main::python_float_repr::python_float_repr; use sqlbuild_core::json::models::{JsonDialect, JsonInteger, JsonValue, StdlibJsonOptions}; @@ -56,8 +57,8 @@ impl<'py> Encoder<'py> { let builtins = py.import(BUILTINS_MODULE)?; Ok(Self { default_str, - int_repr: builtins.getattr("int")?.getattr("__repr__")?, - sorted: builtins.getattr("sorted")?, + int_repr: native_field(&builtins, "int")?.getattr("__repr__")?, + sorted: native_field(&builtins, "sorted")?, }) } 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 a909523176..f830a766ec 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_identity/metadata.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_identity/metadata.rs @@ -19,6 +19,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::{imported, keywords use crate::bindings::_helpers::discovery::hook_entries::hook_entries; use crate::bindings::_helpers::planner_identity::hashing::function_definition_hash; use crate::bindings::_helpers::planner_identity::json_values::{COMPACT, SPACED, sorted_dumps}; +use crate::bindings::_helpers::records::fields::field as native_field; const LOCAL_FUNCTION_HASHES_KEY: &str = "local_function_hashes"; const DATACLASSES_MODULE: &str = "dataclasses"; @@ -86,20 +87,22 @@ pub(crate) fn model_version_identity_metadata_json<'py>( let py: Python<'py> = model.py(); let local_function_hashes: Bound<'py, PyDict> = PyDict::new(py); if let Some(function_hashes) = function_local_hashes.filter(|hashes| !hashes.is_none()) { - for upstream_key in model.getattr("deps")?.try_iter()? { + for upstream_key in native_field(model, "deps")?.try_iter()? { let upstream_key: Bound<'py, PyAny> = upstream_key?; - let kind: String = upstream_key.getattr("resource_type")?.str()?.extract()?; + let kind: String = native_field(&upstream_key, "resource_type")? + .str()? + .extract()?; if kind != UDF_RESOURCE && kind != TABLE_FN_RESOURCE { continue; } - let name: Bound<'py, PyAny> = upstream_key.getattr("name")?; + let name: Bound<'py, PyAny> = native_field(&upstream_key, "name")?; let upstream_hash: Bound<'py, PyAny> = function_hashes.call_method1("get", (&name,))?; if !upstream_hash.is_none() { local_function_hashes.set_item(name, upstream_hash)?; } } } - let config_values: Bound<'py, PyAny> = model.getattr("config")?.getattr("values")?; + let config_values: Bound<'py, PyAny> = native_field(model, "config")?.getattr("values")?; let signature: Bound<'py, PyDict> = execution_signature(model, &config_values, hook_version_hashes)?; version_identity_metadata_json( @@ -164,39 +167,39 @@ fn contract_output_signature<'py>( config_values: &Bound<'py, PyAny>, ) -> PyResult>> { let py: Python<'py> = model.py(); - let schema_entry: Bound<'py, PyAny> = model.getattr("schema_entry")?; + let schema_entry: Bound<'py, PyAny> = native_field(model, "schema_entry")?; if schema_entry.is_none() { return Ok(None); } - let columns: Bound<'py, PyAny> = schema_entry.getattr("columns")?; - let families: Bound<'py, PyAny> = schema_entry.getattr("dynamic_columns")?; + let columns: Bound<'py, PyAny> = native_field(&schema_entry, "columns")?; + let families: Bound<'py, PyAny> = native_field(&schema_entry, "dynamic_columns")?; if !(columns.is_truthy()? || families.is_truthy()?) { return Ok(None); } let enforced: bool = config_values .call_method1("get", (CONTRACT_CONFIG_KEY,))? .eq(ENFORCED_CONTRACT_POLICY)?; - if !enforced && schema_entry.getattr("model_schema")?.is_none() { + if !enforced && native_field(&schema_entry, "model_schema")?.is_none() { return Ok(None); } - let enum_columns: Bound<'py, PyAny> = model.getattr("enum_columns")?; + let enum_columns: Bound<'py, PyAny> = native_field(model, "enum_columns")?; let rendered: Bound<'py, PyList> = PyList::empty(py); for column in columns.try_iter()? { let column: Bound<'py, PyAny> = column?; let entry: Bound<'py, PyDict> = column_signature(&column)?; let declaration: Bound<'py, PyAny> = - enum_columns.call_method1("get", (column.getattr("name")?,))?; + enum_columns.call_method1("get", (native_field(&column, "name")?,))?; if !declaration.is_none() { let members: Bound<'py, PyList> = PyList::empty(py); - for member in declaration.getattr("members")?.try_iter()? { + for member in native_field(&declaration, "members")?.try_iter()? { let member: Bound<'py, PyAny> = member?; let rendered_member: Bound<'py, PyDict> = PyDict::new(py); - rendered_member.set_item("name", member.getattr("name")?)?; - rendered_member.set_item("value", member.getattr("value")?)?; + rendered_member.set_item("name", native_field(&member, "name")?)?; + rendered_member.set_item("value", native_field(&member, "value")?)?; members.append(rendered_member)?; } let enum_entry: Bound<'py, PyDict> = PyDict::new(py); - enum_entry.set_item("name", declaration.getattr("name")?)?; + enum_entry.set_item("name", native_field(&declaration, "name")?)?; enum_entry.set_item("members", members)?; entry.set_item("enum", enum_entry)?; } @@ -242,7 +245,7 @@ fn family_signatures<'py>(families: &Bound<'py, PyAny>) -> PyResult) -> PyResult { let py: Python<'_> = model.py(); - let schema_entry: Bound<'_, PyAny> = model.getattr("schema_entry")?; + let schema_entry: Bound<'_, PyAny> = native_field(model, "schema_entry")?; let payload: Bound<'_, PyDict> = PyDict::new(py); payload.set_item( "contract", @@ -255,11 +258,11 @@ pub(crate) fn model_declared_columns_hash(model: &Bound<'_, PyAny>) -> PyResult< let mut families: Bound<'_, PyList> = PyList::empty(py); let mut type_enforcement: Bound<'_, PyAny> = py.None().into_bound(py); if !schema_entry.is_none() { - for column in schema_entry.getattr("columns")?.try_iter()? { + for column in native_field(&schema_entry, "columns")?.try_iter()? { columns.append(column_signature(&column?)?)?; } - families = family_signatures(&schema_entry.getattr("dynamic_columns")?)?; - type_enforcement = schema_entry.getattr("type_enforcement")?; + families = family_signatures(&native_field(&schema_entry, "dynamic_columns")?)?; + type_enforcement = native_field(&schema_entry, "type_enforcement")?; } payload.set_item("columns", columns)?; payload.set_item("dynamic_columns", families)?; @@ -280,13 +283,13 @@ pub(crate) fn function_local_hashes<'py>( let hashes: Bound<'py, PyDict> = PyDict::new(py); for function in functions.try_iter()? { let function: Bound<'py, PyAny> = function?; - let name: String = function.getattr("name")?.extract()?; - let language: Bound<'py, PyAny> = function.getattr("language")?.getattr("value")?; + let name: String = native_field(&function, "name")?.extract()?; + let language: Bound<'py, PyAny> = native_field(&function, "language")?.getattr("value")?; let body_hash: String = function_definition_hash( py, &name, ( - &function.getattr("body_sql")?.extract::()?, + &native_field(&function, "body_sql")?.extract::()?, &language.extract::()?, dialect, ), @@ -294,12 +297,12 @@ pub(crate) fn function_local_hashes<'py>( let payload: Bound<'py, PyDict> = PyDict::new(py); payload.set_item( "arguments", - name_type_pairs(&function.getattr("arguments")?)?, + name_type_pairs(&native_field(&function, "arguments")?)?, )?; - payload.set_item("returns", function.getattr("returns")?)?; + payload.set_item("returns", native_field(&function, "returns")?)?; payload.set_item( "return_columns", - name_type_pairs(&function.getattr("return_columns")?)?, + name_type_pairs(&native_field(&function, "return_columns")?)?, )?; payload.set_item("body_sql", body_hash)?; payload.set_item("language", language)?; @@ -320,7 +323,7 @@ fn name_type_pairs<'py>(items: &Bound<'py, PyAny>) -> PyResult = item?; pairs.append(PyTuple::new( items.py(), - [item.getattr("name")?, item.getattr("type")?], + [native_field(&item, "name")?, native_field(&item, "type")?], )?)?; } Ok(pairs) @@ -352,10 +355,10 @@ fn rendered_pairs(items: &Bound<'_, PyAny>) -> PyResult> { /// Stable function definition text used for change fingerprints. pub(crate) fn compiled_function_fingerprint_sql(function: &Bound<'_, PyAny>) -> PyResult { Ok(function_fingerprint_sql(&FunctionFingerprintFields { - language: function.getattr("language")?.str()?.extract()?, - arguments: rendered_pairs(&function.getattr("arguments")?)?, - returns: function.getattr("returns")?.str()?.extract()?, - return_columns: rendered_pairs(&function.getattr("return_columns")?)?, + language: native_field(function, "language")?.str()?.extract()?, + arguments: rendered_pairs(&native_field(function, "arguments")?)?, + returns: native_field(function, "returns")?.str()?.extract()?, + 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 @@ -363,29 +366,29 @@ pub(crate) fn compiled_function_fingerprint_sql(function: &Bound<'_, PyAny>) -> .try_iter()? .map(|package| package?.extract::()) .collect::>()?, - body_sql: function.getattr("body_sql")?.extract()?, + body_sql: native_field(function, "body_sql")?.extract()?, })) } /// `(identity_hash, metadata_json)` of one compiled seed from its content and load-affecting config. pub(crate) fn seed_identity(seed: &Bound<'_, PyAny>) -> PyResult<(String, String)> { let py: Python<'_> = seed.py(); - let schema_entry: Bound<'_, PyAny> = seed.getattr("schema_entry")?; - let csv_settings: Bound<'_, PyAny> = schema_entry.getattr("csv_settings")?; + let schema_entry: Bound<'_, PyAny> = native_field(seed, "schema_entry")?; + let csv_settings: Bound<'_, PyAny> = native_field(&schema_entry, "csv_settings")?; let columns: Bound<'_, PyList> = PyList::empty(py); - for column in schema_entry.getattr("columns")?.try_iter()? { + for column in native_field(&schema_entry, "columns")?.try_iter()? { columns.append(column_signature(&column?)?)?; } - let rows: Bound<'_, PyAny> = if seed.getattr("external")?.is_truthy()? { + let rows: Bound<'_, PyAny> = if native_field(seed, "external")?.is_truthy()? { PyList::empty(py).into_any() } else { seed_rows( - &seed.getattr("seed_file")?.getattr("file_path")?, + &native_field(seed, "seed_file")?.getattr("file_path")?, &csv_settings, )? }; let payload: Bound<'_, PyDict> = PyDict::new(py); - payload.set_item("name", seed.getattr("name")?)?; + payload.set_item("name", native_field(seed, "name")?)?; payload.set_item( "csv_settings", imported(py, DATACLASSES_MODULE, "asdict")?.call1((&csv_settings,))?, @@ -402,7 +405,7 @@ fn seed_rows<'py>( csv_settings: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = file_path.py(); - let encoding: Bound<'py, PyAny> = match csv_settings.getattr("encoding")? { + let encoding: Bound<'py, PyAny> = match native_field(csv_settings, "encoding")? { value if value.is_truthy()? => value, _ => PyString::new(py, DEFAULT_SEED_ENCODING).into_any(), }; 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 716f47a687..99901a016b 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_identity/staleness.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_identity/staleness.rs @@ -19,6 +19,7 @@ use sqlbuild_analysis::planner_identity::models::{ use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; 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; @@ -32,7 +33,7 @@ const OWN_CHANGE_KINDS: [&str; 5] = [ ]; fn own_change(change: &Bound<'_, PyAny>) -> PyResult { - let kind: String = change.getattr("change_kind")?.str()?.extract()?; + let kind: String = native_field(change, "change_kind")?.str()?.extract()?; Ok(OWN_CHANGE_KINDS.contains(&kind.as_str())) } @@ -71,7 +72,7 @@ struct StalenessInputs<'py> { fn changed_model_names(inputs: &StalenessInputs<'_>) -> PyResult> { let py: Python<'_> = inputs.snapshot.py(); - let models: Bound<'_, PyAny> = inputs.changes.getattr("models")?; + let models: Bound<'_, PyAny> = native_field(&inputs.changes, "models")?; let mut changed: HashSet = HashSet::new(); for item in models.call_method0("items")?.try_iter()? { let (name, change): (String, Bound<'_, PyAny>) = item?.extract()?; @@ -79,8 +80,10 @@ fn changed_model_names(inputs: &StalenessInputs<'_>) -> PyResult changed.insert(name); } } - let version_hashes: Bound<'_, PyAny> = inputs.identities.getattr("model_version_hashes")?; - let metadata_jsons: Bound<'_, PyAny> = inputs.identities.getattr("model_metadata_jsons")?; + let version_hashes: Bound<'_, PyAny> = + native_field(&inputs.identities, "model_version_hashes")?; + let metadata_jsons: Bound<'_, PyAny> = + native_field(&inputs.identities, "model_metadata_jsons")?; let pass = ModelPass { snapshot: inputs.snapshot.clone(), query_change_tracking: true, @@ -88,7 +91,7 @@ fn changed_model_names(inputs: &StalenessInputs<'_>) -> PyResult function_local_hashes: py.None().into_bound(py), hook_version_hashes: py.None().into_bound(py), }; - let models_by_name: Bound<'_, PyAny> = inputs.original_scope.getattr("models_by_name")?; + let models_by_name: Bound<'_, PyAny> = native_field(&inputs.original_scope, "models_by_name")?; for item in models_by_name.call_method0("items")?.try_iter()? { let (name, model): (String, Bound<'_, PyAny>) = item?.extract()?; let selected_change: Bound<'_, PyAny> = models.call_method1("get", (&name,))?; @@ -119,7 +122,8 @@ fn changed_model_names(inputs: &StalenessInputs<'_>) -> PyResult } fn changed_seed_names(inputs: &StalenessInputs<'_>) -> PyResult> { - let recorded: Bound<'_, PyAny> = inputs.snapshot.getattr("fingerprints")?.getattr("seeds")?; + let recorded: Bound<'_, PyAny> = + native_field(&inputs.snapshot, "fingerprints")?.getattr("seeds")?; let mut changed: HashSet = HashSet::new(); for item in inputs .identities @@ -132,7 +136,7 @@ fn changed_seed_names(inputs: &StalenessInputs<'_>) -> PyResult> continue; } let fingerprint: Bound<'_, PyAny> = recorded.call_method1("get", (&name,))?; - if fingerprint.is_none() || !fingerprint.getattr("version_hash")?.eq(&expected)? { + if fingerprint.is_none() || !native_field(&fingerprint, "version_hash")?.eq(&expected)? { changed.insert(name); } } @@ -205,14 +209,14 @@ pub(crate) fn build_stale_out_of_selection_warnings<'py>( let source_freshness: Bound<'py, PyAny> = arguments.value("source_freshness")?; let mut changed_sources: HashSet = HashSet::new(); if include_sources && !source_freshness.is_none() { - for identity in source_freshness.getattr("changed_identities")?.try_iter()? { - changed_sources.insert(identity?.getattr("source_name")?.extract()?); + for identity in native_field(&source_freshness, "changed_identities")?.try_iter()? { + changed_sources.insert(native_field(&identity?, "source_name")?.extract()?); } } let graph = StalenessGraph { - upstream_deps: upstream_graph(&inputs.original_scope.getattr("upstream_deps")?)?, + upstream_deps: upstream_graph(&native_field(&inputs.original_scope, "upstream_deps")?)?, selected_model_names: names_of_kind( - &inputs.original_scope.getattr("selected_keys")?, + &native_field(&inputs.original_scope, "selected_keys")?, MODEL_RESOURCE, )? .into_iter() @@ -242,19 +246,19 @@ fn classify_selection_staleness_warnings<'py>( ) -> PyResult> { let py: Python<'py> = graph.py(); let staleness = StalenessGraph { - upstream_deps: upstream_graph(&graph.getattr("upstream_deps")?)?, - selected_model_names: name_set(&graph.getattr("selected_model_names")?)? + upstream_deps: upstream_graph(&native_field(graph, "upstream_deps")?)?, + selected_model_names: name_set(&native_field(graph, "selected_model_names")?)? .into_iter() .collect(), run: StalenessNames { - models: name_set(&graph.getattr("run_model_names")?)?, - seeds: name_set(&graph.getattr("run_seed_names")?)?, - sources: name_set(&graph.getattr("run_source_names")?)?, + models: name_set(&native_field(graph, "run_model_names")?)?, + seeds: name_set(&native_field(graph, "run_seed_names")?)?, + sources: name_set(&native_field(graph, "run_source_names")?)?, }, changed: StalenessNames { - models: name_set(&graph.getattr("changed_model_names")?)?, - seeds: name_set(&graph.getattr("changed_seed_names")?)?, - sources: name_set(&graph.getattr("changed_source_names")?)?, + models: name_set(&native_field(graph, "changed_model_names")?)?, + seeds: name_set(&native_field(graph, "changed_seed_names")?)?, + sources: name_set(&native_field(graph, "changed_source_names")?)?, }, }; let warning_class: Bound<'py, PyAny> = 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 4299bea43d..c439eae2a3 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 @@ -8,6 +8,7 @@ use crate::bindings::_helpers::planner_migrations::models::{ ColumnCandidate, ColumnMigrationInputs, MigrationPhase, }; use crate::bindings::_helpers::planner_migrations::projections::parse_query_shape; +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; use pyo3::types::{PyDict, PyDictMethods, PyFrozenSet, PyTuple}; @@ -19,16 +20,16 @@ pub(super) fn candidates<'py>( let snapshot = phase.snapshot.clone(); let py = scope.py(); let mut result: Vec> = Vec::new(); - for key in scope.getattr("execution_order")?.try_iter()? { + for key in native_field(&scope, "execution_order")?.try_iter()? { let key = key?; - if !scope.getattr("selected_keys")?.contains(&key)? - || !key.getattr("resource_type")?.eq("model")? + if !native_field(&scope, "selected_keys")?.contains(&key)? + || !native_field(&key, "resource_type")?.eq("model")? { continue; } let model = scope .getattr("models_by_name")? - .call_method1("get", (key.getattr("name")?,))?; + .call_method1("get", (native_field(&key, "name")?,))?; if model.is_none() { continue; } @@ -38,7 +39,7 @@ pub(super) fn candidates<'py>( } let name = model_reads::name_object(&model)?; if columns.full_refresh_model_names.contains(&name)? - || !snapshot.getattr("existing_relations")?.contains(&name)? + || !native_field(&snapshot, "existing_relations")?.contains(&name)? { continue; } @@ -46,13 +47,16 @@ pub(super) fn candidates<'py>( .getattr("existing_columns")? .call_method1("get", (&name, PyTuple::empty(py)))?; let declared = PyDict::new(py); - let schema = model.getattr("schema_entry")?; + let schema = native_field(&model, "schema_entry")?; if !schema.is_none() { - for column in schema.getattr("columns")?.try_iter()? { + for column in native_field(&schema, "columns")?.try_iter()? { let column = column?; - let origin = column.getattr("migrate_from")?; + let origin = native_field(&column, "migrate_from")?; if !origin.is_none() { - declared.set_item(column.getattr("name")?, origin.call_method0("strip")?)?; + declared.set_item( + native_field(&column, "name")?, + origin.call_method0("strip")?, + )?; } } } @@ -70,12 +74,12 @@ pub(super) fn candidates<'py>( let by_name = PyDict::new(py); for column in warehouse.try_iter()? { let column = column?; - by_name.set_item(lower(&column.getattr("name")?)?, column)?; + by_name.set_item(lower(&native_field(&column, "name")?)?, column)?; } let previous = if changed { parse_query_shape( py, - &fingerprint.getattr("definition")?.extract::()?, + &native_field(&fingerprint, "definition")?.extract::()?, dialect.extract::>()?.as_deref(), )? } else { 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 128f2c6273..5385aa4854 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,6 +14,7 @@ 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::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; @@ -58,8 +59,8 @@ pub(super) fn decide<'py>( } let mut outputs: Vec = Vec::new(); if !candidate.current.is_none() { - for projection in candidate.current.getattr("projections")?.try_iter()? { - outputs.push(projection?.getattr("key")?.extract::()?); + for projection in native_field(&candidate.current, "projections")?.try_iter()? { + outputs.push(native_field(&projection?, "key")?.extract::()?); } } else { for (name, _, _) in inferred_rows(&candidate.model)?.unwrap_or_default() { @@ -83,7 +84,7 @@ pub(super) fn decide<'py>( )?; let recorded = !newest.is_none() && call_with( - &newest.getattr("renamed")?, + &native_field(&newest, "renamed")?, &[ ("origin_column", origin_name.clone()), ("destination_column", destination.clone()), @@ -97,7 +98,7 @@ pub(super) fn decide<'py>( ); let decision = registry::class(py, RecordId::ColumnMigrationDecision)?.call1((decision_name,))?; - if discovery == AUTOMATIC && decision.getattr("blocks_build")?.is_truthy()? { + if discovery == AUTOMATIC && native_field(&decision, "blocks_build")?.is_truthy()? { continue; } let done = decision_name == DONE && !newest.is_none(); @@ -110,7 +111,7 @@ pub(super) fn decide<'py>( ( "origin_column", if let Some(origin) = origin { - origin.getattr("name")? + native_field(&origin, "name")? } else { origin_name }, @@ -124,7 +125,7 @@ pub(super) fn decide<'py>( ( "target_name", if done { - newest.getattr("target_name")? + native_field(&newest, "target_name")? } else { runtime .getattr("project")? @@ -134,7 +135,7 @@ pub(super) fn decide<'py>( ( "completed_at", if done { - newest.getattr("created_at")? + native_field(&newest, "created_at")? } else { py.None().into_bound(py) }, @@ -154,7 +155,9 @@ pub(super) fn checked<'py>( let py = runtime.py(); let mut pending = false; for entry in &entries { - pending |= entry.getattr("decision")?.getattr("renames")?.is_truthy()?; + pending |= native_field(entry, "decision")? + .getattr("renames")? + .is_truthy()?; } if !pending { return Ok(CheckedColumns { @@ -163,13 +166,13 @@ pub(super) fn checked<'py>( }); } let destination = model_reads::destination(&candidate.model)?; - let qualified = destination.getattr("qualified_name")?; + let qualified = native_field(&destination, "qualified_name")?; let relation = if physical.is_truthy()? { physical.clone() } else if qualified.is_truthy()? { qualified } else { - destination.getattr("name")? + native_field(&destination, "name")? }; let reason = call_with( &runtime @@ -192,9 +195,12 @@ pub(super) fn checked<'py>( let mut checked: Vec> = Vec::new(); let mut warnings: Vec> = Vec::new(); for entry in entries { - if !entry.getattr("decision")?.getattr("renames")?.is_truthy()? { + if !native_field(&entry, "decision")? + .getattr("renames")? + .is_truthy()? + { checked.push(entry); - } else if entry.getattr("discovery")?.eq("manual")? { + } else if native_field(&entry, "discovery")?.eq("manual")? { checked.push(replace( &entry, &[ @@ -230,8 +236,11 @@ pub(super) fn hints<'py>( let excluded = declared_names(candidate)?.call_method0("copy")?; let claimed = PyDict::new(py); for entry in entries { - claimed.set_item(lower(&entry.getattr("origin_column")?)?, py.None())?; - claimed.set_item(lower(&entry.getattr("destination_column")?)?, py.None())?; + claimed.set_item(lower(&native_field(entry, "origin_column")?)?, py.None())?; + claimed.set_item( + lower(&native_field(entry, "destination_column")?)?, + py.None(), + )?; } object( py, 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 333e6dc940..6bc3a383e4 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 @@ -5,6 +5,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::{call_with, importe 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{NativeItem, RecordId}; use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; @@ -47,7 +48,7 @@ pub(super) fn native<'py>( pub(super) fn config<'py>(model: &Bound<'py, PyAny>, key: &str) -> PyResult> { Ok( - authored_config_str(&model.getattr("config")?.getattr("values")?, key)? + authored_config_str(&native_field(model, "config")?.getattr("values")?, key)? .unwrap_or_else(|| model.py().None().into_bound(model.py())), ) } 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 936d5c1fa8..b05e2eaf77 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 @@ -12,6 +12,7 @@ use crate::bindings::_helpers::planner_migrations::models::{ }; use crate::bindings::_helpers::planner_migrations::projections::renames_explain_change; use crate::bindings::_helpers::planner_phases::objects::object; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; use pyo3::types::{PyDict, PyDictMethods, PyTuple}; @@ -27,28 +28,32 @@ pub(super) fn overlay<'py>( } let py = snapshot.py(); let columns = - imported(py, "builtins", "dict")?.call1((snapshot.getattr("existing_columns")?,))?; - let stored = snapshot.getattr("fingerprints")?; - let fingerprints = imported(py, "builtins", "dict")?.call1((stored.getattr("models")?,))?; + imported(py, "builtins", "dict")?.call1((native_field(snapshot, "existing_columns")?,))?; + let stored = native_field(snapshot, "fingerprints")?; + let fingerprints = + imported(py, "builtins", "dict")?.call1((native_field(&stored, "models")?,))?; for candidate in candidates { let name = model_reads::name_object(&candidate.model)?; let renames = PyDict::new(py); let pending = PyDict::new(py); let mut blocked = false; for entry in entries { - if !entry.getattr("model_name")?.eq(&name)? { + if !native_field(entry, "model_name")?.eq(&name)? { continue; } - blocked |= entry.getattr("blocks_build")?.is_truthy()? - || entry.getattr("origin_missing")?.is_truthy()?; + blocked |= native_field(entry, "blocks_build")?.is_truthy()? + || native_field(entry, "origin_missing")?.is_truthy()?; renames.set_item( - entry.getattr("origin_column")?, - entry.getattr("destination_column")?, + native_field(entry, "origin_column")?, + native_field(entry, "destination_column")?, )?; - if entry.getattr("decision")?.getattr("renames")?.is_truthy()? { + if native_field(entry, "decision")? + .getattr("renames")? + .is_truthy()? + { pending.set_item( - lower(&entry.getattr("origin_column")?)?, - entry.getattr("destination_column")?, + lower(&native_field(entry, "origin_column")?)?, + native_field(entry, "destination_column")?, )?; } } @@ -62,7 +67,7 @@ pub(super) fn overlay<'py>( .try_iter()? { let column = column?; - let old = column.getattr("name")?; + let old = native_field(&column, "name")?; let new = pending.get_item(lower(&old)?)?.unwrap_or(old); renamed.push(replace(&column, &[("name", new)])?); } @@ -113,9 +118,9 @@ pub(super) fn overlay<'py>( RecordId::WarehouseFingerprints, &[ ("models", fingerprints), - ("functions", stored.getattr("functions")?), - ("seeds", stored.getattr("seeds")?), - ("python_nodes", stored.getattr("python_nodes")?), + ("functions", native_field(&stored, "functions")?), + ("seeds", native_field(&stored, "seeds")?), + ("python_nodes", native_field(&stored, "python_nodes")?), ], )?, ), @@ -130,21 +135,23 @@ pub(super) fn cursors<'py>( ) -> PyResult> { let py = snapshot.py(); let scope = phase.scope.clone(); - let models = scope.getattr("models_by_name")?; + let models = native_field(&scope, "models_by_name")?; let origins = PyDict::new(py); for entry in entries { - let name = entry.getattr("model_name")?; + let name = native_field(entry, "model_name")?; let model = models.call_method1("get", (&name,))?; let cursor = if model.is_truthy()? { config(&model, "cursor")? } else { py.None().into_bound(py) }; - if entry.getattr("decision")?.getattr("renames")?.is_truthy()? + if native_field(entry, "decision")? + .getattr("renames")? + .is_truthy()? && !cursor.is_none() - && lower(&cursor)? == lower(&entry.getattr("destination_column")?)? + && lower(&cursor)? == lower(&native_field(entry, "destination_column")?)? { - origins.set_item(name, entry.getattr("origin_column")?)?; + origins.set_item(name, native_field(entry, "origin_column")?)?; } } if origins.is_empty() { @@ -170,11 +177,14 @@ pub(super) fn cursors<'py>( } let redirected = super::planning_boundary::redirected_cursor_snapshots( phase, - (snapshot.getattr("existing_relations")?, targets.into_any()), + ( + native_field(&snapshot, "existing_relations")?, + targets.into_any(), + ), Some(origins.into_any()), )?; let cursors = - imported(py, "builtins", "dict")?.call1((snapshot.getattr("cursor_snapshots")?,))?; + imported(py, "builtins", "dict")?.call1((native_field(&snapshot, "cursor_snapshots")?,))?; cursors.call_method1("update", (redirected,))?; replace(&snapshot, &[("cursor_snapshots", cursors)]) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/column_planning.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/column_planning.rs index 66423a9c63..4b13d175f8 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/column_planning.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/column_planning.rs @@ -8,6 +8,7 @@ use crate::bindings::_helpers::planner_migrations::models::{ ColumnMigrationInputs, ColumnMigrationPlanning, MigrationPhase, }; use crate::bindings::_helpers::planner_phases::runtime::runtime_field; +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; use pyo3::types::{PyDict, PySet, PySetMethods}; use pyo3::{pyfunction, wrap_pyfunction}; @@ -78,19 +79,19 @@ pub(crate) fn plan_column_migrations<'py>( let state = super::state_inspection::inspection(&runtime, None)?; let schemas = PySet::empty(py)?; for candidate in &candidates { - let schema = model_reads::destination(&candidate.model)?.getattr("schema")?; + let schema = native_field(&model_reads::destination(&candidate.model)?, "schema")?; if !schema.is_none() { schemas.add(schema)?; } } call_with( - &state.getattr("inspect_column_events")?, + &native_field(&state, "inspect_column_events")?, &[("schemas", schemas.into_any())], )?; let inputs = Bound::new( py, KnownInputColumns::new( - &snapshot.getattr("existing_columns")?, + &native_field(&snapshot, "existing_columns")?, &columns.source_columns, ), )? @@ -102,7 +103,7 @@ pub(crate) fn plan_column_migrations<'py>( let decided = super::column_entries::decide( &runtime, candidate, - &state.getattr("column_events")?, + &native_field(&state, "column_events")?, &inputs, )?; let physical = columns 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 15f6de2d0d..18abe9e14e 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,6 +6,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; use pyo3::exceptions::PyKeyError; @@ -24,7 +25,7 @@ pub(super) fn warning<'py>( py, RecordId::PlanWarning, &[ - ("model_name", entry.getattr("model_name")?), + ("model_name", native_field(entry, "model_name")?), ( "severity", registry::class(py, RecordId::WarningSeverity)?.getattr(if error { @@ -40,36 +41,36 @@ pub(super) fn warning<'py>( } pub(super) fn entry_warning<'py>(entry: &Bound<'py, PyAny>) -> PyResult>> { - let destination = entry.getattr("destination")?; - let qualified = destination.getattr("qualified_name")?; + let destination = native_field(entry, "destination")?; + let qualified = native_field(&destination, "qualified_name")?; let relation: String = if qualified.is_truthy()? { qualified } else { - destination.getattr("name")? + native_field(&destination, "name")? } .extract()?; - let origin: String = entry.getattr("origin_column")?.extract()?; - let column: String = entry.getattr("destination_column")?.extract()?; - let name: String = entry.getattr("model_name")?.extract()?; - let decision: String = entry.getattr("decision")?.extract()?; - if decision == DONE && !entry.getattr("discovery")?.eq("manual")? { + let origin: String = native_field(entry, "origin_column")?.extract()?; + let column: String = native_field(entry, "destination_column")?.extract()?; + let name: String = native_field(entry, "model_name")?.extract()?; + let decision: String = native_field(entry, "decision")?.extract()?; + if decision == DONE && !native_field(entry, "discovery")?.eq("manual")? { return Ok(None); } - let completed = entry.getattr("completed_at")?; + let completed = native_field(entry, "completed_at")?; let completed: String = if completed.is_truthy()? { completed.call_method0("isoformat")?.extract()? } else { "unknown time".into() }; - let target = entry.getattr("target_name")?; + let target = native_field(entry, "target_name")?; let target: String = if target.is_truthy()? { target.extract()? } else { "default".into() }; let outcome: String = - missing_origin_outcome(&entry.getattr("missing_origin_policy")?)?.to_owned(); - let message: String = entry.getattr("message")?.str()?.extract()?; + missing_origin_outcome(&native_field(entry, "missing_origin_policy")?)?.to_owned(); + let message: String = native_field(entry, "message")?.str()?.extract()?; let Some((code, message)) = column_warning( &decision, &ColumnWarning { @@ -89,7 +90,7 @@ pub(super) fn entry_warning<'py>(entry: &Bound<'py, PyAny>) -> PyResult( entry: &Bound<'py, PyAny>, reason: &str, ) -> PyResult> { - let origin: String = entry.getattr("origin_column")?.extract()?; - let destination: String = entry.getattr("destination_column")?.extract()?; + let origin: String = native_field(entry, "origin_column")?.extract()?; + let destination: String = native_field(entry, "destination_column")?.extract()?; let facts = ColumnWarning { model_name: "", relation: "", 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 479e79c652..292a8a600d 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/columns.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/columns.rs @@ -12,18 +12,19 @@ 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::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(); - for item in shape.getattr("projections")?.try_iter()? { + for item in native_field(shape, "projections")?.try_iter()? { let item = item?; result.push(Projection { - name: item.getattr("name")?.extract()?, - key: item.getattr("key")?.extract()?, - expression: item.getattr("expression")?.extract()?, - references: item.getattr("references")?.extract()?, + name: native_field(&item, "name")?.extract()?, + key: native_field(&item, "key")?.extract()?, + expression: native_field(&item, "expression")?.extract()?, + references: native_field(&item, "references")?.extract()?, }); } Ok(result) @@ -114,7 +115,7 @@ pub(crate) fn rename_hints<'py>( (&projections(&previous)?, &projections(¤t)?), &arguments.value("excluded")?.extract()?, &arguments.value("live_columns")?.extract()?, - previous.getattr("body")?.eq(current.getattr("body")?)?, + native_field(&previous, "body")?.eq(native_field(¤t, "body")?)?, ); let mut result: Vec> = Vec::new(); for hint in hints { 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 4a1d4e2c3b..6ac4996040 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/compatibility.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/compatibility.rs @@ -8,6 +8,7 @@ use crate::bindings::_helpers::planner_changes::detect::detect_model_changes; use crate::bindings::_helpers::planner_migrations::models::{ CompatibilityCheck, MigrationCompatibilityResult, }; +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; @@ -36,7 +37,7 @@ fn blocking<'py>( let policy = if let Some(snapshots_config) = snapshots_config { config(model, "snapshot_schema_change")? .filter(|value| !value.is_empty()) - .unwrap_or(snapshots_config.getattr("schema_change")?.extract()?) + .unwrap_or(native_field(snapshots_config, "schema_change")?.extract()?) } else { config(model, "on_schema_change")? .filter(|value| !value.is_empty()) @@ -55,10 +56,10 @@ fn blocking<'py>( for finding in findings_in.try_iter()? { let finding = finding?; findings.push(CompatibilityFinding { - kind: finding.getattr("kind")?.extract()?, - column_name: finding.getattr("column_name")?.extract()?, - expected_type: finding.getattr("expected_type")?.extract()?, - actual_type: finding.getattr("actual_type")?.extract()?, + kind: native_field(&finding, "kind")?.extract()?, + column_name: native_field(&finding, "column_name")?.extract()?, + expected_type: native_field(&finding, "expected_type")?.extract()?, + actual_type: native_field(&finding, "actual_type")?.extract()?, }); } let dialect: Option = dialect.extract()?; @@ -118,7 +119,7 @@ pub(super) fn check_migration_compatibility<'py>( let py = check.model.py(); let model = check.model.clone(); let relation = check.origin_relation.clone(); - let relation_type: String = relation.getattr("relation_type")?.extract()?; + let relation_type: String = native_field(&relation, "relation_type")?.extract()?; if relation_type.to_uppercase().contains("VIEW") { return result( py, @@ -163,11 +164,11 @@ pub(super) fn check_migration_compatibility<'py>( ], )?), )?; - let schema = model.getattr("schema_entry")?; + let schema = native_field(&model, "schema_entry")?; let mut has_columns = inferred_rows(&model)?.is_some_and(|rows| !rows.is_empty()); if !schema.is_none() { - for column in schema.getattr("columns")?.try_iter()? { - has_columns |= !column?.getattr("type")?.is_none(); + for column in native_field(&schema, "columns")?.try_iter()? { + has_columns |= !native_field(&column?, "type")?.is_none(); } } if !has_columns { @@ -176,7 +177,7 @@ pub(super) fn check_migration_compatibility<'py>( let snapshot = config(&model, "materialized")?.as_deref() == Some("snapshot"); let snapshots_config = check.snapshots_config.clone(); let findings = blocking( - (&model, &change.getattr("schema_findings")?), + (&model, &native_field(&change, "schema_findings")?), &check.column_dialect, snapshot.then_some(&snapshots_config), )?; 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 923c24df27..5c70a6033a 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/discovery.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/discovery.rs @@ -10,6 +10,7 @@ use crate::bindings::_helpers::planner_migrations::models::{ use crate::bindings::_helpers::planner_migrations::{ discovery_events, discovery_models, discovery_requests, }; +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::prelude::{PyAnyMethods, PyResult}; use pyo3::types::{PyDict, PyDictMethods, PyListMethods, PyTuple, PyTupleMethods}; use std::collections::BTreeSet; @@ -51,13 +52,13 @@ pub(super) fn discover_model_migrations<'py>( fingerprints: cache, }; call_with( - &state.getattr("inspect_schemas")?, + &native_field(&state, "inspect_schemas")?, &[("schemas", project_schemas)], )?; let declared = discovery_events::declared(&declarations, &state)?; let names = PyDict::new(py); for item in &models { - names.set_item(item.getattr("name")?, item)?; + names.set_item(native_field(item, "name")?, item)?; } let event_renames = discovery_events::renames(&runtime, &names, &state)?; for (name, origin) in declared.iter() { @@ -75,10 +76,10 @@ pub(super) fn discover_model_migrations<'py>( } let unbuilt = PyDict::new(py); for item in &models { - let name = item.getattr("name")?; + let name = native_field(item, "name")?; if !declared_names.contains(&name)? && call_with( - &state.getattr("named_fingerprint")?, + &native_field(&state, "named_fingerprint")?, &[("model_name", name.clone())], )? .is_none() 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 5b1e8635ce..6df7bfbf46 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 @@ -5,6 +5,7 @@ use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::planner_migrations::column_objects::config; use crate::bindings::_helpers::planner_migrations::models::ModelMigrationDeclaration; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::state_rows::migration_projections::newest_move; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; use pyo3::types::{PyDict, PyDictMethods, PyList, PyListMethods, PyTuple}; @@ -39,7 +40,7 @@ pub(super) fn declared<'py>( let result = PyDict::new(state.py()); for item in declarations { let fingerprint = call_with( - &state.getattr("model_fingerprint")?, + &native_field(state, "model_fingerprint")?, &[ ("location", item.origin_location.clone()), ("model_name", item.origin_model.clone()), @@ -49,7 +50,7 @@ pub(super) fn declared<'py>( let origin = if explicit.is_truthy()? { explicit } else if !fingerprint.is_none() { - fingerprint.getattr("node_name")? + native_field(&fingerprint, "node_name")? } else { state.py().None().into_bound(state.py()) }; @@ -76,8 +77,8 @@ pub(super) fn renames<'py>( .collect::>()?; for (index, event) in events.iter().enumerate() { ordered.append(( - event.getattr("created_at")?, - event.getattr("event_id")?, + native_field(event, "created_at")?, + native_field(event, "event_id")?, index, ))?; } @@ -86,14 +87,14 @@ pub(super) fn renames<'py>( for item in ordered.iter() { let index: usize = item.get_item(2)?.extract()?; let event = &events[index]; - let event_target = event.getattr("target_name")?; + let event_target = native_field(event, "target_name")?; if event_target.is_none() || event_target.eq(&target)? { - latest.set_item(event.getattr("destination_model")?, event)?; + latest.set_item(native_field(event, "destination_model")?, event)?; } } let result = PyDict::new(py); for (name, event) in latest.iter() { - let origin = event.getattr("origin_model")?; + let origin = native_field(&event, "origin_model")?; if names.contains(&name)? && !origin.is_none() && !names.contains(&origin)? { result.set_item(name, origin)?; } @@ -111,24 +112,24 @@ pub(super) fn resumed<'py>( let target = runtime .getattr("project")? .getattr("effective_target_name")?; - let events = state.getattr("events")?; + let events = native_field(state, "events")?; for item in models { - let name = item.getattr("name")?; + let name = native_field(item, "name")?; if !unbuilt.contains(&name)? || !(history(item)? || identity(item)?) { continue; } - let destination = item.getattr("destination")?; + let destination = native_field(item, "destination")?; let event = newest(&events, &destination)?; if event.is_none() { continue; } - let event_target = event.getattr("target_name")?; + let event_target = native_field(&event, "target_name")?; if event .getattr("destination")? .call_method1("matches", (relation(&destination)?,))? .is_truthy()? && (event_target.is_none() || event_target.eq(&target)?) - && event.getattr("decision")?.eq("renamed")? == identity(item)? + && native_field(&event, "decision")?.eq("renamed")? == identity(item)? { result.set_item(name, event)?; } @@ -137,7 +138,7 @@ pub(super) fn resumed<'py>( } pub(super) fn key<'py>(location: &Bound<'py, PyAny>) -> PyResult> { - let schema = location.getattr("schema")?; + let schema = native_field(location, "schema")?; let schema = if schema.is_truthy()? { schema } else { @@ -147,7 +148,7 @@ pub(super) fn key<'py>(location: &Bound<'py, PyAny>) -> PyResult(runtime: &Bound<'py, PyAny>) -> PyResult = Vec::new(); - for dependency in item.getattr("deps")?.try_iter()? { + for dependency in native_field(&item, "deps")?.try_iter()? { let dependency = dependency?; - if dependency.getattr("resource_type")?.eq("model")? { - upstream.push(dependency.getattr("name")?.extract()?); + if native_field(&dependency, "resource_type")?.eq("model")? { + upstream.push(native_field(&dependency, "name")?.extract()?); } } dependencies.insert(name.clone(), upstream); @@ -58,10 +59,10 @@ pub(super) fn triggered( .collect::>>>() })?; let relevant = relevant.0; - let fingerprints = snapshot.getattr("fingerprints")?.getattr("models")?; + let fingerprints = native_field(snapshot, "fingerprints")?.getattr("models")?; for item in models { - let name = item.getattr("name")?; - if relevant.contains(item.getattr("key")?)? + let name = native_field(item, "name")?; + if relevant.contains(native_field(item, "key")?)? && !fingerprints.contains(&name)? && !declared.contains(&name)? { @@ -80,7 +81,7 @@ pub(super) fn metadata<'py>( let functions = object( py, build_function_local_hashes( - &project.getattr("functions")?, + &native_field(&project, "functions")?, project .getattr("sql_analysis_dialect")? .extract::>()? @@ -88,19 +89,19 @@ pub(super) fn metadata<'py>( )?, )?; let identities = host::entry(py, UserCodeHost::BuildHookIdentities)? - .call1((project.getattr("hook_functions")?,))?; + .call1((native_field(&project, "hook_functions")?,))?; let hooks = PyDict::new(py); for pair in identities.call_method0("items")?.try_iter()? { let pair = pair?; hooks.set_item( pair.get_item(0)?, - pair.get_item(1)?.getattr("version_hash")?, + native_field(&pair.get_item(1)?, "version_hash")?, )?; } let result = PyDict::new(py); for item in models { result.set_item( - item.getattr("name")?, + native_field(item, "name")?, object( py, build_model_version_identity_metadata_json( 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 9f6e729988..cdccb3cc10 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,6 +9,7 @@ use crate::bindings::_helpers::planner_migrations::discovery_events::{ use crate::bindings::_helpers::planner_migrations::models::{ ModelMigrationDeclaration, ModelMigrationRequest, }; +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}; @@ -26,20 +27,20 @@ fn location<'py>( ) -> PyResult> { let fields = if event { [ - ("database", item.getattr("database")?), - ("schema", item.getattr("schema")?), - ("name", item.getattr("name")?), + ("database", native_field(item, "database")?), + ("schema", native_field(item, "schema")?), + ("name", native_field(item, "name")?), ] } else { - let target = item.getattr("target_name")?; + let target = native_field(item, "target_name")?; let name = if target.is_truthy()? { target } else { - item.getattr("node_name")? + native_field(item, "node_name")? }; [ - ("database", item.getattr("target_database")?), - ("schema", item.getattr("target_schema")?), + ("database", native_field(item, "target_database")?), + ("schema", native_field(item, "target_schema")?), ("name", name), ] }; @@ -58,10 +59,10 @@ pub(super) fn candidates<'py>( excluded.add(key(&declaration.origin_location)?)?; } for (_, event) in claims.1.iter() { - excluded.add(key(&event.getattr("origin")?)?)?; + excluded.add(key(&native_field(&event, "origin")?)?)?; } let orphans = call_with( - &state.getattr("orphan_fingerprints")?, + &native_field(state, "orphan_fingerprints")?, &[( "project_model_names", PyFrozenSet::new(py, names.keys().iter())?.into_any(), @@ -71,16 +72,16 @@ pub(super) fn candidates<'py>( let mut locations: Vec> = Vec::new(); for item in orphans.try_iter()? { let item = item?; - let metadata: String = item.getattr("metadata_json")?.extract()?; + let metadata: String = native_field(&item, "metadata_json")?.extract()?; if stored_migration_fingerprint(&metadata).is_some() - && !item.getattr("target_name")?.is_none() + && !native_field(&item, "target_name")?.is_none() { locations.push(location(runtime, &item, false)?); fingerprints.push(item); } } call_with( - &state.getattr("inspect_relations")?, + &native_field(state, "inspect_relations")?, &[("locations", PyTuple::new(py, &locations)?.into_any())], )?; let mut result: Vec> = Vec::new(); @@ -90,7 +91,7 @@ pub(super) fn candidates<'py>( { continue; } - let event = newest(&state.getattr("events")?, &location)?; + let event = newest(&native_field(state, "events")?, &location)?; if !event.is_none() && event .getattr("origin")? @@ -115,25 +116,25 @@ pub(super) fn automatic<'py>( for item in models { if !scope .getattr("selected_keys")? - .contains(item.getattr("key")?)? + .contains(native_field(item, "key")?)? { continue; } - let name = item.getattr("name")?; + let name = native_field(item, "name")?; let event = matches.1.get_item(&name)?; let candidate = matches.0.call_method1("get", (&name,))?; let origin = if let Some(event) = event { Some(( - location(runtime, &event.getattr("origin")?, true)?, - event.getattr("origin_model")?, + location(runtime, &native_field(&event, "origin")?, true)?, + native_field(&event, "origin_model")?, )) } else if !candidate.is_none() { let materialized: Option = config(item, "materialized")?.extract()?; - let metadata: String = candidate.getattr("metadata_json")?.extract()?; + let metadata: String = native_field(&candidate, "metadata_json")?.extract()?; if discovery_materialization(materialized.as_deref().unwrap_or_default(), &metadata) { Some(( location(runtime, &candidate, false)?, - candidate.getattr("node_name")?, + native_field(&candidate, "node_name")?, )) } else { None @@ -167,9 +168,9 @@ pub(super) fn warnings<'py>( for item in models { if scope .getattr("selected_keys")? - .contains(item.getattr("key")?)? + .contains(native_field(item, "key")?)? { - selected.add(item.getattr("name")?)?; + selected.add(native_field(item, "name")?)?; } } let ambiguous: BTreeMap = ambiguous.extract()?; 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 44eaaf8b95..79ffcd78a0 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/fingerprint.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/fingerprint.rs @@ -2,6 +2,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 pyo3::exceptions::PyValueError; use pyo3::prelude::{Bound, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; use pyo3::types::{PyDict, PyDictMethods, PyFrozenSet, PyTuple}; @@ -93,14 +94,14 @@ pub(crate) fn with_migration_fingerprints<'py>( let entry = entry?; let model = arguments .value("models_by_name")? - .call_method1("get", (entry.getattr("name")?,))?; - let metadata = entry.getattr("fingerprint_metadata_json")?; - if model.is_none() || metadata.is_none() || entry.getattr("action")?.eq("skip")? { + .call_method1("get", (native_field(&entry, "name")?,))?; + let metadata = native_field(&entry, "fingerprint_metadata_json")?; + if model.is_none() || metadata.is_none() || native_field(&entry, "action")?.eq("skip")? { attached.push(entry); continue; } let fingerprint = call_with( - &cache.getattr("fingerprint")?, + &native_field(&cache, "fingerprint")?, &[ ("query_sql", model_reads::query_sql(&model)?), ("metadata_json", metadata), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/known_input_columns.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/known_input_columns.rs index 358b2d003c..f322fdf91e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/known_input_columns.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/known_input_columns.rs @@ -1,5 +1,6 @@ //! Physical columns of the relations a model reads, as inspected by the planner. +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::prelude::{Bound, Py, PyAny, PyAnyMethods, PyResult, Python}; use pyo3::types::PyFrozenSet; use pyo3::{pyclass, pymethods}; @@ -43,7 +44,11 @@ impl KnownInputColumns { } let mut names: Vec = Vec::new(); for column in columns.try_iter()? { - names.push(column?.getattr("name")?.extract::()?.to_lowercase()); + names.push( + native_field(&column?, "name")? + .extract::()? + .to_lowercase(), + ); } PyFrozenSet::new(py, names).map(Some) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/matching.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/matching.rs index 592daf36bb..fb33fd20c4 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/matching.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/matching.rs @@ -5,6 +5,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::call_with; use crate::bindings::_helpers::planner_migrations::models::{ FingerprintInputs, ModelMigrationRequest, }; +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::IntoPyObject; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; use pyo3::types::{PyDict, PyDictMethods, PyTuple}; @@ -19,7 +20,7 @@ fn variants<'py>( ) -> PyResult> { let name = model_reads::name_object(model)?; let metadata = inputs.metadata_jsons.get_item(name)?; - let cache = inputs.fingerprints.getattr("fingerprint")?; + let cache = native_field(&inputs.fingerprints, "fingerprint")?; let mut hashes: Vec = Vec::new(); for map in maps.try_iter()? { let hash: Option = call_with( @@ -62,7 +63,7 @@ pub(super) fn match_migration_fingerprints<'py>( let mut fingerprints: Vec = Vec::new(); for candidate in &original { fingerprints.push(MigrationCandidate { - name: candidate.getattr("node_name")?.extract()?, + name: native_field(candidate, "node_name")?.extract()?, fingerprint: stored_migration_fingerprint(candidate)?.unwrap_or_default(), }); } @@ -111,6 +112,6 @@ pub(super) fn migration_destination_fingerprints<'py>( pub(super) fn stored_migration_fingerprint( fingerprint: &Bound<'_, PyAny>, ) -> PyResult> { - let metadata: String = fingerprint.getattr("metadata_json")?.extract()?; + let metadata: String = native_field(fingerprint, "metadata_json")?.extract()?; Ok(sqlbuild_analysis::planner_migrations::main::stored_migration_fingerprint::stored_migration_fingerprint(&metadata)) } 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 aa28283afe..1837c63193 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::state_rows::migration_projections::newest_move; @@ -23,14 +24,14 @@ pub(super) fn relation<'py>(location: &Bound<'py, PyAny>) -> PyResult) -> PyResult<(String, String)> { - let schema = location.getattr("schema")?; + let schema = native_field(location, "schema")?; Ok(( if schema.is_truthy()? { lower(&schema)? } else { String::new() }, - lower(&location.getattr("name")?)?, + lower(&native_field(location, "name")?)?, )) } @@ -70,23 +71,23 @@ pub(super) fn decide<'py>( let py = runtime.py(); let request = decision_request.request; let item = request.model.clone(); - let destination = item.getattr("destination")?; + let destination = native_field(&item, "destination")?; let origin = decision_request.origin.clone(); let snapshot = decision_request.snapshot.clone(); let state = decision_request.state.clone(); let origin_model = decision_request.origin_model.clone(); if key(&origin)? == key(&destination)? { - let name: String = item.getattr("name")?.extract()?; + let name: String = native_field(&item, "name")?.extract()?; return Err(pyo3::PyErr::from_value( registry::error(py, ErrorId::PlannerInput)?.call1((format!( "model '{name}': migrate_from names the model's own relation" ),))?, )); } - let newest = newest_move(&state.getattr("events")?, &relation(&destination)?)?; + let newest = newest_move(&native_field(&state, "events")?, &relation(&destination)?)?; let origin_relation = state.call_method1("relation", (&origin,))?; let fingerprint = call_with( - &state.getattr("model_fingerprint")?, + &native_field(&state, "model_fingerprint")?, &[ ("location", origin.clone()), ("model_name", origin_model.clone()), @@ -96,7 +97,7 @@ pub(super) fn decide<'py>( py, RecordId::ModelMigrationPlanEntry, &[ - ("model_name", item.getattr("name")?), + ("model_name", native_field(&item, "name")?), ("discovery", request.discovery.clone()), ( "decision", @@ -120,7 +121,7 @@ pub(super) fn decide<'py>( if fingerprint.is_none() { text(py, "") } else { - fingerprint.getattr("version_hash")? + native_field(&fingerprint, "version_hash")? }, ), ( @@ -128,7 +129,7 @@ pub(super) fn decide<'py>( PyBool::new( py, !origin_relation.is_none() - && origin_relation.getattr("is_transient")?.is_truthy()?, + && native_field(&origin_relation, "is_transient")?.is_truthy()?, ) .to_owned() .into_any(), @@ -151,7 +152,7 @@ pub(super) fn decide<'py>( .call_method1("matches", (relation(&origin)?,))? .is_truthy()?; if request.identity_only { - let recorded = recorded && newest.getattr("decision")?.eq("renamed")?; + let recorded = recorded && native_field(&newest, "decision")?.eq("renamed")?; if request.discovery.eq("manual")? && !recorded && origin_relation.is_none() { return result( decision(&base, "origin_missing")?, @@ -163,7 +164,7 @@ pub(super) fn decide<'py>( snapshot .getattr("fingerprints")? .getattr("models")? - .call_method1("get", (item.getattr("name")?,))? + .call_method1("get", (native_field(&item, "name")?,))? } else { empty_version(&item, &fingerprint)? }; @@ -173,7 +174,7 @@ pub(super) fn decide<'py>( &[( "completed_at", if recorded { - newest.getattr("created_at")? + native_field(&newest, "created_at")? } else { py.None().into_bound(py) }, @@ -186,23 +187,27 @@ pub(super) fn decide<'py>( if recorded { let applies = applies(&item, &newest, &snapshot)?; let inherited = if applies { - handover(&item, &fingerprint, &newest.getattr("origin_version_hash")?)? + handover( + &item, + &fingerprint, + &native_field(&newest, "origin_version_hash")?, + )? } else { snapshot .getattr("fingerprints")? .getattr("models")? - .call_method1("get", (item.getattr("name")?,))? + .call_method1("get", (native_field(&item, "name")?,))? }; return result( replace( &decision(&base, "done")?, &[ - ("completed_at", newest.getattr("created_at")?), + ("completed_at", native_field(&newest, "created_at")?), ( "origin_version_hash", - newest.getattr("origin_version_hash")?, + native_field(&newest, "origin_version_hash")?, ), - ("target_name", newest.getattr("target_name")?), + ("target_name", native_field(&newest, "target_name")?), ], )?, inherited, @@ -224,13 +229,13 @@ pub(super) fn decide<'py>( .getattr("project")? .getattr("settings")? .getattr("sql_analysis")?, - column_dialect: snapshot.getattr("column_dialect")?, + column_dialect: native_field(&snapshot, "column_dialect")?, snapshots_config: decision_request.snapshots_config.clone(), })?; let name = model_decision( snapshot .getattr("existing_relations")? - .contains(item.getattr("name")?)?, + .contains(native_field(&item, "name")?)?, ( !newest.is_none(), !newest.is_none() @@ -241,7 +246,7 @@ pub(super) fn decide<'py>( snapshot .getattr("fingerprints")? .getattr("models")? - .contains(item.getattr("name")?)?, + .contains(native_field(&item, "name")?)?, ), request.force, ); @@ -256,11 +261,15 @@ pub(super) fn decide<'py>( .getattr("decision")? .getattr("moves_data")? .is_truthy()? - || entry.getattr("blocks_build")?.is_truthy()? + || native_field(&entry, "blocks_build")?.is_truthy()? { return result(entry, py.None().into_bound(py), false); } - let inherited = handover(&item, &fingerprint, &entry.getattr("origin_version_hash")?)?; + let inherited = handover( + &item, + &fingerprint, + &native_field(&entry, "origin_version_hash")?, + )?; result( super::planning_execution::migration_with_execution(&runtime, &item, &entry)?, inherited, 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 74ae995674..9d713cfc26 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 @@ -6,6 +6,7 @@ use crate::bindings::_helpers::fingerprints::query_hashes::compute_query_hash; use crate::bindings::_helpers::planner_migrations::column_objects::{replace, text}; use crate::bindings::_helpers::planner_migrations::constants::MIGRATION_MODEL_NAME_METADATA_KEY; use crate::bindings::_helpers::planner_phases::objects::object; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::state_rows::timestamps::presumed_utc; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; use pyo3::types::{PyDict, PyDictMethods}; @@ -20,7 +21,8 @@ pub(super) fn applies( .getattr("models")? .call_method1("get", (model_reads::name_object(model)?,))?; Ok(own.is_none() - || presumed_utc(&own.getattr("ts")?)? < presumed_utc(&event.getattr("created_at")?)?) + || presumed_utc(&native_field(&own, "ts")?)? + < presumed_utc(&native_field(event, "created_at")?)?) } pub(super) fn handover<'py>( @@ -32,11 +34,11 @@ pub(super) fn handover<'py>( return Ok(fingerprint.clone()); } let py = model.py(); - let mut metadata = fingerprint.getattr("metadata_json")?; + let mut metadata = native_field(fingerprint, "metadata_json")?; let json = py.import("json")?; let payload = match json.call_method1("loads", (&metadata,)) { Ok(payload) => payload, - Err(error) if error.is_instance(py, &json.getattr("JSONDecodeError")?) => { + Err(error) if error.is_instance(py, &native_field(&json, "JSONDecodeError")?) => { py.None().into_bound(py) } Err(error) => return Err(error), @@ -50,22 +52,22 @@ pub(super) fn handover<'py>( kwargs.set_item("sort_keys", true)?; kwargs.set_item("separators", (",", ":"))?; kwargs.set_item("default", imported(py, "builtins", "str")?)?; - metadata = json.getattr("dumps")?.call((payload,), Some(&kwargs))?; + metadata = native_field(&json, "dumps")?.call((payload,), Some(&kwargs))?; } let destination = model_reads::destination(model)?; replace( fingerprint, &[ ("node_name", model_reads::name_object(model)?), - ("target_database", destination.getattr("database")?), - ("target_schema", destination.getattr("schema")?), - ("target_name", destination.getattr("name")?), + ("target_database", native_field(&destination, "database")?), + ("target_schema", native_field(&destination, "schema")?), + ("target_name", native_field(&destination, "name")?), ( "version_hash", if version.is_truthy()? { version.clone() } else { - fingerprint.getattr("version_hash")? + native_field(fingerprint, "version_hash")? }, ), ("metadata_json", metadata), 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 1b9941282b..60f7564254 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 @@ -4,6 +4,7 @@ use crate::bindings::_helpers::planner_migrations::column_objects::native; use crate::bindings::_helpers::planner_migrations::column_objects::{config, replace}; use crate::bindings::_helpers::planner_migrations::models::MigrationPhase; use crate::bindings::_helpers::planner_phases::runtime::runtime_field; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; use pyo3::types::{PyDict, PyDictMethods}; @@ -28,14 +29,14 @@ pub(super) fn overlay<'py>( .getattr("existing_columns")? .cast_into::()? .copy()?; - let fingerprints = snapshot.getattr("fingerprints")?; + let fingerprints = native_field(&snapshot, "fingerprints")?; let models = fingerprints .getattr("models")? .cast_into::()? .copy()?; let redirected = PyDict::new(py); for entry in entries { - let name = entry.getattr("model_name")?; + let name = native_field(entry, "model_name")?; let Some(handover) = handovers.get_item(&name)? else { continue; }; @@ -51,25 +52,25 @@ pub(super) fn overlay<'py>( { continue; } - let origin = entry.getattr("origin")?; + let origin = native_field(entry, "origin")?; let origin_relation = state.call_method1("relation", (&origin,))?; if origin_relation.is_none() { continue; } - let destination = entry.getattr("destination")?; - let transient = entry.getattr("stage_is_transient")?; + let destination = native_field(entry, "destination")?; + let transient = native_field(entry, "stage_is_transient")?; relations.set_item( &name, replace( &origin_relation, &[ - ("database", destination.getattr("database")?), - ("schema", destination.getattr("schema")?), - ("name", destination.getattr("name")?), + ("database", native_field(&destination, "database")?), + ("schema", native_field(&destination, "schema")?), + ("name", native_field(&destination, "name")?), ( "is_transient", if transient.is_none() { - origin_relation.getattr("is_transient")? + native_field(&origin_relation, "is_transient")? } else { transient }, @@ -78,7 +79,7 @@ pub(super) fn overlay<'py>( )?, )?; columns.set_item(&name, state.call_method1("relation_columns", (&origin,))?)?; - let qualified = origin.getattr("qualified_name")?; + let qualified = native_field(&origin, "qualified_name")?; if !qualified.is_none() && cursor_enabled(&runtime, &name)? { redirected.set_item(name, qualified)?; } @@ -105,9 +106,9 @@ pub(super) fn overlay<'py>( RecordId::WarehouseFingerprints, &[ ("models", models.into_any()), - ("functions", fingerprints.getattr("functions")?), - ("seeds", fingerprints.getattr("seeds")?), - ("python_nodes", fingerprints.getattr("python_nodes")?), + ("functions", native_field(&fingerprints, "functions")?), + ("seeds", native_field(&fingerprints, "seeds")?), + ("python_nodes", native_field(&fingerprints, "python_nodes")?), ], )?, ), @@ -122,7 +123,7 @@ fn cursor_enabled(runtime: &Bound<'_, PyAny>, name: &Bound<'_, PyAny>) -> PyResu .try_iter()? { let item = item?; - if item.getattr("name")?.eq(name)? { + if native_field(&item, "name")?.eq(name)? { return Ok(config(&item, "materialized")?.eq("incremental")? && !config(&item, "cursor")?.is_none()); } 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 093f5c8be9..32e4de77cc 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,6 +14,7 @@ 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::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}; @@ -23,7 +24,7 @@ use pyo3::types::{ fn declared<'py>(item: &Bound<'py, PyAny>) -> PyResult>> { let py = item.py(); - let values = item.getattr("config")?.getattr("values")?; + let values = native_field(item, "config")?.getattr("values")?; let origin = values.call_method1("get", ("migrate_from",))?; if !origin.is_instance_of::() { return Ok(None); @@ -46,16 +47,16 @@ fn manual_migration_requests<'py>( scope: &Bound<'py, PyAny>, ) -> PyResult>> { let mut requests: Vec> = Vec::new(); - for key in scope.getattr("execution_order")?.try_iter()? { + for key in native_field(scope, "execution_order")?.try_iter()? { let key = key?; - if !scope.getattr("selected_keys")?.contains(&key)? - || !key.getattr("resource_type")?.eq("model")? + if !native_field(scope, "selected_keys")?.contains(&key)? + || !native_field(&key, "resource_type")?.eq("model")? { continue; } let item = scope .getattr("models_by_name")? - .call_method1("get", (key.getattr("name")?,))?; + .call_method1("get", (native_field(&key, "name")?,))?; if !item.is_none() && let Some(request) = declared(&item)? { @@ -73,8 +74,10 @@ pub(crate) fn plan_model_migrations<'py>( let runtime = phase.runtime.clone(); let snapshot = phase.snapshot.clone(); let schemas = super::planning_origin::project_schemas(&runtime)?; - let state = - super::state_inspection::inspection(&runtime, Some(&snapshot.getattr("fingerprints")?))?; + let state = super::state_inspection::inspection( + &runtime, + Some(&native_field(&snapshot, "fingerprints")?), + )?; let discovery = discover((phase, fingerprints), &state, &schemas)?; finish(phase, &state, &schemas, &discovery) } @@ -98,7 +101,7 @@ fn discover<'py>( } if !declared_requests.is_empty() { call_with( - &state.getattr("inspect_schemas")?, + &native_field(state, "inspect_schemas")?, &[("schemas", schemas.clone())], )?; } @@ -106,7 +109,7 @@ fn discover<'py>( for request in &declared_requests { let (origin_location, origin_model) = resolve_origin(request, &runtime, state)?; declarations.push(ModelMigrationDeclaration { - model_name: request.model.getattr("name")?, + model_name: native_field(&request.model, "name")?, origin_location, origin_model, }); @@ -149,30 +152,30 @@ fn finish<'py>( let mut origins: Vec> = Vec::new(); for (_, origin, _) in &resolved { require_same_database(state, origin)?; - let schema = origin.getattr("schema")?; + let schema = native_field(origin, "schema")?; if !schema.is_none() { origin_schemas.add(schema)?; } origins.push(origin.clone()); } call_with( - &state.getattr("inspect_schemas")?, + &native_field(state, "inspect_schemas")?, &[("schemas", origin_schemas.into_any())], )?; call_with( - &state.getattr("inspect_relations")?, + &native_field(state, "inspect_relations")?, &[("locations", PyTuple::new(py, origins)?.into_any())], )?; let project_config = runtime_field(&runtime, |runtime| &runtime.project_config)?; let snapshots_config = if project_config.is_none() { registry::class(py, RecordId::SnapshotsConfig)?.call0()? } else { - project_config.getattr("snapshots")? + native_field(&project_config, "snapshots")? }; let policy = super::planning_boundary::missing_origin_policy(&runtime)?; for (request, origin, origin_model) in resolved { let item = request.model.clone(); - let name = item.getattr("name")?; + let name = native_field(&item, "name")?; let decided = super::model_entries::decide(&MigrationDecisionRequest { runtime: runtime.clone(), request, @@ -196,7 +199,7 @@ fn finish<'py>( &item, decided.inherited, &destinations, - &snapshot.getattr("column_dialect")?, + &native_field(&snapshot, "column_dialect")?, )?, )?; } @@ -207,12 +210,12 @@ fn finish<'py>( let mut warnings: Vec> = Vec::new(); warnings.extend(discovery.warnings.iter().cloned()); for entry in &entries { - let name = entry.getattr("model_name")?; + let name = native_field(entry, "model_name")?; if let Some(handover) = handovers.get_item(&name)? { - if !handover.is_none() && entry.getattr("decision")?.eq("renamed")? { + if !handover.is_none() && native_field(entry, "decision")?.eq("renamed")? { renamed.push(name.clone()); } - let origin_model = entry.getattr("origin_model")?; + let origin_model = native_field(entry, "origin_model")?; if !origin_model.is_none() { planned.set_item(&name, origin_model)?; } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/model_warnings.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/model_warnings.rs index 35267749c3..5d48e34ad5 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/model_warnings.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/model_warnings.rs @@ -4,6 +4,7 @@ use crate::bindings::_helpers::planner_migrations::column_warnings::{ missing_origin_outcome, warning, }; use crate::bindings::_helpers::planner_migrations::constants::HIDDEN_ORIGIN_REMEDY; +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; use pyo3::types::PyString; use sqlbuild_analysis::planner_migrations::main::model_warning::model_warning; @@ -11,35 +12,35 @@ use sqlbuild_analysis::planner_migrations::models::ModelWarning; pub(super) fn entry_warning<'py>(entry: &Bound<'py, PyAny>) -> PyResult>> { let py = entry.py(); - let name: String = entry.getattr("model_name")?.extract()?; - let origin = display(&entry.getattr("origin")?)?; - let destination = display(&entry.getattr("destination")?)?; - let decision: String = entry.getattr("decision")?.extract()?; - let completed = entry.getattr("completed_at")?; + let name: String = native_field(entry, "model_name")?.extract()?; + let origin = display(&native_field(entry, "origin")?)?; + let destination = display(&native_field(entry, "destination")?)?; + let decision: String = native_field(entry, "decision")?.extract()?; + let completed = native_field(entry, "completed_at")?; let completed: Option = if completed.is_none() { None } else { Some(completed.call_method0("isoformat")?.extract()?) }; - let target: String = entry.getattr("target_label")?.extract()?; - let policy = entry.getattr("missing_origin_policy")?; + let target: String = native_field(entry, "target_label")?.extract()?; + let policy = native_field(entry, "missing_origin_policy")?; let remedy: String = HIDDEN_ORIGIN_REMEDY.to_owned(); let outcome: String = missing_origin_outcome(&policy)?.to_owned(); let findings: String = PyString::new(py, "; ") - .call_method1("join", (entry.getattr("compatibility_findings")?,))? + .call_method1("join", (native_field(entry, "compatibility_findings")?,))? .extract()?; let facts = ModelWarning { name: &name, origin: &origin, destination: &destination, decision: &decision, - manual: entry.getattr("discovery")?.eq("manual")?, + manual: native_field(entry, "discovery")?.eq("manual")?, completed: completed.as_deref(), target: &target, - hidden: entry.getattr("origin_hidden")?.is_truthy()?, - missing: entry.getattr("origin_missing")?.is_truthy()?, + hidden: native_field(entry, "origin_hidden")?.is_truthy()?, + missing: native_field(entry, "origin_missing")?.is_truthy()?, deny: policy.eq("deny")?, - blocked: entry.getattr("blocks_build")?.is_truthy()?, + blocked: native_field(entry, "blocks_build")?.is_truthy()?, remedy: &remedy, outcome: &outcome, findings: &findings, @@ -50,10 +51,10 @@ pub(super) fn entry_warning<'py>(entry: &Bound<'py, PyAny>) -> PyResult) -> PyResult { - let qualified = location.getattr("qualified_name")?; + let qualified = native_field(location, "qualified_name")?; if qualified.is_truthy()? { qualified.extract() } else { - location.getattr("name")?.extract() + native_field(location, "name")?.extract() } } 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 6d9a45f2e5..86f7b37a68 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,6 +12,7 @@ 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::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}; @@ -30,14 +31,14 @@ impl<'py> OldNamePlanner<'py> { let config = if project_config.is_none() { registry::class(py, RecordId::MigrationsConfig)?.call0()? } else { - project_config.getattr("migrations")? + native_field(&project_config, "migrations")? }; Ok(Self { models, histories, views, columns: columns.clone(), - retention: config.getattr("old_name_views")?, + retention: native_field(&config, "old_name_views")?, now: runtime_field(&runtime, |runtime| &runtime.invocation_time)?, runtime, }) @@ -50,7 +51,7 @@ impl<'py> OldNamePlanner<'py> { ) -> PyResult>> { let mut newest: Option<(Bound<'py, PyTuple>, Bound<'py, PyAny>)> = None; for history in &self.histories { - let movement = history.getattr("move")?; + let movement = native_field(history, "move")?; if !movement .getattr("destination")? .call_method1("matches", (destination,))? @@ -69,8 +70,8 @@ impl<'py> OldNamePlanner<'py> { let key = PyTuple::new( self.runtime.py(), [ - movement.getattr("created_at")?, - movement.getattr("event_id")?, + native_field(&movement, "created_at")?, + native_field(&movement, "event_id")?, ], )?; if let Some((previous, _)) = &newest @@ -88,19 +89,22 @@ impl<'py> OldNamePlanner<'py> { entry: &Bound<'py, PyAny>, item: &Bound<'py, PyAny>, ) -> PyResult>> { - if entry.getattr("blocks_build")?.is_truthy()? { + if native_field(entry, "blocks_build")?.is_truthy()? { return Ok(None); } - let decision = entry.getattr("decision")?; - let completed = !entry.getattr("completed_at")?.is_none(); - if decision.getattr("moves_data")?.is_truthy()? || (decision.eq("renamed")? && !completed) { + let decision = native_field(entry, "decision")?; + let completed = !native_field(entry, "completed_at")?.is_none(); + if native_field(&decision, "moves_data")?.is_truthy()? + || (decision.eq("renamed")? && !completed) + { return self.new_move(entry, item).map(Some); } if decision.eq("done")? || (decision.eq("renamed")? && completed) { - let origin = relation(&entry.getattr("origin")?)?; - if let Some(history) = - self.newest(&relation(&entry.getattr("destination")?)?, Some(&origin))? - { + let origin = relation(&native_field(entry, "origin")?)?; + if let Some(history) = self.newest( + &relation(&native_field(entry, "destination")?)?, + Some(&origin), + )? { return self.resume(&history, item); } } @@ -111,22 +115,23 @@ impl<'py> OldNamePlanner<'py> { &self, item: &Bound<'py, PyAny>, ) -> PyResult>> { - let Some(history) = self.newest(&relation(&item.getattr("destination")?)?, None)? else { + let Some(history) = self.newest(&relation(&native_field(item, "destination")?)?, None)? + else { return Ok(None); }; if !history .getattr("move")? .getattr("destination_model")? - .eq(item.getattr("name")?)? + .eq(native_field(item, "name")?)? { return Ok(None); } let Some(planned) = self.resume(&history, item)? else { return Ok(None); }; - if planned.getattr("runs_steps")?.is_truthy()? - || (planned.getattr("action")?.eq("none")? - && !planned.getattr("migration_event_id")?.is_none()) + if native_field(&planned, "runs_steps")?.is_truthy()? + || (native_field(&planned, "action")?.eq("none")? + && !native_field(&planned, "migration_event_id")?.is_none()) { Ok(Some(planned)) } else { @@ -169,15 +174,15 @@ impl<'py> OldNamePlanner<'py> { let mut aliases: Vec> = Vec::new(); for entry in self.columns.try_iter()? { let entry = entry?; - let decision = entry.getattr("decision")?; - if entry.getattr("model_name")?.eq(item.getattr("name")?)? + let decision = native_field(&entry, "decision")?; + if native_field(&entry, "model_name")?.eq(native_field(item, "name")?)? && (decision.eq("rename")? || decision.eq("record")?) { aliases.push(PyTuple::new( py, [ - entry.getattr("origin_column")?, - entry.getattr("destination_column")?, + native_field(&entry, "origin_column")?, + native_field(&entry, "destination_column")?, ], )?); } @@ -195,15 +200,15 @@ impl<'py> OldNamePlanner<'py> { let py = self.runtime.py(); let retention = retention(item, &self.retention)?; let reason = self.skip( - &entry.getattr("origin")?, - entry.getattr("origin_tracked")?, + &native_field(entry, "origin")?, + native_field(entry, "origin_tracked")?, &retention, )?; let mut fields = vec![ - ("model_name", entry.getattr("model_name")?), - ("origin", entry.getattr("origin")?), - ("destination", entry.getattr("destination")?), - ("target_name", entry.getattr("target_name")?), + ("model_name", native_field(entry, "model_name")?), + ("origin", native_field(entry, "origin")?), + ("destination", native_field(entry, "destination")?), + ("target_name", native_field(entry, "target_name")?), ]; if !reason.is_none() || retention.is_none() { fields.extend([ @@ -248,8 +253,11 @@ impl<'py> OldNamePlanner<'py> { item: &Bound<'py, PyAny>, ) -> PyResult>> { let py = self.runtime.py(); - let status: String = - call_with(&history.getattr("status")?, &[("now", self.now.clone())])?.extract()?; + let status: String = call_with( + &native_field(history, "status")?, + &[("now", self.now.clone())], + )? + .extract()?; let action_name = match status.as_str() { "pending_archive" => "ARCHIVE_AND_VIEW", "pending_view" => "VIEW_ONLY", @@ -258,7 +266,7 @@ impl<'py> OldNamePlanner<'py> { }; if status == PENDING_ARCHIVE || status == PENDING_VIEW { let reason = self.skip( - &location(&self.runtime, &history.getattr("old")?)?, + &location(&self.runtime, &native_field(history, "old")?)?, py.None().into_bound(py), &retention(item, &self.retention)?, )?; @@ -269,21 +277,24 @@ impl<'py> OldNamePlanner<'py> { &[ ( "model_name", - history.getattr("move")?.getattr("destination_model")?, + native_field(history, "move")?.getattr("destination_model")?, + ), + ( + "origin", + location(&self.runtime, &native_field(history, "old")?)?, ), - ("origin", location(&self.runtime, &history.getattr("old")?)?), ( "destination", - location(&self.runtime, &history.getattr("new")?)?, + location(&self.runtime, &native_field(history, "new")?)?, ), ("action", action(&self.runtime, "NONE")?), ( "target_name", - history.getattr("move")?.getattr("target_name")?, + native_field(history, "move")?.getattr("target_name")?, ), ( "migration_event_id", - history.getattr("move")?.getattr("event_id")?, + native_field(history, "move")?.getattr("event_id")?, ), ("reason", reason), ], @@ -291,7 +302,7 @@ impl<'py> OldNamePlanner<'py> { .map(Some); } } - let retention = history.getattr("required")?.getattr("view_retention")?; + let retention = native_field(history, "required")?.getattr("view_retention")?; let duration = registry::class(py, RecordId::Duration)?.call_method1( "parse", (if retention.is_truthy()? { @@ -300,9 +311,9 @@ impl<'py> OldNamePlanner<'py> { text(py, "") },), )?; - let created = history.getattr("created")?; + let created = native_field(history, "created")?; let expires_at = if !created.is_none() { - created.getattr("expires_at")? + native_field(&created, "expires_at")? } else if duration.is_none() { py.None().into_bound(py) } else { @@ -311,22 +322,25 @@ impl<'py> OldNamePlanner<'py> { let grants = if created.is_none() { py.None().into_bound(py) } else { - created.getattr("grants_copied")? + native_field(&created, "grants_copied")? }; model( py, RecordId::OldNameViewPlanEntry, &[ - ("model_name", item.getattr("name")?), - ("origin", location(&self.runtime, &history.getattr("old")?)?), + ("model_name", native_field(item, "name")?), + ( + "origin", + location(&self.runtime, &native_field(history, "old")?)?, + ), ( "destination", - location(&self.runtime, &history.getattr("new")?)?, + location(&self.runtime, &native_field(history, "new")?)?, ), ("action", action(&self.runtime, action_name)?), ( "target_name", - history.getattr("move")?.getattr("target_name")?, + native_field(history, "move")?.getattr("target_name")?, ), ("retention", retention), ("expires_at", expires_at), @@ -335,7 +349,7 @@ impl<'py> OldNamePlanner<'py> { if created.is_none() { self.aliases(item)? } else { - created.getattr("column_aliases")? + native_field(&created, "column_aliases")? }, ), ( @@ -344,7 +358,7 @@ impl<'py> OldNamePlanner<'py> { ), ( "migration_event_id", - history.getattr("move")?.getattr("event_id")?, + native_field(history, "move")?.getattr("event_id")?, ), ( "grants_copied", @@ -362,7 +376,7 @@ impl<'py> OldNamePlanner<'py> { ), ( "archived", - PyBool::new(py, !history.getattr("archived")?.is_none()) + PyBool::new(py, !native_field(history, "archived")?.is_none()) .to_owned() .into_any(), ), 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 282804b077..6eff236782 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,6 +4,7 @@ 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::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; @@ -18,9 +19,9 @@ pub(super) fn location<'py>( relation: &Bound<'py, PyAny>, ) -> PyResult> { let mut fields = vec![ - ("database", relation.getattr("database")?), - ("schema", relation.getattr("schema")?), - ("name", relation.getattr("name")?), + ("database", native_field(relation, "database")?), + ("schema", native_field(relation, "schema")?), + ("name", native_field(relation, "name")?), ]; let qualified = call_with( &runtime @@ -44,7 +45,7 @@ pub(super) fn retention<'py>( item: &Bound<'py, PyAny>, project: &Bound<'py, PyAny>, ) -> PyResult> { - old_name_retention(&item.getattr("config")?.getattr("values")?, project) + old_name_retention(&native_field(item, "config")?.getattr("values")?, project) } pub(super) fn expires<'py>( @@ -66,15 +67,15 @@ pub(super) fn recorded_view<'py>( models: &[Bound<'py, PyAny>], ) -> PyResult> { let py = runtime.py(); - let movement = history.getattr("move")?; - let created = history.getattr("created")?; + let movement = native_field(history, "move")?; + let created = native_field(history, "created")?; let mut reused = py.None().into_bound(py); for item in models { - if relation(&item.getattr("destination")?)? - .call_method1("matches", (history.getattr("old")?,))? + if relation(&native_field(item, "destination")?)? + .call_method1("matches", (native_field(history, "old")?,))? .is_truthy()? { - reused = item.getattr("name")?; + reused = native_field(item, "name")?; break; } } @@ -82,15 +83,18 @@ pub(super) fn recorded_view<'py>( py, RecordId::OldNameView, &[ - ("destination_model", movement.getattr("destination_model")?), - ("old", location(runtime, &history.getattr("old")?)?), - ("new", location(runtime, &history.getattr("new")?)?), + ( + "destination_model", + native_field(&movement, "destination_model")?, + ), + ("old", location(runtime, &native_field(history, "old")?)?), + ("new", location(runtime, &native_field(history, "new")?)?), ( "expires_at", if created.is_none() { py.None().into_bound(py) } else { - created.getattr("expires_at")? + native_field(&created, "expires_at")? }, ), ( @@ -98,11 +102,11 @@ pub(super) fn recorded_view<'py>( if created.is_none() { pyo3::types::PyTuple::empty(py).into_any() } else { - created.getattr("column_aliases")? + native_field(&created, "column_aliases")? }, ), - ("migration_event_id", movement.getattr("event_id")?), - ("target_name", movement.getattr("target_name")?), + ("migration_event_id", native_field(&movement, "event_id")?), + ("target_name", native_field(&movement, "target_name")?), ("name_reused_by", reused), ], ) 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 10a26869ee..a79839ee77 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 @@ -10,6 +10,7 @@ use crate::bindings::_helpers::planner_migrations::old_names::old_name_claim_err use crate::bindings::_helpers::planner_migrations::old_names::old_name_schemas_to_read; use crate::bindings::_helpers::planner_phases::objects::object; use crate::bindings::_helpers::planner_phases::runtime::runtime_field; +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; use pyo3::types::{PyDict, PyDictMethods, PySet, PySetMethods, PyTuple}; @@ -22,13 +23,13 @@ fn schemas<'py>( let snapshot = phase.snapshot.clone(); let needed = PySet::empty(py)?; for item in selected { - let schema = item.getattr("destination")?.getattr("schema")?; + let schema = native_field(item, "destination")?.getattr("schema")?; if !schema.is_none() { needed.add(schema)?; } } for entry in migration_entries.try_iter()? { - let schema = entry?.getattr("origin")?.getattr("schema")?; + let schema = native_field(&entry?, "origin")?.getattr("schema")?; if !schema.is_none() { needed.add(schema)?; } @@ -41,10 +42,10 @@ fn schemas<'py>( for item in selected { let existing = snapshot .getattr("existing_relations")? - .call_method1("get", (item.getattr("name")?,))?; + .call_method1("get", (native_field(item, "name")?,))?; if !existing.is_none() && host::entry(py, AdapterHost::NormalizeRelationType)? - .call1((existing.getattr("relation_type")?,))? + .call1((native_field(&existing, "relation_type")?,))? .eq("view")? { include_project = true; @@ -79,14 +80,14 @@ pub(crate) fn plan_old_name_views<'py>( .collect::>()?; let by_name = PyDict::new(py); for item in &models { - by_name.set_item(item.getattr("name")?, item)?; + by_name.set_item(native_field(item, "name")?, item)?; } let mut selected: Vec> = Vec::new(); - for key in scope.getattr("execution_order")?.try_iter()? { + for key in native_field(&scope, "execution_order")?.try_iter()? { let key = key?; - if scope.getattr("selected_keys")?.contains(&key)? - && key.getattr("resource_type")?.eq("model")? - && let Some(item) = by_name.get_item(key.getattr("name")?)? + if native_field(&scope, "selected_keys")?.contains(&key)? + && native_field(&key, "resource_type")?.eq("model")? + && let Some(item) = by_name.get_item(native_field(&key, "name")?)? { selected.push(item); } @@ -95,20 +96,22 @@ pub(crate) fn plan_old_name_views<'py>( let needed = schemas((phase, moves.0), &selected)?; if needed.is_truthy()? { call_with( - &state.getattr("inspect_old_name_views")?, + &native_field(&state, "inspect_old_name_views")?, &[("schemas", needed)], )?; } - let target = project.getattr("effective_target_name")?; + let target = native_field(&project, "effective_target_name")?; let mut histories: Vec> = Vec::new(); let mut views: Vec> = Vec::new(); - for history in state.getattr("old_name_histories")?.try_iter()? { + for history in native_field(&state, "old_name_histories")?.try_iter()? { let history = history?; - let history_target = history.getattr("move")?.getattr("target_name")?; + let history_target = native_field(&history, "move")?.getattr("target_name")?; if !history_target.is_none() && !history_target.eq(&target)? { continue; } - if !history.getattr("created")?.is_none() && history.getattr("dropped")?.is_none() { + if !native_field(&history, "created")?.is_none() + && native_field(&history, "dropped")?.is_none() + { views.push(recorded_view(&runtime, &history, &models)?); } histories.push(history); @@ -124,7 +127,7 @@ pub(crate) fn plan_old_name_views<'py>( let covered = PySet::empty(py)?; for entry in moves.0.try_iter()? { let entry = entry?; - let name = entry.getattr("model_name")?; + let name = native_field(&entry, "model_name")?; if let Some(item) = by_name.get_item(&name)? && let Some(planned) = planner.for_migration(&entry, &item)? { @@ -133,7 +136,7 @@ pub(crate) fn plan_old_name_views<'py>( } } for item in &selected { - if !covered.contains(item.getattr("name")?)? + if !covered.contains(native_field(item, "name")?)? && let Some(planned) = planner.pending_for(item)? { entries.push(planned); 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 763428c7d7..29a95213ef 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,6 +7,7 @@ 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::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; @@ -31,9 +32,9 @@ fn old_relation<'py>( let relation = call_with( &imported(py, "sqlbuild.compiler.references.models", "RelationName")?, &[ - ("name", old.getattr("name")?), - ("schema", old.getattr("schema")?), - ("database", old.getattr("database")?), + ("name", native_field(old, "name")?), + ("schema", native_field(old, "schema")?), + ("database", native_field(old, "database")?), ], )?; call_with( @@ -62,15 +63,21 @@ pub(super) fn errors<'py>( for view in views.try_iter()? { let view = view?; relations.set_item( - old_relation(&view.getattr("old")?, view.getattr("destination_model")?)?, + old_relation( + &native_field(&view, "old")?, + native_field(&view, "destination_model")?, + )?, py.None(), )?; } for entry in entries.try_iter()? { let entry = entry?; - if entry.getattr("runs_steps")?.is_truthy()? { + if native_field(&entry, "runs_steps")?.is_truthy()? { relations.set_item( - old_relation(&entry.getattr("origin")?, entry.getattr("model_name")?)?, + old_relation( + &native_field(&entry, "origin")?, + native_field(&entry, "model_name")?, + )?, py.None(), )?; } @@ -86,9 +93,9 @@ pub(super) fn errors<'py>( let mut errors: Vec> = Vec::new(); for literal in literals.try_iter()? { let literal = literal?; - let relation = literal.getattr("relation")?; + let relation = native_field(&literal, "relation")?; let matched = object(py, match_project_relation(&index, &relation, None, None)?)?; - if matched.is_none() || matched.getattr("compatibility_for")?.is_none() { + if matched.is_none() || native_field(&matched, "compatibility_for")?.is_none() { continue; } let mut parts: Vec = Vec::new(); @@ -101,16 +108,16 @@ pub(super) fn errors<'py>( let message: String = object( py, old_name_reference_message( - &literal.getattr("owner_label")?.extract::()?, + &native_field(&literal, "owner_label")?.extract::()?, &parts.join("."), - &literal.getattr("method")?.extract::()?, - &matched.getattr("compatibility_for")?.extract::()?, + &native_field(&literal, "method")?.extract::()?, + &native_field(&matched, "compatibility_for")?.extract::()?, ), )? .extract()?; let remedy: String = hard_coded_relation_remedy( - &literal.getattr("owner_kind")?.extract::()?, - &matched.getattr("ref")?, + &native_field(&literal, "owner_kind")?.extract::()?, + &native_field(&matched, "ref")?, None, )?; let path = literal @@ -118,7 +125,7 @@ pub(super) fn errors<'py>( .str()? .to_str()? .to_owned(); - let line = literal.getattr("line")?.str()?.to_str()?.to_owned(); + let line = native_field(&literal, "line")?.str()?.to_str()?.to_owned(); errors.push(native( py, RecordId::PlanWarning, 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 b6a5675b08..5925e19d1a 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 @@ -5,6 +5,7 @@ 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::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}; @@ -20,14 +21,14 @@ pub(crate) fn declared_column_aliases<'py>( py: Python<'py>, model: &Bound<'py, PyAny>, ) -> PyResult> { - let schema = model.getattr("schema_entry")?; + let schema = native_field(model, "schema_entry")?; let mut columns: Vec<(String, Option)> = Vec::new(); if !schema.is_none() { - for column in schema.getattr("columns")?.try_iter()? { + for column in native_field(&schema, "columns")?.try_iter()? { let column = column?; columns.push(( - column.getattr("name")?.extract()?, - column.getattr("migrate_from")?.extract()?, + native_field(&column, "name")?.extract()?, + native_field(&column, "migrate_from")?.extract()?, )); } } @@ -42,12 +43,12 @@ pub(crate) fn old_name_schemas_to_read<'py>( needed: &Bound<'py, PyAny>, ) -> PyResult> { let listed: Option> = - snapshot.getattr("listed_state_schemas")?.extract()?; + native_field(snapshot, "listed_state_schemas")?.extract()?; PySet::new( py, old_name_schemas::old_name_schemas( &needed.extract()?, - &snapshot.getattr("old_name_view_state_schemas")?.extract()?, + &native_field(snapshot, "old_name_view_state_schemas")?.extract()?, listed.as_ref(), ), ) @@ -73,12 +74,12 @@ fn old_name_claim_message<'py>( } fn claim_message(model: &str, view: &Bound<'_, PyAny>, now: &Bound<'_, PyAny>) -> PyResult { - let old = view.getattr("old")?; - let qualified: Option = old.getattr("qualified_name")?.extract()?; + let old = native_field(view, "old")?; + let qualified: Option = native_field(&old, "qualified_name")?.extract()?; let name: String = qualified .filter(|name| !name.is_empty()) - .unwrap_or(old.getattr("name")?.extract()?); - let expires = view.getattr("expires_at")?; + .unwrap_or(native_field(&old, "name")?.extract()?); + let expires = native_field(view, "expires_at")?; let date: Option = if expires.is_none() { None } else { @@ -89,7 +90,7 @@ fn claim_message(model: &str, view: &Bound<'_, PyAny>, now: &Bound<'_, PyAny>) - model, ( &name, - &view.getattr("destination_model")?.extract::()?, + &native_field(view, "destination_model")?.extract::()?, ), (date.as_deref(), expired), )) @@ -119,15 +120,15 @@ pub(crate) fn old_name_claim_errors<'py>( continue; } let kind = host::entry(py, AdapterHost::NormalizeRelationType)? - .call1((existing.getattr("relation_type")?,))?; + .call1((native_field(&existing, "relation_type")?,))?; if !kind.eq("view")? { continue; } let destination = relation(&model_reads::destination(&model)?)?; for view in arguments.value("views")?.try_iter()? { let view = view?; - if view.getattr("destination_model")?.eq(&name)? - || !relation(&view.getattr("old")?)? + if native_field(&view, "destination_model")?.eq(&name)? + || !relation(&native_field(&view, "old")?)? .call_method1("matches", (&destination,))? .is_truthy()? { @@ -189,7 +190,7 @@ pub(crate) fn old_name_view_skip_reason<'py>( let mut compatibility_view = false; if reuser.is_none() { for view in arguments.value("views")?.try_iter()? { - if relation(&view?.getattr("old")?)? + if relation(&native_field(&view?, "old")?)? .call_method1("matches", (&old,))? .is_truthy()? { 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 97951f109f..1b846482ba 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 @@ -7,6 +7,7 @@ use crate::bindings::_helpers::planner_phases::full_refresh::effectively_full_re use crate::bindings::_helpers::planner_phases::retention::effective_target_config; use crate::bindings::_helpers::planner_phases::runtime::runtime_field; use crate::bindings::_helpers::planner_warehouse::snapshot::gather_redirected_cursor_snapshots; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; @@ -14,7 +15,10 @@ use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; pub(super) fn missing_origin_policy<'py>( runtime: &Bound<'py, PyAny>, ) -> PyResult> { - effective_target_config(runtime)?.getattr("missing_migration_origin") + native_field( + &effective_target_config(runtime)?, + "missing_migration_origin", + ) } /// Live cursor snapshots of models whose history now sits in a redirected relation. @@ -32,25 +36,31 @@ pub(super) fn redirected_cursor_snapshots<'py>( let start = if config.is_none() { py.None().into_bound(py) } else { - config.getattr("cursors")?.getattr("start")? + native_field(&config, "cursors")?.getattr("start")? }; let cursor_scope = native( py, RecordId::CursorSnapshotScope, &[ - ("model_keys", scope.getattr("selected_keys")?), - ("runtime_producer_keys", scope.getattr("selected_keys")?), + ("model_keys", native_field(scope, "selected_keys")?), + ( + "runtime_producer_keys", + native_field(scope, "selected_keys")?, + ), ( "invocation_time", runtime_field(runtime, |runtime| &runtime.invocation_time)?, ), ("start_cursor_config", start), - ("cursor_overrides", overrides.getattr("cursor_overrides")?), + ( + "cursor_overrides", + native_field(overrides, "cursor_overrides")?, + ), ], )?; let full_refresh = effectively_full_refreshed_model_names( &project, - overrides.getattr("full_refresh")?.is_truthy()?, + native_field(overrides, "full_refresh")?.is_truthy()?, )?; gather_redirected_cursor_snapshots( py, @@ -72,7 +82,7 @@ pub(super) fn redirected_cursor_snapshots<'py>( ("full_refresh_model_names", full_refresh), ( "deferred_locations", - phase.deferral.getattr("deferred_locations")?, + native_field(&phase.deferral, "deferred_locations")?, ), ( "on_progress", 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 97a010b63c..b507dc28d1 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,17 +5,18 @@ 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::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; fn label<'py>(location: &Bound<'py, PyAny>) -> PyResult> { - let qualified = location.getattr("qualified_name")?; + let qualified = native_field(location, "qualified_name")?; if qualified.is_truthy()? { Ok(qualified) } else { - location.getattr("name") + native_field(location, "name") } } @@ -29,12 +30,12 @@ fn migration_promotion<'py>( .call_method0("supports_transactional_ddl")? .is_truthy()? { - return promotion.getattr("TRANSACTIONAL_RENAME"); + return native_field(&promotion, "TRANSACTIONAL_RENAME"); } - if !decision.eq("migrate")? && adapter.getattr("adapter_name")?.eq("snowflake")? { - return promotion.getattr("SWAP"); + if !decision.eq("migrate")? && native_field(&adapter, "adapter_name")?.eq("snowflake")? { + return native_field(&promotion, "SWAP"); } - promotion.getattr("RENAME") + native_field(&promotion, "RENAME") } /// The migration entry with the adapter's staging, transfer and promotion choices. @@ -45,7 +46,7 @@ pub(super) fn migration_with_execution<'py>( ) -> PyResult> { let py = runtime.py(); let adapter = runtime_field(runtime, |runtime| &runtime.adapter)?; - let transient = if adapter.getattr("adapter_name")?.eq("snowflake")? { + let transient = if native_field(&adapter, "adapter_name")?.eq("snowflake")? { PyBool::new( py, model @@ -60,16 +61,19 @@ pub(super) fn migration_with_execution<'py>( py.None().into_bound(py) }; let stage = call_with( - &adapter.getattr("render_migration_stage")?, + &native_field(&adapter, "render_migration_stage")?, &[ - ("origin", label(&entry.getattr("origin")?)?), - ("stage", label(&entry.getattr("destination")?)?), - ("origin_is_transient", entry.getattr("origin_is_transient")?), + ("origin", label(&native_field(entry, "origin")?)?), + ("stage", label(&native_field(entry, "destination")?)?), + ( + "origin_is_transient", + native_field(entry, "origin_is_transient")?, + ), ("stage_is_transient", transient.clone()), ], )?; - let transfer = stage.getattr("transfer")?; - let fallback = if stage.getattr("fallback_statements")?.is_truthy()? { + let transfer = native_field(&stage, "transfer")?; + let fallback = if native_field(&stage, "fallback_statements")?.is_truthy()? { host::entry(py, AdapterHost::MigrationTransfer)?.getattr("COPY")? } else { py.None().into_bound(py) @@ -82,7 +86,7 @@ pub(super) fn migration_with_execution<'py>( ("transfer_fallback", fallback), ( "promotion", - migration_promotion(runtime, &entry.getattr("decision")?)?, + migration_promotion(runtime, &native_field(entry, "decision")?)?, ), ], ) 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 de08a94111..a669f815d0 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; use pyo3::PyErr; @@ -39,24 +40,25 @@ fn named<'py>( .try_iter()? { let item = item?; - if item.getattr("name")?.eq(name)? { - return Ok((item.getattr("destination")?, name.clone())); + if native_field(&item, "name")?.eq(name)? { + return Ok((native_field(&item, "destination")?, name.clone())); } } let fingerprint = call_with( - &state.getattr("named_fingerprint")?, + &native_field(state, "named_fingerprint")?, &[("model_name", name.clone())], )?; - let fields = if !fingerprint.is_none() && !fingerprint.getattr("target_name")?.is_none() { + let fields = if !fingerprint.is_none() && !native_field(&fingerprint, "target_name")?.is_none() + { [ - ("database", fingerprint.getattr("target_database")?), - ("schema", fingerprint.getattr("target_schema")?), - ("name", fingerprint.getattr("target_name")?), + ("database", native_field(&fingerprint, "target_database")?), + ("schema", native_field(&fingerprint, "target_schema")?), + ("name", native_field(&fingerprint, "target_name")?), ] } else { [ - ("database", destination.getattr("database")?), - ("schema", destination.getattr("schema")?), + ("database", native_field(destination, "database")?), + ("schema", native_field(destination, "schema")?), ("name", name.clone()), ] }; @@ -72,9 +74,9 @@ fn input_error(py: Python<'_>, message: String) -> PyResult { /// The database migration planning inspects: the first model destination's, else the target's. pub(super) fn planning_database<'py>(runtime: &Bound<'py, PyAny>) -> PyResult> { let project = runtime_field(runtime, |runtime| &runtime.project)?; - let fallback = project.getattr("effective_target_database")?; - for item in project.getattr("models")?.try_iter()? { - let database = item?.getattr("destination")?.getattr("database")?; + let fallback = native_field(&project, "effective_target_database")?; + for item in native_field(&project, "models")?.try_iter()? { + let database = native_field(&item?, "destination")?.getattr("database")?; if !database.is_none() { return Ok(database); } @@ -89,7 +91,7 @@ pub(super) fn project_schemas<'py>(runtime: &Bound<'py, PyAny>) -> PyResult, location: &Bound<'_, PyAny>, ) -> PyResult<()> { - let database = location.getattr("database")?; + let database = native_field(location, "database")?; if database.is_none() { return Ok(()); } - let inspected = state.getattr("database")?; + let inspected = native_field(state, "database")?; if inspected.is_none() || database .call_method0("lower")? @@ -114,11 +116,11 @@ pub(super) fn require_same_database( { return Ok(()); } - let qualified = location.getattr("qualified_name")?; + let qualified = native_field(location, "qualified_name")?; let label = if qualified.is_truthy()? { qualified } else { - location.getattr("name")? + native_field(location, "name")? }; Err(input_error( state.py(), @@ -143,7 +145,7 @@ pub(super) fn resolve_origin<'py>( _ => super::column_objects::text(py, ""), }; let item = request.model.clone(); - let destination = item.getattr("destination")?; + let destination = native_field(&item, "destination")?; let mut parts: Vec> = Vec::new(); for part in raw.call_method1("split", (".",))?.try_iter()? { parts.push( @@ -162,7 +164,7 @@ pub(super) fn resolve_origin<'py>( py, format!( "model '{}': migrate_from '{raw}' is not a model name or a qualified relation", - item.getattr("name")? + native_field(&item, "name")? ), )?); } @@ -172,7 +174,7 @@ pub(super) fn resolve_origin<'py>( let database = if parts.len() == maximum { parts[0].clone() } else { - destination.getattr("database")? + native_field(&destination, "database")? }; let origin = location( runtime, 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 cec042fd6c..1494a50e7f 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/projections.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_migrations/projections.rs @@ -2,6 +2,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; use crate::bindings::_helpers::planner_migrations::errors::ProofError; +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; @@ -13,23 +14,23 @@ use sqlbuild_analysis::planner_migrations::models::{QueryProjection, QueryShape} fn read_shape(shape: &Bound<'_, PyAny>) -> PyResult { let mut projections: Vec = Vec::new(); - for item in shape.getattr("projections")?.try_iter()? { + for item in native_field(shape, "projections")?.try_iter()? { let item = item?; projections.push(QueryProjection { - name: item.getattr("name")?.extract()?, - expression: item.getattr("expression")?.extract()?, - references: item.getattr("references")?.extract()?, - passthrough: item.getattr("passthrough")?.extract()?, + name: native_field(&item, "name")?.extract()?, + expression: native_field(&item, "expression")?.extract()?, + references: native_field(&item, "references")?.extract()?, + passthrough: native_field(&item, "passthrough")?.extract()?, }); } Ok(QueryShape { projections, - body: shape.getattr("body")?.extract()?, - alias_clauses: shape.getattr("alias_clauses")?.extract()?, - positional: shape.getattr("positional")?.extract()?, - input_relations: shape.getattr("input_relations")?.extract()?, - local_input_columns: shape.getattr("local_input_columns")?.extract()?, - clause_references: shape.getattr("clause_references")?.extract()?, + body: native_field(shape, "body")?.extract()?, + alias_clauses: native_field(shape, "alias_clauses")?.extract()?, + positional: native_field(shape, "positional")?.extract()?, + input_relations: native_field(shape, "input_relations")?.extract()?, + local_input_columns: native_field(shape, "local_input_columns")?.extract()?, + clause_references: native_field(shape, "clause_references")?.extract()?, }) } 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 14c1d73c87..470131e94e 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 @@ -20,6 +20,7 @@ use crate::bindings::_helpers::planner_migrations::constants::{ GET_COLUMNS_FOR_INSPECTION, LIST_RELATIONS_FOR_INSPECTION, }; use crate::bindings::_helpers::planner_phases::runtime::runtime_field; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::state_rows::constants::{COLUMN_KIND, MODEL_KIND, OLD_NAME_KIND}; use crate::bindings::_helpers::state_rows::migration_io::{read_events, stored_old_name_columns}; use crate::bindings::_helpers::state_rows::migration_objects::layout; @@ -67,8 +68,8 @@ fn schema_or_empty<'py>(value: Bound<'py, PyAny>) -> Bound<'py, PyAny> { /// `((schema or "").lower(), name.lower())` of a relation or location. fn relation_key(value: &Bound<'_, PyAny>) -> PyResult { Ok(( - lowered(&schema_or_empty(value.getattr("schema")?))?, - lowered(&value.getattr("name")?)?, + lowered(&schema_or_empty(native_field(value, "schema")?))?, + lowered(&native_field(value, "name")?)?, )) } @@ -97,13 +98,13 @@ fn tuple_of<'py>(py: Python<'py>, items: &[Py]) -> PyResult) -> PyResult { - let target_name: Bound<'_, PyAny> = fingerprint.getattr("target_name")?; + let target_name: Bound<'_, PyAny> = native_field(fingerprint, "target_name")?; let target: Option = if target_name.is_none() { None } else { Some(EventRelation { - database: fingerprint.getattr("target_database")?.extract()?, - schema: fingerprint.getattr("target_schema")?.extract()?, + database: native_field(fingerprint, "target_database")?.extract()?, + schema: native_field(fingerprint, "target_schema")?.extract()?, name: target_name.extract()?, }) }; @@ -113,9 +114,9 @@ fn fingerprint_key(fingerprint: &Bound<'_, PyAny>) -> PyResult { .getattr("ts")? .call_method0("isoformat")? .extract()?, - run_id: fingerprint.getattr("run_id")?.extract()?, + run_id: native_field(fingerprint, "run_id")?.extract()?, }, - node_name: fingerprint.getattr("node_name")?.extract()?, + node_name: native_field(fingerprint, "node_name")?.extract()?, target, }) } @@ -138,8 +139,8 @@ impl MigrationStateInspection { let adapter: &Bound<'py, PyAny> = self.adapter.bind(py); Ok(StateAccess { connection: self.connection.bind(py).clone(), - execute: adapter.getattr("execute")?, - render_qualified_name: adapter.getattr("render_qualified_name")?, + execute: native_field(adapter, "execute")?, + render_qualified_name: native_field(adapter, "render_qualified_name")?, }) } @@ -179,22 +180,22 @@ impl MigrationStateInspection { py, &[ ("connection", self.connection.bind(py).clone()), - ("execute", adapter.getattr("execute")?), + ("execute", native_field(adapter, "execute")?), ("table_exists", PyBool::new(py, true).to_owned().into_any()), ("database", self.database.bind(py).clone()), ("schema", schema.clone()), ( "render_qualified_name", - adapter.getattr("render_qualified_name")?, + native_field(adapter, "render_qualified_name")?, ), ( "render_read_latest_sql", - adapter.getattr("render_read_latest_fingerprints_sql")?, + native_field(adapter, "render_read_latest_fingerprints_sql")?, ), ], )?))?; let by_identity: Bound<'py, PyAny> = - fingerprint_set.getattr("fingerprints_by_identity")?; + native_field(&fingerprint_set, "fingerprints_by_identity")?; if by_identity.is_truthy()? { by_identity.call_method0("values")? } else { @@ -206,7 +207,7 @@ impl MigrationStateInspection { let mut models: Vec> = Vec::new(); for fingerprint in latest.try_iter()? { let fingerprint: Bound<'py, PyAny> = fingerprint?; - if fingerprint.getattr("node_type")?.eq(NODE_TYPE_MODEL)? { + if native_field(&fingerprint, "node_type")?.eq(NODE_TYPE_MODEL)? { models.push(fingerprint.unbind()); } } @@ -227,9 +228,10 @@ impl MigrationStateInspection { let py: Python<'_> = adapter.py(); let known: Bound<'_, PyAny> = match known_fingerprints { Some(known) - if !known.is_none() && known.getattr("unfiltered_database")?.eq(database)? => + if !known.is_none() + && native_field(known, "unfiltered_database")?.eq(database)? => { - known.getattr("unfiltered_schemas")? + native_field(known, "unfiltered_schemas")? } _ => PyDict::new(py).into_any(), }; @@ -297,8 +299,10 @@ impl MigrationStateInspection { )?; let mut present: HashSet = HashSet::new(); for relation in &listed { - if lowered(&relation.getattr("name")?)? == COLUMN_MIGRATION_TABLE_NAME { - present.insert(lowered(&schema_or_empty(relation.getattr("schema")?))?); + if lowered(&native_field(relation, "name")?)? == COLUMN_MIGRATION_TABLE_NAME { + present.insert(lowered(&schema_or_empty(native_field( + relation, "schema", + )?))?); } } for (schema, key) in pending { @@ -446,7 +450,7 @@ impl MigrationStateInspection { let mut names: Vec = Vec::new(); for location in locations.try_iter()? { let location: Bound<'_, PyAny> = location?; - if location.getattr("schema")?.is_none() { + if native_field(&location, "schema")?.is_none() { continue; } let key: RelationKey = relation_key(&location)?; @@ -454,8 +458,8 @@ impl MigrationStateInspection { continue; } wanted.insert(key); - schemas.push(location.getattr("schema")?.extract()?); - names.push(location.getattr("name")?.extract()?); + schemas.push(native_field(&location, "schema")?.extract()?); + names.push(native_field(&location, "name")?.extract()?); } if wanted.is_empty() { return Ok(()); @@ -501,7 +505,7 @@ impl MigrationStateInspection { let empty: Bound<'_, PyTuple> = PyTuple::empty(py); for (key, relation) in matched { let relation_columns: Bound<'_, PyAny> = - columns.call_method1("get", (relation.getattr("identity")?, &empty))?; + columns.call_method1("get", (native_field(&relation, "identity")?, &empty))?; let mut this: PyRefMut<'_, Self> = slf.borrow_mut(); this.relations.insert(key.clone(), relation.unbind()); this.columns.insert(key, relation_columns.unbind()); @@ -533,15 +537,15 @@ impl MigrationStateInspection { location: &Bound<'py, PyAny>, model_name: Option, ) -> PyResult>> { - let key: String = lowered(&schema_or_empty(location.getattr("schema")?))?; + let key: String = lowered(&schema_or_empty(native_field(location, "schema")?))?; let Some((_, candidates)) = self.fingerprints.iter().find(|(stored, _)| *stored == key) else { return Ok(None); }; let target = EventRelation { - database: location.getattr("database")?.extract()?, - schema: location.getattr("schema")?.extract()?, - name: location.getattr("name")?.extract()?, + database: native_field(location, "database")?.extract()?, + schema: native_field(location, "schema")?.extract()?, + name: native_field(location, "name")?.extract()?, }; newest(py, candidates, (Some(&target), model_name.as_deref())) } @@ -571,7 +575,7 @@ impl MigrationStateInspection { for (_, fingerprints) in &self.fingerprints { for fingerprint in fingerprints { let fingerprint: &Bound<'py, PyAny> = fingerprint.bind(py); - if !project_model_names.contains(fingerprint.getattr("node_name")?)? { + if !project_model_names.contains(native_field(fingerprint, "node_name")?)? { orphans.push(fingerprint.clone()); } } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/buildability.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/buildability.rs index 645b9cb43c..c4e0320e7f 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/buildability.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/buildability.rs @@ -12,6 +12,7 @@ use crate::bindings::_helpers::planner_phases::objects::{frozenset, planner_erro 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_tables::check_selected_source_tables_exist; +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; use pyo3::pyfunction; @@ -27,10 +28,10 @@ pub(crate) fn check_selected_scope_buildability<'py>( let project = runtime_field(runtime, |runtime| &runtime.project)?; let selected_scope = class_field(scopes, |s: &PlannerScopeResolution| &s.selected_scope)?; let mut external_seeds: Vec<_> = Vec::new(); - for seed in project.getattr("seeds")?.try_iter()? { + for seed in native_field(&project, "seeds")?.try_iter()? { let seed = seed?; - if seed.getattr("external")?.is_truthy()? { - external_seeds.push(seed.getattr("key")?); + if native_field(&seed, "external")?.is_truthy()? { + external_seeds.push(native_field(&seed, "key")?); } } let missing = object( @@ -40,15 +41,21 @@ pub(crate) fn check_selected_scope_buildability<'py>( Some(keywords( py, &[ - ("selected_keys", selected_scope.getattr("selected_keys")?), - ("upstream_deps", selected_scope.getattr("upstream_deps")?), + ( + "selected_keys", + native_field(&selected_scope, "selected_keys")?, + ), + ( + "upstream_deps", + native_field(&selected_scope, "upstream_deps")?, + ), ( "snapshot", warehouse_field(warehouse, |warehouse| &warehouse.snapshot)?, ), ( "deferred_relations", - deferral.getattr("deferred_relations")?, + native_field(deferral, "deferred_relations")?, ), ("satisfied_keys", frozenset(py, &external_seeds)?), ], 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 76370f1dca..fa39d71c99 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 @@ -14,6 +14,7 @@ use crate::bindings::_helpers::planner_phases::models::PlannerIdentityContext; use crate::bindings::_helpers::planner_phases::models::{ChangeReconciliation, ModelEntryResults}; use crate::bindings::_helpers::planner_phases::runtime::runtime_field; use crate::bindings::_helpers::planner_phases::runtime::warehouse_field; +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; use pyo3::types::PyFrozenSet; @@ -87,14 +88,14 @@ pub(crate) fn build_planner_entry_results<'py>( .getattr("source_warehouse_columns")?, relation_keys: py.None().into_bound(py), })?; - let propagation = source_freshness.getattr("propagation")?; + let propagation = native_field(source_freshness, "propagation")?; let blocked = if propagation.is_none() { PyFrozenSet::empty(py)?.into_any() } else { - propagation.getattr("blocked_model_names")? + native_field(&propagation, "blocked_model_names")? }; let none = || py.None().into_bound(py); - let max_override = overrides.getattr("max_microbatches")?; + let max_override = native_field(overrides, "max_microbatches")?; let max_microbatches = if max_override.is_none() { config_or(&config, &["microbatches", "limits", "max_batches"], none())? } else { @@ -138,9 +139,9 @@ pub(crate) fn build_planner_entry_results<'py>( scope: execution_scope.clone(), snapshot: warehouse_field(warehouse, |warehouse| &warehouse.snapshot)?, relations, - resolved_models: reconciliation.resolved_actions.getattr("models")?, - cursor_overrides: overrides.getattr("cursor_overrides")?, - full_refresh: overrides.getattr("full_refresh")?.is_truthy()?, + resolved_models: native_field(&reconciliation.resolved_actions, "models")?, + cursor_overrides: native_field(overrides, "cursor_overrides")?, + full_refresh: native_field(overrides, "full_refresh")?.is_truthy()?, build_inputs, }) } 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 5e578431fd..5f1d7d43c8 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 @@ -3,6 +3,7 @@ use crate::bindings::_helpers::analysis::model_reads; use crate::bindings::_helpers::planner_phases::constants::FULL_REFRESH_DISABLED_REBUILD_CODE; use crate::bindings::_helpers::planner_phases::objects::{frozenset, planner_error, strings}; +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; use pyo3::pyfunction; use pyo3::types::PyBool; @@ -38,7 +39,7 @@ pub(crate) fn effectively_full_refreshed_model_names<'py>( cli_full_refresh: bool, ) -> PyResult> { let mut names: Vec<_> = Vec::new(); - for model in project.getattr("models")?.try_iter()? { + for model in native_field(project, "models")?.try_iter()? { let model = model?; if resolve_model_full_refresh(&model, cli_full_refresh)? { names.push(model_reads::name_object(&model)?); @@ -59,9 +60,9 @@ pub(crate) fn describe_full_rebuild_cause( .getattr("change_kind")? .str()? .extract::()?, - query_changed: change_result.getattr("query_changed")?.is_truthy()?, - changed_functions: strings(&change_result.getattr("changed_functions")?)?, - has_schema_findings: change_result.getattr("schema_findings")?.is_truthy()?, + query_changed: native_field(change_result, "query_changed")?.is_truthy()?, + changed_functions: strings(&native_field(change_result, "changed_functions")?)?, + has_schema_findings: native_field(change_result, "schema_findings")?.is_truthy()?, })) } @@ -76,8 +77,8 @@ pub(crate) fn check_full_rebuild_allowed( if !configured_full_refresh(model)?.is(PyBool::new(py, false)) { return Ok(()); } - if change_result.getattr("change_kind")?.eq("first_run")? - || change_result.getattr("query_changed")?.is_truthy()? + if native_field(change_result, "change_kind")?.eq("first_run")? + || native_field(change_result, "query_changed")?.is_truthy()? { return Ok(()); } 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 a1166b854f..4cc53740ba 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/identities.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/identities.rs @@ -5,6 +5,7 @@ use crate::bindings::_helpers::planner_phases::contexts::class_field; use crate::bindings::_helpers::planner_phases::models::PlannerIdentityContext; use crate::bindings::_helpers::planner_phases::models::PlannerScopeResolution; use crate::bindings::_helpers::planner_phases::objects::{frozenset, native, replace}; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; use pyo3::IntoPyObject; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; @@ -21,11 +22,11 @@ fn version_identities<'py>( project.py(), NativeAttr::BuildDirectModelVersionIdentities, &[ - ("functions", project.getattr("functions")?), - ("seeds", project.getattr("seeds")?), + ("functions", native_field(project, "functions")?), + ("seeds", native_field(project, "seeds")?), ("scope", scope), - ("hook_functions", project.getattr("hook_functions")?), - ("dialect", project.getattr("sql_analysis_dialect")?), + ("hook_functions", native_field(project, "hook_functions")?), + ("dialect", native_field(project, "sql_analysis_dialect")?), ], ) } @@ -74,7 +75,7 @@ fn reusable_model_names( let stale = class_field(identities, |i: &PlannerIdentityContext| { &i.stale_warning_identities })?; - for name in execution_changes.getattr("models")?.try_iter()? { + for name in native_field(execution_changes, "models")?.try_iter()? { let name = name?; let same = |field: &str| -> PyResult { execution @@ -143,8 +144,8 @@ pub(crate) fn detect_stale_warning_changes<'py>( .try_iter()? { let key = key?; - if key.getattr("resource_type")?.eq("model")? - && !reusable.contains(&key.getattr("name")?.extract::()?) + if native_field(&key, "resource_type")?.eq("model")? + && !reusable.contains(&native_field(&key, "name")?.extract::()?) { remaining.push(key); } @@ -171,11 +172,11 @@ pub(crate) fn detect_stale_warning_changes<'py>( ), ( "expected_version_hashes", - stale_identities.getattr("model_version_hashes")?, + native_field(&stale_identities, "model_version_hashes")?, ), ( "expected_metadata_jsons", - stale_identities.getattr("model_metadata_jsons")?, + native_field(&stale_identities, "model_metadata_jsons")?, ), ], )?; 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 1ee9b4fd85..fa2922368e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/objects.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/objects.rs @@ -4,6 +4,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::{call_with, importe 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, NativeItem, RecordId}; use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyErr, PyResult, Python}; @@ -117,8 +118,8 @@ pub(crate) fn model_key_names(keys: &Bound<'_, PyAny>) -> PyResult> let mut names: Vec<_> = Vec::new(); for key in keys.try_iter()? { let key = key?; - if key.getattr("resource_type")?.eq("model")? { - names.push(key.getattr("name")?.extract()?); + if native_field(&key, "resource_type")?.eq("model")? { + names.push(native_field(&key, "name")?.extract()?); } } Ok(names) @@ -149,14 +150,14 @@ pub(crate) fn by_name<'py>( let nodes = pyo3::types::PyDict::new(project.py()); for node in project.getattr(field)?.try_iter()? { let node = node?; - pyo3::types::PyDictMethods::set_item(&nodes, node.getattr("name")?, node)?; + pyo3::types::PyDictMethods::set_item(&nodes, native_field(&node, "name")?, node)?; } Ok(nodes) } /// 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(&model.getattr("config")?.getattr("values")?, key)? + authored_config_str(&native_field(model, "config")?.getattr("values")?, key)? .map(|value| value.extract()) .transpose() } 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 381a50a5a1..7004855328 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 @@ -21,6 +21,7 @@ use crate::bindings::_helpers::planner_phases::retention::plan_retention; 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; @@ -92,11 +93,11 @@ pub(crate) fn base_plan_output<'py>( &[ ( "seed_version_hashes", - version.getattr("seed_version_hashes")?, + native_field(&version, "seed_version_hashes")?, ), ( "seed_metadata_jsons", - version.getattr("seed_metadata_jsons")?, + native_field(&version, "seed_metadata_jsons")?, ), ("planned_sql_tests", planned_sql_tests.clone()), ], @@ -111,13 +112,16 @@ pub(crate) fn base_plan_output<'py>( changes: reconciliation.changes.clone(), model_entries: entries.entries, model_warnings: entries.warnings, - reload_sources: overrides.getattr("reload_sources")?, + reload_sources: native_field(overrides, "reload_sources")?, extras, })?, metadata: Vec::new(), }; output.set("source_freshness", source_freshness.clone()); - output.set("cursor_overrides", overrides.getattr("cursor_overrides")?); + output.set( + "cursor_overrides", + native_field(overrides, "cursor_overrides")?, + ); Ok(output) } @@ -132,7 +136,7 @@ pub(crate) fn with_column_rename_hints<'py>( let by_model = PyDict::new(py); for hint in hints.try_iter()? { let hint = hint?; - let name = hint.getattr("model_name")?; + let name = native_field(&hint, "model_name")?; if let Some(existing) = by_model.get_item(&name)? { existing.call_method1("append", (hint,))?; } else { @@ -142,7 +146,7 @@ pub(crate) fn with_column_rename_hints<'py>( let mut attached: Vec<_> = Vec::new(); for entry in entries.try_iter()? { let entry = entry?; - match by_model.get_item(entry.getattr("name")?)? { + match by_model.get_item(native_field(&entry, "name")?)? { Some(found) => attached.push(replace( &entry, &[( @@ -262,7 +266,7 @@ pub(crate) fn add_plan_warnings<'py>( let warnings = concat(py, &[output.field("warnings")?, freshness])?; output.set("warnings", warnings); } - if !policies.getattr("selection_diagnostics")?.is_truthy()? { + if !native_field(policies, "selection_diagnostics")?.is_truthy()? { return Ok(output); } let stale = object( @@ -314,7 +318,7 @@ pub(crate) fn add_plan_warnings<'py>( fn source_names<'py>(items: &Bound<'py, PyAny>) -> PyResult> { let mut names: Vec<_> = Vec::new(); for item in items.try_iter()? { - names.push(item?.getattr("source_name")?.extract::()?); + names.push(native_field(&item?, "source_name")?.extract::()?); } sorted_names(items.py(), names) } @@ -324,7 +328,7 @@ fn age_names<'py>(statuses: &Bound<'py, PyAny>, wanted: &str) -> PyResult, Bound<'py, PyAny>) = item?.extract()?; if status.eq(wanted)? { - names.push(identity.getattr("source_name")?.extract::()?); + names.push(native_field(&identity, "source_name")?.extract::()?); } } sorted_names(statuses.py(), names) @@ -332,7 +336,7 @@ fn age_names<'py>(statuses: &Bound<'py, PyAny>, wanted: &str) -> PyResult(source_freshness: &Bound<'py, PyAny>) -> PyResult> { let py = source_freshness.py(); - let propagation = source_freshness.getattr("propagation")?; + let propagation = native_field(source_freshness, "propagation")?; let propagated = |field: &str| -> PyResult> { if propagation.is_none() { return tuple(py, Vec::new()); @@ -347,30 +351,33 @@ fn freshness_metadata<'py>(source_freshness: &Bound<'py, PyAny>) -> PyResult( let py = source_freshness.py(); let diagnostics = PyDict::new(py); diagnostics.set_item("mode", text(py, "direct"))?; - diagnostics.set_item("enabled", policies.getattr("selection_diagnostics")?)?; + diagnostics.set_item("enabled", native_field(policies, "selection_diagnostics")?)?; output.metadata.push(( "direct_source_freshness", freshness_metadata(source_freshness)?, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/retention.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/retention.rs index 39a084effd..ea3d43c149 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/retention.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/retention.rs @@ -15,6 +15,7 @@ use crate::bindings::_helpers::planner_phases::objects::{ planner_model, replace, sorted_names, text, tuple, }; use crate::bindings::_helpers::planner_phases::runtime::runtime_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 pyo3::IntoPyObject; @@ -54,7 +55,7 @@ fn selected_models<'py>( runtime: &Bound<'py, PyAny>, scope: &Bound<'py, PyAny>, ) -> PyResult>> { - let names: BTreeSet = model_key_names(&scope.getattr("selected_keys")?)? + let names: BTreeSet = model_key_names(&native_field(scope, "selected_keys")?)? .into_iter() .collect(); let mut models: Vec<_> = Vec::new(); @@ -120,9 +121,9 @@ pub(crate) fn plan_table_types<'py>( let materialization = registry::class(py, RecordId::MaterializationType)?; let mut entries: Vec<_> = Vec::new(); for model in models { - let config = model.getattr("config")?; + let config = native_field(&model, "config")?; let backed = call_with( - &materialization.getattr("is_table_backed")?, + &native_field(&materialization, "is_table_backed")?, &[( "materialized", config @@ -137,9 +138,9 @@ pub(crate) fn plan_table_types<'py>( if relation.is_none() { continue; } - let table_type = config.getattr("table_type")?; - let desired_transient = table_type.getattr("value")?.eq(TRANSIENT_TABLE_TYPE)?; - let actual = relation.getattr("is_transient")?; + let table_type = native_field(&config, "table_type")?; + let desired_transient = native_field(&table_type, "value")?.eq(TRANSIENT_TABLE_TYPE)?; + let actual = native_field(&relation, "is_transient")?; let actual: Option = if actual.is_none() { None } else { @@ -153,7 +154,7 @@ pub(crate) fn plan_table_types<'py>( let limit: i64 = adapter .call_method0("maximum_identifier_length")? .extract()?; - let target_name: String = destination.getattr("name")?.extract()?; + let target_name: String = native_field(&destination, "name")?.extract()?; entries.push(planner_model( py, RecordId::TableTypePlanEntry, @@ -161,7 +162,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", table_type.getattr("value")?.getattr("value")?), + ("desired_type", native_field(&table_type, "value")?.getattr("value")?), ( "actual_type", optional_text( @@ -175,11 +176,11 @@ pub(crate) fn plan_table_types<'py>( }), ), ), - ("source", table_type.getattr("source")?.getattr("value")?), + ("source", native_field(&table_type, "source")?.getattr("value")?), ("downgrade", downgrade.into_pyobject(py)?.to_owned().into_any()), ( "downgrade_policy", - target.getattr("table_type_downgrade")?.getattr("value")?, + native_field(&target, "table_type_downgrade")?.getattr("value")?, ), ( "irreversible_warning", @@ -250,7 +251,7 @@ fn entry<'py>( fn flatten<'py>(changes: &Bound<'py, PyAny>) -> PyResult> { let mut statements: Vec<_> = Vec::new(); for change in changes.try_iter()? { - for statement in change?.getattr("statements")?.try_iter()? { + for statement in native_field(&change?, "statements")?.try_iter()? { statements.push(statement?); } } @@ -279,7 +280,10 @@ fn inspect<'py>( request: &Bound<'py, PyAny>, ) -> PyResult> { call_with( - &runtime_field(runtime, |runtime| &runtime.adapter)?.getattr("inspect_retention")?, + &native_field( + &runtime_field(runtime, |runtime| &runtime.adapter)?, + "inspect_retention", + )?, &[ ( "connection", @@ -297,7 +301,7 @@ fn state_direction(state: &Bound<'_, PyAny>, desired_days: i64) -> PyResult<&'st optional("delta_log_retention_days")?, optional("delta_deleted_file_retention_days")?, ], - state.getattr("effective_days")?.extract()?, + native_field(state, "effective_days")?.extract()?, desired_days, )) } @@ -319,18 +323,20 @@ fn relation_retention<'py>( ) -> PyResult>> { let py = runtime.py(); let name: String = model_reads::model_name(model)?; - let config = model.getattr("config")?; - let retention = config.getattr("time_travel_retention")?; - if retention.getattr("desired_days")?.is_none() || retention.getattr("source")?.is_none() { + let config = native_field(model, "config")?; + let retention = native_field(&config, "time_travel_retention")?; + if native_field(&retention, "desired_days")?.is_none() + || native_field(&retention, "source")?.is_none() + { return Err(planner_error( py, &format!("model '{name}': managed retention is incomplete"), (None, None), )); } - let desired_days: i64 = retention.getattr("desired_days")?.extract()?; + let desired_days: i64 = native_field(&retention, "desired_days")?.extract()?; let destination = model_reads::destination(model)?; - let schema = destination.getattr("schema")?; + let schema = native_field(&destination, "schema")?; if schema.is_none() { return Err(planner_error( py, @@ -347,20 +353,20 @@ fn relation_retention<'py>( "scope", member(py, AdapterHost::RetentionScope, "relation")?, ), - ("database", destination.getattr("database")?), + ("database", native_field(&destination, "database")?), ("schema", schema.clone()), - ("name", destination.getattr("name")?), - ("desired_days", retention.getattr("desired_days")?), + ("name", native_field(&destination, "name")?), + ("desired_days", native_field(&retention, "desired_days")?), ], )?; let mut base = RetentionEntry { request: request.clone(), model_names: tuple(py, vec![model_reads::name_object(model)?])?, state: None, - source: retention.getattr("source")?.getattr("value")?, + source: native_field(&retention, "source")?.getattr("value")?, }; let snowflake = adapter_is(runtime, SNOWFLAKE_ADAPTER)?; - let table_type = config.getattr("table_type")?.getattr("value")?; + let table_type = native_field(&config, "table_type")?.getattr("value")?; let relations = warehouse .getattr("snapshot")? .getattr("existing_relations")?; @@ -376,17 +382,17 @@ fn relation_retention<'py>( )?]); } let relation = relations.get_item(model_reads::name_object(model)?)?; - let relation_transient = relation.getattr("is_transient")?; + let relation_transient = native_field(&relation, "is_transient")?; let lookup = call_with( &host::entry(py, AdapterHost::RelationLookup)?.getattr("key")?, &[ - ("database", destination.getattr("database")?), + ("database", native_field(&destination, "database")?), ("schema", schema), - ("name", destination.getattr("name")?), + ("name", native_field(&destination, "name")?), ], )?; let mut listed = py.None().into_bound(py); - if relation.getattr("identity")?.eq(&lookup)? { + if native_field(&relation, "identity")?.eq(&lookup)? { listed = call_with( &runtime .getattr("adapter")? @@ -399,7 +405,7 @@ fn relation_retention<'py>( } else { inspect(runtime, &request)? }; - if state.getattr("is_transient")?.is_truthy()? + if native_field(&state, "is_transient")?.is_truthy()? && desired_days > 1 && table_type.eq(TRANSIENT_TABLE_TYPE)? { @@ -435,11 +441,11 @@ fn relation_retention<'py>( let mut entries: Vec<_> = Vec::new(); for change in changes.try_iter()? { let change = change?; - let phase: String = change.getattr("phase")?.str()?.extract()?; + let phase: String = native_field(&change, "phase")?.str()?.extract()?; entries.push(entry( &base, retention_change(&phase, direction), - change.getattr("statements")?, + native_field(&change, "statements")?, )?); } Ok(entries) @@ -468,18 +474,18 @@ fn bigquery_retention<'py>( .try_iter()? { let model = model?; - let retention = model.getattr("config")?.getattr("time_travel_retention")?; + let retention = native_field(&model, "config")?.getattr("time_travel_retention")?; let destination = model_reads::destination(&model)?; - if retention.getattr("unmanaged")?.is_truthy()? - || retention.getattr("desired_days")?.is_none() - || destination.getattr("schema")?.is_none() + if native_field(&retention, "unmanaged")?.is_truthy()? + || native_field(&retention, "desired_days")?.is_none() + || native_field(&destination, "schema")?.is_none() { continue; } managed .entry(( - destination.getattr("database")?.extract()?, - destination.getattr("schema")?.extract()?, + native_field(&destination, "database")?.extract()?, + native_field(&destination, "schema")?.extract()?, )) .or_default() .push(model); @@ -487,8 +493,8 @@ fn bigquery_retention<'py>( let mut scopes: BTreeSet<(String, String, Option)> = BTreeSet::new(); for model in models { let destination = model_reads::destination(model)?; - let database: Option = destination.getattr("database")?.extract()?; - let schema: String = destination.getattr("schema")?.str()?.extract()?; + let database: Option = native_field(&destination, "database")?.extract()?; + let schema: String = native_field(&destination, "schema")?.str()?.extract()?; scopes.insert((database.clone().unwrap_or_default(), schema, database)); } let mut entries: Vec<_> = Vec::new(); @@ -502,21 +508,21 @@ fn bigquery_retention<'py>( let mut sources: BTreeSet = BTreeSet::new(); let mut names: Vec<_> = Vec::new(); for item in &scope_models { - let retention = item.getattr("config")?.getattr("time_travel_retention")?; - let desired = retention.getattr("desired_days")?; + let retention = native_field(item, "config")?.getattr("time_travel_retention")?; + let desired = native_field(&retention, "desired_days")?; details.push(format!( "{}={}d", - item.getattr("name")?.str()?, + native_field(item, "name")?.str()?, desired.str()? )); if !desired.is_none() { desired_values.insert(desired.extract()?); } - let source = retention.getattr("source")?; + let source = native_field(&retention, "source")?; if !source.is_none() { - sources.insert(source.getattr("value")?.extract()?); + sources.insert(native_field(&source, "value")?.extract()?); } - names.push(item.getattr("name")?.extract::()?); + names.push(native_field(item, "name")?.extract::()?); } let prefix = database .as_ref() @@ -553,7 +559,7 @@ fn bigquery_retention<'py>( state: Some(state.clone()), source: text(py, &sources.into_iter().collect::>().join(",")), }; - if !state.getattr("exists")?.is_truthy()? { + if !native_field(&state, "exists")?.is_truthy()? { let changes = render_changes(runtime, &request, None)?; entries.push(entry( &base, @@ -591,9 +597,9 @@ 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 = model.getattr("config")?.getattr("time_travel_retention")?; - if !retention.getattr("unmanaged")?.is_truthy()? - && !retention.getattr("desired_days")?.is_none() + let retention = native_field(&model, "config")?.getattr("time_travel_retention")?; + if !native_field(&retention, "unmanaged")?.is_truthy()? + && !native_field(&retention, "desired_days")?.is_none() { models.push(model); } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/scopes.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/scopes.rs index 0fd177e226..179955248f 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/scopes.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_phases/scopes.rs @@ -7,6 +7,7 @@ use crate::bindings::_helpers::planner_phases::models::PlannerScopeResolution; use crate::bindings::_helpers::planner_phases::objects::object; use crate::bindings::_helpers::planner_phases::objects::{replace, tuple}; use crate::bindings::_helpers::planner_scope::scope::build_planner_scope; +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; use pyo3::pyfunction; use pyo3::types::{PyDict, PyDictMethods, PyTuple}; @@ -19,24 +20,24 @@ pub(crate) fn resolve_planner_scopes<'py>( policies: &Bound<'py, PyAny>, ) -> PyResult> { let py = project.py(); - let auto_load = policies.getattr("auto_load_sources")?; + let auto_load = native_field(policies, "auto_load_sources")?; let selected = build_planner_scope( py, Some(keywords( py, &[ ("project", project.clone()), - ("select", selection.getattr("select")?), - ("exclude", selection.getattr("exclude")?), + ("select", native_field(selection, "select")?), + ("exclude", native_field(selection, "exclude")?), ("auto_load_sources", auto_load.clone()), - ("selected_keys", selection.getattr("selected_keys")?), + ("selected_keys", native_field(selection, "selected_keys")?), ( "python_read_source_names", - selection.getattr("python_read_source_names")?, + native_field(selection, "python_read_source_names")?, ), ( "sql_test_selection", - selection.getattr("sql_test_selection")?, + native_field(selection, "sql_test_selection")?, ), ], )?), @@ -57,26 +58,32 @@ pub(crate) fn resolve_planner_scopes<'py>( let stale_keys = object( py, transitive_closure_many( - &selected.getattr("selected_keys")?, - &full.getattr("upstream_deps")?, + &native_field(&selected, "selected_keys")?, + &native_field(&full, "upstream_deps")?, true, None, )?, )?; let all_keys = PyDict::new(py); - for (name, key) in full.getattr("all_keys")?.downcast::()?.iter() { + for (name, key) in native_field(&full, "all_keys")? + .downcast::()? + .iter() + { if stale_keys.contains(&key)? { all_keys.set_item(name, key)?; } } let models = PyDict::new(py); - for (name, model) in full.getattr("models_by_name")?.downcast::()?.iter() { + for (name, model) in native_field(&full, "models_by_name")? + .downcast::()? + .iter() + { if stale_keys.contains(model_reads::model_key(&model)?)? { models.set_item(name, model)?; } } let mut order: Vec<_> = Vec::new(); - for key in full.getattr("execution_order")?.try_iter()? { + for key in native_field(&full, "execution_order")?.try_iter()? { let key = key?; if stale_keys.contains(&key)? { order.push(key); @@ -87,11 +94,11 @@ pub(crate) fn resolve_planner_scopes<'py>( &[ ("all_keys", all_keys.into_any()), ("models_by_name", models.into_any()), - ("selected_keys", selected.getattr("selected_keys")?), + ("selected_keys", native_field(&selected, "selected_keys")?), ("execution_order", tuple(py, order)?), ( "user_selected_keys", - selected.getattr("user_selected_keys")?, + native_field(&selected, "user_selected_keys")?, ), ], )?; 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 47802ad3e2..9784b71f37 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 @@ -16,6 +16,7 @@ use crate::bindings::_helpers::planner_phases::objects::native; use crate::bindings::_helpers::planner_phases::runtime::runtime_field; use crate::bindings::_helpers::planner_warehouse::fingerprint_cache::MigrationFingerprintCache; use crate::bindings::_helpers::planner_warehouse::snapshot::{SnapshotRequest, warehouse_snapshot}; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use pyo3::prelude::{Bound, Py, PyAny, PyAnyMethods, PyResult}; use pyo3::pyfunction; @@ -43,22 +44,25 @@ pub(crate) fn gather_planner_warehouse_state<'py>( let project = runtime_field(runtime, |runtime| &runtime.project)?; let fingerprints = Py::new( py, - MigrationFingerprintCache::open(py, Some(project.getattr("compile_cache_dir")?))?, + MigrationFingerprintCache::open(py, Some(native_field(&project, "compile_cache_dir")?))?, )? .into_bound(py) .into_any(); progress(runtime, "Inspecting warehouse state...")?; let selected_scope = class_field(scopes, |s: &PlannerScopeResolution| &s.selected_scope)?; - let full_refresh: bool = overrides.getattr("full_refresh")?.extract()?; + let full_refresh: bool = native_field(overrides, "full_refresh")?.extract()?; let config = runtime_field(runtime, |runtime| &runtime.project_config)?; let cursor_scope = native( py, RecordId::CursorSnapshotScope, &[ - ("model_keys", selected_scope.getattr("selected_keys")?), + ( + "model_keys", + native_field(&selected_scope, "selected_keys")?, + ), ( "runtime_producer_keys", - selected_scope.getattr("selected_keys")?, + native_field(&selected_scope, "selected_keys")?, ), ( "invocation_time", @@ -69,10 +73,13 @@ pub(crate) fn gather_planner_warehouse_state<'py>( if config.is_none() { py.None().into_bound(py) } else { - config.getattr("cursors")?.getattr("start")? + native_field(&config, "cursors")?.getattr("start")? }, ), - ("cursor_overrides", overrides.getattr("cursor_overrides")?), + ( + "cursor_overrides", + native_field(overrides, "cursor_overrides")?, + ), ], )?; let adapter = runtime_field(runtime, |runtime| &runtime.adapter)?; @@ -89,12 +96,12 @@ pub(crate) fn gather_planner_warehouse_state<'py>( project: project.clone(), adapter: adapter.clone(), connection: runtime_field(runtime, |runtime| &runtime.connection)?, - execute: adapter.getattr("execute")?, + execute: native_field(&adapter, "execute")?, selected_keys: stale_keys, full_refresh: false, full_refresh_model_names: effectively_full_refreshed_model_names(&project, full_refresh)?, on_progress: runtime_field(runtime, |runtime| &runtime.on_progress)?, - deferred_locations: deferral.getattr("deferred_locations")?, + deferred_locations: native_field(deferral, "deferred_locations")?, cursor_scope, })?; let phase = MigrationPhase { @@ -124,14 +131,14 @@ pub(crate) fn gather_planner_warehouse_state<'py>( .getattr("moves_data")? .is_truthy()? { - let origin = entry.getattr("origin")?; - let qualified = origin.getattr("qualified_name")?; + let origin = native_field(entry, "origin")?; + let qualified = native_field(&origin, "qualified_name")?; physical.set_item( - entry.getattr("model_name")?, + native_field(entry, "model_name")?, if qualified.is_truthy()? { qualified } else { - origin.getattr("name")? + native_field(&origin, "name")? }, )?; } @@ -147,7 +154,7 @@ pub(crate) fn gather_planner_warehouse_state<'py>( full_refresh, )?, physical_relations: physical.into_any(), - source_columns: inspection_relations.getattr("source_warehouse_columns")?, + source_columns: native_field(&inspection_relations, "source_warehouse_columns")?, }, )?; let migration_entries = PyTuple::new(py, &migrations.entries)?.into_any(); 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 d0ef32f341..f7ebb34e6a 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 @@ -8,6 +8,7 @@ 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::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}; @@ -47,7 +48,7 @@ fn read_snapshot<'py>( object: &Bound<'py, PyAny>, ) -> PyResult { let mut end_inputs: Vec<(Option, Option)> = Vec::new(); - for pair in object.getattr("upstream_end_inputs")?.try_iter()? { + for pair in native_field(object, "upstream_end_inputs")?.try_iter()? { let pair = pair?; end_inputs.push(( optional_scalar(session, &pair.get_item(0)?)?, @@ -55,13 +56,13 @@ fn read_snapshot<'py>( )); } Ok(CursorSnapshot { - target_max: optional_scalar(session, &object.getattr("target_max")?)?, + target_max: optional_scalar(session, &native_field(object, "target_max")?)?, upstream_mins: scalars(session, object, "upstream_mins")?, upstream_maxes: scalars(session, object, "upstream_maxes")?, terminal_starts: scalars(session, object, "upstream_terminal_starts")?, terminal_ends: scalars(session, object, "upstream_terminal_ends")?, availability_ends: scalars(session, object, "upstream_availability_ends")?, - any_input: object.getattr("cursor_watermark_mode")?.eq("any")?, + any_input: native_field(object, "cursor_watermark_mode")?.eq("any")?, end_inputs, }) } @@ -198,13 +199,13 @@ pub(crate) fn compute_cursor_bounds<'py>( return call_with( &bounds_class, &[ - ("start", sentinel.getattr("START")?), - ("end", sentinel.getattr("END")?), + ("start", native_field(&sentinel, "START")?), + ("end", native_field(&sentinel, "END")?), ], ); } let snapshot = args.value("cursor_snapshot")?; - if !snapshot.getattr("watermarks_available")?.is_truthy()? { + if !native_field(&snapshot, "watermarks_available")?.is_truthy()? { return Ok(py.None().into_bound(py)); } let session = CursorSession::new(py)?; 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 897166f1a8..7326f8870f 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,13 +27,14 @@ 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::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, PyResult, Python}; use pyo3::{IntoPyObject, PyErr}; fn config<'py>(model: &Bound<'py, PyAny>, key: &str) -> PyResult> { - let values: Bound<'py, PyAny> = model.getattr("config")?.getattr("values")?; + let values: Bound<'py, PyAny> = native_field(model, "config")?.getattr("values")?; let value: Option> = if key == CURSOR_START { config_cursor_bound(&values, key)? } else { @@ -47,8 +48,8 @@ fn unbounded(py: Python<'_>) -> PyResult> { call_with( ®istry::class(py, RecordId::CursorBounds)?, &[ - ("start", sentinel.getattr("START")?), - ("end", sentinel.getattr("END")?), + ("start", native_field(&sentinel, "START")?), + ("end", native_field(&sentinel, "END")?), ], ) } @@ -64,18 +65,21 @@ fn destination_exists(model: &Bound<'_, PyAny>, snapshot: &Bound<'_, PyAny>) -> let key = call_with( &host::entry(model.py(), AdapterHost::RelationLookup)?.getattr("key")?, &[ - ("database", destination.getattr("database")?), - ("schema", destination.getattr("schema")?), - ("name", destination.getattr("name")?), + ("database", native_field(&destination, "database")?), + ("schema", native_field(&destination, "schema")?), + ("name", native_field(&destination, "name")?), ], )?; - relation.getattr("identity")?.eq(key) + native_field(&relation, "identity")?.eq(key) } fn history_redirected(model: &Bound<'_, PyAny>, snapshot: &Bound<'_, PyAny>) -> PyResult { - let target = snapshot.getattr("target_relation")?; + let target = native_field(snapshot, "target_relation")?; Ok(!target.is_none() - && target.ne(model_reads::destination(model)?.getattr("qualified_name")?)?) + && target.ne(native_field( + &model_reads::destination(model)?, + "qualified_name", + )?)?) } /// One model's cursor bounds from its snapshot, overrides and cursor policies; `None` when unbounded by a full refresh. @@ -124,7 +128,7 @@ pub(crate) fn compute_model_cursor_bounds<'py>( &c.runtime_cursor_producer_names })?; let mut runtime_owned = false; - for name in roles.getattr("watermark_inputs")?.try_iter()? { + for name in native_field(&roles, "watermark_inputs")?.try_iter()? { if producers.contains(name?)? { runtime_owned = true; break; @@ -133,7 +137,10 @@ pub(crate) fn compute_model_cursor_bounds<'py>( let full_refresh = request.full_refresh; let replaces = request.replaces_relation; if !full_refresh && !runtime_owned && !cursor_snapshot.is_none() { - let empty = cursor_inputs_without_rows(&cursor_snapshot)?.getattr("input_names")?; + let empty = native_field( + &cursor_inputs_without_rows(&cursor_snapshot)?, + "input_names", + )?; if empty.is_truthy()? && replaces && (destination_exists(&model, snapshot)? @@ -163,11 +170,11 @@ pub(crate) fn compute_model_cursor_bounds<'py>( .getattr("watermarks_available")? .is_truthy()? { - let unreadable = cursor_snapshot.getattr("unreadable_input_names")?; + let unreadable = native_field(&cursor_snapshot, "unreadable_input_names")?; let names = if unreadable.is_truthy()? { unreadable } else { - cursor_snapshot.getattr("unavailable_watermark_tags")? + native_field(&cursor_snapshot, "unavailable_watermark_tags")? }; return Err(PyErr::from_value(unreadable_cursor_inputs_error( py, @@ -190,8 +197,8 @@ pub(crate) fn compute_model_cursor_bounds<'py>( cursor_snapshot = without_destination_cursor(&cursor_snapshot)?; } let backfill = &request.backfill; - let duration = if backfill.getattr("action")?.eq("bounded")? { - backfill.getattr("duration")? + let duration = if native_field(backfill, "action")?.eq("bounded")? { + native_field(backfill, "duration")? } else { py.None().into_bound(py) }; @@ -254,7 +261,10 @@ pub(crate) fn compute_model_cursor_bounds<'py>( .to_owned() .into_any(), ), - ("input_evidence", cursor_snapshot.getattr("input_evidence")?), + ( + "input_evidence", + native_field(&cursor_snapshot, "input_evidence")?, + ), ], )?), ) 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 90173837c6..585e1fd5c5 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; use pyo3::prelude::{ @@ -41,12 +42,12 @@ pub(crate) fn type_cursor_overrides<'py>( selected: &[Bound<'py, PyAny>], ) -> PyResult> { let py = overrides.py(); - if overrides.is_none() || !overrides.getattr("has_untyped")?.is_truthy()? { + if overrides.is_none() || !native_field(overrides, "has_untyped")?.is_truthy()? { return Ok(overrides.clone()); } let mut models: Vec<(String, String)> = Vec::new(); for model in selected { - let values = model.getattr("config")?.getattr("values")?; + let values = native_field(model, "config")?.getattr("values")?; let config = |key: &str| -> PyResult> { Ok(authored_config_str(&values, key)?.unwrap_or_else(|| py.None().into_bound(py))) }; 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 c066e4a339..4756bff2a7 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,6 +1,7 @@ //! Model-level cursor safety policy overrides over project configuration. 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::RecordId; use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult}; @@ -43,7 +44,7 @@ fn resolve_policy<'py>( } else { class.call0()? }; - let values = model.getattr("config")?.getattr("values")?; + let values = native_field(model, "config")?.getattr("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)?; @@ -54,7 +55,7 @@ fn resolve_policy<'py>( raw_distance }; } - let mut action = configured.getattr("action")?; + let mut action = native_field(&configured, "action")?; if raw_action.is_instance_of::() { action = registry::class(py, RecordId::FutureCursorAction)?.call1((raw_action,))?; } 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 e765b7856f..318f829f48 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::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}; @@ -81,7 +82,7 @@ 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(&model.getattr("config")?.getattr("values")?, key)? + authored_config_str(&native_field(model, "config")?.getattr("values")?, key)? .unwrap_or_else(|| model.py().None().into_bound(model.py())), ) } @@ -93,7 +94,7 @@ pub(crate) fn config_bound<'py>( ) -> PyResult> { object( model.py(), - config_cursor_bound(&model.getattr("config")?.getattr("values")?, key)?, + config_cursor_bound(&native_field(model, "config")?.getattr("values")?, key)?, ) } @@ -145,8 +146,8 @@ fn clamp_cursor_end<'py>( if bounds.is_none() || cursor_end.is_none() { return Ok(bounds); } - let start = bounds.getattr("start")?; - let end = bounds.getattr("end")?; + let start = native_field(&bounds, "start")?; + let end = native_field(&bounds, "end")?; if start.eq(sentinel(py, "START")?)? || end.eq(sentinel(py, "END")?)? { return Ok(bounds); } @@ -202,8 +203,8 @@ fn rolling_window_bounds<'py>( } else { invocation_time.clone() }; - let moment = if moment.getattr("tzinfo")?.is_none() { - call_with(&moment.getattr("replace")?, &[("tzinfo", utc)])? + let moment = if native_field(&moment, "tzinfo")?.is_none() { + call_with(&native_field(&moment, "replace")?, &[("tzinfo", utc)])? } else { utc_clock(&moment)? }; @@ -214,7 +215,7 @@ fn rolling_window_bounds<'py>( let clock = Scalar { value: ScalarValue::Timestamp(Timestamp { local: local_date_time(&moment)?, - fold: moment.getattr("fold")?.extract::()? != 0, + fold: native_field(&moment, "fold")?.extract::()? != 0, offset: ZoneOffset::Fixed(0), template: None, source_text: None, @@ -234,7 +235,7 @@ fn rolling_window_bounds<'py>( if !cursor_start.is_none() && order( &session, - (&token(py, &bounds.getattr("start")?)?, &cursor_start), + (&token(py, &native_field(&bounds, "start")?)?, &cursor_start), ×tamp, )? == Ordering::Less { @@ -250,7 +251,7 @@ fn apply_future_safety<'py>( input_evidence: Bound<'py, PyAny>, ) -> PyResult> { let py = bounds.py(); - if bounds.is_none() || bounds.getattr("end")?.eq(sentinel(py, "END")?)? { + if bounds.is_none() || native_field(&bounds, "end")?.eq(sentinel(py, "END")?)? { return Ok(bounds); } resolve_future_cursor_safety( @@ -292,8 +293,8 @@ fn bounded_override<'py>( } fn backfill_duration<'py>(backfill: &Bound<'py, PyAny>) -> PyResult> { - if backfill.getattr("action")?.eq(BOUNDED_BACKFILL)? { - return backfill.getattr("duration"); + if native_field(backfill, "action")?.eq(BOUNDED_BACKFILL)? { + return native_field(backfill, "duration"); } Ok(backfill.py().None().into_bound(backfill.py())) } @@ -346,7 +347,7 @@ fn snapshot_bounds<'py>( bounds, (&request.typing.0, &request.typing.1), inputs, - request.cursor_snapshot.getattr("input_evidence")?, + native_field(&request.cursor_snapshot, "input_evidence")?, )?; clamp_cursor_end(bounds, model) } @@ -386,7 +387,7 @@ pub(crate) fn plan_cursor_bounds<'py>( if cursor_snapshot.is_none() { return Ok(cursor_snapshot); } - if inputs.backfill.getattr("action")?.eq(FULL_BACKFILL)? { + if native_field(&inputs.backfill, "action")?.eq(FULL_BACKFILL)? { cursor_snapshot = without_destination(cursor_snapshot)?; } snapshot_bounds( @@ -444,7 +445,7 @@ pub(crate) fn microbatch_range<'py>( let cursor_type = config_str(model, "cursor_type")?; let mut grains: Vec> = Vec::new(); for relation in inputs.cursor_input_relations.try_iter()? { - grains.push(relation?.getattr("cursor_grain")?); + grains.push(native_field(&relation?, "cursor_grain")?); } let effective_grain = resolve_effective_timestamp_grain( &cursor_type, @@ -495,8 +496,8 @@ pub(crate) fn empty_cursor_inputs<'py>( } let decision = cursor_inputs_without_rows(&cursor_snapshot)?; Ok(( - decision.getattr("input_names")?, - decision.getattr("waiting_on_empty_inputs")?.is_truthy()?, + native_field(&decision, "input_names")?, + native_field(&decision, "waiting_on_empty_inputs")?.is_truthy()?, )) } 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 c1259754d4..cde6947fd6 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,6 +3,7 @@ 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::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, PyErr, PyResult, Python}; @@ -63,9 +64,13 @@ pub(super) fn horizon<'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 || discovered.getattr("value")?.getattr("tzinfo")?.is_none() { + if date_only + || native_field(discovered, "value")? + .getattr("tzinfo")? + .is_none() + { clock = call_with( - &clock.getattr("replace")?, + &native_field(&clock, "replace")?, &[("tzinfo", py.None().into_bound(py))], )?; } 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 05fd7b0b17..905ac9abb2 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 @@ -3,6 +3,7 @@ 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::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}; @@ -21,7 +22,7 @@ pub(crate) fn future_cap_message(bounds: &Bound<'_, PyAny>) -> PyResult) -> PyResult) -> PyResult( { return registry::class(cursor_snapshot.py(), RecordId::EmptyCursorInputDecision)?.call0(); } - let rows = cursor_snapshot.getattr("upstream_maxes")?.is_truthy()? + 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()?; - let start = !cursor_snapshot.getattr("target_max")?.is_none() - || cursor_snapshot.getattr("upstream_mins")?.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()?; decide_empty_cursor_inputs( - &cursor_snapshot.getattr("empty_input_names")?, + &native_field(cursor_snapshot, "empty_input_names")?, !cursor_snapshot .getattr("cursor_watermark_mode")? .eq("any")?, @@ -95,7 +96,7 @@ pub(crate) fn empty_cursor_window<'py>( if cursor_start.is_none() && !kind.eq("integer")? { bound = call_with( ®istry::class(py, RecordId::TimestampValue)?, - &[("value", bound.getattr("value")?)], + &[("value", native_field(&bound, "value")?)], )?; } call_with( @@ -249,17 +250,17 @@ pub(crate) fn normalize_cursor_snapshot_grain<'py>( .collect::>>()?; kwargs.set_item(key, PyTuple::new(py, values)?)?; } - let physical = cursor_snapshot.getattr("physical_target_max")?; + let physical = native_field(cursor_snapshot, "physical_target_max")?; kwargs.set_item( "physical_target_max", if physical.is_truthy()? { physical } else { - cursor_snapshot.getattr("target_max")? + native_field(cursor_snapshot, "target_max")? }, )?; let mut ends: Vec<(Bound<'py, PyAny>, Bound<'py, PyAny>)> = Vec::new(); - for pair in cursor_snapshot.getattr("upstream_end_inputs")?.try_iter()? { + for pair in native_field(cursor_snapshot, "upstream_end_inputs")?.try_iter()? { let pair = pair?; ends.push((normalize(pair.get_item(0)?)?, pair.get_item(1)?)); } 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 67d3aa58c6..9a1d12aa84 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,6 +5,7 @@ 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::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}; @@ -42,17 +43,17 @@ pub(crate) fn resolve_future_cursor_safety<'py>( let config = args.value("config")?; let clock = args.value("invocation_time")?; if config.is_none() - || config.getattr("max_distance")?.is_none() + || native_field(&config, "max_distance")?.is_none() || !args.value("cursor_type")?.eq("timestamp")? || clock.is_none() || args.flag("has_complete_override")? { return Ok(bounds); } - let distance = config.getattr("max_distance")?; + let distance = native_field(&config, "max_distance")?; let duration = safety_duration(&distance, true)?; - let start = bounds.getattr("start")?; - let end = bounds.getattr("end")?; + let start = native_field(&bounds, "start")?; + let end = native_field(&bounds, "end")?; if !timestamp_bound(&start)? || !timestamp_bound(&end)? { return Ok(bounds); } @@ -83,7 +84,7 @@ pub(crate) fn resolve_future_cursor_safety<'py>( let mut determining: Option<(Bound<'py, PyAny>, Scalar)> = None; for item in evidence.try_iter()? { let item = item?; - let maximum = session.scalar(&item.getattr("maximum")?)?; + let maximum = session.scalar(&native_field(&item, "maximum")?)?; let smaller = if let Some((_, previous)) = &determining { session.checked(compare(&maximum, previous, &session))? == Ordering::Less } else { @@ -93,7 +94,7 @@ pub(crate) fn resolve_future_cursor_safety<'py>( determining = Some((item, maximum)); } } - if config.getattr("action")?.eq("error")? { + if native_field(&config, "action")?.eq("error")? { return Err(safety_error( py, ErrorId::FutureCursorSafety, @@ -122,7 +123,7 @@ pub(crate) fn resolve_future_cursor_safety<'py>( let safety = call_with( ®istry::class(py, RecordId::FutureCursorSafetyEvidence)?, &[ - ("action", config.getattr("action")?), + ("action", native_field(&config, "action")?), ("max_distance", distance), ( "invocation_time", diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/locations.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/locations.rs index 40c12926b4..57b0a41787 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/locations.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/locations.rs @@ -1,5 +1,6 @@ //! Location maps retaining the original compiled Python destination objects. +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult}; use pyo3::types::{PyDict, PyDictMethods}; use pyo3::{pyfunction, wrap_pyfunction}; @@ -8,7 +9,10 @@ fn locations<'py>(items: &Bound<'py, PyAny>) -> PyResult> { let result = PyDict::new(items.py()); for item in items.try_iter()? { let item = item?; - result.set_item(item.getattr("name")?, item.getattr("destination")?)?; + result.set_item( + native_field(&item, "name")?, + native_field(&item, "destination")?, + )?; } Ok(result) } 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 7b2321512b..b376d39a61 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; use pyo3::prelude::{ @@ -93,17 +94,17 @@ pub(crate) fn apply_maximum_start_policy<'py>( )?; let bounds = args.value("bounds")?; let snapshot = args.value("snapshot")?; - let physical = snapshot.getattr("physical_target_max")?; + let physical = native_field(&snapshot, "physical_target_max")?; let physical = if physical.is_truthy()? { physical } else { - snapshot.getattr("target_max")? + native_field(&snapshot, "target_max")? }; let policy = args.value("policy")?; - let config = policy.getattr("config")?; - let clock = policy.getattr("invocation_time")?; + let config = native_field(&policy, "config")?; + let clock = native_field(&policy, "invocation_time")?; if config.is_none() - || config.getattr("max_ahead")?.is_none() + || native_field(&config, "max_ahead")?.is_none() || !args.value("cursor_type")?.eq("timestamp")? || clock.is_none() || args.flag("has_start_override")? @@ -111,10 +112,12 @@ pub(crate) fn apply_maximum_start_policy<'py>( { return Ok(bounds); } - if !timestamp_bound(&bounds.getattr("start")?)? || !timestamp_bound(&bounds.getattr("end")?)? { + if !timestamp_bound(&native_field(&bounds, "start")?)? + || !timestamp_bound(&native_field(&bounds, "end")?)? + { return Ok(bounds); } - let duration = safety_duration(&config.getattr("max_ahead")?, false)?; + let duration = safety_duration(&native_field(&config, "max_ahead")?, false)?; if !timestamp_bound(&physical)? { return Err(safety_error( py, @@ -133,7 +136,7 @@ pub(crate) fn apply_maximum_start_policy<'py>( if session.checked(compare(&physical_typed, &horizon, &session))? != Ordering::Greater { return Ok(bounds); } - let target = optional_scalar(&session, &snapshot.getattr("target_eligible_max")?)?; + let target = optional_scalar(&session, &native_field(&snapshot, "target_eligible_max")?)?; let upstream = if target.is_none() { scalars(&session, &snapshot, "upstream_mins")? } else { @@ -161,7 +164,7 @@ pub(crate) fn apply_maximum_start_policy<'py>( let effective = if let Some(eligible) = &eligible { let mut replay = vec![ ("start", session.python_scalar(eligible)?), - ("end", bounds.getattr("end")?), + ("end", native_field(&bounds, "end")?), ( "has_start_override", false.into_pyobject(py)?.to_owned().into_any(), @@ -177,15 +180,15 @@ pub(crate) fn apply_maximum_start_policy<'py>( } apply_typed_cursor_replay_policy(py, Some(keywords(py, &replay)?))? } else { - bounds.getattr("start")? + native_field(&bounds, "start")? }; - let relation = snapshot.getattr("target_relation")?; - let column = snapshot.getattr("destination_cursor_column")?; + let relation = native_field(&snapshot, "target_relation")?; + let column = native_field(&snapshot, "destination_cursor_column")?; let evidence = call_with( ®istry::class(py, RecordId::MaximumStartSafetyEvidence)?, &[ - ("action", config.getattr("action")?), - ("max_ahead", config.getattr("max_ahead")?), + ("action", native_field(&config, "action")?), + ("max_ahead", native_field(&config, "max_ahead")?), ( "invocation_time", call_with( @@ -196,7 +199,7 @@ pub(crate) fn apply_maximum_start_policy<'py>( ("physical_target_max", physical), ( "highest_eligible_target_max", - snapshot.getattr("target_eligible_max")?, + native_field(&snapshot, "target_eligible_max")?, ), ("effective_start", effective.clone()), ("maximum_allowed_start", session.python_scalar(&horizon)?), @@ -230,14 +233,14 @@ pub(crate) fn apply_maximum_start_policy<'py>( ], )?)) }; - if config.getattr("action")?.eq("error")? + if native_field(&config, "action")?.eq("error")? || eligible.is_none() || session.checked(compare(&session.scalar(&effective)?, &horizon, &session))? == Ordering::Greater { return error(false); } - let strategy = policy.getattr("incremental_strategy")?; + let strategy = native_field(&policy, "incremental_strategy")?; if !strategy.eq("delete_insert")? && !strategy.eq("merge")? { return error(true); } @@ -258,7 +261,7 @@ fn exceeded_message(evidence: &Bound<'_, PyAny>, non_idempotent: bool) -> PyResu } else { "" }; - let highest = evidence.getattr("highest_eligible_target_max")?; + let highest = native_field(evidence, "highest_eligible_target_max")?; let highest: String = if highest.is_none() { "None".to_owned() } else { @@ -268,12 +271,12 @@ fn exceeded_message(evidence: &Bound<'_, PyAny>, non_idempotent: bool) -> PyResu }; Ok(format!( "maximum automatic start policy exceeded{detail}: physical target MAX='{}', highest eligible MAX={highest}, effective post-lookback start='{}', horizon='{}', action={}, input={}.{}", - rendered(&evidence.getattr("physical_target_max")?)?, - rendered(&evidence.getattr("effective_start")?)?, - rendered(&evidence.getattr("maximum_allowed_start")?)?, - evidence.getattr("action")?.getattr("value")?.str()?, - evidence.getattr("target_relation")?.str()?, - evidence.getattr("cursor_column")?.str()?, + 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(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 f1860f511b..ab2abd45d0 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/orchestration.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/orchestration.rs @@ -18,6 +18,7 @@ use crate::bindings::_helpers::planner_resolution::refs::{ resolve_udf_references, }; use crate::bindings::_helpers::planner_resolution::sources::resolve_sources; +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}; @@ -109,7 +110,7 @@ impl<'py> Resolution<'py> { let kwargs = PyDict::new(py); kwargs.set_item( "statement", - self.unbounded(value.getattr("statement")?, label)?, + self.unbounded(native_field(&value, "statement")?, label)?, )?; return imported(py, "dataclasses", "replace")?.call((value,), Some(&kwargs)); } @@ -170,8 +171,8 @@ fn resolve_model_sql_of<'py>( backfill: args.value("backfill")?, full_refresh: args.flag("full_refresh")?, cursor_overrides: ( - overrides.getattr("start_cursor_override")?, - overrides.getattr("end_cursor_override")?, + native_field(&overrides, "start_cursor_override")?, + native_field(&overrides, "end_cursor_override")?, ), replaces_relation: args.flag("replaces_relation")?, full_rebuild_cause: if cause.is_truthy()? { @@ -192,14 +193,16 @@ pub(crate) fn resolve_model_sql<'py>( ) -> PyResult { let model = &request.model; let py = model.py(); - let cursor_type = - authored_config_str(&model.getattr("config")?.getattr("values")?, "cursor_type")? - .unwrap_or_else(|| py.None().into_bound(py)); + let cursor_type = authored_config_str( + &native_field(model, "config")?.getattr("values")?, + "cursor_type", + )? + .unwrap_or_else(|| py.None().into_bound(py)); let bounds = compute_model_cursor_bounds(request, context)?; let roles = resolve_cursor_input_roles(model)?; let filtering = CursorFiltering { bounds: bounds.clone(), - filter_inputs: roles.getattr("filter_inputs")?, + filter_inputs: native_field(&roles, "filter_inputs")?, cursor_type: cursor_type.clone(), lower_bound_inclusive: append_cursor_inclusive(model)?, }; @@ -228,7 +231,7 @@ pub(crate) fn resolve_model_sql<'py>( fn append_cursor_inclusive(model: &Bound<'_, PyAny>) -> PyResult { let py = model.py(); let strategy = authored_config_str( - &model.getattr("config")?.getattr("values")?, + &native_field(model, "config")?.getattr("values")?, "incremental_strategy", )? .unwrap_or_else(|| py.None().into_bound(py)); @@ -249,7 +252,7 @@ fn intrinsic_bounds<'py>( let py = adapter.py(); let literal = |field: &str| -> PyResult { call_with( - &adapter.getattr("render_cursor_bound_literal")?, + &native_field(adapter, "render_cursor_bound_literal")?, &[ ( "value", @@ -313,9 +316,11 @@ pub(crate) fn resolve_function_sql<'py>( function_locations: args.value("function_locations")?, }; let function = args.value("function")?; - let name: String = function.getattr("name")?.extract()?; - let sql: String = - resolution.unbounded(function.getattr("body_sql")?, &format!("Function '{name}'"))?; + let name: String = native_field(&function, "name")?.extract()?; + let sql: String = resolution.unbounded( + native_field(&function, "body_sql")?, + &format!("Function '{name}'"), + )?; Ok(PyString::new(py, &sql).into_any()) } @@ -336,7 +341,7 @@ pub(crate) fn model_hook_entries<'py>( model: &Bound<'py, PyAny>, resolution: &Resolution<'py>, ) -> PyResult> { - let values = model.getattr("config")?.getattr("values")?; + let values = native_field(model, "config")?.getattr("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/refs.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/refs.rs index bf84bd6ba9..e8e83c03dd 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/refs.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/refs.rs @@ -7,6 +7,7 @@ use crate::bindings::_helpers::planner_resolution::models::CursorFiltering; use crate::bindings::_helpers::planner_resolution::values::{ cursor_wrap, filtering_of, method, string, }; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::sqltext::code_scanner::SqlCodeScanner; use pyo3::IntoPyObject; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; @@ -157,7 +158,7 @@ fn resolve_function_calls( let qualified = if target.is_none() { py.None().into_bound(py) } else { - target.getattr("qualified_name")? + native_field(&target, "qualified_name")? }; let suffix = span.end + query_sql[span.end..].len() - query_sql[span.end..].trim_start().len(); @@ -235,7 +236,7 @@ pub(crate) fn apply_deferred_locations<'py>( ) -> PyResult<(Bound<'py, PyDict>, Bound<'py, PyDict>)> { let selected = selected_keys .try_iter()? - .map(|key| key?.getattr("name")?.extract::()) + .map(|key| native_field(&key?, "name")?.extract::()) .collect::>>()?; let models = model_locations.copy()?; let seeds = seed_locations.copy()?; 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 4b0f31535e..4d3d5f2379 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/sources.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_resolution/sources.rs @@ -6,6 +6,7 @@ use crate::bindings::_helpers::planner_resolution::values::{ cursor_wrap, filtering_of, method, string, }; use crate::bindings::_helpers::planner_strategy::values::input_error; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::ErrorId; use crate::bindings::_helpers::references::source_relations::render_source_relation; use crate::bindings::_helpers::type_system::equality::types_equal; @@ -29,8 +30,8 @@ fn rows(value: &Bound<'_, PyAny>) -> PyResult)>> { .map(|row| { let row = row?; Ok(( - row.getattr("name")?.extract()?, - row.getattr("type")?.extract()?, + native_field(&row, "name")?.extract()?, + native_field(&row, "type")?.extract()?, )) }) .collect() @@ -43,7 +44,7 @@ fn check_columns( relation: &str, ) -> PyResult<()> { let py = entry.py(); - let declared = rows(&entry.getattr("columns")?)?; + let declared = rows(&native_field(entry, "columns")?)?; let available = rows(columns)?; let available: std::collections::HashMap> = available .into_iter() @@ -83,7 +84,7 @@ fn check_columns( continue; } let equal: bool = types_equal( - &adapter.getattr("sql_analysis_dialect_name")?, + &native_field(adapter, "sql_analysis_dialect_name")?, actual, &kind, )?; @@ -130,13 +131,13 @@ fn cast_source( adapter: &Bound<'_, PyAny>, relation: String, ) -> PyResult { - let declared = rows(&entry.getattr("columns")?)?; + let declared = rows(&native_field(entry, "columns")?)?; if !declared.iter().any(|(_, kind)| kind.is_some()) { return Ok(relation); } let py = entry.py(); - let expression = !entry.getattr("expression")?.is_none(); - let source_name: String = entry.getattr("name")?.extract()?; + let expression = !native_field(entry, "expression")?.is_none(); + let source_name: String = native_field(entry, "name")?.extract()?; if columns.is_none() { if expression { return Err(source_error( @@ -261,7 +262,7 @@ fn resolve_one( let adapter = adapter.clone(); let mut relation = render_source_read_relation(&adapter, &entry)?; let columns = warehouse_columns.call_method1("get", (name,))?; - let expression = !entry.getattr("expression")?.is_none(); + let expression = !native_field(&entry, "expression")?.is_none(); if columns.is_truthy()? && (!expression || entry @@ -277,7 +278,7 @@ fn resolve_one( }; check_columns(&entry, &columns, &adapter, &label)?; } - if entry.getattr("type_enforcement")?.is_truthy()? { + if native_field(&entry, "type_enforcement")?.is_truthy()? { relation = cast_source(&entry, &columns, &adapter, relation)?; } cursor_wrap(&adapter, filtering, relation, (name, false)) 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 e0c5bbdedb..649f4bd0aa 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/artifacts.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/artifacts.rs @@ -6,6 +6,7 @@ use crate::bindings::_helpers::planner_scenario::constants::{ SCENARIO_PLAN_RELATION_COLLISION, }; use crate::bindings::_helpers::planner_scenario::values::{error, string}; +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; @@ -84,7 +85,7 @@ pub(crate) fn build_scenario_hash_index<'py>( let result = PyDict::new(py); let mut seen: std::collections::HashMap = std::collections::HashMap::new(); for scenario in scenarios.try_iter()? { - let name: String = scenario?.getattr("name")?.extract()?; + let name: String = native_field(&scenario?, "name")?.extract()?; let prefix = compute_prefix( py, (&project_name, &name, run_namespace.as_deref()), @@ -211,8 +212,8 @@ pub(crate) fn build_scenario_relation_map<'py>( let artifact = artifact?; let physical = build_scenario_artifact_name( &hash, - artifact.getattr("kind")?, - artifact.getattr("logical_name")?, + native_field(&artifact, "kind")?, + native_field(&artifact, "logical_name")?, limit, )?; let normalized = if normalize.is_none() { @@ -223,15 +224,15 @@ pub(crate) fn build_scenario_relation_map<'py>( if let Some(existing) = seen.get_item(&normalized)? { let kind = registry::class(py, RecordId::ScenarioArtifactKind)?; let old_kind: String = kind - .call1((existing.getattr("kind")?,))? + .call1((native_field(&existing, "kind")?,))? .getattr("value")? .extract()?; let new_kind: String = kind - .call1((artifact.getattr("kind")?,))? + .call1((native_field(&artifact, "kind")?,))? .getattr("value")? .extract()?; - let old_name: String = existing.getattr("logical_name")?.extract()?; - let new_name: String = artifact.getattr("logical_name")?.extract()?; + let old_name: String = native_field(&existing, "logical_name")?.extract()?; + let new_name: String = native_field(&artifact, "logical_name")?.extract()?; return Err(error( py, &format!( 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 ea4175949b..d41ffba177 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::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, PyErr, PyResult, Python, pyfunction}; @@ -51,10 +52,10 @@ pub(crate) fn build_cli_scenario_plan<'py>( py, &[ ("project_name", args.value("project_name")?), - ("scenarios", project.getattr("sql_scenarios")?), + ("scenarios", native_field(&project, "sql_scenarios")?), ( "run_namespace", - project.getattr("scenario")?.getattr("run_namespace")?, + native_field(&project, "scenario")?.getattr("run_namespace")?, ), ], )?), @@ -86,8 +87,11 @@ pub(crate) fn build_cli_scenario_plan<'py>( Some(keywords( py, &[ - ("scenario_name", scenario.getattr("name")?), - ("hash_prefix", hashes.get_item(scenario.getattr("name")?)?), + ("scenario_name", native_field(&scenario, "name")?), + ( + "hash_prefix", + hashes.get_item(native_field(&scenario, "name")?)?, + ), ("artifacts", PyTuple::new(py, identities)?.into_any()), ( "identifier_limit", @@ -101,7 +105,7 @@ pub(crate) fn build_cli_scenario_plan<'py>( project: project.clone(), graph_plan: graph.clone(), relation_map: map, - render_qualified_name: adapter.getattr("render_qualified_name")?, + render_qualified_name: native_field(&adapter, "render_qualified_name")?, database, schema, })?; @@ -132,7 +136,7 @@ fn namespace<'py>( ) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> { let py = project.py(); let models = PyDict::new(py); - for model in project.getattr("models")?.try_iter()? { + for model in native_field(project, "models")?.try_iter()? { let model = model?; models.set_item(model_reads::name_object(&model)?, model)?; } @@ -141,8 +145,8 @@ fn namespace<'py>( if let Some(model) = models.get_item(name?)? { let destination = model_reads::destination(&model)?; return Ok(( - destination.getattr("database")?, - destination.getattr("schema")?, + native_field(&destination, "database")?, + native_field(&destination, "schema")?, )); } } @@ -157,16 +161,16 @@ fn raise_for_warnings(warnings: &Bound<'_, PyAny>) -> PyResult<()> { let mut code = None; for warning in warnings.try_iter()? { let warning = warning?; - if warning.getattr("severity")?.eq(&severity)? { + if native_field(&warning, "severity")?.eq(&severity)? { if code.is_none() { - let value = warning.getattr("code")?; + let value = native_field(&warning, "code")?; code = Some(if value.is_truthy()? { value } else { string(py, SCENARIO_PLAN_GRAPH_VALIDATION) }); } - messages.push(warning.getattr("message")?.extract::()?); + messages.push(native_field(&warning, "message")?.extract::()?); } } if let Some(code) = code { 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 ee04b3e7fa..f2d801ae36 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/entries.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/entries.rs @@ -10,6 +10,7 @@ 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::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}; @@ -22,16 +23,16 @@ pub(super) fn seed_entries<'py>( relations: &Bound<'py, PyAny>, ) -> PyResult> { let py = project.py(); - let seeds = by_attribute(&project.getattr("seeds")?, "name")?; - let fixtures = graph.getattr("seed_fixture_names")?; + let seeds = by_attribute(&native_field(project, "seeds")?, "name")?; + let fixtures = native_field(graph, "seed_fixture_names")?; let mut result: Vec> = Vec::new(); - for name in graph.getattr("seed_names")?.try_iter()? { + for name in native_field(graph, "seed_names")?.try_iter()? { let name = name?; if fixtures.contains(&name)? { continue; } let Some(seed) = seeds.get_item(&name)? else { - let scenario: String = graph.getattr("name")?.extract()?; + let scenario: String = native_field(graph, "name")?.extract()?; let name: String = name.extract()?; return Err(error( py, @@ -49,17 +50,17 @@ pub(super) fn seed_entries<'py>( result.push(call_with( ®istry::class(py, RecordId::SeedPlanEntry)?, &[ - ("key", seed.getattr("key")?), - ("name", seed.getattr("name")?), + ("key", native_field(&seed, "key")?), + ("name", native_field(&seed, "name")?), ("destination", destination), ( "file_path", - seed.getattr("seed_file")?.getattr("file_path")?, + native_field(&seed, "seed_file")?.getattr("file_path")?, ), ("columns", extract_seed_columns(&seed)?.into_any()), ( "csv_settings", - seed.getattr("schema_entry")?.getattr("csv_settings")?, + native_field(&seed, "schema_entry")?.getattr("csv_settings")?, ), ], )?); @@ -83,14 +84,17 @@ pub(super) fn function_entry<'py>( ("function", function.clone()), ( "model_locations", - context.relations.getattr("model_locations")?, + native_field(&context.relations, "model_locations")?, ), ( "seed_locations", - context.relations.getattr("seed_locations")?, + native_field(&context.relations, "seed_locations")?, ), ("function_locations", locations.clone()), - ("source_map", context.relations.getattr("source_map")?), + ( + "source_map", + native_field(&context.relations, "source_map")?, + ), ("source_warehouse_columns", warehouse_columns.clone()), ( "star_exclude_keyword", 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 8c52b0241b..4b6548cff7 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/execution.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/execution.rs @@ -23,6 +23,7 @@ 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::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}; @@ -81,18 +82,18 @@ pub(crate) fn build_scenario_execution_plan<'py>( } else { analysis.is_truthy()? }; - let models = by_attribute(&project.getattr("models")?, "name")?; - let functions = by_attribute(&project.getattr("functions")?, "key")?; - let locations = build_function_locations(&project.getattr("functions")?)?.into_any(); + let models = by_attribute(&native_field(&project, "models")?, "name")?; + let functions = by_attribute(&native_field(&project, "functions")?, "key")?; + let locations = build_function_locations(&native_field(&project, "functions")?)?.into_any(); let upstream = build_execution_upstream_deps(&project)?.into_any(); let ordered = object(py, topologically_order_keys(&upstream, None)?)?; let mut model_keys: Vec> = Vec::new(); for key in ordered.try_iter()? { let key = key?; - if key.getattr("resource_type")?.eq("model")? + if native_field(&key, "resource_type")?.eq("model")? && graph .getattr("model_names")? - .contains(key.getattr("name")?)? + .contains(native_field(&key, "name")?)? { model_keys.push(key); } @@ -107,7 +108,7 @@ pub(crate) fn build_scenario_execution_plan<'py>( let mut function_entries: Vec> = Vec::new(); for key in ordered.try_iter()? { let key = key?; - if graph.getattr("function_deps")?.contains(&key)? + if native_field(&graph, "function_deps")?.contains(&key)? && let Some(function) = functions.get_item(key)? { function_entries.push(function_entry(&function, &context, &locations, &columns)?); @@ -117,7 +118,7 @@ pub(crate) fn build_scenario_execution_plan<'py>( let mut warnings: Vec> = Vec::new(); for key in model_keys { let model = models - .get_item(key.getattr("name")?)? + .get_item(native_field(&key, "name")?)? .ok_or_else(|| pyo3::exceptions::PyKeyError::new_err("scenario model"))?; let destination = required_target( &context.relations, @@ -172,8 +173,8 @@ pub(crate) fn build_scenario_execution_plan<'py>( let plan = call_with( ®istry::class(py, RecordId::ScenarioExecutionPlan)?, &[ - ("key", scenario.getattr("key")?), - ("name", scenario.getattr("name")?), + ("key", native_field(&scenario, "key")?), + ("name", native_field(&scenario, "name")?), ("graph_plan", graph), ("relation_plan", context.relations), ("fixture_plans", fixtures.into_any()), @@ -183,7 +184,7 @@ pub(crate) fn build_scenario_execution_plan<'py>( PyTuple::new(py, function_entries)?.into_any(), ), ("model_entries", PyTuple::new(py, entries)?.into_any()), - ("hook_functions", project.getattr("hook_functions")?), + ("hook_functions", native_field(&project, "hook_functions")?), ("expected_expectations", expected), ("assertion_expectations", assertions), ], @@ -217,7 +218,7 @@ fn complete_fixtures<'py>( } let names = keys .iter() - .map(|key| key.getattr("name")) + .map(|key| native_field(key, "name")) .collect::>>()?; let names = names .iter() @@ -234,10 +235,10 @@ fn complete_fixtures<'py>( for diagnostic in &completion.diagnostics { details.push(format!( "- {}", - diagnostic.getattr("message")?.extract::()? + native_field(diagnostic, "message")?.extract::()? )); } - let name: String = scenario.getattr("name")?.extract()?; + let name: String = native_field(scenario, "name")?.extract()?; return Err(error( py, &format!( @@ -263,14 +264,14 @@ fn model_context<'py>( Ok(Bound::new( py, ModelPlanContext { - model_locations: context.relations.getattr("model_locations")?.unbind(), + model_locations: native_field(&context.relations, "model_locations")?.unbind(), models_by_name: models.clone().into_any().unbind(), - functions_by_name: by_attribute(&project.getattr("functions")?, "name")? + functions_by_name: by_attribute(&native_field(project, "functions")?, "name")? .into_any() .unbind(), - seed_locations: context.relations.getattr("seed_locations")?.unbind(), + seed_locations: native_field(&context.relations, "seed_locations")?.unbind(), function_locations: locations.clone().unbind(), - source_map: context.relations.getattr("source_map")?.unbind(), + source_map: native_field(&context.relations, "source_map")?.unbind(), source_warehouse_columns: columns.clone().unbind(), star_exclude_keyword: context .adapter @@ -291,14 +292,14 @@ fn expectations<'py>( ) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> { let py = scenario.py(); let mut expected: Vec> = Vec::new(); - for cte in scenario.getattr("expected_ctes")?.try_iter()? { + for cte in native_field(scenario, "expected_ctes")?.try_iter()? { let cte = cte?; - let name: String = cte.getattr("name")?.extract()?; + let name: String = native_field(&cte, "name")?.extract()?; let name = name.strip_prefix("__expected__").unwrap_or(&name); let target = required_target(&context.relations, "model_locations", name, "model")?; let sql = resolve_sql( context, - &cte.getattr("sql_body")?.extract::()?, + &native_field(&cte, "sql_body")?.extract::()?, false, )?; expected.push(call_with( @@ -311,12 +312,12 @@ fn expectations<'py>( )?); } let mut assertions: Vec> = Vec::new(); - for cte in scenario.getattr("assertion_ctes")?.try_iter()? { + for cte in native_field(scenario, "assertion_ctes")?.try_iter()? { let cte = cte?; - let name: String = cte.getattr("name")?.extract()?; + let name: String = native_field(&cte, "name")?.extract()?; let sql = resolve_sql( context, - &cte.getattr("sql_body")?.extract::()?, + &native_field(&cte, "sql_body")?.extract::()?, false, )?; assertions.push(call_with( 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 3ed53c053d..dc88417c35 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/fixtures.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/fixtures.rs @@ -7,6 +7,7 @@ use crate::bindings::_helpers::planner_scenario::{ sql::{required_target, resolve_sql, source_label}, values::{error, string}, }; +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}; @@ -17,7 +18,7 @@ pub(super) fn extract_fixtures<'py>( prefix: &str, ) -> PyResult> { crate::bindings::_helpers::sql_test_glue::authored_ctes::prefixed_ctes( - &scenario.getattr("authored_ctes")?, + &native_field(scenario, "authored_ctes")?, prefix, ) } @@ -30,9 +31,9 @@ pub(super) fn fixture_plans<'py>( ) -> PyResult> { let py = scenario.py(); let mut helpers: Vec = Vec::new(); - for cte in scenario.getattr("authored_ctes")?.try_iter()? { + for cte in native_field(scenario, "authored_ctes")?.try_iter()? { let cte = cte?; - let name: String = cte.getattr("name")?.extract()?; + let name: String = native_field(&cte, "name")?.extract()?; if ["__source__", "__ref__", "__seed__", "__dbt_ref__"] .iter() .any(|prefix| name.starts_with(prefix)) @@ -41,7 +42,7 @@ pub(super) fn fixture_plans<'py>( } let sql = resolve_sql( context, - &cte.getattr("sql_body")?.extract::()?, + &native_field(&cte, "sql_body")?.extract::()?, true, )?; helpers.push(format!("{name} AS ({sql})")); 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 e550a8d14e..dc1dd028e4 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/graph.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/graph.rs @@ -5,6 +5,7 @@ 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::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}; @@ -23,19 +24,19 @@ pub(crate) fn plan_scenario_graph<'py>( let _ = (sql_analysis_enabled, sql_analysis_dialect); let py = scenario.py(); let mut input = ScenarioGraphInput { - name: scenario.getattr("name")?.extract()?, - expected: scenario.getattr("expected_model_names")?.extract()?, + name: native_field(scenario, "name")?.extract()?, + expected: native_field(scenario, "expected_model_names")?.extract()?, assertions: scenario .getattr("assertion_target_model_names")? .extract()?, - targets: scenario.getattr("target_model_names")?.extract()?, - refs: scenario.getattr("ref_fixture_names")?.extract()?, - sources: scenario.getattr("source_fixture_names")?.extract()?, - seeds: scenario.getattr("seed_fixture_names")?.extract()?, - dbt_refs: scenario.getattr("dbt_ref_fixture_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()?, + seeds: native_field(scenario, "seed_fixture_names")?.extract()?, + dbt_refs: native_field(scenario, "dbt_ref_fixture_names")?.extract()?, ..ScenarioGraphInput::default() }; - for model in project.getattr("models")?.try_iter()? { + for model in native_field(project, "models")?.try_iter()? { let model = model?; let deps = model .getattr("deps")? @@ -46,15 +47,15 @@ pub(crate) fn plan_scenario_graph<'py>( .models .insert(model_reads::name_object(&model)?.extract()?, deps); } - for source in project.getattr("sources")?.try_iter()? { + for source in native_field(project, "sources")?.try_iter()? { input .project_sources - .insert(source?.getattr("name")?.extract()?); + .insert(native_field(&source?, "name")?.extract()?); } - for seed in project.getattr("seeds")?.try_iter()? { + for seed in native_field(project, "seeds")?.try_iter()? { input .project_seeds - .insert(seed?.getattr("name")?.extract()?); + .insert(native_field(&seed?, "name")?.extract()?); } let output = graph(&input); input.targets.sort(); @@ -62,8 +63,8 @@ pub(crate) fn plan_scenario_graph<'py>( let plan = call_with( ®istry::class(py, RecordId::ScenarioGraphPlan)?, &[ - ("key", scenario.getattr("key")?), - ("name", scenario.getattr("name")?), + ("key", native_field(scenario, "key")?), + ("name", native_field(scenario, "name")?), ( "target_model_names", PyTuple::new(py, input.targets)?.into_any(), 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 f26919e2b8..e701a6d8ea 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/relations.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/relations.rs @@ -9,6 +9,7 @@ use crate::bindings::_helpers::planner_scenario::constants::{ }; use crate::bindings::_helpers::planner_scenario::models::ScenarioRelations; use crate::bindings::_helpers::planner_scenario::values::error; +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}; @@ -51,18 +52,24 @@ pub(crate) fn build_scenario_relation_plan<'py>( let py = args.project.py(); let graph = &args.graph_plan; let artifacts = PyDict::new(py); - for artifact in args.relation_map.getattr("artifacts")?.try_iter()? { + for artifact in native_field(&args.relation_map, "artifacts")?.try_iter()? { let artifact = artifact?; - let identity = artifact.getattr("identity")?; + let identity = native_field(&artifact, "identity")?; artifacts.set_item( - (identity.getattr("kind")?, identity.getattr("logical_name")?), - artifact.getattr("physical_name")?, + ( + native_field(&identity, "kind")?, + native_field(&identity, "logical_name")?, + ), + native_field(&artifact, "physical_name")?, )?; } let sources = PyDict::new(py); - for source in args.project.getattr("sources")?.try_iter()? { + for source in native_field(&args.project, "sources")?.try_iter()? { let source = source?; - sources.set_item(source.getattr("name")?, source.getattr("source_entry")?)?; + sources.set_item( + native_field(&source, "name")?, + native_field(&source, "source_entry")?, + )?; } let models = PyDict::new(py); let refs = PyDict::new(py); @@ -92,7 +99,7 @@ pub(crate) fn build_scenario_relation_plan<'py>( for field in ["database", "schema"] { changes.set_item(field, target.getattr(field)?)?; } - changes.set_item("table", target.getattr("name")?)?; + changes.set_item("table", native_field(&target, "name")?)?; changes.set_item("expression", py.None())?; changes.set_item("type_enforcement", false)?; let replacement = @@ -101,7 +108,7 @@ pub(crate) fn build_scenario_relation_plan<'py>( } } } - for name in graph.getattr("seed_fixture_names")?.try_iter()? { + for name in native_field(graph, "seed_fixture_names")?.try_iter()? { let name: String = name?.extract()?; let Some(target) = seeds.get_item(&name)? else { return Err(error( @@ -116,7 +123,7 @@ pub(crate) fn build_scenario_relation_plan<'py>( call_with( ®istry::class(py, RecordId::ScenarioRelationPlan)?, &[ - ("scenario_name", graph.getattr("name")?), + ("scenario_name", native_field(graph, "name")?), ("relation_map", args.relation_map.clone()), ("model_locations", models.into_any()), ("seed_locations", seeds.into_any()), 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 797ed05898..14e10ff9f6 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/sql.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_scenario/sql.rs @@ -9,6 +9,7 @@ use crate::bindings::_helpers::planner_scenario::{ models::SqlContext, values::{error, string}, }; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::references::source_relations::render_source_relation; use crate::bindings::_helpers::sqltext::lexical_syntax::LexicalSyntaxInput; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python, pyfunction}; @@ -24,7 +25,7 @@ pub(super) fn resolve_sql( project_sources: bool, ) -> PyResult { let py = context.adapter.py(); - let syntax: LexicalSyntaxInput = context.syntax.getattr("native_mapping")?.extract()?; + let syntax: LexicalSyntaxInput = native_field(&context.syntax, "native_mapping")?.extract()?; let mut result = sql.to_owned(); for (kind, field) in [ (0, "model_locations"), @@ -47,7 +48,7 @@ pub(super) fn resolve_sql( let target = locations.call_method1("get", (name,))?; let target = if target.is_none() {target} else if kind == super::constants::SOURCE_MARKER { object(py, render_source_relation(&target, Some(&context.adapter.clone()).filter(|value| !value.is_none()))?)? - } else {target.getattr("qualified_name")?}; + } else {native_field(&target, "qualified_name")?}; if target.is_none() { let marker = string(py, marker).repr()?.to_str()?.to_owned(); return Err(error(py, &format!("Scenario SQL in {} references {marker}, which is not a relation in this scenario", context.label), @@ -67,13 +68,13 @@ pub(super) fn resolve_sql( } pub(super) fn resolve_model_dbt_refs(sql: &str, relations: &Bound<'_, PyAny>) -> PyResult { - let locations = relations.getattr("dbt_ref_fixture_locations")?; + let locations = native_field(relations, "dbt_ref_fixture_locations")?; replace_markers(sql, 3, None, |name, marker| { let target = locations.call_method1("get", (name,))?; let qualified = if target.is_none() { target } else { - target.getattr("qualified_name")? + native_field(&target, "qualified_name")? }; Ok(if qualified.is_none() { marker.to_owned() @@ -90,11 +91,11 @@ pub(super) fn resolve_model_dbt_refs(sql: &str, relations: &Bound<'_, PyAny>) -> } pub(super) fn source_label(scenario: &Bound<'_, PyAny>) -> PyResult { - let source = scenario.getattr("source_path")?; + let source = native_field(scenario, "source_path")?; if source.is_truthy()? { Ok(source.str()?.to_str()?.to_owned()) } else { - scenario.getattr("name")?.extract() + native_field(scenario, "name")?.extract() } } 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 22ad2c1584..429e968709 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_scope/loaders.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_scope/loaders.rs @@ -6,6 +6,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, IntoPyObject, PyAny, PyAnyMethods, PyResult, pyfunction}; @@ -34,8 +35,8 @@ pub(crate) fn loader_to_source_entry<'py>( loader: &Bound<'py, PyAny>, ) -> PyResult> { let py = project.py(); - let name = loader.getattr("name")?; - let mut destination = loader.getattr("destination")?; + let name = native_field(loader, "name")?; + let mut destination = native_field(loader, "destination")?; if destination.is_none() { destination = format!("__loader__{}", name.extract::()?) .into_pyobject(py)? @@ -47,11 +48,11 @@ pub(crate) fn loader_to_source_entry<'py>( ("destination", destination), ( "default_database", - project.getattr("effective_target_database")?, + native_field(project, "effective_target_database")?, ), ( "default_schema", - project.getattr("effective_target_schema")?, + native_field(project, "effective_target_schema")?, ), ], )?; @@ -84,9 +85,9 @@ pub(crate) fn intermediate_map<'py>( let graph = project_graph(project)?; let names = intermediate_sources(&graph, &keys(selected)?, upstream); let loaders = PyDict::new(project.py()); - for loader in project.getattr("loader_functions")?.try_iter()? { + for loader in native_field(project, "loader_functions")?.try_iter()? { let loader = loader?; - loaders.set_item(loader.getattr("name")?, &loader)?; + loaders.set_item(native_field(&loader, "name")?, &loader)?; } let result = PyDict::new(project.py()); for name in names { 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 6daf82827c..a6f82e5a16 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_scope/scope.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_scope/scope.rs @@ -5,6 +5,7 @@ use crate::bindings::_helpers::graph::keys::KeyCache; use crate::bindings::_helpers::planner_scope::loaders::expand_selected_loader_dependencies; use crate::bindings::_helpers::planner_scope::values::{edges, keys}; use crate::bindings::_helpers::planner_selection::carrier::{Carrier, py_strings}; +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 pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python, pyfunction}; @@ -24,10 +25,13 @@ pub(crate) fn managed_source_upstream_keys<'py>( project: &Bound<'py, PyAny>, ) -> PyResult> { let mut managed: HashSet = HashSet::new(); - for source in project.getattr("sources")?.try_iter()? { + for source in native_field(project, "sources")?.try_iter()? { let source = source?; - if !source.getattr("source_entry")?.getattr("loader")?.is_none() { - managed.insert(source.getattr("name")?.extract()?); + if !native_field(&source, "source_entry")? + .getattr("loader")? + .is_none() + { + managed.insert(native_field(&source, "name")?.extract()?); } } let selected = managed_sources(&keys(selected_keys)?, &edges(upstream_deps)?, &managed); @@ -67,7 +71,7 @@ pub(crate) fn build_planner_scope<'py>( registry::native(py, NativeAttr::BuildExecutionUpstreamDeps)?.call1((&project,))?; let mut downstream = registry::native(py, NativeAttr::BuildDownstreamDeps)?.call1((&upstream,))?; - let lineage = project.getattr("lineage_graph")?; + let lineage = native_field(&project, "lineage_graph")?; let all_keys = registry::native(py, NativeAttr::LineageGraphNames)?.call1((&lineage,))?; let mut selected = args.value("selected_keys")?; if selected.is_none() { @@ -126,9 +130,9 @@ pub(crate) fn build_planner_scope<'py>( ], )?; let models = PyDict::new(py); - for model in project.getattr("models")?.try_iter()? { + for model in native_field(&project, "models")?.try_iter()? { let model = model?; - models.set_item(model.getattr("name")?, model)?; + models.set_item(native_field(&model, "name")?, model)?; } let mut reads = args.value("python_read_source_names")?; if reads.is_none() { 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 1eccb0a12c..eb19c0135a 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_scope/sources.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_scope/sources.rs @@ -4,6 +4,7 @@ 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::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}; @@ -28,15 +29,15 @@ pub(crate) fn build_source_load_map<'py>( }) .transpose()?; let result = PyDict::new(project.py()); - for source in project.getattr("sources")?.try_iter()? { + for source in native_field(project, "sources")?.try_iter()? { let source = source?; if let Some(keys) = &relevant - && !keys.contains(source.getattr("key")?)? + && !keys.contains(native_field(&source, "key")?)? { continue; } - let entry = source.getattr("source_entry")?; - result.set_item(entry.getattr("name")?, entry)?; + let entry = native_field(&source, "source_entry")?; + result.set_item(native_field(&entry, "name")?, entry)?; } for upstream in [false, true] { for (key, entry) in intermediate_map(project, selected_keys, upstream)?.iter() { @@ -61,14 +62,17 @@ pub(crate) fn build_source_load_entries<'py>( for key in execution_order.try_iter()? { let key = key?; if !selected_keys.contains(&key)? - || key.getattr("resource_type")?.str()?.extract::()? != SOURCE + || native_field(&key, "resource_type")? + .str()? + .extract::()? + != SOURCE { continue; } - let Some(entry) = source_map.get_item(key.getattr("name")?)? else { + let Some(entry) = source_map.get_item(native_field(&key, "name")?)? else { continue; }; - if entry.getattr("loader")?.is_none() { + if native_field(&entry, "loader")?.is_none() { continue; } let kwargs = PyDict::new(py); @@ -82,13 +86,13 @@ pub(crate) fn build_source_load_entries<'py>( ] { kwargs.set_item(field, entry.getattr(field)?)?; } - let table = entry.getattr("table")?; + let table = native_field(&entry, "table")?; kwargs.set_item( "destination", if table.is_truthy()? { table } else { - entry.getattr("name")? + native_field(&entry, "name")? }, )?; let intermediate = entry @@ -100,13 +104,13 @@ pub(crate) fn build_source_load_entries<'py>( kinds.getattr(if intermediate { "LOADER" } else { "SOURCE" })?, )?; kwargs.set_item("is_reload", is_reload)?; - let integration = entry.getattr("integration_loader")?; + let integration = native_field(&entry, "integration_loader")?; kwargs.set_item( "integration_kind", if integration.is_none() { py.None().into_bound(py) } else { - integration.getattr("kind")? + native_field(&integration, "kind")? }, )?; entries.push(class.call((), Some(&kwargs))?); diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_scope/values.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_scope/values.rs index 1f791d8c01..9180913ed7 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_scope/values.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_scope/values.rs @@ -1,6 +1,7 @@ //! Python function identity and compiled graph conversion. use crate::bindings::_helpers::graph::keys::native_key; +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; use pyo3::types::PyDict; use pyo3::types::PyDictMethods; @@ -30,7 +31,10 @@ pub(crate) fn loader_names<'py>(loaders: &Bound<'py, PyAny>) -> PyResult, ) -> PyResult> { let mut dependencies: Vec = Vec::new(); - for dependency in loader.getattr("depends_on")?.try_iter()? { + for dependency in native_field(loader, "depends_on")?.try_iter()? { if let Some(name) = names.get_item(dependency?)? { dependencies.push(name.extract()?); } @@ -54,7 +58,7 @@ pub(crate) fn loader_graph(loaders: &Bound<'_, PyAny>) -> PyResult for loader in loaders.try_iter()? { let loader = loader?; graph.loaders.insert( - loader.getattr("name")?.extract()?, + native_field(&loader, "name")?.extract()?, dependency_names(&loader, &names)?, ); } @@ -62,12 +66,12 @@ pub(crate) fn loader_graph(loaders: &Bound<'_, PyAny>) -> PyResult } pub(crate) fn project_graph(project: &Bound<'_, PyAny>) -> PyResult { - let mut graph = loader_graph(&project.getattr("loader_functions")?)?; - for source in project.getattr("sources")?.try_iter()? { - let entry = source?.getattr("source_entry")?; + let mut graph = loader_graph(&native_field(project, "loader_functions")?)?; + for source in native_field(project, "sources")?.try_iter()? { + let entry = native_field(&source?, "source_entry")?; graph.sources.push(( - entry.getattr("name")?.extract()?, - entry.getattr("loader")?.extract()?, + native_field(&entry, "name")?.extract()?, + native_field(&entry, "loader")?.extract()?, )); } Ok(graph) 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 69a6e63fc8..c4d14e7408 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 @@ -14,6 +14,7 @@ use sqlbuild_analysis::planner_selection::models::{ use crate::bindings::_helpers::compile_driver::python_calls::{call_with, imported}; use crate::bindings::_helpers::planner_selection::carrier::Carrier; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; @@ -68,7 +69,7 @@ fn model_tags(model: &Bound<'_, PyAny>) -> PyResult> { fn selectable_model(model: &Bound<'_, PyAny>) -> PyResult { Ok(SelectableModel { - name: model.getattr("name")?.extract()?, + name: native_field(model, "name")?.extract()?, tags: model_tags(model)?, folder: model .getattr("relative_path")? 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 769f3ff02d..863f746c4d 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_selection/selectors.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_selection/selectors.rs @@ -20,6 +20,7 @@ use crate::bindings::_helpers::graph::keys::KeyCache; use crate::bindings::_helpers::graph::project_graph::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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; @@ -147,7 +148,7 @@ pub(crate) fn select_project_graph<'py>( select: &Bound<'py, PyAny>, exclude: &Bound<'py, PyAny>, ) -> PyResult> { - let native: PyRef<'py, NativeProjectGraph> = graph.getattr("native")?.extract()?; + let native: PyRef<'py, NativeProjectGraph> = native_field(graph, "native")?.extract()?; resolved(native.graph(), (select, exclude)) } @@ -221,11 +222,11 @@ fn match_project_selector<'py>( tag_index, path_index, })?; - let value: Bound<'py, PyString> = parsed.getattr("value")?.downcast_into()?; + let value: Bound<'py, PyString> = native_field(parsed, "value")?.downcast_into()?; let carrier: Carrier<'py> = Carrier::new(py, std::slice::from_ref(&value), || { Ok(graph_names(graph.graph())) })?; - let kind: String = parsed.getattr("kind")?.str()?.extract()?; + let kind: String = native_field(parsed, "kind")?.str()?.extract()?; selected( &carrier, match_selector(graph.graph(), &kind, &carrier.send(&value)?) 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 d0c203f978..e8e322e596 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 @@ -12,6 +12,7 @@ use sqlbuild_analysis::planner_selection::models::{ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; @@ -34,19 +35,19 @@ const RESOURCE_NODES: [&str; 4] = [ ]; fn stem(path: &Bound<'_, PyAny>) -> PyResult { - path.getattr("stem")?.extract() + native_field(path, "stem")?.extract() } fn unit_test_names(discovered: &Bound<'_, PyAny>) -> PyResult> { let mut names: BTreeSet = BTreeSet::new(); - for test_file in discovered.getattr("test_files")?.try_iter()? { + for test_file in native_field(discovered, "test_files")?.try_iter()? { let test_file: Bound<'_, PyAny> = test_file?; - for block in test_file.getattr("blocks")?.try_iter()? { - let name: Bound<'_, PyAny> = block?.getattr("name")?; + for block in native_field(&test_file, "blocks")?.try_iter()? { + let name: Bound<'_, PyAny> = native_field(&block?, "name")?; names.insert(if name.is_truthy()? { name.extract()? } else { - stem(&test_file.getattr("relative_path")?)? + stem(&native_field(&test_file, "relative_path")?)? }); } } @@ -57,19 +58,19 @@ 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()? { - names.insert(stem(&file?.getattr("file_path")?)?); + names.insert(stem(&native_field(&file?, "file_path")?)?); } } for (field, entries) in RESOURCE_ENTRY_FILES { for file in discovered.getattr(field)?.try_iter()? { for entry in file?.getattr(entries)?.try_iter()? { - names.insert(entry?.getattr("name")?.extract()?); + names.insert(native_field(&entry?, "name")?.extract()?); } } } for field in RESOURCE_NODES { for node in discovered.getattr(field)?.try_iter()? { - names.insert(node?.getattr("name")?.extract()?); + names.insert(native_field(&node?, "name")?.extract()?); } } Ok(names) 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 7f917b0b88..d30cd41696 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 @@ -21,6 +21,7 @@ use crate::bindings::_helpers::planner_strategy::constants::{ AUDIT_TYPES_MODULE, COMPILE_TYPES_MODULE, MODEL_ATTACHMENT, SOURCE_ATTACHMENT, }; 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; @@ -95,13 +96,13 @@ fn plan_audit<'py>( ) -> PyResult> { let py = audit.py(); let audit = audit.clone(); - let name: String = audit.getattr("name")?.extract()?; + let name: String = native_field(&audit, "name")?.extract()?; let resolved = render_query( request, - audit.getattr("sql_body")?, + native_field(&audit, "sql_body")?, &format!("audit '{name}' planned SQL"), )?; - let evidence = audit.getattr("evidence_sql")?; + let evidence = native_field(&audit, "evidence_sql")?; let evidence_resolved = if evidence.is_none() { py.None().into_bound(py) } else { @@ -126,7 +127,7 @@ fn plan_audit<'py>( let requested = audit_enum(&audit, "run_scope", "AuditRunScope", "final")?; let severity = audit_enum(&audit, "severity", "AuditSeverity", "warn")?; let effective = resolve_effective_run_scope(&requested, &materialization)?; - let mut target_kind = audit.getattr("attached_target_kind")?; + let mut target_kind = native_field(&audit, "attached_target_kind")?; if target_kind.is_none() && (kind_text == MODEL_ATTACHMENT || kind_text == SOURCE_ATTACHMENT) { target_kind = imported(py, COMPILE_TYPES_MODULE, "AttachedAuditTargetKind")?.call1((kind_text,))?; @@ -147,7 +148,7 @@ fn plan_audit<'py>( ] { values.set_item(field, audit.getattr(field)?)?; } - let measurement = audit.getattr("measurement_contract")?; + let measurement = native_field(&audit, "measurement_contract")?; for field in ["value_column", "sample_count_column", "sample_unit"] { values.set_item( field, @@ -160,13 +161,16 @@ fn plan_audit<'py>( } for (field, value) in [ ("resolved_sql", resolved), - ("unresolved_sql", audit.getattr("sql_body")?), + ("unresolved_sql", native_field(&audit, "sql_body")?), ("attachment_kind", kind), ("severity", severity), ("requested_run_scope", requested), ("effective_run_scope", effective), ("evidence_resolved_sql", evidence_resolved), - ("evidence_unresolved_sql", audit.getattr("evidence_sql")?), + ( + "evidence_unresolved_sql", + native_field(&audit, "evidence_sql")?, + ), ("attached_target_kind", target_kind), ("attached_target_name", target), ( @@ -192,8 +196,8 @@ pub(crate) fn audit_is_selected( selected_keys: &Bound<'_, PyAny>, ) -> PyResult { let py = audit.py(); - let kind = audit.getattr("attached_target_kind")?; - let name = audit.getattr("attached_target_name")?; + let kind = native_field(audit, "attached_target_kind")?; + let name = native_field(audit, "attached_target_name")?; if !kind.is_none() && !name.is_none() { let target_value = imported(py, COMPILE_TYPES_MODULE, "AttachedAuditTargetKind")? .call1((kind,))? @@ -206,7 +210,7 @@ pub(crate) fn audit_is_selected( )?; return selected_keys.contains(key); } - for dependency in audit.getattr("scope_deps")?.try_iter()? { + for dependency in native_field(audit, "scope_deps")?.try_iter()? { if selected_keys.contains(dependency?)? { return Ok(true); } @@ -225,7 +229,7 @@ pub(crate) fn build_selected_audit_entries<'py>( let project = request.get("project")?; let selected_keys = request.get("scope")?.getattr("selected_keys")?; let mut selected: Vec> = Vec::new(); - for audit in project.getattr("audits")?.try_iter()? { + for audit in native_field(&project, "audits")?.try_iter()? { let audit = audit?; if audit_is_selected(&audit, &selected_keys)? { selected.push(audit); @@ -238,13 +242,13 @@ pub(crate) fn build_selected_audit_entries<'py>( let lineage = project_lineage_views(py, &project)?; let relations = request.get("relations")?; let planning = AuditPlanning { - model_locations: relations.getattr("model_locations")?, - seed_locations: relations.getattr("seed_locations")?, - source_map: relations.getattr("source_read_map")?, + model_locations: native_field(&relations, "model_locations")?, + seed_locations: native_field(&relations, "seed_locations")?, + source_map: native_field(&relations, "source_read_map")?, adapter: request.get("adapter")?, model_materializations: request.get("model_materializations")?, - lineage_upstream_deps: lineage.getattr("upstream_deps")?, - lineage_downstream_deps: lineage.getattr("downstream_deps")?, + lineage_upstream_deps: native_field(&lineage, "upstream_deps")?, + lineage_downstream_deps: native_field(&lineage, "downstream_deps")?, }; for audit in selected { entries.append(plan_audit(&audit, &planning)?)?; 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 6b8649f776..68778fbf3a 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/audits.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/audits.rs @@ -12,6 +12,7 @@ use sqlbuild_analysis::planner_strategy::models::{AuditAttachmentKind, AuditRunS use crate::bindings::_helpers::compile_driver::python_calls::{call_with, 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -37,10 +38,10 @@ fn object_key<'py>(py: Python<'py>, kind: &str, name: &str) -> PyResult, kind: &str) -> PyResult> { let mut names: Vec = Vec::new(); - for reference in audit.getattr("references")?.try_iter()? { + for reference in native_field(audit, "references")?.try_iter()? { let reference: Bound<'_, PyAny> = reference?; - if text(&reference.getattr("ref_kind")?)? == kind { - names.push(text(&reference.getattr("ref_name")?)?); + if text(&native_field(&reference, "ref_kind")?)? == kind { + names.push(text(&native_field(&reference, "ref_name")?)?); } } Ok(names) @@ -63,7 +64,7 @@ pub(crate) fn resolve_attachment_kind<'py>( downstream_deps: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = audit.py(); - let attached_kind: Bound<'py, PyAny> = audit.getattr("attached_target_kind")?; + let attached_kind: Bound<'py, PyAny> = native_field(audit, "attached_target_kind")?; let (kind, name): (String, Option) = if attached_kind.is_none() { let reaches = |from: &str, to: &str| -> PyResult { let start: Bound<'py, PyAny> = object_key(py, MODEL_RESOURCE_TYPE, from)?; @@ -80,14 +81,14 @@ pub(crate) fn resolve_attachment_kind<'py>( )?; (attachment_value(kind).to_owned(), name) } else { - let name: Bound<'py, PyAny> = audit.getattr("attached_target_name")?; + let name: Bound<'py, PyAny> = native_field(audit, "attached_target_name")?; if name.is_none() { return Err(input_error( py, ErrorId::PlannerInput, &format!( "audit '{}': attached audit is missing an attached target name", - text(&audit.getattr("name")?)? + text(&native_field(audit, "name")?)? ), None, )); @@ -115,8 +116,8 @@ pub(crate) fn reads_outside_target_lineage( upstream_deps: &Bound<'_, PyAny>, ) -> PyResult { let py: Python<'_> = audit.py(); - let kind: Bound<'_, PyAny> = audit.getattr("attached_target_kind")?; - let name: Bound<'_, PyAny> = audit.getattr("attached_target_name")?; + let kind: Bound<'_, PyAny> = native_field(audit, "attached_target_kind")?; + let name: Bound<'_, PyAny> = native_field(audit, "attached_target_name")?; if kind.is_none() || name.is_none() { return Ok(false); } @@ -124,7 +125,7 @@ pub(crate) fn reads_outside_target_lineage( text(&imported(py, COMPILE_TYPES_MODULE, "AttachedAuditTargetKind")?.call1((kind,))?)?; let target: Bound<'_, PyAny> = object_key(py, &kind, &text(&name)?)?; let lineage: Bound<'_, PySet> = closure(vec![target], upstream_deps, (true, None))?; - for dependency in audit.getattr("scope_deps")?.try_iter()? { + for dependency in native_field(audit, "scope_deps")?.try_iter()? { if !lineage.contains(dependency?)? { return Ok(true); } 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 aa89cdde7e..1b589fff66 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 @@ -21,13 +21,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::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> { - model.getattr("config")?.getattr("values") + native_field(model, "config")?.getattr("values") } /// Whether a model is incremental with a cursor column. @@ -47,7 +48,7 @@ fn incremental_cursor_flag( fn names(items: &Bound<'_, PyAny>) -> PyResult> { items .try_iter()? - .map(|item| text(&item?.getattr("name")?)) + .map(|item| text(&native_field(&item?, "name")?)) .collect() } @@ -115,8 +116,8 @@ pub(crate) fn reject_unexposed_cursor_input_columns<'py>( models_by_name.clone(), lookups.functions_by_name.clone(), )?; - let kind: String = text(&reference.getattr("ref_kind")?)?; - let name: String = text(&reference.getattr("ref_name")?)?; + let kind: String = text(&native_field(&reference, "ref_kind")?)?; + let name: String = text(&native_field(&reference, "ref_name")?)?; let check = |owner, known_columns| CursorInputColumnCheck { model_name: model_name.clone(), owner, @@ -133,11 +134,11 @@ pub(crate) fn reject_unexposed_cursor_input_columns<'py>( { continue; } - let entry: Bound<'py, PyAny> = upstream.getattr("schema_entry")?; + let entry: Bound<'py, PyAny> = native_field(&upstream, "schema_entry")?; let declared: Vec = if entry.is_none() { Vec::new() } else { - names(&entry.getattr("columns")?)? + names(&native_field(&entry, "columns")?)? }; check_column(check(CursorInputColumnOwner::ModelContract, declared), py)?; continue; @@ -146,8 +147,8 @@ pub(crate) fn reject_unexposed_cursor_input_columns<'py>( continue; } let source: Bound<'py, PyAny> = source_map.call_method1("get", (&name,))?; - if !source.is_none() && source.getattr("contract")?.eq(ENFORCED_CONTRACT)? { - let declared: Vec = names(&source.getattr("columns")?)?; + if !source.is_none() && native_field(&source, "contract")?.eq(ENFORCED_CONTRACT)? { + let declared: Vec = names(&native_field(&source, "columns")?)?; check_column(check(CursorInputColumnOwner::SourceContract, declared), py)?; continue; } @@ -192,7 +193,7 @@ fn input_relation<'py>( if location.is_none() { return Ok(None); } - location.getattr("qualified_name")?.extract() + native_field(&location, "qualified_name")?.extract() } /// The resource kind a resolved cursor input reference reads. @@ -270,7 +271,7 @@ pub(crate) fn build_cursor_input_relations<'py>( let class: Bound<'py, PyAny> = registry::class(py, RecordId::CursorInputRelation)?; let mut relations: Vec> = Vec::new(); let watermarks: Bound<'py, PyAny> = - resolve_cursor_input_roles(&model)?.getattr("watermark_inputs")?; + native_field(&resolve_cursor_input_roles(&model)?, "watermark_inputs")?; for item in watermarks.call_method0("items")?.try_iter()? { let (input_name, column): (String, Bound<'py, PyAny>) = item?.extract()?; let reference: Bound<'py, PyAny> = resolve_lineage_reference( @@ -279,8 +280,8 @@ pub(crate) fn build_cursor_input_relations<'py>( models_by_name.clone(), functions_by_name.clone(), )?; - let kind: String = text(&reference.getattr("ref_kind")?)?; - let name: Bound<'py, PyAny> = reference.getattr("ref_name")?; + let kind: String = text(&native_field(&reference, "ref_kind")?)?; + let name: Bound<'py, PyAny> = native_field(&reference, "ref_name")?; let name_text: String = text(&name)?; let Some(relation) = input_relation((lookups, adapter), (&kind, &name_text))? else { continue; @@ -337,14 +338,14 @@ pub(crate) fn runtime_cursor_producer_names<'py>( ) -> PyResult> { let py: Python<'py> = scope.py(); let mut names: Vec> = Vec::new(); - for key in scope.getattr("selected_keys")?.try_iter()? { + for key in native_field(scope, "selected_keys")?.try_iter()? { let key: Bound<'py, PyAny> = key?; - let name: Bound<'py, PyAny> = key.getattr("name")?; - match text(&key.getattr("resource_type")?)?.as_str() { + let name: Bound<'py, PyAny> = native_field(&key, "name")?; + match text(&native_field(&key, "resource_type")?)?.as_str() { "model" | "seed" => names.push(name), "source" => { let source: Bound<'py, PyAny> = source_map.call_method1("get", (&name,))?; - if !source.is_none() && !source.getattr("loader")?.is_none() { + if !source.is_none() && !native_field(&source, "loader")?.is_none() { names.push(name); } } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/cursor_types.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/cursor_types.rs index faa8aca874..ab4b6e400a 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/cursor_types.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/cursor_types.rs @@ -16,6 +16,7 @@ use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::planner_changes::results::planner_enum; use crate::bindings::_helpers::planner_strategy::values::{optional_string, plan_warning, text}; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; const PLANNER_LOGGER: &str = "sqlbuild.planner"; @@ -107,8 +108,8 @@ pub(crate) fn warehouse_column_types( for column in columns.try_iter()? { let column = column?; pairs.push(( - text(&column.getattr("name")?)?, - text(&column.getattr("type")?)?, + text(&native_field(&column, "name")?)?, + text(&native_field(&column, "type")?)?, )); } Ok(pairs) 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 e259839244..15d4c1c727 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 @@ -23,6 +23,7 @@ use crate::bindings::_helpers::planner_strategy::models::{ FixtureReadCacheHost, FixtureRelation, ModelFixtureFacts, RelationFixturePlanningContext, }; use crate::bindings::_helpers::planner_strategy::types::{FixtureKey, FixtureRelationInput}; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::NativeAttr; use crate::bindings::_helpers::records::registry; @@ -47,7 +48,7 @@ pub(crate) fn planning_context( Some(models) => models, None => { let models = PyDict::new(py); - for model in project.getattr("models")?.try_iter()? { + for model in native_field(project, "models")?.try_iter()? { let model = model?; models.set_item(model_reads::name_object(&model)?, &model)?; } @@ -68,14 +69,14 @@ pub(crate) fn planning_context( for resource in project.getattr(collection)?.try_iter()? { let resource = resource?; let entry = resource.getattr(entry)?; - let columns: Vec = columns(&entry.getattr("columns")?, false)?; + let columns: Vec = columns(&native_field(&entry, "columns")?, false)?; let authoritative = if source { - entry.getattr("contract")?.eq("enforced")? + native_field(&entry, "contract")?.eq("enforced")? } else { !columns.is_empty() }; relations.insert( - (kind.to_owned(), resource.getattr("name")?.extract()?), + (kind.to_owned(), native_field(&resource, "name")?.extract()?), FixtureRelation { columns, authoritative, 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 ac20594665..965a96576a 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 @@ -9,15 +9,16 @@ use crate::bindings::_helpers::analysis::model_reads::{inferred_rows, star_flags use crate::bindings::_helpers::compiled_project::compiled_model_view::InferredRow; use crate::bindings::_helpers::planner_strategy::fixture_values::columns; use crate::bindings::_helpers::planner_strategy::models::FixtureRelation; +use crate::bindings::_helpers::records::fields::field as native_field; /// The model's merged inferred and declared columns, authoritative unless its analysis saw a star. pub(crate) fn model_fixture_relation(model: &Bound<'_, PyAny>) -> PyResult { let inferred: Option> = inferred_rows(model)?; - let schema = model.getattr("schema_entry")?; + let schema = native_field(model, "schema_entry")?; let declared: Vec = if schema.is_none() { Vec::new() } else { - columns(&schema.getattr("columns")?, false)? + columns(&native_field(&schema, "columns")?, false)? }; let mut inferred_columns: Vec = Vec::new(); for (name, sql_type, _) in inferred.iter().flatten() { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/fixture_preparation.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/fixture_preparation.rs index ff108f28d9..b26258add2 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/fixture_preparation.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/fixture_preparation.rs @@ -9,6 +9,7 @@ use crate::bindings::_helpers::planner_strategy::fixture_sql::complete_empty_fix use crate::bindings::_helpers::planner_strategy::fixture_values::{diagnostic, label}; use crate::bindings::_helpers::planner_strategy::models::RelationFixturePlanningContext; use crate::bindings::_helpers::planner_strategy::types::FixtureKey; +use crate::bindings::_helpers::records::fields::field as native_field; pub(crate) struct PreparedFixtures<'py> { /// Each fixture's key with its Python key, in request order. @@ -67,13 +68,13 @@ pub(crate) fn prepare<'py>( if !inference.is_none() { prepared .inferred - .set_item(&key, inference.getattr("columns")?)?; + .set_item(&key, native_field(&inference, "columns")?)?; prepared .null_names - .set_item(&key, inference.getattr("null_literal_names")?)?; + .set_item(&key, native_field(&inference, "null_literal_names")?)?; prepared .quoted_names - .set_item(&key, inference.getattr("quoted_names")?)?; + .set_item(&key, native_field(&inference, "quoted_names")?)?; } } } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/fixture_reads.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/fixture_reads.rs index 21d982b07d..0124530157 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/fixture_reads.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/fixture_reads.rs @@ -22,6 +22,7 @@ use crate::bindings::_helpers::planner_strategy::models::{ FixtureReadCacheHost, RelationFixturePlanningContext, }; use crate::bindings::_helpers::planner_strategy::types::FixtureKey; +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; @@ -63,13 +64,13 @@ pub(crate) fn required_fixture_columns( continue; }; let mut references: Vec = Vec::new(); - for reference in model.getattr("references")?.try_iter()? { + for reference in native_field(&model, "references")?.try_iter()? { let reference = reference?; references.push(( - reference.getattr("ref_kind")?.extract()?, - reference.getattr("ref_name")?.extract()?, - reference.getattr("ref_package")?.extract()?, - reference.getattr("call_argument_count")?.extract()?, + native_field(&reference, "ref_kind")?.extract()?, + native_field(&reference, "ref_name")?.extract()?, + native_field(&reference, "ref_package")?.extract()?, + native_field(&reference, "call_argument_count")?.extract()?, )); } models.push(FixtureReadModel { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/fixture_sql.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/fixture_sql.rs index 7f8f327f3a..a791ee7954 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/fixture_sql.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/fixture_sql.rs @@ -13,6 +13,7 @@ use sqlbuild_analysis::planner_fixtures::models::{ use crate::bindings::_helpers::boundary::panics::value_error; use crate::bindings::_helpers::planner_strategy::models::FixtureRelation; +use crate::bindings::_helpers::records::fields::field as native_field; #[pyfunction(name = "is_empty_fixture_query")] pub(crate) fn fixture_marker(sql: &str) -> PyResult { @@ -62,7 +63,7 @@ pub(crate) fn render_completed( if !input.typed_nulls.is_empty() { for column in input.inferred.try_iter()? { let column = column?; - let name = column.getattr("name")?; + let name = native_field(&column, "name")?; let folded = name.call_method0("casefold")?; supplied.push(SuppliedProjection { name: name.extract()?, @@ -106,7 +107,7 @@ pub(crate) fn complete_typed_null_columns( ) -> PyResult { let py = inference.py(); let typed_nulls = PyDict::new(py); - for name in inference.getattr("null_literal_names")?.try_iter()? { + for name in native_field(inference, "null_literal_names")?.try_iter()? { let name = name?; let folded: String = name.extract()?; if let Some((_, expected)) = expected_types.iter().find(|(column, _)| *column == folded) { @@ -119,9 +120,9 @@ pub(crate) fn complete_typed_null_columns( render_completed( CompletionSql { sql, - inferred: inference.getattr("columns")?, + inferred: native_field(inference, "columns")?, typed_nulls, - quoted_names: inference.getattr("quoted_names")?, + quoted_names: native_field(inference, "quoted_names")?, completed: Vec::new(), }, adapter, 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 5d621a37c2..c13379eff9 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,5 +1,6 @@ //! Fixture boundary records and adapter callbacks. +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}; @@ -15,15 +16,15 @@ pub(crate) fn columns(values: &Bound<'_, PyAny>, inferred: bool) -> PyResult( pub(crate) fn label(key: &Bound<'_, PyAny>) -> PyResult { Ok(format!( "mock {} '{}'", - key.get_item(0)?.getattr("value")?.extract::()?, + native_field(&key.get_item(0)?, "value")?.extract::()?, key.get_item(1)?.extract::()? )) } 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 c790f9de30..6423ef4f64 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/lineage.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/lineage.rs @@ -19,6 +19,7 @@ use crate::bindings::_helpers::planner_strategy::microbatches::{ CAP_ACTIONS, microbatch_limit_config, }; use crate::bindings::_helpers::planner_strategy::values::{input_error, text}; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::ErrorId; const MICROBATCH_MODE: &str = "microbatch"; @@ -42,12 +43,12 @@ fn mapped_references<'py>( } fn references<'py>(owner: &Bound<'py, PyAny>) -> PyResult>> { - owner.getattr("references")?.try_iter()?.collect() + native_field(owner, "references")?.try_iter()?.collect() } /// Whether a model's `incremental_mode` is microbatch. fn microbatch_flag(model: &Bound<'_, PyAny>) -> PyResult { - let values: Bound<'_, PyAny> = model.getattr("config")?.getattr("values")?; + let values: Bound<'_, PyAny> = native_field(model, "config")?.getattr("values")?; Ok(match config_str(&values, "incremental_mode")? { Some(mode) => mode.eq(MICROBATCH_MODE)?, None => false, @@ -69,8 +70,8 @@ impl<'py> ReferenceGraph for ProjectLineage<'py> { fn identity(&self, reference: &Bound<'py, PyAny>) -> PyResult { Ok(ReferenceIdentity { - kind: text(&reference.getattr("ref_kind")?)?, - name: text(&reference.getattr("ref_name")?)?, + kind: text(&native_field(reference, "ref_kind")?)?, + name: text(&native_field(reference, "ref_name")?)?, }) } @@ -84,7 +85,7 @@ 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> = producer.getattr("config")?.getattr("values")?; + let values: Bound<'py, PyAny> = native_field(&producer, "config")?.getattr("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 9e99296361..26f52ceb9b 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 @@ -18,6 +18,7 @@ use crate::bindings::_helpers::planner_strategy::microbatches::{ CAP_ACTIONS, capped_microbatch_bounds, count_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; @@ -49,7 +50,7 @@ pub(crate) fn replay_grain( ) -> PyResult> { let mut input_grains: Vec> = Vec::new(); for relation in relations.try_iter()? { - input_grains.push(optional_string(&relation?.getattr("cursor_grain")?)?); + input_grains.push(optional_string(&native_field(&relation?, "cursor_grain")?)?); } effective_timestamp_grain(&TimestampGrainFacts { cursor_type, @@ -63,9 +64,9 @@ pub(crate) fn replay_grain( /// The entry's effective batch size and its batch count over the planned range. fn batch_count(entry: &Bound<'_, PyAny>) -> PyResult> { let py: Python<'_> = entry.py(); - let range: Bound<'_, PyAny> = entry.getattr("microbatch_range")?; - let batch_size: Bound<'_, PyAny> = entry.getattr("batch_size")?; - let cursor_type: Bound<'_, PyAny> = entry.getattr("cursor_type")?; + let range: Bound<'_, PyAny> = native_field(entry, "microbatch_range")?; + let batch_size: Bound<'_, PyAny> = native_field(entry, "batch_size")?; + let cursor_type: Bound<'_, PyAny> = native_field(entry, "cursor_type")?; if range.is_none() || batch_size.is_none() || cursor_type.is_none() { return Ok(None); } @@ -73,16 +74,16 @@ fn batch_count(entry: &Bound<'_, PyAny>) -> PyResult> { let grain: Option = replay_grain( ( Some(cursor_type.clone()), - optional_string(&entry.getattr("microbatch_strategy")?)?, - optional_string(&entry.getattr("cursor_grain")?)?, + optional_string(&native_field(entry, "microbatch_strategy")?)?, + optional_string(&native_field(entry, "cursor_grain")?)?, ), - &entry.getattr("cursor_input_relations")?, + &native_field(entry, "cursor_input_relations")?, )?; let effective_size: String = match grain { Some(grain) => resolve_effective_microbatch_batch_size(py, &batch_size, &grain)?, None => batch_size, }; - let creates: bool = entry.getattr("action")?.eq(CREATE_TABLE_ACTION)?; + let creates: bool = native_field(entry, "action")?.eq(CREATE_TABLE_ACTION)?; let count: i128 = count_microbatches(&range, &effective_size, &cursor_type, creates)?; Ok(Some((effective_size, count))) } @@ -94,7 +95,7 @@ fn limited<'py>( action: Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = entry.py(); - if entry.getattr("action")?.eq(SKIP_ACTION)? { + if native_field(entry, "action")?.eq(SKIP_ACTION)? { return Ok((entry.clone(), None)); } let action_text: String = text(&action)?; @@ -104,7 +105,7 @@ fn limited<'py>( Some(max_batches.extract()?) }; let counted: Option<(String, i128)> = batch_count(entry)?; - let name: Bound<'py, PyAny> = entry.getattr("name")?; + let name: Bound<'py, PyAny> = native_field(entry, "name")?; let cap: bool = CAP_ACTIONS.contains(&action_text.as_str()); let warning: Option = match &counted { Some((_, count)) => { @@ -112,12 +113,12 @@ fn limited<'py>( } None => None, }; - let mut range: Bound<'py, PyAny> = entry.getattr("microbatch_range")?; + let mut range: Bound<'py, PyAny> = native_field(entry, "microbatch_range")?; if let (Some(_), Some((size, _)), Some(maximum), true) = (&warning, &counted, maximum, cap && !range.is_none()) { let cursor_type: String = - optional_string(&entry.getattr("cursor_type")?)?.unwrap_or_default(); + optional_string(&native_field(entry, "cursor_type")?)?.unwrap_or_default(); range = capped_microbatch_bounds(range, (size, &cursor_type), maximum, &action_text)?; } let none: Bound<'py, PyAny> = py.None().into_bound(py); @@ -206,13 +207,13 @@ fn default_limit<'py>( if entry .getattr("microbatch_strategy")? .eq(ROLLING_WINDOW_STRATEGY)? - && !entry.getattr("reason")?.eq(FULL_REFRESH_REASON)? - && entry.getattr("start_cursor_override")?.is_none() - && entry.getattr("end_cursor_override")?.is_none() + && !native_field(entry, "reason")?.eq(FULL_REFRESH_REASON)? + && native_field(entry, "start_cursor_override")?.is_none() + && native_field(entry, "end_cursor_override")?.is_none() { return Ok(py.None().into_bound(py)); } - let model_limit: Bound<'py, PyAny> = entry.getattr("microbatch_limit")?; + let model_limit: Bound<'py, PyAny> = native_field(entry, "microbatch_limit")?; Ok(if model_limit.is_truthy()? { model_limit } else { @@ -228,8 +229,8 @@ pub(crate) fn apply_configured_microbatch_limits<'py>( inputs: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = entry.py(); - let configured: Bound<'py, PyAny> = inputs.getattr("max_microbatches")?; - let configured_action: Bound<'py, PyAny> = inputs.getattr("microbatch_limit_action")?; + 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()? @@ -238,7 +239,7 @@ pub(crate) fn apply_configured_microbatch_limits<'py>( registry::class(py, RecordId::MicrobatchLimitAction)?.getattr("ERROR")?; return with_warnings(limited(entry, configured, error)?, Vec::new()); } - let declared: Bound<'py, PyAny> = entry.getattr("declared_microbatch_limit_action")?; + let declared: Bound<'py, PyAny> = native_field(entry, "declared_microbatch_limit_action")?; if declared.is_none() { let maximum: Bound<'py, PyAny> = default_limit(entry, configured)?; return with_warnings(limited(entry, maximum, configured_action)?, Vec::new()); @@ -256,6 +257,6 @@ pub(crate) fn apply_configured_microbatch_limits<'py>( )?; warnings.extend(safety_warning); } - let model_limit: Bound<'py, PyAny> = entry.getattr("microbatch_limit")?; + let model_limit: Bound<'py, PyAny> = native_field(&entry, "microbatch_limit")?; with_warnings(limited(&entry, model_limit, declared)?, warnings) } 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 270bfe373d..52164499b0 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/microbatches.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/microbatches.rs @@ -22,6 +22,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; @@ -82,7 +83,7 @@ fn typed_bounds<'py>( classes: &[RecordId], ) -> PyResult>> { let py: Python<'py> = bounds.py(); - let (start, end) = (bounds.getattr("start")?, bounds.getattr("end")?); + let (start, end) = (native_field(bounds, "start")?, native_field(bounds, "end")?); for value in [&start, &end] { let mut typed: bool = false; for class in classes { @@ -101,8 +102,8 @@ fn integer_bounds(bounds: &Bound<'_, PyAny>) -> PyResult> { return Ok(None); }; Ok(Some(( - start.getattr("value")?.extract()?, - end.getattr("value")?.extract()?, + native_field(&start, "value")?.extract()?, + native_field(&end, "value")?.extract()?, ))) } @@ -184,7 +185,10 @@ pub(crate) fn capped_microbatch_bounds<'py>( CAP_FROM_END => CapDirection::FromEnd, _ => return Ok(bounds), }; - let (start, end) = (bounds.getattr("start")?, bounds.getattr("end")?); + let (start, end) = ( + native_field(&bounds, "start")?, + native_field(&bounds, "end")?, + ); let capped: CappedBound> = if cursor_type == INTEGER_CURSOR { let Some(range) = integer_bounds(&bounds)? else { return Ok(bounds); 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 d66e0fb1a6..1166105cf1 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,6 +32,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -92,15 +93,15 @@ fn schema_columns<'py>( typed_only: bool, ) -> PyResult> { let py: Python<'py> = model.py(); - let entry: Bound<'py, PyAny> = model.getattr("schema_entry")?; + let entry: Bound<'py, PyAny> = native_field(model, "schema_entry")?; if entry.is_none() { return Ok(PyTuple::empty(py)); } let class: Bound<'py, PyAny> = host::entry(py, AdapterHost::ColumnInfo)?; let mut columns: Vec> = Vec::new(); - for column in entry.getattr("columns")?.try_iter()? { + for column in native_field(&entry, "columns")?.try_iter()? { let column: Bound<'py, PyAny> = column?; - let column_type: Bound<'py, PyAny> = column.getattr("type")?; + let column_type: Bound<'py, PyAny> = native_field(&column, "type")?; if typed_only && column_type.is_none() { continue; } @@ -109,7 +110,10 @@ fn schema_columns<'py>( } else { PyString::new(py, "").into_any() }; - columns.push(column_info(&class, (column.getattr("name")?, column_type))?); + columns.push(column_info( + &class, + (native_field(&column, "name")?, column_type), + )?); } PyTuple::new(py, columns) } @@ -163,9 +167,9 @@ fn on_schema_change<'py>(model: &Bound<'py, PyAny>) -> PyResult(model: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = model.py(); - let entry: Bound<'py, PyAny> = model.getattr("schema_entry")?; + let entry: Bound<'py, PyAny> = native_field(model, "schema_entry")?; if !entry.is_none() { - let enforcement: Bound<'py, PyAny> = entry.getattr("type_enforcement")?; + let enforcement: Bound<'py, PyAny> = native_field(&entry, "type_enforcement")?; if !enforcement.is_none() { return Ok(enforcement); } @@ -223,11 +227,11 @@ fn logical_ddl<'py>( let adapter: &Bound<'py, PyAny> = &request.adapter; let model: &Bound<'py, PyAny> = &request.model; let destination: Bound<'py, PyAny> = model_reads::destination(model)?; - let qualified: Bound<'py, PyAny> = destination.getattr("qualified_name")?; + let qualified: Bound<'py, PyAny> = native_field(&destination, "qualified_name")?; let qualified: Bound<'py, PyAny> = if qualified.is_truthy()? { qualified } else { - destination.getattr("name")? + native_field(&destination, "name")? }; let sql: Bound<'py, PyAny> = request.resolved_sql.clone(); let mut pairs: Vec<(&str, Bound<'py, PyAny>)> = vec![("destination", qualified), ("sql", sql)]; @@ -238,8 +242,8 @@ fn logical_ddl<'py>( "incremental_append" => "render_append", "incremental_delete_insert" if !cursor_column.is_none() && !cursor_bounds.is_none() => { let values: Bound<'py, PyAny> = model_values(model)?; - let start = sentinel_to_token(&cursor_bounds.getattr("start")?)?; - let end = sentinel_to_token(&cursor_bounds.getattr("end")?)?; + let start = sentinel_to_token(&native_field(&cursor_bounds, "start")?)?; + let end = sentinel_to_token(&native_field(&cursor_bounds, "end")?)?; pairs.push(("cursor_column", cursor_column)); pairs.push(("cursor_start", PyString::new(py, &start).into_any())); pairs.push(("cursor_end", PyString::new(py, &end).into_any())); @@ -256,7 +260,7 @@ fn logical_ddl<'py>( "incremental_merge" => { let mut source_columns: Vec> = Vec::new(); for column in warehouse_columns.try_iter()? { - source_columns.push(column?.getattr("name")?); + source_columns.push(native_field(&column?, "name")?); } pairs.push(("unique_key", unique_key.clone().into_any())); pairs.push(( @@ -354,8 +358,8 @@ fn ddl_cursor_bounds<'py>(request: &ModelEntryDecisions<'py>) -> PyResult( .eq(ENFORCED_CONTRACT)?, )?; entry.set_item("contract_columns", schema_columns(model, false)?)?; - let schema_entry: Bound<'py, PyAny> = model.getattr("schema_entry")?; + let schema_entry: Bound<'py, PyAny> = native_field(model, "schema_entry")?; entry.set_item( "contract_dynamic_columns", if schema_entry.is_none() { PyTuple::empty(py).into_any() } else { - schema_entry.getattr("dynamic_columns")? + native_field(&schema_entry, "dynamic_columns")? }, )?; Ok(()) @@ -504,7 +508,7 @@ pub(crate) fn assemble_model_plan_entry<'py>( let name: Bound<'py, PyAny> = model_reads::name_object(model)?; let policy: Bound<'py, PyAny> = on_schema_change(model)?; let schema_actions: Bound<'py, PyAny> = - resolve_schema_actions(&change.getattr("schema_findings")?, &policy)?.into_any(); + 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")? @@ -530,7 +534,7 @@ pub(crate) fn assemble_model_plan_entry<'py>( let entry: Bound<'py, PyDict> = PyDict::new(py); entry.set_item("key", model_reads::model_key(model)?)?; entry.set_item("name", &name)?; - entry.set_item("relative_path", model.getattr("relative_path")?)?; + entry.set_item("relative_path", native_field(model, "relative_path")?)?; entry.set_item("destination", model_reads::destination(model)?)?; entry.set_item("fingerprint_query_sql", model_reads::query_sql(model)?)?; entry.set_item("logical_ddl", table_type_ddl(&ddl, &text(&table_type)?))?; @@ -556,7 +560,7 @@ pub(crate) fn assemble_model_plan_entry<'py>( if fingerprint.is_none() { fingerprint } else { - fingerprint.getattr("definition")? + native_field(&fingerprint, "definition")? }, )?; entry.set_item("schema_actions", schema_actions)?; 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 0617f5ce8e..d899781756 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/values.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_strategy/values.rs @@ -14,6 +14,7 @@ use sqlbuild_analysis::planner_strategy::models::{ }; 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; @@ -77,22 +78,22 @@ fn change_kind(value: &str) -> PlanChangeKind { /// One `SchemaFinding` as a native row. fn finding_row(finding: &Bound<'_, PyAny>) -> PyResult { - let kind: SchemaChangeKind = match text(&finding.getattr("kind")?)?.as_str() { + let kind: SchemaChangeKind = match text(&native_field(finding, "kind")?)?.as_str() { "column_added" => SchemaChangeKind::ColumnAdded, "column_removed" => SchemaChangeKind::ColumnRemoved, _ => SchemaChangeKind::ColumnTypeChanged, }; - let source: SchemaColumnSource = if text(&finding.getattr("source")?)? == YML_SOURCE { + let source: SchemaColumnSource = if text(&native_field(finding, "source")?)? == YML_SOURCE { SchemaColumnSource::Yml } else { SchemaColumnSource::SqlAnalysis }; Ok(SchemaFindingRow { kind, - column_name: text(&finding.getattr("column_name")?)?, + column_name: text(&native_field(finding, "column_name")?)?, source, - expected_type: finding.getattr("expected_type")?.extract()?, - actual_type: finding.getattr("actual_type")?.extract()?, + expected_type: native_field(finding, "expected_type")?.extract()?, + actual_type: native_field(finding, "actual_type")?.extract()?, }) } @@ -106,19 +107,19 @@ pub(crate) fn finding_rows(findings: &Bound<'_, PyAny>) -> PyResult) -> PyResult { - let backfill: String = text(&change.getattr("backfill")?.getattr("action")?)?; + let backfill: String = text(&native_field(change, "backfill")?.getattr("action")?)?; Ok(ChangeFacts { - kind: change_kind(&text(&change.getattr("change_kind")?)?), + kind: change_kind(&text(&native_field(change, "change_kind")?)?), backfill_full: backfill == FULL_BACKFILL, backfill_forward_only: backfill == FORWARD_ONLY_BACKFILL, - query_changed: change.getattr("query_changed")?.is_truthy()?, + query_changed: native_field(change, "query_changed")?.is_truthy()?, changed_functions: change .getattr("changed_functions")? .try_iter()? .map(|name| text(&name?)) .collect::>()?, - config_changed: change.getattr("config_changed")?.is_truthy()?, - schema_findings: finding_rows(&change.getattr("schema_findings")?)?, + 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")? .is_truthy()?, 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 e8e8b9cc21..6aaaacff26 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/cursors.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/cursors.rs @@ -28,6 +28,7 @@ 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::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; @@ -168,7 +169,7 @@ fn target_has_cursor_column( }; let wanted = strip(&text(py, cursor_column))?; for column in columns.try_iter()? { - if strip(&column?.getattr("name")?)? == wanted { + if strip(&native_field(&column?, "name")?)? == wanted { return Ok(true); } } @@ -192,16 +193,19 @@ fn runtime_producer_names( return Ok(names); } let mut loaded: HashSet = HashSet::new(); - for source in project.getattr("sources")?.try_iter()? { + for source in native_field(project, "sources")?.try_iter()? { let source = source?; - if !source.getattr("source_entry")?.getattr("loader")?.is_none() { - loaded.insert(source.getattr("name")?.extract()?); + if !native_field(&source, "source_entry")? + .getattr("loader")? + .is_none() + { + loaded.insert(native_field(&source, "name")?.extract()?); } } for key in selected.try_iter()? { let key = key?; - let kind = key.getattr("resource_type")?; - let name: String = key.getattr("name")?.extract()?; + let kind = native_field(&key, "resource_type")?; + let name: String = native_field(&key, "name")?.extract()?; if kind.eq("model")? || kind.eq("seed")? || (kind.eq("source")? && loaded.contains(&name)) { names.insert(name); } @@ -216,18 +220,18 @@ fn upstream_relation<'py>( (deferred, selected): (&Bound<'py, PyAny>, Option<&HashSet>), ) -> PyResult> { let py = reference.py(); - let kind = reference.getattr("ref_kind")?; - let name = reference.getattr("ref_name")?; + let kind = native_field(reference, "ref_kind")?; + 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>| node.getattr("destination")?.getattr("qualified_name"); + |node: Bound<'py, PyAny>| native_field(&node, "destination")?.getattr("qualified_name"); if (kind.eq("ref")? || kind.eq("seed")?) && !deferred.is_none() && deferred.contains(&name)? && !is_selected { - return deferred.get_item(&name)?.getattr("qualified_name"); + return native_field(&deferred.get_item(&name)?, "qualified_name"); } if kind.eq("ref")? { if let Some(model) = maps.models.get_item(&name)? { @@ -246,7 +250,7 @@ fn upstream_relation<'py>( return object( py, render_source_relation( - &source.getattr("source_entry")?, + &native_field(&source, "source_entry")?, Some(adapter).filter(|value| !value.is_none()), )?, ); @@ -273,15 +277,15 @@ fn terminal_bound<'py>( (key, cursor_type): (&str, Option<&str>), ) -> PyResult>> { let py = reference.py(); - if !reference.getattr("ref_kind")?.eq("ref")? { + if !native_field(reference, "ref_kind")?.eq("ref")? { return Ok(None); } - let Some(upstream) = maps.models.get_item(reference.getattr("ref_name")?)? else { + let Some(upstream) = maps.models.get_item(native_field(reference, "ref_name")?)? else { return Ok(None); }; let value = object( py, - config_cursor_bound(&upstream.getattr("config")?.getattr("values")?, key)?, + config_cursor_bound(&native_field(&upstream, "config")?.getattr("values")?, key)?, )?; if value.is_none() { return Ok(None); @@ -296,10 +300,10 @@ fn terminal_bound<'py>( } fn input_grain(reference: &Bound<'_, PyAny>, maps: &ModelMaps<'_>) -> PyResult> { - if !reference.getattr("ref_kind")?.eq("ref")? { + if !native_field(reference, "ref_kind")?.eq("ref")? { return Ok(None); } - match maps.models.get_item(reference.getattr("ref_name")?)? { + match maps.models.get_item(native_field(reference, "ref_name")?)? { Some(upstream) => config_str(&upstream, "cursor_grain"), None => Ok(None), } @@ -310,8 +314,8 @@ fn start_override_set(model: &Bound<'_, PyAny>, overrides: &Bound<'_, PyAny>) -> return Ok(false); } match config_str(model, "cursor_type")?.as_deref() { - Some(CURSOR_TYPE_TIMESTAMP) => Ok(!overrides.getattr("start_ts")?.is_none()), - Some(CURSOR_TYPE_INTEGER) => Ok(!overrides.getattr("start_int")?.is_none()), + Some(CURSOR_TYPE_TIMESTAMP) => Ok(!native_field(overrides, "start_ts")?.is_none()), + Some(CURSOR_TYPE_INTEGER) => Ok(!native_field(overrides, "start_int")?.is_none()), _ => Ok(false), } } @@ -335,13 +339,13 @@ fn collect_models<'py>( Some( producers .try_iter()? - .map(|key| key?.getattr("name")?.extract::()) + .map(|key| native_field(&key?, "name")?.extract::()) .collect::>()?, ) }; let runtime_names = runtime_producer_names(project, &producers)?; let mut models: Vec<_> = Vec::new(); - for model in project.getattr("models")?.try_iter()? { + for model in native_field(project, "models")?.try_iter()? { let model = model?; let name_value = model_reads::name_object(&model)?; let model_name: String = name_value.extract()?; @@ -357,7 +361,8 @@ fn collect_models<'py>( else { continue; }; - let watermark_inputs = resolve_cursor_input_roles(&model)?.getattr("watermark_inputs")?; + let watermark_inputs = + native_field(&resolve_cursor_input_roles(&model)?, "watermark_inputs")?; let watermark_inputs: Vec<(Bound<'py, PyAny>, Bound<'py, PyAny>)> = watermark_inputs .call_method0("items")? .try_iter()? @@ -382,13 +387,13 @@ fn collect_models<'py>( None => None, }; let target_info = existing_relations.get_item(&name_value)?; - let qualified = model_reads::destination(&model)?.getattr("qualified_name")?; + let qualified = native_field(&model_reads::destination(&model)?, "qualified_name")?; if let Some(redirected) = redirected.filter(|value| !value.is_none()) { target_tag = Some(format!("{model_name}__target__max")); target_relation = Some(redirected.extract::()?); } else if !qualified.is_none() && let Some(info) = target_info - && info.getattr("name")?.eq(&name_value)? + && native_field(&info, "name")?.eq(&name_value)? { if target_has_cursor_column( py, @@ -406,7 +411,7 @@ fn collect_models<'py>( let upstream_column: String = upstream_column.extract()?; let reference = lineage_reference(&model, input_name, maps)?; validate_watermark_column(&model, &reference, &upstream_column, maps_tuple(maps))?; - let ref_name: String = reference.getattr("ref_name")?.extract()?; + let ref_name: String = native_field(&reference, "ref_name")?.extract()?; if runtime_names.contains(&ref_name) { continue; } @@ -420,7 +425,7 @@ fn collect_models<'py>( continue; } if !existing_relations.contains(&ref_name)? - && !reference.getattr("ref_kind")?.eq("source")? + && !native_field(&reference, "ref_kind")?.eq("source")? { continue; } @@ -822,7 +827,7 @@ fn eligible_target_maxes<'py>( continue; }; if config.is_none() - || config.getattr("max_ahead")?.is_none() + || native_field(config, "max_ahead")?.is_none() || info.cursor_type.as_deref() != Some(CURSOR_TYPE_TIMESTAMP) || info.target_relation.is_none() || info.has_start_override @@ -839,7 +844,7 @@ fn eligible_target_maxes<'py>( ("discovered_value", render(target_max)?), ("cursor_grain", info.effective_grain.clone()), ("invocation_time", invocation_time.clone()), - ("max_ahead", config.getattr("max_ahead")?), + ("max_ahead", native_field(config, "max_ahead")?), ], )?), )?, @@ -850,7 +855,7 @@ fn eligible_target_maxes<'py>( continue; } let sql = call_with( - &request.adapter.getattr("render_max_cursor_at_or_before")?, + &native_field(&request.adapter, "render_max_cursor_at_or_before")?, &[ ( "relation", @@ -1052,9 +1057,10 @@ fn assemble<'py>( if available.is_none() && let Some(max_value) = max_value { - available = Some( - partition(max_value, &cursor_grain(py, info, upstream)?)?.getattr("end")?, - ); + available = Some(native_field( + &partition(max_value, &cursor_grain(py, info, upstream)?)?, + "end", + )?); } if let Some(available) = available.filter(|value| !value.is_none()) { availability.push(available); diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/fingerprint_cache.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/fingerprint_cache.rs index 66de58712c..f7e93f1c97 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/fingerprint_cache.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/fingerprint_cache.rs @@ -7,6 +7,7 @@ use crate::bindings::_helpers::planner_changes::metadata::without_declared_colum use crate::bindings::_helpers::planner_migrations::fingerprint::migration_fingerprint_ref_names; use crate::bindings::_helpers::planner_phases::objects::object; use crate::bindings::_helpers::planner_phases::objects::{optional_text, text}; +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::exceptions::{PyOSError, PyValueError}; @@ -219,7 +220,7 @@ impl MigrationFingerprintCache { let created = call_with( &match path .getattr("parent") - .and_then(|parent| parent.getattr("mkdir")) + .and_then(|parent| native_field(&parent, "mkdir")) { Ok(mkdir) => mkdir, Err(error) => return (None, Err(error)), 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 64ecf9b14b..5a321299ae 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 @@ -7,6 +7,7 @@ use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::planner_phases::objects::{text, tuple}; use crate::bindings::_helpers::planner_warehouse::constants::ACTIVE_CATALOG; use crate::bindings::_helpers::planner_warehouse::snapshot::{columns_for, list_relations}; +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; use pyo3::pyfunction; use pyo3::types::PyDict as KeywordDict; @@ -18,23 +19,23 @@ fn casefold(value: &Bound<'_, PyAny>) -> PyResult { } fn wanted_name<'py>(entry: &Bound<'py, PyAny>) -> PyResult> { - let table = entry.getattr("table")?; + let table = native_field(entry, "table")?; if table.is_truthy()? { Ok(table) } else { - entry.getattr("name") + native_field(entry, "name") } } fn matches(entry: &Bound<'_, PyAny>, relation: &Bound<'_, PyAny>) -> PyResult { - if casefold(&relation.getattr("name")?)? != casefold(&wanted_name(entry)?)? { + if casefold(&native_field(relation, "name")?)? != casefold(&wanted_name(entry)?)? { return Ok(false); } - let schema = entry.getattr("schema")?; + let schema = native_field(entry, "schema")?; if schema.is_none() { return Ok(true); } - let relation_schema = relation.getattr("schema")?; + let relation_schema = native_field(relation, "schema")?; let relation_schema = if relation_schema.is_truthy()? { relation_schema } else { @@ -64,11 +65,14 @@ fn source_candidates<'py>( )?; let mut scoped: Vec<_> = Vec::new(); for entry in entries { - if !entry.getattr("schema")?.is_none() { + if !native_field(entry, "schema")?.is_none() { scoped.push(entry.clone()); } } - if !catalog.is_none() && catalog.getattr("schema_scoped")?.is_truthy()? && !scoped.is_empty() { + if !catalog.is_none() + && native_field(&catalog, "schema_scoped")?.is_truthy()? + && !scoped.is_empty() + { let mut schemas: BTreeSet<_> = BTreeSet::new(); let mut names: BTreeSet<_> = BTreeSet::new(); for entry in &scoped { @@ -121,7 +125,7 @@ fn source_candidates<'py>( } let unique = PyDict::new(py); for relation in relations { - let identity = relation.getattr("identity")?; + let identity = native_field(&relation, "identity")?; if !unique.contains(&identity)? { unique.set_item(identity, relation)?; } @@ -187,7 +191,7 @@ pub(crate) fn semantic_source_columns<'py>( return Ok(columns.clone()); } let mut required: BTreeSet = BTreeSet::new(); - for model in project.getattr("models")?.try_iter()? { + for model in native_field(project, "models")?.try_iter()? { let model = model?; let analysis = model .getattr("config")? @@ -198,10 +202,10 @@ pub(crate) fn semantic_source_columns<'py>( { continue; } - for reference in model.getattr("references")?.try_iter()? { + for reference in native_field(&model, "references")?.try_iter()? { let reference = reference?; - if reference.getattr("ref_kind")?.eq("source")? { - required.insert(reference.getattr("ref_name")?.extract()?); + if native_field(&reference, "ref_kind")?.eq("source")? { + required.insert(native_field(&reference, "ref_name")?.extract()?); } } } @@ -212,12 +216,12 @@ pub(crate) fn semantic_source_columns<'py>( } let entry = source_read_map.call_method1("get", (&name,))?; if entry.is_none() - || !entry.getattr("expression")?.is_none() - || entry.getattr("contract")?.eq("enforced")? + || !native_field(&entry, "expression")?.is_none() + || native_field(&entry, "contract")?.eq("enforced")? { continue; } - let database = entry.getattr("database")?; + let database = native_field(&entry, "database")?; let mut placed = false; for (group, entries) in groups.iter_mut() { if group.eq(&database)? { @@ -242,7 +246,7 @@ pub(crate) fn semantic_source_columns<'py>( } } if candidates.len() == 1 { - let name = entry.getattr("name")?; + let name = native_field(entry, "name")?; match chosen .iter_mut() .find(|(chosen_name, _)| chosen_name.eq(&name).unwrap_or(false)) @@ -275,9 +279,9 @@ pub(crate) fn semantic_source_columns<'py>( let identity = PyTuple::new( py, [ - lower(relation.getattr("database")?)?, - lower(relation.getattr("schema")?)?, - relation.getattr("name")?.call_method0("lower")?, + lower(native_field(&relation, "database")?)?, + lower(native_field(&relation, "schema")?)?, + native_field(&relation, "name")?.call_method0("lower")?, ], )?; if inspected.contains(&identity)? { 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 b6859b1eec..b588fec416 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/snapshot.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/snapshot.rs @@ -20,6 +20,7 @@ use crate::bindings::_helpers::planner_warehouse::constants::{ RELATIONS_MAIN_COLUMNS, RELATIONS_MAIN_LIST, }; use crate::bindings::_helpers::planner_warehouse::cursors::{CursorInputs, cursor_snapshots}; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use pyo3::IntoPyObject; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; @@ -79,7 +80,7 @@ pub(crate) fn destination_nodes<'py>( } fn selected_node(selected: &Bound<'_, PyAny>, node: &Bound<'_, PyAny>) -> PyResult { - Ok(selected.is_none() || selected.contains(node.getattr("key")?)?) + Ok(selected.is_none() || selected.contains(native_field(node, "key")?)?) } fn relevant_state_keys<'py>( @@ -102,12 +103,12 @@ fn resolve_database<'py>( if !selected_node(selected, &node)? { continue; } - let database = node.getattr("destination")?.getattr("database")?; + let database = native_field(&node, "destination")?.getattr("database")?; if !database.is_none() { return Ok(database); } } - project.getattr("effective_target_database") + native_field(project, "effective_target_database") } fn target_schemas( @@ -119,12 +120,12 @@ fn target_schemas( if !selected_node(selected, &node)? { continue; } - let schema = node.getattr("destination")?.getattr("schema")?; + let schema = native_field(&node, "destination")?.getattr("schema")?; if !schema.is_none() { schemas.insert(schema.extract()?); } } - let fallback = project.getattr("effective_target_schema")?; + let fallback = native_field(project, "effective_target_schema")?; if schemas.is_empty() && !fallback.is_none() { schemas.insert(fallback.extract()?); } @@ -132,11 +133,11 @@ fn target_schemas( } fn source_table_name(entry: &Bound<'_, PyAny>) -> PyResult { - let table = entry.getattr("table")?; + let table = native_field(entry, "table")?; if table.is_truthy()? { table.extract() } else { - entry.getattr("name")?.extract() + native_field(entry, "name")?.extract() } } @@ -146,17 +147,21 @@ fn model_upstream_names( selected_names: &HashSet, ) -> PyResult> { let mut names: BTreeSet = BTreeSet::new(); - for reference in model.getattr("references")?.try_iter()? { + for reference in native_field(model, "references")?.try_iter()? { let reference = reference?; - let kind = reference.getattr("ref_kind")?; - let name: String = reference.getattr("ref_name")?.extract()?; + let kind = native_field(&reference, "ref_kind")?; + let name: String = native_field(&reference, "ref_name")?.extract()?; let is_ref = kind.eq("ref")?; if !(is_ref || kind.eq("seed")?) || selected_names.contains(&name) { continue; } let lookup = if is_ref { models } else { seeds }; if let Some(node) = lookup.get_item(&name)? { - names.insert(node.getattr("destination")?.getattr("name")?.extract()?); + names.insert( + native_field(&node, "destination")? + .getattr("name")? + .extract()?, + ); } } Ok(names) @@ -171,23 +176,27 @@ fn metadata_name_filter( if selected.is_none() { for field in ["models", "seeds"] { for node in project.getattr(field)?.try_iter()? { - names.insert(node?.getattr("destination")?.getattr("name")?.extract()?); + names.insert( + native_field(&node?, "destination")? + .getattr("name")? + .extract()?, + ); } } } else { let selected_names: HashSet = selected .try_iter()? - .map(|key| key?.getattr("name")?.extract::()) + .map(|key| native_field(&key?, "name")?.extract::()) .collect::>()?; let models = by_name(project, "models")?; let seeds = by_name(project, "seeds")?; let sources = PyDict::new(py); - for source in project.getattr("sources")?.try_iter()? { - let entry = source?.getattr("source_entry")?; - sources.set_item(entry.getattr("name")?, entry)?; + for source in native_field(project, "sources")?.try_iter()? { + let entry = native_field(&source?, "source_entry")?; + sources.set_item(native_field(&entry, "name")?, entry)?; } for key in selected.try_iter()? { - let name = key?.getattr("name")?; + let name = native_field(&key?, "name")?; if let Some(model) = models.get_item(&name)? { names.insert( model_reads::destination(&model)? @@ -200,7 +209,11 @@ fn metadata_name_filter( &selected_names, )?); } else if let Some(seed) = seeds.get_item(&name)? { - names.insert(seed.getattr("destination")?.getattr("name")?.extract()?); + names.insert( + native_field(&seed, "destination")? + .getattr("name")? + .extract()?, + ); } else if let Some(entry) = sources.get_item(&name)? { names.insert(source_table_name(&entry)?); } @@ -249,7 +262,7 @@ pub(crate) fn inspection_concurrency( if catalog.is_none() { return Ok(1); } - catalog.getattr("concurrency")?.extract() + native_field(&catalog, "concurrency")?.extract() } fn prefetch_schemas<'py>( @@ -267,21 +280,28 @@ fn prefetch_schemas<'py>( for schema in schemas { scopes.push(PyTuple::new(py, [database.clone(), text(py, schema)])?.into_any()); } - for source in request.project.getattr("sources")?.try_iter()? { + for source in native_field(&request.project, "sources")?.try_iter()? { let source = source?; - let entry = source.getattr("source_entry")?; - if (!relevant.is_none() && !relevant.contains(source.getattr("key")?)?) - || !entry.getattr("expression")?.is_none() - || entry.getattr("schema")?.is_none() + let entry = native_field(&source, "source_entry")?; + if (!relevant.is_none() && !relevant.contains(native_field(&source, "key")?)?) + || !native_field(&entry, "expression")?.is_none() + || native_field(&entry, "schema")?.is_none() { continue; } scopes.push( - PyTuple::new(py, [entry.getattr("database")?, entry.getattr("schema")?])?.into_any(), + PyTuple::new( + py, + [ + native_field(&entry, "database")?, + native_field(&entry, "schema")?, + ], + )? + .into_any(), ); } call_with( - &catalog.getattr("prefetch_schema_listings")?, + &native_field(&catalog, "prefetch_schema_listings")?, &[ ("scopes", tuple(py, scopes)?), ("best_effort", true.into_pyobject(py)?.to_owned().into_any()), @@ -363,16 +383,16 @@ fn gather_relations<'py>( let logical = PyDict::new(py); for node in destination_nodes(&request.project)? { logical.set_item( - location_identity(&node.getattr("destination")?)?, - node.getattr("name")?, + location_identity(&native_field(&node, "destination")?)?, + native_field(&node, "name")?, )?; } let result = PyDict::new(py); let mut states = StateSchemas::default(); for relation in relations.try_iter()? { let relation = relation?; - let name: String = relation.getattr("name")?.extract()?; - let schema = relation.getattr("schema")?; + let name: String = native_field(&relation, "name")?.extract()?; + let schema = native_field(&relation, "schema")?; let schema = if schema.is_none() { None } else { @@ -398,7 +418,7 @@ fn gather_relations<'py>( } continue; } - let identity = relation.getattr("identity")?; + let identity = native_field(&relation, "identity")?; let key = logical .get_item(&identity)? .unwrap_or_else(|| PyString::new(py, &name).into_any()); @@ -414,7 +434,7 @@ fn gather_columns<'py>( let py = request.project.py(); let mut node_names: HashSet = HashSet::new(); for node in destination_nodes(&request.project)? { - node_names.insert(node.getattr("name")?.extract()?); + node_names.insert(native_field(&node, "name")?.extract()?); } let mut needed: Vec<(Bound<'py, PyAny>, Bound<'py, PyAny>)> = Vec::new(); for (logical, relation) in relations.iter() { @@ -437,7 +457,7 @@ fn gather_columns<'py>( )?, )?; for (logical, relation) in needed { - let identity = relation.getattr("identity")?; + let identity = native_field(&relation, "identity")?; if all.contains(&identity)? { result.set_item(logical, all.get_item(identity)?)?; } @@ -451,11 +471,11 @@ fn shares_unfiltered(project: &Bound<'_, PyAny>, selected: &Bound<'_, PyAny>) -> } let names: HashSet = selected .try_iter()? - .map(|key| key?.getattr("name")?.extract::()) + .map(|key| native_field(&key?, "name")?.extract::()) .collect::>()?; let mut all_selected = true; let mut any_migration = false; - for model in project.getattr("models")?.try_iter()? { + for model in native_field(project, "models")?.try_iter()? { let model = model?; if !names.contains(&model_reads::model_name(&model)?) { all_selected = false; @@ -488,7 +508,7 @@ fn gather_fingerprints<'py>( Some( relevant .try_iter()? - .map(|key| key?.getattr("name")?.extract::()) + .map(|key| native_field(&key?, "name")?.extract::()) .collect::>()?, ) }; @@ -530,7 +550,7 @@ fn gather_fingerprints<'py>( ("schema", text(py, schema)), ( "render_qualified_name", - request.adapter.getattr("render_qualified_name")?, + native_field(&request.adapter, "render_qualified_name")?, ), ( "render_read_latest_sql", @@ -550,7 +570,7 @@ fn gather_fingerprints<'py>( ], )?))?; if read_unfiltered { - let by_identity = set.getattr("fingerprints_by_identity")?; + let by_identity = native_field(&set, "fingerprints_by_identity")?; let values: Vec> = if by_identity.is_none() { Vec::new() } else { @@ -569,8 +589,11 @@ fn gather_fingerprints<'py>( )?; } } - for (name, fingerprint) in set.getattr("fingerprints")?.downcast::()?.iter() { - let node_type: String = fingerprint.getattr("node_type")?.extract()?; + for (name, fingerprint) in native_field(&set, "fingerprints")? + .downcast::()? + .iter() + { + let node_type: String = native_field(&fingerprint, "node_type")?.extract()?; if node_type == NODE_TYPE_MODEL { models.set_item(name, fingerprint)?; } else if FUNCTION_NODE_TYPES.contains(&node_type.as_str()) { @@ -579,11 +602,11 @@ fn gather_fingerprints<'py>( seeds.set_item(name, fingerprint)?; } } - let by_identity = set.getattr("fingerprints_by_identity")?; + let by_identity = native_field(&set, "fingerprints_by_identity")?; if !by_identity.is_none() { for item in by_identity.call_method0("items")?.try_iter()? { let (key, fingerprint): (Bound<'py, PyAny>, Bound<'py, PyAny>) = item?.extract()?; - let node_type: String = fingerprint.getattr("node_type")?.extract()?; + let node_type: String = native_field(&fingerprint, "node_type")?.extract()?; if node_type != NODE_TYPE_MODEL && node_type != NODE_TYPE_SEED && !FUNCTION_NODE_TYPES.contains(&node_type.as_str()) @@ -652,7 +675,7 @@ pub(crate) fn warehouse_snapshot<'py>( let cursor_selected = if scope.is_none() { request.selected_keys.clone() } else { - scope.getattr("model_keys")? + native_field(scope, "model_keys")? }; let cursors = cursor_snapshots( request, @@ -802,7 +825,7 @@ pub(crate) fn gather_redirected_cursor_snapshots<'py>( project: project.clone(), adapter: adapter.clone(), connection: connection.clone(), - execute: adapter.getattr("execute")?, + execute: native_field(adapter, "execute")?, selected_keys: frozenset(py, &keys)?, full_refresh: false, full_refresh_model_names: full_refresh_model_names.clone(), @@ -817,10 +840,10 @@ pub(crate) fn gather_redirected_cursor_snapshots<'py>( selected_keys: frozenset(py, &keys)?, full_refresh_model_names: full_refresh_model_names.clone(), deferred_locations: deferred_locations.clone(), - runtime_producer_keys: cursor_scope.getattr("runtime_producer_keys")?, - invocation_time: cursor_scope.getattr("invocation_time")?, - start_cursor_config: cursor_scope.getattr("start_cursor_config")?, - cursor_overrides: cursor_scope.getattr("cursor_overrides")?, + runtime_producer_keys: native_field(cursor_scope, "runtime_producer_keys")?, + invocation_time: native_field(cursor_scope, "invocation_time")?, + start_cursor_config: native_field(cursor_scope, "start_cursor_config")?, + cursor_overrides: native_field(cursor_scope, "cursor_overrides")?, target_relation_overrides: Some(target_relations), origin_cursor_column_overrides: if origin_cursor_columns.is_none() { None @@ -839,7 +862,7 @@ pub(crate) fn gather_redirected_cursor_snapshots<'py>( &snapshot, &[( "target_relation", - model_reads::destination(&model)?.getattr("qualified_name")?, + native_field(&model_reads::destination(&model)?, "qualified_name")?, )], )?, )?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/source_columns.rs b/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/source_columns.rs index 7b3260e026..9ecf191c99 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/source_columns.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/source_columns.rs @@ -7,6 +7,7 @@ use crate::bindings::_helpers::planner_warehouse::constants::{ COLUMN_INFO, METADATA_NAME_FILTER_LIMIT, RELATION_LOOKUP, }; use crate::bindings::_helpers::planner_warehouse::snapshot::{columns_for, list_relations}; +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; use pyo3::pyfunction; use pyo3::types::{PyDict, PyDictMethods, PyTuple}; @@ -21,22 +22,22 @@ fn entries_of<'py>( None => project .getattr("sources")? .try_iter()? - .map(|source| source?.getattr("source_entry")) + .map(|source| native_field(&source?, "source_entry")) .collect(), } } fn table_name(entry: &Bound<'_, PyAny>) -> PyResult { - let table = entry.getattr("table")?; + let table = native_field(entry, "table")?; if table.is_none() { - entry.getattr("name")?.extract() + native_field(entry, "name")?.extract() } else { table.extract() } } fn database_of(entry: &Bound<'_, PyAny>) -> PyResult> { - let database = entry.getattr("database")?; + let database = native_field(entry, "database")?; if database.is_truthy()? { Ok(Some(database.extract()?)) } else { @@ -51,10 +52,10 @@ fn name_filter( ) -> PyResult>> { let mut names: BTreeSet<_> = BTreeSet::new(); for entry in entries { - if !entry.getattr("expression")?.is_none() { + if !native_field(entry, "expression")?.is_none() { continue; } - let schema: Option = entry.getattr("schema")?.extract()?; + let schema: Option = native_field(entry, "schema")?.extract()?; if !schema.is_some_and(|schema| schemas.contains(&schema)) { continue; } @@ -87,11 +88,11 @@ pub(crate) fn source_inspection<'py>( let mut groups: Vec<(String, BTreeSet)> = Vec::new(); let column_info = host::entry(py, COLUMN_INFO)?; for entry in &entries { - let expression = entry.getattr("expression")?; + let expression = native_field(entry, "expression")?; if !expression.is_none() { - if entry.getattr("type_enforcement")?.is_truthy()? { + if native_field(entry, "type_enforcement")?.is_truthy()? { let names = call_with( - &adapter.getattr("query_column_names")?, + &native_field(adapter, "query_column_names")?, &[("connection", connection.clone()), ("sql", expression)], )?; let mut columns: Vec<_> = Vec::new(); @@ -101,11 +102,11 @@ pub(crate) fn source_inspection<'py>( &[("name", name?), ("type", text(py, ""))], )?); } - result.set_item(entry.getattr("name")?, tuple(py, columns)?)?; + result.set_item(native_field(entry, "name")?, tuple(py, columns)?)?; } continue; } - let schema = entry.getattr("schema")?; + let schema = native_field(entry, "schema")?; if schema.is_none() { continue; } @@ -130,11 +131,12 @@ pub(crate) fn source_inspection<'py>( }; let mut scoped: Vec<(String, Bound<'py, PyAny>, Bound<'py, PyAny>)> = Vec::new(); for entry in &entries { - if !entry.getattr("expression")?.is_none() || database_of(entry)?.as_deref() != database + if !native_field(entry, "expression")?.is_none() + || database_of(entry)?.as_deref() != database { continue; } - let schema: Option = entry.getattr("schema")?.extract()?; + let schema: Option = native_field(entry, "schema")?.extract()?; if !schema.is_some_and(|schema| schemas.contains(&schema)) { continue; } @@ -142,14 +144,14 @@ pub(crate) fn source_inspection<'py>( &lookup, &[ ("database", database_value.clone()), - ("schema", entry.getattr("schema")?), + ("schema", native_field(entry, "schema")?), ("name", text(py, &table_name(entry)?)), ], )?; - let entry_name: String = entry.getattr("name")?.extract()?; + let entry_name: String = native_field(entry, "name")?.extract()?; match scoped.iter_mut().find(|(name, _, _)| *name == entry_name) { Some(existing) => existing.2 = identity, - None => scoped.push((entry_name, entry.getattr("name")?, identity)), + None => scoped.push((entry_name, native_field(entry, "name")?, identity)), } } let relations = list_relations( @@ -166,7 +168,7 @@ pub(crate) fn source_inspection<'py>( py, &relations .iter() - .map(|relation| relation.getattr("identity")) + .map(|relation| native_field(relation, "identity")) .collect::>>()?, )?; for (text_name, name, identity) in &scoped { @@ -183,7 +185,7 @@ pub(crate) fn source_inspection<'py>( )?; let mut chosen: Vec<_> = Vec::new(); for relation in &relations { - if wanted.contains(relation.getattr("identity")?)? { + if wanted.contains(native_field(relation, "identity")?)? { chosen.push(relation.clone()); } } 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 c52a74fe9d..4321c6ccdc 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,6 +13,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; use pyo3::pyfunction; @@ -24,10 +25,10 @@ use std::collections::BTreeSet; fn managed_names(project: &Bound<'_, PyAny>) -> PyResult> { let mut names: BTreeSet<_> = BTreeSet::new(); - for source in project.getattr("sources")?.try_iter()? { - let entry = source?.getattr("source_entry")?; - if !entry.getattr("loader")?.is_none() { - names.insert(entry.getattr("name")?.extract()?); + for source in native_field(project, "sources")?.try_iter()? { + let entry = native_field(&source?, "source_entry")?; + if !native_field(&entry, "loader")?.is_none() { + names.insert(native_field(&entry, "name")?.extract()?); } } Ok(names) @@ -51,7 +52,7 @@ fn hook_source_reads( let py = model.py(); let hook_entry = registry::class(py, SQL_HOOK_ENTRY)?; let source_kind = host::entry(py, UserCodeHost::SqlResourceRefKind)?.getattr("SOURCE")?; - let values = model.getattr("config")?.getattr("values")?; + let values = native_field(model, "config")?.getattr("values")?; for key in MODEL_HOOK_CONFIG_KEYS { let value = values.call_method1("get", (key,))?; let entries: Vec> = @@ -64,10 +65,10 @@ fn hook_source_reads( if !entry.is_instance(&hook_entry)? { continue; } - for reference in entry.getattr("reads")?.try_iter()? { + for reference in native_field(&entry, "reads")?.try_iter()? { let reference = reference?; - if reference.getattr("kind")?.is(&source_kind) { - let name: String = reference.getattr("name")?.extract()?; + if native_field(&reference, "kind")?.is(&source_kind) { + let name: String = native_field(&reference, "name")?.extract()?; if managed.contains(&name) { names.push(name); } @@ -84,7 +85,7 @@ fn selected_managed_refs( managed: &BTreeSet, ) -> PyResult> { let mut names: BTreeSet<_> = BTreeSet::new(); - for model in project.getattr("models")?.try_iter()? { + for model in native_field(project, "models")?.try_iter()? { let model = model?; if !selected.contains(model_reads::model_key(&model)?)? { continue; @@ -92,15 +93,18 @@ fn selected_managed_refs( names.extend(sql_source_reads(&model_reads::query_sql(&model)?, managed)?); names.extend(hook_source_reads(&model, managed)?); } - for function in project.getattr("functions")?.try_iter()? { + for function in native_field(project, "functions")?.try_iter()? { let function = function?; - if selected.contains(function.getattr("key")?)? { - names.extend(sql_source_reads(&function.getattr("body_sql")?, managed)?); + if selected.contains(native_field(&function, "key")?)? { + names.extend(sql_source_reads( + &native_field(&function, "body_sql")?, + managed, + )?); } } - for audit in project.getattr("audits")?.try_iter()? { + for audit in native_field(project, "audits")?.try_iter()? { let audit = audit?; - let deps = audit.getattr("scope_deps")?; + let deps = native_field(&audit, "scope_deps")?; if deps.is_truthy()? { let mut any = false; for dep in deps.try_iter()? { @@ -113,14 +117,17 @@ fn selected_managed_refs( continue; } } - for reference in audit.getattr("references")?.try_iter()? { + for reference in native_field(&audit, "references")?.try_iter()? { let reference = reference?; - let name: String = reference.getattr("ref_name")?.extract()?; - if reference.getattr("ref_kind")?.eq("source")? && managed.contains(&name) { + let name: String = native_field(&reference, "ref_name")?.extract()?; + if native_field(&reference, "ref_kind")?.eq("source")? && managed.contains(&name) { names.insert(name); } } - names.extend(sql_source_reads(&audit.getattr("sql_body")?, managed)?); + names.extend(sql_source_reads( + &native_field(&audit, "sql_body")?, + managed, + )?); } Ok(names) } @@ -147,7 +154,7 @@ fn source_target_name<'py>( if !defer_sources_to.is_none() { return Ok(defer_sources_to.clone()); } - let active = project.getattr("effective_target_name")?; + let active = native_field(project, "effective_target_name")?; if active.is_none() || project_config.is_none() || local_config.is_none() { return Ok(py.None().into_bound(py)); } @@ -161,15 +168,15 @@ fn source_target_name<'py>( } fn raw_source_entry<'py>(source: &Bound<'py, PyAny>) -> PyResult> { - let declared = source.getattr("source_entry")?; - let name = declared.getattr("name")?; + 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()? { let entry = entry?; - if entry.getattr("name")?.eq(&name)? { + if native_field(&entry, "name")?.eq(&name)? { return Ok(entry); } } @@ -211,20 +218,24 @@ fn entry_for_environment<'py>( target: &Bound<'py, PyAny>, effective_vars: &Bound<'py, PyAny>, ) -> PyResult> { - if !entry.getattr("expression")?.is_none() { + if !native_field(entry, "expression")?.is_none() { return Ok(entry.clone()); } - let mut database = entry.getattr("database")?; + let mut database = native_field(entry, "database")?; if database.is_none() { - database = target_field(&target.getattr("database")?, &database, effective_vars)?; + database = target_field( + &native_field(target, "database")?, + &database, + effective_vars, + )?; } - let mut schema = entry.getattr("schema")?; + let mut schema = native_field(entry, "schema")?; if schema.is_none() { - let loader_schema = target.getattr("loader_schema")?; + let loader_schema = native_field(target, "loader_schema")?; let target_schema = if loader_schema.is_truthy()? { loader_schema } else { - target.getattr("schema")? + native_field(target, "schema")? }; schema = target_field(&target_schema, &schema, effective_vars)?; } @@ -284,8 +295,8 @@ pub(crate) fn build_source_read_map<'py>( if target_name.is_none() || project_config.is_none() || local_config.is_none() { return Ok(source_map.clone()); } - if !project_config.getattr("targets")?.contains(&target_name)? - && !local_config.getattr("targets")?.contains(&target_name)? + if !native_field(project_config, "targets")?.contains(&target_name)? + && !native_field(local_config, "targets")?.contains(&target_name)? { return Err(planner_error( py, @@ -294,11 +305,13 @@ pub(crate) fn build_source_read_map<'py>( )); } let target = resolve_target(project_config, local_config, target_name)?; - let effective_vars = project.getattr("effective_vars")?; + let effective_vars = native_field(project, "effective_vars")?; let result = source_map.copy()?; - for source in project.getattr("sources")?.try_iter()? { + for source in native_field(project, "sources")?.try_iter()? { let source = source?; - let name: String = source.getattr("source_entry")?.getattr("name")?.extract()?; + let name: String = native_field(&source, "source_entry")? + .getattr("name")? + .extract()?; if !selected.contains(&name) { continue; } @@ -330,9 +343,9 @@ pub(crate) fn with_declared_source_reads<'py>( return Ok(source_map.clone()); } let result = source_map.copy()?; - for source in project.getattr("sources")?.try_iter()? { - let entry = source?.getattr("source_entry")?; - let name: String = entry.getattr("name")?.extract()?; + for source in native_field(project, "sources")?.try_iter()? { + let entry = native_field(&source?, "source_entry")?; + let name: String = native_field(&entry, "name")?.extract()?; if missing.contains(&name) { result.set_item(name, entry)?; } 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 f26f61aa14..3ebd4939b5 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,6 +12,7 @@ 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::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}; @@ -36,11 +37,11 @@ pub(crate) fn source_freshness_unknown_warnings<'py>( .try_iter()? { let unknown = unknown?; - let reason: String = unknown.getattr("reason")?.str()?.extract()?; + let reason: String = native_field(&unknown, "reason")?.str()?.extract()?; if freshness_unknown_message(&reason, &[]).is_none() { continue; } - let name: String = unknown.getattr("source_name")?.extract()?; + let name: String = native_field(&unknown, "source_name")?.extract()?; match reasons.iter_mut().find(|(existing, _)| *existing == reason) { Some((_, names)) => names.push(name), None => reasons.push((reason, vec![name])), @@ -68,7 +69,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 = node.getattr("destination")?.getattr("database")?; + let database = native_field(&node, "destination")?.getattr("database")?; if !database.is_none() { return Ok(database); } @@ -82,19 +83,19 @@ fn state_schemas( ) -> PyResult> { let mut schemas: BTreeSet<_> = BTreeSet::new(); for node in destination_nodes(project)? { - let schema = node.getattr("destination")?.getattr("schema")?; + let schema = native_field(&node, "destination")?.getattr("schema")?; if !schema.is_none() { schemas.insert(schema.extract()?); } } if schemas.is_empty() { - let models = scope.getattr("models_by_name")?; - for name in model_key_names(&scope.getattr("selected_keys")?)? { + let models = native_field(scope, "models_by_name")?; + for name in model_key_names(&native_field(scope, "selected_keys")?)? { let model = models.call_method1("get", (name,))?; if model.is_none() { continue; } - let schema = model_reads::destination(&model)?.getattr("schema")?; + let schema = native_field(&model_reads::destination(&model)?, "schema")?; if !schema.is_none() { schemas.insert(schema.extract()?); } @@ -160,7 +161,7 @@ pub(crate) fn build_planner_source_freshness_result<'py>( let observed_at = imported(py, "datetime", "datetime")? .call_method1("now", (imported(py, "datetime", "UTC")?,))?; let result = within( - &host::entry(py, RUN_SCOPE)?.call1((project.getattr("run_id")?,))?, + &host::entry(py, RUN_SCOPE)?.call1((native_field(project, "run_id")?,))?, |_| { call_with( wrap_pyfunction!(build_direct_source_freshness_planning_result, py)?.as_any(), @@ -177,7 +178,7 @@ pub(crate) fn build_planner_source_freshness_result<'py>( ("run_id", text(py, "planning")), ( "render_qualified_name", - adapter.getattr("render_qualified_name")?, + native_field(adapter, "render_qualified_name")?, ), ("state_table_exists_by_schema", exists.into_any()), ( 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 1dc2ffc0d5..4e05137381 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 @@ -8,6 +8,7 @@ use crate::bindings::_helpers::planner_phases::objects::{planner_error, tuple}; use crate::bindings::_helpers::planner_warehouse::constants::{ MISSING_SOURCE_TABLE_CODE, RUN_BOUNDED, WILDCARD_TABLE_CHARACTER, }; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::references::source_relations::render_source_relation; use pyo3::IntoPyObject; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; @@ -22,21 +23,21 @@ use std::collections::HashMap; fn selected_source_readers(scope: &Bound<'_, PyAny>) -> PyResult>> { let mut readers: HashMap> = HashMap::new(); - let deps = scope.getattr("upstream_deps")?; - for key in scope.getattr("selected_keys")?.try_iter()? { + let deps = native_field(scope, "upstream_deps")?; + for key in native_field(scope, "selected_keys")?.try_iter()? { let key = key?; - if key.getattr("resource_type")?.eq("source")? { + if native_field(&key, "resource_type")?.eq("source")? { continue; } - let reader: String = key.getattr("name")?.extract()?; + let reader: String = native_field(&key, "name")?.extract()?; for dep in deps .call_method1("get", (&key, PyTuple::empty(key.py())))? .try_iter()? { let dep = dep?; - if dep.getattr("resource_type")?.eq("source")? { + if native_field(&dep, "resource_type")?.eq("source")? { readers - .entry(dep.getattr("name")?.extract()?) + .entry(native_field(&dep, "name")?.extract()?) .or_default() .push(reader.clone()); } @@ -46,20 +47,20 @@ fn selected_source_readers(scope: &Bound<'_, PyAny>) -> PyResult) -> PyResult { - let table = entry.getattr("table")?; + let table = native_field(entry, "table")?; if table.is_none() { - entry.getattr("name")?.extract() + native_field(entry, "name")?.extract() } else { table.extract() } } fn external_table(entry: &Bound<'_, PyAny>) -> PyResult { - Ok(!entry.getattr("managed")?.is_truthy()? - && entry.getattr("loader")?.is_none() - && entry.getattr("integration_loader")?.is_none() - && entry.getattr("expression")?.is_none() - && !entry.getattr("schema")?.is_none() + Ok(!native_field(entry, "managed")?.is_truthy()? + && native_field(entry, "loader")?.is_none() + && native_field(entry, "integration_loader")?.is_none() + && native_field(entry, "expression")?.is_none() + && !native_field(entry, "schema")?.is_none() && !table_name(entry)?.contains(WILDCARD_TABLE_CHARACTER)) } @@ -71,14 +72,14 @@ fn read_relation<'py>( } fn declaration_location(source: &Bound<'_, PyAny>) -> PyResult { - let file = source.getattr("source_file")?; + let file = native_field(source, "source_file")?; let path: String = file .getattr("relative_path")? .call_method0("as_posix")? .extract()?; let line = yaml_entry_line( - &file.getattr("contents")?.extract::()?, - &source.getattr("name")?.extract::()?, + &native_field(&file, "contents")?.extract::()?, + &native_field(source, "name")?.extract::()?, ); Ok(match line { None => path, @@ -95,16 +96,16 @@ pub(crate) fn check_selected_source_tables_exist<'py>( ) -> PyResult<()> { let py = project.py(); let readers = selected_source_readers(scope)?; - let read_map = relations.getattr("source_read_map")?; - let listed = relations.getattr("listed_source_names")?; + let read_map = native_field(relations, "source_read_map")?; + let listed = native_field(relations, "listed_source_names")?; let mut unlisted: Vec<(Bound<'py, PyAny>, Bound<'py, PyAny>, Vec)> = Vec::new(); - for source in project.getattr("sources")?.try_iter()? { + for source in native_field(project, "sources")?.try_iter()? { let source = source?; - let name: String = source.getattr("name")?.extract()?; + let name: String = native_field(&source, "name")?.extract()?; let Some(source_readers) = readers.get(&name).filter(|readers| !readers.is_empty()) else { continue; }; - let declared = source.getattr("source_entry")?; + let declared = native_field(&source, "source_entry")?; let entry = read_map.call_method1("get", (&name, &declared))?; if !external_table(&declared)? || !external_table(&entry)? { continue; @@ -119,7 +120,7 @@ pub(crate) fn check_selected_source_tables_exist<'py>( if unlisted.is_empty() { return Ok(()); } - let probe = adapter.getattr("probe_relation_read")?; + let probe = native_field(adapter, "probe_relation_read")?; let mut tasks: Vec<_> = Vec::new(); for (_, entry, _) in &unlisted { let fields = keywords( @@ -132,7 +133,7 @@ pub(crate) fn check_selected_source_tables_exist<'py>( tasks.push(imported(py, "functools", "partial")?.call((&probe,), Some(&fields))?); } let concurrency: i64 = imported(py, "builtins", "int")? - .call1((adapter.getattr("metadata_inspection_concurrency")?,))? + .call1((native_field(adapter, "metadata_inspection_concurrency")?,))? .extract()?; let probes = call_with( &host::entry(py, RUN_BOUNDED)?, @@ -148,13 +149,13 @@ pub(crate) fn check_selected_source_tables_exist<'py>( let mut entries: Vec<_> = Vec::new(); for ((source, entry, read_by), probe) in unlisted.into_iter().zip(probes.try_iter()?) { let probe = probe?; - let status = probe.getattr("status")?; + let status = native_field(&probe, "status")?; if status.eq("readable")? { continue; } - let role = probe.getattr("role")?; + let role = native_field(&probe, "role")?; missing.push(MissingSourceTable { - source_name: source.getattr("name")?.extract()?, + source_name: native_field(&source, "name")?.extract()?, relation: String::new(), read_by, unreadable: status.eq("missing_or_unreadable")?, @@ -175,9 +176,9 @@ pub(crate) fn check_selected_source_tables_exist<'py>( } let message = missing_source_tables_message(&missing); let sources = PyDict::new(py); - for source in project.getattr("sources")?.try_iter()? { + for source in native_field(project, "sources")?.try_iter()? { let source = source?; - sources.set_item(source.getattr("name")?, source)?; + sources.set_item(native_field(&source, "name")?, source)?; } for item in missing.iter_mut().take(10) { if let Some(source) = sources.get_item(&item.source_name)? { 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 879911322b..7e6a3dd1f9 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/watermarks.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/planner_warehouse/watermarks.rs @@ -7,6 +7,7 @@ use crate::bindings::_helpers::planner_strategy::cursor_types::classify_cursor_s use crate::bindings::_helpers::planner_warehouse::constants::{ CURSOR_TYPE_INTEGER, CURSOR_TYPE_TIMESTAMP, WATERMARK_CONTRACT_CODE, }; +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyErr, PyResult}; use pyo3::pyfunction; use pyo3::types::{PyDict, PyDictMethods}; @@ -38,11 +39,13 @@ fn declared_columns<'py>(columns: &Bound<'py, PyAny>) -> PyResult = HashMap::new(); for column in columns.try_iter()? { let column = column?; - let name: String = column.getattr("name")?.extract()?; - let column_type = column.getattr("type")?; + let name: String = native_field(&column, "name")?.extract()?; + let column_type = native_field(&column, "type")?; if !column_type.is_none() { types.insert( - column.getattr("name")?.call_method0("lower")?.extract()?, + native_field(&column, "name")? + .call_method0("lower")? + .extract()?, column_type, ); } @@ -63,8 +66,8 @@ pub(crate) fn validate_watermark_column<'py>( ), ) -> Result<(), PyErr> { let py = model.py(); - let kind = reference.getattr("ref_kind")?; - let ref_name: String = reference.getattr("ref_name")?.extract()?; + let kind = native_field(reference, "ref_kind")?; + let ref_name: String = native_field(reference, "ref_name")?.extract()?; let model_name: String = model_reads::model_name(model)?; let lowered = |value: &str| -> PyResult { text(py, value).call_method0("lower")?.extract() }; @@ -88,30 +91,30 @@ pub(crate) fn validate_watermark_column<'py>( (Some(WATERMARK_CONTRACT_CODE), None), )); } - let values = upstream.getattr("config")?.getattr("values")?; + let values = native_field(&upstream, "config")?.getattr("values")?; if !values.call_method1("get", ("contract",))?.eq("enforced")? { return Ok(()); } - let schema = upstream.getattr("schema_entry")?; + let schema = native_field(&upstream, "schema_entry")?; if schema.is_none() { (Vec::new(), HashMap::new()) } else { - declared_columns(&schema.getattr("columns")?)? + declared_columns(&native_field(&schema, "columns")?)? } } else if kind.eq("source")? { let Some(source) = sources.get_item(&ref_name)? else { return Ok(()); }; - let entry = source.getattr("source_entry")?; - if !entry.getattr("contract")?.eq("enforced")? { + let entry = native_field(&source, "source_entry")?; + if !native_field(&entry, "contract")?.eq("enforced")? { return Ok(()); } - declared_columns(&entry.getattr("columns")?)? + declared_columns(&native_field(&entry, "columns")?)? } else if kind.eq("seed")? { let Some(seed) = seeds.get_item(&ref_name)? else { return Ok(()); }; - declared_columns(&seed.getattr("schema_entry")?.getattr("columns")?)? + declared_columns(&native_field(&seed, "schema_entry")?.getattr("columns")?)? } else { return Ok(()); }; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/presentation_text/functions.rs b/crates/sqlbuild-python/src/bindings/_helpers/presentation_text/functions.rs index 983c9bf881..7cdd6a24f4 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/presentation_text/functions.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/presentation_text/functions.rs @@ -14,6 +14,7 @@ use sqlbuild_core::cli_text::main::split_count_header::split_count_header; use sqlbuild_core::cli_text::models::{AlignedRow, CliStyle, CompletionState, SummaryCount}; use crate::bindings::_helpers::compile_driver::python_calls::KeywordArguments; +use crate::bindings::_helpers::records::fields::field as native_field; const ALIGNED_OPTIONS: (&[&str], &[&str]) = ( &["plain_name", "styled_name", "value", "name_column_width"], @@ -30,7 +31,7 @@ const WARN_STATE: &str = "warn"; /// The native style of a Python `CliStyle`. pub(crate) fn style_of(style: &Bound<'_, PyAny>) -> PyResult { Ok(CliStyle { - use_color: style.getattr("use_color")?.is_truthy()?, + use_color: native_field(style, "use_color")?.is_truthy()?, }) } @@ -141,7 +142,7 @@ fn visible_entries<'py>( entries: Bound<'py, PyAny>, options: &Bound<'py, PyAny>, ) -> PyResult> { - let limit: Bound<'py, PyAny> = options.getattr("max_entries_per_section")?; + let limit: Bound<'py, PyAny> = native_field(options, "max_entries_per_section")?; if limit.is_none() { return Ok(entries); } 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 494a823e2a..fda2adb7e8 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 @@ -9,6 +9,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; @@ -67,7 +68,7 @@ fn column_nullability<'py>( } fn table_function_name(function: &Bound<'_, PyAny>) -> PyResult { - let name: String = function.getattr("name")?.extract()?; + let name: String = native_field(function, "name")?.extract()?; Ok(format!("{TABLE_FUNCTION_PREFIX}{name}")) } @@ -79,7 +80,7 @@ fn declared_relations<'py>(inputs: Bound<'py, PyAny>) -> PyResult = PyDict::new(py); let nulls: Bound<'py, PyDict> = PyDict::new(py); let complete: Bound<'py, PyDict> = PyDict::new(py); - for model in inputs.getattr("model_inputs")?.try_iter()? { + for model in native_field(&inputs, "model_inputs")?.try_iter()? { let model: Bound<'py, PyAny> = model?; let Some(schema) = attached_schema(&model)? else { continue; @@ -102,18 +103,20 @@ fn declared_relations<'py>(inputs: Bound<'py, PyAny>) -> PyResult, Vec)> = Vec::new(); - for seed in inputs.getattr("seed_inputs")?.try_iter()? { - let entry: Bound<'py, PyAny> = seed?.getattr("schema_entry")?; - let name: Bound<'py, PyAny> = entry.getattr("name")?; - let columns: Vec = python_column_facts(&entry.getattr("columns")?, None)?; + for seed in native_field(&inputs, "seed_inputs")?.try_iter()? { + let entry: Bound<'py, PyAny> = native_field(&seed?, "schema_entry")?; + let name: Bound<'py, PyAny> = native_field(&entry, "name")?; + let columns: Vec = + python_column_facts(&native_field(&entry, "columns")?, None)?; types.set_item(&name, column_types(py, &columns, false)?)?; nulls.set_item(&name, column_nullability(&columns, &nullability)?)?; seeds.push((name, columns)); } - for source in inputs.getattr("source_inputs")?.try_iter()? { - let entry: Bound<'py, PyAny> = source?.getattr("source_entry")?; - let name: Bound<'py, PyAny> = entry.getattr("name")?; - let columns: Vec = python_column_facts(&entry.getattr("columns")?, None)?; + for source in native_field(&inputs, "source_inputs")?.try_iter()? { + let entry: Bound<'py, PyAny> = native_field(&source?, "source_entry")?; + let name: Bound<'py, PyAny> = native_field(&entry, "name")?; + let columns: Vec = + python_column_facts(&native_field(&entry, "columns")?, None)?; types.set_item(&name, column_types(py, &columns, false)?)?; nulls.set_item(&name, column_nullability(&columns, &nullability)?)?; if entry.getattr(CONTRACT_KEY)?.eq(ENFORCED_CONTRACT)? && !columns.is_empty() { @@ -125,10 +128,10 @@ fn declared_relations<'py>(inputs: Bound<'py, PyAny>) -> PyResult = function?; let columns: Vec = - python_column_facts(&function.getattr("return_columns")?, None)?; + python_column_facts(&native_field(&function, "return_columns")?, None)?; if columns.is_empty() { continue; } 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 5958548a0f..93de569bf1 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 @@ -24,6 +24,7 @@ use crate::bindings::_helpers::model_config::template_data::record_reads; use crate::bindings::_helpers::project_assembly::models::{ NativeProjectResources, NativeResourceFailure, }; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::ErrorId; use crate::bindings::_helpers::records::registry; use crate::bindings::types::CompilerDetach; @@ -162,7 +163,7 @@ fn tags(value: &Bound<'_, PyAny>) -> PyResult> { } fn model_facts(model_input: &Bound<'_, PyAny>, checks: &Bound<'_, PyAny>) -> PyResult { - let model_file: Bound<'_, PyAny> = model_input.getattr("model_file")?; + let model_file: Bound<'_, PyAny> = native_field(model_input, "model_file")?; let reference_rows: Vec = input_reference_rows(model_input)? .into_iter() .map(|(kind, name, package, _)| (kind, name, package)) @@ -214,8 +215,8 @@ fn project_request<'py>( syntax_checks: &Bound<'py, PyAny>, ) -> PyResult<(ProjectRequest, Vec>)> { let py: Python<'py> = inputs.py(); - let (variables, held) = variables(&inputs.getattr("effective_vars")?)?; - let target: Bound<'py, PyAny> = inputs.getattr("effective_target")?; + let (variables, held) = variables(&native_field(inputs, "effective_vars")?)?; + let target: Bound<'py, PyAny> = native_field(inputs, "effective_target")?; let target: Option = if target.is_none() { None } else { @@ -225,7 +226,8 @@ fn project_request<'py>( loader_schema: optional_text(&target, "loader_schema")?, }) }; - let defaults: Bound<'py, PyAny> = inputs.getattr("project_config")?.getattr("defaults")?; + let defaults: Bound<'py, PyAny> = + native_field(inputs, "project_config")?.getattr("defaults")?; let defaults = SeedDefaults { database: optional_text(&defaults, "database")?, schema: optional_text(&defaults, "schema")?, @@ -235,9 +237,9 @@ fn project_request<'py>( let seed_inputs: Vec> = items(inputs, "seed_inputs")?; let mut seeds: Vec = Vec::with_capacity(seed_inputs.len()); for seed_input in &seed_inputs { - let entry: Bound<'py, PyAny> = seed_input.getattr("schema_entry")?; + let entry: Bound<'py, PyAny> = native_field(seed_input, "schema_entry")?; seeds.push(SeedFacts { - name: entry.getattr("name")?.extract()?, + name: native_field(&entry, "name")?.extract()?, tags: entry .getattr("tags")? .try_iter()? @@ -279,7 +281,7 @@ fn project_request<'py>( let rows = |attribute: &str| -> PyResult>> { items(inputs, attribute)? .into_iter() - .map(|input| input.getattr("assembly_row")) + .map(|input| native_field(&input, "assembly_row")) .collect() }; let sources: Vec = rows("source_inputs")? 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 b63b199356..cf8b344cf6 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 @@ -8,6 +8,7 @@ use sqlbuild_sqltext::compiler::main::placeholder_defaults::placeholder_defaults use crate::bindings::_helpers::attachments::cursor_intrinsics::analysis_cursor_sql; use crate::bindings::_helpers::compiled_project::model_input_view::attached_schema; use crate::bindings::_helpers::project_assembly::syntax_validation::hook_sql_statements; +use crate::bindings::_helpers::records::fields::field as native_field; const HOOK_NAMES: [&str; 2] = ["pre_hooks", "post_hooks"]; @@ -49,7 +50,7 @@ fn analysis_sql<'py>(model_input: &Bound<'py, PyAny>) -> PyResult()?, + &native_field(model_input, "query_sql")?.extract::()?, &config_value(model_input, "cursor_type")?, )?, ) @@ -63,7 +64,7 @@ fn syntax_checks<'py>( ) -> PyResult> { let py: Python<'py> = model_input.py(); let name: String = model_name(model_input)?; - if !model_input.getattr("sql_validation_enabled")?.is_truthy()? + if !native_field(model_input, "sql_validation_enabled")?.is_truthy()? || (!analysis_model_names.is_none() && !analysis_model_names.contains(&name)?) { return Ok(PyTuple::empty(py)); 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 72415f7c9e..7503096c23 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -63,7 +64,7 @@ fn parse_error(context: &SyntaxContext<'_, '_>, sql: &str) -> PyResult) -> PyResult { - let name: Bound<'_, PyAny> = entry.getattr("name")?; + let name: Bound<'_, PyAny> = native_field(entry, "name")?; Ok(if name.is_none() { format!("{prefix} inline_sql(\"...\")") } else { @@ -96,13 +97,13 @@ fn check_hook_value( Some(label) => label, None => sql_entry_label(site.hook_name, value)?, }; - let relative_path: Bound<'_, PyAny> = value.getattr("relative_path")?; + let relative_path: Bound<'_, PyAny> = native_field(value, "relative_path")?; let file_path = if relative_path.is_truthy()? { relative_path } else { site.file_path }; - let statement: Bound<'_, PyAny> = value.getattr("statement")?; + let statement: Bound<'_, PyAny> = native_field(value, "statement")?; return check_hook_value( context, &statement, @@ -214,7 +215,7 @@ pub(crate) fn hook_statements( return Ok(vec![value.extract()?]); } if value.is_instance(sql_hook_entry)? { - return hook_statements(&value.getattr("statement")?, sql_hook_entry); + return hook_statements(&native_field(value, "statement")?, sql_hook_entry); } let mut statements: Vec = Vec::new(); if value.is_instance_of::() || value.is_instance_of::() { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/python_values/values.rs b/crates/sqlbuild-python/src/bindings/_helpers/python_values/values.rs index 7ec29a1a92..ff1b1d724f 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/python_values/values.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/python_values/values.rs @@ -10,6 +10,7 @@ use sqlbuild_core::json::models::JsonInteger; use crate::bindings::_helpers::boundary::panics::NativeCompilerError; use crate::bindings::_helpers::compile_outputs::json_report::float_key; +use crate::bindings::_helpers::records::fields::field as native_field; const DATACLASS_FIELDS: &str = "__dataclass_fields__"; const SURROGATE_KEY: &str = "a JSON artifact mapping key holds a lone surrogate, which the native \ @@ -94,7 +95,7 @@ fn dataclass_value(value: &Bound<'_, PyAny>) -> PyResult { .call1((value,))?; let mut entries: Vec<(PythonKey, PythonValue)> = Vec::new(); for field in fields.try_iter()? { - let name: Bound<'_, PyAny> = field?.getattr("name")?; + let name: Bound<'_, PyAny> = native_field(&field?, "name")?; let item: Bound<'_, PyAny> = value.getattr(name.downcast::()?)?; entries.push((key_value(&name)?, python_value(&item, true)?)); } 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 45ccf21c8d..f1f72fbcec 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/query_command/dag.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/query_command/dag.rs @@ -9,6 +9,7 @@ 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::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; @@ -69,7 +70,7 @@ fn run_dag_command<'py>(py: Python<'py>, options: Option>) -> ("graph", graph), ( "project_name", - discovered.getattr("project_config")?.getattr("name")?, + native_field(&discovered, "project_config")?.getattr("name")?, ), ("python_graph", python_graph), ], 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 11fe3cdf69..257e448d25 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/query_command/project.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/query_command/project.rs @@ -4,6 +4,7 @@ use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; use crate::bindings::_helpers::command_runtime::adapters::resolve_cli_adapter; 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; @@ -29,8 +30,11 @@ pub(crate) fn project_adapter<'py>( let adapter_name: Bound<'py, PyAny> = call_with( ®istry::native(py, NativeAttr::ResolveEffectiveAdapterName)?, &[ - ("project_config", discovered.getattr("project_config")?), - ("local_config", discovered.getattr("local_config")?), + ( + "project_config", + native_field(&discovered, "project_config")?, + ), + ("local_config", native_field(&discovered, "local_config")?), ], )?; let adapter: Bound<'py, PyAny> = resolve_cli_adapter(&adapter_name, &project_dir)?; 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 698ba6b7d9..64d6817660 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/query_command/query.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/query_command/query.rs @@ -20,6 +20,7 @@ use crate::bindings::_helpers::query_command::project::{ProjectAdapter, project_ 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::registry; @@ -164,7 +165,7 @@ fn run_sql<'py>( None => py.None().into_bound(py), }; let queried: PyResult> = call_with( - &project.adapter.getattr("query")?, + &native_field(&project.adapter, "query")?, &[ ("connection", connection.clone()), ("sql", sql), @@ -178,7 +179,7 @@ fn run_sql<'py>( return error; } let value: Bound<'_, PyAny> = error.value(py).clone().into_any(); - let message: PyResult> = match value.getattr("message") { + let message: PyResult> = match native_field(&value, "message") { Ok(message) => message.str().map(Bound::into_any), Err(_) => value.str().map(Bound::into_any), }; @@ -226,14 +227,16 @@ fn render_json( for value in row { match json_cell(value)? { Some(cell) => cells.push(cell), - None => return python_json_rows(result.py(), &result.getattr("columns")?, rows), + None => { + return python_json_rows(result.py(), &native_field(result, "columns")?, rows); + } } } values.push(cells); } match render_query_json(columns, values) { Ok(rendered) => Ok(rendered), - Err(_) => python_json_rows(result.py(), &result.getattr("columns")?, rows), + Err(_) => python_json_rows(result.py(), &native_field(result, "columns")?, rows), } } @@ -258,7 +261,7 @@ fn render(result: &Bound<'_, PyAny>, output_format: &str, limit: Option) -> Ok(QueryDisplay { columns: columns.clone(), rows: texts(&rows, display_text)?, - truncated: result.getattr("truncated")?.is_truthy()?, + truncated: native_field(result, "truncated")?.is_truthy()?, limit, }) }; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/auditing.rs b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/auditing.rs index e963885efc..1a0b1dee3c 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/auditing.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/auditing.rs @@ -8,6 +8,7 @@ use sqlbuild_model_config::header_metadata::main::check_threshold_bound::check_t use sqlbuild_model_config::header_metadata::main::check_threshold_pair::check_threshold_pair; use sqlbuild_model_config::header_metadata::models::{ThresholdBound, ThresholdInput}; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::ErrorId; use crate::bindings::_helpers::records::registry; @@ -62,11 +63,11 @@ fn record_bound(bound: &Bound<'_, PyAny>) -> PyResult> { if bound.is_none() { return Ok(None); } - let operator: String = bound.getattr("operator")?.str()?.extract()?; + let operator: String = native_field(bound, "operator")?.str()?.extract()?; let inputs = ( - threshold_input(&bound.getattr("limit")?)?, - threshold_input(&bound.getattr("lower")?)?, - threshold_input(&bound.getattr("upper")?)?, + threshold_input(&native_field(bound, "limit")?)?, + threshold_input(&native_field(bound, "lower")?)?, + threshold_input(&native_field(bound, "upper")?)?, ); check_threshold_bound(&operator, inputs) .map(Some) 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 2bb74087d9..e890555235 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/compiled.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/compiled.rs @@ -5,6 +5,7 @@ use pyo3::prelude::{Bound, Py, PyAny, PyAnyMethods, PyErr, PyResult, Python}; use pyo3::types::{PyDict, PyDictMethods, PyList, PyListMethods, PyTuple}; use crate::bindings::_helpers::compile_driver::python_calls::imported; +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::rules::relation_keys::entry_relation_key_row; @@ -92,9 +93,9 @@ pub(crate) fn finish_compiler_diagnostic( } let location: Bound<'_, PyAny> = values[DIAGNOSTIC_LOCATION_FIELD].bind(py).clone(); if !location.is_none() { - values[DIAGNOSTIC_PATH_FIELD] = location.getattr("path")?.unbind(); - values[DIAGNOSTIC_LINE_FIELD] = location.getattr("line")?.unbind(); - values[DIAGNOSTIC_COLUMN_FIELD] = location.getattr("column")?.unbind(); + values[DIAGNOSTIC_PATH_FIELD] = native_field(&location, "path")?.unbind(); + values[DIAGNOSTIC_LINE_FIELD] = native_field(&location, "line")?.unbind(); + values[DIAGNOSTIC_COLUMN_FIELD] = native_field(&location, "column")?.unbind(); } Ok(values) } @@ -104,7 +105,9 @@ pub(crate) fn diagnostic_is_error(instance: &Bound<'_, PyAny>) -> PyResult = instance.py(); let error: Bound<'_, PyAny> = registry::class(py, RecordId::DiagnosticSeverity)?.getattr("ERROR")?; - instance.getattr("severity")?.eq(error)?.into_py_any(py) + native_field(instance, "severity")? + .eq(error)? + .into_py_any(py) } /// `CompileSqlReference`: its kind coerced to `SqlReferenceKind`. @@ -159,12 +162,12 @@ pub(crate) fn finish_sql_scenario( values[SCENARIO_SOURCE_PATH_FIELD] = or_fallback( py, &values[SCENARIO_SOURCE_PATH_FIELD], - scenario_file.getattr("relative_path")?, + native_field(&scenario_file, "relative_path")?, ); values[SCENARIO_OWNERSHIP_ROOT_FIELD] = or_fallback( py, &values[SCENARIO_OWNERSHIP_ROOT_FIELD], - scenario_file.getattr("ownership_root")?, + native_field(&scenario_file, "ownership_root")?, ); if !values[SCENARIO_TARGET_MODELS_FIELD].bind(py).is_truthy()? { let targets: Bound<'_, PyAny> = merged_names( @@ -186,19 +189,19 @@ pub(crate) fn finish_sql_test( values[TEST_SOURCE_PATH_FIELD] = or_fallback( py, &values[TEST_SOURCE_PATH_FIELD], - test_file.getattr("relative_path")?, + native_field(&test_file, "relative_path")?, ); values[TEST_OWNERSHIP_ROOT_FIELD] = or_fallback( py, &values[TEST_OWNERSHIP_ROOT_FIELD], - test_file.getattr("ownership_root")?, + native_field(&test_file, "ownership_root")?, ); values[TEST_BLOCK_INDEX_FIELD] = or_fallback( py, &values[TEST_BLOCK_INDEX_FIELD], - test_block.getattr("test_index")?, + native_field(&test_block, "test_index")?, ); - let block_name: Bound<'_, PyAny> = test_block.getattr("name")?; + let block_name: Bound<'_, PyAny> = native_field(&test_block, "name")?; if !block_name.is_none() { values[TEST_EXPLICIT_NAME_FIELD] = or_fallback(py, &values[TEST_EXPLICIT_NAME_FIELD], block_name.clone()); @@ -207,7 +210,7 @@ pub(crate) fn finish_sql_test( let parent: Bound<'_, PyAny> = if block_name.is_truthy()? { block_name } else { - test_file.getattr("relative_path")?.getattr("stem")? + native_field(&test_file, "relative_path")?.getattr("stem")? }; values[TEST_PARENT_NAME_FIELD] = parent.unbind(); } @@ -221,10 +224,10 @@ fn reference_rows(references: &Bound<'_, PyAny>) -> PyResult> { for reference in references.try_iter()? { let reference: Bound<'_, PyAny> = reference?; rows.append(( - reference.getattr("ref_kind")?.str()?, - reference.getattr("ref_name")?, - reference.getattr("ref_package")?, - reference.getattr("call_argument_count")?, + native_field(&reference, "ref_kind")?.str()?, + native_field(&reference, "ref_name")?, + native_field(&reference, "ref_package")?, + native_field(&reference, "call_argument_count")?, ))?; } Ok(rows.into_any().unbind()) @@ -232,17 +235,17 @@ fn reference_rows(references: &Bound<'_, PyAny>) -> PyResult> { /// `CompileModelInput.reference_rows` and `CompiledModel.reference_rows`. pub(crate) fn record_reference_rows(instance: &Bound<'_, PyAny>) -> PyResult> { - reference_rows(&instance.getattr("references")?) + reference_rows(&native_field(instance, "references")?) } /// The declared key facts of model `name` with `instance`'s config and schema entry. fn model_relation_key_row(instance: &Bound<'_, PyAny>, name: String) -> PyResult> { - let values: Bound<'_, PyAny> = instance.getattr("config")?.getattr("values")?; + let values: Bound<'_, PyAny> = native_field(instance, "config")?.getattr("values")?; entry_relation_key_row( "ref", name, values.cast::()?, - &instance.getattr("schema_entry")?, + &native_field(instance, "schema_entry")?, )? .into_py_any(instance.py()) } @@ -259,24 +262,24 @@ pub(crate) fn input_relation_key_row(instance: &Bound<'_, PyAny>) -> PyResult) -> PyResult> { - let name: String = instance.getattr("name")?.extract()?; + let name: String = native_field(instance, "name")?.extract()?; model_relation_key_row(instance, name) } /// `CompileModelInput.attached_schema_name`: the attached schema entry's name, or `None`. pub(crate) fn input_attached_schema_name(instance: &Bound<'_, PyAny>) -> PyResult> { - let entry: Bound<'_, PyAny> = instance.getattr("schema_entry")?; + let entry: Bound<'_, PyAny> = native_field(instance, "schema_entry")?; if entry.is_none() { return Ok(entry.unbind()); } - Ok(entry.getattr("name")?.unbind()) + Ok(native_field(&entry, "name")?.unbind()) } /// `(resource type, name)` of one compiled object key. fn key_row<'py>(key: &Bound<'py, PyAny>) -> PyResult<(String, String)> { Ok(( - key.getattr("resource_type")?.str()?.extract()?, - key.getattr("name")?.extract()?, + native_field(key, "resource_type")?.str()?.extract()?, + native_field(key, "name")?.extract()?, )) } @@ -287,28 +290,28 @@ fn key_rows(keys: &Bound<'_, PyAny>) -> PyResult> { /// `CompiledModel.key_row`. pub(crate) fn model_key_row(instance: &Bound<'_, PyAny>) -> PyResult> { - key_row(&instance.getattr("key")?)?.into_py_any(instance.py()) + key_row(&native_field(instance, "key")?)?.into_py_any(instance.py()) } /// `CompiledModel.dep_rows`. pub(crate) fn model_dep_rows(instance: &Bound<'_, PyAny>) -> PyResult> { - key_rows(&instance.getattr("deps")?)?.into_py_any(instance.py()) + key_rows(&native_field(instance, "deps")?)?.into_py_any(instance.py()) } /// `CompiledModel.destination_row`: `(database, schema, name)` of the destination. pub(crate) fn model_destination_row(instance: &Bound<'_, PyAny>) -> PyResult> { - let destination: Bound<'_, PyAny> = instance.getattr("destination")?; + let destination: Bound<'_, PyAny> = native_field(instance, "destination")?; ( - destination.getattr("database")?, - destination.getattr("schema")?, - destination.getattr("name")?, + native_field(&destination, "database")?, + native_field(&destination, "schema")?, + native_field(&destination, "name")?, ) .into_py_any(instance.py()) } /// `CompiledModel.inferred_rows`: each inferred column as `(name, type, nullability)`. pub(crate) fn model_inferred_rows(instance: &Bound<'_, PyAny>) -> PyResult> { - let columns: Bound<'_, PyAny> = instance.getattr("inferred_columns")?; + let columns: Bound<'_, PyAny> = native_field(instance, "inferred_columns")?; if columns.is_none() { return Ok(columns.unbind()); } @@ -316,9 +319,9 @@ pub(crate) fn model_inferred_rows(instance: &Bound<'_, PyAny>) -> PyResult = column?; rows.append(( - column.getattr("name")?, - column.getattr("type")?, - column.getattr("nullability")?.str()?, + native_field(&column, "name")?, + native_field(&column, "type")?, + native_field(&column, "nullability")?.str()?, ))?; } Ok(rows.into_any().unbind()) @@ -326,33 +329,36 @@ pub(crate) fn model_inferred_rows(instance: &Bound<'_, PyAny>) -> PyResult) -> PyResult> { - let entry: Bound<'_, PyAny> = instance.getattr("schema_entry")?; + let entry: Bound<'_, PyAny> = native_field(instance, "schema_entry")?; if entry.is_none() { return Ok(entry.unbind()); } let rows: Bound<'_, PyList> = PyList::empty(instance.py()); - for column in entry.getattr("columns")?.try_iter()? { + for column in native_field(&entry, "columns")?.try_iter()? { let column: Bound<'_, PyAny> = column?; - rows.append((column.getattr("name")?, column.getattr("type")?))?; + rows.append(( + native_field(&column, "name")?, + native_field(&column, "type")?, + ))?; } Ok(rows.into_any().unbind()) } /// `CompiledModel.has_dynamic_columns`. pub(crate) fn model_has_dynamic_columns(instance: &Bound<'_, PyAny>) -> PyResult> { - let entry: Bound<'_, PyAny> = instance.getattr("schema_entry")?; - let dynamic: bool = !entry.is_none() && entry.getattr("dynamic_columns")?.is_truthy()?; + let entry: Bound<'_, PyAny> = native_field(instance, "schema_entry")?; + let dynamic: bool = !entry.is_none() && native_field(&entry, "dynamic_columns")?.is_truthy()?; dynamic.into_py_any(instance.py()) } /// `CompiledModel.output_column_count`: inferred, else declared, columns, else zero. pub(crate) fn model_output_column_count(instance: &Bound<'_, PyAny>) -> PyResult> { - let inferred: Bound<'_, PyAny> = instance.getattr("inferred_columns")?; - let entry: Bound<'_, PyAny> = instance.getattr("schema_entry")?; + let inferred: Bound<'_, PyAny> = native_field(instance, "inferred_columns")?; + let entry: Bound<'_, PyAny> = native_field(instance, "schema_entry")?; let count: usize = if !inferred.is_none() { inferred.len()? } else if !entry.is_none() { - entry.getattr("columns")?.len()? + native_field(&entry, "columns")?.len()? } else { 0 }; @@ -365,20 +371,20 @@ pub(crate) fn build_context_declaration_expansion( ) -> PyResult> { let py: Python<'_> = instance.py(); let declarations: Bound<'_, PyDict> = PyDict::new(py); - declarations.set_item("enums", instance.getattr("public_enums")?)?; - declarations.set_item("constants", instance.getattr("public_constants")?)?; + declarations.set_item("enums", native_field(instance, "public_enums")?)?; + declarations.set_item("constants", native_field(instance, "public_constants")?)?; let keywords: Bound<'_, PyDict> = PyDict::new(py); keywords.set_item( "declarations", registry::class(py, RecordId::DeclarationResolutionContext)? .call((), Some(&declarations))?, )?; - keywords.set_item("value_renderer", instance.getattr("value_renderer")?)?; + keywords.set_item("value_renderer", native_field(instance, "value_renderer")?)?; keywords.set_item( "collection_rendering", - instance.getattr("collection_rendering")?, + native_field(instance, "collection_rendering")?, )?; - keywords.set_item("resolver", instance.getattr("declaration_resolver")?)?; + keywords.set_item("resolver", native_field(instance, "declaration_resolver")?)?; Ok(registry::class(py, RecordId::DeclarationExpansionContext)? .call((), Some(&keywords))? .unbind()) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/config.rs b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/config.rs index 5278ad1d62..ae02ee0d2e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/config.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/config.rs @@ -1,8 +1,9 @@ //! Computed attributes of the natively hosted project configuration records. -use pyo3::prelude::{Bound, Py, PyAny, PyAnyMethods, PyResult}; +use crate::bindings::_helpers::records::fields::field as native_field; +use pyo3::prelude::{Bound, Py, PyAny, PyResult}; /// `SettingsConfig.sql_validation`: the legacy name of the unified SQL analysis gate. pub(crate) fn settings_sql_validation(instance: &Bound<'_, PyAny>) -> PyResult> { - Ok(instance.getattr("sql_analysis")?.unbind()) + Ok(native_field(instance, "sql_analysis")?.unbind()) } 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 803d2a8ebd..18e18bd252 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/cursors.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/cursors.rs @@ -7,6 +7,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; @@ -60,7 +61,7 @@ fn bound<'py>(raw: &Bound<'py, PyAny>) -> PyResult> { } for member in sentinel.try_iter()? { let member: Bound<'py, PyAny> = member?; - if raw.eq(member.getattr("value")?)? { + if raw.eq(native_field(&member, "value")?)? { return Ok(member); } } @@ -185,5 +186,5 @@ pub(crate) fn finish_input_relation( /// `CursorInputRelation.is_runtime_owned`: whether bounds are read after upstream execution. pub(crate) fn input_relation_runtime_owned(instance: &Bound<'_, PyAny>) -> PyResult> { - Ok(instance.getattr("is_runtime_produced")?.unbind()) + Ok(native_field(instance, "is_runtime_produced")?.unbind()) } 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 b2bdc75cf3..78d83afa8e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/discovery.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/discovery.rs @@ -4,6 +4,7 @@ use pyo3::IntoPyObjectExt; use pyo3::prelude::{Bound, Py, PyAny, PyAnyMethods, PyResult}; use crate::bindings::_helpers::graph::views::lineage_graph_views; +use crate::bindings::_helpers::records::fields::field as native_field; /// SQL scalar type labels by logical value kind. const SCALAR_TYPE_LABELS: [(&str, &str); 6] = [ @@ -25,8 +26,8 @@ pub(crate) fn constant_logical_type(declaration: &Bound<'_, PyAny>) -> PyResult< /// `ConstantDeclaration.scalar_type`: the SQL scalar type label, or the upper-cased logical type. pub(crate) fn constant_scalar_type(declaration: &Bound<'_, PyAny>) -> PyResult> { - let value: Bound<'_, PyAny> = declaration.getattr("value")?; - let kind: String = value.getattr("kind")?.str()?.extract()?; + let value: Bound<'_, PyAny> = native_field(declaration, "value")?; + let kind: String = native_field(&value, "kind")?.str()?.extract()?; if let Some((_, label)) = SCALAR_TYPE_LABELS.iter().find(|(name, _)| *name == kind) { return label.into_py_any(declaration.py()); } @@ -39,12 +40,14 @@ pub(crate) fn constant_scalar_type(declaration: &Bound<'_, PyAny>) -> PyResult

) -> PyResult> { - let native = graph.getattr("native")?; + let native = native_field(graph, "native")?; Ok(lineage_graph_views(graph.py(), native.extract()?)?.unbind()) } fn view(graph: &Bound<'_, PyAny>, name: &str) -> PyResult> { - Ok(graph.getattr("lineage_views")?.getattr(name)?.unbind()) + Ok(native_field(graph, "lineage_views")? + .getattr(name)? + .unbind()) } /// `ProjectGraph.upstream_deps`. 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 46314aded6..5d07014474 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 @@ -4,6 +4,7 @@ use pyo3::prelude::{Bound, Py, PyAny, PyAnyMethods, PyResult, Python}; use pyo3::types::{PyDict, PyDictMethods, PyString, PyTuple}; use crate::bindings::_helpers::record_behaviours::call_arguments::call_argument; +use crate::bindings::_helpers::records::fields::field as native_field; const HELP_FIELD: &str = "help"; const LOCATION_FIELD: &str = "location"; @@ -27,7 +28,7 @@ fn initialized( let py: Python<'_> = error.py(); error.setattr("args", PyTuple::new(py, [message])?)?; error.setattr("message", message)?; - if let Ok(code) = error.get_type().getattr("code") { + if let Ok(code) = native_field(&error.get_type(), "code") { error.setattr("code", code)?; } error.setattr("help", help) @@ -62,7 +63,10 @@ pub(crate) fn macro_argument_error_init( let position: Bound<'_, PyAny> = field("relative_position")?; (position.get_item(0)?, position.get_item(1)?) } else { - (location.getattr("line")?, location.getattr("column")?) + ( + native_field(&location, "line")?, + native_field(&location, "column")?, + ) }; let (scope, suffix): (String, String) = if location.is_none() { ( @@ -102,7 +106,7 @@ fn macro_argument_copy<'py>( } keywords.set_item(name, error.getattr(name)?)?; } - let help: Bound<'py, PyAny> = error.getattr("help")?; + let help: Bound<'py, PyAny> = native_field(error, "help")?; if !help.is_truthy()? { keywords.set_item("help", "")?; } @@ -119,7 +123,7 @@ pub(crate) fn macro_argument_error_shifted( kwargs: Option<&Bound<'_, PyDict>>, ) -> PyResult> { let by: Bound<'_, PyAny> = call_argument((args, kwargs), "by", 0)?; - let offset: Bound<'_, PyAny> = error.getattr("offset")?.add(by)?; + let offset: Bound<'_, PyAny> = native_field(error, "offset")?.add(by)?; Ok(macro_argument_copy(error, &[("offset", offset)])?.unbind()) } @@ -142,12 +146,12 @@ pub(crate) fn sql_test_reference_error_init( let py: Python<'_> = error.py(); let message: Bound<'_, PyAny> = call_argument((args, kwargs), "message", 0)?; let location: Bound<'_, PyAny> = keyword(py, kwargs, "location")?; - let path: Bound<'_, PyAny> = location.getattr("path")?.call_method0("as_posix")?; + let path: Bound<'_, PyAny> = native_field(&location, "path")?.call_method0("as_posix")?; let text: String = format!( "{}:{}:{}: {}", path.str()?, - location.getattr("line")?.str()?, - location.getattr("column")?.str()?, + native_field(&location, "line")?.str()?, + native_field(&location, "column")?.str()?, message.str()? ); initialized( 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 1160ad05c2..92ca032d2c 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/lexical.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/lexical.rs @@ -10,6 +10,7 @@ use sqlbuild_sqltext::sql_scan::main::reads_differently_from_generic::reads_diff use sqlbuild_sqltext::sql_scan::main::unsupported_line_comment_prefixes::unsupported_line_comment_prefixes; use sqlbuild_sqltext::sql_scan::models::LexicalSyntax; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::ErrorId; use crate::bindings::_helpers::records::registry; @@ -30,12 +31,12 @@ fn sorted_texts(values: &Bound<'_, PyAny>) -> PyResult> { fn record_syntax(instance: &Bound<'_, PyAny>) -> PyResult { let flag = |name: &str| -> PyResult { instance.getattr(name)?.is_truthy() }; Ok(LexicalSyntax { - backslash_escape_quotes: sorted_texts(&instance.getattr("backslash_escape_quotes")?)?, + backslash_escape_quotes: sorted_texts(&native_field(instance, "backslash_escape_quotes")?)?, escape_string_prefix: flag("escape_string_prefix")?, raw_string_prefix: flag("raw_string_prefix")?, triple_quoted_strings: flag("triple_quoted_strings")?, nested_block_comments: flag("nested_block_comments")?, - line_comment_prefixes: sorted_texts(&instance.getattr("line_comment_prefixes")?)?, + line_comment_prefixes: sorted_texts(&native_field(instance, "line_comment_prefixes")?)?, }) } 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 144ea2cdb4..6427234029 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,6 +6,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; @@ -48,7 +49,7 @@ fn resource_name<'py>( facts: &Bound<'py, PyAny>, index: &Bound<'py, PyAny>, ) -> PyResult> { - let names: Bound<'py, PyAny> = facts.getattr("resource_names")?; + let names: Bound<'py, PyAny> = native_field(facts, "resource_names")?; let name: Bound<'py, PyAny> = names.call_method1("get", (index,))?; if !name.is_none() { return Ok(name); @@ -56,7 +57,7 @@ fn resource_name<'py>( let pooled: Bound<'py, PyAny> = facts .getattr("resource_name_indexes")? .call_method1("get", (index, index))?; - facts.getattr("string_pool")?.get_item(pooled) + native_field(facts, "string_pool")?.get_item(pooled) } /// `CompactLineageFacts.resource_name(index)`. @@ -72,7 +73,7 @@ pub(crate) fn facts_resource_name( /// One compact row projected to a `CompiledLineageColumnFact`. fn project_row(facts: &Bound<'_, PyAny>, row: &Bound<'_, PyAny>) -> PyResult> { let py: Python<'_> = facts.py(); - let pool: Bound<'_, PyAny> = facts.getattr("string_pool")?; + let pool: Bound<'_, PyAny> = native_field(facts, "string_pool")?; let (name_index, transform_code, confidence_code, sources): ( Bound<'_, PyAny>, usize, @@ -129,7 +130,9 @@ pub(crate) fn facts_len( _args: &Bound<'_, PyTuple>, _kwargs: Option<&Bound<'_, PyDict>>, ) -> PyResult> { - instance.getattr("rows")?.len()?.into_py_any(instance.py()) + native_field(instance, "rows")? + .len()? + .into_py_any(instance.py()) } /// `facts[index]`: one fact, or a tuple of facts for a slice. @@ -139,7 +142,7 @@ pub(crate) fn facts_getitem( _kwargs: Option<&Bound<'_, PyDict>>, ) -> PyResult> { let index: Bound<'_, PyAny> = args.get_item(0)?; - let projected: Bound<'_, PyAny> = instance.getattr("facts")?; + let projected: Bound<'_, PyAny> = native_field(instance, "facts")?; if index.is_instance_of::() { return Ok(projected.get_item(index)?.unbind()); } @@ -162,7 +165,10 @@ pub(crate) fn facts_iter( _args: &Bound<'_, PyTuple>, _kwargs: Option<&Bound<'_, PyDict>>, ) -> PyResult> { - Ok(instance.getattr("facts")?.try_iter()?.into_any().unbind()) + Ok(native_field(instance, "facts")? + .try_iter()? + .into_any() + .unbind()) } /// `facts == other`: item-wise equality with any sequence. @@ -177,7 +183,7 @@ pub(crate) fn facts_eq( if !other.is_instance(&sequence)? { return Ok(py.NotImplemented()); } - let mine: Bound<'_, PyAny> = instance.getattr("facts")?; + let mine: Bound<'_, PyAny> = native_field(instance, "facts")?; let theirs: Bound<'_, PyTuple> = PyTuple::new( py, other 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 2ba6bba172..9acd116b41 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/macros.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/macros.rs @@ -7,6 +7,7 @@ use pyo3::types::{PyDict, PyList, PyTuple, PyTupleMethods}; use crate::bindings::_helpers::graph::project_graph::{ResourceRow, project_graph}; use crate::bindings::_helpers::record_behaviours::call_arguments::call_argument; use crate::bindings::_helpers::record_behaviours::compiled::compile_input_error; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::render::declaration_values::{ constant_declaration_sql, render_enum_member_value, }; @@ -24,7 +25,7 @@ pub(crate) fn macro_injects_context(instance: &Bound<'_, PyAny>) -> PyResult) -> PyResult = first.getattr("kind")?; + let kind: Bound<'_, PyAny> = native_field(&first, "kind")?; let mut positional: bool = false; for name in POSITIONAL_KINDS { - positional |= kind.eq(inspect.getattr("Parameter")?.getattr(name)?)?; + positional |= kind.eq(native_field(&inspect, "Parameter")?.getattr(name)?)?; } - let named: bool = first.getattr("name")?.eq(MACRO_CONTEXT_PARAMETER_NAME)?; + let named: bool = native_field(&first, "name")?.eq(MACRO_CONTEXT_PARAMETER_NAME)?; (positional && named).into_py_any(py) } @@ -50,14 +51,14 @@ pub(crate) fn context_render_constant( ) -> PyResult> { let py: Python<'_> = instance.py(); let name: Bound<'_, PyAny> = call_argument((args, kwargs), "name", 0)?; - let value_renderer: Bound<'_, PyAny> = instance.getattr("_value_renderer")?; + let value_renderer: Bound<'_, PyAny> = native_field(instance, "_value_renderer")?; if value_renderer.is_none() { return Err(compile_input_error( py, "Constant SQL rendering is available only during a project macro invocation", )?); } - let _ = instance.getattr("constants")?.get_item(&name)?; + let _ = native_field(instance, "constants")?.get_item(&name)?; let declaration: Bound<'_, PyAny> = instance .getattr("_constant_declarations")? .call_method1("get", (&name,))?; @@ -70,7 +71,7 @@ pub(crate) fn context_render_constant( constant_declaration_sql( &declaration, &value_renderer, - (&instance.getattr("_collection_rendering")?, None), + (&native_field(instance, "_collection_rendering")?, None), )? .into_py_any(py) } @@ -98,8 +99,8 @@ pub(crate) fn context_render_enum_member( /// `(resource type, name)` of one compiled object key. fn graph_key(key: &Bound<'_, PyAny>) -> PyResult<(String, String)> { Ok(( - key.getattr("resource_type")?.str()?.extract()?, - key.getattr("name")?.extract()?, + native_field(key, "resource_type")?.str()?.extract()?, + native_field(key, "name")?.extract()?, )) } @@ -134,8 +135,8 @@ fn resource_rows( let resource: Bound<'_, PyAny> = resource?; let (tags, folder) = describe(&resource)?; rows.push(( - graph_key(&resource.getattr("key")?)?, - graph_keys(&resource.getattr("deps")?)?, + graph_key(&native_field(&resource, "key")?)?, + graph_keys(&native_field(&resource, "deps")?)?, tags, folder, )); @@ -186,11 +187,17 @@ fn function_tags(function: &Bound<'_, PyAny>) -> PyResult { /// `CompiledProject.lineage_graph`: lineage edges, tags, folders and names indexed natively. pub(crate) fn project_lineage_graph(instance: &Bound<'_, PyAny>) -> PyResult> { let mut rows: Vec = Vec::new(); - rows.extend(resource_rows(&instance.getattr("models")?, model_tags)?); - rows.extend(resource_rows(&instance.getattr("sources")?, source_tags)?); - rows.extend(resource_rows(&instance.getattr("seeds")?, seed_tags)?); rows.extend(resource_rows( - &instance.getattr("functions")?, + &native_field(instance, "models")?, + model_tags, + )?); + rows.extend(resource_rows( + &native_field(instance, "sources")?, + source_tags, + )?); + rows.extend(resource_rows(&native_field(instance, "seeds")?, seed_tags)?); + rows.extend(resource_rows( + &native_field(instance, "functions")?, function_tags, )?); project_graph(rows)?.into_py_any(instance.py()) 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 7df46ea42a..2b7d5b410e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/migrations.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/migrations.rs @@ -10,6 +10,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; @@ -83,7 +84,7 @@ fn lowered<'py>(text: &Bound<'py, PyAny>) -> PyResult> { /// `(database.lower() or None, (schema or "").lower(), name.lower())`. fn relation_identity<'py>(relation: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = relation.py(); - let schema: Bound<'py, PyAny> = relation.getattr("schema")?; + let schema: Bound<'py, PyAny> = native_field(relation, "schema")?; let schema: Bound<'py, PyAny> = if schema.is_none() { "".into_bound_py_any(py)? } else { @@ -92,9 +93,9 @@ fn relation_identity<'py>(relation: &Bound<'py, PyAny>) -> PyResult>, ) -> PyResult> { let relation: Bound<'_, PyAny> = call_argument((args, kwargs), "relation", 0)?; - let mentions: bool = relations_match(&event.getattr("origin")?, &relation)? - || relations_match(&event.getattr("destination")?, &relation)?; + let mentions: bool = relations_match(&native_field(event, "origin")?, &relation)? + || relations_match(&native_field(event, "destination")?, &relation)?; mentions.into_py_any(event.py()) } @@ -145,9 +146,10 @@ pub(crate) fn column_event_mentions( ) -> PyResult> { let relation: Bound<'_, PyAny> = call_argument((args, kwargs), "relation", 0)?; let columns: Bound<'_, PyAny> = call_argument((args, kwargs), "columns", 1)?; - let mentions: bool = relations_match(&event.getattr("relation")?, &relation)? - && (columns.contains(event.getattr("origin_column")?.call_method0("lower")?)? - || columns.contains(event.getattr("destination_column")?.call_method0("lower")?)?); + let mentions: bool = relations_match(&native_field(event, "relation")?, &relation)? + && (columns.contains(native_field(event, "origin_column")?.call_method0("lower")?)? + || columns + .contains(native_field(event, "destination_column")?.call_method0("lower")?)?); mentions.into_py_any(event.py()) } @@ -177,17 +179,17 @@ pub(crate) fn history_status( ) -> PyResult> { let py: Python<'_> = history.py(); let now: Bound<'_, PyAny> = call_argument((args, kwargs), "now", 0)?; - let created: Bound<'_, PyAny> = history.getattr("created")?; - let status: &str = if !history.getattr("dropped")?.is_none() { + let created: Bound<'_, PyAny> = native_field(history, "created")?; + let status: &str = if !native_field(history, "dropped")?.is_none() { "DROPPED" } else if !created.is_none() { - let expires_at: Bound<'_, PyAny> = created.getattr("expires_at")?; + let expires_at: Bound<'_, PyAny> = native_field(&created, "expires_at")?; if !expires_at.is_none() && expires_at.le(&now)? { "EXPIRED" } else { "LIVE" } - } else if !history.getattr("archived")?.is_none() { + } else if !native_field(history, "archived")?.is_none() { "PENDING_VIEW" } else { "PENDING_ARCHIVE" @@ -199,10 +201,10 @@ pub(crate) fn history_status( /// `OldNameViewHistory.old`: the relation name the compatibility view occupies. pub(crate) fn history_old(history: &Bound<'_, PyAny>) -> PyResult> { - Ok(history.getattr("required")?.getattr("old")?.unbind()) + Ok(native_field(history, "required")?.getattr("old")?.unbind()) } /// `OldNameViewHistory.new`: the relation the compatibility view reads. pub(crate) fn history_new(history: &Bound<'_, PyAny>) -> PyResult> { - Ok(history.getattr("required")?.getattr("new")?.unbind()) + Ok(native_field(history, "required")?.getattr("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 270d52fef3..f7f6d813a8 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,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -40,7 +41,7 @@ fn flag(py: Python<'_>, value: bool) -> PyResult> { /// `RetentionPlanEntry.decreases`: whether the entry lowers any live retention value. pub(crate) fn retention_decreases(instance: &Bound<'_, PyAny>) -> PyResult> { - let direction: String = text(&instance.getattr("direction")?)?; + let direction: String = text(&native_field(instance, "direction")?)?; flag( instance.py(), DECREASING_DIRECTIONS.contains(&direction.as_str()), @@ -49,7 +50,7 @@ pub(crate) fn retention_decreases(instance: &Bound<'_, PyAny>) -> PyResult) -> PyResult> { - let action: String = text(&instance.getattr("action")?)?; + let action: String = text(&native_field(instance, "action")?)?; flag(instance.py(), STEP_ACTIONS.contains(&action.as_str())) } @@ -61,7 +62,7 @@ pub(crate) fn plan_python_source_entries(instance: &Bound<'_, PyAny>) -> PyResul return Ok(entries.unbind()); } } - Ok(instance.getattr("source_map")?.unbind()) + Ok(native_field(instance, "source_map")?.unbind()) } /// `MaterializationType.is_table_backed(materialized=...)`. @@ -79,7 +80,7 @@ pub(crate) fn materialization_is_table_backed( } fn decision_traits(entry: &Bound<'_, PyAny>) -> PyResult<(String, DecisionTraits)> { - let decision: String = text(&entry.getattr("decision")?)?; + let decision: String = text(&native_field(entry, "decision")?)?; let traits: DecisionTraits = migration_decision_traits(&decision); Ok((decision, traits)) } @@ -88,10 +89,10 @@ fn model_facts(entry: &Bound<'_, PyAny>) -> PyResult { let (decision, traits) = decision_traits(entry)?; Ok(ModelMigrationFacts { origin_missing: decision == ORIGIN_MISSING, - origin_tracked: entry.getattr("origin_tracked")?.is_truthy()?, - deny_missing_origin: text(&entry.getattr("missing_origin_policy")?)? == DENY_POLICY, + origin_tracked: native_field(entry, "origin_tracked")?.is_truthy()?, + deny_missing_origin: text(&native_field(entry, "missing_origin_policy")?)? == DENY_POLICY, decision: traits, - incompatible: text(&entry.getattr("compatibility")?)? == INCOMPATIBLE, + incompatible: text(&native_field(entry, "compatibility")?)? == INCOMPATIBLE, }) } @@ -108,8 +109,8 @@ pub(crate) fn model_migration_origin_hidden(entry: &Bound<'_, PyAny>) -> PyResul /// `ModelMigrationPlanEntry.storage_transition`: the table-type transition label, when known. pub(crate) fn model_migration_storage_transition(entry: &Bound<'_, PyAny>) -> PyResult> { - let origin: bool = entry.getattr("origin_is_transient")?.is_truthy()?; - let stage: Bound<'_, PyAny> = entry.getattr("stage_is_transient")?; + let origin: bool = native_field(entry, "origin_is_transient")?.is_truthy()?; + let stage: Bound<'_, PyAny> = native_field(entry, "stage_is_transient")?; let stage: Option = if stage.is_none() { None } else { @@ -120,7 +121,7 @@ pub(crate) fn model_migration_storage_transition(entry: &Bound<'_, PyAny>) -> Py /// `ModelMigrationPlanEntry.target_label`: the target name, naming the unnamed target `default`. pub(crate) fn model_migration_target_label(entry: &Bound<'_, PyAny>) -> PyResult> { - let target: Bound<'_, PyAny> = entry.getattr("target_name")?; + let target: Bound<'_, PyAny> = native_field(entry, "target_name")?; if target.is_truthy()? { return Ok(target.unbind()); } @@ -136,17 +137,17 @@ pub(crate) fn model_migration_blocks_build(entry: &Bound<'_, PyAny>) -> PyResult pub(crate) fn column_migration_origin_missing(entry: &Bound<'_, PyAny>) -> PyResult> { flag( entry.py(), - text(&entry.getattr("decision")?)? == SOURCE_MISSING, + text(&native_field(entry, "decision")?)? == SOURCE_MISSING, ) } /// `ColumnMigrationPlanEntry.blocks_build`. pub(crate) fn column_migration_blocks_build(entry: &Bound<'_, PyAny>) -> PyResult> { - let decision: Bound<'_, PyAny> = entry.getattr("decision")?; + let decision: Bound<'_, PyAny> = native_field(entry, "decision")?; let blocks: bool = if text(&decision)? == SOURCE_MISSING { - text(&entry.getattr("missing_origin_policy")?)? == DENY_POLICY + text(&native_field(entry, "missing_origin_policy")?)? == DENY_POLICY } else { - decision.getattr("blocks_build")?.is_truthy()? + native_field(&decision, "blocks_build")?.is_truthy()? }; flag(entry.py(), blocks) } @@ -235,7 +236,7 @@ pub(crate) fn finish_cursor_overrides( /// `CursorOverrides.has_untyped`: whether `--start-cursor` or `--end-cursor` awaits a type. pub(crate) fn overrides_have_untyped(instance: &Bound<'_, PyAny>) -> PyResult> { let untyped: bool = - !instance.getattr("start")?.is_none() || !instance.getattr("end")?.is_none(); + !native_field(instance, "start")?.is_none() || !native_field(instance, "end")?.is_none(); flag(instance.py(), untyped) } @@ -324,7 +325,7 @@ pub(crate) fn finish_scenario_artifact_kind( /// `ColumnRenameHint.message`: the one-line hint shown beside the added column. pub(crate) fn rename_hint_message(hint: &Bound<'_, PyAny>) -> PyResult> { - let relation: &str = if hint.getattr("identical")?.is_truthy()? { + let relation: &str = if native_field(hint, "identical")?.is_truthy()? { "same expression as" } else { "similar to" @@ -341,12 +342,14 @@ pub(crate) fn rename_hint_message(hint: &Bound<'_, PyAny>) -> PyResult format!( "{relation} {}; if this is a rename, add {} (migrate_from {origin})", candidates.join(", "), - hint.getattr("added_column")?.str()? + native_field(hint, "added_column")?.str()? ) .into_py_any(hint.py()) } /// `QueryProjection.key`: the case-insensitive output name. pub(crate) fn projection_key(projection: &Bound<'_, PyAny>) -> PyResult> { - Ok(projection.getattr("name")?.call_method0("lower")?.unbind()) + Ok(native_field(projection, "name")? + .call_method0("lower")? + .unbind()) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/python_nodes.rs b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/python_nodes.rs index f0c4c41ace..27fbe35e3f 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/python_nodes.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/python_nodes.rs @@ -7,6 +7,7 @@ use pyo3::types::{ use sqlbuild_core::graph_walk::main::breadth_first_reach::breadth_first_reach; use crate::bindings::_helpers::graph::node_set::PyNodeSet; +use crate::bindings::_helpers::records::fields::field as native_field; /// `PythonNodeGraph.selected_sql_refs(selected_names=...)`: SQL references of the selected closure. pub(crate) fn graph_selected_sql_refs( @@ -23,8 +24,8 @@ pub(crate) fn graph_selected_sql_refs( Some(selected) => selected, None => args.get_item(0)?, }; - let nodes_by_name: Bound<'_, PyAny> = graph.getattr("nodes_by_name")?; - let upstream: Bound<'_, PyAny> = graph.getattr("upstream_deps")?; + let nodes_by_name: Bound<'_, PyAny> = native_field(graph, "nodes_by_name")?; + let upstream: Bound<'_, PyAny> = native_field(graph, "upstream_deps")?; let refs: Bound<'_, PySet> = PySet::empty(py)?; let starts: Vec> = selected.try_iter()?.collect::>()?; let mut visited: PyNodeSet<'_> = PyNodeSet(PySet::new(py, &starts)?); @@ -37,7 +38,7 @@ pub(crate) fn graph_selected_sql_refs( for name in visited.0.iter() { let node: Bound<'_, PyAny> = nodes_by_name.call_method1("get", (name,))?; if !node.is_none() { - for reference in node.getattr("sql_deps")?.try_iter()? { + for reference in native_field(&node, "sql_deps")?.try_iter()? { refs.add(reference?)?; } } 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 901ee0b876..833a295f66 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,6 +6,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; @@ -14,17 +15,17 @@ const REQUIRED_OPTION_FIELDS: [&str; 3] = ["name", "default", "description"]; /// `pathlib.Path(value)` of a `ProjectPath`. fn project_path<'py>(instance: &Bound<'py, PyAny>) -> PyResult> { - imported(instance.py(), "pathlib", "Path")?.call1((instance.getattr("value")?,)) + imported(instance.py(), "pathlib", "Path")?.call1((native_field(instance, "value")?,)) } /// `ProjectPath.parts`. pub(crate) fn project_path_parts(instance: &Bound<'_, PyAny>) -> PyResult> { - Ok(project_path(instance)?.getattr("parts")?.unbind()) + Ok(native_field(&project_path(instance)?, "parts")?.unbind()) } /// `ProjectPath.name`. pub(crate) fn project_path_name(instance: &Bound<'_, PyAny>) -> PyResult> { - Ok(project_path(instance)?.getattr("name")?.unbind()) + Ok(native_field(&project_path(instance)?, "name")?.unbind()) } /// `ProjectPath.depth`: the number of path parts. 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 9634a620d9..4778b7c67b 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/scopes.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/scopes.rs @@ -1,5 +1,6 @@ //! Computed attributes of the natively hosted scope records. +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::IntoPyObjectExt; use pyo3::prelude::{Bound, Py, PyAny, PyAnyMethods, PyResult}; @@ -24,7 +25,7 @@ pub(crate) fn completeness_complete(instance: &Bound<'_, PyAny>) -> PyResult) -> PyResult> { - let unknown: bool = !instance.getattr("matches")?.is_truthy()? - && !instance.getattr("declaration_matches")?.is_truthy()?; + let unknown: bool = !native_field(instance, "matches")?.is_truthy()? + && !native_field(instance, "declaration_matches")?.is_truthy()?; unknown.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 a0b1704ce5..2a836823f1 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/values.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/record_behaviours/values.rs @@ -1,23 +1,28 @@ //! Computed attributes of the natively hosted SQL value and fingerprint records. +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::IntoPyObjectExt; use pyo3::prelude::{Bound, Py, PyAny, PyAnyMethods, PyResult, Python}; 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 = instance.getattr("kind")?.getattr("value")?.extract()?; - let element: Bound<'_, PyAny> = instance.getattr("element_type")?; + let kind: String = native_field(instance, "kind")? + .getattr("value")? + .extract()?; + let element: Bound<'_, PyAny> = native_field(instance, "element_type")?; if element.is_none() { return kind.into_py_any(instance.py()); } - let inner: String = element.getattr("display_name")?.extract()?; + let inner: String = native_field(&element, "display_name")?.extract()?; format!("{kind}<{inner}>").into_py_any(instance.py()) } /// `SqlValue.kind`: the outer logical kind. pub(crate) fn sql_value_kind(instance: &Bound<'_, PyAny>) -> PyResult> { - Ok(instance.getattr("logical_type")?.getattr("kind")?.unbind()) + Ok(native_field(instance, "logical_type")? + .getattr("kind")? + .unbind()) } /// `FingerprintSet`: index the fingerprints by `(node_type, node_name)` when not given. @@ -34,8 +39,8 @@ pub(crate) fn finish_fingerprint_set( let identity: Bound<'_, PyTuple> = PyTuple::new( py, [ - fingerprint.getattr("node_type")?, - fingerprint.getattr("node_name")?, + native_field(&fingerprint, "node_type")?, + native_field(&fingerprint, "node_name")?, ], )?; by_identity.set_item(identity, fingerprint)?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/records/error_classes.rs b/crates/sqlbuild-python/src/bindings/_helpers/records/error_classes.rs index 67c2079c95..474616f077 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/records/error_classes.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/records/error_classes.rs @@ -6,6 +6,7 @@ use pyo3::types::{PyDict, PyDictMethods, PyTuple, PyTupleMethods, PyTypeMethods} use pyo3::{IntoPyObjectExt, pyclass, pymethods}; use crate::bindings::_helpers::records::constants::{ERROR_SPEC_ATTRIBUTE, ERRORS}; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::models::{ClassValue, ErrorSpec}; /// The `__init__` of the expected-error classes: `Error(message, *, code=None, help=None, **fields)`. @@ -127,7 +128,7 @@ fn initialize( match code { Some(code) if !code.is_none() => error.setattr("code", code)?, _ => { - if let Ok(code) = class.getattr("code") { + if let Ok(code) = native_field(&class, "code") { error.setattr("code", code)?; } } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/records/fields.rs b/crates/sqlbuild-python/src/bindings/_helpers/records/fields.rs index 54b76127e7..c462f7d879 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/records/fields.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/records/fields.rs @@ -5,10 +5,12 @@ use pyo3::exceptions::{PyAttributeError, PyRuntimeError, PyTypeError}; use pyo3::prelude::{Bound, Py, PyAny, PyAnyMethods, PyErr, PyResult, Python}; use pyo3::pyclass::CompareOp; use pyo3::types::{ - PyBool, PyDict, PyDictMethods, PyFrozenSet, PyString, PyTuple, PyTupleMethods, PyType, - PyTypeMethods, + PyBool, PyDict, PyDictMethods, PyFrozenSet, PyString, PyStringMethods, PyTuple, PyTupleMethods, + PyType, PyTypeMethods, }; +use sqlbuild_rules::project_tree::main::pure_path_posix::pure_path_posix; + use crate::bindings::_helpers::compile_driver::python_calls::imported; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::records::constants::{ @@ -36,6 +38,45 @@ pub(crate) fn record_spec(cls: &Bound<'_, PyType>) -> PyResult<&'static RecordSp .ok_or_else(|| PyTypeError::new_err("unknown native record class")) } +/// A declared field of a native record read from its values; any other attribute, or object, by `getattr`. +pub(crate) fn field<'py>(value: &Bound<'py, PyAny>, name: &str) -> PyResult> { + if let Ok(record) = value.cast::() { + let record: &NativeRecord = record.get(); + if let Some(found) = record + .spec + .fields + .iter() + .position(|field| field.name == name) + .and_then(|index| record.values.get(index)) + { + return Ok(found.bind(value.py()).clone()); + } + } + value.getattr(name) +} + +/// `dataclasses.replace(value, **changes)`: a native record is rebuilt from its own values. +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); + for (name, changed) in changes { + keywords.set_item(*name, changed)?; + } + let Ok(record) = value.cast::() else { + return imported(py, "dataclasses", "replace")?.call((value,), Some(&keywords)); + }; + let record: &NativeRecord = record.get(); + for (field, current) in record.spec.fields.iter().zip(&record.values) { + if !keywords.contains(field.name)? { + keywords.set_item(field.name, current.bind(py))?; + } + } + value.get_type().call((), Some(&keywords)) +} + /// Field values bound from `args` and `kwargs` as a dataclass `__init__` binds them. pub(crate) fn build_values( spec: &RecordSpec, @@ -217,3 +258,9 @@ pub(crate) fn store_cached( } Ok(()) } + +/// `path.as_posix()` of a `pathlib` path, spelled natively from its text. +pub(crate) fn path_posix(path: &Bound<'_, PyAny>) -> PyResult { + let text: String = path.str()?.to_string_lossy().into_owned(); + Ok(String::from_utf8_lossy(&pure_path_posix(std::path::Path::new(&text))).into_owned()) +} diff --git a/crates/sqlbuild-python/src/bindings/_helpers/records/models.rs b/crates/sqlbuild-python/src/bindings/_helpers/records/models.rs index 7430d50770..d7b6db29d4 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/records/models.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/records/models.rs @@ -98,6 +98,8 @@ pub(crate) struct ErrorSpec { #[derive(Debug)] #[pyclass(module = "sqlbuild._native", subclass, frozen)] pub(crate) struct NativeRecord { + /// The layout of the record's class. + pub(crate) spec: &'static RecordSpec, pub(crate) values: Vec>, /// Values of the record's cached computed attributes, by attribute name. pub(crate) cached: Mutex)>>, @@ -115,6 +117,7 @@ impl NativeRecord { ) -> PyResult { let spec: &RecordSpec = record_spec(cls)?; Ok(Self { + spec, values: build_values(spec, args, kwargs)?, cached: Mutex::new(Vec::new()), }) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/records/registration.rs b/crates/sqlbuild-python/src/bindings/_helpers/records/registration.rs index e26703adf0..4f69782b9d 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/records/registration.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/records/registration.rs @@ -14,6 +14,7 @@ use crate::bindings::_helpers::records::error_classes::{ BoundErrorInitializer, ErrorInitializer, class_value, }; use crate::bindings::_helpers::records::fields::all_specs; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::models::{ BoundNativeMethod, EnumSpec, ErrorSpec, NativeMethod, NativeProperty, NativeRecord, RecordAttribute, RecordSpec, @@ -133,7 +134,8 @@ fn dataclass_fields<'py>( let options: Bound<'py, PyDict> = PyDict::new(py); options.set_item("compare", field.compared)?; options.set_item("repr", field.shown)?; - let declared: Bound<'py, PyAny> = dataclasses.getattr("field")?.call((), Some(&options))?; + let declared: Bound<'py, PyAny> = + native_field(dataclasses, "field")?.call((), Some(&options))?; declared.setattr("name", field.name)?; declared.setattr("type", "object")?; declared.setattr("_field_type", &field_kind)?; 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 1aeded45f9..9e2e4c3b05 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/refactor/contract_compare.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/refactor/contract_compare.rs @@ -9,6 +9,7 @@ use sqlbuild_core::text::main::python_casefold::python_casefold; use crate::bindings::_helpers::boundary::keyword_request::Request; 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, RecordId}; use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::type_system::equality::types_equal as native_types_equal; @@ -46,8 +47,8 @@ impl<'py> Vocabulary<'py> { fields: Vec<(&str, Bound<'py, PyAny>)>, ) -> PyResult> { let keywords: Bound<'py, PyDict> = PyDict::new(self.py()); - keywords.set_item("resource_type", item.getattr("resource_type")?)?; - keywords.set_item("resource_name", item.getattr("resource_name")?)?; + keywords.set_item("resource_type", native_field(item, "resource_type")?)?; + keywords.set_item("resource_name", native_field(item, "resource_name")?)?; keywords.set_item("kind", self.kinds.getattr(kind)?)?; for (name, value) in fields { keywords.set_item(name, value)?; @@ -104,11 +105,11 @@ fn column_infos<'py>( let mut infos: Vec> = Vec::new(); for column in columns.try_iter()? { let column: Bound<'py, PyAny> = column?; - let declared: Option = column.getattr("type")?.extract()?; + let declared: Option = native_field(&column, "type")?.extract()?; infos.push(keyword_call( &column_type, vec![ - ("name", column.getattr("name")?), + ("name", native_field(&column, "name")?), ("type", text(py, &declared.unwrap_or_default())), ], )?); @@ -125,27 +126,27 @@ fn resource_type<'py>(py: Python<'py>, value: &str) -> PyResult(model: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = model.py(); - let destination: Bound<'py, PyAny> = model.getattr("destination")?; - let schema_entry: Bound<'py, PyAny> = model.getattr("schema_entry")?; + let destination: Bound<'py, PyAny> = native_field(model, "destination")?; + let schema_entry: Bound<'py, PyAny> = native_field(model, "schema_entry")?; let (declared, dynamic): (Bound<'py, PyAny>, Bound<'py, PyAny>) = if schema_entry.is_none() { (PyTuple::empty(py).into_any(), PyTuple::empty(py).into_any()) } else { ( - column_infos(py, &schema_entry.getattr("columns")?)?.into_any(), - schema_entry.getattr("dynamic_columns")?, + column_infos(py, &native_field(&schema_entry, "columns")?)?.into_any(), + native_field(&schema_entry, "dynamic_columns")?, ) }; keyword_call( ®istry::class(py, RecordId::ContractEvidence)?, vec![ ("resource_type", resource_type(py, MODEL_RESOURCE)?), - ("resource_name", model.getattr("name")?), - ("database", destination.getattr("database")?), - ("schema", destination.getattr("schema")?), - ("relation", destination.getattr("name")?), + ("resource_name", native_field(model, "name")?), + ("database", native_field(&destination, "database")?), + ("schema", native_field(&destination, "schema")?), + ("relation", native_field(&destination, "name")?), ("declared_columns", declared), ("physical_columns", py.None().into_bound(py)), - ("source_path", model.getattr("relative_path")?), + ("source_path", native_field(model, "relative_path")?), ("dynamic_columns", dynamic), ], ) @@ -157,21 +158,24 @@ fn source_evidence<'py>( source: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = source.py(); - let entry: Bound<'py, PyAny> = source.getattr("source_entry")?; - let table: Option = entry.getattr("table")?.extract()?; + let entry: Bound<'py, PyAny> = native_field(source, "source_entry")?; + let table: Option = native_field(&entry, "table")?.extract()?; let relation: String = match table.filter(|table| !table.is_empty()) { Some(table) => table, - None => entry.getattr("name")?.extract()?, + None => native_field(&entry, "name")?.extract()?, }; - let name: Bound<'py, PyAny> = source.getattr("name")?; - let inspectable: bool = entry.getattr("expression")?.is_none() && !relation.is_empty(); + let name: Bound<'py, PyAny> = native_field(source, "name")?; + let inspectable: bool = native_field(&entry, "expression")?.is_none() && !relation.is_empty(); let resource: Bound<'py, PyAny> = resource_type(py, SOURCE_RESOURCE)?; let mut findings: Vec> = Vec::new(); if !inspectable { let keywords: Bound<'py, PyDict> = PyDict::new(py); keywords.set_item("resource_type", &resource)?; keywords.set_item("resource_name", &name)?; - keywords.set_item("kind", vocabulary.kinds.getattr("INSPECTION_UNAVAILABLE")?)?; + keywords.set_item( + "kind", + native_field(&vocabulary.kinds, "INSPECTION_UNAVAILABLE")?, + )?; keywords.set_item( "message", "source expression has no inspectable physical relation", @@ -188,17 +192,17 @@ fn source_evidence<'py>( vec![ ("resource_type", resource), ("resource_name", name), - ("database", entry.getattr("database")?), - ("schema", entry.getattr("schema")?), + ("database", native_field(&entry, "database")?), + ("schema", native_field(&entry, "schema")?), ("relation", relation), ( "declared_columns", - column_infos(py, &entry.getattr("columns")?)?.into_any(), + column_infos(py, &native_field(&entry, "columns")?)?.into_any(), ), ("physical_columns", py.None().into_bound(py)), ( "source_path", - source.getattr("source_file")?.getattr("relative_path")?, + native_field(source, "source_file")?.getattr("relative_path")?, ), ("findings", PyTuple::new(py, findings)?.into_any()), ], @@ -249,25 +253,25 @@ fn undeclared_finding<'py>( adapter: &Bound<'py, PyAny>, ) -> PyResult>> { let py: Python<'py> = vocabulary.py(); - let name: String = physical.getattr("name")?.extract()?; - let physical_type: Bound<'py, PyAny> = physical.getattr("type")?; + 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", item.getattr("dynamic_columns")?), + ("families", native_field(item, "dynamic_columns")?), ("column_name", text(py, &name)), ], )?; if matching.len()? == 1 { let family: Bound<'py, PyAny> = matching.get_item(0)?; - let family_type: Bound<'py, PyAny> = family.getattr("type")?; + let family_type: Bound<'py, PyAny> = native_field(&family, "type")?; if types_equal(adapter, (&family_type, &physical_type))? { return Ok(None); } let message: String = format!( "dynamic column '{name}' is physically {} but family '{}' declares {}", physical_type.str()?, - family.getattr("name")?.str()?, + native_field(&family, "name")?.str()?, family_type.str()? ); return vocabulary @@ -307,9 +311,9 @@ fn declared_finding<'py>( adapter: &Bound<'py, PyAny>, ) -> PyResult>> { let py: Python<'py> = vocabulary.py(); - let declared_name: Bound<'py, PyAny> = declared.getattr("name")?; - let declared_type: Bound<'py, PyAny> = declared.getattr("type")?; - let physical_type: Bound<'py, PyAny> = physical.getattr("type")?; + let declared_name: Bound<'py, PyAny> = native_field(declared, "name")?; + let declared_type: Bound<'py, PyAny> = native_field(declared, "type")?; + let physical_type: Bound<'py, PyAny> = native_field(physical, "type")?; if !declared_type.is_truthy()? { let message: String = format!( "declared column '{}' has no type; physical type is {}", @@ -321,7 +325,7 @@ fn declared_finding<'py>( item, "MISSING_DECLARATION", vec![ - ("column_name", physical.getattr("name")?), + ("column_name", native_field(physical, "name")?), ("physical_type", physical_type), ("message", text(py, &message)), ], @@ -383,13 +387,13 @@ fn compared<'py>( let mut declared_keys: Vec = Vec::with_capacity(declared.len()); for column in &declared { declared_keys.push(python_casefold( - &column.getattr("name")?.extract::()?, + &native_field(column, "name")?.extract::()?, )); } let mut physical_keys: Vec = Vec::with_capacity(physical.len()); for column in &physical { physical_keys.push(python_casefold( - &column.getattr("name")?.extract::()?, + &native_field(column, "name")?.extract::()?, )); } let mut findings: Vec> = Vec::new(); @@ -410,8 +414,8 @@ fn compared<'py>( if physical_keys.contains(key) { continue; } - let name: Bound<'py, PyAny> = column.getattr("name")?; - let declared_type: Bound<'py, PyAny> = column.getattr("type")?; + let name: Bound<'py, PyAny> = native_field(column, "name")?; + let declared_type: Bound<'py, PyAny> = native_field(column, "type")?; let message: String = format!( "declared column '{}' is absent from the physical relation", name.str()? @@ -457,9 +461,9 @@ fn relation_info<'py>(item: &Bound<'py, PyAny>) -> PyResult> { keyword_call( &host::entry(py, AdapterHost::RelationInfo)?, vec![ - ("database", item.getattr("database")?), - ("schema", item.getattr("schema")?), - ("name", item.getattr("relation")?), + ("database", native_field(item, "database")?), + ("schema", native_field(item, "schema")?), + ("name", native_field(item, "relation")?), ("relation_type", text(py, "")), ], ) @@ -483,7 +487,7 @@ fn compare_contracts<'py>(request: Option<&Bound<'py, PyDict>>) -> PyResult = source_evidence(&vocabulary, &source?)?; - if item.getattr("findings")?.is_truthy()? { + if native_field(&item, "findings")?.is_truthy()? { evidence.push(item); } else { inspectable.push(item); @@ -509,7 +513,7 @@ fn compare_contracts<'py>(request: Option<&Bound<'py, PyDict>>) -> PyResult = - physical.call_method1("get", (relation.getattr("identity")?,))?; + physical.call_method1("get", (native_field(relation, "identity")?,))?; evidence.push(compared(&vocabulary, (item, &columns), adapter)?); } PyTuple::new(py, evidence) 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 7d6ec9ae98..c541041842 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -59,8 +60,8 @@ fn physical_rows(columns: &Bound<'_, PyAny>) -> PyResult> { for column in columns.try_iter()? { let column: Bound<'_, PyAny> = column?; rows.push(( - column.getattr("name")?.extract()?, - column.getattr("type")?.extract()?, + native_field(&column, "name")?.extract()?, + native_field(&column, "type")?.extract()?, )); } Ok(rows) @@ -75,32 +76,33 @@ fn edit_contract_model( physical_columns: &Bound<'_, PyAny>, overwrite: bool, ) -> PyResult<(Option, Option)> { - let schema: Bound<'_, PyAny> = model.getattr("schema_entry")?; + let schema: Bound<'_, PyAny> = native_field(model, "schema_entry")?; let mut columns: Vec = Vec::new(); let mut dynamic_patterns: Vec> = Vec::new(); - let shared_schema: bool = !schema.is_none() && !schema.getattr("model_schema")?.is_none(); + let shared_schema: bool = + !schema.is_none() && !native_field(&schema, "model_schema")?.is_none(); if !schema.is_none() { - for column in schema.getattr("columns")?.try_iter()? { + for column in native_field(&schema, "columns")?.try_iter()? { let column: Bound<'_, PyAny> = column?; - let location: Bound<'_, PyAny> = column.getattr("location")?; + let location: Bound<'_, PyAny> = native_field(&column, "location")?; let location_path: Option = if location.is_none() { None } else { - Some(location.getattr("path")?.str()?.extract()?) + Some(native_field(&location, "path")?.str()?.extract()?) }; columns.push(ContractModelColumn { - name: column.getattr("name")?.extract()?, - declared_type: column.getattr("type")?.extract()?, + name: native_field(&column, "name")?.extract()?, + declared_type: native_field(&column, "type")?.extract()?, location_path, }); } - for family in schema.getattr("dynamic_columns")?.try_iter()? { - dynamic_patterns.push(family?.getattr("name_pattern")?.extract()?); + for family in native_field(&schema, "dynamic_columns")?.try_iter()? { + dynamic_patterns.push(native_field(&family?, "name_pattern")?.extract()?); } } let request = ContractModelRequest { - contents: model.getattr("authored_sql")?.extract()?, - relative_path: model.getattr("relative_path")?.str()?.extract()?, + contents: native_field(model, "authored_sql")?.extract()?, + relative_path: native_field(model, "relative_path")?.str()?.extract()?, shared_schema, columns, dynamic_patterns, @@ -132,14 +134,14 @@ fn edit_contract_source( { let column: Bound<'_, PyAny> = column?; columns.push(( - column.getattr("name")?.extract()?, - column.getattr("type")?.extract()?, + native_field(&column, "name")?.extract()?, + native_field(&column, "type")?.extract()?, )); } let request = ContractSourceRequest { contents: contents.unwrap_or_else(|| discovered_contents.clone()), discovered_contents, - name: source.getattr("name")?.extract()?, + name: native_field(source, "name")?.extract()?, columns, physical_columns: physical_rows(physical_columns)?, overwrite, @@ -166,8 +168,8 @@ fn remaining_contract_findings<'py>( )); }; let keywords: Bound<'py, PyDict> = PyDict::new(py); - keywords.set_item("resource_type", first.getattr("resource_type")?)?; - keywords.set_item("resource_name", first.getattr("resource_name")?)?; + keywords.set_item("resource_type", native_field(first, "resource_type")?)?; + keywords.set_item("resource_name", native_field(first, "resource_name")?)?; keywords.set_item( "kind", registry::class(py, RecordId::ContractFindingKind)?.getattr("OWNERSHIP_CONFLICT")?, @@ -181,7 +183,7 @@ fn remaining_contract_findings<'py>( } let mut remaining: Vec> = Vec::new(); for finding in findings { - let kind: String = finding.getattr("kind")?.str()?.extract()?; + let kind: String = native_field(&finding, "kind")?.str()?.extract()?; let resolved: bool = RESOLVED_KINDS.0.contains(&kind.as_str()) || (overwrite && RESOLVED_KINDS.1.contains(&kind.as_str())); if !resolved { 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 100c1e2f82..48394339ee 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/refactor/offline_compile.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/refactor/offline_compile.rs @@ -8,6 +8,7 @@ use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyErr, PyResult, Python}; use pyo3::types::{PyBool, PyString}; 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, NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; @@ -36,12 +37,12 @@ fn matches_any_class(py: Python<'_>, error: &PyErr, classes: &[ErrorId]) -> PyRe fn compile_error_diagnostic<'py>(py: Python<'py>, error: &PyErr) -> PyResult> { let value: Bound<'py, PyAny> = error.value(py).clone().into_any(); let code: Bound<'py, PyAny> = if value.hasattr("code")? { - value.getattr("code")?.str()?.into_any() + native_field(&value, "code")?.str()?.into_any() } else { PyString::new(py, DEFAULT_ERROR_CODE).into_any() }; let message: Bound<'py, PyAny> = if value.hasattr("message")? { - value.getattr("message")?.str()?.into_any() + native_field(&value, "message")?.str()?.into_any() } else { value.str()?.into_any() }; @@ -67,7 +68,7 @@ fn planning_error_diagnostic<'py>(py: Python<'py>, error: &PyErr) -> PyResult = error.value(py).clone().into_any(); let location: Bound<'py, PyAny> = if value.is_instance(®istry::error(py, ErrorId::SqlTestReference)?)? { - value.getattr("location")? + native_field(&value, "location")? } else { py.None().into_bound(py) }; @@ -79,9 +80,9 @@ fn planning_error_diagnostic<'py>(py: Python<'py>, error: &PyErr) -> PyResult(diagnostics: Bound<'py, PyAny>) -> PyResult> = Vec::new(); for diagnostic in diagnostics.try_iter()? { let diagnostic: Bound<'py, PyAny> = diagnostic?; - if diagnostic.getattr("is_error")?.is_truthy()? { + if native_field(&diagnostic, "is_error")?.is_truthy()? { errors.push(diagnostic); } } @@ -150,7 +151,8 @@ pub(crate) fn compile_for_refactor<'py>( } Err(error) => return Err(error), }; - let mut errors: Vec> = error_diagnostics(analysis.getattr("diagnostics")?)?; + let mut errors: Vec> = + error_diagnostics(native_field(&analysis, "diagnostics")?)?; if errors.is_empty() { let planned: PyResult> = call_with( ®istry::native(py, NativeAttr::StaticSqlTestPlanningDiagnostics)?, @@ -159,8 +161,11 @@ pub(crate) fn compile_for_refactor<'py>( "target_dir", project_dir.join("target").into_pyobject(py)?.into_any(), ), - ("adapter", analysis.getattr("adapter")?), - ("project", analysis.getattr("graph")?.getattr("project")?), + ("adapter", native_field(&analysis, "adapter")?), + ( + "project", + native_field(&analysis, "graph")?.getattr("project")?, + ), ], ); match planned { @@ -181,8 +186,8 @@ pub(crate) fn compile_for_refactor<'py>( ®istry::class(py, RecordId::RefactorProject)?, &[ ("project_dir", project_dir.into_pyobject(py)?.into_any()), - ("graph", analysis.getattr("graph")?), - ("discovered", analysis.getattr("discovered_inputs")?), + ("graph", native_field(&analysis, "graph")?), + ("discovered", native_field(&analysis, "discovered_inputs")?), ], )?; Ok(OfflineCompile { project, errors }) 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 1532b42d39..021a22be97 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/refactor/python_strings.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/refactor/python_strings.rs @@ -9,6 +9,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; @@ -31,8 +32,8 @@ fn python_paths<'py>( let mut paths: Vec<(Vec, Bound<'py, PyAny>)> = Vec::new(); for declarations in PYTHON_DECLARATIONS { for item in discovered.getattr(declarations)?.try_iter()? { - let path: Bound<'py, PyAny> = item?.getattr("relative_path")?; - let parts: Vec = path.getattr("parts")?.extract()?; + 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) { paths.push((parts, path)); } 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 d1d5a10730..938af86448 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/refactor/refactor_command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/refactor/refactor_command.rs @@ -36,6 +36,7 @@ use crate::bindings::_helpers::boundary::panics::{NativeCompilerError, compiler_ use crate::bindings::_helpers::build_progress::transient_status::status_reporter; use crate::bindings::_helpers::lineage::lineage_index::lineage_key; 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; use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::refactor::offline_compile::{OfflineCompile, compile_for_refactor}; @@ -112,7 +113,7 @@ fn relative_diagnostics( ) -> PyResult> { let mut diagnostics: Vec = Vec::new(); for error in errors { - let path: Option = optional_posix(&error.getattr("path")?)?.map(|path| { + let path: Option = optional_posix(&native_field(error, "path")?)?.map(|path| { let candidate: &Path = Path::new(&path); match candidate.strip_prefix(staging_dir) { Ok(relative) if candidate.is_absolute() => relative.to_string_lossy().into_owned(), @@ -120,10 +121,10 @@ fn relative_diagnostics( } }); diagnostics.push(RefactorDiagnostic { - code: error.getattr("code")?.extract()?, - message: error.getattr("message")?.extract()?, + code: native_field(error, "code")?.extract()?, + message: native_field(error, "message")?.extract()?, path, - line: error.getattr("line")?.extract()?, + line: native_field(error, "line")?.extract()?, }); } Ok(diagnostics) @@ -133,10 +134,10 @@ fn relative_diagnostics( fn write_errors(py: Python<'_>, errors: &[Bound<'_, PyAny>]) -> PyResult<()> { let stderr: Bound<'_, PyAny> = py.import("sys")?.getattr("stderr")?; for error in errors { - let location: String = optional_posix(&error.getattr("path")?)? + let location: String = optional_posix(&native_field(error, "path")?)? .map_or_else(String::new, |path| format!("{path}: ")); - let code: String = error.getattr("code")?.str()?.extract()?; - let message: String = error.getattr("message")?.str()?.extract()?; + let code: String = native_field(error, "code")?.str()?.extract()?; + let message: String = native_field(error, "message")?.str()?.extract()?; stderr.call_method1("write", (format!("error[{code}] {location}{message}\n"),))?; } Ok(()) @@ -212,11 +213,14 @@ fn target_lookup(all_keys: &Bound<'_, PyAny>) -> PyResult) -> PyResult { Ok(RefactorTargetRequest { - is_move: request.getattr("command")?.str()?.extract::()? == MOVE_COMMAND, - target: request.getattr("target")?.extract()?, - new_name: request.getattr("new_name")?.extract()?, - destination: request.getattr("destination")?.extract()?, - cascade: request.getattr("cascade")?.extract()?, + is_move: native_field(request, "command")? + .str()? + .extract::()? + == MOVE_COMMAND, + target: native_field(request, "target")?.extract()?, + new_name: native_field(request, "new_name")?.extract()?, + destination: native_field(request, "destination")?.extract()?, + cascade: native_field(request, "cascade")?.extract()?, }) } @@ -302,7 +306,7 @@ fn run(py: Python<'_>, command: &Command<'_>, status: &Status<'_>) -> PyResult = - target_lookup(&before.project.getattr("graph")?.getattr("all_keys")?)?; + target_lookup(&native_field(&before.project, "graph")?.getattr("all_keys")?)?; let lookup = |name: &str| names.get(name).cloned(); let request: RefactorRequest = refactor_request_for_target(&target_request(&command.request)?, &lookup) @@ -322,7 +326,7 @@ fn run(py: Python<'_>, command: &Command<'_>, status: &Status<'_>) -> PyResult = imported(py, "tempfile", "TemporaryDirectory")?.call((), Some(&options))?; - let staging_dir: PathBuf = staging.getattr("name")?.extract()?; + let staging_dir: PathBuf = native_field(&staging, "name")?.extract()?; let verified = verify( py, command, @@ -347,7 +351,7 @@ fn run(py: Python<'_>, command: &Command<'_>, status: &Status<'_>) -> PyResult()? { + if native_field(&command.request, "dry_run")?.extract::()? { return Ok(Some(Outcome { plan, status: RefactorStatus::DryRun, @@ -370,7 +374,7 @@ fn finish(py: Python<'_>, command: &Command<'_>, outcome: &Outcome) -> PyResult< render_refactor_json(&outcome.plan, outcome.status, &outcome.diagnostics) .map_err(|error| PyValueError::new_err(format!("{error:?}")))? } else { - let project_dir: Bound<'_, PyAny> = command.request.getattr("project_dir")?; + let project_dir: Bound<'_, PyAny> = native_field(&command.request, "project_dir")?; let project_dir: Option = if project_dir.is_none() { None } else { @@ -403,13 +407,13 @@ fn finish(py: Python<'_>, command: &Command<'_>, outcome: &Outcome) -> PyResult< #[pyfunction] #[pyo3(signature = (*, request))] fn run_refactor_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { - let project_dir: Bound<'_, PyAny> = match request.getattr("project_dir")? { + 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")?, }; - let json_output: bool = request.getattr("json_output")?.extract()?; + let json_output: bool = native_field(request, "json_output")?.extract()?; let use_color: bool = - !request.getattr("no_color")?.extract::()? && stdout_supports_color(py)?; + !native_field(request, "no_color")?.extract::()? && stdout_supports_color(py)?; let command: Command<'_> = Command { request: request.clone(), project_dir: project_dir.call_method0("resolve")?.extract()?, 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 9440b2cd1d..6c7ee9d889 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/refactor/refactor_facts.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/refactor/refactor_facts.rs @@ -11,6 +11,7 @@ use sqlbuild_refactor::refactoring::models::{ SchemaColumnFacts, SqlFileRole, }; +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::scopes::scope_lookup::build_scope_lookup; @@ -35,8 +36,8 @@ fn discovered_files(files: &Bound<'_, PyAny>) -> PyResult> { for item in files.try_iter()? { let item: Bound<'_, PyAny> = item?; discovered.push(DiscoveredFile { - path: posix(&item.getattr("relative_path")?)?, - contents: item.getattr("contents")?.extract()?, + path: posix(&native_field(&item, "relative_path")?)?, + contents: native_field(&item, "contents")?.extract()?, }); } Ok(discovered) @@ -45,7 +46,7 @@ fn discovered_files(files: &Bound<'_, PyAny>) -> PyResult> { fn column_names(columns: &Bound<'_, PyAny>) -> PyResult> { columns .try_iter()? - .map(|column| column?.getattr("name")?.extract()) + .map(|column| native_field(&column?, "name")?.extract()) .collect() } @@ -57,8 +58,8 @@ fn resource_columns( for resource in resources.try_iter()? { let resource: Bound<'_, PyAny> = resource?; facts.push(ResourceColumnsFacts { - name: resource.getattr("name")?.extract()?, - columns: column_names(&resource.getattr(entry)?.getattr("columns")?)?, + name: native_field(&resource, "name")?.extract()?, + columns: column_names(&native_field(&resource.getattr(entry)?, "columns")?)?, }); } Ok(facts) @@ -69,10 +70,10 @@ fn spans(spans: &Bound<'_, PyAny>) -> PyResult> { for span in spans.try_iter()? { let span: Bound<'_, PyAny> = span?; read.push(ExpansionSpan { - source_start: span.getattr("source_start")?.extract()?, - source_end: span.getattr("source_end")?.extract()?, - output_start: span.getattr("output_start")?.extract()?, - output_end: span.getattr("output_end")?.extract()?, + source_start: native_field(&span, "source_start")?.extract()?, + source_end: native_field(&span, "source_end")?.extract()?, + output_start: native_field(&span, "output_start")?.extract()?, + output_end: native_field(&span, "output_end")?.extract()?, }); } Ok(read) @@ -80,7 +81,7 @@ fn spans(spans: &Bound<'_, PyAny>) -> PyResult> { fn expansion_facts(expansion: &Bound<'_, PyAny>) -> PyResult { Ok(ExpansionFacts { - expanded_sql: expansion.getattr("expanded_sql")?.extract()?, + expanded_sql: native_field(expansion, "expanded_sql")?.extract()?, passes: expansion .getattr("passes")? .try_iter()? @@ -94,19 +95,19 @@ fn schema_columns(entry: &Bound<'_, PyAny>) -> PyResult = Vec::new(); - for column in entry.getattr("columns")?.try_iter()? { + for column in native_field(entry, "columns")?.try_iter()? { let column: Bound<'_, PyAny> = column?; - let location: Bound<'_, PyAny> = column.getattr("location")?; + let location: Bound<'_, PyAny> = native_field(&column, "location")?; columns.push(SchemaColumnFacts { - name: column.getattr("name")?.extract()?, - migrate_from: !column.getattr("migrate_from")?.is_none(), + name: native_field(&column, "name")?.extract()?, + migrate_from: !native_field(&column, "migrate_from")?.is_none(), location: if location.is_none() { None } else { Some(LocationFacts { - path: posix(&location.getattr("path")?)?, - line: location.getattr("line")?.extract()?, - column: location.getattr("column")?.extract()?, + path: posix(&native_field(&location, "path")?)?, + line: native_field(&location, "line")?.extract()?, + column: native_field(&location, "column")?.extract()?, }) }, }); @@ -117,7 +118,10 @@ fn schema_columns(entry: &Bound<'_, PyAny>) -> PyResult, expansion: Option<&Bound<'_, PyAny>>) -> PyResult { let py: Python<'_> = model.py(); let values: Bound<'_, PyDict> = PyDict::new(py); - values.call_method1("update", (model.getattr("config")?.getattr("values")?,))?; + values.call_method1( + "update", + (native_field(model, "config")?.getattr("values")?,), + )?; let options: Bound<'_, PyDict> = PyDict::new(py); options.set_item("values", &values)?; options.set_item("key", MATERIALIZED_KEY)?; @@ -126,35 +130,35 @@ fn model(model: &Bound<'_, PyAny>, expansion: Option<&Bound<'_, PyAny>>) -> PyRe .extract()?; let migrate_from: Option> = values.get_item(MIGRATE_FROM_KEY)?; let mut deps: Vec = Vec::new(); - for dep in model.getattr("deps")?.try_iter()? { + for dep in native_field(model, "deps")?.try_iter()? { let dep: Bound<'_, PyAny> = dep?; deps.push(DependencyFacts { - resource_type: dep.getattr("resource_type")?.str()?.extract()?, - name: dep.getattr("name")?.extract()?, + resource_type: native_field(&dep, "resource_type")?.str()?.extract()?, + name: native_field(&dep, "name")?.extract()?, }); } - let destination: Bound<'_, PyAny> = model.getattr("destination")?; - let inferred: Bound<'_, PyAny> = model.getattr("inferred_columns")?; + let destination: Bound<'_, PyAny> = native_field(model, "destination")?; + let inferred: Bound<'_, PyAny> = native_field(model, "inferred_columns")?; Ok(ModelFacts { - name: model.getattr("name")?.extract()?, - path: posix(&model.getattr("relative_path")?)?, + name: native_field(model, "name")?.extract()?, + path: posix(&native_field(model, "relative_path")?)?, deps, - query_sql: model.getattr("query_sql")?.extract()?, - authored_query_sql: model.getattr("authored_query_sql")?.extract()?, + query_sql: native_field(model, "query_sql")?.extract()?, + authored_query_sql: native_field(model, "authored_query_sql")?.extract()?, materialized, declares_migrate_from: migrate_from.is_some(), migrate_from_set: migrate_from.is_some_and(|value| !value.is_none()), destination: DestinationFacts { - database: destination.getattr("database")?.extract()?, - schema: destination.getattr("schema")?.extract()?, - name: destination.getattr("name")?.extract()?, + database: native_field(&destination, "database")?.extract()?, + schema: native_field(&destination, "schema")?.extract()?, + name: native_field(&destination, "name")?.extract()?, }, inferred_columns: if inferred.is_none() { Vec::new() } else { column_names(&inferred)? }, - schema_columns: schema_columns(&model.getattr("schema_entry")?)?, + schema_columns: schema_columns(&native_field(model, "schema_entry")?)?, expansion: expansion.map(expansion_facts).transpose()?, }) } @@ -166,8 +170,8 @@ pub(crate) fn model_facts( if refactor_project.is_none() { return Ok(None); } - let project: Bound<'_, PyAny> = refactor_project.getattr("graph")?.getattr("project")?; - let project_dir: Bound<'_, PyAny> = refactor_project.getattr("project_dir")?; + let project: Bound<'_, PyAny> = native_field(refactor_project, "graph")?.getattr("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")? @@ -179,10 +183,10 @@ pub(crate) fn model_facts( expansions.insert(path.call_method0("resolve")?.str()?.extract()?, expansion); } let mut facts: Vec = Vec::new(); - for compiled in project.getattr("models")?.try_iter()? { + for compiled in native_field(&project, "models")?.try_iter()? { let compiled: Bound<'_, PyAny> = compiled?; let resolved: String = project_dir - .div(compiled.getattr("relative_path")?)? + .div(native_field(&compiled, "relative_path")?)? .call_method0("resolve")? .str()? .extract()?; @@ -194,7 +198,7 @@ pub(crate) fn model_facts( fn block_bodies(blocks: &Bound<'_, PyAny>) -> PyResult> { blocks .try_iter()? - .map(|block| block?.getattr("sql_body")?.extract()) + .map(|block| native_field(&block?, "sql_body")?.extract()) .collect() } @@ -223,13 +227,13 @@ fn authored_files(discovered: &Bound<'_, PyAny>) -> PyResult> let item: Bound<'_, PyAny> = item?; let texts: Vec = match (body, blocks) { (Some(body), _) => vec![item.getattr(body)?.extract()?], - (None, true) => block_bodies(&item.getattr("blocks")?)?, + (None, true) => block_bodies(&native_field(&item, "blocks")?)?, (None, false) => Vec::new(), }; files.push(AuthoredFile { role, - path: posix(&item.getattr("relative_path")?)?, - contents: item.getattr("contents")?.extract()?, + path: posix(&native_field(&item, "relative_path")?)?, + contents: native_field(&item, "contents")?.extract()?, texts, }); } @@ -244,8 +248,8 @@ fn python_locations( let py: Python<'_> = project.py(); let old: &str = &request.model_name; let options: Bound<'_, PyDict> = PyDict::new(py); - options.set_item("project_dir", project.getattr("project_dir")?)?; - options.set_item("discovered", project.getattr("discovered")?)?; + options.set_item("project_dir", native_field(project, "project_dir")?)?; + options.set_item("discovered", native_field(project, "discovered")?)?; if request.operation == RefactorOperation::RenameColumn { let column: &str = request.column_name.as_deref().unwrap_or_default(); options.set_item("names", PyTuple::new(py, [column])?)?; @@ -268,10 +272,10 @@ fn python_locations( { let location: Bound<'_, PyAny> = location?; locations.push(ManualLocation { - path: location.getattr("path")?.extract()?, - line: location.getattr("line")?.extract()?, - column: location.getattr("column")?.extract()?, - reason: location.getattr("reason")?.extract()?, + path: native_field(&location, "path")?.extract()?, + line: native_field(&location, "line")?.extract()?, + column: native_field(&location, "column")?.extract()?, + reason: native_field(&location, "reason")?.extract()?, }); } Ok(locations) @@ -282,21 +286,24 @@ pub(crate) fn refactor_facts( project: &Bound<'_, PyAny>, request: &RefactorRequest, ) -> PyResult { - let compiled: Bound<'_, PyAny> = project.getattr("graph")?.getattr("project")?; - let discovered: Bound<'_, PyAny> = project.getattr("discovered")?; - let dialect: Option = compiled.getattr("sql_analysis_dialect")?.extract()?; + let compiled: Bound<'_, PyAny> = native_field(project, "graph")?.getattr("project")?; + let discovered: Bound<'_, PyAny> = native_field(project, "discovered")?; + let dialect: Option = native_field(&compiled, "sql_analysis_dialect")?.extract()?; let mut yaml_files: Vec = - discovered_files(&discovered.getattr("source_files")?)?; - yaml_files.extend(discovered_files(&discovered.getattr("schema_files")?)?); + discovered_files(&native_field(&discovered, "source_files")?)?; + yaml_files.extend(discovered_files(&native_field( + &discovered, + "schema_files", + )?)?); Ok(RefactorFacts { - project_dir: project.getattr("project_dir")?.str()?.extract()?, + project_dir: native_field(project, "project_dir")?.str()?.extract()?, dialect: dialect .filter(|dialect| !dialect.is_empty()) .unwrap_or_else(|| GENERIC_DIALECT.to_owned()), models: model_facts(project)?.unwrap_or_default(), - sources: resource_columns(&compiled.getattr("sources")?, "source_entry")?, - seeds: resource_columns(&compiled.getattr("seeds")?, "schema_entry")?, - model_files: discovered_files(&discovered.getattr("model_files")?)?, + sources: resource_columns(&native_field(&compiled, "sources")?, "source_entry")?, + seeds: resource_columns(&native_field(&compiled, "seeds")?, "schema_entry")?, + model_files: discovered_files(&native_field(&discovered, "model_files")?)?, authored_files: authored_files(&discovered)?, yaml_files, python_locations: python_locations(project, request)?, @@ -356,8 +363,8 @@ pub(crate) fn declaration_placement( { let record: Bound<'_, PyAny> = record?; relocated.push(RelocatedDeclaration { - key: identity_key(&record.getattr("identity")?)?, - path: record.getattr("path")?.extract()?, + key: identity_key(&native_field(&record, "identity")?)?, + path: native_field(&record, "path")?.extract()?, }); } Some(relocated) @@ -366,24 +373,26 @@ pub(crate) fn declaration_placement( } }; let mut declarations: Vec = Vec::new(); - for record in index.getattr("declarations")?.try_iter()? { + for record in native_field(&index, "declarations")?.try_iter()? { let record: Bound<'_, PyAny> = record?; - let identity: Bound<'_, PyAny> = record.getattr("identity")?; - let kind: String = identity.getattr("kind")?.getattr("value")?.extract()?; - let name: String = identity.getattr("name")?.extract()?; + let identity: Bound<'_, PyAny> = native_field(&record, "identity")?; + let kind: String = native_field(&identity, "kind")? + .getattr("value")? + .extract()?; + let name: String = native_field(&identity, "name")?.extract()?; declarations.push(PlacedDeclaration { key: identity_key(&identity)?, label: format!("{kind}:{name}"), - path: record.getattr("path")?.extract()?, - line: record.getattr("line")?.extract()?, - column: record.getattr("column")?.extract()?, + path: native_field(&record, "path")?.extract()?, + line: native_field(&record, "line")?.extract()?, + column: native_field(&record, "column")?.extract()?, }); } Ok(DeclarationPlacement { relocated, diagnostics: diagnostics .try_iter()? - .map(|item| item?.getattr("message")?.extract()) + .map(|item| native_field(&item?, "message")?.extract()) .collect::>()?, declarations, }) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/references/relations.rs b/crates/sqlbuild-python/src/bindings/_helpers/references/relations.rs index de66cb5576..46d397cf98 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/references/relations.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/references/relations.rs @@ -10,6 +10,7 @@ use crate::bindings::_helpers::analysis::relation_names::{ }; 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; const RUNTIME_RELATION_PREFIX: &str = "SELECT 1 FROM "; @@ -76,7 +77,7 @@ pub(crate) fn match_project_relation<'py>( default_database: Option, default_schema: Option, ) -> PyResult>> { - let name: String = python_casefold(&relation.getattr("name")?.extract::()?); + let name: String = python_casefold(&native_field(relation, "name")?.extract::()?); let schema: Option = relation .getattr("schema")? .extract::>()? @@ -85,14 +86,14 @@ pub(crate) fn match_project_relation<'py>( .getattr("database")? .extract::>()? .or(default_database); - for candidate in index.getattr("relations")?.try_iter()? { + for candidate in native_field(index, "relations")?.try_iter()? { let candidate: Bound<'py, PyAny> = candidate?; - let target: Bound<'py, PyAny> = candidate.getattr("relation")?; - if python_casefold(&target.getattr("name")?.extract::()?) != name { + let target: Bound<'py, PyAny> = native_field(&candidate, "relation")?; + if python_casefold(&native_field(&target, "name")?.extract::()?) != name { continue; } - let target_schema: Option = target.getattr("schema")?.extract()?; - let target_database: Option = target.getattr("database")?.extract()?; + let target_schema: Option = native_field(&target, "schema")?.extract()?; + let target_database: Option = native_field(&target, "database")?.extract()?; if parts_compatible(schema.as_deref(), target_schema.as_deref()) && parts_compatible(database.as_deref(), target_database.as_deref()) { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/references/source_relations.rs b/crates/sqlbuild-python/src/bindings/_helpers/references/source_relations.rs index 296726479a..7fc39f8452 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/references/source_relations.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/references/source_relations.rs @@ -1,5 +1,6 @@ //! A declared source rendered as the SQL table factor that reads it. +use crate::bindings::_helpers::records::fields::field as native_field; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult}; use pyo3::types::{PyDict, PyDictMethods}; use pyo3::{pyfunction, wrap_pyfunction}; @@ -32,7 +33,7 @@ pub(crate) fn render_source_relation<'py>( adapter: Option<&Bound<'py, PyAny>>, ) -> PyResult { let adapter: Option<&Bound<'py, PyAny>> = adapter.filter(|adapter| !adapter.is_none()); - let expression: Option = entry.getattr("expression")?.extract()?; + let expression: Option = native_field(entry, "expression")?.extract()?; if let Some(expression) = expression { return match adapter { Some(adapter) => { @@ -45,13 +46,13 @@ pub(crate) fn render_source_relation<'py>( None => Ok(expression_relation(&expression)), }; } - let table: Option = entry.getattr("table")?.extract()?; + let table: Option = native_field(entry, "table")?.extract()?; let table: String = match table { Some(table) => table, - None => entry.getattr("name")?.extract()?, + None => native_field(entry, "name")?.extract()?, }; - let database: Option = entry.getattr("database")?.extract()?; - let schema: Option = entry.getattr("schema")?.extract()?; + let database: Option = native_field(entry, "database")?.extract()?; + let schema: Option = native_field(entry, "schema")?.extract()?; if let Some(adapter) = adapter { let keywords: Bound<'py, PyDict> = PyDict::new(entry.py()); keywords.set_item("database", &database)?; 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 f333f5920c..e4de6e0117 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/render/authored_sql.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/render/authored_sql.rs @@ -11,6 +11,7 @@ use crate::bindings::_helpers::compile_attachment::sql_expansion_context::SqlExp 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::render::declaration_expansion::{ @@ -309,11 +310,11 @@ fn input_declarations<'py>( ("constants", merged("constants")?), ( "inaccessible_enums", - declarations.getattr("inaccessible_enums")?, + native_field(&declarations, "inaccessible_enums")?, ), ( "inaccessible_constants", - declarations.getattr("inaccessible_constants")?, + native_field(&declarations, "inaccessible_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 2f2f6c537c..0ee96d3607 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/render/declaration_indexes.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/render/declaration_indexes.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::ErrorId; use crate::bindings::_helpers::records::registry; use pyo3::prelude::{ @@ -32,9 +33,9 @@ fn index_declarations<'py>( ) -> PyResult<()> { let py: Python<'py> = files.py(); for file in files.try_iter()? { - for declaration in file?.getattr("declarations")?.try_iter()? { + for declaration in native_field(&file?, "declarations")?.try_iter()? { let declaration: Bound<'py, PyAny> = declaration?; - let name: Bound<'py, PyAny> = declaration.getattr("name")?; + let name: Bound<'py, PyAny> = native_field(&declaration, "name")?; if let Some(existing) = index.get_item(&name)? { return Err(compile_input_error( py, @@ -60,8 +61,12 @@ pub(crate) fn public_declaration_indexes<'py>( let py: Python<'py> = inputs.py(); let enums: Bound<'py, PyDict> = PyDict::new(py); let constants: Bound<'py, PyDict> = PyDict::new(py); - index_declarations(&inputs.getattr("enum_files")?, "enum", &enums)?; - index_declarations(&inputs.getattr("constant_files")?, "constant", &constants)?; + index_declarations(&native_field(&inputs, "enum_files")?, "enum", &enums)?; + index_declarations( + &native_field(&inputs, "constant_files")?, + "constant", + &constants, + )?; Ok((enums, constants)) } @@ -91,7 +96,7 @@ impl<'py> SchemaResolution<'py> { )); } let declaration: Bound<'py, PyAny> = self.authored[self.by_name[name]].1.clone(); - let extends: Option = declaration.getattr("extends")?.extract()?; + let extends: Option = native_field(&declaration, "extends")?.extract()?; let mut inherited: Vec> = Vec::new(); if let Some(parent) = extends { if !self.by_name.contains_key(&parent) { @@ -106,13 +111,13 @@ impl<'py> SchemaResolution<'py> { let mut chain: Vec = resolving.to_vec(); chain.push(name.to_owned()); let parent: Bound<'py, PyAny> = self.resolve(&parent, &chain)?; - for column in parent.getattr("columns")?.try_iter()? { + for column in native_field(&parent, "columns")?.try_iter()? { inherited.push(column?); } } self.reject_overrides(name, &declaration, &inherited)?; let mut columns: Vec> = inherited; - for column in declaration.getattr("columns")?.try_iter()? { + for column in native_field(&declaration, "columns")?.try_iter()? { columns.push(column?); } let fields: Bound<'py, PyDict> = PyDict::new(self.py); @@ -134,12 +139,12 @@ impl<'py> SchemaResolution<'py> { for column in inherited { by_name.insert(lowered_name(column)?, column); } - for local in declaration.getattr("columns")?.try_iter()? { + for local in native_field(declaration, "columns")?.try_iter()? { let local: Bound<'py, PyAny> = local?; let Some(column) = by_name.get(&lowered_name(&local)?) else { continue; }; - let location: Bound<'py, PyAny> = column.getattr("location")?; + let location: Bound<'py, PyAny> = native_field(column, "location")?; let origin: String = if location.is_none() { "an ancestor schema".to_owned() } else { @@ -161,7 +166,9 @@ impl<'py> SchemaResolution<'py> { /// `column.name.lower()`. fn lowered_name(column: &Bound<'_, PyAny>) -> PyResult { - column.getattr("name")?.call_method0("lower")?.extract() + native_field(column, "name")? + .call_method0("lower")? + .extract() } /// Every public model schema by name, with its inherited columns first, collision-checked. @@ -172,10 +179,10 @@ pub(crate) fn public_model_schema_index<'py>( let py: Python<'py> = inputs.py(); let mut authored: Vec<(String, Bound<'py, PyAny>)> = Vec::new(); let mut by_name: HashMap = HashMap::new(); - for file in inputs.getattr("model_schema_files")?.try_iter()? { - for declaration in file?.getattr("declarations")?.try_iter()? { + for file in native_field(&inputs, "model_schema_files")?.try_iter()? { + for declaration in native_field(&file?, "declarations")?.try_iter()? { let declaration: Bound<'py, PyAny> = declaration?; - let name: String = declaration.getattr("name")?.extract()?; + let name: String = native_field(&declaration, "name")?.extract()?; if let Some(index) = by_name.get(&name) { return Err(compile_input_error( py, 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 58f0c57528..7c4dd7b3fe 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/render/declaration_values.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/render/declaration_values.rs @@ -9,6 +9,7 @@ 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::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::scope_lookup::build_scope_lookup; @@ -50,10 +51,14 @@ fn runtime_declarations<'py>( 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 declaration in file?.getattr("declarations")?.try_iter()? { + for declaration in native_field(&file?, "declarations")?.try_iter()? { let declaration: Bound<'py, PyAny> = declaration?; - let identity = - declaration_identity(&classes, kind, &declaration.getattr("name")?, None)?; + let identity = declaration_identity( + &classes, + kind, + &native_field(&declaration, "name")?, + None, + )?; declarations.set_item(identity, declaration)?; } } @@ -61,7 +66,7 @@ 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")?; - for model_file in discovered.getattr("model_files")?.try_iter()? { + 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")? @@ -69,7 +74,7 @@ fn runtime_declarations<'py>( let name: Bound<'py, PyAny> = if name.is_instance_of::() && name.is_truthy()? { name } else { - model_file.getattr("relative_path")?.getattr("stem")? + native_field(&model_file, "relative_path")?.getattr("stem")? }; let owner: Bound<'py, PyAny> = resource_identity.call1((&model_kind, name))?; for (attribute, kind) in [ @@ -81,7 +86,7 @@ fn runtime_declarations<'py>( let identity = declaration_identity( &classes, kind, - &declaration.getattr("name")?, + &native_field(&declaration, "name")?, Some(&owner), )?; declarations.set_item(identity, declaration)?; @@ -91,8 +96,12 @@ fn runtime_declarations<'py>( if !loaded_macros.is_none() { for name_and_macro in loaded_macros.call_method0("values")?.try_iter()? { let declared_macro: Bound<'py, PyAny> = name_and_macro?; - let identity = - declaration_identity(&classes, "MACRO", &declared_macro.getattr("name")?, None)?; + let identity = declaration_identity( + &classes, + "MACRO", + &native_field(&declared_macro, "name")?, + None, + )?; declarations.set_item(identity, declared_macro)?; } } @@ -105,15 +114,15 @@ fn resource_specific<'py>(scope_index: &Bound<'py, PyAny>) -> PyResult = registry::class(py, RecordId::ScopeKind)?.getattr("PRIVATE")?; let mut resources: Vec> = Vec::new(); - for declaration in scope_index.getattr("declarations")?.try_iter()? { + for declaration in native_field(scope_index, "declarations")?.try_iter()? { let declaration: Bound<'py, PyAny> = declaration?; - let owner: Bound<'py, PyAny> = declaration.getattr("identity")?.getattr("owner")?; - if declaration.getattr("scope")?.is(&private) && !owner.is_none() { + let owner: Bound<'py, PyAny> = native_field(&declaration, "identity")?.getattr("owner")?; + if native_field(&declaration, "scope")?.is(&private) && !owner.is_none() { resources.push(owner); } } - for grant in scope_index.getattr("grants")?.try_iter()? { - resources.push(grant?.getattr("resource")?); + for grant in native_field(scope_index, "grants")?.try_iter()? { + resources.push(native_field(&grant?, "resource")?); } PyFrozenSet::new(py, resources) } @@ -143,7 +152,7 @@ pub(crate) fn declaration_scope_resolver<'py>( call_with( ®istry::class(py, RecordId::DeclarationScopeResolver)?, &[ - ("project_dir", discovered.getattr("project_dir")?), + ("project_dir", native_field(discovered, "project_dir")?), ("lookup", lookup), ("projection", projection), ( @@ -207,11 +216,11 @@ pub(crate) fn native_declaration_contexts<'py>( registry::class(py, RecordId::ConstantDeclaration)?, registry::class(py, RecordId::LoadedMacro)?, ]; - let index: Bound<'py, PyAny> = lookup.getattr("visibility_index")?; - let lookup_declarations: Bound<'py, PyAny> = lookup.getattr("declarations")?; + let index: Bound<'py, PyAny> = native_field(lookup, "visibility_index")?; + let lookup_declarations: Bound<'py, PyAny> = native_field(lookup, "declarations")?; let identity_keys: Bound<'py, PyDict> = PyDict::new(py); let mut positions: Vec> = Vec::new(); - for identity in index.getattr("identities")?.try_iter()? { + for identity in native_field(&index, "identities")?.try_iter()? { let identity: Bound<'py, PyAny> = identity?; let key: Bound<'py, PyAny> = identity_keys.call_method1("setdefault", (&identity, identity_keys.len()))?; @@ -221,8 +230,8 @@ pub(crate) fn native_declaration_contexts<'py>( [ identity.clone(), lookup_declarations.get_item(&identity)?.get_item(0)?, - identity.getattr("name")?, - kind_code(&kinds, &identity.getattr("kind")?) + native_field(&identity, "name")?, + kind_code(&kinds, &native_field(&identity, "kind")?) .into_pyobject(py)? .into_any(), value.clone(), @@ -236,7 +245,7 @@ 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()? { let reason: Bound<'py, PyAny> = reason?; - reasons.set_item(reason.getattr("value")?, reason)?; + reasons.set_item(native_field(&reason, "value")?, reason)?; } let classes: Bound<'py, PyDict> = PyDict::new(py); classes.set_item( @@ -250,15 +259,24 @@ pub(crate) fn native_declaration_contexts<'py>( classes.set_item("reasons", reasons)?; let keys: Bound<'py, PyDict> = PyDict::new(py); keys.set_item("identity_keys", identity_keys)?; - keys.set_item("private_positions", index.getattr("private_positions")?)?; - keys.set_item("grants_by_resource", lookup.getattr("grants_by_resource")?)?; + keys.set_item( + "private_positions", + native_field(&index, "private_positions")?, + )?; + keys.set_item( + "grants_by_resource", + native_field(lookup, "grants_by_resource")?, + )?; let lookups: Bound<'py, PyDict> = PyDict::new(py); - lookups.set_item("resources_by_path", lookup.getattr("resources_by_path")?)?; - lookups.set_item("resources", lookup.getattr("resources")?)?; + lookups.set_item( + "resources_by_path", + native_field(lookup, "resources_by_path")?, + )?; + lookups.set_item("resources", native_field(lookup, "resources")?)?; lookups.set_item("declarations", &lookup_declarations)?; lookups.set_item( "index_declarations", - lookup.getattr("index")?.getattr("declarations")?, + native_field(lookup, "index")?.getattr("declarations")?, )?; for (name, class) in [ ("resource_record", RecordId::ResourceRecord), @@ -296,8 +314,8 @@ pub(crate) fn native_declaration_contexts<'py>( PyList::new(py, positions)?, ( global_positions, - listed_positions(&index.getattr("local_positions")?)?, - listed_positions(&index.getattr("inherited_positions")?)?, + listed_positions(&native_field(&index, "local_positions")?)?, + listed_positions(&native_field(&index, "inherited_positions")?)?, ), classes, (keys, lookups), @@ -335,8 +353,8 @@ fn rendered_constant<'py>( value_renderer: &Bound<'py, PyAny>, selected: &Bound<'py, PyAny>, ) -> PyResult { - let value: Bound<'py, PyAny> = declaration.getattr("value")?; - let kind: String = value.getattr("kind")?.str()?.extract()?; + let value: Bound<'py, PyAny> = native_field(declaration, "value")?; + let kind: String = native_field(&value, "kind")?.str()?.extract()?; let method: &str = if SCALAR_KINDS.contains(&kind.as_str()) { "render_typed_scalar" } else if COLLECTION_KINDS.contains(&kind.as_str()) { @@ -354,8 +372,8 @@ fn rendered_constant<'py>( if size > MAX_SQL_VALUE_SIZE { let context: String = format!( "{} constant '{}'", - declaration.getattr("relative_path")?.str()?, - declaration.getattr("name")?.str()? + native_field(declaration, "relative_path")?.str()?, + native_field(declaration, "name")?.str()? ); return Err(PyErr::from_value( registry::native(declaration.py(), NativeAttr::SqlValueValidationError)? @@ -374,7 +392,7 @@ pub(crate) fn constant_declaration_sql<'py>( (collection_rendering, file_path): (&Bound<'py, PyAny>, Option<&str>), ) -> PyResult { let py: Python<'py> = declaration.py(); - let render_as: Bound<'py, PyAny> = declaration.getattr("render_as")?; + let render_as: Bound<'py, PyAny> = native_field(declaration, "render_as")?; let selected: Bound<'py, PyAny> = if render_as.is_truthy()? { render_as } else { @@ -391,7 +409,7 @@ pub(crate) fn constant_declaration_sql<'py>( if !(raised.is_instance(&rendering_error)? || raised.is_instance(&validation_error)?) { return Err(error); } - let relative_path: Bound<'py, PyAny> = declaration.getattr("relative_path")?; + let relative_path: Bound<'py, PyAny> = native_field(declaration, "relative_path")?; let location: String = match file_path { Some(file_path) => file_path.to_owned(), None => relative_path.str()?.to_string(), @@ -400,10 +418,10 @@ pub(crate) fn constant_declaration_sql<'py>( registry::error(py, ErrorId::CompileInput)?.call1((format!( "{} constant '{}' could not be rendered in '{}' by adapter '{}' as {}: {}", relative_path.str()?, - declaration.getattr("name")?.str()?, + native_field(declaration, "name")?.str()?, location, - value_renderer.getattr("adapter_name")?.str()?, - selected.getattr("value")?.str()?, + native_field(value_renderer, "adapter_name")?.str()?, + native_field(&selected, "value")?.str()?, raised.str()?, ),))?; compile_error.setattr("__cause__", &raised)?; 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 ed4f0f630d..32828a6b24 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 @@ -8,6 +8,7 @@ use pyo3::types::{PyDict, PyDictMethods}; use sqlbuild_render::macro_calls::main::call_class_store_text::call_class_store_text; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::render::macro_expansion_facts::ResourceRef; /// `(kind, name, owner (kind, name))` of a `DeclarationIdentity`, compared by value. @@ -83,7 +84,7 @@ impl CallClassState { inputs.loaded_macros.as_any() }; let macros: Bound<'_, PyAny> = if scoped { - inputs.declarations.getattr("macros")? + native_field(inputs.declarations, "macros")? } else { inputs.loaded_macros.clone().into_any() }; @@ -230,8 +231,8 @@ impl CallClassState { None } else { Some(( - value_ids(&declarations.getattr("constants")?.downcast_into()?)?, - value_ids(&declarations.getattr("enums")?.downcast_into()?)?, + value_ids(&native_field(declarations, "constants")?.downcast_into()?)?, + value_ids(&native_field(declarations, "enums")?.downcast_into()?)?, )) }; let store_token: Option = if self.store_tokens { @@ -264,18 +265,20 @@ fn identity_key(identity: &Bound<'_, PyAny>) -> PyResult> { if identity.is_none() { return Ok(None); } - let owner: Bound<'_, PyAny> = identity.getattr("owner")?; + let owner: Bound<'_, PyAny> = native_field(identity, "owner")?; let owner_key: Option<(String, String)> = if owner.is_none() { None } else { Some(( - owner.getattr("kind")?.getattr("value")?.extract()?, - owner.getattr("name")?.extract()?, + native_field(&owner, "kind")?.getattr("value")?.extract()?, + native_field(&owner, "name")?.extract()?, )) }; Ok(Some(( - identity.getattr("kind")?.getattr("value")?.extract()?, - identity.getattr("name")?.extract()?, + native_field(identity, "kind")? + .getattr("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 7a2d74ca01..cd0c168c06 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/render/macro_evaluation.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/render/macro_evaluation.rs @@ -14,6 +14,7 @@ use sqlbuild_render::macro_calls::main::macro_call_names::macro_call_names as sc use sqlbuild_render::macro_calls::main::scan_macro_call_sites::scan_macro_call_sites as scan_sites; use sqlbuild_render::macro_calls::models::{MacroCallScan, MacroCallSite}; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::render::macro_arguments::parsed_arguments; use crate::bindings::_helpers::render::macro_expansion::{ ExpansionClasses, NativeMacroExpansion, raised, @@ -157,8 +158,8 @@ fn invoke<'py>( ) -> PyResult> { let (name, loaded_macro) = called; let (args, kwargs) = arguments; - let function: Bound<'py, PyAny> = loaded_macro.getattr("function")?; - if !loaded_macro.getattr("injects_context")?.is_truthy()? { + let function: Bound<'py, PyAny> = native_field(loaded_macro, "function")?; + if !native_field(loaded_macro, "injects_context")?.is_truthy()? { return function.call(args, Some(kwargs)); } if kwargs.contains(CONTEXT_PARAMETER)? { @@ -211,7 +212,7 @@ fn final_sql(run: &Evaluation<'_, '_>, name: &str, value: &Bound<'_, PyAny>) -> if !error.is_instance_of::(py) { return Err(error); } - let start: Bound<'_, PyAny> = error.value(py).getattr("start")?; + let start: Bound<'_, PyAny> = native_field(error.value(py), "start")?; let surrogate: String = value.get_item(start)?.repr()?.to_string(); let message: String = format!( "Macro '@{name}' in '{}' returned text with the lone surrogate {surrogate}, which is \ 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 09fd041490..1545aedf17 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/render/macro_expansion.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/render/macro_expansion.rs @@ -15,6 +15,7 @@ use sqlbuild_render::macro_calls::models::{ MacroCallEntry, MacroCallEvent, MacroCallScan, MacroCallSite, MacroScanFailure, MacroSpliceSpan, }; +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, @@ -321,13 +322,14 @@ impl NativeMacroExpansion { let macros: Bound<'py, PyAny> = if declarations.is_none() { self.loaded_macros.bind(py).clone().into_any() } else { - declarations.getattr("macros")? + native_field(declarations, "macros")? }; if !declarations.is_none() { - let inaccessible: Bound<'py, PyAny> = declarations.getattr("inaccessible_macros")?; + let inaccessible: Bound<'py, PyAny> = + native_field(declarations, "inaccessible_macros")?; if inaccessible.contains(name)? { let record: Bound<'py, PyAny> = inaccessible.get_item(name)?; - let owning: Bound<'py, PyAny> = record.getattr("owning_path")?; + let owning: Bound<'py, PyAny> = native_field(&record, "owning_path")?; let owner: String = if owning.is_truthy()? { owning.str()?.to_string() } else { @@ -337,9 +339,9 @@ impl NativeMacroExpansion { "Macro '@{name}' in '{}' is inaccessible. Defined at '{}:{}:{}' with scope \ owner '{owner}'; consumer/definition path is '{}'. Visible macros: {}", self.file_label, - record.getattr("path")?.str()?, - record.getattr("line")?.str()?, - record.getattr("column")?.str()?, + native_field(&record, "path")?.str()?, + native_field(&record, "line")?.str()?, + native_field(&record, "column")?.str()?, self.file_label, sorted_names(¯os)?, ); @@ -391,7 +393,7 @@ impl NativeMacroExpansion { usages.push(usage( classes, (consumer, identity), - Some(&visible.getattr("through")?), + Some(&native_field(&visible, "through")?), )?); } usages @@ -424,7 +426,7 @@ impl NativeMacroExpansion { let consumer: Bound<'_, PyAny> = if own_consumer.is_truthy()? { own_consumer.clone() } else { - declarations.getattr("consumer")? + native_field(declarations, "consumer")? }; if consumer.is_none() { return Ok(()); @@ -444,15 +446,15 @@ impl NativeMacroExpansion { { usages.push(usage( classes, - (&consumer, &visible.getattr("declaration")?), - Some(&visible.getattr("through")?), + (&consumer, &native_field(&visible, "declaration")?), + Some(&native_field(&visible, "through")?), )?); } } else { let declaration: Bound<'_, PyAny> = declarations .getattr(if constant { "constants" } else { "enums" })? .get_item(name)?; - let model_name: Bound<'_, PyAny> = declaration.getattr("model_name")?; + let model_name: Bound<'_, PyAny> = native_field(&declaration, "model_name")?; let owner: Option> = if model_name.is_none() { None } else { @@ -528,7 +530,7 @@ impl NativeMacroExpansion { PyErr::new::(macro_name.clone()) })? } else { - declarations.getattr("macros")?.get_item(¯o_name)? + native_field(declarations, "macros")?.get_item(¯o_name)? }; self.report_generated(classes, ¯o_name, &loaded_macro, &sql) } @@ -680,8 +682,10 @@ fn top_level_output( /// A `SqlResourceRef`'s kind value and name. pub(crate) fn resource_key(reference: &Bound<'_, PyAny>) -> PyResult { Ok(( - reference.getattr("kind")?.getattr("value")?.extract()?, - reference.getattr("name")?.extract()?, + native_field(reference, "kind")? + .getattr("value")? + .extract()?, + native_field(reference, "name")?.extract()?, )) } @@ -707,7 +711,9 @@ fn relationship_consumer( if !consumer.is_instance(&classes.resource_identity)? { return Ok(false); } - let kind: String = consumer.getattr("kind")?.getattr("value")?.extract()?; + let kind: String = native_field(consumer, "kind")? + .getattr("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 4b14f89002..8e58c7ded7 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 @@ -11,6 +11,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::{ }; 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::registry; use crate::bindings::_helpers::render::declaration_values::resolved_declaration_context; @@ -130,7 +131,7 @@ fn built_classes(py: Python<'_>) -> PyResult> { "declaration_identity", registry::class(py, RecordId::DeclarationIdentity)?, ), - ("macro_kind", declaration_kind.getattr("MACRO")?), + ("macro_kind", native_field(&declaration_kind, "MACRO")?), ("declaration_kind", declaration_kind), ("usage_record", registry::class(py, RecordId::UsageRecord)?), ( 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 19e2f37ea4..8cb827b0b4 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::records::fields::field as native_field; use crate::bindings::_helpers::sqltext::lexical_syntax::LexicalSyntaxInput; const REFERENCE_CALL_SYNTAX_CODE: &str = "P012"; @@ -72,7 +73,7 @@ pub(crate) fn report_generated_references( classes, origin, loaded_macro, - macro_name: loaded_macro.getattr("name")?.extract()?, + macro_name: native_field(loaded_macro, "name")?.extract()?, resource: resource_kind(classes, origin.consumer)?, }; for invalid_call in &scan.invalid_calls { @@ -97,8 +98,10 @@ fn resource_kind( return Ok(None); } Ok(Some(( - consumer.getattr("kind")?.getattr("value")?.extract()?, - consumer.getattr("name")?.extract()?, + native_field(consumer, "kind")? + .getattr("value")? + .extract()?, + native_field(consumer, "name")?.extract()?, ))) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/result_output/aggregation.rs b/crates/sqlbuild-python/src/bindings/_helpers/result_output/aggregation.rs index cf0ecef542..ebc31c43f0 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/result_output/aggregation.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/result_output/aggregation.rs @@ -10,6 +10,7 @@ use crate::bindings::_helpers::boundary::keyword_request::Request; 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::records::fields::field as native_field; const RESULT_FIELDS: [&str; 7] = [ "model_results", @@ -34,10 +35,10 @@ fn facts(sections: &[Bound<'_, PyAny>]) -> PyResult { .map(|model| { let model: Bound<'_, PyAny> = model?; Ok(ModelResultFacts { - status: model.getattr("status")?.str()?.extract()?, - error_code: model.getattr("error_code")?.extract()?, - warning_count: model.getattr("warning_messages")?.len()?, - audit_outcomes: texts(&model.getattr("audit_results")?, "outcome")?, + status: native_field(&model, "status")?.str()?.extract()?, + error_code: native_field(&model, "error_code")?.extract()?, + warning_count: native_field(&model, "warning_messages")?.len()?, + audit_outcomes: texts(&native_field(&model, "audit_results")?, "outcome")?, }) }) .collect::>()?; @@ -46,8 +47,8 @@ fn facts(sections: &[Bound<'_, PyAny>]) -> PyResult { .map(|function| { let function: Bound<'_, PyAny> = function?; Ok(( - function.getattr("status")?.str()?.extract()?, - function.getattr("warning_messages")?.len()?, + native_field(&function, "status")?.str()?.extract()?, + native_field(&function, "warning_messages")?.len()?, )) }) .collect::>()?; 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 a31e58fdbb..106c358225 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/result_output/assets.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/result_output/assets.rs @@ -5,6 +5,7 @@ use pyo3::types::{PyDict, PyDictMethods, PyList}; use crate::bindings::_helpers::plan_output::safety::{future_cursor_safety, maximum_start_safety}; use crate::bindings::_helpers::plan_output::values::{any, dict, items, optional_enum_value}; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::result_output::projection::{Projection, rounded}; const FAILED_STATUS: &str = "failed"; @@ -44,7 +45,7 @@ fn targets<'py>( let names: Bound<'py, PyDict> = PyDict::new(py); for item in plan.getattr(field)?.call_method0("items")?.try_iter()? { let (name, location): (Bound<'py, PyAny>, Bound<'py, PyAny>) = item?.extract()?; - names.set_item(name, location.getattr("qualified_name")?)?; + names.set_item(name, native_field(&location, "qualified_name")?)?; } Ok(Some(names.into_any())) } @@ -70,14 +71,14 @@ fn last_recorded_sql<'py>(result: &Bound<'py, PyAny>) -> PyResult> = items(&result.getattr("lifecycle_events")?)?; + 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)? { - return event.getattr("content"); + return native_field(event, "content"); } } Ok(py.None().into_bound(py)) @@ -86,7 +87,7 @@ fn last_recorded_sql<'py>(result: &Bound<'py, PyAny>) -> PyResult(result: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = result.py(); - let run_type: Bound<'py, PyAny> = result.getattr("microbatch_run_type")?; + let run_type: Bound<'py, PyAny> = native_field(result, "microbatch_run_type")?; if run_type.is_none() { return Ok(run_type); } @@ -142,7 +143,7 @@ fn microbatch<'py>(result: &Bound<'py, PyAny>) -> PyResult> { state.set_item(key, result.getattr(field)?)?; } let intervals: Bound<'py, PyList> = PyList::empty(py); - for interval in items(&result.getattr("microbatch_accounting_intervals")?)? { + for interval in items(&native_field(result, "microbatch_accounting_intervals")?)? { let fields: Vec<(&str, Bound<'py, PyAny>)> = [ "partition_start", "partition_end", @@ -158,21 +159,21 @@ fn microbatch<'py>(result: &Bound<'py, PyAny>) -> PyResult> { pyo3::types::PyListMethods::append(&intervals, dict(py, fields)?)?; } state.set_item("intervals", intervals)?; - let limit: Bound<'py, PyAny> = result.getattr("microbatch_limit")?; + let limit: Bound<'py, PyAny> = native_field(result, "microbatch_limit")?; if !limit.is_none() { state.set_item("limit", limit)?; - state.set_item("count", result.getattr("microbatch_limit_count")?)?; + state.set_item("count", native_field(result, "microbatch_limit_count")?)?; state.set_item( "action", - optional_enum_value(&result.getattr("microbatch_limit_action")?)?, + optional_enum_value(&native_field(result, "microbatch_limit_action")?)?, )?; - state.set_item("warning", result.getattr("microbatch_limit_warning")?)?; + state.set_item("warning", native_field(result, "microbatch_limit_warning")?)?; } Ok(state.into_any()) } fn value<'py>(result: &Bound<'py, PyAny>, field: &str) -> PyResult> { - result.getattr(field)?.getattr("value") + native_field(&result.getattr(field)?, "value") } /// `.value` when it is set. @@ -181,7 +182,7 @@ fn optional_value<'py>(result: &Bound<'py, PyAny>, field: &str) -> PyResult( @@ -190,10 +191,10 @@ fn model_asset<'py>( targets: Option<&Bound<'py, PyAny>>, ) -> PyResult> { let py: Python<'py> = result.py(); - let name: Bound<'py, PyAny> = result.getattr("model_name")?; + let name: Bound<'py, PyAny> = native_field(result, "model_name")?; let planned: Bound<'py, PyAny> = target(py, targets, &name)?; let empty_inputs: Bound<'py, PyAny> = - PyList::new(py, items(&result.getattr("empty_cursor_inputs")?)?)?.into_any(); + PyList::new(py, items(&native_field(result, "empty_cursor_inputs")?)?)?.into_any(); without_none( py, vec![ @@ -202,38 +203,41 @@ fn model_asset<'py>( ("status", value(result, "status")?), ( "duration_ms", - projection.result_duration(result, &result.getattr("duration_ms")?)?, + projection.result_duration(result, &native_field(result, "duration_ms")?)?, ), ( "target", if planned.is_truthy()? { planned } else { - result.getattr("promoted_relation")? + native_field(result, "promoted_relation")? }, ), - ("staging_relation", result.getattr("staging_relation")?), + ( + "staging_relation", + native_field(result, "staging_relation")?, + ), ("failed_phase", optional_value(result, "failed_phase")?), ("skip_mode", optional_value(result, "skip_mode")?), - ("skip_reason", result.getattr("skip_reason")?), - ("error_code", result.getattr("error_code")?), - ("error_help", result.getattr("error_help")?), - ("error_message", result.getattr("error_message")?), - ("failed_sql", result.getattr("failed_sql")?), + ("skip_reason", native_field(result, "skip_reason")?), + ("error_code", native_field(result, "error_code")?), + ("error_help", native_field(result, "error_help")?), + ("error_message", native_field(result, "error_message")?), + ("failed_sql", native_field(result, "failed_sql")?), ("last_recorded_sql", last_recorded_sql(result)?), - ("warnings", result.getattr("warning_messages")?), + ("warnings", native_field(result, "warning_messages")?), ("empty_cursor_inputs", or_none(empty_inputs)?), ( "waiting_on_empty_inputs", - or_none(result.getattr("waiting_on_empty_inputs")?)?, + or_none(native_field(result, "waiting_on_empty_inputs")?)?, ), ( "future_cursor_safety", - future_cursor_safety(&result.getattr("future_cursor_safety")?)?, + future_cursor_safety(&native_field(result, "future_cursor_safety")?)?, ), ( "maximum_start_safety", - maximum_start_safety(&result.getattr("maximum_start_safety")?)?, + maximum_start_safety(&native_field(result, "maximum_start_safety")?)?, ), ("microbatch", microbatch(result)?), ], @@ -266,8 +270,8 @@ pub(crate) fn seed_assets<'py>( let reasons: Option> = match plan { Some(plan) => { let reasons: Bound<'py, PyDict> = PyDict::new(plan.py()); - for entry in items(&plan.getattr("seed_entries")?)? { - reasons.set_item(entry.getattr("name")?, value(&entry, "reason")?)?; + for entry in items(&native_field(plan, "seed_entries")?)? { + reasons.set_item(native_field(&entry, "name")?, value(&entry, "reason")?)?; } Some(reasons.into_any()) } @@ -279,7 +283,7 @@ pub(crate) fn seed_assets<'py>( continue; } let py: Python<'py> = result.py(); - let name: Bound<'py, PyAny> = result.getattr("seed_name")?; + let name: Bound<'py, PyAny> = native_field(result, "seed_name")?; assets.push(without_none( py, vec![ @@ -288,13 +292,13 @@ pub(crate) fn seed_assets<'py>( ("status", value(result, "status")?), ( "duration_ms", - projection.result_duration(result, &result.getattr("duration_ms")?)?, + projection.result_duration(result, &native_field(result, "duration_ms")?)?, ), ("target", target(py, targets.as_ref(), &name)?), ("reason", target(py, reasons.as_ref(), &name)?), - ("error_code", result.getattr("error_code")?), - ("error_help", result.getattr("error_help")?), - ("error_message", result.getattr("error_message")?), + ("error_code", native_field(result, "error_code")?), + ("error_help", native_field(result, "error_help")?), + ("error_message", native_field(result, "error_message")?), ], )?); } @@ -314,22 +318,22 @@ pub(crate) fn function_assets<'py>( continue; } let py: Python<'py> = result.py(); - let name: Bound<'py, PyAny> = result.getattr("function_name")?; + let name: Bound<'py, PyAny> = native_field(result, "function_name")?; assets.push(without_none( py, vec![ - ("kind", result.getattr("function_kind")?), + ("kind", native_field(result, "function_kind")?), ("name", name.clone()), ("status", value(result, "status")?), ( "duration_ms", - projection.result_duration(result, &result.getattr("duration_ms")?)?, + projection.result_duration(result, &native_field(result, "duration_ms")?)?, ), ("target", target(py, targets.as_ref(), &name)?), - ("error_code", result.getattr("error_code")?), - ("error_help", result.getattr("error_help")?), - ("error_message", result.getattr("error_message")?), - ("warnings", result.getattr("warning_messages")?), + ("error_code", native_field(result, "error_code")?), + ("error_help", native_field(result, "error_help")?), + ("error_message", native_field(result, "error_message")?), + ("warnings", native_field(result, "warning_messages")?), ], )?); } @@ -351,20 +355,26 @@ pub(crate) fn load_assets<'py>( py, vec![ ("kind", value(result, "resource_kind")?), - ("name", result.getattr("source_name")?), + ("name", native_field(result, "source_name")?), ("status", value(result, "status")?), ( "duration_ms", - projection.result_duration(result, &result.getattr("duration_ms")?)?, + projection.result_duration(result, &native_field(result, "duration_ms")?)?, + ), + ("target", native_field(result, "target")?), + ( + "staging_relation", + native_field(result, "staging_relation")?, ), - ("target", result.getattr("target")?), - ("staging_relation", result.getattr("staging_relation")?), - ("loader", result.getattr("loader_name")?), - ("rows_loaded", result.getattr("rows_loaded")?), + ("loader", native_field(result, "loader_name")?), + ("rows_loaded", native_field(result, "rows_loaded")?), ("skip_mode", optional_value(result, "skip_mode")?), - ("skip_reason", result.getattr("skip_reason")?), - ("error_message", result.getattr("error_message")?), - ("warnings", or_none(result.getattr("warning_messages")?)?), + ("skip_reason", native_field(result, "skip_reason")?), + ("error_message", native_field(result, "error_message")?), + ( + "warnings", + or_none(native_field(result, "warning_messages")?)?, + ), ], )?); } @@ -386,14 +396,17 @@ pub(crate) fn python_node_assets<'py>( py, vec![ ("kind", value(result, "kind")?), - ("name", result.getattr("node_name")?), + ("name", native_field(result, "node_name")?), ("status", value(result, "status")?), - ("metadata", result.getattr("metadata")?), - ("materialized", result.getattr("materialized")?), + ("metadata", native_field(result, "metadata")?), + ("materialized", native_field(result, "materialized")?), ("skip_mode", optional_value(result, "skip_mode")?), - ("skip_reason", result.getattr("skip_reason")?), - ("error_message", result.getattr("error_message")?), - ("warnings", or_none(result.getattr("warning_messages")?)?), + ("skip_reason", native_field(result, "skip_reason")?), + ("error_message", native_field(result, "error_message")?), + ( + "warnings", + or_none(native_field(result, "warning_messages")?)?, + ), ], )?); } @@ -407,8 +420,8 @@ pub(crate) fn clone_asset<'py>( resource_type: &str, ) -> PyResult> { let py: Python<'py> = item.py(); - let name: String = item.getattr("name")?.extract()?; - let seconds: Bound<'py, PyAny> = item.getattr("duration_seconds")?; + let name: String = native_field(item, "name")?.extract()?; + let seconds: Bound<'py, PyAny> = native_field(item, "duration_seconds")?; let fallback: Option = if seconds.is_none() { None } else { @@ -430,9 +443,9 @@ pub(crate) fn clone_asset<'py>( None => py.None().into_bound(py), }, ), - ("origin_relation", item.getattr("origin_relation")?), - ("target", item.getattr("destination_relation")?), - ("message", item.getattr("message")?), + ("origin_relation", native_field(item, "origin_relation")?), + ("target", native_field(item, "destination_relation")?), + ("message", native_field(item, "message")?), ], ) } 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 27469ab676..b2f50535f9 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/result_output/checks.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/result_output/checks.rs @@ -4,6 +4,7 @@ use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; use pyo3::types::{PyDict, PyDictMethods, PyList, PyListMethods, PyTuple}; use crate::bindings::_helpers::plan_output::values::{any, dict, items, text}; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::result_output::assets::{or_none, without_none}; use crate::bindings::_helpers::result_output::projection::{ Projection, audit_check_id, rounded, sql_test_check_id, @@ -19,7 +20,7 @@ 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> { - result.getattr(field)?.getattr("value") + native_field(&result.getattr(field)?, "value") } fn optional_value<'py>(result: &Bound<'py, PyAny>, field: &str) -> PyResult> { @@ -27,7 +28,7 @@ fn optional_value<'py>(result: &Bound<'py, PyAny>, field: &str) -> PyResult( @@ -47,8 +48,8 @@ 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(&value.getattr("kind")?.getattr("value")?)?; - let inner: Bound<'py, PyAny> = value.getattr("value")?; + let kind: String = text(&native_field(value, "kind")?.getattr("value")?)?; + let inner: Bound<'py, PyAny> = native_field(value, "value")?; if kind == DECIMAL_KIND { return Ok(inner.str()?.into_any()); } @@ -75,26 +76,26 @@ fn case_metadata<'py>( result: &Bound<'py, PyAny>, ) -> PyResult)>> { let py: Python<'py> = result.py(); - let source_path: Bound<'py, PyAny> = result.getattr("source_path")?; - if result.getattr("case_name")?.is_none() || source_path.is_none() { + let source_path: Bound<'py, PyAny> = native_field(result, "source_path")?; + if native_field(result, "case_name")?.is_none() || source_path.is_none() { return Ok(Vec::new()); } let parameter_types: Bound<'py, PyDict> = PyDict::new(py); let mut schema: Vec> = Vec::new(); - for parameter in items(&result.getattr("parameter_schema")?)? { + for parameter in items(&native_field(result, "parameter_schema")?)? { let parameter_type: Bound<'py, PyAny> = value(¶meter, "value_type")?; - parameter_types.set_item(parameter.getattr("name")?, ¶meter_type)?; + parameter_types.set_item(native_field(¶meter, "name")?, ¶meter_type)?; schema.push(dict( py, vec![ - ("name", parameter.getattr("name")?), + ("name", native_field(¶meter, "name")?), ("type", parameter_type), - ("nullable", parameter.getattr("nullable")?), + ("nullable", native_field(¶meter, "nullable")?), ], )?); } let mut parameters: Vec> = Vec::new(); - for item in items(&result.getattr("parameter_values")?)? { + for item in items(&native_field(result, "parameter_values")?)? { let (name, parameter_value): (Bound<'py, PyAny>, Bound<'py, PyAny>) = item.extract()?; let parameter_type: Bound<'py, PyAny> = parameter_types.as_any().get_item(&name)?; parameters.push(dict( @@ -128,7 +129,7 @@ fn difference_samples<'py>(samples: &Bound<'py, PyAny>) -> PyResult = PyList::empty(py); for sample in items(samples)? { let pairs: Bound<'py, PyList> = PyList::empty(py); - for pair in items(&sample.getattr("values")?)? { + for pair in items(&native_field(&sample, "values")?)? { let (name, sample_value): (Bound<'py, PyAny>, Bound<'py, PyAny>) = pair.extract()?; pairs.append(dict(py, vec![("name", name), ("value", sample_value)])?)?; } @@ -140,14 +141,14 @@ fn difference_samples<'py>(samples: &Bound<'py, PyAny>) -> PyResult(step: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = step.py(); let differences: Bound<'py, PyList> = PyList::empty(py); - for difference in items(&step.getattr("column_differences")?)? { + for difference in items(&native_field(step, "column_differences")?)? { differences.append(dict( py, fields(&difference, &["name", "actual", "expected"])?, )?)?; } let mut entries: Vec<(&str, Bound<'py, PyAny>)> = vec![ - ("model_name", step.getattr("model_name")?), + ("model_name", native_field(step, "model_name")?), ("status", value(step, "outcome")?), ]; entries.extend(fields( @@ -162,11 +163,11 @@ fn sql_test_step<'py>(step: &Bound<'py, PyAny>) -> PyResult> )?); entries.push(( "unexpected_samples", - difference_samples(&step.getattr("unexpected_samples")?)?, + difference_samples(&native_field(step, "unexpected_samples")?)?, )); entries.push(( "missing_samples", - difference_samples(&step.getattr("missing_samples")?)?, + difference_samples(&native_field(step, "missing_samples")?)?, )); entries.push(("column_differences", or_none(differences.into_any())?)); entries.extend(fields( @@ -187,11 +188,11 @@ pub(crate) fn sql_test_checks<'py>( continue; } let py: Python<'py> = result.py(); - let steps: Vec> = items(&result.getattr("step_results")?)?; + let steps: Vec> = items(&native_field(result, "step_results")?)?; let outcome: Bound<'py, PyAny> = value(result, "outcome")?; let mut entries: Vec<(&str, Bound<'py, PyAny>)> = vec![ ("kind", any(py, "sql_test")?), - ("name", result.getattr("test_name")?), + ("name", native_field(result, "test_name")?), ("check_id", any(py, sql_test_check_id(result)?)?), ]; entries.extend(case_metadata(result)?); @@ -204,7 +205,7 @@ pub(crate) fn sql_test_checks<'py>( entries.push(( "asset_name", match steps.first() { - Some(step) => step.getattr("model_name")?, + Some(step) => native_field(step, "model_name")?, None => py.None().into_bound(py), }, )); @@ -227,10 +228,10 @@ pub(crate) fn sql_test_checks<'py>( fn threshold_bound<'py>(bound: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = bound.py(); let rendered: Bound<'py, PyDict> = dict(py, vec![("operator", value(bound, "operator")?)])?; - let limit: Bound<'py, PyAny> = bound.getattr("limit")?; + let limit: Bound<'py, PyAny> = native_field(bound, "limit")?; if limit.is_none() { - rendered.set_item("lower", bound.getattr("lower")?)?; - rendered.set_item("upper", bound.getattr("upper")?)?; + rendered.set_item("lower", native_field(bound, "lower")?)?; + rendered.set_item("upper", native_field(bound, "upper")?)?; } else { rendered.set_item("limit", limit)?; } @@ -263,8 +264,8 @@ pub(crate) fn measurement_evidence<'py>( return Ok((py.None().into_bound(py), py.None().into_bound(py))); } Ok(( - any(py, result.getattr("evidence_rows")?.len()?)?, - result.getattr("evidence_truncated")?, + any(py, native_field(result, "evidence_rows")?.len()?)?, + native_field(result, "evidence_truncated")?, )) } @@ -288,7 +289,7 @@ pub(crate) fn audit_checks<'py>( let py: Python<'py> = result.py(); let (evidence_count, evidence_truncated) = measurement_evidence(&result)?; let mut entries: Vec<(&str, Bound<'py, PyAny>)> = vec![("kind", any(py, "audit")?)]; - entries.push(("name", result.getattr("audit_name")?)); + entries.push(("name", native_field(&result, "audit_name")?)); entries.extend(fields( &result, &["audit_definition_name", "audit_description"], @@ -297,19 +298,19 @@ pub(crate) fn audit_checks<'py>( entries.push(("passed", any(py, audit_passed(&result)?)?)); entries.push(("status", value(&result, "outcome")?)); entries.push(("severity", value(&result, "severity")?)); - entries.push(("row_count", result.getattr("row_count")?)); + entries.push(("row_count", native_field(&result, "row_count")?)); entries.push(("attachment_kind", value(&result, "attachment_kind")?)); entries.push(( "attached_target_kind", optional_value(&result, "attached_target_kind")?, )); - entries.push(("asset_name", result.getattr("attached_target_name")?)); + entries.push(("asset_name", native_field(&result, "attached_target_name")?)); entries.push(( "attached_column_name", - result.getattr("attached_column_name")?, + native_field(&result, "attached_column_name")?, )); entries.push(("run_scope_phase", value(&result, "run_scope_phase")?)); - entries.push(("reused", result.getattr("reused")?)); + entries.push(("reused", native_field(&result, "reused")?)); entries.push(("evaluation_mode", value(&result, "evaluation_mode")?)); entries.extend(fields( &result, @@ -322,11 +323,11 @@ pub(crate) fn audit_checks<'py>( )?); entries.push(( "thresholds", - measurement_thresholds(&result.getattr("thresholds")?)?, + measurement_thresholds(&native_field(&result, "thresholds")?)?, )); entries.push(("evidence_count", evidence_count)); entries.push(("evidence_truncated", evidence_truncated)); - entries.push(("execution_error", result.getattr("execution_error")?)); + entries.push(("execution_error", native_field(&result, "execution_error")?)); checks.push(without_none(py, entries)?); } Ok(checks) @@ -334,9 +335,9 @@ pub(crate) fn audit_checks<'py>( /// `pass`, `warn` or `fail` of a Python check result. pub(crate) fn python_check_status(result: &Bound<'_, PyAny>) -> PyResult<&'static str> { - Ok(if result.getattr("passed")?.is_truthy()? { + Ok(if native_field(result, "passed")?.is_truthy()? { "pass" - } else if result.getattr("warned")?.is_truthy()? { + } else if native_field(result, "warned")?.is_truthy()? { "warn" } else { "fail" @@ -354,20 +355,23 @@ pub(crate) fn python_checks<'py>( continue; } let py: Python<'py> = result.py(); - let name: String = text(&result.getattr("node_name")?)?; + let name: String = text(&native_field(result, "node_name")?)?; checks.push(without_none( py, vec![ ("kind", any(py, "python_check")?), - ("name", result.getattr("node_name")?), + ("name", native_field(result, "node_name")?), ("check_id", any(py, format!("python_check:{name}"))?), - ("passed", result.getattr("passed")?), + ("passed", native_field(result, "passed")?), ("status", any(py, python_check_status(result)?)?), ("severity", value(result, "severity")?), - ("message", result.getattr("message")?), - ("error_message", result.getattr("error_message")?), - ("metadata", result.getattr("metadata")?), - ("warnings", or_none(result.getattr("warning_messages")?)?), + ("message", native_field(result, "message")?), + ("error_message", native_field(result, "error_message")?), + ("metadata", native_field(result, "metadata")?), + ( + "warnings", + or_none(native_field(result, "warning_messages")?)?, + ), ], )?); } @@ -385,12 +389,12 @@ fn with_errors<'py>( /// The scenario check with its expected-result and assertion outcomes. pub(crate) fn scenario_check<'py>(result: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = result.py(); - let name: String = text(&result.getattr("scenario_name")?)?; + let name: String = text(&native_field(result, "scenario_name")?)?; let status: Bound<'py, PyAny> = value(result, "status")?; let mut expected: Vec> = Vec::new(); - for item in items(&result.getattr("expected_results")?)? { + for item in items(&native_field(result, "expected_results")?)? { let mut entries: Vec<(&'static str, Bound<'py, PyAny>)> = vec![ - ("model_name", item.getattr("model_name")?), + ("model_name", native_field(&item, "model_name")?), ("status", value(&item, "status")?), ]; entries.extend(fields( @@ -404,12 +408,15 @@ pub(crate) fn scenario_check<'py>(result: &Bound<'py, PyAny>) -> PyResult> = Vec::new(); - for item in items(&result.getattr("assertion_results")?)? { + for item in items(&native_field(result, "assertion_results")?)? { let entries: Vec<(&'static str, Bound<'py, PyAny>)> = vec![ - ("name", item.getattr("name")?), + ("name", native_field(&item, "name")?), ("status", value(&item, "status")?), - ("failing_row_count", item.getattr("failing_row_count")?), - ("sample_rows", item.getattr("sample_rows")?), + ( + "failing_row_count", + native_field(&item, "failing_row_count")?, + ), + ("sample_rows", native_field(&item, "sample_rows")?), ]; assertions.push(with_errors(&item, entries)?); } @@ -428,7 +435,7 @@ pub(crate) fn scenario_check<'py>(result: &Bound<'py, PyAny>) -> PyResult(result: &Bound<'py, PyAny>) -> PyResult> { - let duration: Bound<'py, PyAny> = result.getattr("duration_ms")?; + let duration: Bound<'py, PyAny> = native_field(result, "duration_ms")?; if duration.is_none() { return Ok(duration); } @@ -438,23 +445,23 @@ fn rounded_duration<'py>(result: &Bound<'py, PyAny>) -> PyResult(result: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = result.py(); - let path: Bound<'py, PyAny> = result.getattr("local_duckdb_path")?; - let local_status: Bound<'py, PyAny> = result.getattr("local_status")?; + let path: Bound<'py, PyAny> = native_field(result, "local_duckdb_path")?; + let local_status: Bound<'py, PyAny> = native_field(result, "local_status")?; with_errors( result, vec![ - ("name", result.getattr("scenario_name")?), + ("name", native_field(result, "scenario_name")?), ("status", value(result, "status")?), ("duration_ms", rounded_duration(result)?), ( "local_status", if local_status.is_truthy()? { - local_status.getattr("value")? + native_field(&local_status, "value")? } else { py.None().into_bound(py) }, ), - ("retained", result.getattr("retained")?), + ("retained", native_field(result, "retained")?), ( "local_duckdb_path", if path.is_truthy()? { @@ -470,35 +477,38 @@ pub(crate) fn scenario_result<'py>(result: &Bound<'py, PyAny>) -> PyResult(result: &Bound<'py, PyAny>) -> PyResult> { let py: Python<'py> = result.py(); - let capture: Bound<'py, PyAny> = result.getattr("capture_result")?; + let capture: Bound<'py, PyAny> = native_field(result, "capture_result")?; let mut relations: Vec> = Vec::new(); let mut manifest_path: Bound<'py, PyAny> = py.None().into_bound(py); if !capture.is_none() { - for relation in items(&capture.getattr("relation_results")?)? { + for relation in items(&native_field(&capture, "relation_results")?)? { relations.push(with_errors( &relation, vec![ ("kind", value(&relation, "kind")?), - ("logical_name", relation.getattr("logical_name")?), - ("source_relation", relation.getattr("source_relation")?), + ("logical_name", native_field(&relation, "logical_name")?), + ( + "source_relation", + native_field(&relation, "source_relation")?, + ), ( "file_path", - relation.getattr("file_path")?.str()?.into_any(), + native_field(&relation, "file_path")?.str()?.into_any(), ), ("status", value(&relation, "status")?), - ("row_count", relation.getattr("row_count")?), - ("byte_count", relation.getattr("byte_count")?), + ("row_count", native_field(&relation, "row_count")?), + ("byte_count", native_field(&relation, "byte_count")?), ], )?); } - manifest_path = capture.getattr("manifest_path")?.str()?.into_any(); + manifest_path = native_field(&capture, "manifest_path")?.str()?.into_any(); } with_errors( result, vec![ - ("name", result.getattr("scenario_name")?), + ("name", native_field(result, "scenario_name")?), ("status", value(result, "status")?), - ("retained", result.getattr("retained")?), + ("retained", native_field(result, "retained")?), ("relations", tuple_of(py, relations)?), ("manifest_path", manifest_path), ], 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 074c9f7beb..e27d6af154 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/result_output/documents.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/result_output/documents.rs @@ -9,6 +9,7 @@ use crate::bindings::_helpers::compile_outputs::json_report::indented_json; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::plan_output::values::{any, dict, items, text}; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::result_output::aggregation::aggregated_from_lists; use crate::bindings::_helpers::result_output::assets::{ clone_asset, function_assets, load_assets, model_assets, python_node_assets, seed_assets, @@ -93,7 +94,7 @@ fn emitted<'py>( let py: Python<'py> = document.summary.py(); let mut status: String = document.status; if let Some(terminal) = projection.lifecycle_terminal()? - && text(&terminal.getattr("event_type")?)?.ends_with(FAILED) + && text(&native_field(&terminal, "event_type")?)?.ends_with(FAILED) { status = FAILED.to_owned(); } @@ -129,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 result.getattr(field)?.getattr("value")?.eq(status)? { + if native_field(&result.getattr(field)?, "value")?.eq(status)? { count += 1; } } @@ -301,9 +302,9 @@ fn build_json<'py>( let check_pass: usize = count_truthy(&checks_run, "passed")?; let mut python_warnings: usize = 0; for warned in nodes.iter().chain(&checks_run).chain(&retained[3]) { - python_warnings += warned.getattr("warning_messages")?.len()?; + python_warnings += native_field(warned, "warning_messages")?.len()?; } - let result_status: String = text(&result.getattr("status")?.getattr("value")?)?; + let result_status: String = text(&native_field(&result, "status")?.getattr("value")?)?; let status: String = if result_status == FAILED || python_fail > 0 || check_fail > 0 { FAILED.to_owned() } else { @@ -320,7 +321,7 @@ fn build_json<'py>( for model in &retained[0] { checks.extend(audit_checks( &projection, - &model.getattr("audit_results")?, + &native_field(model, "audit_results")?, projection.terminal_recorded(model)?, )?); } @@ -450,13 +451,13 @@ fn format_clone_execution_json<'py>( let projection: Projection<'py> = Projection::installed(py)?; let mut assets: Vec> = Vec::new(); let (mut failure_count, mut warning_count): (usize, usize) = (0, 0); - for item in items(&result.getattr("item_results")?)? { - let name: String = text(&item.getattr("name")?)?; + for item in items(&native_field(&result, "item_results")?)? { + let name: String = text(&native_field(&item, "name")?)?; let resource_type: String = text(&resource_types_by_name.get_item(&name)?)?; if !projection.resource_terminal_recorded(&name, &format!("{resource_type}:{name}"))? { continue; } - let status: Bound<'py, PyAny> = item.getattr("status")?.getattr("value")?; + let status: Bound<'py, PyAny> = native_field(&item, "status")?.getattr("value")?; failure_count += usize::from(status.eq(FAILED)?); warning_count += usize::from(status.eq("warning")?); assets.push(clone_asset(&projection, &item, &resource_type)?); @@ -590,13 +591,13 @@ fn namespace_execution<'py>( fn scenario_failed(result: &Bound<'_, PyAny>, local: bool) -> PyResult { if !local { - return result.getattr("status")?.getattr("value")?.eq(FAILED); + return native_field(result, "status")?.getattr("value")?.eq(FAILED); } - let local_status: Bound<'_, PyAny> = result.getattr("local_status")?; + let local_status: Bound<'_, PyAny> = native_field(result, "local_status")?; if local_status.is_none() { return Ok(false); } - let value: Bound<'_, PyAny> = local_status.getattr("value")?; + let value: Bound<'_, PyAny> = native_field(&local_status, "value")?; Ok(value.eq("FAIL")? || value.eq("ERROR")?) } @@ -621,7 +622,7 @@ fn format_scenario_execution_json<'py>( let mut projection: Projection<'py> = Projection::installed(py)?; let mut kept: Vec> = Vec::new(); for result in items(&results)? { - let name: String = text(&result.getattr("scenario_name")?)?; + let name: String = text(&native_field(&result, "scenario_name")?)?; if projection.resource_terminal_recorded(&name, &format!("sql_scenario:{name}"))? { kept.push(result); } @@ -630,20 +631,20 @@ fn format_scenario_execution_json<'py>( let (mut assets, mut checks, mut scenarios) = (Vec::new(), Vec::new(), Vec::new()); for result in &kept { fail_count += usize::from(scenario_failed(result, local)?); - projection.scenario = Some(text(&result.getattr("scenario_name")?)?); + projection.scenario = Some(text(&native_field(result, "scenario_name")?)?); assets.extend(seed_assets( &projection, - &items(&result.getattr("seed_results")?)?, + &items(&native_field(result, "seed_results")?)?, None, )?); assets.extend(function_assets( &projection, - &items(&result.getattr("function_results")?)?, + &items(&native_field(result, "function_results")?)?, None, )?); assets.extend(model_assets( &projection, - &items(&result.getattr("model_results")?)?, + &items(&native_field(result, "model_results")?)?, None, )?); projection.scenario = None; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/result_output/event_writer.rs b/crates/sqlbuild-python/src/bindings/_helpers/result_output/event_writer.rs index 6362c065ae..abb88195cf 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/result_output/event_writer.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/result_output/event_writer.rs @@ -10,6 +10,7 @@ use sqlbuild_core::integration_results::errors::IntegrationRecordError; use sqlbuild_core::terminal_events::models::TerminalRequest; use crate::bindings::_helpers::compile_driver::python_calls::KeywordArguments; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::result_output::integration_records::{ clone_record, degraded_record, degraded_warning, execution_succeeded, integration_record, }; @@ -134,8 +135,7 @@ impl ExecutionEventWriter { let keywords: Bound<'_, PyDict> = PyDict::new(py); keywords.set_item("parents", true)?; keywords.set_item("exist_ok", true)?; - path.getattr("parent")? - .call_method("mkdir", (), Some(&keywords))?; + native_field(&path, "parent")?.call_method("mkdir", (), Some(&keywords))?; let options: Bound<'_, PyDict> = PyDict::new(py); options.set_item("mode", "a")?; options.set_item("encoding", "utf-8")?; @@ -201,7 +201,7 @@ impl ExecutionEventWriter { resource_type: &str, ) -> PyResult<()> { self.serialized(py, |stream| { - let name: String = item.getattr("name")?.extract()?; + let name: String = native_field(&item, "name")?.extract()?; let Some(claim) = self.claim( py, &TerminalRequest { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/result_output/integration_envelope.rs b/crates/sqlbuild-python/src/bindings/_helpers/result_output/integration_envelope.rs index 60fdca0baf..3c0b433103 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/result_output/integration_envelope.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/result_output/integration_envelope.rs @@ -11,6 +11,7 @@ use sqlbuild_core::integration_results::main::normalize_integration_record::norm use sqlbuild_core::integration_results::main::strip_optional_enrichment::strip_optional_enrichment; use sqlbuild_core::json::models::JsonValue; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::result_output::integration_values::{json_value, validation_error}; use crate::bindings::_helpers::scopes::scope_report_objects::json_to_python; @@ -75,7 +76,7 @@ fn parsed(py: Python<'_>, line: &str) -> PyResult = py.import("json")?; let payload: Bound<'_, PyAny> = match json.call_method1("loads", (line,)) { Ok(payload) => payload, - Err(error) if error.is_instance(py, &json.getattr("JSONDecodeError")?) => { + Err(error) if error.is_instance(py, &native_field(&json, "JSONDecodeError")?) => { return Ok(Err(IntegrationRecordError { message: "malformed integration result JSON".to_owned(), })); 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 de5bc7aa13..718ab80d5d 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 @@ -10,6 +10,7 @@ use sqlbuild_core::json::models::{JsonInteger, JsonValue}; use crate::bindings::_helpers::plan_output::safety::{future_cursor_safety, maximum_start_safety}; use crate::bindings::_helpers::plan_output::values::items; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::result_output::checks::{ audit_passed, measurement_evidence, measurement_thresholds, python_check_status, }; @@ -81,7 +82,7 @@ fn entries(values: Vec<(&str, Bound<'_, PyAny>)>) -> Checked { } fn value<'py>(result: &Bound<'py, PyAny>, field: &str) -> PyResult> { - result.getattr(field)?.getattr("value") + native_field(&result.getattr(field)?, "value") } /// `.value` when it is truthy, else `None`. @@ -90,7 +91,7 @@ fn truthy_value<'py>(result: &Bound<'py, PyAny>, field: &str) -> PyResult[name].qualified_name` when the plan locates the resource. @@ -107,12 +108,12 @@ fn planned_target<'py>( if !locations.contains(name)? { return Ok(py.None().into_bound(py)); } - locations.get_item(name)?.getattr("qualified_name") + native_field(&locations.get_item(name)?, "qualified_name") } fn model_microbatch(result: &Bound<'_, PyAny>) -> PyResult { let py: Python<'_> = result.py(); - let run_type: Bound<'_, PyAny> = result.getattr("microbatch_run_type")?; + let run_type: Bound<'_, PyAny> = native_field(result, "microbatch_run_type")?; if run_type.is_none() { return Ok(JsonValue::Object(Vec::new())); } @@ -124,13 +125,13 @@ fn model_microbatch(result: &Bound<'_, PyAny>) -> PyResult { pyo3::types::PyDictMethods::set_item(&values, key, item)?; } } - let count: Bound<'_, PyAny> = result.getattr("microbatch_limit_count")?; + let count: Bound<'_, PyAny> = native_field(result, "microbatch_limit_count")?; if !count.is_none() { pyo3::types::PyDictMethods::set_item(&values, "count", count)?; } - let action: Bound<'_, PyAny> = result.getattr("microbatch_limit_action")?; + let action: Bound<'_, PyAny> = native_field(result, "microbatch_limit_action")?; if !action.is_none() { - pyo3::types::PyDictMethods::set_item(&values, "action", action.getattr("value")?)?; + pyo3::types::PyDictMethods::set_item(&values, "action", native_field(&action, "value")?)?; } bounded_metadata(values.as_any()) } @@ -141,7 +142,7 @@ fn asset<'py>(result: &Bound<'py, PyAny>, plan: &Bound<'py, PyAny>) -> Checked)> = match result_kind(result)? { ResultKind::Model => { - let name: Bound<'py, PyAny> = result.getattr("model_name")?; + let name: Bound<'py, PyAny> = native_field(result, "model_name")?; let planned: Bound<'py, PyAny> = planned_target(plan, "model_locations", &name)?; let mut values: Vec<(&str, Bound<'py, PyAny>)> = vec![ ("kind", "model".into_pyobject(py)?.into_any()), @@ -152,10 +153,13 @@ fn asset<'py>(result: &Bound<'py, PyAny>, plan: &Bound<'py, PyAny>) -> Checked(result: &Bound<'py, PyAny>, plan: &Bound<'py, PyAny>) -> Checked = if evidence.is_none() { @@ -183,7 +187,7 @@ fn asset<'py>(result: &Bound<'py, PyAny>, plan: &Bound<'py, PyAny>) -> Checked { - let name: Bound<'py, PyAny> = result.getattr("seed_name")?; + let name: Bound<'py, PyAny> = native_field(result, "seed_name")?; vec![ ("kind", "seed".into_pyobject(py)?.into_any()), ("target", planned_target(plan, "seed_locations", &name)?), @@ -192,9 +196,9 @@ fn asset<'py>(result: &Bound<'py, PyAny>, plan: &Bound<'py, PyAny>) -> Checked { - let name: Bound<'py, PyAny> = result.getattr("function_name")?; + let name: Bound<'py, PyAny> = native_field(result, "function_name")?; vec![ - ("kind", result.getattr("function_kind")?), + ("kind", native_field(result, "function_kind")?), ("target", planned_target(plan, "function_locations", &name)?), ("name", name), ("status", value(result, "status")?), @@ -202,18 +206,21 @@ fn asset<'py>(result: &Bound<'py, PyAny>, plan: &Bound<'py, PyAny>) -> Checked vec![ ("kind", value(result, "resource_kind")?), - ("name", result.getattr("source_name")?), + ("name", native_field(result, "source_name")?), ("status", value(result, "status")?), - ("target", result.getattr("target")?), - ("staging_relation", result.getattr("staging_relation")?), - ("loader", result.getattr("loader_name")?), - ("rows_loaded", result.getattr("rows_loaded")?), + ("target", native_field(result, "target")?), + ( + "staging_relation", + native_field(result, "staging_relation")?, + ), + ("loader", native_field(result, "loader_name")?), + ("rows_loaded", native_field(result, "rows_loaded")?), ], ResultKind::PythonNode => vec![ ("kind", value(result, "kind")?), - ("name", result.getattr("node_name")?), + ("name", native_field(result, "node_name")?), ("status", value(result, "status")?), - ("materialized", result.getattr("materialized")?), + ("materialized", native_field(result, "materialized")?), ], _ => { let _ = none; @@ -229,7 +236,7 @@ fn audit_check<'py>(result: &Bound<'py, PyAny>) -> Checked { let (evidence_count, evidence_truncated) = measurement_evidence(result)?; entries(vec![ ("kind", "audit".into_pyobject(py)?.into_any()), - ("name", result.getattr("audit_name")?), + ("name", native_field(result, "audit_name")?), ("check_id", check_id.clone()), ("dag_check_id", check_id), ( @@ -238,35 +245,38 @@ fn audit_check<'py>(result: &Bound<'py, PyAny>) -> Checked { ), ("status", value(result, "outcome")?), ("severity", value(result, "severity")?), - ("row_count", result.getattr("row_count")?), + ("row_count", native_field(result, "row_count")?), ("attachment_kind", value(result, "attachment_kind")?), - ("asset_name", result.getattr("attached_target_name")?), + ("asset_name", native_field(result, "attached_target_name")?), ( "attached_column_name", - result.getattr("attached_column_name")?, + native_field(result, "attached_column_name")?, ), ( "attached_target_name", - result.getattr("attached_target_name")?, + native_field(result, "attached_target_name")?, ), ("run_scope_phase", value(result, "run_scope_phase")?), - ("reused", result.getattr("reused")?), + ("reused", native_field(result, "reused")?), ("evaluation_mode", value(result, "evaluation_mode")?), - ("measured_value", result.getattr("measured_value")?), - ("sample_count", result.getattr("sample_count")?), - ("sample_unit", result.getattr("sample_unit")?), - ("minimum_samples", result.getattr("minimum_samples")?), + ("measured_value", native_field(result, "measured_value")?), + ("sample_count", native_field(result, "sample_count")?), + ("sample_unit", native_field(result, "sample_unit")?), + ("minimum_samples", native_field(result, "minimum_samples")?), ( "thresholds", - measurement_thresholds(&result.getattr("thresholds")?)?, + measurement_thresholds(&native_field(result, "thresholds")?)?, ), ("evidence_count", evidence_count), ("evidence_truncated", evidence_truncated), ( "audit_definition_name", - result.getattr("audit_definition_name")?, + native_field(result, "audit_definition_name")?, + ), + ( + "audit_description", + native_field(result, "audit_description")?, ), - ("audit_description", result.getattr("audit_description")?), ]) } @@ -281,11 +291,11 @@ fn checks<'py>(result: &Bound<'py, PyAny>) -> Checked> { ResultKind::SqlTest => { let check_id: Bound<'py, PyAny> = sql_test_check_id(result)?.into_pyobject(py)?.into_any(); - let steps: Vec> = items(&result.getattr("step_results")?)?; + let steps: Vec> = items(&native_field(result, "step_results")?)?; let outcome: Bound<'py, PyAny> = value(result, "outcome")?; single(entries(vec![ ("kind", "sql_test".into_pyobject(py)?.into_any()), - ("name", result.getattr("test_name")?), + ("name", native_field(result, "test_name")?), ("check_id", check_id.clone()), ("dag_check_id", check_id), ( @@ -298,21 +308,21 @@ fn checks<'py>(result: &Bound<'py, PyAny>) -> Checked> { ( "asset_name", match steps.first() { - Some(step) => step.getattr("model_name")?, + Some(step) => native_field(step, "model_name")?, None => py.None().into_bound(py), }, ), ])) } ResultKind::PythonCheck => { - let name: String = result.getattr("node_name")?.extract()?; + let name: String = native_field(result, "node_name")?.extract()?; let check_id: Bound<'py, PyAny> = format!("check:{name}").into_pyobject(py)?.into_any(); single(entries(vec![ ("kind", "python_check".into_pyobject(py)?.into_any()), - ("name", result.getattr("node_name")?), + ("name", native_field(result, "node_name")?), ("check_id", check_id.clone()), ("dag_check_id", check_id), - ("passed", result.getattr("passed")?), + ("passed", native_field(result, "passed")?), ( "status", python_check_status(result)?.into_pyobject(py)?.into_any(), @@ -343,7 +353,7 @@ fn duration(value: &Bound<'_, PyAny>) -> PyResult { /// The envelope fields every record copies from its terminal, in record order. fn terminal_fields(terminal: &Bound<'_, PyAny>, event_sequence: usize) -> PyResult { - let payload: Bound<'_, PyAny> = terminal.getattr("payload")?; + 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)) @@ -362,7 +372,7 @@ fn terminal_fields(terminal: &Bound<'_, PyAny>, event_sequence: usize) -> PyResu ("event_type".to_owned(), attribute("event_type")?), ( "occurred_at".to_owned(), - optional_text(&terminal.getattr("occurred_at")?.call_method0("isoformat")?)?, + optional_text(&native_field(terminal, "occurred_at")?.call_method0("isoformat")?)?, ), ("invocation_id".to_owned(), attribute("invocation_id")?), ]; @@ -388,7 +398,7 @@ fn terminal_fields(terminal: &Bound<'_, PyAny>, event_sequence: usize) -> PyResu } fn failure_fields(terminal: &Bound<'_, PyAny>) -> PyResult { - let payload: Bound<'_, PyAny> = terminal.getattr("payload")?; + let payload: Bound<'_, PyAny> = native_field(terminal, "payload")?; let mut fields: Entries = Vec::new(); for key in ["error_code", "error_type", "skip_code", "skip_mode"] { fields.push(( @@ -410,7 +420,7 @@ fn canonical_status(event_type: &str, fallback: JsonValue) -> JsonValue { fn payload_text(terminal: &Bound<'_, PyAny>, key: &str) -> PyResult { let value: JsonValue = - optional_text(&terminal.getattr("payload")?.call_method1("get", (key,))?)?; + optional_text(&native_field(terminal, "payload")?.call_method1("get", (key,))?)?; Ok(match value { JsonValue::Null => text(""), other => other, @@ -446,7 +456,7 @@ fn encoded( } fn event_type(terminal: &Bound<'_, PyAny>) -> PyResult { - terminal.getattr("event_type")?.extract() + native_field(terminal, "event_type")?.extract() } /// `build_integration_result`, encoded; `None` when the result carries no enrichment. @@ -492,8 +502,8 @@ pub(crate) fn clone_record( let event: String = event_type(terminal)?; let values: Entries = match entries(vec![ ("action", value(item, "action")?), - ("target", item.getattr("destination_relation")?), - ("origin_relation", item.getattr("origin_relation")?), + ("target", native_field(item, "destination_relation")?), + ("origin_relation", native_field(item, "origin_relation")?), ])? { Ok(values) => values, Err(error) => return Ok(Err(error)), @@ -526,7 +536,7 @@ pub(crate) fn clone_record( /// `str(payload.get(key) or "")`, as the clone asset's identity. fn payload_text_or_str(terminal: &Bound<'_, PyAny>, key: &str) -> PyResult { - text_or_empty(&terminal.getattr("payload")?.call_method1("get", (key,))?) + text_or_empty(&native_field(terminal, "payload")?.call_method1("get", (key,))?) } /// `build_degraded_integration_result`, encoded; `None` for results without enrichment. @@ -561,7 +571,7 @@ pub(crate) fn degraded_record( } ResultKind::Audit | ResultKind::SqlTest | ResultKind::PythonCheck => { let failed: bool = event == FAILED_EVENT; - let check_id: JsonValue = text_or_empty(&terminal.getattr("resource_id")?)?; + let check_id: JsonValue = text_or_empty(&native_field(terminal, "resource_id")?)?; ( None, vec![vec![ 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 a2fdde3fb4..80dbb1d709 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/result_output/projection.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/result_output/projection.rs @@ -6,6 +6,7 @@ use pyo3::types::{PyFloat, PyInt}; use crate::bindings::_helpers::native_host::executor::ExecutorHost; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::plan_output::values::text; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::result_output::resource_ids::{audit_identity, sql_test_identity}; use crate::bindings::_helpers::result_output::terminal_index::{TerminalEventIndex, current_index}; @@ -87,7 +88,7 @@ pub(crate) fn resource_identity(result: &Bound<'_, PyAny>) -> PyResult { let resource: String = name("node_name")?; - let kind: String = text(&result.getattr("kind")?.getattr("value")?)?; + let kind: String = text(&native_field(result, "kind")?.getattr("value")?)?; (resource.clone(), format!("{kind}:{resource}")) } ResultKind::PythonCheck => { 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 7e3c8f4812..21130eb179 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 @@ -5,6 +5,7 @@ use pyo3::types::PyDict; use pyo3::{pyfunction, wrap_pyfunction}; use crate::bindings::_helpers::boundary::keyword_request::Request; +use crate::bindings::_helpers::records::fields::field as native_field; use sqlbuild_core::nested_progress::main::audit_resource_id::audit_resource_id as audit_id; use sqlbuild_core::nested_progress::main::sql_test_resource_id::sql_test_resource_id as sql_test_id; @@ -61,7 +62,7 @@ fn audit_identity_of( let column: Option = column.extract()?; Ok(audit_id( &audit_name.extract::()?, - &kind.getattr("value")?.extract::()?, + &native_field(kind, "value")?.extract::()?, (target.as_deref(), column.as_deref()), )) } @@ -73,26 +74,26 @@ pub(crate) fn sql_test_identity( ) -> PyResult { sql_test_resource_id( &result.getattr(test_name_field)?.extract::()?, - result.getattr("source_path")?, - result.getattr("block_index")?.extract()?, - result.getattr("case_name")?.extract()?, + native_field(result, "source_path")?, + native_field(result, "block_index")?.extract()?, + native_field(result, "case_name")?.extract()?, ) } /// `audit_resource_id` of one audit result. pub(crate) fn audit_identity(result: &Bound<'_, PyAny>) -> PyResult { - let target_kind: Bound<'_, PyAny> = result.getattr("attached_target_kind")?; + let target_kind: Bound<'_, PyAny> = native_field(result, "attached_target_kind")?; let kind: Bound<'_, PyAny> = if target_kind.is_none() { - result.getattr("attachment_kind")? + native_field(result, "attachment_kind")? } else { target_kind }; audit_identity_of( - &result.getattr("audit_name")?, + &native_field(result, "audit_name")?, &kind, ( - &result.getattr("attached_target_name")?, - &result.getattr("attached_column_name")?, + &native_field(result, "attached_target_name")?, + &native_field(result, "attached_column_name")?, ), ) } 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 78d947b28b..2efa5b5e67 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 @@ -11,6 +11,7 @@ use pyo3::{pyclass, pyfunction, pymethods, wrap_pyfunction}; use sqlbuild_core::terminal_events::main::claim_resource_terminal::claim_resource_terminal; 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; use sqlbuild_core::terminal_events::main::consume_terminal_fact::consume_terminal_fact; @@ -74,10 +75,10 @@ fn optional_text(event: &Bound<'_, PyAny>, name: &str) -> PyResult) -> PyResult { - let payload: Bound<'_, PyAny> = event.getattr("payload")?; + let payload: Bound<'_, PyAny> = native_field(event, "payload")?; Ok(TerminalFact { - event_id: event.getattr("event_id")?.extract()?, - event_type: event.getattr("event_type")?.extract()?, + event_id: native_field(event, "event_id")?.extract()?, + event_type: native_field(event, "event_type")?.extract()?, resource_id: optional_text(event, "resource_id")?, resource_name: payload_text(&payload, "resource_name")?, resource_attempt_id: optional_text(event, "resource_attempt_id")?, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/catalogue.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/catalogue.rs index 5452d97113..7eeb1913e9 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/catalogue.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/catalogue.rs @@ -1,6 +1,6 @@ //! The validated rule catalogue, the active rules it selects and resolved ignore selectors. -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::path::{Path, PathBuf}; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; @@ -16,10 +16,13 @@ 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::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; +use crate::bindings::_helpers::rules::fact_replay::{project_attribute, project_models}; use crate::bindings::_helpers::rules::selection::{rules_error, selected_rule_codes}; const DATACLASSES_MODULE: &str = "dataclasses"; @@ -32,6 +35,11 @@ const BUILTIN_CHECK_MESSAGE: &str = "built-in rules execute only through the nat /// The Python rules configuration as the native configuration model. pub(crate) fn native_rules_config(config: &Bound<'_, PyAny>) -> PyResult { let py: Python<'_> = config.py(); + if let Some(fields) = asdict_json(config)? { + return serde_json::from_value(fields).map_err(|error| { + rules_error(py, &format!("invalid rules configuration: {error}"), None) + }); + } let fields: Bound<'_, PyAny> = imported(py, DATACLASSES_MODULE, "asdict")?.call1((config,))?; let encoded: String = imported(py, JSON_MODULE, "dumps")? .call( @@ -50,15 +58,15 @@ pub(crate) fn native_rules_config(config: &Bound<'_, PyAny>) -> PyResult) -> PyResult { let mut selectors: Vec = Vec::new(); for field in ["select", "ignore", "rule_options"] { - for selector in config.getattr(field)?.try_iter()? { + for selector in record_field(config, field)?.try_iter()? { selectors.push(selector?.extract()?); } } - for entry in config.getattr("rule_exceptions")?.try_iter()? { - selectors.push(entry?.getattr("rule")?.extract()?); + for entry in record_field(config, "rule_exceptions")?.try_iter()? { + selectors.push(record_field(&entry?, "rule")?.extract()?); } - for entry in config.getattr("rule_ignores")?.try_iter()? { - for rule in entry?.getattr("rules")?.try_iter()? { + for entry in record_field(config, "rule_ignores")?.try_iter()? { + for rule in record_field(&entry?, "rules")?.try_iter()? { selectors.push(rule?.extract()?); } } @@ -113,18 +121,17 @@ fn option_value(value: &Bound<'_, PyAny>) -> PyResult { } fn optional_integer(option: &Bound<'_, PyAny>, field: &str) -> PyResult> { - option.getattr(field)?.extract() + record_field(option, field)?.extract() } fn option_spec(option: &Bound<'_, PyAny>) -> PyResult { - let item_type: Bound<'_, PyAny> = option.getattr("item_type")?; + let item_type: Bound<'_, PyAny> = record_field(option, "item_type")?; Ok(OptionSpec { - name: option.getattr("name")?.extract()?, - description: option.getattr("description")?.extract()?, - value_type: option_kind(&option.getattr("value_type")?)?, - default: option_value(&option.getattr("default")?)?, - choices: option - .getattr("choices")? + name: record_field(option, "name")?.extract()?, + description: record_field(option, "description")?.extract()?, + value_type: option_kind(&record_field(option, "value_type")?)?, + default: option_value(&record_field(option, "default")?)?, + choices: record_field(option, "choices")? .try_iter()? .map(|choice| option_value(&choice?)) .collect::>()?, @@ -141,9 +148,8 @@ fn option_spec(option: &Bound<'_, PyAny>) -> PyResult { fn catalogue_rule(rule: &Bound<'_, PyAny>) -> PyResult { Ok(CatalogueRule { - code: rule.getattr("code")?.extract()?, - options: rule - .getattr("options")? + code: record_field(rule, "code")?.extract()?, + options: record_field(rule, "options")? .try_iter()? .map(|option| option_spec(&option?)) .collect::>()?, @@ -154,7 +160,7 @@ fn configured_options( config: &Bound<'_, PyAny>, ) -> PyResult>> { let mut configured: BTreeMap> = BTreeMap::new(); - let options: Bound<'_, PyDict> = config.getattr("rule_options")?.cast_into::()?; + let options: Bound<'_, PyDict> = record_field(config, "rule_options")?.cast_into::()?; for (code, values) in options.iter() { let values: Bound<'_, PyDict> = values.cast_into::()?; let mut entries: BTreeMap = BTreeMap::new(); @@ -243,7 +249,7 @@ pub(crate) fn select_rules<'py>( }; let mut by_code: BTreeMap> = BTreeMap::new(); for rule in catalogue { - by_code.insert(rule.getattr("code")?.extract()?, rule.clone()); + by_code.insert(record_field(rule, "code")?.extract()?, rule.clone()); } selected_rule_codes(config, &payloads)? .into_iter() @@ -299,49 +305,61 @@ fn select_catalogue_rules<'py>( ) } -fn resource_paths<'py>(project: &Bound<'py, PyAny>) -> PyResult> { - let paths: Bound<'py, PyDict> = PyDict::new(project.py()); +/// Each model, function, seed and source key's relative path as POSIX text. +fn resource_paths(project: &Bound<'_, PyAny>) -> PyResult> { + let mut paths: HashMap<(String, String), String> = HashMap::new(); + for model in project_models(project)? { + paths.insert(model.key, model.posix); + } for (collection, file) in [ - ("models", None), ("functions", None), ("seeds", Some("seed_file")), ("sources", Some("source_file")), ] { - for resource in project.getattr(collection)?.try_iter()? { - let resource: Bound<'py, PyAny> = resource?; - let owner: Bound<'py, PyAny> = match file { - Some(file) => resource.getattr(file)?, + for resource in project_attribute(project, collection)?.try_iter()? { + let resource: Bound<'_, PyAny> = resource?; + let owner: Bound<'_, PyAny> = match file { + Some(file) => record_field(&resource, file)?, None => resource.clone(), }; - paths.set_item(resource.getattr("key")?, owner.getattr("relative_path")?)?; + paths.insert( + object_key(&record_field(&resource, "key")?)?, + path_posix(&record_field(&owner, "relative_path")?)?, + ); } } Ok(paths) } +/// `(resource type, name)` of a `CompiledObjectKey`. +fn object_key(key: &Bound<'_, PyAny>) -> PyResult<(String, String)> { + Ok(( + record_field(key, "resource_type")?.str()?.extract()?, + record_field(key, "name")?.extract()?, + )) +} + /// `config` with every selector-scoped rule ignore resolved into deterministic finding paths. pub(crate) fn resolved_ignore_selectors<'py>( config: &Bound<'py, PyAny>, project: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = config.py(); - let ignores: Vec> = config - .getattr("rule_ignores")? + let ignores: Vec> = record_field(config, "rule_ignores")? .try_iter()? .collect::>()?; let mut scoped: bool = false; for ignore in &ignores { - scoped |= ignore.getattr("selectors")?.is_truthy()?; + scoped |= record_field(ignore, "selectors")?.is_truthy()?; } if !scoped { return Ok(config.clone()); } - let replace: Bound<'py, PyAny> = imported(py, DATACLASSES_MODULE, "replace")?; let scope: Bound<'py, PyAny> = registry::native(py, NativeAttr::BuildPlannerScope)?; - let path_by_key: Bound<'py, PyDict> = resource_paths(project)?; + let path_by_key: HashMap<(String, String), String> = resource_paths(project)?; let mut resolved: Vec> = Vec::with_capacity(ignores.len()); for ignore in ignores { - let selectors: Bound<'py, PyAny> = ignore.getattr("selectors")?; + let selectors: Bound<'py, PyAny> = record_field(&ignore, "selectors")?; if !selectors.is_truthy()? { resolved.push(ignore); continue; @@ -351,9 +369,9 @@ pub(crate) fn resolved_ignore_selectors<'py>( &[("project", project.clone()), ("select", selectors.clone())], )?; let mut selector_paths: BTreeSet = BTreeSet::new(); - for key in selected.getattr("selected_keys")?.try_iter()? { - if let Some(path) = path_by_key.get_item(key?)? { - selector_paths.insert(path.call_method0("as_posix")?.extract()?); + for key in record_field(&selected, "selected_keys")?.try_iter()? { + if let Some(path) = path_by_key.get(&object_key(&key?)?) { + selector_paths.insert(path.clone()); } } if selector_paths.is_empty() { @@ -367,26 +385,20 @@ pub(crate) fn resolved_ignore_selectors<'py>( None, )); } - for path in ignore.getattr("paths")?.try_iter()? { + for path in record_field(&ignore, "paths")?.try_iter()? { selector_paths.insert(path?.extract()?); } - resolved.push(replace.call( - (ignore,), - Some(&keywords( - py, - &[ - ("paths", PyTuple::new(py, selector_paths)?.into_any()), - ("selectors", PyTuple::empty(py).into_any()), - ], - )?), + resolved.push(replaced( + &ignore, + &[ + ("paths", PyTuple::new(py, selector_paths)?.into_any()), + ("selectors", PyTuple::empty(py).into_any()), + ], )?); } - replace.call( - (config,), - Some(&keywords( - py, - &[("rule_ignores", PyTuple::new(py, resolved)?.into_any())], - )?), + replaced( + config, + &[("rule_ignores", PyTuple::new(py, resolved)?.into_any())], ) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/config_json.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/config_json.rs new file mode 100644 index 0000000000..00912aeee4 --- /dev/null +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/config_json.rs @@ -0,0 +1,85 @@ +//! Native records and plain values as the JSON `json.dumps(asdict(value), default=str)` writes. + +use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; +use pyo3::types::{ + PyBool, PyBoolMethods, PyDict, PyDictMethods, PyFloat, PyInt, PyList, PyString, + PyStringMethods, PyTuple, +}; +use serde_json::{Map, Number, Value}; + +use crate::bindings::_helpers::records::models::NativeRecord; + +const DATACLASS_FIELDS: &str = "__dataclass_fields__"; + +/// `json.loads(json.dumps(asdict(value), default=str))`, or `None` when only Python spells it. +pub(crate) fn asdict_json(value: &Bound<'_, PyAny>) -> PyResult> { + json_value(value, true) +} + +/// `value` as `json.loads(json.dumps(value, default=str))` reads it back, or `None` as above. +pub(crate) fn dumped_json(value: &Bound<'_, PyAny>) -> PyResult> { + json_value(value, false) +} + +/// `None` for non-finite or wide numbers, non-text keys and Python dataclasses. +fn json_value(value: &Bound<'_, PyAny>, records: bool) -> PyResult> { + if value.is_none() { + return Ok(Some(Value::Null)); + } + if let Ok(flag) = value.downcast::() { + return Ok(Some(Value::Bool(flag.is_true()))); + } + if value.is_instance_of::() { + if let Ok(number) = value.extract::() { + return Ok(Some(Value::from(number))); + } + if let Ok(number) = value.extract::() { + return Ok(Some(Value::from(number))); + } + return Ok(None); + } + if value.is_instance_of::() { + return Ok(Number::from_f64(value.extract::()?).map(Value::Number)); + } + if let Ok(text) = value.downcast::() { + return Ok(Some(Value::String(text.to_str()?.to_owned()))); + } + if value.is_instance_of::() || value.is_instance_of::() { + let mut items: Vec = Vec::new(); + for item in value.try_iter()? { + let Some(item) = json_value(&item?, records)? else { + return Ok(None); + }; + items.push(item); + } + return Ok(Some(Value::Array(items))); + } + if let Ok(mapping) = value.downcast::() { + let mut entries: Map = Map::new(); + for (key, item) in mapping.iter() { + let Ok(key) = key.downcast::() else { + return Ok(None); + }; + let Some(item) = json_value(&item, records)? else { + return Ok(None); + }; + entries.insert(key.to_str()?.to_owned(), item); + } + return Ok(Some(Value::Object(entries))); + } + if records && let Ok(record) = value.cast::() { + let record: &NativeRecord = record.get(); + let mut entries: Map = Map::new(); + for (field, item) in record.spec.fields.iter().zip(&record.values) { + let Some(item) = json_value(item.bind(value.py()), records)? else { + return Ok(None); + }; + entries.insert(field.name.to_owned(), item); + } + return Ok(Some(Value::Object(entries))); + } + if records && value.hasattr(DATACLASS_FIELDS)? { + return Ok(None); + } + Ok(Some(Value::String(value.str()?.to_str()?.to_owned()))) +} 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 d692334b08..41e812d345 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/custom_evaluation.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/custom_evaluation.rs @@ -17,6 +17,7 @@ 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::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::rules::constants::{ @@ -76,7 +77,12 @@ pub(crate) fn evaluate_custom_rules_cached<'py>( let mut custom: Vec<(String, Bound<'py, PyAny>)> = run .rules .iter() - .map(|rule| Ok((rule.getattr("code")?.extract::()?, rule.clone()))) + .map(|rule| { + Ok(( + native_field(rule, "code")?.extract::()?, + rule.clone(), + )) + }) .collect::>()?; custom.sort_by(|left, right| left.0.cmp(&right.0)); if custom.is_empty() { @@ -125,7 +131,7 @@ pub(crate) fn evaluate_custom_rules_cached<'py>( let mut cache: CustomRuleCache = CustomRuleCache::new( cache_path, rows, - run.project.getattr("effective_target_name")?.extract()?, + native_field(run.project, "effective_target_name")?.extract()?, models, ); let pending: Vec = cache.pending_readsets().to_vec(); @@ -322,11 +328,11 @@ fn host_project<'py>(project: &Bound<'py, PyAny>) -> PyResult> } fn subject_value(rule: &Bound<'_, PyAny>) -> PyResult> { - let subject: Bound<'_, PyAny> = rule.getattr("subject")?; + let subject: Bound<'_, PyAny> = native_field(rule, "subject")?; if subject.is_none() { return Ok(None); } - subject.getattr("value")?.extract() + native_field(&subject, "value")?.extract() } fn host_subject(rule: &Bound<'_, PyAny>) -> PyResult { @@ -375,11 +381,11 @@ fn rule_identity( "runtime_version", pyo3::types::PyString::new(py, CUSTOM_HOST_RUNTIME_VERSION).into_any(), ), - ("code", rule.getattr("code")?), - ("family", rule.getattr("family")?), - ("slug", rule.getattr("slug")?), - ("message", rule.getattr("message")?), - ("remediation", rule.getattr("remediation")?), + ("code", native_field(rule, "code")?), + ("family", native_field(rule, "family")?), + ("slug", native_field(rule, "slug")?), + ("message", native_field(rule, "message")?), + ("remediation", native_field(rule, "remediation")?), ("subject", subject.into_bound_py_any(py)?), ("implementation_fingerprint", fingerprint), ("config", run.config.clone()), @@ -397,8 +403,8 @@ fn rule_identity( fn cpu_seconds(py: Python<'_>) -> PyResult { let thread: f64 = imported(py, "time", "thread_time")?.call0()?.extract()?; let times: Bound<'_, PyAny> = imported(py, "os", "times")?.call0()?; - let user: f64 = times.getattr("children_user")?.extract()?; - let system: f64 = times.getattr("children_system")?.extract()?; + let user: f64 = native_field(×, "children_user")?.extract()?; + let system: f64 = native_field(×, "children_system")?.extract()?; Ok(thread + user + system) } 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 0018608488..832fc8e2da 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/custom_hosts.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/custom_hosts.rs @@ -27,6 +27,7 @@ use sqlbuild_rules::engine::main::custom_host::run_custom_host_json; use crate::bindings::_helpers::compile_driver::python_calls::imported; use crate::bindings::_helpers::native_host::constants::CUSTOM_HOST_LAUNCH_MODULE; +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::rules::constants::{ @@ -412,7 +413,7 @@ pub(crate) fn host_payload_project<'py>( 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 project.getattr("models")?.try_iter()? { + 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 { 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 75cbfb4da9..d3dd9b1398 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 @@ -17,6 +17,7 @@ use sqlbuild_pysource::rule_evidence::models::{ }; use crate::bindings::_helpers::compile_driver::python_calls::imported; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::ErrorId; use crate::bindings::_helpers::records::registry; @@ -34,7 +35,7 @@ fn python_version(py: Python<'_>) -> (u8, u8) { /// `Path(rule.source).resolve()` as a Python path, or `None` for a rule without a source file. fn resolved_source<'py>(rule: &Bound<'py, PyAny>) -> PyResult>> { - let source: Bound<'py, PyAny> = rule.getattr("source")?; + let source: Bound<'py, PyAny> = native_field(rule, "source")?; if source.is_none() { return Ok(None); } @@ -83,7 +84,7 @@ pub(crate) fn custom_rule_implementation_fingerprint( let py: Python<'_> = rule.py(); let Some(source) = resolved_source(rule)? else { let text: Bound<'_, PyAny> = - imported(py, INSPECT_MODULE, "getsource")?.call1((rule.getattr("check")?,))?; + imported(py, INSPECT_MODULE, "getsource")?.call1((native_field(rule, "check")?,))?; return imported(py, HASHLIB_MODULE, "sha256")? .call1((text.call_method0("encode")?,))? .call_method0("hexdigest")? @@ -113,7 +114,7 @@ fn custom_rule_test_evidence<'py>( return Ok(Vec::new()); }; let root_path: Bound<'py, PyAny> = project_dir.call_method0("resolve")?; - let check_name: String = match rule.getattr("check")?.getattr("__name__") { + let check_name: String = match native_field(rule, "check")?.getattr("__name__") { Ok(name) => name.extract()?, Err(_) => String::new(), }; @@ -207,7 +208,7 @@ pub(crate) fn verify_custom_rules( let checked: Bound<'_, PySet> = PySet::empty(py)?; for rule in rules.try_iter()? { let rule: Bound<'_, PyAny> = rule?; - if !rule.getattr("custom")?.is_truthy()? { + if !native_field(&rule, "custom")?.is_truthy()? { continue; } let Some(source) = resolved_source(&rule)? else { 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 674fc5886f..091d27f073 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/dsl_headers.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/dsl_headers.rs @@ -13,6 +13,7 @@ use sqlbuild_rules::dsl_headers::models::{HeaderRulesConfig, HeaderSpan, HeaderV use crate::bindings::_helpers::boundary::panics::value_error; 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::RecordId; use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::rules::lint_pipeline::{LintViolationRow, ViolationClasses}; @@ -39,8 +40,8 @@ fn text(contents: &Bound<'_, PyAny>) -> PyResult> { pub(crate) fn rules_config(config: &Bound<'_, PyAny>) -> PyResult { Ok(HeaderRulesConfig { - line_width: config.getattr("line_width")?.extract()?, - max_description_lines: config.getattr("max_description_lines")?.extract()?, + line_width: native_field(config, "line_width")?.extract()?, + max_description_lines: native_field(config, "max_description_lines")?.extract()?, }) } @@ -50,9 +51,9 @@ fn spans(headers: &Bound<'_, PyAny>) -> PyResult> { .map(|header| { let header: Bound<'_, PyAny> = header?; Ok(HeaderSpan { - kind: header.getattr("kind")?.extract()?, - start: header.getattr("start")?.extract()?, - end: header.getattr("end")?.extract()?, + kind: native_field(&header, "kind")?.extract()?, + start: native_field(&header, "start")?.extract()?, + end: native_field(&header, "end")?.extract()?, }) }) .collect() diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/early_sql_lint.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/early_sql_lint.rs index b43a2d45c9..89bfd5a6f3 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/early_sql_lint.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/early_sql_lint.rs @@ -25,6 +25,7 @@ use crate::bindings::_helpers::compile_attachment::sql_expansion_context::{ SqlExpansionInputs, present, }; use crate::bindings::_helpers::native_threads::interruptible_join::{Joined, join_interruptibly}; +use crate::bindings::_helpers::records::fields::field as record_field; use crate::bindings::_helpers::rules::lint_expansion::{ LintExpansionSources, lint_expansion_inputs, }; @@ -86,15 +87,14 @@ struct EarlyLintPlan { /// The plan early SQL lint runs with, when the project selects SQL rules without a warm cache. fn early_lint_plan(inputs: &Bound<'_, PyAny>, dialect: &str) -> PyResult> { let py: Python<'_> = inputs.py(); - let project_dir: Bound<'_, PyAny> = inputs - .getattr("discovered_inputs")? - .getattr("project_dir")?; + let project_dir: Bound<'_, PyAny> = + record_field(&record_field(inputs, "discovered_inputs")?, "project_dir")?; if project_dir.is_none() { return Ok(None); } let project_dir: PathBuf = project_dir.extract()?; - for diagnostic in inputs.getattr("diagnostics")?.try_iter()? { - if diagnostic?.getattr("is_error")?.is_truthy()? { + for diagnostic in record_field(inputs, "diagnostics")?.try_iter()? { + if record_field(&diagnostic?, "is_error")?.is_truthy()? { return Ok(None); } } @@ -113,16 +113,16 @@ fn early_lint_plan(inputs: &Bound<'_, PyAny>, dialect: &str) -> PyResult = Vec::new(); - for model in inputs.getattr("model_inputs")?.try_iter()? { + for model in record_field(inputs, "model_inputs")?.try_iter()? { rows.push(model_key_row(&model?)?); } let mut seeds: Vec> = Vec::new(); - for seed in inputs.getattr("seed_inputs")?.try_iter()? { - seeds.push(seed?.getattr("schema_entry")?); + for seed in record_field(inputs, "seed_inputs")?.try_iter()? { + seeds.push(record_field(&seed?, "schema_entry")?); } let mut sources: Vec> = Vec::new(); - for source in inputs.getattr("source_inputs")?.try_iter()? { - sources.push(source?.getattr("source_entry")?); + for source in record_field(inputs, "source_inputs")?.try_iter()? { + sources.push(record_field(&source?, "source_entry")?); } let keys: RelationKeys = relation_keys( py, @@ -212,7 +212,7 @@ fn start_early_lint( return Ok(None); }; let mut compiled: HashMap = HashMap::new(); - for model in inputs.getattr("model_inputs")?.try_iter()? { + for model in record_field(inputs, "model_inputs")?.try_iter()? { if let Some((path, expansion)) = model_lint_expansion(&model?)? { compiled.insert(path, expansion); } @@ -220,8 +220,8 @@ fn start_early_lint( let work: EarlyLintWork = EarlyLintWork { project_dir: plan.project_dir.clone(), settings: plan.settings, - discovered: inputs.getattr("discovered_inputs")?.unbind(), - static_scope: inputs.getattr("declaration_scope")?.unbind(), + discovered: record_field(inputs, "discovered_inputs")?.unbind(), + static_scope: record_field(inputs, "declaration_scope")?.unbind(), compiled, stop: Arc::clone(stop), }; @@ -362,12 +362,12 @@ impl EarlySqlLint { slf.preparation = start_early_lint(inputs, dialect, &stop)? .map(|preparation| Py::new(py, preparation)) .transpose()?; - let declaration_scope: Bound<'_, PyAny> = inputs.getattr("declaration_scope")?; + let declaration_scope: Bound<'_, PyAny> = record_field(inputs, "declaration_scope")?; slf.expansion_reuse = if declaration_scope.is_none() { None } else { Some(ExpansionReuse { - discovered_inputs: inputs.getattr("discovered_inputs")?.unbind(), + discovered_inputs: record_field(inputs, "discovered_inputs")?.unbind(), declaration_scope: declaration_scope.unbind(), }) }; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/fact_encoder.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/fact_encoder.rs index e03d93ca64..ff7e5d1568 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/fact_encoder.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/fact_encoder.rs @@ -19,6 +19,7 @@ use sqlbuild_core::json::main::dumps::dumps; use sqlbuild_core::json::models::{JsonDialect, JsonValue, OrjsonOptions}; use crate::bindings::_helpers::compile_driver::python_calls::imported; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::fields::record_spec; use crate::bindings::_helpers::records::models::NativeRecord; @@ -197,7 +198,7 @@ impl FactEncoder { } let encoded: JsonValue = JsonValue::Array(vec![ JsonValue::String(qualified_name), - self.encode(&value.getattr("value")?)?, + self.encode(&native_field(value, "value")?)?, ]); self.enums .borrow_mut() 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 97919bc7ca..ec38b06a57 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/fact_replay.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/fact_replay.rs @@ -21,6 +21,7 @@ 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::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; @@ -158,7 +159,7 @@ pub(crate) struct ModelEntry<'py> { path_text: String, pub(crate) posix: String, pub(crate) config_values: Option>, - key: (String, String), + pub(crate) key: (String, String), deps: Vec<(String, String)>, } @@ -590,6 +591,20 @@ impl<'py> FactReplay<'py> { } } +/// One project attribute: a native project view's stored field, a record's field, or by `getattr`. +pub(crate) fn project_attribute<'py>( + project: &Bound<'py, PyAny>, + name: &str, +) -> PyResult> { + let py: Python<'py> = project.py(); + if let Ok(view) = project.downcast::() + && let Some(field) = view.get().fact_field(py, name) + { + return Ok(field.into_bound(py)); + } + record_field_or_attribute(project, name) +} + /// One project field, from a native project view or a project record. fn project_field<'py>( encoder: &FactEncoder, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/findings.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/findings.rs index 3f1681e55a..7a459f62a6 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::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, PyErr, PyResult, Python}; @@ -80,16 +81,16 @@ impl<'py> FindingClasses<'py> { /// The native fault row of one Python `Finding`. pub(crate) fn finding_fault(finding: &Bound<'_, PyAny>) -> PyResult { Ok(Fault { - unevaluated: finding.getattr("unevaluated")?.extract()?, - code: finding.getattr("code")?.extract()?, + unevaluated: native_field(finding, "unevaluated")?.extract()?, + code: native_field(finding, "code")?.extract()?, path: finding .getattr("path")? .call_method0("as_posix")? .extract()?, - line: finding.getattr("line")?.extract()?, - column: finding.getattr("column")?.extract()?, - message: finding.getattr("message")?.extract()?, - remediation: finding.getattr("remediation")?.extract()?, + line: native_field(finding, "line")?.extract()?, + column: native_field(finding, "column")?.extract()?, + message: native_field(finding, "message")?.extract()?, + remediation: native_field(finding, "remediation")?.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 519b5f4f15..8fe9e4a7c9 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/fix_facts.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/fix_facts.rs @@ -5,8 +5,11 @@ use pyo3::pyfunction; use pyo3::types::{PyDict, PyDictMethods, PyTuple}; use crate::bindings::_helpers::compile_driver::python_calls::call_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. #[pyfunction] @@ -19,19 +22,19 @@ pub(crate) fn compile_facts<'py>( let models = PyDict::new(py); let model_paths = PyDict::new(py); let paths_by_name = PyDict::new(py); - for model in project.getattr("models")?.try_iter()? { + for model in native_field(project, "models")?.try_iter()? { let model = model?; - let key = model.getattr("key")?; + let key = native_field(&model, "key")?; let path = project_dir - .call_method1("__truediv__", (model.getattr("relative_path")?,))? + .call_method1("__truediv__", (native_field(&model, "relative_path")?,))? .call_method0("resolve")?; models.set_item(&key, &model)?; model_paths.set_item(&key, &path)?; - paths_by_name.set_item(model.getattr("name")?, &path)?; + paths_by_name.set_item(native_field(&model, "name")?, &path)?; } let diagnostics = PyDict::new(py); let counter = py.import("collections")?.getattr("Counter")?; - for diagnostic in project.getattr("diagnostics")?.try_iter()? { + for diagnostic in project_attribute(project, "diagnostics")?.try_iter()? { let diagnostic = diagnostic?; let owner = diagnostic_owner(&diagnostic, &paths_by_name, project_dir)?; let counts = if let Some(counts) = diagnostics.get_item(&owner)? { @@ -44,10 +47,10 @@ pub(crate) fn compile_facts<'py>( let identity = PyTuple::new( py, [ - diagnostic.getattr("code")?, - text_or_empty(&diagnostic.getattr("resource_type")?)?, - text_or_empty(&diagnostic.getattr("resource_name")?)?, - diagnostic.getattr("message")?, + record_field(&diagnostic, "code")?, + text_or_empty(&record_field(&diagnostic, "resource_type")?)?, + text_or_empty(&record_field(&diagnostic, "resource_name")?)?, + record_field(&diagnostic, "message")?, ], )?; let count: i64 = counts.get_item(&identity)?.extract()?; @@ -68,19 +71,19 @@ fn diagnostic_owner<'py>( paths_by_name: &Bound<'py, PyDict>, project_dir: &Bound<'py, PyAny>, ) -> PyResult> { - let resource_type = diagnostic.getattr("resource_type")?; + let resource_type = record_field(diagnostic, "resource_type")?; if resource_type.eq("model")? - && let Some(path) = paths_by_name.get_item(diagnostic.getattr("resource_name")?)? + && let Some(path) = paths_by_name.get_item(record_field(diagnostic, "resource_name")?)? { return Ok(path); } - let mut path = diagnostic.getattr("path")?; + let mut path = record_field(diagnostic, "path")?; if !path.is_truthy()? { - let location = diagnostic.getattr("location")?; + let location = record_field(diagnostic, "location")?; path = if location.is_none() { location } else { - location.getattr("path")? + record_field(&location, "path")? }; } if path.is_none() { 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 e8e1ecdba5..f5a82effb1 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/fix_session.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/fix_session.rs @@ -6,16 +6,25 @@ use std::path::PathBuf; use pyo3::exceptions::PyException; use pyo3::prelude::{Bound, IntoPyObject, PyAny, PyAnyMethods, PyErr, PyResult, Python}; use pyo3::pyfunction; -use pyo3::types::{PyDict, PyDictMethods, PyFrozenSet, PyList, PyListMethods, PyTuple}; +use pyo3::types::{ + PyDict, PyDictMethods, PyFrozenSet, PyList, PyListMethods, PyString, PyStringMethods, PyTuple, +}; +use sqlbuild_core::text::main::python_strip::python_strip; use sqlbuild_rules::fix_verification::main::refusal::refusal; use sqlbuild_rules::fix_verification::main::with_edits::with_edits; use sqlbuild_rules::fix_verification::models::{FixEdit, FixEligibility}; 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::project::build_compiled_project; +use crate::bindings::_helpers::compile_driver::python_calls::{ + KeywordArguments, call_with, 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::ids::{ErrorId, NativeAttr, RecordId}; +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}; use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::rules::constants::SAFE_RULE_FIX_CODES; use crate::bindings::_helpers::rules::fix_facts::compile_facts; @@ -109,11 +118,9 @@ impl<'py> FixSession<'py> { fn run(&mut self) -> PyResult> { let py = self.original.py(); let mut current = self.original.copy()?; - let paths: Vec> = self - .inputs - .getattr("model_files")? + let paths: Vec> = field(&self.inputs, "model_files")? .try_iter()? - .map(|model| model?.getattr("file_path")) + .map(|model| model_file_path(&model?)) .collect::>()?; let model_paths = PyFrozenSet::new(py, paths)?; let mut seen: BTreeSet = BTreeSet::new(); @@ -204,15 +211,14 @@ impl<'py> FixSession<'py> { let mut pending: Vec> = Vec::new(); let mut remaining: Vec> = Vec::new(); for violation in violations { - if !violation - .getattr("code")? + if !field(&violation, "code")? .extract::()? .starts_with("SQBRSQL") { continue; } let reason = self.refusal(&violation, model_paths)?; - let edit = violation.getattr("fix")?; + let edit = field(&violation, "fix")?; if reason.is_some() || edit.is_none() { remaining.push(report( &violation, @@ -227,7 +233,7 @@ impl<'py> FixSession<'py> { )?); continue; } - let path = edit.getattr("file_path")?; + let path = field(&edit, "file_path")?; let chosen = if let Some(chosen) = edits.get_item(&path)? { chosen.cast_into::()? } else { @@ -235,12 +241,12 @@ impl<'py> FixSession<'py> { edits.set_item(&path, &chosen)?; chosen }; - let start: usize = edit.getattr("start")?.extract()?; - let end: usize = edit.getattr("end")?.extract()?; + let start: usize = field(&edit, "start")?.extract()?; + let end: usize = field(&edit, "end")?.extract()?; let mut overlaps = false; for other in chosen.iter() { - if start < other.getattr("end")?.extract::()? - && other.getattr("start")?.extract::()? < end + if start < field(&other, "end")?.extract::()? + && field(&other, "start")?.extract::()? < end { overlaps = true; break; @@ -259,20 +265,20 @@ impl<'py> FixSession<'py> { violation: &Bound<'py, PyAny>, model_paths: &Bound<'py, PyAny>, ) -> PyResult> { - let code = violation.getattr("code")?; - let path = violation.getattr("file_path")?; - let has_fix = !violation.getattr("fix")?.is_none(); + let code = field(violation, "code")?; + let path = field(violation, "file_path")?; + let has_fix = !field(violation, "fix")?.is_none(); let facts = FixEligibility { model_body: model_paths.contains(&path)?, has_fix, safe_code: SAFE_RULE_FIX_CODES.contains(&code.extract::()?.as_str()), conditionless_join: code.eq("SQBRSQL003")?, - snowflake: self.config.getattr("dialect")?.eq("snowflake")?, + snowflake: field(&self.config, "dialect")?.eq("snowflake")?, }; if let Some(reason) = refusal(&facts) { return Ok(Some(reason.to_owned())); } - let unavailable = violation.getattr("fix_unavailable_reason")?; + let unavailable = field(violation, "fix_unavailable_reason")?; if !unavailable.is_none() || !has_fix { return unavailable.extract(); } @@ -317,7 +323,7 @@ impl<'py> FixSession<'py> { let mut reports: Vec> = Vec::new(); for report in self.reports.iter().chain(pending) { reports.push( - if let Some(reason) = self.blocked.get_item(report.getattr("file_path")?)? { + if let Some(reason) = self.blocked.get_item(field(report, "file_path")?)? { refused(report, &reason.extract::()?)? } else { report.clone() @@ -369,12 +375,16 @@ impl<'py> FixSession<'py> { if let Some(baseline) = &self.baseline { return Ok(baseline.clone()); } - let baseline = call_with( - ®istry::native(self.inputs.py(), NativeAttr::BuildCompiledProject)?, - &[ - ("discovered_inputs", self.inputs.clone()), - ("adapter", self.adapter.clone()), - ], + let py = self.inputs.py(); + let baseline = build_compiled_project( + py, + Some(keywords( + py, + &[ + ("discovered_inputs", self.inputs.clone()), + ("adapter", self.adapter.clone()), + ], + )?), )?; self.baseline = Some(baseline.clone()); Ok(baseline) @@ -411,12 +421,15 @@ impl<'py> FixSession<'py> { if self.baseline_facts.is_none() { self.baseline_facts = Some(compile_facts(py, &baseline, &self.project_dir)?); } - let compiled = call_with( - ®istry::native(py, NativeAttr::BuildCompiledProject)?, - &[ - ("discovered_inputs", updated_inputs(&self.inputs, files)?), - ("adapter", self.adapter.clone()), - ], + let compiled = build_compiled_project( + py, + Some(keywords( + py, + &[ + ("discovered_inputs", updated_inputs(&self.inputs, files)?), + ("adapter", self.adapter.clone()), + ], + )?), )?; let mut edited: Vec> = Vec::new(); for (path, text) in files.iter() { @@ -465,8 +478,8 @@ impl<'py> FixSession<'py> { let affected = self.blocked.copy()?; let mut reports: Vec> = Vec::new(); for report in &self.reports { - if report.getattr("status")?.eq("applied")? { - affected.set_item(report.getattr("file_path")?, py.None())?; + if field(report, "status")?.eq("applied")? { + affected.set_item(field(report, "file_path")?, py.None())?; } reports.push(refused(report, reason)?); } @@ -483,13 +496,61 @@ fn replace<'py>( object: &Bound<'py, PyAny>, changes: &[(&str, Bound<'py, PyAny>)], ) -> PyResult> { - let kwargs = - crate::bindings::_helpers::compile_driver::python_calls::keywords(object.py(), changes)?; - object - .py() - .import("dataclasses")? - .getattr("replace")? - .call((object,), Some(&kwargs)) + replaced(object, changes) +} + +/// A discovered model file's contents, read in place from a native model file. +fn model_contents<'py>(model: &Bound<'py, PyAny>) -> PyResult> { + match model.cast::() { + Ok(native) => Ok(native.get().texts(model.py()).1.clone().into_any()), + Err(_) => native_field(model, "contents"), + } +} + +/// A discovered model file's path, read in place from a native model file. +fn model_file_path<'py>(model: &Bound<'py, PyAny>) -> PyResult> { + match model.cast::() { + Ok(native) => Ok(native.get().paths(model.py()).0.clone()), + Err(_) => native_field(model, "file_path"), + } +} + +/// `model` with its edited text and the query `text` holds where the old query started. +fn edited_model<'py>( + model: &Bound<'py, PyAny>, + text: Bound<'py, PyAny>, +) -> PyResult> { + let py = model.py(); + let Ok(native) = model.cast::() else { + let current = native_field(model, "contents")?; + let start = current.call_method1("index", (native_field(model, "query_sql")?,))?; + let query = text + .get_item(pyo3::types::PySlice::new( + py, + start.extract()?, + isize::MAX, + 1, + ))? + .call_method0("strip")?; + return model.call_method1("with_text", (text, query)); + }; + let native: &NativeModelFile = native.get(); + let (query_sql, contents) = native.texts(py); + let contents: String = contents.to_str()?.to_owned(); + let Some(offset) = contents.find(query_sql.to_str()?) else { + return Err(pyo3::exceptions::PyValueError::new_err( + "substring not found", + )); + }; + let start: usize = contents[..offset].chars().count(); + let text: Bound<'py, PyString> = text.cast_into::()?; + let edited: String = text.to_str()?.to_owned(); + let tail: &str = edited + .char_indices() + .nth(start) + .map_or("", |(index, _)| &edited[index..]); + let query: Bound<'py, PyString> = PyString::new(py, python_strip(tail)); + Ok(Bound::new(py, native.with_text(py, text.unbind(), query.unbind()))?.into_any()) } fn updated_inputs<'py>( @@ -498,22 +559,13 @@ fn updated_inputs<'py>( ) -> PyResult> { let py = inputs.py(); let mut models: Vec> = Vec::new(); - for model in inputs.getattr("model_files")?.try_iter()? { + for model in field(inputs, "model_files")?.try_iter()? { let model = model?; - let current = model.getattr("contents")?; - if let Some(text) = files.get_item(model.getattr("file_path")?)? + let current = model_contents(&model)?; + if let Some(text) = files.get_item(model_file_path(&model)?)? && !text.eq(¤t)? { - let start = current.call_method1("index", (model.getattr("query_sql")?,))?; - let query = text - .get_item(pyo3::types::PySlice::new( - py, - start.extract()?, - isize::MAX, - 1, - ))? - .call_method0("strip")?; - models.push(model.call_method1("with_text", (text, query))?); + models.push(edited_model(&model, text)?); } else { models.push(model); } @@ -538,9 +590,9 @@ fn edited_files<'py>( .map(|edit| { let edit = edit?; Ok(FixEdit { - start: edit.getattr("start")?.extract()?, - end: edit.getattr("end")?.extract()?, - replacement: edit.getattr("replacement")?.extract()?, + start: field(&edit, "start")?.extract()?, + end: field(&edit, "end")?.extract()?, + replacement: field(&edit, "replacement")?.extract()?, }) }) .collect::>()?; @@ -565,7 +617,7 @@ fn failing_edits<'py>( for (path, _) in edits.iter() { resolved.set_item(path.call_method0("resolve")?, path)?; } - for (path, reason) in verdict.getattr("failing")?.cast_into::()?.iter() { + for (path, reason) in field(verdict, "failing")?.cast_into::()?.iter() { if let Some(original) = resolved.get_item(path)? { result.set_item(original, reason)?; } @@ -608,9 +660,9 @@ fn report<'py>( call_with( ®istry::class(py, RecordId::RuleFixResult)?, &[ - ("file_path", source.getattr("file_path")?), - ("code", source.getattr("code")?), - ("line", source.getattr("line")?), + ("file_path", field(source, "file_path")?), + ("code", field(source, "code")?), + ("line", field(source, "line")?), ("status", status.into_pyobject(py)?.into_any()), ("reason", reason.into_pyobject(py)?.into_any()), ], @@ -618,7 +670,7 @@ fn report<'py>( } fn refused<'py>(report: &Bound<'py, PyAny>, reason: &str) -> PyResult> { - if !report.getattr("status")?.eq("applied")? { + if !field(report, "status")?.eq("applied")? { return Ok(report.clone()); } replace( @@ -691,6 +743,5 @@ fn result<'py>( /// Whether a fix verdict proved every edit: nothing failed and nothing was unattributed. fn verified(verdict: &Bound<'_, PyAny>) -> PyResult { - Ok(!verdict.getattr("failing")?.is_truthy()? - && !verdict.getattr("unattributed")?.is_truthy()?) + Ok(!field(verdict, "failing")?.is_truthy()? && !field(verdict, "unattributed")?.is_truthy()?) } 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 3f66d44e87..8aa4c95f2d 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/fix_verdict.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/fix_verdict.rs @@ -9,6 +9,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; @@ -21,8 +22,8 @@ pub(crate) fn fix_verdict<'py>( after: &Bound<'py, PyAny>, edited: &Bound<'py, PyAny>, ) -> PyResult> { - let before_models = before.getattr("models")?.cast_into::()?; - let after_models = after.getattr("models")?.cast_into::()?; + let before_models = native_field(before, "models")?.cast_into::()?; + let after_models = native_field(after, "models")?.cast_into::()?; let same_models = before_models .call_method0("keys")? .eq(after_models.call_method0("keys")?)?; @@ -31,26 +32,27 @@ pub(crate) fn fix_verdict<'py>( let mut models: Vec = Vec::new(); let mut diagnostics: Vec = Vec::new(); if same_models { - let paths = before.getattr("model_paths")?; + let paths = native_field(before, "model_paths")?; for (key, model) in before_models.iter() { let other = after_models .get_item(&key)? .ok_or_else(|| pyo3::exceptions::PyKeyError::new_err("model key"))?; let path = paths.get_item(&key)?; let owner = owner_index((&owners, &indices), &path)?; - let columns = model.getattr("inferred_columns")?; - let other_columns = other.getattr("inferred_columns")?; + let columns = native_field(&model, "inferred_columns")?; + let other_columns = native_field(&other, "inferred_columns")?; models.push(ModelComparison { owner, edited: edited.contains(&path)?, columns_known: !columns.is_none() && !other_columns.is_none(), columns_equal: columns.eq(&other_columns)?, - dependencies_equal: model.getattr("deps")?.eq(other.getattr("deps")?)?, + dependencies_equal: native_field(&model, "deps")? + .eq(native_field(&other, "deps")?)?, lineage_equal: lineage(&model)?.eq(lineage(&other)?)?, }); } - let previous = before.getattr("diagnostics")?.cast_into::()?; - let current = after.getattr("diagnostics")?.cast_into::()?; + let previous = native_field(before, "diagnostics")?.cast_into::()?; + let current = native_field(after, "diagnostics")?.cast_into::()?; let diagnostic_owners = PyDict::new(py); for (owner, _) in previous.iter().chain(current.iter()) { diagnostic_owners.set_item(owner, py.None())?; @@ -96,7 +98,7 @@ fn owner_index( } fn lineage<'py>(model: &Bound<'py, PyAny>) -> PyResult> { - let value = model.getattr("fast_lineage_columns")?; + let value = native_field(model, "fast_lineage_columns")?; if value.is_none() { Ok(value) } else { 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 8bb4a852ed..5d85cd1d93 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/fixture_formatting.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/fixture_formatting.rs @@ -11,6 +11,7 @@ use sqlbuild_sqltext::sql_scan::models::LexicalSyntax; use crate::bindings::_helpers::boundary::panics::{compiler_guard, value_error}; use crate::bindings::_helpers::discovery::sql_test_records::parse_sql_test_file; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::rules::constants::MODEL_TEST_MODE; use crate::bindings::_helpers::rules::fixture_schemas; use crate::bindings::_helpers::sqltext::lexical_syntax::LexicalSyntaxInput; @@ -18,7 +19,7 @@ use crate::bindings::_helpers::sqltext::lexical_syntax::LexicalSyntaxInput; #[pyfunction] #[pyo3(signature = (*, sql, relation))] fn simplify_fixture_null_projections(sql: &str, relation: &Bound<'_, PyAny>) -> PyResult { - let columns = fixture_schemas::columns(&relation.getattr("columns")?)?; + let columns = fixture_schemas::columns(&native_field(relation, "columns")?)?; compiler_guard(|| simplify_fixture_nulls(sql, &columns).map_err(value_error)) } @@ -38,9 +39,9 @@ fn format_redundant_fixture_nulls<'py>( .extract::()? .into(); let updated = PyDict::new(py); - for file in discovered_inputs.getattr("test_files")?.try_iter()? { + for file in native_field(discovered_inputs, "test_files")?.try_iter()? { let file = file?; - let relative = file.getattr("relative_path")?; + let relative = native_field(&file, "relative_path")?; let path = project_dir .call_method1("__truediv__", (&relative,))? .call_method0("resolve")?; @@ -65,7 +66,7 @@ fn format_redundant_fixture_nulls<'py>( .map(|block| { let block = block?; Ok(FixtureFormatBlock { - sql_body: block.getattr("sql_body")?.extract()?, + sql_body: native_field(&block, "sql_body")?.extract()?, model_mode: block .getattr("mode")? .getattr("value")? diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/fixture_schemas.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/fixture_schemas.rs index 75c88a8b82..1c60070c08 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/fixture_schemas.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/fixture_schemas.rs @@ -7,9 +7,12 @@ use sqlbuild_model_config::path_defaults::models::PathDefaultChoice; use sqlbuild_rules::lint_text::models::FixtureFormatRelation; use sqlbuild_rules::lint_text::types::FixtureFormatColumn; use std::collections::HashMap; +use std::path::Path; +use crate::bindings::_helpers::discovery::model_file_record::NativeModelFile; use crate::bindings::_helpers::model_config::header_columns::schema_columns; use crate::bindings::_helpers::model_config::model_config_builder::raised_config_error; +use crate::bindings::_helpers::records::fields::field; pub(crate) fn columns(columns: &Bound<'_, PyAny>) -> PyResult> { columns @@ -17,31 +20,68 @@ pub(crate) fn columns(columns: &Bound<'_, PyAny>) -> PyResult { + file_path: Bound<'py, PyAny>, + relative_path: Bound<'py, PyAny>, + header_values: Bound<'py, PyDict>, + header_column_locations: Bound<'py, PyDict>, +} + +/// A native model file's parts read in place; any other model file's by attribute. +fn model_file_view<'py>(model: &Bound<'py, PyAny>) -> PyResult> { + let py = model.py(); + if let Ok(native) = model.cast::() { + let native: &NativeModelFile = native.get(); + let (file_path, relative_path) = native.paths(py); + return Ok(ModelFileView { + file_path: file_path.clone(), + relative_path: relative_path.clone(), + header_values: native.header_value_dict(py).clone(), + header_column_locations: native.header_locations(py)?, + }); + } + Ok(ModelFileView { + file_path: field(model, "file_path")?, + relative_path: field(model, "relative_path")?, + header_values: field(model, "header_values")?.cast_into::()?, + header_column_locations: field(model, "header_column_locations")?.cast_into::()?, + }) +} + +/// `PurePath(file_name).stem`: the final component without its last suffix. +fn file_stem(file_name: &str) -> String { + Path::new(file_name) + .file_stem() + .map_or_else(String::new, |stem| stem.to_string_lossy().into_owned()) +} + pub(crate) fn relations( inputs: &Bound<'_, PyAny>, ) -> PyResult> { let py = inputs.py(); - let config = inputs.getattr("project_config")?; + let config = field(inputs, "project_config")?; let default_contract: Option = - config.getattr("defaults")?.getattr("contract")?.extract()?; - let defaults = config.getattr("path_defaults")?.cast_into::()?; + field(&field(&config, "defaults")?, "contract")?.extract()?; + let defaults = field(&config, "path_defaults")?.cast_into::()?; let keys: Vec = defaults .keys() .iter() .map(|key| key.extract()) .collect::>()?; let mut result: HashMap = HashMap::new(); - for model in inputs.getattr("model_files")?.try_iter()? { + for model in field(inputs, "model_files")?.try_iter()? { let model = model?; - let path = model.getattr("relative_path")?.str()?.to_str()?.to_owned(); + let file: ModelFileView<'_> = model_file_view(&model)?; + let path = file.relative_path.str()?.to_str()?.to_owned(); let mut contract = default_contract.clone(); match select_path_default(&path, &keys) { PathDefaultChoice::Selected(Some(key)) => { @@ -58,39 +98,35 @@ pub(crate) fn relations( PathDefaultChoice::Selected(None) => {} PathDefaultChoice::Conflict(error) => return Err(raised_config_error(py, error)), } - let header = model.getattr("header_values")?.cast_into::()?; + let header = &file.header_values; if let Some(value) = header.get_item("contract")? { contract = value.extract()?; } if contract.as_deref() != Some("enforced") { continue; } - let file = model.getattr("file_path")?; - let file_name = file.str()?.to_str()?.to_owned(); + let file_name = file.file_path.str()?.to_str()?.to_owned(); let raw_columns = header .get_item("columns")? .unwrap_or_else(|| py.None().into_bound(py)); - let locations = model - .getattr("header_column_locations")? - .cast_into::()?; let parsed = schema_columns( &raw_columns, (&file_name, "model"), (false, true, false), - &locations, + &file.header_column_locations, )?; let columns = columns(parsed.as_any())?; if !columns.is_empty() { result.insert( - format!("__ref__{}", file.getattr("stem")?.extract::()?), + format!("__ref__{}", file_stem(&file_name)), FixtureFormatRelation { columns }, ); } } - for source_file in inputs.getattr("source_files")?.try_iter()? { - for source in source_file?.getattr("source_entries")?.try_iter()? { + for source_file in field(inputs, "source_files")?.try_iter()? { + for source in field(&source_file?, "source_entries")?.try_iter()? { let source = source?; - let contract: Option = source.getattr("contract")?.extract()?; + let contract: Option = field(&source, "contract")?.extract()?; let contract = contract .filter(|value| !value.is_empty()) .or_else(|| default_contract.clone()); @@ -98,20 +134,20 @@ pub(crate) fn relations( continue; } result.insert( - format!("__source__{}", source.getattr("name")?.extract::()?), + format!("__source__{}", field(&source, "name")?.extract::()?), FixtureFormatRelation { - columns: columns(&source.getattr("columns")?)?, + columns: columns(&field(&source, "columns")?)?, }, ); } } - for schema_file in inputs.getattr("schema_files")?.try_iter()? { - for seed in schema_file?.getattr("seed_entries")?.try_iter()? { + for schema_file in field(inputs, "schema_files")?.try_iter()? { + for seed in field(&schema_file?, "seed_entries")?.try_iter()? { let seed = seed?; - let columns = columns(&seed.getattr("columns")?)?; + let columns = columns(&field(&seed, "columns")?)?; if !columns.is_empty() { result.insert( - format!("__seed__{}", seed.getattr("name")?.extract::()?), + format!("__seed__{}", field(&seed, "name")?.extract::()?), FixtureFormatRelation { columns }, ); } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/guidance.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/guidance.rs index b9a2663c78..da53ed57d0 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/guidance.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/guidance.rs @@ -20,6 +20,7 @@ use sqlbuild_rules::rule_catalogue::models::{FingerprintRule, OptionText, RuleTe use crate::bindings::_helpers::boundary::panics::compiler_guard; use crate::bindings::_helpers::compile_driver::python_calls::imported; +use crate::bindings::_helpers::records::fields::field as record_field; use crate::bindings::_helpers::rules::catalogue::{ native_rules_config, references_custom_rules, rule_catalogue, select_rules, verify_custom_rules, }; @@ -36,8 +37,8 @@ const SKILL_PATHS: [&str; 3] = [ const SKILL_LOCK_DIRECTORY: &str = "sqlbuild-rules-skill-locks"; const FRESH_SKILL: &str = "fresh"; -fn text_field(object: &Bound<'_, PyAny>, field: &str) -> PyResult { - object.getattr(field)?.extract() +fn text_field(object: &Bound<'_, PyAny>, name: &str) -> PyResult { + record_field(object, name)?.extract() } fn repr_text(value: &Bound<'_, PyAny>) -> PyResult { @@ -48,17 +49,16 @@ fn repr_text(value: &Bound<'_, PyAny>) -> PyResult { fn rule_text(rule: &Bound<'_, PyAny>, config: Option<&Bound<'_, PyAny>>) -> PyResult { let code: String = text_field(rule, "code")?; let configured: Option> = match config { - Some(config) => config - .getattr("rule_options")? + Some(config) => record_field(config, "rule_options")? .cast_into::()? .get_item(&code)?, None => None, }; let mut options: Vec = Vec::new(); - for option in rule.getattr("options")?.try_iter()? { + for option in record_field(rule, "options")?.try_iter()? { let option: Bound<'_, PyAny> = option?; let name: String = text_field(&option, "name")?; - let default: Bound<'_, PyAny> = option.getattr("default")?; + let default: Bound<'_, PyAny> = record_field(&option, "default")?; let effective: Bound<'_, PyAny> = match &configured { Some(values) => values.call_method1("get", (&name, &default))?, None => default.clone(), @@ -70,16 +70,16 @@ fn rule_text(rule: &Bound<'_, PyAny>, config: Option<&Bound<'_, PyAny>>) -> PyRe effective_repr: repr_text(&effective)?, }); } - let guidance: Bound<'_, PyAny> = rule.getattr("guidance")?; + let guidance: Bound<'_, PyAny> = record_field(rule, "guidance")?; Ok(RuleText { code, family: text_field(rule, "family")?, slug: text_field(rule, "slug")?, message: text_field(rule, "message")?, remediation: text_field(rule, "remediation")?, - enabled_by_default: rule.getattr("enabled_by_default")?.is_truthy()?, - custom: rule.getattr("custom")?.is_truthy()?, - source: rule.getattr("source")?.extract()?, + enabled_by_default: record_field(rule, "enabled_by_default")?.is_truthy()?, + custom: record_field(rule, "custom")?.is_truthy()?, + source: record_field(rule, "source")?.extract()?, options, good_example: if guidance.is_none() { None @@ -103,14 +103,14 @@ fn fingerprint_rule( rule: &Bound<'_, PyAny>, project_dir: &Bound<'_, PyAny>, ) -> PyResult { - let custom: bool = rule.getattr("custom")?.is_truthy()?; + let custom: bool = record_field(rule, "custom")?.is_truthy()?; Ok(FingerprintRule { code: text_field(rule, "code")?, family: text_field(rule, "family")?, slug: text_field(rule, "slug")?, message: text_field(rule, "message")?, remediation: text_field(rule, "remediation")?, - source: rule.getattr("source")?.extract()?, + source: record_field(rule, "source")?.extract()?, implementation_hash: if custom { custom_rule_implementation_fingerprint(rule, project_dir, None)? } else { @@ -130,7 +130,7 @@ fn render_skills( let rules = select_rules(&catalogue, config, project_dir, None)?; let mut custom_selected: bool = false; for rule in &rules { - custom_selected |= rule.getattr("custom")?.is_truthy()?; + custom_selected |= record_field(rule, "custom")?.is_truthy()?; } if custom_selected { verify_custom_rules(&rules, project_dir)?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_expansion.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_expansion.rs index 33db3aa639..f648ff9397 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_expansion.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_expansion.rs @@ -13,6 +13,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr}; use crate::bindings::_helpers::records::registry; @@ -89,8 +90,8 @@ fn expansion_inputs(sources: LintExpansionSources<'_>) -> PyResult renderer, None => { let adapter_name: Bound<'_, PyAny> = resolve_effective_adapter_name( - &discovered.getattr("project_config")?, - &discovered.getattr("local_config")?, + &native_field(&discovered, "project_config")?, + &native_field(&discovered, "local_config")?, )?; call_with( &host::entry(py, AdapterHost::ResolveAdapter)?, 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 05251751e4..724e4ca762 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_headers.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_headers.rs @@ -6,6 +6,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; @@ -32,7 +33,7 @@ pub(crate) fn lint_file_role<'py>( file_path.call_method1("relative_to", (project_dir,))? } }; - let parts: Vec = relative.getattr("parts")?.extract()?; + 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,))?, None => py.None().into_bound(py), 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 d8217b0402..23a843bc46 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_inputs.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_inputs.rs @@ -10,6 +10,7 @@ use crate::bindings::_helpers::compiled_project::model_input_view::{ ExpansionRecord, ModelInputRecord, NativeModelInput, model_relation_key_row, }; use crate::bindings::_helpers::compiled_project::project_view::NativeProjectView; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::rules::lint_pipeline::{ CompiledExpansion, CompiledSource, span_pass, }; @@ -28,7 +29,7 @@ pub(crate) fn model_key_row(model: &Bound<'_, PyAny>) -> PyResult() { return compiled.get().relation_key_row(py); } - model.getattr("relation_key_row")?.extract() + native_field(model, "relation_key_row")?.extract() } /// The model file path of a native input record. @@ -75,9 +76,9 @@ pub(crate) fn model_lint_expansion( ))) }); } - let mut record: Bound<'_, PyAny> = model.getattr("lint_expansion")?; + let mut record: Bound<'_, PyAny> = native_field(model, "lint_expansion")?; if !record.is_truthy()? { - record = model.getattr("sql_expansion")?; + record = native_field(model, "sql_expansion")?; } if record.is_none() { return Ok(None); @@ -115,7 +116,8 @@ pub(crate) fn project_sql_expansions( } return Ok(expansions); } - crate::bindings::_helpers::rules::run_lint::compiled_expansion_records( - &project.getattr("sql_expansions")?, - ) + crate::bindings::_helpers::rules::run_lint::compiled_expansion_records(&native_field( + project, + "sql_expansions", + )?) } 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 0b7114cd39..fe3ae76b86 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::fields::field as record_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::render::authored_sql::{AuthoredExpansion, expand_with_inputs}; @@ -95,28 +96,27 @@ impl LintSettings { /// The settings a `LintConfig` record holds. pub(crate) fn from_record(config: &Bound<'_, PyAny>) -> PyResult { let mut relation_keys: Vec<(String, Vec>)> = Vec::new(); - for item in config - .getattr("relation_keys")? + for item in record_field(config, "relation_keys")? .call_method0("items")? .try_iter()? { relation_keys.push(item?.extract()?); } Ok(Self { - native_enabled: config.getattr("native_enabled")?.is_truthy()?, - header_rules_enabled: config.getattr("header_rules_enabled")?.is_truthy()?, + native_enabled: record_field(config, "native_enabled")?.is_truthy()?, + header_rules_enabled: record_field(config, "header_rules_enabled")?.is_truthy()?, header_rules: HeaderRulesConfig { - line_width: config.getattr("line_width")?.extract()?, - max_description_lines: config.getattr("max_description_lines")?.extract()?, + line_width: record_field(config, "line_width")?.extract()?, + max_description_lines: record_field(config, "max_description_lines")?.extract()?, }, sql: LintSqlOptions { - dialect: config.getattr("dialect")?.extract()?, - enabled_rules: config.getattr("enabled_native_rules")?.extract()?, - ignored_rules: config.getattr("ignored_native_rules")?.extract()?, - max_literal_length: config.getattr("max_literal_length")?.extract()?, - max_ranking_order_by: config.getattr("max_ranking_order_by")?.extract()?, + dialect: record_field(config, "dialect")?.extract()?, + enabled_rules: record_field(config, "enabled_native_rules")?.extract()?, + ignored_rules: record_field(config, "ignored_native_rules")?.extract()?, + max_literal_length: record_field(config, "max_literal_length")?.extract()?, + max_ranking_order_by: record_field(config, "max_ranking_order_by")?.extract()?, }, - allow_suppressions: config.getattr("allow_suppressions")?.is_truthy()?, + allow_suppressions: record_field(config, "allow_suppressions")?.is_truthy()?, relation_keys, }) } @@ -150,22 +150,22 @@ impl CompiledExpansion { /// The expansion a `CompiledSqlExpansion` record holds. pub(crate) fn from_record(record: &Bound<'_, PyAny>) -> PyResult { let mut passes: Vec> = Vec::new(); - for pass in record.getattr("passes")?.try_iter()? { + for pass in record_field(record, "passes")?.try_iter()? { let mut spans: Vec = Vec::new(); for span in pass?.try_iter()? { let span: Bound<'_, PyAny> = span?; spans.push(ExpansionSpan { - source_start: span.getattr("source_start")?.extract()?, - source_end: span.getattr("source_end")?.extract()?, - output_start: span.getattr("output_start")?.extract()?, - output_end: span.getattr("output_end")?.extract()?, + source_start: record_field(&span, "source_start")?.extract()?, + source_end: record_field(&span, "source_end")?.extract()?, + output_start: record_field(&span, "output_start")?.extract()?, + output_end: record_field(&span, "output_end")?.extract()?, }); } passes.push(spans); } Ok(Self { - authored_sql: record.getattr("authored_sql")?.extract()?, - expanded_sql: record.getattr("expanded_sql")?.extract()?, + authored_sql: record_field(record, "authored_sql")?.extract()?, + expanded_sql: record_field(record, "expanded_sql")?.extract()?, passes, }) } 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 10597b7345..8e063a9d8e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_preparation.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_preparation.rs @@ -7,6 +7,7 @@ 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::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::{ @@ -70,7 +71,7 @@ pub(crate) fn prepare_lint_body<'py>( let file_path: Bound<'py, PyAny> = arguments.value("file_path")?; let contents: String = arguments.value("contents")?.extract()?; let dialect: String = arguments.value("dialect")?.extract()?; - let kind: Option = role.getattr("declaration_kind")?.extract()?; + let kind: Option = native_field(&role, "declaration_kind")?.extract()?; let prepared: SingleBody = prepare_single_body(&SingleBodyRequest { context: &SqlExpansionInputs::from_record(&arguments.value("context")?)?, file_path: &file_path, @@ -78,8 +79,8 @@ pub(crate) fn prepare_lint_body<'py>( range: arguments.value("body_range")?.extract()?, role: BodyRole { kind: kind.as_deref(), - in_project: role.getattr("in_project")?.is_truthy()?, - in_hook_directory: role.getattr("in_hook_directory")?.is_truthy()?, + in_project: native_field(&role, "in_project")?.is_truthy()?, + in_hook_directory: native_field(&role, "in_hook_directory")?.is_truthy()?, }, dialect: &dialect, })?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_sql_bodies.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_sql_bodies.rs index 5e8c656119..c2cdc90f4d 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_sql_bodies.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_sql_bodies.rs @@ -9,6 +9,7 @@ use sqlbuild_rules::lint_text::models::{ }; use crate::bindings::_helpers::boundary::panics::value_error; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::rules::lint_pipeline::{LintViolationRow, ViolationClasses}; use crate::bindings::types::CompilerDetach; @@ -21,10 +22,10 @@ fn pass_spans(pass: &Bound<'_, PyAny>) -> PyResult> { .map(|span| { let span: Bound<'_, PyAny> = span?; Ok(ExpansionSpan { - source_start: span.getattr("source_start")?.extract()?, - source_end: span.getattr("source_end")?.extract()?, - output_start: span.getattr("output_start")?.extract()?, - output_end: span.getattr("output_end")?.extract()?, + source_start: native_field(&span, "source_start")?.extract()?, + source_end: native_field(&span, "source_end")?.extract()?, + output_start: native_field(&span, "output_start")?.extract()?, + output_end: native_field(&span, "output_end")?.extract()?, }) }) .collect() @@ -36,7 +37,7 @@ fn relation_keys( declared: &Bound<'_, PyAny>, ) -> PyResult>)>> { let mut keys: Vec<(String, Vec>)> = Vec::new(); - for relation in body.getattr("dependency_relations")?.try_iter()? { + for relation in native_field(body, "dependency_relations")?.try_iter()? { let (sentinel, name): (String, Bound<'_, PyAny>) = relation?.extract()?; let found: Bound<'_, PyAny> = declared.call_method1("get", (name,))?; if found.is_truthy()? { @@ -53,30 +54,30 @@ fn lint_body( ) -> PyResult { let file: usize = index_by_path .as_any() - .get_item(body.getattr("file_path")?)? + .get_item(native_field(body, "file_path")?)? .extract()?; Ok(LintSqlBody { file, - body_start: body.getattr("body_start")?.extract()?, - lint_text: body.getattr("lint_text")?.extract()?, - passes: spans(&body.getattr("passes")?)?, - external_identifiers: body.getattr("external_identifiers")?.extract()?, - dependency_identifiers: body.getattr("dependency_identifiers")?.extract()?, - externally_referenced_ctes: body.getattr("externally_referenced_ctes")?.extract()?, - allows_ceremonial_select: body.getattr("allows_ceremonial_select")?.extract()?, - allows_dynamic_output_star: body.getattr("allows_dynamic_output_star")?.extract()?, - allows_empty_fixture_star: body.getattr("allows_empty_fixture_star")?.extract()?, + body_start: native_field(body, "body_start")?.extract()?, + lint_text: native_field(body, "lint_text")?.extract()?, + passes: spans(&native_field(body, "passes")?)?, + external_identifiers: native_field(body, "external_identifiers")?.extract()?, + dependency_identifiers: native_field(body, "dependency_identifiers")?.extract()?, + externally_referenced_ctes: native_field(body, "externally_referenced_ctes")?.extract()?, + allows_ceremonial_select: native_field(body, "allows_ceremonial_select")?.extract()?, + allows_dynamic_output_star: native_field(body, "allows_dynamic_output_star")?.extract()?, + allows_empty_fixture_star: native_field(body, "allows_empty_fixture_star")?.extract()?, relation_keys: relation_keys(body, declared)?, }) } fn options(config: &Bound<'_, PyAny>) -> PyResult { Ok(LintSqlOptions { - dialect: config.getattr("dialect")?.extract()?, - enabled_rules: config.getattr("enabled_native_rules")?.extract()?, - ignored_rules: config.getattr("ignored_native_rules")?.extract()?, - max_literal_length: config.getattr("max_literal_length")?.extract()?, - max_ranking_order_by: config.getattr("max_ranking_order_by")?.extract()?, + dialect: native_field(config, "dialect")?.extract()?, + enabled_rules: native_field(config, "enabled_native_rules")?.extract()?, + ignored_rules: native_field(config, "ignored_native_rules")?.extract()?, + max_literal_length: native_field(config, "max_literal_length")?.extract()?, + max_ranking_order_by: native_field(config, "max_ranking_order_by")?.extract()?, }) } @@ -96,7 +97,7 @@ fn lint_native_sql_bodies<'py>( paths.push(path); texts.push(text.extract()?); } - let declared: Bound<'py, PyAny> = config.getattr("relation_keys")?; + let declared: Bound<'py, PyAny> = native_field(&config, "relation_keys")?; let inputs: Vec = bodies .iter() .map(|body| lint_body(body, &index_by_path, &declared)) 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 9de19afb41..0183cc8d3b 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_text.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/lint_text.rs @@ -23,6 +23,7 @@ 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::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::{ @@ -127,7 +128,7 @@ fn scan_fixture_typed_null_candidates<'py>( fn named_declaration_role_kind<'py>( relative_path: &Bound<'py, PyAny>, ) -> PyResult>> { - let parts: Vec = relative_path.getattr("parts")?.extract()?; + 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,))?, @@ -178,15 +179,15 @@ fn suppressible( violation: &Bound<'_, PyAny>, index_by_path: &Bound<'_, PyDict>, ) -> PyResult { - let file: Option = match index_by_path.get_item(violation.getattr("file_path")?)? { + let file: Option = match index_by_path.get_item(native_field(violation, "file_path")?)? { Some(index) => Some(index.extract()?), None => None, }; Ok(SuppressibleViolation { file: file.unwrap_or(usize::MAX), - line: violation.getattr("line")?.extract()?, - code: violation.getattr("code")?.extract()?, - native: violation.getattr("engine")?.extract::()? == NATIVE_ENGINE, + line: native_field(violation, "line")?.extract()?, + code: native_field(violation, "code")?.extract()?, + native: native_field(violation, "engine")?.extract::()? == NATIVE_ENGINE, }) } @@ -220,8 +221,8 @@ fn format_lint_sql_bodies<'py>( }) }) .collect::>()?; - let dialect: String = config.getattr("dialect")?.extract()?; - let line_width: usize = config.getattr("line_width")?.extract()?; + let dialect: String = native_field(config, "dialect")?.extract()?; + let line_width: usize = native_field(config, "line_width")?.extract()?; let result: FormattedBodies = py .compiler_detach(|| { format_sql_bodies(&inputs, &dialect, line_width).map_err(|error| error.to_string()) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/mod.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/mod.rs index 45f26037a7..f39c6ee92c 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/mod.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/mod.rs @@ -1,5 +1,6 @@ pub(crate) mod builtin_catalogue; pub(crate) mod catalogue; +pub(crate) mod config_json; pub(crate) mod constants; pub(crate) mod custom_evaluation; pub(crate) mod custom_host_observation; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/project_tree_queries.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/project_tree_queries.rs index 7538f41b7c..7303f28cc9 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/project_tree_queries.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/project_tree_queries.rs @@ -22,6 +22,7 @@ use sqlbuild_rules::project_tree::main::read_project_text::read_project_text; use sqlbuild_rules::project_tree::main::resolve_project_path::resolve_project_path; use crate::bindings::_helpers::compile_driver::python_calls::imported; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::ErrorId; use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::rules::rule_api::{usage_error_for, validate_project_relative}; @@ -38,7 +39,7 @@ fn node_values<'py>(nodes: &Bound<'py, PyAny>) -> PyResult<(Vec> = nodes.try_iter()?.collect::>()?; let values: Vec = nodes .iter() - .map(|node| node.getattr("value")?.extract()) + .map(|node| native_field(node, "value")?.extract()) .collect::>()?; Ok((nodes, values)) } 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 aacff66cec..f673fe6c24 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/relation_keys.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/relation_keys.rs @@ -10,6 +10,7 @@ use sqlbuild_rules::sql_quality::main::declared_relation_keys::declared_relation use sqlbuild_rules::sql_quality::models::{DeclaredKeyColumn, DeclaredRelation}; use crate::bindings::_helpers::compiled_project::schema_facts::python_column_facts; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::rules::lint_inputs::model_key_row; const MATERIALIZED_CONFIG: &str = "materialized"; @@ -58,7 +59,7 @@ pub(crate) fn relation_key_row( .downcast::()? .get_item(UNIQUE_AUDIT_COLUMN_ARGUMENT)?; audits.push(( - audit.getattr("definition_name")?.extract()?, + native_field(&audit, "definition_name")?.extract()?, argument .filter(pyo3::types::PyAnyMethods::is_instance_of::) .map(|value| value.extract()) @@ -149,7 +150,7 @@ pub(crate) fn relation_keys<'py>( for (kind, entries) in [(RELATION_KIND_SEED, seeds), (RELATION_KIND_SOURCE, sources)] { for entry in entries.try_iter()? { let entry: Bound<'py, PyAny> = entry?; - let name: String = entry.getattr("name")?.extract()?; + let name: String = native_field(&entry, "name")?.extract()?; rows.push(entry_relation_key_row( kind, name, @@ -192,16 +193,16 @@ fn declared_relation_keys<'py>( pub(crate) fn project_relation_keys(project: &Bound<'_, PyAny>) -> PyResult { let py: Python<'_> = project.py(); let mut rows: Vec = Vec::new(); - for model in project.getattr("models")?.try_iter()? { + for model in native_field(project, "models")?.try_iter()? { rows.push(model_key_row(&model?)?); } let mut seeds: Vec> = Vec::new(); - for seed in project.getattr("seeds")?.try_iter()? { - seeds.push(seed?.getattr("schema_entry")?); + for seed in native_field(project, "seeds")?.try_iter()? { + seeds.push(native_field(&seed?, "schema_entry")?); } let mut sources: Vec> = Vec::new(); - for source in project.getattr("sources")?.try_iter()? { - sources.push(source?.getattr("source_entry")?); + for source in native_field(project, "sources")?.try_iter()? { + sources.push(native_field(&source?, "source_entry")?); } relation_keys( py, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/results.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/results.rs index 9e5c883e7c..f4e41c3b2b 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/results.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/results.rs @@ -2,33 +2,34 @@ use pyo3::exceptions::PyOSError; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyErr, PyModule, PyModuleMethods, PyResult}; -use pyo3::types::{PyDict, PyDictMethods, PyString, PyTuple}; +use pyo3::types::{PyString, PyTuple}; use pyo3::{pyfunction, wrap_pyfunction}; +use std::path::PathBuf; + +use sqlbuild_core::text::main::python_splitlines::python_splitlines; use sqlbuild_rules::rule_results::main::duplicate_literal_hints::duplicate_literal_hints; use sqlbuild_rules::rule_results::main::format_result::format_result; use sqlbuild_rules::rule_results::main::format_rules_progress::format_rules_progress; use sqlbuild_rules::rule_results::main::format_skipped_rules::format_skipped_rules; use sqlbuild_rules::rule_results::models::{ResultCounts, RuleFinding}; -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::records::fields::field as native_field; +use crate::bindings::_helpers::records::fields::{field, path_posix, replaced}; -const DATACLASSES_MODULE: &str = "dataclasses"; const UTF8: &str = "utf-8"; pub(crate) fn rule_finding(finding: &Bound<'_, PyAny>) -> PyResult { Ok(RuleFinding { - code: finding.getattr("code")?.extract()?, - path: finding - .getattr("path")? - .call_method0("as_posix")? - .extract()?, - line: finding.getattr("line")?.extract()?, - column: finding.getattr("column")?.extract()?, - message: finding.getattr("message")?.extract()?, - remediation: finding.getattr("remediation")?.extract()?, - unevaluated: finding.getattr("unevaluated")?.is_truthy()?, - affected_rules: finding.getattr("affected_rules")?.extract()?, - fixable: finding.getattr("fixable")?.is_truthy()?, + code: field(finding, "code")?.extract()?, + path: path_posix(&field(finding, "path")?)?, + line: field(finding, "line")?.extract()?, + column: field(finding, "column")?.extract()?, + message: field(finding, "message")?.extract()?, + remediation: field(finding, "remediation")?.extract()?, + unevaluated: field(finding, "unevaluated")?.is_truthy()?, + affected_rules: field(finding, "affected_rules")?.extract()?, + fixable: field(finding, "fixable")?.is_truthy()?, }) } @@ -47,13 +48,13 @@ pub(crate) fn format_rules_result( json_output: bool, ) -> PyResult { let counts = ResultCounts { - evaluated_models: result.getattr("evaluated_models")?.extract()?, - unevaluated_resources: result.getattr("unevaluated_resources")?.extract()?, - cache_hits: result.getattr("cache_hits")?.extract()?, - cache_misses: result.getattr("cache_misses")?.extract()?, + evaluated_models: field(result, "evaluated_models")?.extract()?, + unevaluated_resources: field(result, "unevaluated_resources")?.extract()?, + cache_hits: field(result, "cache_hits")?.extract()?, + cache_misses: field(result, "cache_misses")?.extract()?, }; Ok(format_result( - &rule_findings(&result.getattr("findings")?)?, + &rule_findings(&field(result, "findings")?)?, counts, json_output, )) @@ -66,15 +67,15 @@ pub(crate) fn rules_progress( note_skipped_rules: bool, ) -> PyResult { let skipped: Option> = if note_skipped_rules { - Some(result.getattr("skipped_type_proof_rules")?.extract()?) + Some(field(result, "skipped_type_proof_rules")?.extract()?) } else { None }; Ok(format_rules_progress( elapsed_seconds, ( - result.getattr("built_in_ms")?.extract()?, - result.getattr("custom_ms")?.extract()?, + field(result, "built_in_ms")?.extract()?, + field(result, "custom_ms")?.extract()?, ), skipped.as_deref(), )) @@ -90,9 +91,19 @@ pub(crate) fn format_skipped_type_proof_rules(codes: Vec) -> Option, path: &str) -> PyResult> { let py = project_dir.py(); + let root: PathBuf = project_dir.extract()?; + let Ok(bytes) = std::fs::read(root.join(path)) else { + return Ok(Vec::new()); + }; + if let Ok(text) = std::str::from_utf8(&bytes) { + return Ok(python_splitlines(text) + .into_iter() + .map(str::to_owned) + .collect()); + } let file: Bound<'_, PyAny> = project_dir.div(path)?; let read: PyResult> = call_with( - &file.getattr("read_text")?, + &native_field(&file, "read_text")?, &[("encoding", PyString::new(py, UTF8).into_any())], ); match read { @@ -117,15 +128,13 @@ fn with_duplicate_literal_hints<'py>( .collect::>()?; let hinted: Vec> = duplicate_literal_hints::(&rows, |path| file_lines(project_dir, path))?; - let replace: Bound<'py, PyAny> = imported(py, DATACLASSES_MODULE, "replace")?; let mut output: Vec> = Vec::with_capacity(originals.len()); for (finding, remediation) in originals.into_iter().zip(hinted) { output.push(match remediation { - Some(remediation) => { - let keywords: Bound<'py, PyDict> = PyDict::new(py); - keywords.set_item("remediation", remediation)?; - replace.call((finding,), Some(&keywords))? - } + Some(remediation) => replaced( + &finding, + &[("remediation", PyString::new(py, &remediation).into_any())], + )?, None => finding, }); } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/rows.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/rows.rs index 0197843b48..bd58de2122 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/rows.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/rows.rs @@ -29,12 +29,16 @@ 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::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::fields::field; use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; -use crate::bindings::_helpers::rules::fact_replay::project_models; +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}; @@ -117,17 +121,25 @@ fn built_request(options: &Request<'_, '_>) -> PyResult { "config": config_value(&config)?, "scope_index": plain_value(&call_with( ®istry::native(py, NativeAttr::ScopeMetadataProjection)?, - &[("index", project.getattr("scope_index")?)], + &[("index", project_attribute(&project, "scope_index")?)], )?)?, "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))?, }); let (public_enums, public_constants) = declaration_rows( - PyTuple::new(py, mapping_values(&project.getattr("public_enums")?)?)?.as_any(), - PyTuple::new(py, mapping_values(&project.getattr("public_constants")?)?)?.as_any(), + PyTuple::new( + py, + mapping_values(&project_attribute(&project, "public_enums")?)?, + )? + .as_any(), + PyTuple::new( + py, + mapping_values(&project_attribute(&project, "public_constants")?)?, + )? + .as_any(), )?; - let enabled: bool = config.getattr("cache")?.getattr("enabled")?.is_truthy()?; + let enabled: bool = field(&field(&config, "cache")?, "enabled")?.is_truthy()?; let memo: Option<(String, String)> = if enabled { let unresolved: PathBuf = project_dir.extract()?; Some(( @@ -141,17 +153,17 @@ fn built_request(options: &Request<'_, '_>) -> PyResult { None }; let mut attached_audit_targets: Vec = Vec::new(); - for audit in project.getattr("audits")?.try_iter()? { - let target: Bound<'_, PyAny> = audit?.getattr("attached_target_name")?; - if !target.is_none() { - attached_audit_targets.push(target.extract()?); - } + for audit in project_attribute(&project, "audits")?.try_iter()? { + let audit: Bound<'_, PyAny> = audit?; + let target: Option = match audit.cast::() { + Ok(view) => view.get().attached_target_name(py)?, + Err(_) => field(&audit, "attached_target_name")?.extract()?, + }; + attached_audit_targets.extend(target); } let mut rows = ProjectRows { memo, - sql_analysis_enabled: project - .getattr("settings")? - .getattr("sql_analysis")? + sql_analysis_enabled: field(&project_attribute(&project, "settings")?, "sql_analysis")? .is_truthy()?, include_type_proof: options.get("include_type_proof")?.is_truthy()?, attached_audit_targets, @@ -163,20 +175,15 @@ fn built_request(options: &Request<'_, '_>) -> PyResult { .collect::>()?, dialect, }; - let facts: Bound<'_, NativeCompiledProject> = - registry::native(py, NativeAttr::CompiledProjectFacts)? - .call1((&project,))? - .downcast_into()?; + let facts: Bound<'_, NativeCompiledProject> = compiled_project_facts(&project)?; let names: Vec = project_models(&project)? .into_iter() .map(|model| model.name) .collect(); - let sql_tests: Vec> = project - .getattr("sql_tests")? + let sql_tests: Vec> = project_attribute(&project, "sql_tests")? .try_iter()? .collect::>()?; - let scenarios: Vec> = project - .getattr("sql_scenarios")? + let scenarios: Vec> = project_attribute(&project, "sql_scenarios")? .try_iter()? .collect::>()?; compiler_guard(|| { @@ -298,7 +305,7 @@ fn finalized<'py>( let mut faults: Vec = Vec::with_capacity(findings.len()); for finding in &findings { let fault: Fault = finding_fault(finding)?; - if finding.getattr("fixable")?.extract::()? { + if field(finding, "fixable")?.extract::()? { fixable.insert(fault_identity(&fault)); } faults.push(fault); @@ -319,6 +326,9 @@ fn finalized<'py>( /// `asdict(config)` as orjson with `default=str` encodes it. fn config_value(config: &Bound<'_, PyAny>) -> PyResult { + if let Some(value) = asdict_json(config)? { + return Ok(value); + } plain_value(&imported(config.py(), DATACLASSES_MODULE, "asdict")?.call1((config,))?) } 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 cfcc0e6bc1..a5515d7d8e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/rule_cache.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/rule_cache.rs @@ -24,6 +24,7 @@ use sqlbuild_rules::rule_cache::models::{CacheLookup, CachedEntry, LintKeyInput, use sqlbuild_rules::rule_cache::types::CachedEntries; use sqlbuild_rules::rule_results::models::RuleFinding; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::rules::lint_pipeline::LintSettings; /// Stored entries as `{path: (key, findings JSON)}`. @@ -226,14 +227,14 @@ fn finding_payload(finding: &Bound<'_, PyAny>) -> PyResult { .call_method0("as_posix")? .extract()?; Ok(json!({ - "unevaluated": finding.getattr("unevaluated")?.extract::()?, - "code": finding.getattr("code")?.extract::()?, + "unevaluated": native_field(finding, "unevaluated")?.extract::()?, + "code": native_field(finding, "code")?.extract::()?, "path": path, - "line": finding.getattr("line")?.extract::>()?, - "column": finding.getattr("column")?.extract::>()?, - "message": finding.getattr("message")?.extract::()?, - "remediation": finding.getattr("remediation")?.extract::>()?, - "fixable": finding.getattr("fixable")?.extract::()?, + "line": native_field(finding, "line")?.extract::>()?, + "column": native_field(finding, "column")?.extract::>()?, + "message": native_field(finding, "message")?.extract::()?, + "remediation": native_field(finding, "remediation")?.extract::>()?, + "fixable": native_field(finding, "fixable")?.extract::()?, })) } 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 37f2cbce91..73ef523fab 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/rule_dispatch.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/rule_dispatch.rs @@ -1,6 +1,6 @@ //! Run built-in, SQL and custom rule phases on their threads and combine their findings. -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::sync::Mutex; use std::sync::atomic::Ordering; use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender, channel}; @@ -11,21 +11,32 @@ use pyo3::exceptions::{PyKeyboardInterrupt, PyRuntimeError}; use pyo3::prelude::{ Bound, Py, PyAny, PyAnyMethods, PyErr, PyModule, PyModuleMethods, PyResult, Python, }; -use pyo3::types::{PyDict, PyFrozenSet, PyTuple}; +use pyo3::types::{PyDict, PyTuple}; use pyo3::{IntoPyObjectExt, pyfunction, wrap_pyfunction}; use crate::bindings::_helpers::cache::native_store::current_code_identity; -use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; +use crate::bindings::_helpers::compile_driver::python_calls::{ + KeywordArguments, call_with, keywords, +}; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::native_host::user_code::UserCodeHost; +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::rules::catalogue::{ + resolved_ignore_selectors, rule_catalogue, select_rules, +}; use crate::bindings::_helpers::rules::constants::TYPE_PROOF_RULE_CODES; use crate::bindings::_helpers::rules::custom_evaluation::{ CustomRun, evaluate_custom_rules_cached, outcome, }; 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::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}; +use crate::bindings::_helpers::rules::sql_rules::run_planned_sql_rules; const CUSTOM_THREAD: &str = "sqlbuild-custom-rules"; const GENERIC_DIALECT: &str = "generic"; @@ -121,32 +132,18 @@ fn evaluate_project_rules<'py>( let project_dir: Bound<'py, PyAny> = arguments.value("project_dir")?; let dialect: String = dialect_argument(&arguments)?; let verify_determinism: bool = arguments.flag("verify_determinism")?; - let effective: Bound<'py, PyAny> = call_with( - &native(py, NativeAttr::ResolveRuleIgnoreSelectors)?, - &[("config", config), ("project", project.clone())], - )?; - let catalogue: Bound<'py, PyAny> = call_with( - &native(py, NativeAttr::BuildRuleCatalogue)?, - &[ - ("config", effective.clone()), - ("project_dir", project_dir.clone()), - ], - )?; + let effective: Bound<'py, PyAny> = resolved_ignore_selectors(&config, &project)?; + let rules: Vec> = rule_catalogue(&effective, &project_dir, true)?; let mut custom: bool = false; - for rule in catalogue.try_iter()? { - custom = custom || rule?.getattr("custom")?.is_truthy()?; + for rule in &rules { + custom = custom || field(rule, "custom")?.is_truthy()?; } if custom { - let rules: Bound<'py, PyAny> = call_with( - &native(py, NativeAttr::SelectCatalogueRules)?, - &[ - ("catalogue", catalogue.clone()), - ("config", effective.clone()), - ("project_dir", project_dir.clone()), - ], - )?; - verify_custom_rules(&rules, &project_dir)?; + let selected: Vec> = + select_rules(&rules, &effective, &project_dir, None)?; + verify_custom_rules(PyTuple::new(py, selected)?.as_any(), &project_dir)?; } + let catalogue: Bound<'py, PyAny> = PyTuple::new(py, rules)?.into_any(); let run: NativeRun<'py> = NativeRun { project, config: effective, @@ -232,8 +229,8 @@ pub(crate) fn evaluate_rules<'py>( KeywordArguments::read(py, "evaluate_rules", options, EVALUATE_RULES_OPTIONS)?; let graph: Bound<'py, PyAny> = arguments.value("graph")?; let dialect: Bound<'py, PyAny> = arguments.value("dialect")?; - let plan: Bound<'py, PyAny> = call_with( - &native(py, NativeAttr::PlanRuleRun)?, + let plan: Bound<'py, PyAny> = plan_rule_run(Some(&keywords( + py, &[ ("graph", graph.clone()), ("config", arguments.value("config")?), @@ -244,18 +241,18 @@ pub(crate) fn evaluate_rules<'py>( arguments.value_or_false("no_sql_analysis")?, ), ], - )?; + )?))?; let run: NativeRun<'py> = NativeRun { - project: plan.getattr("project")?, - config: plan.getattr("config")?, - project_dir: plan.getattr("project_dir")?, - catalogue: plan.getattr("catalogue")?, + project: field(&plan, "project")?, + config: field(&plan, "config")?, + project_dir: field(&plan, "project_dir")?, + catalogue: field(&plan, "catalogue")?, dialect: dialect.extract()?, verify_determinism: false, initial_findings: None, cancel: HostCancel::default(), }; - let payloads: Bound<'py, PyAny> = plan.getattr("custom_payloads")?; + let payloads: Bound<'py, PyAny> = field(&plan, "custom_payloads")?; let custom: Option = start_custom_rules(&run, &payloads)?; let native_run: [Py; 5] = [ run.project.clone().unbind(), @@ -287,8 +284,8 @@ pub(crate) fn evaluate_rules<'py>( .map(Bound::unbind) }) })?; - let sql_result: PyResult> = call_with( - &native(py, NativeAttr::RunPlannedSqlRules)?, + let sql_result: PyResult> = run_planned_sql_rules(Some(&keywords( + py, &[ ("plan", plan.clone()), ("graph", graph), @@ -296,19 +293,12 @@ pub(crate) fn evaluate_rules<'py>( ("dialect", dialect), ("early_lint", arguments.value("early_lint")?), ], - ); + )?)); cancel_on_interrupt(py, &sql_result, &run.cancel); let result: PyResult> = joined(py, native_thread, &run.cancel); let sql_result: Bound<'py, PyAny> = sql_result?; let result: Bound<'py, PyAny> = result?; - call_with( - &native(py, NativeAttr::FinishRuleRun)?, - &[ - ("plan", plan), - ("sql_result", sql_result), - ("result", result), - ], - ) + finish_rule_run(&plan, &sql_result, &result) } /// Evaluate built-in rules natively while custom rules run beside them. @@ -341,38 +331,33 @@ fn evaluate_native<'py>( }; let native_result: Bound<'py, PyAny> = evaluated?; let custom: Bound<'py, PyAny> = custom?; - let mut findings: Bound<'py, PyAny> = PyTuple::new( - py, - native_result - .getattr("findings")? - .try_iter()? - .chain(custom.getattr("findings")?.try_iter()?) - .collect::>>()?, - )? - .into_any(); - if !defer_suppressions { - findings = call_with( - &native(py, NativeAttr::FinalizeRuleFindingsRows)?, - &[ - ("config", run.config.clone()), - ("project_dir", run.project_dir.clone()), - ("evaluated_codes", native_result.getattr("selected_codes")?), - ("findings", findings), - ], - )?; - } - let reused: bool = native_result.getattr("reused")?.is_truthy()?; - let native_hits: i64 = native_result.getattr("cache_hits")?.extract()?; - let native_misses: i64 = native_result.getattr("cache_misses")?.extract()?; - let custom_hits: i64 = custom.getattr("cache_hits")?.extract()?; - let custom_misses: i64 = custom.getattr("cache_misses")?.extract()?; + let combined: Vec> = field(&native_result, "findings")? + .try_iter()? + .chain(field(&custom, "findings")?.try_iter()?) + .collect::>>()?; + let findings: Bound<'py, PyAny> = if defer_suppressions { + PyTuple::new(py, combined)?.into_any() + } else { + finalize_rule_findings_rows( + &run.config, + &run.project_dir, + field(&native_result, "selected_codes")?.extract()?, + combined, + )? + .into_any() + }; + let reused: bool = field(&native_result, "reused")?.is_truthy()?; + 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)?, &[ ("findings", findings), ( "evaluated_models", - native_result.getattr("evaluated_models")?, + field(&native_result, "evaluated_models")?, ), ( "cache_hits", @@ -392,11 +377,11 @@ fn evaluate_native<'py>( if reused { 0.into_bound_py_any(py)? } else { - native_result.getattr("built_in_ms")? + field(&native_result, "built_in_ms")? }, ), - ("custom_ms", custom.getattr("custom_ms")?), - ("custom_cpu_ms", custom.getattr("custom_cpu_ms")?), + ("custom_ms", field(&custom, "custom_ms")?), + ("custom_cpu_ms", field(&custom, "custom_cpu_ms")?), ], ) } @@ -419,15 +404,7 @@ fn native_request<'py>( }; let mut include_type_proof: bool = false; for code in TYPE_PROOF_RULE_CODES { - include_type_proof = include_type_proof - || call_with( - &native(py, NativeAttr::RuleSelected)?, - &[ - ("config", run.config.clone()), - ("code", pyo3::types::PyString::new(py, code).into_any()), - ], - )? - .is_truthy()?; + include_type_proof = include_type_proof || rule_selected(&run.config, code)?; } let request: Bound<'py, PyAny> = call_with( &native(py, NativeAttr::BuildRulesRequest)?, @@ -461,34 +438,22 @@ fn start_custom_rules( if !payloads.is_truthy()? { return Ok(None); } - let selected: Bound<'_, PyFrozenSet> = PyFrozenSet::new( - py, - call_with( - &native(py, NativeAttr::SelectedRuleCodes)?, - &[ - ("config", run.config.clone()), - ("custom_payloads", payloads.clone()), - ], - )? - .try_iter()? - .collect::>>()?, - )?; + let selected: HashSet = selected_rule_codes(&run.config, payloads)? + .into_iter() + .collect(); let mut rules: Vec> = Vec::new(); for rule in run.catalogue.try_iter()? { let rule: Bound<'_, PyAny> = rule?; - if rule.getattr("custom")?.is_truthy()? && selected.contains(rule.getattr("code")?)? { + if field(&rule, "custom")?.is_truthy()? + && selected.contains(&field(&rule, "code")?.extract::()?) + { rules.push(rule.unbind()); } } if rules.is_empty() { return Ok(None); } - if run - .config - .getattr("cache")? - .getattr("enabled")? - .is_truthy()? - { + if field(&field(&run.config, "cache")?, "enabled")?.is_truthy()? { let _identity: String = current_code_identity(py)?; } let mut fingerprints: HashMap = HashMap::new(); 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 9633425702..8c71f4a136 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/rule_plan.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/rule_plan.rs @@ -3,11 +3,14 @@ use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; use pyo3::types::{PyDict, PyFrozenSet, PySet, PySetMethods, PyTuple}; use pyo3::{pyfunction, wrap_pyfunction}; +use sqlbuild_rules::project_tree::main::pure_path_posix::pure_path_posix; +use std::path::Path; 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::call_with; use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::native_host::user_code::UserCodeHost; +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::rules::catalogue::{ @@ -15,21 +18,21 @@ use crate::bindings::_helpers::rules::catalogue::{ verify_custom_rules, }; use crate::bindings::_helpers::rules::constants::TYPE_PROOF_RULE_CODES; -use crate::bindings::_helpers::rules::fact_replay::project_models; +use crate::bindings::_helpers::rules::fact_replay::{project_attribute, project_models}; use crate::bindings::_helpers::rules::rule_run::freed_memory_released; -const COMPILE_TYPES_MODULE: &str = "sqlbuild.compiler.compile.types"; -const DATACLASSES_MODULE: &str = "dataclasses"; -const PATHLIB_MODULE: &str = "pathlib"; +const MODEL_RESOURCE_TYPE: &str = "model"; /// Build the catalogue and select the rules, project slice and settings one run evaluates. #[pyfunction] #[pyo3(signature = (**options))] -fn plan_rule_run<'py>(options: Option<&Bound<'py, PyDict>>) -> PyResult> { +pub(crate) fn plan_rule_run<'py>( + options: Option<&Bound<'py, PyDict>>, +) -> PyResult> { let options = Request::new("plan_rule_run", options)?; let py: Python<'py> = options.py(); let graph: Bound<'py, PyAny> = options.get("graph")?; - let project: Bound<'py, PyAny> = graph.getattr("project")?; + let project: Bound<'py, PyAny> = field(&graph, "project")?; freed_memory_released(py); let effective_config: Bound<'py, PyAny> = resolved_ignore_selectors(&options.get("config")?, &project)?; @@ -53,14 +56,11 @@ fn plan_rule_run<'py>(options: Option<&Bound<'py, PyDict>>) -> PyResult> = Vec::new(); if options.get("no_sql_analysis")?.is_truthy()? - || !project - .getattr("settings")? - .getattr("sql_analysis")? - .is_truthy()? + || !field(&project_attribute(&project, "settings")?, "sql_analysis")?.is_truthy()? { let mut kept: Vec> = Vec::with_capacity(selected.len()); for rule in selected { - let code: Bound<'py, PyAny> = rule.getattr("code")?; + let code: Bound<'py, PyAny> = field(&rule, "code")?; if TYPE_PROOF_RULE_CODES.contains(&code.extract::()?.as_str()) { skipped_type_proof_rules.push(code); } else { @@ -71,7 +71,7 @@ fn plan_rule_run<'py>(options: Option<&Bound<'py, PyDict>>) -> PyResult> = Vec::new(); for rule in &selected { - if rule.getattr("custom")?.is_truthy()? { + if field(rule, "custom")?.is_truthy()? { custom_rules.push(rule.clone()); } } @@ -86,19 +86,16 @@ fn plan_rule_run<'py>(options: Option<&Bound<'py, PyDict>>) -> PyResult> = Vec::with_capacity(selected.len()); for rule in &selected { - codes.push(rule.getattr("code")?); + codes.push(field(rule, "code")?); } let selection_config: Bound<'py, PyAny> = selection_rules_config(&effective_config, model_paths.as_ref(), &selected_project)?; - let selected_config: Bound<'py, PyAny> = imported(py, DATACLASSES_MODULE, "replace")?.call( - (selection_config,), - Some(&keywords( - py, - &[ - ("select", PyTuple::new(py, codes)?.into_any()), - ("ignore", PyTuple::empty(py).into_any()), - ], - )?), + let selected_config: Bound<'py, PyAny> = replaced( + &selection_config, + &[ + ("select", PyTuple::new(py, codes)?.into_any()), + ("ignore", PyTuple::empty(py).into_any()), + ], )?; call_with( ®istry::class(py, RecordId::RuleRunPlan)?, @@ -131,17 +128,15 @@ fn selected_model_paths<'py>( if selected_keys.is_none() { return Ok(None); } - let model_type: Bound<'py, PyAny> = - imported(py, COMPILE_TYPES_MODULE, "CompiledResourceType")?.getattr("MODEL")?; let names: Bound<'py, PySet> = PySet::empty(py)?; for key in selected_keys.try_iter()? { let key: Bound<'py, PyAny> = key?; - if key.getattr("resource_type")?.is(&model_type) { - names.add(key.getattr("name")?)?; + if field(&key, "resource_type")?.eq(MODEL_RESOURCE_TYPE)? { + names.add(field(&key, "name")?)?; } } let paths: Bound<'py, PySet> = PySet::empty(py)?; - for model in project_models(&graph.getattr("project")?)? { + for model in project_models(&field(graph, "project")?)? { if names.contains(&model.name)? { paths.add(model.posix)?; } @@ -180,43 +175,38 @@ fn selection_rules_config<'py>( let Some(model_paths) = model_paths else { return Ok(config.clone()); }; - let path_class: Bound<'py, PyAny> = imported(py, PATHLIB_MODULE, "Path")?; let model_names: Bound<'py, PySet> = PySet::empty(py)?; for model in project_models(project)? { model_names.add(model.name)?; } let mut rule_exceptions: Vec> = Vec::new(); - for entry in config.getattr("rule_exceptions")?.try_iter()? { + for entry in field(config, "rule_exceptions")?.try_iter()? { let entry: Bound<'py, PyAny> = entry?; - let path: Bound<'py, PyAny> = path_class - .call1((entry.getattr("path")?,))? - .call_method0("as_posix")?; + let path: String = field(&entry, "path")?.extract()?; + let path: String = String::from_utf8_lossy(&pure_path_posix(Path::new(&path))).into_owned(); if model_paths.contains(path)? { rule_exceptions.push(entry); } } let mut graph_edge_exceptions: Vec> = Vec::new(); - for entry in config.getattr("graph_edge_exceptions")?.try_iter()? { + for entry in field(config, "graph_edge_exceptions")?.try_iter()? { let entry: Bound<'py, PyAny> = entry?; - if model_names.contains(entry.getattr("consumer")?)? { + if model_names.contains(field(&entry, "consumer")?)? { graph_edge_exceptions.push(entry); } } - imported(py, DATACLASSES_MODULE, "replace")?.call( - (config,), - Some(&keywords( - py, - &[ - ( - "rule_exceptions", - PyTuple::new(py, rule_exceptions)?.into_any(), - ), - ( - "graph_edge_exceptions", - PyTuple::new(py, graph_edge_exceptions)?.into_any(), - ), - ], - )?), + replaced( + config, + &[ + ( + "rule_exceptions", + PyTuple::new(py, rule_exceptions)?.into_any(), + ), + ( + "graph_edge_exceptions", + PyTuple::new(py, graph_edge_exceptions)?.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 230c255033..ab494cc30c 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/rule_run.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/rule_run.rs @@ -12,6 +12,8 @@ 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::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; @@ -22,7 +24,7 @@ const PATHLIB_MODULE: &str = "pathlib"; const CTYPES_MODULE: &str = "ctypes"; fn integer(object: &Bound<'_, PyAny>, field: &str) -> PyResult { - object.getattr(field)?.extract() + record_field(object, field)?.extract() } fn python_finding<'py>(py: Python<'py>, finding: RuleFinding) -> PyResult> { @@ -51,23 +53,26 @@ fn python_finding<'py>(py: Python<'py>, finding: RuleFinding) -> PyResult( +pub(crate) fn finish_rule_run<'py>( plan: &Bound<'py, PyAny>, sql_result: &Bound<'py, PyAny>, result: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = plan.py(); let mut evaluated_codes: Vec = Vec::new(); - for rule in plan.getattr("selected")?.try_iter()? { - evaluated_codes.push(rule?.getattr("code")?.extract()?); + for rule in record_field(plan, "selected")?.try_iter()? { + evaluated_codes.push(record_field(&rule?, "code")?.extract()?); } let mut combined: Vec> = Vec::new(); - for findings in [sql_result.getattr("findings")?, result.getattr("findings")?] { + for findings in [ + record_field(sql_result, "findings")?, + record_field(result, "findings")?, + ] { combined.extend(findings.try_iter()?.collect::>>()?); } let finalized: Vec = finalize_rule_findings_rows( - &plan.getattr("config")?, - &plan.getattr("project_dir")?, + &record_field(plan, "config")?, + &record_field(plan, "project_dir")?, evaluated_codes, combined, )? @@ -81,7 +86,7 @@ fn finish_rule_run<'py>( .map(|finding| finding.path.clone()) .collect(); let mut unevaluated_models: i64 = 0; - for model in project_models(&plan.getattr("project")?)? { + for model in project_models(&record_field(plan, "project")?)? { unevaluated_models += i64::from(unevaluated.contains(&model.posix)); } let findings: Vec> = grouped @@ -110,13 +115,13 @@ fn finish_rule_run<'py>( (integer(sql_result, "sql_ms")? + integer(result, "built_in_ms")?) .into_bound_py_any(py)?, ), - ("custom_ms", result.getattr("custom_ms")?), - ("custom_cpu_ms", result.getattr("custom_cpu_ms")?), + ("custom_ms", record_field(result, "custom_ms")?), + ("custom_cpu_ms", record_field(result, "custom_cpu_ms")?), ("cache_hits", total("cache_hits")?), ("cache_misses", total("cache_misses")?), ( "skipped_type_proof_rules", - plan.getattr("skipped_type_proof_rules")?, + record_field(plan, "skipped_type_proof_rules")?, ), ], ) @@ -146,7 +151,7 @@ pub(crate) fn freed_memory_released(py: Python<'_>) { } let trim = || -> PyResult<()> { let process: Bound<'_, PyAny> = imported(py, CTYPES_MODULE, "CDLL")?.call1((py.None(),))?; - let _ = process.getattr("malloc_trim")?.call1((0,))?; + let _ = native_field(&process, "malloc_trim")?.call1((0,))?; Ok(()) }; let _ = trim(); diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/selection.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/selection.rs index 9243c00e77..205764a1c6 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/selection.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/selection.rs @@ -9,8 +9,10 @@ use pyo3::{pyfunction, wrap_pyfunction}; use sqlbuild_rules::rules::main::selected_codes::selected_codes_json; use crate::bindings::_helpers::boundary::panics::compiler_guard; +use crate::bindings::_helpers::records::fields::field as record_field; use crate::bindings::_helpers::records::ids::ErrorId; use crate::bindings::_helpers::records::registry; +use crate::bindings::_helpers::rules::config_json::{asdict_json, dumped_json}; use crate::bindings::_helpers::rules::constants::RULES_NATIVE_API_VERSION; const DATACLASSES_MODULE: &str = "dataclasses"; @@ -25,6 +27,16 @@ pub(crate) fn selected_rule_codes( custom_payloads: &Bound<'_, PyAny>, ) -> PyResult> { let py: Python<'_> = config.py(); + if let (Some(config), Some(custom_rules)) = + (asdict_json(config)?, dumped_json(custom_payloads)?) + { + let request: serde_json::Value = serde_json::json!({ + "config": config, + "custom_rules": custom_rules, + "version": RULES_NATIVE_API_VERSION, + }); + return selected_codes(py, &request.to_string()); + } let request: Bound<'_, PyDict> = PyDict::new(py); request.set_item("version", RULES_NATIVE_API_VERSION)?; request.set_item( @@ -52,7 +64,12 @@ pub(crate) fn selected_rule_codes( } Err(error) => return Err(error), }; - let selected: Result = compiler_guard(|| Ok(selected_codes_json(&encoded)))?; + selected_codes(py, &encoded) +} + +/// The rule codes the native engine selects for one encoded selection request. +fn selected_codes(py: Python<'_>, encoded: &str) -> PyResult> { + let selected: Result = compiler_guard(|| Ok(selected_codes_json(encoded)))?; let codes: String = selected.map_err(|message| rules_error(py, &message, None))?; serde_json::from_str::>(&codes).map_err(|_| { rules_error( @@ -66,9 +83,9 @@ pub(crate) fn selected_rule_codes( /// Whether `code` is selected and not ignored by the configuration's prefixes. #[pyfunction] #[pyo3(signature = (*, config, code))] -fn rule_selected(config: &Bound<'_, PyAny>, code: &str) -> PyResult { +pub(crate) fn rule_selected(config: &Bound<'_, PyAny>, code: &str) -> PyResult { let matches = |field: &str| -> PyResult { - for selector in config.getattr(field)?.try_iter()? { + for selector in record_field(config, field)?.try_iter()? { let selector: String = selector?.extract()?; if code.starts_with(&selector) { return Ok(true); 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 3fe77192ba..0d996c3266 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/sql_rules.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/sql_rules.rs @@ -9,7 +9,7 @@ use pyo3::prelude::{ Bound, IntoPyObject, PyAny, PyAnyMethods, PyErr, PyModule, PyModuleMethods, PyRef, PyResult, Python, }; -use pyo3::types::{PyBytes, PyDict, PyString, PyTuple}; +use pyo3::types::{PyBytes, PyDict, PyDictMethods, PyString, PyTuple}; use pyo3::{IntoPyObjectExt, pyfunction, wrap_pyfunction}; use sqlbuild_core::text::main::python_splitlines::python_splitlines; use sqlbuild_rules::rule_cache::main::file_rule_key::file_rule_key; @@ -19,12 +19,18 @@ 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::compiled_project::compiled_model_view::{ + NativeCompiledModel, SqlRuleInputs, SqlRuleKey, +}; +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}; use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::rules::constants::SAFE_RULE_FIX_CODES; use crate::bindings::_helpers::rules::early_sql_lint::{ EarlyLintResult, EarlySqlLint, PreparedSqlLint, sql_lint_settings, sql_rule_cache_path, }; +use crate::bindings::_helpers::rules::fact_replay::project_attribute; use crate::bindings::_helpers::rules::lint_expansion::{ LintExpansionSources, lint_expansion_inputs, }; @@ -52,7 +58,7 @@ const UTF8: &str = "utf-8"; fn sql_rule_codes(rules: &[Bound<'_, PyAny>]) -> PyResult> { let mut codes: Vec = Vec::new(); for rule in rules { - let code: String = rule.getattr("code")?.extract()?; + let code: String = field(rule, "code")?.extract()?; if code.starts_with(SQL_RULE_PREFIX) { codes.push(code); } @@ -92,43 +98,46 @@ fn posix(path: &Path) -> String { /// Run the planned SQL rules on the caller while built-in rules evaluate natively. #[pyfunction] #[pyo3(signature = (**options))] -fn run_planned_sql_rules<'py>(options: Option<&Bound<'py, PyDict>>) -> PyResult> { +pub(crate) fn run_planned_sql_rules<'py>( + options: Option<&Bound<'py, PyDict>>, +) -> PyResult> { let options = Request::new("run_planned_sql_rules", options)?; let py: Python<'py> = options.py(); let plan: Bound<'py, PyAny> = options.get("plan")?; let graph: Bound<'py, PyAny> = options.get("graph")?; - let effective_config: Bound<'py, PyAny> = plan.getattr("effective_config")?; + let effective_config: Bound<'py, PyAny> = field(&plan, "effective_config")?; let mut native_rules: Vec> = Vec::new(); - for rule in plan.getattr("selected")?.try_iter()? { + for rule in field(&plan, "selected")?.try_iter()? { let rule: Bound<'py, PyAny> = rule?; - if !rule.getattr("custom")?.is_truthy()? { + if !field(&rule, "custom")?.is_truthy()? { native_rules.push(rule); } } let codes: Vec = sql_rule_codes(&native_rules)?; - let thresholds: Bound<'py, PyAny> = effective_config.getattr("thresholds")?; + let thresholds: Bound<'py, PyDict> = + field(&effective_config, "thresholds")?.cast_into::()?; let dialect: String = options.get("dialect")?.extract()?; let settings: LintSettings = sql_lint_settings( &dialect, &codes, - |key| thresholds.call_method1("get", (key,))?.extract(), + |key| match thresholds.get_item(key)? { + Some(value) => value.extract(), + None => Ok(None), + }, ( - project_relation_keys(&graph.getattr("project")?)?, - effective_config.getattr("allow_exceptions")?.is_truthy()?, + project_relation_keys(&field(&graph, "project")?)?, + field(&effective_config, "allow_exceptions")?.is_truthy()?, ), )?; let early_lint: Bound<'py, PyAny> = options.get("early_lint")?; let run = SqlRun { codes: &codes, - project_dir: plan.getattr("project_dir")?.extract()?, + project_dir: field(&plan, "project_dir")?.extract()?, discovered_inputs: options.get("discovered_inputs")?, - project: plan.getattr("project")?, + project: field(&plan, "project")?, settings, - model_selection: !plan.getattr("model_paths")?.is_none(), - cache_enabled: effective_config - .getattr("cache")? - .getattr("enabled")? - .is_truthy()?, + model_selection: !field(&plan, "model_paths")?.is_none(), + cache_enabled: field(&field(&effective_config, "cache")?, "enabled")?.is_truthy()?, early_lint: if early_lint.is_none() { None } else { @@ -186,52 +195,103 @@ fn finding_objects(py: Python<'_>, findings: Vec) -> PyResult { + path: String, + key: Option, + model: Bound<'py, PyAny>, +} + /// The project's models SQL rules analyse, keyed by relative path in project order. -fn sql_rule_models<'py>(project: &Bound<'py, PyAny>) -> PyResult)>> { - let mut models: Vec<(String, Bound<'py, PyAny>)> = Vec::new(); - if !project - .getattr("settings")? - .getattr("sql_analysis")? - .is_truthy()? - { +fn sql_rule_models<'py>( + project: &Bound<'py, PyAny>, + keyed: bool, +) -> PyResult>> { + let mut models: Vec> = Vec::new(); + if !field(&project_attribute(project, "settings")?, "sql_analysis")?.is_truthy()? { return Ok(models); } - let excluded: Bound<'py, PyAny> = false.into_bound_py_any(project.py())?; - for model in project.getattr("models")?.try_iter()? { - let model: Bound<'py, PyAny> = model?; - let analysis: Bound<'py, PyAny> = model - .getattr("config")? - .getattr("values")? - .call_method1("get", ("sql_analysis",))?; - if analysis.is(&excluded) { + for model in project_attribute(project, "models")?.try_iter()? { + let Some(entry) = sql_rule_model(model?, keyed)? else { continue; - } - let path: String = model - .getattr("relative_path")? - .call_method0("as_posix")? - .extract()?; - if let Some(index) = models.iter().position(|(known, _)| *known == path) { - models[index].1 = model; + }; + if let Some(index) = models.iter().position(|known| known.path == entry.path) { + models[index] = entry; } else { - models.push((path, model)); + models.push(entry); } } Ok(models) } +/// The model's SQL rule entry, or `None` when its config opts out of SQL analysis. +fn sql_rule_model(model: Bound<'_, PyAny>, keyed: bool) -> PyResult>> { + let py: Python<'_> = model.py(); + if let Ok(view) = model.downcast::() { + let inputs: SqlRuleInputs = view.get().sql_rule_inputs(py, keyed)?; + let excluded: bool = match inputs.excluded { + Some(excluded) => excluded, + None => python_excluded(&model)?, + }; + if excluded { + return Ok(None); + } + return Ok(Some(SqlRuleModel { + path: inputs.posix_path, + key: inputs.key, + model, + })); + } + if python_excluded(&model)? { + return Ok(None); + } + let key: Option = keyed + .then(|| -> PyResult { + Ok(SqlRuleKey { + authored_sql: native_field(&model, "authored_sql")?.extract()?, + query_sql: native_field(&model, "query_sql")?.extract()?, + contract: contract_text(&model)?, + }) + }) + .transpose()?; + Ok(Some(SqlRuleModel { + path: model_posix_path(&model)?, + key, + model, + })) +} + +/// The model's relative path as POSIX text; a native view answers from its recorded facts. +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")? + .call_method0("as_posix")? + .extract() +} + +/// 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")? + .call_method1("get", ("sql_analysis",))? + .is(&excluded)) +} + /// Each analysed model's SQL rule cache key, in project order. fn model_rule_identities(run: &SqlRun<'_, '_>, lint_key: &str) -> PyResult> { if run.codes.is_empty() { return Ok(Vec::new()); } let mut models: Vec<(String, String, String, Option)> = Vec::new(); - for (path, model) in sql_rule_models(&run.project)? { - models.push(( - path, - model.getattr("authored_sql")?.extract()?, - model.getattr("query_sql")?.extract()?, - contract_text(&model)?, - )); + for model in sql_rule_models(&run.project, true)? { + if let Some(key) = model.key { + models.push((model.path, key.authored_sql, key.query_sql, key.contract)); + } } let keys: BTreeMap = model_rule_keys(lint_key, run.codes, &models); Ok(models @@ -243,10 +303,10 @@ fn model_rule_identities(run: &SqlRun<'_, '_>, lint_key: &str) -> PyResult) -> PyResult> { let py: Python<'_> = model.py(); - let contract: Bound<'_, PyAny> = model.getattr("dynamic_column_contract")?; + let contract: Bound<'_, PyAny> = native_field(model, "dynamic_column_contract")?; if contract.is_none() { return Ok(None); } @@ -289,7 +349,7 @@ fn evaluated_sql_rules( return Ok((Vec::new(), 0, 0)); } let project_dir: &Path = &run.project_dir; - let models: Vec<(String, Bound<'_, PyAny>)> = sql_rule_models(&run.project)?; + let models: Vec> = sql_rule_models(&run.project, false)?; let cache_path: PathBuf = sql_rule_cache_path(project_dir); let mut bucket: StoredEntries = if run.cache_enabled { stored_entries(&cache_path)? @@ -315,9 +375,8 @@ fn evaluated_sql_rules( let compiled: HashMap = project_sql_expansions(&run.project)?; if !run.model_selection { let mut model_paths: HashSet = HashSet::new(); - for model in run.project.getattr("models")?.try_iter()? { - model_paths - .insert(project_dir.join(model?.getattr("relative_path")?.extract::()?)); + for model in project_attribute(&run.project, "models")?.try_iter()? { + model_paths.insert(project_dir.join(model_posix_path(&model?)?)); } let files: Vec = match prepared_result(run, py) { Some(Ok(result)) => result.outcome.files.clone(), @@ -371,16 +430,14 @@ fn evaluated_sql_rules( Vec::new() } else { let mut dynamic: HashSet = HashSet::new(); - for (_, model) in &models { - if output_proven(model)? { - dynamic.insert(resolved( - &project_dir.join(model.getattr("relative_path")?.extract::()?), - )); + for model in &models { + if output_proven(&model.model)? { + dynamic.insert(resolved(&project_dir.join(&model.path))); } } prepared_lint_violations(run, (&selected_paths, &dynamic), &compiled, project_files)? }; - let model_paths: BTreeSet = models.into_iter().map(|(path, _)| path).collect(); + let model_paths: BTreeSet = models.into_iter().map(|model| model.path).collect(); let evaluated: Vec = violation_findings(run, &violations, &model_paths)?; findings.extend(evaluated.iter().cloned()); let mut written: Vec<(String, String)> = misses.clone(); 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 903a68e7d3..d81d234335 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 @@ -4,7 +4,7 @@ use std::collections::HashMap; use pyo3::exceptions::PyValueError; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; -use pyo3::types::PyString; +use pyo3::types::{PyDict, PyDictMethods, PyString}; use sqlbuild_rules::models::{ DirectTestResourceKind, SqlScenarioFact, SqlTestCteFact, SqlTestFact, SqlTestMode, SqlTestParameterFact, SqlTestParameterValueFact, TestedResource, @@ -13,6 +13,7 @@ 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::fields::field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; @@ -35,18 +36,17 @@ fn either<'py>( } fn names(owner: &Bound<'_, PyAny>, attribute: &str) -> PyResult> { - owner.getattr(attribute)?.extract() + field(owner, attribute)?.extract() } fn ctes(payload: &Bound<'_, PyAny>, attribute: &str) -> PyResult> { - payload - .getattr(attribute)? + field(payload, attribute)? .try_iter()? .map(|cte| { let cte = cte?; Ok(SqlTestCteFact { - name: cte.getattr("name")?.extract()?, - sql: cte.getattr("sql_body")?.extract()?, + name: field(&cte, "name")?.extract()?, + sql: field(&cte, "sql_body")?.extract()?, }) }) .collect() @@ -72,21 +72,21 @@ fn tested_kind(value: &str) -> PyResult { } fn tested_resources(test: &Bound<'_, PyAny>) -> PyResult> { - test.getattr("tested_resources")? + field(test, "tested_resources")? .try_iter()? .map(|resource| { let resource = resource?; - let kind: String = resource.getattr("kind")?.getattr("value")?.extract()?; + let kind: String = field(&resource, "kind")?.extract()?; Ok(TestedResource { kind: tested_kind(&kind)?, - name: resource.getattr("name")?.extract()?, + name: field(&resource, "name")?.extract()?, }) }) .collect() } fn with_payload(test: &Bound<'_, PyAny>, mut fact: SqlTestFact) -> PyResult { - let payload: Bound<'_, PyAny> = test.getattr("payload")?; + let payload: Bound<'_, PyAny> = field(test, "payload")?; let model_payload: Bound<'_, PyAny> = registry::class(test.py(), RecordId::CompiledModelSqlTestPayload)?; if !payload.is_instance(&model_payload)? { @@ -95,26 +95,23 @@ fn with_payload(test: &Bound<'_, PyAny>, mut fact: SqlTestFact) -> PyResult, mut fact: SqlTestFact) -> PyResult { - let case_name: Option = test.getattr("case_name")?.extract()?; + let case_name: Option = field(test, "case_name")?.extract()?; let Some(case_name) = case_name else { return Ok(fact); }; let mut schema: Vec = Vec::new(); - for parameter in test.getattr("parameter_schema")?.try_iter()? { + for parameter in field(test, "parameter_schema")?.try_iter()? { let parameter = parameter?; schema.push(SqlTestParameterFact { - name: parameter.getattr("name")?.extract()?, - value_type: parameter - .getattr("value_type")? - .getattr("value")? - .extract()?, - nullable: parameter.getattr("nullable")?.extract()?, + name: field(¶meter, "name")?.extract()?, + value_type: field(¶meter, "value_type")?.extract()?, + nullable: field(¶meter, "nullable")?.extract()?, }); } let types: HashMap<&str, &str> = schema @@ -122,7 +119,7 @@ fn with_case(test: &Bound<'_, PyAny>, mut fact: SqlTestFact) -> PyResult = Vec::new(); - for item in test.getattr("parameter_values")?.try_iter()? { + for item in field(test, "parameter_values")?.try_iter()? { let (name, value): (String, Bound<'_, PyAny>) = item?.extract()?; let value_type: String = types .get(name.as_str()) @@ -136,32 +133,32 @@ fn with_case(test: &Bound<'_, PyAny>, mut fact: SqlTestFact) -> PyResult) -> PyResult { - let test_file: Bound<'_, PyAny> = test.getattr("test_file")?; - let source_path: Bound<'_, PyAny> = either(test.getattr("source_path")?, || { - test_file.getattr("relative_path") + let test_file: Bound<'_, PyAny> = field(test, "test_file")?; + let source_path: Bound<'_, PyAny> = either(field(test, "source_path")?, || { + field(&test_file, "relative_path") })?; - let ownership_root: Bound<'_, PyAny> = either(test.getattr("ownership_root")?, || { - test_file.getattr("ownership_root") + let ownership_root: Bound<'_, PyAny> = either(field(test, "ownership_root")?, || { + field(&test_file, "ownership_root") })?; - let block_index: Bound<'_, PyAny> = either(test.getattr("block_index")?, || { - test.getattr("test_block")?.getattr("test_index") + let block_index: Bound<'_, PyAny> = either(field(test, "block_index")?, || { + field(&field(test, "test_block")?, "test_index") })?; - let mode: String = test.getattr("mode")?.getattr("value")?.extract()?; + let mode: String = field(test, "mode")?.extract()?; let fact = SqlTestFact { source_path: posix(&source_path)?, ownership_root: posix(&ownership_root)?, block_index: block_index.extract()?, - name: test.getattr("name")?.extract()?, - explicit_name: test.getattr("explicit_name")?.extract()?, + name: field(test, "name")?.extract()?, + explicit_name: field(test, "explicit_name")?.extract()?, parent_name: None, case_name: None, case_index: None, @@ -186,25 +183,24 @@ pub(crate) fn sql_test_fact(test: &Bound<'_, PyAny>) -> PyResult { /// One compiled SQL scenario as the facts the built-in rules read. pub(crate) fn scenario_fact(scenario: &Bound<'_, PyAny>) -> PyResult { - let scenario_file: Bound<'_, PyAny> = scenario.getattr("scenario_file")?; - let source_path: Bound<'_, PyAny> = either(scenario.getattr("source_path")?, || { - scenario_file.getattr("relative_path") + let scenario_file: Bound<'_, PyAny> = field(scenario, "scenario_file")?; + let source_path: Bound<'_, PyAny> = either(field(scenario, "source_path")?, || { + field(&scenario_file, "relative_path") })?; - let ownership_root: Bound<'_, PyAny> = either(scenario.getattr("ownership_root")?, || { - scenario_file.getattr("ownership_root") + let ownership_root: Bound<'_, PyAny> = either(field(scenario, "ownership_root")?, || { + field(&scenario_file, "ownership_root") })?; - let header: Bound<'_, PyAny> = scenario_file - .getattr("header_values")? - .call_method1("get", (DESCRIPTION_KEY,))?; - let description: Option = if header.is_instance_of::() { - Some(header.extract()?) - } else { - None + let description: Option = match field(&scenario_file, "header_values")? + .cast_into::()? + .get_item(DESCRIPTION_KEY)? + { + Some(header) if header.is_instance_of::() => Some(header.extract()?), + _ => None, }; Ok(SqlScenarioFact { source_path: posix(&source_path)?, ownership_root: posix(&ownership_root)?, - name: scenario.getattr("name")?.extract()?, + name: field(scenario, "name")?.extract()?, description, expected_model_names: names(scenario, "expected_model_names")?, assertion_names: names(scenario, "assertion_names")?, 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 833b74060d..598976dc7e 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 @@ -12,6 +12,7 @@ use sqlbuild_core::scenario_command::models::SqlAnalysisCommand; 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::records::fields::field as native_field; use crate::bindings::_helpers::scenario_command::conversions::succeeded; use crate::bindings::_helpers::scenario_command::presentation::{ CompletionKind, SectionStatus, activity, begin_section, connection_hooks, progress_relay, @@ -37,7 +38,7 @@ fn count(label: &str, value: usize) -> SummaryCount { #[pyfunction] #[pyo3(signature = (*, request))] fn run_scenario_capture_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult { - let no_sql_validation: Bound<'_, PyAny> = request.getattr("no_sql_validation")?; + let no_sql_validation: Bound<'_, PyAny> = native_field(request, "no_sql_validation")?; if let Some(error) = sql_analysis_required_error( SqlAnalysisCommand::Capture, no_sql_validation.is_truthy()?, @@ -82,7 +83,7 @@ fn run_scenario_capture_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> P ( &adapter_name, &capture_dialect, - request.getattr("retain")?.is_truthy()?, + native_field(request, "retain")?.is_truthy()?, ), limits, )?; @@ -123,10 +124,13 @@ fn run_scenario_capture_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> P ), ("settings", settings), ("connection_hooks", hooks), - ("on_scenario_start", relay.getattr("on_scenario_start")?), + ( + "on_scenario_start", + native_field(&relay, "on_scenario_start")?, + ), ( "on_scenario_complete", - relay.getattr("on_scenario_complete")?, + native_field(&relay, "on_scenario_complete")?, ), ], )? 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 997eebc2fe..6c506050f9 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/scenario_command/conversions.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/scenario_command/conversions.rs @@ -18,6 +18,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; @@ -35,12 +36,12 @@ fn items<'py>(value: &Bound<'py, PyAny>) -> PyResult>> { } fn enum_value(value: &Bound<'_, PyAny>) -> PyResult { - value.getattr("value")?.extract() + native_field(value, "value")?.extract() } /// `result.status` is the given `ExecutionStatus` value. pub(crate) fn matching_status(result: &Bound<'_, PyAny>, status: &str) -> PyResult { - Ok(enum_value(&result.getattr("status")?)? == status) + Ok(enum_value(&native_field(result, "status")?)? == status) } pub(crate) fn succeeded(result: &Bound<'_, PyAny>) -> PyResult { @@ -54,34 +55,34 @@ fn posix(path: &Bound<'_, PyAny>) -> PyResult { /// The result's coded error, when its message is set. fn coded_error(result: &Bound<'_, PyAny>) -> PyResult> { - let message: Option = result.getattr("error_message")?.extract()?; + let message: Option = native_field(result, "error_message")?.extract()?; let Some(message) = message else { return Ok(None); }; Ok(Some(CodedText { - code: result.getattr("error_code")?.extract()?, + code: native_field(result, "error_code")?.extract()?, message, - help: result.getattr("error_help")?.extract()?, + help: native_field(result, "error_help")?.extract()?, })) } fn expected_row(expected: &Bound<'_, PyAny>) -> PyResult { Ok(ExpectedRow { - model_name: expected.getattr("model_name")?.extract()?, + model_name: native_field(expected, "model_name")?.extract()?, failed: matching_status(expected, FAILED_STATUS)?, - actual_row_count: expected.getattr("actual_row_count")?.extract()?, - expected_row_count: expected.getattr("expected_row_count")?.extract()?, - mismatched_row_count: expected.getattr("mismatched_row_count")?.extract()?, + actual_row_count: native_field(expected, "actual_row_count")?.extract()?, + expected_row_count: native_field(expected, "expected_row_count")?.extract()?, + mismatched_row_count: native_field(expected, "mismatched_row_count")?.extract()?, error: coded_error(expected)?, }) } fn assertion_row(assertion: &Bound<'_, PyAny>) -> PyResult { - let samples: Vec> = items(&assertion.getattr("sample_rows")?)?; + let samples: Vec> = items(&native_field(assertion, "sample_rows")?)?; Ok(AssertionRow { - name: assertion.getattr("name")?.extract()?, + name: native_field(assertion, "name")?.extract()?, failed: matching_status(assertion, FAILED_STATUS)?, - failing_row_count: assertion.getattr("failing_row_count")?.extract()?, + failing_row_count: native_field(assertion, "failing_row_count")?.extract()?, first_sample: samples .first() .map(|sample| sample.str()?.extract()) @@ -93,14 +94,14 @@ 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(&relation_map.getattr("artifacts")?)? + items(&native_field(relation_map, "artifacts")?)? .iter() .map(|artifact| { - let identity: Bound<'_, PyAny> = artifact.getattr("identity")?; + let identity: Bound<'_, PyAny> = native_field(artifact, "identity")?; Ok(RetainedRelation { - kind: enum_value(&kinds.call1((identity.getattr("kind")?,))?)?, - logical_name: identity.getattr("logical_name")?.extract()?, - physical_name: artifact.getattr("physical_name")?.extract()?, + kind: enum_value(&kinds.call1((native_field(&identity, "kind")?,))?)?, + logical_name: native_field(&identity, "logical_name")?.extract()?, + physical_name: native_field(artifact, "physical_name")?.extract()?, }) }) .collect() @@ -108,7 +109,7 @@ fn retained_relations(relation_map: &Bound<'_, PyAny>) -> PyResult) -> PyResult { - let local_status: Bound<'_, PyAny> = result.getattr("local_status")?; + let local_status: Bound<'_, PyAny> = native_field(result, "local_status")?; let local: bool = !local_status.is_none(); let status: String = if local { enum_value(&local_status)? @@ -117,15 +118,15 @@ pub(crate) fn scenario_result_row(result: &Bound<'_, PyAny>) -> PyResult = result.getattr("relation_map")?; - let duckdb_path: Bound<'_, PyAny> = result.getattr("local_duckdb_path")?; + let relation_map: Bound<'_, PyAny> = native_field(result, "relation_map")?; + let duckdb_path: Bound<'_, PyAny> = native_field(result, "local_duckdb_path")?; Ok(ScenarioResultRow { - name: result.getattr("scenario_name")?.extract()?, + name: native_field(result, "scenario_name")?.extract()?, status, local, - duration_ms: result.getattr("duration_ms")?.extract()?, + duration_ms: native_field(result, "duration_ms")?.extract()?, error: coded_error(result)?, - retained: result.getattr("retained")?.is_truthy()?, + retained: native_field(result, "retained")?.is_truthy()?, retained_relations: if relation_map.is_none() { None } else { @@ -136,11 +137,11 @@ pub(crate) fn scenario_result_row(result: &Bound<'_, PyAny>) -> PyResult>()?, - assertions: items(&result.getattr("assertion_results")?)? + assertions: items(&native_field(result, "assertion_results")?)? .iter() .map(assertion_row) .collect::>()?, @@ -168,24 +169,25 @@ pub(crate) fn capture_row( result: &Bound<'_, PyAny>, (project_dir, scenario_plan): (&Bound<'_, PyAny>, Option<&Bound<'_, PyAny>>), ) -> PyResult { - let retained: bool = result.getattr("retained")?.is_truthy()?; - let capture: Bound<'_, PyAny> = result.getattr("capture_result")?; + let retained: bool = native_field(result, "retained")?.is_truthy()?; + let capture: Bound<'_, PyAny> = native_field(result, "capture_result")?; let (relations, snapshot_path) = if capture.is_none() { (None, None) } else { - let relations: Vec = items(&capture.getattr("relation_results")?)? - .iter() - .map(|relation| { - Ok(CaptureRelationRow { - kind: enum_value(&relation.getattr("kind")?)?, - logical_name: relation.getattr("logical_name")?.extract()?, - passed: succeeded(relation)?, - row_count: relation.getattr("row_count")?.extract()?, - byte_count: relation.getattr("byte_count")?.extract()?, + let relations: Vec = + items(&native_field(&capture, "relation_results")?)? + .iter() + .map(|relation| { + Ok(CaptureRelationRow { + kind: enum_value(&native_field(relation, "kind")?)?, + logical_name: native_field(relation, "logical_name")?.extract()?, + passed: succeeded(relation)?, + row_count: native_field(relation, "row_count")?.extract()?, + byte_count: native_field(relation, "byte_count")?.extract()?, + }) }) - }) - .collect::>()?; - let manifest_path: Bound<'_, PyAny> = capture.getattr("manifest_path")?; + .collect::>()?; + let manifest_path: Bound<'_, PyAny> = native_field(&capture, "manifest_path")?; let snapshot_path: Option = if manifest_path.is_none() { None } else { @@ -195,26 +197,26 @@ pub(crate) fn capture_row( }; let retained_inputs: Option = if retained && scenario_plan.is_some() { Some(RetainedInputs { - fixtures: items(&result.getattr("fixture_results")?)? + fixtures: items(&native_field(result, "fixture_results")?)? .iter() .map(|fixture| { Ok(RetainedRelation { - kind: enum_value(&fixture.getattr("kind")?)?, - logical_name: fixture.getattr("logical_name")?.extract()?, - physical_name: fixture.getattr("target_relation")?.extract()?, + kind: enum_value(&native_field(fixture, "kind")?)?, + logical_name: native_field(fixture, "logical_name")?.extract()?, + physical_name: native_field(fixture, "target_relation")?.extract()?, }) }) .collect::>()?, - seeds: items(&result.getattr("seed_results")?)? + seeds: items(&native_field(result, "seed_results")?)? .iter() - .map(|seed| seed.getattr("seed_name")?.extract()) + .map(|seed| native_field(seed, "seed_name")?.extract()) .collect::>()?, }) } else { None }; Ok(CaptureRow { - name: result.getattr("scenario_name")?.extract()?, + name: native_field(result, "scenario_name")?.extract()?, passed: succeeded(result)?, error: coded_error(result)?, retained, @@ -227,9 +229,9 @@ pub(crate) fn capture_row( /// The SQL contents of an executed result's lifecycle events. fn sql_statements(result: &Bound<'_, PyAny>) -> PyResult> { let mut statements: Vec = Vec::new(); - for event in items(&result.getattr("lifecycle_events")?)? { - if enum_value(&event.getattr("kind")?)? == SQL_EVENT_KIND { - statements.push(event.getattr("content")?.extract()?); + for event in items(&native_field(result, "lifecycle_events")?)? { + if enum_value(&native_field(&event, "kind")?)? == SQL_EVENT_KIND { + statements.push(native_field(&event, "content")?.extract()?); } } Ok(statements) @@ -249,37 +251,37 @@ fn expectation_sql( ) -> PyResult { let py: Python<'_> = adapter.py(); let mut expectations: ExpectationSql = ExpectationSql::default(); - for expected in items(&scenario_plan.getattr("expected_expectations")?)? { + for expected in items(&native_field(scenario_plan, "expected_expectations")?)? { let actual_relation: String = call_with( &host::entry(py, AdapterHost::ResolveRelationLocationQualifiedName)?, &[ ("adapter", adapter.clone()), - ("location", expected.getattr("actual_destination")?), + ("location", native_field(&expected, "actual_destination")?), ], )? .extract()?; let sql: String = scenario_expected_comparison_sql( &format!("SELECT * FROM {actual_relation}"), - &expected.getattr("expected_sql")?.extract::()?, + &native_field(&expected, "expected_sql")?.extract::()?, &adapter .call_method0("render_set_difference_operator")? .extract::()?, ); expectations .expected - .push((expected.getattr("model_name")?.extract()?, sql)); + .push((native_field(&expected, "model_name")?.extract()?, sql)); } - for assertion in items(&scenario_plan.getattr("assertion_expectations")?)? { + for assertion in items(&native_field(scenario_plan, "assertion_expectations")?)? { expectations.assertions.push(( - assertion.getattr("name")?.extract()?, - assertion.getattr("sql")?.extract()?, + native_field(&assertion, "name")?.extract()?, + native_field(&assertion, "sql")?.extract()?, )); } Ok(expectations) } fn scenario_run_dir(target_dir: &Path, scenario_plan: &Bound<'_, PyAny>) -> PyResult { - let name: String = scenario_plan.getattr("name")?.extract()?; + let name: String = native_field(scenario_plan, "name")?.extract()?; Ok(target_dir.join(RUN_DIR).join(SCENARIOS_DIR).join(name)) } @@ -304,30 +306,30 @@ pub(crate) fn write_warehouse_runtime_target( .collect() }; let inputs: WarehouseRuntimeInputs = WarehouseRuntimeInputs { - prepare_cleanup: cleanup_statements(&result.getattr("prepare_cleanup_result")?)?, - fixtures: items(&result.getattr("fixture_results")?)? + prepare_cleanup: cleanup_statements(&native_field(result, "prepare_cleanup_result")?)?, + fixtures: items(&native_field(result, "fixture_results")?)? .iter() .map(|fixture| { Ok(FixtureStatements { - kind: enum_value(&fixture.getattr("kind")?)?, - logical_name: fixture.getattr("logical_name")?.extract()?, + kind: enum_value(&native_field(fixture, "kind")?)?, + logical_name: native_field(fixture, "logical_name")?.extract()?, statements: sql_statements(fixture)?, }) }) .collect::>()?, - seeds: named(result.getattr("seed_results")?, "seed_name")?, - models: named(result.getattr("model_results")?, "model_name")?, - model_entries: items(&scenario_plan.getattr("model_entries")?)? + seeds: named(native_field(result, "seed_results")?, "seed_name")?, + models: named(native_field(result, "model_results")?, "model_name")?, + model_entries: items(&native_field(scenario_plan, "model_entries")?)? .iter() .map(|entry| { Ok(ModelArtifactEntry { - name: entry.getattr("name")?.extract()?, - relative_path: entry.getattr("relative_path")?.extract()?, + name: native_field(entry, "name")?.extract()?, + relative_path: native_field(entry, "relative_path")?.extract()?, }) }) .collect::>()?, expectations: expectation_sql(scenario_plan, adapter)?, - final_cleanup: cleanup_statements(&result.getattr("cleanup_result")?)?, + final_cleanup: cleanup_statements(&native_field(result, "cleanup_result")?)?, }; let root: PathBuf = scenario_run_dir(target_dir, scenario_plan)?; written( @@ -340,30 +342,30 @@ fn function_statements( entry: &Bound<'_, PyAny>, adapter: &Bound<'_, PyAny>, ) -> PyResult { - let destination: Bound<'_, PyAny> = entry.getattr("destination")?; - let qualified: Bound<'_, PyAny> = destination.getattr("qualified_name")?; + let destination: Bound<'_, PyAny> = native_field(entry, "destination")?; + let qualified: Bound<'_, PyAny> = native_field(&destination, "qualified_name")?; let destination_name: Bound<'_, PyAny> = if qualified.is_truthy()? { qualified } else { - destination.getattr("name")? + native_field(&destination, "name")? }; - let language: Bound<'_, PyAny> = entry.getattr("language")?; + let language: Bound<'_, PyAny> = native_field(entry, "language")?; let rendered: Bound<'_, PyAny> = call_with( - &adapter.getattr("render_create_function")?, + &native_field(adapter, "render_create_function")?, &[ ("destination", destination_name), - ("arguments", entry.getattr("arguments")?), - ("returns", entry.getattr("returns")?), - ("body_sql", entry.getattr("body_sql")?), - ("return_columns", entry.getattr("return_columns")?), + ("arguments", native_field(entry, "arguments")?), + ("returns", native_field(entry, "returns")?), + ("body_sql", native_field(entry, "body_sql")?), + ("return_columns", native_field(entry, "return_columns")?), ("language", language.clone()), - ("runtime_version", entry.getattr("runtime_version")?), - ("entry_point", entry.getattr("entry_point")?), - ("packages", entry.getattr("packages")?), + ("runtime_version", native_field(entry, "runtime_version")?), + ("entry_point", native_field(entry, "entry_point")?), + ("packages", native_field(entry, "packages")?), ], )?; Ok(FunctionStatements { - relative_path: entry.getattr("relative_path")?.extract()?, + relative_path: native_field(entry, "relative_path")?.extract()?, language: enum_value(&language)?, statements: items(&rendered)? .iter() @@ -377,40 +379,40 @@ pub(crate) fn write_local_runtime_target( target_dir: &Path, (adapter, scenario_plan, result): (&Bound<'_, PyAny>, &Bound<'_, PyAny>, &Bound<'_, PyAny>), ) -> PyResult<()> { - let local_plan: Bound<'_, PyAny> = match result.getattr("local_execution_plan")? { + let local_plan: Bound<'_, PyAny> = match native_field(result, "local_execution_plan")? { plan if plan.is_truthy()? => plan, _ => scenario_plan.clone(), }; - let executed: HashSet = items(&result.getattr("function_results")?)? + let executed: HashSet = items(&native_field(result, "function_results")?)? .iter() - .map(|function| function.getattr("function_name")?.extract()) + .map(|function| native_field(function, "function_name")?.extract()) .collect::>()?; let mut functions: Vec = Vec::new(); - for entry in items(&local_plan.getattr("function_entries")?)? { - if executed.contains(&entry.getattr("name")?.extract::()?) { + for entry in items(&native_field(&local_plan, "function_entries")?)? { + if executed.contains(&native_field(&entry, "name")?.extract::()?) { functions.push(function_statements(&entry, adapter)?); } } let inputs: LocalRuntimeInputs = LocalRuntimeInputs { - loaded_relations: items(&result.getattr("local_snapshot_relations")?)? + loaded_relations: items(&native_field(result, "local_snapshot_relations")?)? .iter() .map(|relation| { Ok(LoadedRelation { - kind: enum_value(&relation.getattr("kind")?)?, - logical_name: relation.getattr("logical_name")?.extract()?, - file_path: posix(&relation.getattr("file_path")?)?, - table_name: relation.getattr("table_name")?.extract()?, - row_count: relation.getattr("row_count")?.extract()?, + kind: enum_value(&native_field(relation, "kind")?)?, + logical_name: native_field(relation, "logical_name")?.extract()?, + file_path: posix(&native_field(relation, "file_path")?)?, + table_name: native_field(relation, "table_name")?.extract()?, + row_count: native_field(relation, "row_count")?.extract()?, }) }) .collect::>()?, functions, - models: items(&local_plan.getattr("model_entries")?)? + models: items(&native_field(&local_plan, "model_entries")?)? .iter() .map(|entry| { Ok(( - entry.getattr("relative_path")?.extract()?, - entry.getattr("resolved_sql")?.extract()?, + native_field(entry, "relative_path")?.extract()?, + native_field(entry, "resolved_sql")?.extract()?, )) }) .collect::>()?, 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 a699b2b376..99217f2c63 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/scenario_command/presentation.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/scenario_command/presentation.rs @@ -16,6 +16,7 @@ use crate::bindings::_helpers::build_progress::transient_status::status_reporter 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::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::{ @@ -79,7 +80,7 @@ impl ScenarioProgressRelay { CompletionKind::Run { target_dir } => { if let Some(plan) = plan { let adapter: &Bound<'_, PyAny> = self.adapter.bind(py); - if result.getattr("local_status")?.is_none() { + if native_field(&result, "local_status")?.is_none() { write_warehouse_runtime_target(target_dir, (adapter, plan, &result))?; } else { write_local_runtime_target(target_dir, (adapter, plan, &result))?; @@ -180,13 +181,16 @@ pub(crate) fn connection_hooks<'py>( &[ ( "on_connection_start", - reporter.getattr("on_connection_start")?, + native_field(&reporter, "on_connection_start")?, ), ( "on_connection_complete", - relay.getattr("on_connection_complete")?, + native_field(&relay, "on_connection_complete")?, + ), + ( + "on_connection_error", + native_field(&relay, "on_connection_error")?, ), - ("on_connection_error", relay.getattr("on_connection_error")?), ], ) } 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 91e0b19097..bfc075e488 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/scenario_command/setup.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/scenario_command/setup.rs @@ -26,6 +26,7 @@ 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::presentation_text::terminal::stdout_supports_color; +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; @@ -109,7 +110,7 @@ pub(crate) fn discover_project<'py>( py: Python<'py>, request: &Bound<'py, PyAny>, ) -> PyResult> { - let project_dir: Bound<'py, PyAny> = match request.getattr("project_dir")? { + let project_dir: Bound<'py, PyAny> = match native_field(request, "project_dir")? { directory if !directory.is_none() => directory, _ => py.import("pathlib")?.getattr("Path")?.call_method0("cwd")?, }; @@ -117,8 +118,8 @@ pub(crate) fn discover_project<'py>( ®istry::native(py, NativeAttr::DiscoverProjectInputs)?, &[("project_dir", project_dir.clone())], )?; - let selectors: Bound<'py, PyAny> = request.getattr("selectors")?; - let exclude: Bound<'py, PyAny> = request.getattr("exclude")?; + 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)?, &[ @@ -127,23 +128,23 @@ pub(crate) fn discover_project<'py>( ("discovered_inputs", discovered.clone()), ], )?; - let local_config: Bound<'py, PyAny> = discovered.getattr("local_config")?; + let local_config: Bound<'py, PyAny> = native_field(&discovered, "local_config")?; let candidates: NamespaceCandidates = NamespaceCandidates { - cli: request.getattr("scenario_namespace")?.extract()?, + cli: native_field(request, "scenario_namespace")?.extract()?, env: py .import("os")? .getattr("environ")? .call_method1("get", (SCENARIO_NAMESPACE_ENV,))? .extract()?, local_config: config_namespace(&local_config)?, - project_config: config_namespace(&discovered.getattr("project_config")?)?, + project_config: config_namespace(&native_field(&discovered, "project_config")?)?, }; let namespace: ScenarioNamespace = resolve_scenario_namespace(candidates).map_err(|error| cli_user_error(py, error))?; let discovered: Bound<'py, PyAny> = match &namespace.value { Some(value) => { let scenario: Bound<'py, PyAny> = replaced( - &local_config.getattr("scenario")?, + &native_field(&local_config, "scenario")?, ("run_namespace", text(py, value)), )?; replaced( @@ -190,9 +191,12 @@ pub(crate) fn project_adapter_name(project: &ScenarioProject<'_>) -> PyResult( /// `not no_color and supports_color()`. pub(crate) fn style(request: &Bound<'_, PyAny>) -> PyResult { let use_color: bool = - !request.getattr("no_color")?.is_truthy()? && stdout_supports_color(request.py())?; + !native_field(request, "no_color")?.is_truthy()? && stdout_supports_color(request.py())?; Ok(CliStyle { use_color }) } @@ -312,7 +316,7 @@ pub(crate) fn compile<'py>( ("discovered_inputs", project.discovered.clone()), ("adapter", adapter.clone()), ("options", options.clone()), - ("on_progress", planning.getattr("on_progress")?), + ("on_progress", native_field(planning, "on_progress")?), ], ) }) @@ -331,11 +335,11 @@ pub(crate) fn selected_scenarios<'py>( .collect::>()?; let mut files: Vec = Vec::with_capacity(compiled.len()); for scenario in &compiled { - let file: Bound<'py, PyAny> = scenario.getattr("scenario_file")?; + let file: Bound<'py, PyAny> = native_field(scenario, "scenario_file")?; files.push(ScenarioFile { - name: scenario.getattr("name")?.extract()?, - file_path: file.getattr("file_path")?.extract()?, - relative_path: file.getattr("relative_path")?.extract()?, + name: native_field(scenario, "name")?.extract()?, + file_path: native_field(&file, "file_path")?.extract()?, + relative_path: native_field(&file, "relative_path")?.extract()?, }); } let project_dir: PathBuf = project_dir.extract()?; @@ -361,15 +365,18 @@ pub(crate) fn capture_limits<'py>( project: &ScenarioProject<'py>, ) -> PyResult> { let py: Python<'py> = request.py(); - let inputs: Bound<'py, PyAny> = request.getattr("limit_inputs")?; + let inputs: Bound<'py, PyAny> = native_field(request, "limit_inputs")?; let configured: Bound<'py, PyAny> = call_with( ®istry::native(py, NativeAttr::ResolveEffectiveScenarioConfig)?, &[ ( "project_config", - project.discovered.getattr("project_config")?, + native_field(&project.discovered, "project_config")?, + ), + ( + "local_config", + native_field(&project.discovered, "local_config")?, ), - ("local_config", project.discovered.getattr("local_config")?), ], )? .getattr("snapshot_limits")?; @@ -379,7 +386,7 @@ pub(crate) fn capture_limits<'py>( max_total_rows: optional_int(&inputs, "max_snapshot_total_rows")?, max_bytes_per_relation: optional_int(&inputs, "max_snapshot_bytes")?, max_total_bytes: optional_int(&inputs, "max_snapshot_total_bytes")?, - force: inputs.getattr("force")?.is_truthy()?, + force: native_field(&inputs, "force")?.is_truthy()?, }, SnapshotLimitConfig { max_rows_per_relation: optional_int(&configured, "max_rows_per_relation")?, @@ -408,9 +415,9 @@ pub(crate) fn capture_limits<'py>( fn sqlbuild_version(py: Python<'_>) -> PyResult { let metadata: Bound<'_, PyAny> = py.import(METADATA_MODULE)?.into_any(); - match metadata.getattr("version")?.call1((DISTRIBUTION_NAME,)) { + match native_field(&metadata, "version")?.call1((DISTRIBUTION_NAME,)) { Ok(version) => version.extract(), - Err(error) if error.is_instance(py, &metadata.getattr("PackageNotFoundError")?) => { + Err(error) if error.is_instance(py, &native_field(&metadata, "PackageNotFoundError")?) => { Ok(UNKNOWN_VERSION.to_owned()) } Err(error) => Err(error), 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 dc35415aa3..a4479105cd 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 @@ -10,6 +10,7 @@ use sqlbuild_core::scenario_command::models::ScenarioNamespace; 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::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::succeeded; @@ -53,8 +54,11 @@ impl LocalSync<'_> { ®istry::native(py, NativeAttr::WriteExecutionJsonOutput)?, &[ ("payload", payload), - ("json_output", request.getattr("json_output")?), - ("json_output_path", request.getattr("json_output_path")?), + ("json_output", native_field(request, "json_output")?), + ( + "json_output_path", + native_field(request, "json_output_path")?, + ), ], )?; Ok(self.exit_code) @@ -75,7 +79,7 @@ pub(crate) fn sync_local_snapshots<'py>( (local_pipeline, local_scenarios): (&Bound<'py, PyAny>, &Bound<'py, PyTuple>), ) -> PyResult> { let py: Python<'py> = request.py(); - let refresh: bool = request.getattr("refresh")?.is_truthy()?; + 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 @@ -95,7 +99,7 @@ pub(crate) fn sync_local_snapshots<'py>( ("refresh", boolean(py, refresh)), ( "source_lexical_syntax", - run.project_adapter.getattr("sql_lexical_syntax")?, + native_field(&run.project_adapter, "sql_lexical_syntax")?, ), ], )? @@ -121,7 +125,10 @@ pub(crate) fn sync_local_snapshots<'py>( CompileInputs { adapter: &run.project_adapter, options: &[ - ("no_sql_validation", request.getattr("no_sql_validation")?), + ( + "no_sql_validation", + native_field(request, "no_sql_validation")?, + ), ("source_deferral_enabled", boolean(py, false)), ("connection_config", project_connection.clone()), ], @@ -180,10 +187,13 @@ pub(crate) fn sync_local_snapshots<'py>( ("project_name", project_name), ("settings", settings), ("connection_hooks", hooks), - ("on_scenario_start", relay.getattr("on_scenario_start")?), + ( + "on_scenario_start", + native_field(&relay, "on_scenario_start")?, + ), ( "on_scenario_complete", - relay.getattr("on_scenario_complete")?, + native_field(&relay, "on_scenario_complete")?, ), ], )? 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 a72d78c342..8858b1a1c9 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/scopes/compile_usages.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/scopes/compile_usages.rs @@ -19,6 +19,7 @@ use sqlbuild_scopes::scope_index::models::{ }; use crate::bindings::_helpers::compiled_project::model_loop_named::scope_path_message; +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; @@ -95,11 +96,11 @@ 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 = resolver.getattr("lookup")?.getattr("declarations")?; + let declarations = native_field(resolver, "lookup")?.getattr("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()? - || !identity.getattr("owner")?.is_none() + || !native_field(&identity, "owner")?.is_none() || !SCHEMA_USAGE_KINDS.contains( &identity .getattr("kind")? @@ -111,11 +112,13 @@ pub(crate) fn model_schema_scope_usages<'py>( continue; } let record: Bound<'py, PyAny> = found.get_item(0)?; - let recorded: Bound<'py, PyAny> = record.getattr("identity")?; + let recorded: Bound<'py, PyAny> = native_field(&record, "identity")?; record_rows.push(( ( - recorded.getattr("kind")?.getattr("value")?.extract()?, - recorded.getattr("name")?.extract()?, + native_field(&recorded, "kind")? + .getattr("value")? + .extract()?, + native_field(&recorded, "name")?.extract()?, ), record_row(&record)?, )); @@ -123,19 +126,19 @@ pub(crate) fn model_schema_scope_usages<'py>( } let mut schemas: Vec = Vec::new(); for file in schema_files.try_iter()? { - for declaration in file?.getattr("declarations")?.try_iter()? { + for declaration in native_field(&file?, "declarations")?.try_iter()? { let declaration: Bound<'py, PyAny> = declaration?; let mut columns: Vec<(String, Option)> = Vec::new(); - for column in declaration.getattr("columns")?.try_iter()? { + for column in native_field(&declaration, "columns")?.try_iter()? { let column: Bound<'py, PyAny> = column?; columns.push(( - column.getattr("name")?.extract()?, - column.getattr("type")?.extract()?, + native_field(&column, "name")?.extract()?, + native_field(&column, "type")?.extract()?, )); } schemas.push(( - declaration.getattr("name")?.extract()?, - declaration.getattr("extends")?.extract()?, + native_field(&declaration, "name")?.extract()?, + native_field(&declaration, "extends")?.extract()?, columns, )); } @@ -151,8 +154,8 @@ pub(crate) fn model_schema_scope_usages<'py>( 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", records[consumer].getattr("identity")?)?; - fields.set_item("declaration", records[used].getattr("identity")?)?; + 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))?); } PyTuple::new(py, built) @@ -160,20 +163,20 @@ pub(crate) fn model_schema_scope_usages<'py>( /// A declaration record's path, scope, private owner, owning path and ownership root path. fn record_row(record: &Bound<'_, PyAny>) -> PyResult { - let identity: Bound<'_, PyAny> = record.getattr("identity")?; - let owner: Bound<'_, PyAny> = identity.getattr("owner")?; + let identity: Bound<'_, PyAny> = native_field(record, "identity")?; + let owner: Bound<'_, PyAny> = native_field(&identity, "owner")?; Ok(( - record.getattr("path")?.extract()?, - record.getattr("scope")?.getattr("value")?.extract()?, + native_field(record, "path")?.extract()?, + native_field(record, "scope")?.getattr("value")?.extract()?, if owner.is_none() { None } else { Some(( - owner.getattr("kind")?.getattr("value")?.extract()?, - owner.getattr("name")?.extract()?, + native_field(&owner, "kind")?.getattr("value")?.extract()?, + native_field(&owner, "name")?.extract()?, )) }, - record.getattr("owning_path")?.extract()?, + native_field(record, "owning_path")?.extract()?, record .getattr("ownership_root")? .getattr("path")? diff --git a/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_cache.rs b/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_cache.rs index 71844d288f..8cbb2dc344 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_cache.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_cache.rs @@ -13,6 +13,7 @@ use sqlbuild_scopes::scope_cache::main::write_scope_cache::write_scope_cache; use sqlbuild_scopes::scope_cache::models::{CachedIndex, Fingerprint, FingerprintInputs}; use crate::bindings::_helpers::boundary::panics::compiler_error; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::scopes::scope_objects::{ ScopeClasses, index_object, read_completeness, read_index, }; @@ -25,9 +26,9 @@ const UNKNOWN_VERSION: &str = "0+unknown"; /// `importlib.metadata.version("sqlbuild")`, or `0+unknown` when it is not installed. fn sqlbuild_version(py: Python<'_>) -> PyResult { let metadata: Bound<'_, PyModule> = py.import(METADATA_MODULE)?; - match metadata.getattr("version")?.call1((DISTRIBUTION_NAME,)) { + match native_field(&metadata, "version")?.call1((DISTRIBUTION_NAME,)) { Ok(version) => version.extract(), - Err(error) if error.is_instance(py, &metadata.getattr("PackageNotFoundError")?) => { + Err(error) if error.is_instance(py, &native_field(&metadata, "PackageNotFoundError")?) => { Ok(UNKNOWN_VERSION.to_owned()) } Err(error) => Err(error), 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 f9ce48b8b1..ac95e6dfb9 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_command.rs @@ -14,6 +14,7 @@ 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::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::scope_lookup::build_scope_lookup; @@ -65,24 +66,24 @@ fn target_matches( ) -> PyResult<(BTreeSet, Vec)> { let mut bare: BTreeSet = BTreeSet::new(); let mut prefixed: BTreeSet = BTreeSet::new(); - for identity in lookup.getattr("resources")?.try_iter()? { + for identity in native_field(lookup, "resources")?.try_iter()? { let identity: Bound<'_, PyAny> = identity?; - if identity.getattr("name")?.extract::()? != target { + if native_field(&identity, "name")?.extract::()? != target { continue; } - let kind: String = identity.getattr("kind")?.str()?.extract()?; + let kind: String = native_field(&identity, "kind")?.str()?.extract()?; if BARE_KINDS.contains(&kind.as_str()) { bare.insert(kind); } else { prefixed.insert(format!("{kind}:{target}")); } } - for identity in lookup.getattr("declarations")?.try_iter()? { + for identity in native_field(lookup, "declarations")?.try_iter()? { let identity: Bound<'_, PyAny> = identity?; - if identity.getattr("owner")?.is_none() - && identity.getattr("name")?.extract::()? == target + if native_field(&identity, "owner")?.is_none() + && native_field(&identity, "name")?.extract::()? == target { - let kind: String = identity.getattr("kind")?.str()?.extract()?; + let kind: String = native_field(&identity, "kind")?.str()?.extract()?; prefixed.insert(format!("{kind}:{target}")); } } @@ -132,26 +133,26 @@ fn qualify_target( fn text_request(request: &Bound<'_, PyAny>, target: Option) -> PyResult { let mut kinds: Vec = Vec::new(); - for kind in request.getattr("kinds")?.try_iter()? { + for kind in native_field(request, "kinds")?.try_iter()? { kinds.push(kind?.extract()?); } Ok(ScopeTextRequest { target, - at: request.getattr("at")?.extract()?, - as_path: request.getattr("as_path")?.extract()?, - defined_under: request.getattr("defined_under")?.extract()?, + at: native_field(request, "at")?.extract()?, + as_path: native_field(request, "as_path")?.extract()?, + defined_under: native_field(request, "defined_under")?.extract()?, kinds, - glob: request.getattr("match")?.extract()?, - used_only: request.getattr("used_only")?.extract()?, - include_nearby: request.getattr("include_nearby")?.extract()?, - nearby_depth: request.getattr("nearby_depth")?.extract()?, - dependency_depth: request.getattr("dependency_depth")?.extract()?, - explain: request.getattr("explain")?.extract()?, - globals: request.getattr("globals")?.extract()?, - page_size: request.getattr("page_size")?.extract()?, - paths: request.getattr("paths")?.extract()?, - verbose: request.getattr("verbose")?.extract()?, - no_cache: request.getattr("no_cache")?.extract()?, + glob: native_field(request, "match")?.extract()?, + used_only: native_field(request, "used_only")?.extract()?, + include_nearby: native_field(request, "include_nearby")?.extract()?, + nearby_depth: native_field(request, "nearby_depth")?.extract()?, + dependency_depth: native_field(request, "dependency_depth")?.extract()?, + explain: native_field(request, "explain")?.extract()?, + globals: native_field(request, "globals")?.extract()?, + page_size: native_field(request, "page_size")?.extract()?, + paths: native_field(request, "paths")?.extract()?, + verbose: native_field(request, "verbose")?.extract()?, + no_cache: native_field(request, "no_cache")?.extract()?, }) } @@ -161,22 +162,25 @@ fn report_filters<'py>( ) -> PyResult> { let kind_class: Bound<'_, PyAny> = registry::class(py, RecordId::DeclarationKind)?; let mut kinds: Vec> = Vec::new(); - for kind in request.getattr("kinds")?.try_iter()? { + for kind in native_field(request, "kinds")?.try_iter()? { kinds.push(kind_class.call1((kind?,))?); } let options: Bound<'_, PyDict> = keywords( py, &[ - ("include_nearby", request.getattr("include_nearby")?), - ("defined_under", request.getattr("defined_under")?), + ("include_nearby", native_field(request, "include_nearby")?), + ("defined_under", native_field(request, "defined_under")?), ("kinds", PyTuple::new(py, kinds)?.into_any()), - ("glob", request.getattr("match")?), - ("used_only", request.getattr("used_only")?), - ("dependency_depth", request.getattr("dependency_depth")?), - ("cursor", request.getattr("after")?), - ("page_size", request.getattr("page_size")?), - ("nearby_depth", request.getattr("nearby_depth")?), - ("globals", request.getattr("globals")?), + ("glob", native_field(request, "match")?), + ("used_only", native_field(request, "used_only")?), + ( + "dependency_depth", + native_field(request, "dependency_depth")?, + ), + ("cursor", native_field(request, "after")?), + ("page_size", native_field(request, "page_size")?), + ("nearby_depth", native_field(request, "nearby_depth")?), + ("globals", native_field(request, "globals")?), ], )?; registry::class(py, RecordId::ScopeReportFilters)?.call((), Some(&options)) @@ -201,8 +205,8 @@ fn merged<'py>( (diagnostics, complete): (Bound<'py, PyAny>, bool), ) -> PyResult> { let py: Python<'py> = report.py(); - let combined: Bound<'py, PyAny> = report.getattr("diagnostics")?.add(diagnostics)?; - let complete: bool = report.getattr("complete")?.extract::()? && complete; + let combined: Bound<'py, PyAny> = native_field(report, "diagnostics")?.add(diagnostics)?; + let complete: bool = native_field(report, "complete")?.extract::()? && complete; let changes: Bound<'py, PyDict> = keywords( py, &[ @@ -230,9 +234,9 @@ fn query_result<'py>( lookup: &Bound<'py, PyAny>, (request, target): (&Bound<'py, PyAny>, Option), ) -> PyResult> { - let at: Option = request.getattr("at")?.extract()?; + let at: Option = native_field(request, "at")?.extract()?; let prospective: bool = at.is_some(); - let browse: Option = request.getattr("browse")?.extract()?; + let browse: Option = native_field(request, "browse")?.extract()?; if let Some(folder) = browse { return Ok(scope_browse( py, @@ -242,7 +246,7 @@ fn query_result<'py>( .into_bound(py)); } let filters: Bound<'_, PyAny> = report_filters(py, request)?; - let list_path: Option = request.getattr("list_path")?.extract()?; + let list_path: Option = native_field(request, "list_path")?.extract()?; if let Some(folder) = list_path { return Ok(scope_list( py, @@ -270,7 +274,7 @@ fn query_result<'py>( ), )? .into_bound(py); - let explain: Option = request.getattr("explain")?.extract()?; + let explain: Option = native_field(request, "explain")?.extract()?; if let Some(declaration) = explain { let (explanation, diagnostics): (Bound<'_, PyAny>, Bound<'_, PyAny>) = scope_explain( py, @@ -285,14 +289,14 @@ fn query_result<'py>( .map(|(explanation, diagnostics)| { (explanation.into_bound(py), diagnostics.into_bound(py)) })?; - let complete: bool = explanation.getattr("complete")?.extract()?; + let complete: bool = native_field(&explanation, "complete")?.extract()?; report = merged( &report, ("explanation", explanation), (diagnostics, complete), )?; } - let as_path: Option = request.getattr("as_path")?.extract()?; + let as_path: Option = native_field(request, "as_path")?.extract()?; if let Some(destination) = as_path { let (preview, diagnostics): (Bound<'_, PyAny>, Bound<'_, PyAny>) = scope_move_preview( py, @@ -309,7 +313,7 @@ fn query_result<'py>( diagnostics.into_bound(py), ) })?; - let complete: bool = !preview.is_none() && preview.getattr("complete")?.extract()?; + let complete: bool = !preview.is_none() && native_field(&preview, "complete")?.extract()?; report = merged(&report, ("move_preview", preview), (diagnostics, complete))?; } Ok(report) @@ -320,7 +324,7 @@ fn use_color( request: &Bound<'_, PyAny>, output_stream: Option<&Bound<'_, PyAny>>, ) -> PyResult { - if output_stream.is_some() || request.getattr("no_color")?.extract::()? { + if output_stream.is_some() || native_field(request, "no_color")?.extract::()? { return Ok(false); } stdout_supports_color(py) @@ -335,7 +339,7 @@ fn run_scope_command( load_scope_index: Option>, output_stream: Option>, ) -> PyResult { - let project_dir: Bound<'_, PyAny> = match request.getattr("project_dir")? { + 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")?, }; @@ -347,17 +351,17 @@ fn run_scope_command( py, &[ ("project_dir", project_dir), - ("no_cache", request.getattr("no_cache")?), + ("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, request.getattr("target")?.extract()?)?; + qualify_target(py, &lookup, native_field(request, "target")?.extract()?)?; let result: Bound<'_, PyAny> = query_result(py, &lookup, (request, target.clone()))?; let classes: ScopeClasses<'_> = ScopeClasses::new(py)?; let (output, native_result): (String, ScopeResult) = - if request.getattr("json_output")?.extract()? { + if native_field(request, "json_output")?.extract()? { let output: String = serialize_scope_report(py, &result)?; (output, read_result(&classes, &result)?) } else { @@ -387,7 +391,7 @@ fn render_scope_result( use_color: bool, ) -> PyResult { let classes: ScopeClasses<'_> = ScopeClasses::new(py)?; - let target: Option = request.getattr("target")?.extract()?; + let target: Option = native_field(request, "target")?.extract()?; Ok(render_scope_text( &read_result(&classes, result)?, &text_request(request, target)?, 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 ecc9d77def..663f1e4a99 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_errors.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_errors.rs @@ -1,5 +1,6 @@ //! Report a scope index's error findings as individual compile diagnostics. +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 pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; @@ -13,16 +14,20 @@ pub(crate) fn report_scope_index_errors(index: &Bound<'_, PyAny>) -> PyResult<() 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 index.getattr("diagnostics")?.try_iter()? { + for item in native_field(index, "diagnostics")?.try_iter()? { let item: Bound<'_, PyAny> = item?; - if item.getattr("severity")?.str()?.extract::()? != ERROR_SEVERITY { + if native_field(&item, "severity")? + .str()? + .extract::()? + != ERROR_SEVERITY + { continue; } - let code: Bound<'_, PyAny> = item.getattr("code")?.getattr("value")?; - let message: Bound<'_, PyAny> = item.getattr("message")?; - let path: Bound<'_, PyAny> = item.getattr("path")?; - let declaration: Bound<'_, PyAny> = item.getattr("declaration")?; - let resource: Bound<'_, PyAny> = item.getattr("resource")?; + let code: Bound<'_, PyAny> = native_field(&item, "code")?.getattr("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")?; + let resource: Bound<'_, PyAny> = native_field(&item, "resource")?; let key: Bound<'_, PyTuple> = PyTuple::new( py, [ @@ -46,8 +51,8 @@ pub(crate) fn report_scope_index_errors(index: &Bound<'_, PyAny>) -> PyResult<() } else { fields.set_item("path", path_class.call1((path,))?)?; } - fields.set_item("line", item.getattr("line")?)?; - fields.set_item("column", item.getattr("column")?)?; + 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)?; @@ -71,7 +76,7 @@ fn owning_resource( declaration: &Bound<'_, PyAny>, ) -> PyResult<(Option<&'static str>, Option)> { if !resource.is_none() { - let kind: String = resource.getattr("kind")?.str()?.extract()?; + let kind: String = native_field(resource, "kind")?.str()?.extract()?; let resource_type: Option<&'static str> = match kind.as_str() { "model" => Some("model"), "source" => Some("source"), @@ -82,13 +87,19 @@ fn owning_resource( _ => None, }; if resource_type.is_some() { - return Ok((resource_type, Some(resource.getattr("name")?.extract()?))); + return Ok(( + resource_type, + Some(native_field(resource, "name")?.extract()?), + )); } } if !declaration.is_none() { - let kind: String = declaration.getattr("kind")?.str()?.extract()?; + let kind: String = native_field(declaration, "kind")?.str()?.extract()?; if matches!(kind.as_str(), "audit" | "singular_audit") { - return Ok((Some("audit"), Some(declaration.getattr("name")?.extract()?))); + return Ok(( + Some("audit"), + Some(native_field(declaration, "name")?.extract()?), + )); } } Ok((None, None)) 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 21bd4d6cfc..6e8970b78b 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_index.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_index.rs @@ -20,6 +20,7 @@ use sqlbuild_scopes::scope_index::models::{ }; use crate::bindings::_helpers::boundary::panics::compiler_error; +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::scopes::relationship_scan::{ @@ -92,7 +93,7 @@ fn optional_text(item: &Bound<'_, PyAny>, name: &str) -> PyResult } fn stem(path: &Bound<'_, PyAny>) -> PyResult { - path.getattr("stem")?.extract() + native_field(path, "stem")?.extract() } /// `value or fallback` with Python truthiness. @@ -118,8 +119,7 @@ fn name_or_stem(value: &Bound<'_, PyAny>, path: &Bound<'_, PyAny>) -> PyResult(file: &Bound<'py, PyAny>) -> PyResult> { - file.getattr("header_values")? - .call_method1("get", ("name",)) + native_field(file, "header_values")?.call_method1("get", ("name",)) } fn items<'py>(item: &Bound<'py, PyAny>, name: &str) -> PyResult>> { @@ -194,7 +194,7 @@ impl<'py> Walk<'py> { placement: Placement, ) -> PyResult<()> { let identity: DeclarationIdentity = self.native_identity(&values.identity)?; - let scope_text: String = values.scope.getattr("value")?.extract()?; + let scope_text: String = native_field(&values.scope, "value")?.extract()?; let scope: ScopeKind = ScopeKind::parse(&scope_text).unwrap_or_else(|| { self.indexable = false; ScopeKind::Global @@ -251,34 +251,34 @@ impl<'py> Walk<'py> { let member_class: Bound<'py, PyAny> = self.classes.model(RecordId::EnumMemberMetadata)?; let members: Vec> = items(declaration, "members")? .iter() - .map(|member| member_class.call1((member.getattr("name")?,))) + .map(|member| member_class.call1((native_field(member, "name")?,))) .collect::>()?; self.classes.model(RecordId::EnumMetadata)?.call1(( PyTuple::new(declaration.py(), members)?, - declaration.getattr("scalar_type")?, + native_field(declaration, "scalar_type")?, )) } fn constant_metadata(&self, declaration: &Bound<'py, PyAny>) -> PyResult> { - let value: Bound<'py, PyAny> = declaration.getattr("value")?; - let kind: Bound<'py, PyAny> = value.getattr("kind")?; - let kind_text: String = kind.getattr("value")?.extract()?; + let value: Bound<'py, PyAny> = native_field(declaration, "value")?; + let kind: Bound<'py, PyAny> = native_field(&value, "kind")?; + let kind_text: String = native_field(&kind, "value")?.extract()?; let is_collection: bool = COLLECTION_KINDS.contains(&kind_text.as_str()); - let payload: Bound<'py, PyAny> = value.getattr("value")?; + let payload: Bound<'py, PyAny> = native_field(&value, "value")?; let item_count: Option = if is_collection && payload.is_instance_of::() { Some(payload.len()?) } else { None }; let render_as: Option> = optional_item(declaration, "render_as")? - .map(|rendering| rendering.getattr("value")) + .map(|rendering| native_field(&rendering, "value")) .transpose()?; self.classes.model(RecordId::ConstantMetadata)?.call1(( declaration .getattr("logical_type")? .getattr("display_name")?, if is_collection { - Some(kind.getattr("value")?) + Some(native_field(&kind, "value")?) } else { None }, @@ -290,7 +290,7 @@ impl<'py> Walk<'py> { fn add_models(&mut self, inputs: &Bound<'py, PyAny>) -> PyResult<()> { for model_file in items(inputs, "model_files")? { - let path: Bound<'py, PyAny> = model_file.getattr("relative_path")?; + let path: Bound<'py, PyAny> = native_field(&model_file, "relative_path")?; let model_name: String = name_or_stem(&header_name(&model_file)?, &path)?; self.add_resource( ResourceKind::Model, @@ -309,7 +309,7 @@ impl<'py> Walk<'py> { for declaration in items(&model_file, "enum_declarations")? { let identity: Bound<'py, PyAny> = self.declaration_identity( DeclarationKind::Enum, - &declaration.getattr("name")?, + &native_field(&declaration, "name")?, Some(&owner), )?; let metadata = [None, Some(self.enum_metadata(&declaration)?), None]; @@ -319,7 +319,7 @@ impl<'py> Walk<'py> { for declaration in items(&model_file, "constant_declarations")? { let identity: Bound<'py, PyAny> = self.declaration_identity( DeclarationKind::Constant, - &declaration.getattr("name")?, + &native_field(&declaration, "name")?, Some(&owner), )?; let metadata = [None, None, Some(self.constant_metadata(&declaration)?)]; @@ -341,17 +341,18 @@ impl<'py> Walk<'py> { path: text(path)?, ownership_root: Some(PRIVATE_ROOT.to_owned()), root_fallback: PRIVATE_ROOT.to_owned(), - owning_path: Some(text(&path.getattr("parent")?)?), + owning_path: Some(text(&native_field(path, "parent")?)?), }, ) } fn add_resources(&mut self, inputs: &Bound<'py, PyAny>) -> PyResult<()> { for test_file in items(inputs, "test_files")? { - let path: Bound<'py, PyAny> = test_file.getattr("relative_path")?; + let path: Bound<'py, PyAny> = native_field(&test_file, "relative_path")?; for block in items(&test_file, "blocks")? { - let name: Bound<'py, PyAny> = - or_else(block.getattr("name")?, || path.getattr("stem"))?; + let name: Bound<'py, PyAny> = or_else(native_field(&block, "name")?, || { + native_field(&path, "stem") + })?; self.add_resource( ResourceKind::Test, name.extract()?, @@ -363,20 +364,23 @@ impl<'py> Walk<'py> { self.add_resource( ResourceKind::Scenario, field(&scenario, "name")?, - (&scenario.getattr("relative_path")?, ResourceRoot::Kind), + ( + &native_field(&scenario, "relative_path")?, + ResourceRoot::Kind, + ), )?; } for function in items(inputs, "sql_function_files")? { - let path: Bound<'py, PyAny> = function.getattr("relative_path")?; + let path: Bound<'py, PyAny> = native_field(&function, "relative_path")?; let name: String = name_or_stem(&header_name(&function)?, &path)?; self.add_resource(ResourceKind::Function, name, (&path, ResourceRoot::Kind))?; } for function in items(inputs, "python_function_files")? { self.add_resource( ResourceKind::Function, - stem(&function.getattr("file_path")?)?, + stem(&native_field(&function, "file_path")?)?, ( - &function.getattr("relative_path")?, + &native_field(&function, "relative_path")?, ResourceRoot::PythonFunction, ), )?; @@ -384,12 +388,12 @@ impl<'py> Walk<'py> { for seed in items(inputs, "seed_files")? { self.add_resource( ResourceKind::Seed, - stem(&seed.getattr("file_path")?)?, - (&seed.getattr("relative_path")?, ResourceRoot::Seed), + stem(&native_field(&seed, "file_path")?)?, + (&native_field(&seed, "relative_path")?, ResourceRoot::Seed), )?; } for source_file in items(inputs, "source_files")? { - let path: Bound<'py, PyAny> = source_file.getattr("relative_path")?; + let path: Bound<'py, PyAny> = native_field(&source_file, "relative_path")?; for source in items(&source_file, "source_entries")? { self.add_resource( ResourceKind::Source, @@ -403,7 +407,7 @@ impl<'py> Walk<'py> { fn file_placement(file: &Bound<'py, PyAny>, root_fallback: String) -> PyResult { Ok(Placement { - path: text(&file.getattr("relative_path")?)?, + path: text(&native_field(file, "relative_path")?)?, ownership_root: optional_text(file, "ownership_root")?, root_fallback, owning_path: optional_text(file, "owning_path")?, @@ -415,11 +419,12 @@ impl<'py> Walk<'py> { for declaration in items(&enum_file, "declarations")? { let identity: Bound<'py, PyAny> = self.declaration_identity( DeclarationKind::Enum, - &declaration.getattr("name")?, + &native_field(&declaration, "name")?, None, )?; let metadata = [None, Some(self.enum_metadata(&declaration)?), None]; - let values = self.values(identity, enum_file.getattr("scope_kind")?, metadata); + let values = + self.values(identity, native_field(&enum_file, "scope_kind")?, metadata); let placement = Self::file_placement(&enum_file, ENUM_FALLBACK.to_owned())?; self.add_declaration(values, placement)?; } @@ -428,11 +433,15 @@ impl<'py> Walk<'py> { for declaration in items(&constant_file, "declarations")? { let identity: Bound<'py, PyAny> = self.declaration_identity( DeclarationKind::Constant, - &declaration.getattr("name")?, + &native_field(&declaration, "name")?, None, )?; let metadata = [None, None, Some(self.constant_metadata(&declaration)?)]; - let values = self.values(identity, constant_file.getattr("scope_kind")?, metadata); + let values = self.values( + identity, + native_field(&constant_file, "scope_kind")?, + metadata, + ); let placement = Self::file_placement(&constant_file, CONSTANT_FALLBACK.to_owned())?; self.add_declaration(values, placement)?; } @@ -447,9 +456,13 @@ impl<'py> Walk<'py> { name: &Bound<'py, PyAny>, ) -> PyResult<()> { let identity: Bound<'py, PyAny> = self.declaration_identity(kind, name, None)?; - let values = self.values(identity, item.getattr("scope_kind")?, [None, None, None]); - let fallback: Bound<'py, PyAny> = or_else(item.getattr("declaration_root")?, || { - item.getattr("relative_path")?.getattr("parent") + let values = self.values( + identity, + native_field(item, "scope_kind")?, + [None, None, None], + ); + let fallback: Bound<'py, PyAny> = or_else(native_field(item, "declaration_root")?, || { + native_field(item, "relative_path")?.getattr("parent") })?; let placement = Self::file_placement(item, text(&fallback)?)?; self.add_declaration(values, placement) @@ -459,14 +472,14 @@ 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> = - audit_file.getattr("relative_path")?.getattr("stem")?; + native_field(&audit_file, "relative_path")?.getattr("stem")?; if value_of(&audit_file, "declaration_kind")? == audit_kind { self.add_named(&audit_file, DeclarationKind::Audit, &stem_name)?; continue; } for block in items(&audit_file, "blocks")? { let name: Bound<'py, PyAny> = - or_else(block.getattr("name")?, || Ok(stem_name.clone()))?; + or_else(native_field(&block, "name")?, || Ok(stem_name.clone()))?; self.add_named(&audit_file, DeclarationKind::SingularAudit, &name)?; } } @@ -475,7 +488,7 @@ impl<'py> Walk<'py> { self.add_named( &schema_file, DeclarationKind::Schema, - &declaration.getattr("name")?, + &native_field(&declaration, "name")?, )?; } } @@ -483,11 +496,15 @@ impl<'py> Walk<'py> { self.add_named( &hook_file, DeclarationKind::SqlHook, - &hook_file.getattr("name")?, + &native_field(&hook_file, "name")?, )?; } for hook in items(inputs, "hook_functions")? { - self.add_named(&hook, DeclarationKind::PythonHook, &hook.getattr("name")?)?; + self.add_named( + &hook, + DeclarationKind::PythonHook, + &native_field(&hook, "name")?, + )?; } Ok(()) } @@ -498,17 +515,20 @@ impl<'py> Walk<'py> { source: (&Bound<'py, PyAny>, i64, Bound<'py, PyAny>), ) -> PyResult<(DeclarationValues<'py>, Placement)> { let (export, line, metadata) = source; - let identity: Bound<'py, PyAny> = - self.declaration_identity(DeclarationKind::Macro, &export.getattr("name")?, None)?; + let identity: Bound<'py, PyAny> = self.declaration_identity( + DeclarationKind::Macro, + &native_field(export, "name")?, + None, + )?; let scope: Bound<'py, PyAny> = match macro_file { - Some(file) => file.getattr("scope_kind")?, + Some(file) => native_field(file, "scope_kind")?, None => self .classes .member(RecordId::ScopeKind, ScopeKind::Global.as_str())?, }; let dependencies: Vec> = items(export, "dependencies")?; let placement: Placement = Placement { - path: text(&export.getattr("relative_path")?)?, + path: text(&native_field(export, "relative_path")?)?, ownership_root: match macro_file { Some(file) => optional_text(file, "ownership_root")?, None => None, @@ -533,7 +553,7 @@ impl<'py> Walk<'py> { fn loaded_metadata(&self, loaded: &Bound<'py, PyAny>) -> PyResult<(i64, Bound<'py, PyAny>)> { let py: Python<'py> = loaded.py(); - let function: Bound<'py, PyAny> = loaded.getattr("function")?; + let function: Bound<'py, PyAny> = native_field(loaded, "function")?; let parameters: Vec> = py .import(INSPECT_MODULE)? .getattr("signature")? @@ -544,13 +564,13 @@ impl<'py> Walk<'py> { let raw_source: String = field(loaded, "raw_source")?; let metadata: Bound<'py, PyAny> = self.classes.model(RecordId::MacroMetadata)?.call1(( PyTuple::new(py, parameters)?, - loaded.getattr("dependencies")?, + native_field(loaded, "dependencies")?, macro_source_digest(&raw_source), ))?; let code_type: Bound<'py, PyAny> = py.import(CODE_TYPE_MODULE)?.getattr("CodeType")?; let line: i64 = match function.getattr_opt("__code__")? { Some(code) if code.is_instance(&code_type)? => { - code.getattr("co_firstlineno")?.extract()? + native_field(&code, "co_firstlineno")?.extract()? } _ => 1, }; @@ -566,12 +586,12 @@ impl<'py> Walk<'py> { let py: Python<'py> = inputs.py(); let mut macro_files: HashMap> = HashMap::new(); for macro_file in items(inputs, "macro_files")? { - let key: String = normalized(py, ¯o_file.getattr("relative_path")?)?; + let key: String = normalized(py, &native_field(¯o_file, "relative_path")?)?; macro_files.insert(key, macro_file); } for loaded in loaded_macros.call_method0("values")?.try_iter()? { let loaded: Bound<'py, PyAny> = loaded?; - let key: String = normalized(py, &loaded.getattr("relative_path")?)?; + let key: String = normalized(py, &native_field(&loaded, "relative_path")?)?; let (line, metadata) = self.loaded_metadata(&loaded)?; let (values, placement) = self.macro_values(macro_files.get(&key), (&loaded, line, metadata))?; @@ -579,12 +599,12 @@ impl<'py> Walk<'py> { } for export in static_macros.try_iter()? { let export: Bound<'py, PyAny> = export?; - let key: String = normalized(py, &export.getattr("relative_path")?)?; + let key: String = normalized(py, &native_field(&export, "relative_path")?)?; let metadata: Bound<'py, PyAny> = self.classes.model(RecordId::MacroMetadata)?.call1(( - export.getattr("parameters")?, - export.getattr("dependencies")?, - export.getattr("source_digest")?, + native_field(&export, "parameters")?, + native_field(&export, "dependencies")?, + native_field(&export, "source_digest")?, ))?; let (values, placement) = self.macro_values( macro_files.get(&key), @@ -690,7 +710,7 @@ fn materialise<'py>(walk: Walk<'py>, native: ScopeIndex) -> PyResult> = Vec::with_capacity(native.usages.len()); for usage in &native.usages { usages.push(usage_class.call1(( - declarations[usage.consumer].getattr("identity")?, + native_field(&declarations[usage.consumer], "identity")?, &walk.declarations[usage.consumer].dependencies[usage.dependency], &dependency_kind, ))?); @@ -706,14 +726,14 @@ fn materialise<'py>(walk: Walk<'py>, native: ScopeIndex) -> PyResult(walk: Walk<'py>, native: ScopeIndex) -> PyResult) -> PyResult { - let path: Bound<'_, PyAny> = test_file.getattr("relative_path")?; + let path: Bound<'_, PyAny> = native_field(test_file, "relative_path")?; let mut blocks: Vec<(String, String, String)> = Vec::new(); for block in items(test_file, "blocks")? { - let name: Bound<'_, PyAny> = or_else(block.getattr("name")?, || path.getattr("stem"))?; + let name: Bound<'_, PyAny> = or_else(native_field(&block, "name")?, || { + native_field(&path, "stem") + })?; blocks.push(( name.extract()?, field(&block, "sql_body")?, @@ -792,7 +814,7 @@ fn test_file_row(test_file: &Bound<'_, PyAny>) -> PyResult { fn scenario_row(scenario: &Bound<'_, PyAny>) -> PyResult { Ok(( field(scenario, "name")?, - text(&scenario.getattr("relative_path")?)?, + text(&native_field(scenario, "relative_path")?)?, field(scenario, "sql_body")?, )) } @@ -830,9 +852,9 @@ impl NativeScopeIndex { .collect::>()?; let (store, algorithm) = scan_store; let settings: Bound<'_, PyDict> = PyDict::new(py); - settings.set_item("syntax", syntax.getattr("native_mapping")?)?; + settings.set_item("syntax", native_field(syntax, "native_mapping")?)?; settings.set_item("store", store)?; - settings.set_item("scan_key", (algorithm, syntax.getattr("cache_key")?))?; + settings.set_item("scan_key", (algorithm, native_field(syntax, "cache_key")?))?; let settings: RelationshipScanSettings = settings.extract()?; let scan: RelationshipScan = scanned_relationships(py, &files, &scenarios, settings)?; let index: &ScopeIndex = &self.native; @@ -889,7 +911,7 @@ impl NativeScopeIndex { fn index_with_relationships(&self, py: Python<'_>) -> PyResult> { let index: &Bound<'_, PyAny> = self.index.bind(py); let completeness: Bound<'_, PyAny> = replace( - &index.getattr("completeness")?, + &native_field(index, "completeness")?, &[("relationships", true.into_bound_py_any(py)?)], )?; Ok(replace( 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 4464779cb3..271d0ccd10 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_loading.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_loading.rs @@ -19,6 +19,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::scopes::scope_cache::{ @@ -59,8 +60,8 @@ fn project_adapter<'py>( let discovered: Bound<'py, PyAny> = registry::native(py, NativeAttr::DiscoverProjectInputs)? .call((), Some(&discover_keywords))?; let adapter_name: Bound<'py, PyAny> = resolve_effective_adapter_name( - &discovered.getattr("project_config")?, - &discovered.getattr("local_config")?, + &native_field(&discovered, "project_config")?, + &native_field(&discovered, "local_config")?, ) .and_then(|value| value.into_bound_py_any(py))?; let adapter: Bound<'py, PyAny> = call_with( @@ -77,7 +78,7 @@ fn compiled_index<'py>( syntax: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = discovered.py(); - let project_config: Bound<'py, PyAny> = discovered.getattr("project_config")?; + let project_config: Bound<'py, PyAny> = native_field(&discovered, "project_config")?; 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))?; @@ -127,10 +128,10 @@ fn tolerant_index<'py>( ) -> PyResult> { let py: Python<'py> = project_dir.py(); let snapshot: Bound<'py, PyAny> = discover_scope_snapshot(project_dir)?; - let discovered: Bound<'py, PyAny> = snapshot.getattr("discovered_inputs")?; + let discovered: Bound<'py, PyAny> = native_field(&snapshot, "discovered_inputs")?; let inventory: Bound<'py, PyAny> = call_with( &host::entry(py, UserCodeHost::InventoryProjectMacros)?, - &[("macro_files", discovered.getattr("macro_files")?)], + &[("macro_files", native_field(&discovered, "macro_files")?)], )?; let native: Bound<'py, PyAny> = Bound::new( py, @@ -138,7 +139,7 @@ fn tolerant_index<'py>( py, &discovered, &PyDict::new(py).into_any(), - Some(inventory.getattr("exports")?), + Some(native_field(&inventory, "exports")?), )?, )? .into_any(); @@ -150,7 +151,7 @@ fn tolerant_index<'py>( ], )?; let relationship_faults: Bound<'py, PyAny> = call_with( - &native.getattr("grant_relationships")?, + &native_field(&native, "grant_relationships")?, &[ ("discovered_inputs", discovered), ("syntax", syntax.clone()), @@ -161,7 +162,7 @@ fn tolerant_index<'py>( py, &native.call_method0("index_with_relationships")?, &snapshot, - (inventory.getattr("faults")?, relationship_faults), + (native_field(&inventory, "faults")?, relationship_faults), )? .into_bound(py)) } @@ -188,7 +189,7 @@ fn load_or_build_scope_index<'py>( let mut syntax: Bound<'py, PyAny> = registry::class(py, RecordId::SqlLexicalSyntax)?.call0()?; let compiled: PyResult> = match project_adapter(&resolved) { Ok((discovered, adapter)) => { - syntax = adapter.getattr("sql_lexical_syntax")?; + syntax = native_field(&adapter, "sql_lexical_syntax")?; compiled_index(discovered, &adapter, &syntax) } Err(error) => Err(error), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_lookup.rs b/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_lookup.rs index 1f7cd1852f..7e06318cc3 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_lookup.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_lookup.rs @@ -17,6 +17,7 @@ use sqlbuild_scopes::scope_index::models::{ }; use crate::bindings::_helpers::boundary::panics::compiler_error; +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::scopes::placement::{QualifiedRow, qualified_identity}; @@ -242,7 +243,7 @@ fn visibility_index<'py>( let py: Python<'py> = canonical_declarations.py(); let identities: Vec> = canonical_declarations .iter() - .map(|record| record.getattr("identity")) + .map(|record| native_field(&record, "identity")) .collect::>()?; let private: Bound<'py, PyDict> = PyDict::new(py); for (owner, positions) in &visibility.private { @@ -290,10 +291,10 @@ pub(crate) fn build_scope_lookup(py: Python<'_>, index: &Bound<'_, PyAny>) -> Py &canonical_declarations, picked(&usages, &lookup.usages)?, picked(&grants, &lookup.grants)?, - index.getattr("visibility")?, - index.getattr("inaccessible")?, + native_field(index, "visibility")?, + native_field(index, "inaccessible")?, picked(&tuple_attribute(index, "diagnostics")?, &lookup.diagnostics)?, - index.getattr("completeness")?, + native_field(index, "completeness")?, ))?; let groups = &lookup.groups; let mappings: [Bound<'_, PyAny>; 6] = [ 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 a2ebbd44cf..60fd2ec87e 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_objects.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_objects.rs @@ -17,6 +17,7 @@ use sqlbuild_scopes::scope_report::models::{ ProjectionGrant, ProjectionIndex, ProjectionStanding, ProjectionUsage, ReportValues, }; +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::{ @@ -58,13 +59,13 @@ impl<'py> ScopeClasses<'py> { pub(crate) fn enum_text(value: &Bound<'_, PyAny>) -> PyResult { let enum_type = value.py().import(ENUM_MODULE)?.getattr("Enum")?; if value.is_instance(&enum_type)? { - return value.getattr("value")?.str()?.extract(); + return native_field(value, "value")?.str()?.extract(); } value.str()?.extract() } pub(crate) fn value_of(item: &Bound<'_, PyAny>, name: &str) -> PyResult { - item.getattr(name)?.getattr("value")?.extract() + native_field(&item.getattr(name)?, "value")?.extract() } pub(crate) fn field<'py, T: pyo3::FromPyObject<'py>>( @@ -126,7 +127,7 @@ fn read_optional_qualified( fn root_row(root: &Bound<'_, PyAny>) -> PyResult { let resource_kind: Option = optional_item(root, "resource_kind")? - .map(|kind| kind.getattr("value")?.extract()) + .map(|kind| native_field(&kind, "value")?.extract()) .transpose()?; Ok((field(root, "path")?, value_of(root, "kind")?, resource_kind)) } @@ -138,9 +139,9 @@ fn read_root(root: &Bound<'_, PyAny>) -> PyResult { /// One `ResourceRecord` as its native record. pub(crate) fn read_resource(record: &Bound<'_, PyAny>) -> PyResult { Ok(ResourceRecord { - identity: read_resource_identity(&record.getattr("identity")?)?, + identity: read_resource_identity(&native_field(record, "identity")?)?, path: field(record, "path")?, - ownership_root: read_root(&record.getattr("ownership_root")?)?, + ownership_root: read_root(&native_field(record, "ownership_root")?)?, }) } @@ -179,12 +180,12 @@ fn read_constant(facts: &Bound<'_, PyAny>) -> PyResult { pub(crate) fn read_declaration(record: &Bound<'_, PyAny>) -> PyResult { Ok(CachedDeclaration { record: declaration_record(( - qualified_row(&record.getattr("identity")?)?, + qualified_row(&native_field(record, "identity")?)?, field(record, "path")?, field(record, "line")?, field(record, "column")?, value_of(record, "scope")?, - root_row(&record.getattr("ownership_root")?)?, + root_row(&native_field(record, "ownership_root")?)?, field(record, "owning_path")?, ))?, macro_facts: optional_item(record, "macro")? @@ -201,8 +202,8 @@ pub(crate) fn read_declaration(record: &Bound<'_, PyAny>) -> PyResult) -> PyResult { Ok(CachedUsage { - consumer: read_qualified(&usage.getattr("consumer")?)?, - declaration: read_declaration_identity(&usage.getattr("declaration")?)?, + consumer: read_qualified(&native_field(usage, "consumer")?)?, + declaration: read_declaration_identity(&native_field(usage, "declaration")?)?, kind: value_of(usage, "kind")?, through: read_optional_qualified(usage, "through")?, enum_member: field(usage, "enum_member")?, @@ -211,17 +212,17 @@ fn read_usage(usage: &Bound<'_, PyAny>) -> PyResult { fn read_grant(grant: &Bound<'_, PyAny>) -> PyResult { Ok(ReportGrant { - resource: read_resource_identity(&grant.getattr("resource")?)?, - declaration: read_declaration_identity(&grant.getattr("declaration")?)?, - through: read_qualified(&grant.getattr("through")?)?, + resource: read_resource_identity(&native_field(grant, "resource")?)?, + declaration: read_declaration_identity(&native_field(grant, "declaration")?)?, + through: read_qualified(&native_field(grant, "through")?)?, kind: grant_kind(&value_of(grant, "kind")?)?, }) } fn read_visibility(fact: &Bound<'_, PyAny>) -> PyResult { Ok(CachedVisibility { - resource: read_resource_identity(&fact.getattr("resource")?)?, - declaration: read_declaration_identity(&fact.getattr("declaration")?)?, + resource: read_resource_identity(&native_field(fact, "resource")?)?, + declaration: read_declaration_identity(&native_field(fact, "declaration")?)?, reason: value_of(fact, "reason")?, through: read_optional_qualified(fact, "through")?, }) @@ -229,8 +230,8 @@ fn read_visibility(fact: &Bound<'_, PyAny>) -> PyResult { fn read_inaccessible(fact: &Bound<'_, PyAny>) -> PyResult { Ok(CachedInaccessible { - resource: read_resource_identity(&fact.getattr("resource")?)?, - declaration: read_declaration_identity(&fact.getattr("declaration")?)?, + resource: read_resource_identity(&native_field(fact, "resource")?)?, + declaration: read_declaration_identity(&native_field(fact, "declaration")?)?, reason: value_of(fact, "reason")?, }) } @@ -238,9 +239,9 @@ fn read_inaccessible(fact: &Bound<'_, PyAny>) -> PyResult { /// One `ScopeDiagnostic`, with its code and severity as their text. pub(crate) fn read_diagnostic(diagnostic: &Bound<'_, PyAny>) -> PyResult { Ok(CachedDiagnostic { - code: enum_text(&diagnostic.getattr("code")?)?, + code: enum_text(&native_field(diagnostic, "code")?)?, message: field(diagnostic, "message")?, - severity: enum_text(&diagnostic.getattr("severity")?)?, + severity: enum_text(&native_field(diagnostic, "severity")?)?, path: field(diagnostic, "path")?, line: field(diagnostic, "line")?, column: field(diagnostic, "column")?, @@ -255,7 +256,7 @@ pub(crate) fn read_diagnostic(diagnostic: &Bound<'_, PyAny>) -> PyResult) -> PyResult<[bool; 6]> { - let done: Bound<'_, PyAny> = index.getattr("completeness")?; + let done: Bound<'_, PyAny> = native_field(index, "completeness")?; Ok([ field(&done, "discovery")?, field(&done, "static_visibility")?, @@ -298,8 +299,8 @@ pub(crate) fn read_placement_index(index: &Bound<'_, PyAny>) -> PyResult) -> PyResult { .getattr("Enum") .and_then(|enum_type| value.is_instance(&enum_type))? { - return python_to_json(&value.getattr("value")?); + return python_to_json(&native_field(value, "value")?); } if value.is_instance_of::() { let decimal: String = value.str()?.extract()?; @@ -420,9 +421,9 @@ pub(crate) fn list_object<'py>( } fn read_declaration_report(report: &Bound<'_, PyAny>) -> PyResult { - let definition: Bound<'_, PyAny> = report.getattr("definition")?; + let definition: Bound<'_, PyAny> = native_field(report, "definition")?; let mut metadata: Vec<(String, JsonValue)> = Vec::new(); - for entry in report.getattr("metadata")?.try_iter()? { + for entry in native_field(report, "metadata")?.try_iter()? { let (key, value): (Bound<'_, PyAny>, Bound<'_, PyAny>) = entry?.extract()?; metadata.push((key.str()?.extract()?, python_to_json(&value)?)); } @@ -484,7 +485,7 @@ fn read_section(section: &Bound<'_, PyAny>) -> PyResult { fn read_folder(folder: &Bound<'_, PyAny>) -> PyResult { let mut kind_counts: Vec<(String, i64)> = Vec::new(); - for entry in folder.getattr("kind_counts")?.try_iter()? { + for entry in native_field(folder, "kind_counts")?.try_iter()? { let (kind, count): (Bound<'_, PyAny>, i64) = entry?.extract()?; let kind: String = kind.str()?.extract()?; match kind_counts @@ -506,7 +507,7 @@ fn read_folder(folder: &Bound<'_, PyAny>) -> PyResult { } fn read_diagnostics(item: &Bound<'_, PyAny>) -> PyResult> { - item.getattr("diagnostics")? + native_field(item, "diagnostics")? .try_iter()? .map(|diagnostic| { Ok(ReportDiagnostic::Raised(diagnostic_facts(read_diagnostic( @@ -541,7 +542,7 @@ fn read_move(preview: &Bound<'_, PyAny>) -> PyResult { } fn read_report(report: &Bound<'_, PyAny>) -> PyResult { - let resource: Bound<'_, PyAny> = report.getattr("resource")?; + let resource: Bound<'_, PyAny> = native_field(report, "resource")?; Ok(ScopeReport { resource: ResourceReport { target: field(&resource, "target")?, @@ -567,7 +568,7 @@ fn read_report(report: &Bound<'_, PyAny>) -> PyResult { used: read_declaration_reports(report, "used")?, relationship_scope: read_declaration_reports(report, "relationship_scope")?, nearby_unavailable: read_declaration_reports(report, "nearby_unavailable")?, - filters: read_filters(&report.getattr("filters")?)?, + filters: read_filters(&native_field(report, "filters")?)?, sections: report .getattr("sections")? .try_iter()? @@ -607,7 +608,7 @@ pub(crate) fn read_result( Ok(ScopeResult::List(ListResult { folder: field(result, "folder")?, declarations: read_declaration_reports(result, "declarations")?, - section: read_section(&result.getattr("section")?)?, + section: read_section(&native_field(result, "section")?)?, diagnostics: read_diagnostics(result)?, })) } 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 1bc4610d7b..909afc238f 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_reports.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_reports.rs @@ -21,6 +21,7 @@ use sqlbuild_scopes::scope_report::models::{ }; 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::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::scopes::scope_lookup::{ @@ -80,8 +81,8 @@ impl LookupReports { /// The reports of a `ScopeLookup`: its native lookup with its canonical declarations' values. fn lookup_reports(lookup: &Bound<'_, PyAny>) -> PyResult { - let index: Bound<'_, PyAny> = lookup.getattr("index")?; - let native: Py = lookup.getattr("native")?.extract()?; + let index: Bound<'_, PyAny> = native_field(lookup, "index")?; + let native: Py = native_field(lookup, "native")?.extract()?; let rows: Vec = index .getattr("declarations")? .try_iter()? @@ -164,7 +165,8 @@ pub(crate) fn scope_report( let native: &NativeScopeLookup = reports.lookup.get(); match build_scope_report(reports.source(native), &request) { Ok(report) => { - let diagnostics: Bound<'_, PyAny> = lookup.getattr("index")?.getattr("diagnostics")?; + let diagnostics: Bound<'_, PyAny> = + native_field(lookup, "index")?.getattr("diagnostics")?; Ok(report_object(&classes, report, (&selected, &diagnostics))?.unbind()) } Err(failure) => Err(failure_error(py, &failure)?), @@ -190,7 +192,7 @@ pub(crate) fn scope_explain( match explain_scope_declaration(reports.source(native), &request) { Ok((explanation, diagnostics)) => { let index_diagnostics: Bound<'_, PyAny> = - lookup.getattr("index")?.getattr("diagnostics")?; + native_field(lookup, "index")?.getattr("diagnostics")?; Ok(( explanation_object(&classes, explanation)?.unbind(), diagnostics_object(&classes, &index_diagnostics, raised(diagnostics))?.unbind(), @@ -228,7 +230,7 @@ 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> = lookup.getattr("index")?.getattr("diagnostics")?; + let diagnostics: Bound<'_, PyAny> = native_field(lookup, "index")?.getattr("diagnostics")?; Ok(browse_object(&classes, result, &diagnostics)?.unbind()) } @@ -246,7 +248,8 @@ pub(crate) fn scope_list( let native: &NativeScopeLookup = reports.lookup.get(); match list_scope_declarations(reports.source(native), &request) { Ok(result) => { - let diagnostics: Bound<'_, PyAny> = lookup.getattr("index")?.getattr("diagnostics")?; + let diagnostics: Bound<'_, PyAny> = + native_field(lookup, "index")?.getattr("diagnostics")?; Ok(list_object(&classes, result, &diagnostics)?.unbind()) } Err(failure) => Err(failure_error(py, &failure)?), @@ -272,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> = - lookup.getattr("index")?.getattr("diagnostics")?; + native_field(lookup, "index")?.getattr("diagnostics")?; Ok(( preview .map(|preview| Ok::<_, PyErr>(move_object(&classes, preview)?.unbind())) 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 09e9c5c0d7..36ed52f3c4 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_tolerant.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_tolerant.rs @@ -8,6 +8,7 @@ use sqlbuild_scopes::scope_index::models::{ OwnershipRoot, OwnershipRootKind, ResourceIdentity, ResourceKind, ResourceRecord, }; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::scopes::scope_objects::{ ScopeClasses, enum_text, field, optional_item, read_resource, root_object, tuple_attribute, @@ -82,7 +83,7 @@ fn order_key(diagnostic: &Bound<'_, PyAny>) -> PyResult { path: field(diagnostic, "path")?, line: field(diagnostic, "line")?, column: field(diagnostic, "column")?, - code: enum_text(&diagnostic.getattr("code")?)?, + code: enum_text(&native_field(diagnostic, "code")?)?, message: field(diagnostic, "message")?, }) } @@ -95,7 +96,7 @@ fn prospective_resource(path: &Bound<'_, PyAny>) -> PyResult(py: Python<'py>, items: Vec>) -> PyResult) -> PyResult { Ok(( - qualified_row(&usage.getattr("consumer")?)?, - qualified_row(&usage.getattr("declaration")?)?, + qualified_row(&native_field(usage, "consumer")?)?, + qualified_row(&native_field(usage, "declaration")?)?, value_of(usage, "kind")?, optional_item(usage, "through")? .map(|through| qualified_row(&through)) @@ -81,7 +82,10 @@ fn dependency_row( .collect::>()?, ), }; - Ok((qualified_row(&record.getattr("identity")?)?, dependencies)) + Ok(( + qualified_row(&native_field(record, "identity")?)?, + dependencies, + )) } fn with_dependencies<'py>( @@ -99,7 +103,7 @@ fn with_dependencies<'py>( if is_own { own.get_item(position) } else { - usages[position].getattr("declaration") + native_field(&usages[position], "declaration") } }) .collect::>()?; @@ -146,7 +150,7 @@ fn merged_usage_index<'py>( .collect::>()?; let true_value: Bound<'py, PyAny> = true.into_bound_py_any(py)?; let completeness: Bound<'py, PyAny> = replace( - &index.getattr("completeness")?, + &native_field(index, "completeness")?, &[ ("runtime_usage", true_value.clone()), ("promotion_impact", true_value), @@ -176,7 +180,7 @@ fn order_key(diagnostic: &Bound<'_, PyAny>) -> PyResult { path: field(diagnostic, "path")?, line: field(diagnostic, "line")?, column: field(diagnostic, "column")?, - code: enum_text(&diagnostic.getattr("code")?)?, + code: enum_text(&native_field(diagnostic, "code")?)?, message: field(diagnostic, "message")?, }) } @@ -188,7 +192,7 @@ fn with_placement<'py>( ) -> PyResult> { let py: Python<'py> = index.py(); let completeness: Bound<'py, PyAny> = replace( - &index.getattr("completeness")?, + &native_field(index, "completeness")?, &[("placement", placement.into_bound_py_any(py)?)], )?; replace( @@ -209,7 +213,7 @@ fn placement_validated_index<'py>( let mut kept: Vec> = Vec::new(); let mut keys: Vec = Vec::new(); for diagnostic in tuple_attribute(index, "diagnostics")?.iter() { - if PlacementCode::parse(&enum_text(&diagnostic.getattr("code")?)?).is_none() { + if PlacementCode::parse(&enum_text(&native_field(&diagnostic, "code")?)?).is_none() { keys.push(order_key(&diagnostic)?); kept.push(diagnostic); } @@ -246,10 +250,10 @@ fn placement_validated_index<'py>( classes.member(RecordId::ScopeDiagnosticCode, item.code.as_str())?, item.message, &severity, - record.getattr("path")?, - record.getattr("line")?, - record.getattr("column")?, - record.getattr("identity")?, + native_field(&record, "path")?, + native_field(&record, "line")?, + native_field(&record, "column")?, + native_field(&record, "identity")?, ))?); } let ordered: Vec> = diagnostic_order(&keys) @@ -339,11 +343,11 @@ pub(crate) fn scope_index_with_compile_usages<'py>( inputs: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = inputs.py(); - let scope: Bound<'py, PyAny> = inputs.getattr("declaration_scope")?; + let scope: Bound<'py, PyAny> = native_field(inputs, "declaration_scope")?; let resolver: Bound<'py, PyAny> = if scope.is_none() { scope } else { - scope.getattr("resolver")? + native_field(&scope, "resolver")? }; let schema_files: Bound<'py, PyAny> = inputs .getattr("discovered_inputs")? @@ -352,14 +356,14 @@ pub(crate) fn scope_index_with_compile_usages<'py>( usages.extend(usage_records( model_schema_scope_usages(&schema_files, &resolver)?.as_any(), )?); - for model in inputs.getattr("model_inputs")?.try_iter()? { + for model in native_field(inputs, "model_inputs")?.try_iter()? { let model: Bound<'py, PyAny> = model?; - usages.extend(usage_records(&model.getattr("macro_usages")?)?); - usages.extend(usage_records(&model.getattr("declaration_usages")?)?); + usages.extend(usage_records(&native_field(&model, "macro_usages")?)?); + usages.extend(usage_records(&native_field(&model, "declaration_usages")?)?); } for group in COMPILE_USAGE_GROUPS { for item in inputs.getattr(group)?.try_iter()? { - usages.extend(usage_records(&item?.getattr("declaration_usages")?)?); + usages.extend(usage_records(&native_field(&item?, "declaration_usages")?)?); } } let enforce_placement: bool = inputs @@ -368,7 +372,7 @@ pub(crate) fn scope_index_with_compile_usages<'py>( .getattr("enforce_placement")? .extract()?; Ok(scope_placement_validated_index( - &inputs.getattr("scope_index")?, + &native_field(inputs, "scope_index")?, enforce_placement, Some(PyTuple::new(py, usages)?), )? @@ -383,9 +387,9 @@ pub(crate) fn validate_scope_index(index: &Bound<'_, PyAny>) -> Result<(), PyErr let error: Bound<'_, PyAny> = registry::class(py, RecordId::ScopeDiagnosticSeverity)?.getattr("ERROR")?; let mut errors: Vec> = Vec::new(); - for item in index.getattr("diagnostics")?.try_iter()? { + for item in native_field(index, "diagnostics")?.try_iter()? { let item: Bound<'_, PyAny> = item?; - if item.getattr("severity")?.is(&error) { + if native_field(&item, "severity")?.is(&error) { errors.push(item); } } @@ -394,8 +398,8 @@ 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 = item.getattr("code")?.getattr("value")?.extract()?; - let text: String = item.getattr("message")?.extract()?; + let code: String = native_field(item, "code")?.getattr("value")?.extract()?; + let text: String = native_field(item, "message")?.extract()?; message.push_str(&format!("\n[{code}] {text}")); } let options: Bound<'_, PyDict> = PyDict::new(py); diff --git a/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_visibility.rs b/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_visibility.rs index 6ef152b40c..403dd2c93d 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_visibility.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/scopes/scope_visibility.rs @@ -10,6 +10,7 @@ use sqlbuild_scopes::scope_index::models::{ OwnershipRoot, OwnershipRootKind, RejectedPath, ResourceIdentity, ResourceKind, ResourceRecord, }; +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::scopes::scope_lookup::{ @@ -24,7 +25,7 @@ type PyObject = Py; const CURRENT_PATH: &str = "."; fn native_lookup(lookup: &Bound<'_, PyAny>) -> PyResult> { - lookup.getattr("native")?.extract() + native_field(lookup, "native")?.extract() } /// Python's `query_target`: the resources or declarations `target` names. @@ -61,7 +62,7 @@ fn target_query<'py>( } else { raw.into_bound_py_any(py)? }; - let index: Bound<'py, PyAny> = lookup.getattr("index")?; + let index: Bound<'py, PyAny> = native_field(lookup, "index")?; let records: Bound<'py, PyTuple> = tuple_attribute( &index, if declarations { @@ -143,9 +144,9 @@ fn resolved_visibility( ) -> PyResult { let classes: ScopeClasses<'_> = ScopeClasses::new(py)?; let query: Bound<'_, PyAny> = target_query(&classes, lookup, target)?; - let index: Bound<'_, PyAny> = lookup.getattr("index")?; + let index: Bound<'_, PyAny> = native_field(lookup, "index")?; let identities: Bound<'_, PyTuple> = - tuple_attribute(&lookup.getattr("visibility_index")?, "identities")?; + tuple_attribute(&native_field(lookup, "visibility_index")?, "identities")?; let declarations: Bound<'_, PyTuple> = tuple_attribute(&index, "declarations")?; let grants: Bound<'_, PyTuple> = tuple_attribute(&index, "grants")?; let visible_class: Bound<'_, PyAny> = classes.model(RecordId::VisibilityRecord)?; @@ -155,10 +156,10 @@ fn resolved_visibility( for resource in tuple_attribute(&query, "matches")?.iter() { let (resource_visible, resource_inaccessible, _) = resolved(py, lookup, &read_resource(&resource)?)?; - let owner: Bound<'_, PyAny> = resource.getattr("identity")?; + let owner: Bound<'_, PyAny> = native_field(&resource, "identity")?; for (position, reason, grant) in resource_visible { let through: Option> = grant - .map(|grant| grants.get_item(grant)?.getattr("through")) + .map(|grant| native_field(&grants.get_item(grant)?, "through")) .transpose()?; visible.push(visible_class.call1(( &owner, @@ -169,7 +170,7 @@ fn resolved_visibility( } for (position, reason) in resource_inaccessible { let identity: Bound<'_, PyAny> = - declarations.get_item(position)?.getattr("identity")?; + native_field(&declarations.get_item(position)?, "identity")?; inaccessible.push(if with_reasons { inaccessible_class.call1(( &owner, @@ -229,7 +230,7 @@ fn scope_resolve_path_visibility( }; let (visible, inaccessible, _) = resolved(py, lookup, &resource)?; let declarations: Bound<'_, PyTuple> = - tuple_attribute(&lookup.getattr("index")?, "declarations")?; + tuple_attribute(&native_field(lookup, "index")?, "declarations")?; let pick = |positions: Vec| -> PyResult> { let records: Vec> = positions .into_iter() 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 4b5ecca39c..82362541fa 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/seed_command/command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/seed_command/command.rs @@ -19,6 +19,7 @@ use crate::bindings::_helpers::compile_driver::plan_result::connection_with_hook 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::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; @@ -31,7 +32,7 @@ fn text<'py>(py: Python<'py>, value: &str) -> Bound<'py, PyAny> { } fn status_value(result: &Bound<'_, PyAny>) -> PyResult { - result.getattr("status")?.getattr("value")?.extract() + native_field(result, "status")?.getattr("value")?.extract() } /// Forward positional connection timing to a reporter that takes keywords. @@ -112,11 +113,11 @@ struct SeedCompletionRelay { #[pymethods] impl SeedCompletionRelay { fn on_seed_complete(&self, py: Python<'_>, result: Bound<'_, PyAny>) -> PyResult<()> { - let name: String = result.getattr("seed_name")?.extract()?; + let name: String = native_field(&result, "seed_name")?.extract()?; let mut result: Bound<'_, PyAny> = result; if let Some(projector) = &self.projector { let duration: Bound<'_, PyAny> = call_with( - &projector.bind(py).getattr("consume_resource_terminal")?, + &native_field(projector.bind(py), "consume_resource_terminal")?, &[ ("resource_name", text(py, &name)), ("resource_id", text(py, &format!("seed:{name}"))), @@ -145,10 +146,10 @@ impl SeedCompletionRelay { ordinal, total: self.total, succeeded: status_value(&result)? == SUCCESS_STATUS, - duration_ms: result.getattr("duration_ms")?.extract()?, - error_message: result.getattr("error_message")?.extract()?, - error_code: result.getattr("error_code")?.extract()?, - error_help: result.getattr("error_help")?.extract()?, + duration_ms: native_field(&result, "duration_ms")?.extract()?, + error_message: native_field(&result, "error_message")?.extract()?, + error_code: native_field(&result, "error_code")?.extract()?, + error_help: native_field(&result, "error_help")?.extract()?, }; let stream: &Bound<'_, PyAny> = self.stream.bind(py); stream.call_method1("write", (render_seed_row(&row, style),))?; @@ -171,16 +172,16 @@ fn compile_seeds<'py>( invocation: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = request.py(); - let stream: Bound<'py, PyAny> = invocation.getattr("progress_stream")?; - let adapter: Bound<'py, PyAny> = invocation.getattr("adapter")?; - let connection_config: Bound<'py, PyAny> = invocation.getattr("connection_config")?; - let discovered: Bound<'py, PyAny> = invocation.getattr("discovered_inputs")?; + let stream: Bound<'py, PyAny> = native_field(invocation, "progress_stream")?; + 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", invocation.getattr("adapter_name")?), + ("adapter_name", native_field(invocation, "adapter_name")?), ("stream", stream.clone()), - ("use_color", invocation.getattr("use_color")?), + ("use_color", native_field(invocation, "use_color")?), ], )?; stream.call_method1("write", ("\n",))?; @@ -188,18 +189,21 @@ fn compile_seeds<'py>( let resolver: Bound<'py, PyAny> = call_with( ®istry::native(py, NativeAttr::ResolveExternalSqlReferenceResolver)?, &[ - ("project_dir", invocation.getattr("effective_project_dir")?), + ( + "project_dir", + native_field(invocation, "effective_project_dir")?, + ), ("discovered_inputs", discovered.clone()), ], )?; let options: Bound<'py, PyAny> = call_with( ®istry::class(py, RecordId::CompilePipelineOptions)?, &[ - ("selected_target", request.getattr("selected_target")?), - ("select", request.getattr("select")?), - ("exclude", request.getattr("exclude")?), + ("selected_target", native_field(request, "selected_target")?), + ("select", native_field(request, "select")?), + ("exclude", native_field(request, "exclude")?), ("connection_config", connection_config.clone()), - ("cli_vars", request.getattr("cli_vars")?), + ("cli_vars", native_field(request, "cli_vars")?), ("external_sql_reference_resolver", resolver), ], )?; @@ -211,13 +215,13 @@ fn compile_seeds<'py>( ("options", options), ], )?; - let project: Bound<'py, PyAny> = context.getattr("project")?; - let scope: Bound<'py, PyAny> = context.getattr("scope")?; - let selected_keys: Bound<'py, PyAny> = scope.getattr("selected_keys")?; + 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")?; let mut selected: Vec> = Vec::new(); - for seed in project.getattr("seeds")?.try_iter()? { + for seed in native_field(&project, "seeds")?.try_iter()? { let seed: Bound<'py, PyAny> = seed?; - if selected_keys.contains(seed.getattr("key")?)? { + if selected_keys.contains(native_field(&seed, "key")?)? { selected.push(seed); } } @@ -230,13 +234,16 @@ fn compile_seeds<'py>( &[ ( "on_connection_start", - connection_progress.getattr("on_connection_start")?, + native_field(&connection_progress, "on_connection_start")?, ), ( "on_connection_complete", - relay.getattr("on_connection_complete")?, + native_field(&relay, "on_connection_complete")?, + ), + ( + "on_connection_error", + native_field(&relay, "on_connection_error")?, ), - ("on_connection_error", relay.getattr("on_connection_error")?), ], )?; let connection: Bound<'py, PyAny> = @@ -259,7 +266,7 @@ fn compile_seeds<'py>( &[ ("project", project), ("scope", scope), - ("relations", context.getattr("relations")?), + ("relations", native_field(&context, "relations")?), ("fingerprints", fingerprints), ], )?; @@ -274,7 +281,7 @@ fn seed_names(pipeline: &Bound<'_, PyAny>) -> PyResult> { .getattr("plan_output")? .getattr("seed_entries")? .try_iter()? - .map(|entry| entry?.getattr("name")?.extract()) + .map(|entry| native_field(&entry?, "name")?.extract()) .collect() } @@ -285,8 +292,8 @@ fn write_header( concurrency: i64, ) -> PyResult<()> { let py: Python<'_> = invocation.py(); - let stream: Bound<'_, PyAny> = invocation.getattr("progress_stream")?; - let use_color: Bound<'_, PyAny> = invocation.getattr("use_color")?; + let stream: Bound<'_, PyAny> = native_field(invocation, "progress_stream")?; + let use_color: Bound<'_, PyAny> = native_field(invocation, "use_color")?; let style: CliStyle = CliStyle { use_color: use_color.is_truthy()?, }; @@ -308,10 +315,10 @@ fn load_seeds<'py>( concurrency: i64, ) -> PyResult>> { let py: Python<'py> = invocation.py(); - let stream: Bound<'py, PyAny> = invocation.getattr("progress_stream")?; - let use_color: Bound<'py, PyAny> = invocation.getattr("use_color")?; - let plan: Bound<'py, PyAny> = pipeline.getattr("plan_output")?; - let project: Bound<'py, PyAny> = pipeline.getattr("project")?; + let stream: Bound<'py, PyAny> = native_field(invocation, "progress_stream")?; + let use_color: Bound<'py, PyAny> = native_field(invocation, "use_color")?; + let plan: Bound<'py, PyAny> = native_field(pipeline, "plan_output")?; + let project: Bound<'py, PyAny> = native_field(pipeline, "project")?; let names: Vec = seed_names(pipeline)?; let ordinals: HashMap = names .iter() @@ -330,7 +337,7 @@ fn load_seeds<'py>( order.set_item(name, index + 1)?; } call_with( - &projector.getattr("configure_resources")?, + &native_field(&projector, "configure_resources")?, &[ ("ordinals", order.into_any()), ("total", PyInt::new(py, names.len()).into_any()), @@ -353,7 +360,7 @@ fn load_seeds<'py>( let connection_progress: Bound<'py, PyAny> = call_with( ®istry::native(py, NativeAttr::ConnectionProgressReporter)?, &[ - ("adapter_name", invocation.getattr("adapter_name")?), + ("adapter_name", native_field(invocation, "adapter_name")?), ("stream", stream), ( "blank_line_after_complete", @@ -370,27 +377,33 @@ fn load_seeds<'py>( ("plan", plan), ( "connection_config", - invocation.getattr("connection_config")?, + native_field(invocation, "connection_config")?, ), - ("adapter", invocation.getattr("adapter")?), + ("adapter", native_field(invocation, "adapter")?), ("max_concurrency", PyInt::new(py, concurrency).into_any()), - ("run_id", project.getattr("run_id")?), + ("run_id", native_field(&project, "run_id")?), ( "query_change_tracking", project .getattr("settings")? .getattr("query_change_tracking")?, ), - ("on_seed_complete", completion.getattr("on_seed_complete")?), + ( + "on_seed_complete", + native_field(&completion, "on_seed_complete")?, + ), ( "on_connection_start", - connection_progress.getattr("on_connection_start")?, + native_field(&connection_progress, "on_connection_start")?, ), ( "on_connection_complete", - relay.getattr("on_connection_complete")?, + native_field(&relay, "on_connection_complete")?, + ), + ( + "on_connection_error", + native_field(&relay, "on_connection_error")?, ), - ("on_connection_error", relay.getattr("on_connection_error")?), ], )? .try_iter()? @@ -409,7 +422,7 @@ fn run_seed_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult let invocation: Bound<'_, PyAny> = resolve_command_invocation(request, ®istry::class(py, RecordId::SeedInvocation)?)?; let pipeline: Bound<'_, PyAny> = compile_seeds(request, &invocation)?; - let requested: Bound<'_, PyAny> = request.getattr("concurrency")?; + let requested: Bound<'_, PyAny> = native_field(request, "concurrency")?; let concurrency: i64 = if requested.is_none() { pipeline .getattr("project")? @@ -433,9 +446,9 @@ fn run_seed_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult _ => {} } } - let stream: Bound<'_, PyAny> = invocation.getattr("progress_stream")?; + let stream: Bound<'_, PyAny> = native_field(&invocation, "progress_stream")?; let style: CliStyle = CliStyle { - use_color: invocation.getattr("use_color")?.is_truthy()?, + use_color: native_field(&invocation, "use_color")?.is_truthy()?, }; stream.call_method1( "write", @@ -450,15 +463,18 @@ fn run_seed_command(py: Python<'_>, request: &Bound<'_, PyAny>) -> PyResult ®istry::native(py, NativeAttr::FormatSeedExecutionJson)?, &[ ("results", PyTuple::new(py, &results)?.into_any()), - ("plan", pipeline.getattr("plan_output")?), + ("plan", native_field(&pipeline, "plan_output")?), ], )?; call_with( ®istry::native(py, NativeAttr::WriteExecutionJsonOutput)?, &[ ("payload", payload), - ("json_output", request.getattr("json_output")?), - ("json_output_path", request.getattr("json_output_path")?), + ("json_output", native_field(request, "json_output")?), + ( + "json_output_path", + 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 66565fa232..15d805883f 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::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}; use crate::bindings::_helpers::semantic_checks::session_facts::{ @@ -32,12 +33,12 @@ pub(crate) fn completed_diagnostics<'py>( shared: &StageShared<'_>, ) -> PyResult> { let py: Python<'py> = classes.py(); - let models: Vec> = items(&project.getattr("models")?)?; + let models: Vec> = items(&native_field(project, "models")?)?; let ids: Bound<'py, PyDict> = diagnostic_ids(project, &models)?; - let diagnostics: Vec> = items(&project.getattr("diagnostics")?)?; + let diagnostics: Vec> = items(&native_field(project, "diagnostics")?)?; let mut recovers: bool = false; for item in &diagnostics { - if item.getattr("code")?.eq(RECOVERED_CODE)? { + if native_field(item, "code")?.eq(RECOVERED_CODE)? { recovers = true; break; } @@ -56,7 +57,7 @@ pub(crate) fn completed_diagnostics<'py>( completion_models.push(completion_model(model, recovers, shared)?); } let request = CompletionRequest { - dialect: project.getattr("sql_analysis_dialect")?.extract()?, + dialect: native_field(project, "sql_analysis_dialect")?.extract()?, diagnostics: semantic, models: completion_models, shapes, @@ -72,7 +73,7 @@ pub(crate) fn completed_diagnostics<'py>( rebuilt.push(completed_diagnostic(classes, original, row)?); } let models_value: Bound<'py, PyAny> = match model_bindings { - None => project.getattr("models")?, + None => native_field(project, "models")?, Some(model_bindings) => { let mut updated: Vec> = Vec::with_capacity(models.len()); for (model, positions) in models.iter().zip(model_bindings) { @@ -107,8 +108,11 @@ pub(crate) fn completed_diagnostics<'py>( ("code", &classes.opt_out_code), ("message", &PyString::new(py, &opt_out.message).into_any()), ("resource_type", &classes.model_resource), - ("resource_name", &model.getattr("name")?), - ("location", &model.getattr("rejected_sql_analysis_opt_out")?), + ("resource_name", &native_field(model, "name")?), + ( + "location", + &native_field(model, "rejected_sql_analysis_opt_out")?, + ), ("notes", ¬es), ("help", &PyString::new(py, &opt_out.help).into_any()), ], @@ -129,10 +133,10 @@ fn optional_location(location: &Bound<'_, PyAny>) -> PyResult, ids: &Bound<'_, PyDict>, ) -> PyResult { - let resource_type: Bound<'_, PyAny> = item.getattr("resource_type")?; + let resource_type: Bound<'_, PyAny> = native_field(item, "resource_type")?; Ok(SemanticDiagnostic { id: id_of(ids, item)?, - code: item.getattr("code")?.extract()?, - message: item.getattr("message")?.extract()?, + code: native_field(item, "code")?.extract()?, + message: native_field(item, "message")?.extract()?, resource_type: if resource_type.is_none() { None } else { Some(resource_type.str()?.extract()?) }, - resource_name: item.getattr("resource_name")?.extract()?, - line: item.getattr("line")?.extract()?, - column: item.getattr("column")?.extract()?, - location: optional_location(&item.getattr("location")?)?, - help: item.getattr("help")?.extract()?, - notes: items(&item.getattr("notes")?)? + resource_name: native_field(item, "resource_name")?.extract()?, + line: native_field(item, "line")?.extract()?, + column: native_field(item, "column")?.extract()?, + location: optional_location(&native_field(item, "location")?)?, + help: native_field(item, "help")?.extract()?, + notes: items(&native_field(item, "notes")?)? .iter() .map(PyAnyMethods::extract) .collect::>()?, @@ -167,7 +171,7 @@ fn completion_model( recovers: bool, shared: &StageShared<'_>, ) -> PyResult { - let name: String = model.getattr("name")?.extract()?; + let name: String = native_field(model, "name")?.extract()?; let from_session: bool = shared.session_models.contains(&name); let inferred_columns: Vec = if from_session { column_names(session_model_facts(shared.session, &name).map_err(compiler_error)?) @@ -185,22 +189,22 @@ fn completion_model( } else { lineage_inputs(model)? }; - let opt_out: Bound<'_, PyAny> = model.getattr("rejected_sql_analysis_opt_out")?; + let opt_out: Bound<'_, PyAny> = native_field(model, "rejected_sql_analysis_opt_out")?; let mut binding_codes: Vec = Vec::new(); - for item in model.getattr("binding_diagnostics")?.try_iter()? { - binding_codes.push(item?.getattr("code")?.extract()?); + for item in native_field(model, "binding_diagnostics")?.try_iter()? { + binding_codes.push(native_field(&item?, "code")?.extract()?); } Ok(CompletionModel { name, - query_sql: model.getattr("query_sql")?.extract()?, - authored_sql: model.getattr("authored_sql")?.extract()?, + query_sql: native_field(model, "query_sql")?.extract()?, + authored_sql: native_field(model, "authored_sql")?.extract()?, inferred_columns, lineage: lineage_outputs(lineage), binding_codes, rejected_opt_out_file: if opt_out.is_none() { None } else { - Some(opt_out.getattr("path")?.getattr("name")?.extract()?) + Some(native_field(&opt_out, "path")?.getattr("name")?.extract()?) }, }) } @@ -214,7 +218,7 @@ fn completed_diagnostic<'py>( return Ok(original); } let py: Python<'py> = classes.py(); - let mut current: Bound<'py, PyAny> = original.getattr("location")?; + let mut current: Bound<'py, PyAny> = native_field(&original, "location")?; if let (Some(existing), Some(location)) = (optional_location(¤t)?, row.location.as_ref()) && ( existing.line, 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 762df3d046..47ba40d9f2 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/inputs.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/inputs.rs @@ -7,6 +7,7 @@ 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::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::semantic_checks::resource_sql::resource_sql_diagnostics_of; @@ -39,17 +40,17 @@ pub(crate) fn sql_test_columns<'py>( ) -> PyResult> { let py: Python<'py> = project.py(); let bodies: Bound<'py, PyDict> = PyDict::new(py); - for test in project.getattr("sql_tests")?.try_iter()? { - let payload: Bound<'py, PyAny> = test?.getattr("payload")?; + for test in native_field(project, "sql_tests")?.try_iter()? { + let payload: Bound<'py, PyAny> = native_field(&test?, "payload")?; if !payload.is_instance(model_test_payload)? { continue; } for cte in payload .getattr("authored_ctes")? .try_iter()? - .chain(payload.getattr("expected_ctes")?.try_iter()?) + .chain(native_field(&payload, "expected_ctes")?.try_iter()?) { - bodies.set_item(cte?.getattr("sql_body")?, py.None())?; + bodies.set_item(native_field(&cte?, "sql_body")?, py.None())?; } } let columns: Bound<'py, PyDict> = PyDict::new(py); @@ -73,10 +74,10 @@ pub(crate) fn sql_test_columns<'py>( for (sql, analysis) in bodies.keys().iter().zip(analyses.try_iter()?) { let analysis: Bound<'py, PyAny> = analysis?; let mut names: Vec> = Vec::new(); - let inferred: Bound<'py, PyAny> = analysis.getattr("columns")?; + let inferred: Bound<'py, PyAny> = native_field(&analysis, "columns")?; if !inferred.is_none() { for column in inferred.try_iter()? { - names.push(column?.getattr("name")?); + names.push(native_field(&column?, "name")?); } } columns.set_item(sql, PyTuple::new(py, names)?)?; @@ -91,28 +92,29 @@ pub(crate) fn revalidation_request<'py>( shapes: &Bound<'py, PyDict>, ) -> PyResult> { let py: Python<'py> = project.py(); - let dialect: Bound<'py, PyAny> = project.getattr("sql_analysis_dialect")?; - let functions: Bound<'py, PyAny> = project.getattr("functions")?; + let dialect: Bound<'py, PyAny> = native_field(project, "sql_analysis_dialect")?; + let functions: Bound<'py, PyAny> = native_field(project, "functions")?; let schema: Bound<'py, PyDict> = PyDict::new(py); - for reference in model.getattr("references")?.try_iter()? { - let name: Bound<'py, PyAny> = reference?.getattr("ref_name")?; + for reference in native_field(model, "references")?.try_iter()? { + let name: Bound<'py, PyAny> = native_field(&reference?, "ref_name")?; let shape: Bound<'py, PyAny> = match shapes.get_item(&name)? { Some(shape) => shape, None => PyDict::new(py).into_any(), }; schema.set_item(name, shape)?; } - let ignores: bool = ignores_quoted_case(&project.getattr("effective_connection")?, &dialect)?; + let ignores: bool = + ignores_quoted_case(&native_field(project, "effective_connection")?, &dialect)?; schema_validation_request( py, SchemaRequestParts { - query_sql: model.getattr("query_sql")?.extract()?, + query_sql: native_field(model, "query_sql")?.extract()?, placeholders: py.None().into_bound(py), known_functions: PyTuple::new(py, function_names(&functions)?)?.into_any(), known_types: PyTuple::new(py, declared_types(&functions, shapes.as_any())?)?.into_any(), dialect, schema: schema.into_any(), - catalog: project.getattr("binding_catalog")?, + catalog: native_field(project, "binding_catalog")?, quoted_identifiers_ignore_case: ignores, }, ) @@ -124,7 +126,7 @@ pub(crate) fn resource_sql_diagnostics<'py>( shapes: &Bound<'py, PyAny>, profile: &Bound<'py, PyAny>, ) -> PyResult> { - let native: Bound<'py, PyAny> = project.getattr("binding_catalog")?.getattr("native")?; + let native: Bound<'py, PyAny> = native_field(project, "binding_catalog")?.getattr("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 da04b93276..b62d2db3fb 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/metadata.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/metadata.rs @@ -17,6 +17,7 @@ use sqlbuild_analysis::semantic_validation::models as validation; use crate::bindings::_helpers::analysis::model_reads::binding_validated; use crate::bindings::_helpers::boundary::panics::compiler_error; use crate::bindings::_helpers::compiled_project::compiled_model_view::NativeCompiledModel; +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::inputs::{ column_names, resource_sql_diagnostics, sql_test_columns, @@ -66,7 +67,7 @@ pub(crate) fn metadata_diagnostics<'py>( .compiler_detach(|| Ok(check_semantic_metadata(&request, catalog))) .map_err(compiler_error)? .map_err(|failure| semantic_error(py, &failure))?; - let dialect: Bound<'py, PyAny> = profile.getattr("sql_analysis_dialect")?; + let dialect: Bound<'py, PyAny> = native_field(profile, "sql_analysis_dialect")?; for type_sql in &outcome.fallback_types { let _ = classes.call( &classes.normalize_type, @@ -84,11 +85,12 @@ 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> = source.getattr("source_file")?.getattr("relative_path")?; + let path: Bound<'py, PyAny> = + native_field(source, "source_file")?.getattr("relative_path")?; diagnostics.push(diagnostic( classes, finding, - (&classes.source_resource, &source.getattr("name")?), + (&classes.source_resource, &native_field(source, "name")?), (&path, None), )?); } @@ -115,7 +117,7 @@ fn metadata_request<'py>( ) -> PyResult<(MetadataInputs<'py>, MetadataRequest)> { let shapes: Bound<'py, PyAny> = project_shapes(project, Some(profile.clone()))?.into_any(); let mut tests: Vec> = Vec::new(); - for test in project.getattr("sql_tests")?.try_iter()? { + for test in native_field(project, "sql_tests")?.try_iter()? { let test: Bound<'py, PyAny> = test?; if test .getattr("payload")? @@ -127,19 +129,20 @@ fn metadata_request<'py>( let columns_by_sql: Bound<'py, PyAny> = sql_test_columns(project, profile, &classes.model_test_payload)?.into_any(); let functions: Vec = metadata_functions(project)?; - let sources: Vec> = items(&project.getattr("sources")?)?; + let sources: Vec> = items(&native_field(project, "sources")?)?; let (files, contents) = file_contents(classes.py(), &sources, &tests)?; - let models: Vec> = items(&project.getattr("models")?)?; + let models: Vec> = items(&native_field(project, "models")?)?; let mut metadata_models: Vec = Vec::with_capacity(models.len()); for model in &models { metadata_models.push(metadata_model(classes, model)?); } let mut metadata_sources: Vec = Vec::with_capacity(sources.len()); for source in &sources { - let cursor: Bound<'py, PyAny> = source.getattr("source_entry")?.getattr("cursor_column")?; - let index: usize = file_index(&files, &source.getattr("source_file")?)?; + let cursor: Bound<'py, PyAny> = + native_field(source, "source_entry")?.getattr("cursor_column")?; + let index: usize = file_index(&files, &native_field(source, "source_file")?)?; metadata_sources.push(MetadataSource { - name: source.getattr("name")?.extract()?, + name: native_field(source, "name")?.extract()?, cursor_column: if cursor.is_truthy()? { Some(cursor.extract()?) } else { @@ -150,18 +153,18 @@ fn metadata_request<'py>( } let mut metadata_tests: Vec = Vec::with_capacity(tests.len()); for test in &tests { - let index: usize = file_index(&files, &test.getattr("test_file")?)?; + let index: usize = file_index(&files, &native_field(test, "test_file")?)?; metadata_tests.push(MetadataSqlTest { contents: contents[index].clone(), - ctes: test_ctes(&test.getattr("payload")?, &columns_by_sql)?, + ctes: test_ctes(&native_field(test, "payload")?, &columns_by_sql)?, }); } let request = MetadataRequest { - dialect: profile.getattr("sql_analysis_dialect")?.extract()?, + dialect: native_field(profile, "sql_analysis_dialect")?.extract()?, quoted_ignore_case: profile .getattr("quoted_identifiers_ignore_case")? .extract()?, - return_types: pairs(&profile.getattr("function_return_types")?)?, + return_types: pairs(&native_field(profile, "function_return_types")?)?, functions, shapes: shape_rows(&shapes)?, models: metadata_models, @@ -180,21 +183,21 @@ fn metadata_request<'py>( /// Declared functions by `name.casefold()`, the last declaration winning in the first's place. fn metadata_functions(project: &Bound<'_, PyAny>) -> PyResult> { let functions: Bound<'_, PyDict> = PyDict::new(project.py()); - for function in project.getattr("functions")?.try_iter()? { + for function in native_field(project, "functions")?.try_iter()? { let function: Bound<'_, PyAny> = function?; functions.set_item( - function.getattr("name")?.call_method0("casefold")?, + native_field(&function, "name")?.call_method0("casefold")?, &function, )?; } let mut metadata: Vec = Vec::with_capacity(functions.len()); for (key, function) in functions.iter() { let mut arguments: Vec<(String, String)> = Vec::new(); - for argument in function.getattr("arguments")?.try_iter()? { + for argument in native_field(&function, "arguments")?.try_iter()? { let argument: Bound<'_, PyAny> = argument?; arguments.push(( - argument.getattr("name")?.extract()?, - argument.getattr("type")?.extract()?, + native_field(&argument, "name")?.extract()?, + native_field(&argument, "type")?.extract()?, )); } metadata.push(MetadataFunction { @@ -214,13 +217,13 @@ fn file_contents<'py>( let file_texts: Bound<'py, PyDict> = PyDict::new(py); for file in sources .iter() - .map(|source| source.getattr("source_file")) - .chain(tests.iter().map(|test| test.getattr("test_file"))) + .map(|source| native_field(source, "source_file")) + .chain(tests.iter().map(|test| native_field(test, "test_file"))) { let file: Bound<'py, PyAny> = file?; - let path: Bound<'py, PyAny> = file.getattr("file_path")?; + let path: Bound<'py, PyAny> = native_field(&file, "file_path")?; if !file_texts.contains(&path)? { - file_texts.set_item(path, file.getattr("contents")?)?; + file_texts.set_item(path, native_field(&file, "contents")?)?; } } let files: Bound<'py, PyDict> = PyDict::new(py); @@ -235,7 +238,7 @@ fn file_contents<'py>( fn file_index(files: &Bound<'_, PyDict>, file: &Bound<'_, PyAny>) -> PyResult { files .as_any() - .get_item(file.getattr("file_path")?)? + .get_item(native_field(file, "file_path")?)? .extract() } @@ -248,13 +251,13 @@ fn test_ctes( for cte in payload .getattr("authored_ctes")? .try_iter()? - .chain(payload.getattr("expected_ctes")?.try_iter()?) + .chain(native_field(payload, "expected_ctes")?.try_iter()?) { let cte: Bound<'_, PyAny> = cte?; ctes.push(( - cte.getattr("name")?.extract()?, + native_field(&cte, "name")?.extract()?, columns_by_sql - .get_item(cte.getattr("sql_body")?)? + .get_item(native_field(&cte, "sql_body")?)? .try_iter()? .map(|name| name?.extract()) .collect::>()?, @@ -305,7 +308,7 @@ fn sql_test_diagnostic<'py>( end_column: i64, ) -> PyResult> { let py: Python<'py> = classes.py(); - let path: Bound<'py, PyAny> = test.getattr("test_file")?.getattr("relative_path")?; + let path: Bound<'py, PyAny> = native_field(test, "test_file")?.getattr("relative_path")?; let location: Bound<'py, PyAny> = classes.call( &classes.location, &[ @@ -319,7 +322,7 @@ fn sql_test_diagnostic<'py>( diagnostic( classes, finding, - (&classes.sql_test_resource, &test.getattr("name")?), + (&classes.sql_test_resource, &native_field(test, "name")?), (&path, Some(&location)), ) } @@ -365,8 +368,8 @@ fn model_error<'py>( ("code", &PyString::new(py, code).into_any()), ("message", &PyString::new(py, message).into_any()), ("resource_type", &classes.model_resource), - ("resource_name", &model.getattr("name")?), - ("path", &model.getattr("relative_path")?), + ("resource_name", &native_field(model, "name")?), + ("path", &native_field(model, "relative_path")?), ("line", &line.into_bound_py_any(py)?), ("column", &column.into_bound_py_any(py)?), ], @@ -421,7 +424,7 @@ fn text(value: &Bound<'_, PyAny>) -> PyResult> { } fn metadata_model(classes: &StageClasses<'_>, model: &Bound<'_, PyAny>) -> PyResult { - let values: Bound<'_, PyAny> = model.getattr("config")?.getattr("values")?; + let values: Bound<'_, PyAny> = native_field(model, "config")?.getattr("values")?; let get = |key: &str| values.call_method1("get", (key,)); let mut references: Vec = Vec::new(); for key in METADATA_CONFIG_KEYS { @@ -452,17 +455,17 @@ fn metadata_model(classes: &StageClasses<'_>, model: &Bound<'_, PyAny>) -> PyRes } } let mut calls_functions: bool = false; - for reference in model.getattr("references")?.try_iter()? { - let kind: Bound<'_, PyAny> = reference?.getattr("ref_kind")?; + for reference in native_field(model, "references")?.try_iter()? { + let kind: Bound<'_, PyAny> = native_field(&reference?, "ref_kind")?; if kind.eq(&classes.udf_kind)? || kind.eq(&classes.table_function_kind)? { calls_functions = true; break; } } Ok(MetadataModel { - name: model.getattr("name")?.extract()?, - query_sql: model.getattr("query_sql")?.extract()?, - authored_sql: model.getattr("authored_sql")?.extract()?, + name: native_field(model, "name")?.extract()?, + query_sql: native_field(model, "query_sql")?.extract()?, + authored_sql: native_field(model, "authored_sql")?.extract()?, checked: checked(classes, model)?, calls_functions, references, @@ -488,23 +491,23 @@ fn metadata_audits(model: &Bound<'_, PyAny>) -> PyResult> { } for (index, name) in audited { let column: Bound<'_, PyAny> = columns.bind(py).get_item(index)?; - for audit in column.getattr("audits")?.try_iter()? { + for audit in native_field(&column, "audits")?.try_iter()? { attached.push((Some(name.clone()), audit?)); } } } Err(_) => { - let entry: Bound<'_, PyAny> = model.getattr("schema_entry")?; + let entry: Bound<'_, PyAny> = native_field(model, "schema_entry")?; if entry.is_none() { return Ok(Vec::new()); } - for audit in entry.getattr("audits")?.try_iter()? { + for audit in native_field(&entry, "audits")?.try_iter()? { attached.push((None, audit?)); } - for column in entry.getattr("columns")?.try_iter()? { + for column in native_field(&entry, "columns")?.try_iter()? { let column: Bound<'_, PyAny> = column?; - let name: String = column.getattr("name")?.extract()?; - for audit in column.getattr("audits")?.try_iter()? { + let name: String = native_field(&column, "name")?.extract()?; + for audit in native_field(&column, "audits")?.try_iter()? { attached.push((Some(name.clone()), audit?)); } } @@ -513,7 +516,7 @@ fn metadata_audits(model: &Bound<'_, PyAny>) -> PyResult> { let empty: Bound<'_, PyAny> = PyString::new(py, "").into_any(); let mut audits: Vec = Vec::with_capacity(attached.len()); for (column, audit) in attached { - let arguments: Bound<'_, PyAny> = audit.getattr("arguments")?; + let arguments: Bound<'_, PyAny> = native_field(&audit, "arguments")?; let values: Bound<'_, PyAny> = arguments.call_method1("get", ("values",))?; let values: Option> = if values.is_instance_of::() || values.is_instance_of::() { @@ -528,7 +531,7 @@ fn metadata_audits(model: &Bound<'_, PyAny>) -> PyResult> { }; audits.push(MetadataAudit { column, - definition: audit.getattr("definition_name")?.extract()?, + definition: native_field(&audit, "definition_name")?.extract()?, expression: text(&arguments.call_method1("get", ("expression",))?)?, target: arguments .call_method1("get", ("to", &empty))? 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 00a9cddbbf..f999653e4a 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; use crate::bindings::models::ProjectCatalog; @@ -73,27 +74,27 @@ fn request( let py: Python<'_> = project.py(); let reference_class: Bound<'_, PyAny> = registry::class(py, RecordId::CompileSqlReference)?; let mut audits: Vec = Vec::new(); - for audit in project.getattr("audits")?.try_iter()? { + for audit in native_field(project, "audits")?.try_iter()? { audits.push(audit_input(&audit?, &reference_class)?); } let mut sql_tests: Vec = Vec::new(); - for test in project.getattr("sql_tests")?.try_iter()? { + for test in native_field(project, "sql_tests")?.try_iter()? { sql_tests.push(sql_test_input(&test?)?); } let mut models: Vec = Vec::new(); - for model in project.getattr("models")?.try_iter()? { + for model in native_field(project, "models")?.try_iter()? { models.push(model_input(&model?, &reference_class)?); } let mut hook_files: Vec<(String, Arc)> = Vec::new(); - for file in project.getattr("sql_hook_files")?.try_iter()? { + for file in native_field(project, "sql_hook_files")?.try_iter()? { let file: Bound<'_, PyAny> = file?; hook_files.push(( - file.getattr("relative_path")?.str()?.extract()?, - Arc::from(file.getattr("contents")?.extract::()?), + native_field(&file, "relative_path")?.str()?.extract()?, + Arc::from(native_field(&file, "contents")?.extract::()?), )); } Ok(ResourceSqlRequest { - dialect: profile.getattr("sql_analysis_dialect")?.extract()?, + dialect: native_field(profile, "sql_analysis_dialect")?.extract()?, quoted_ignore_case: profile .getattr("quoted_identifiers_ignore_case")? .extract()?, @@ -128,14 +129,16 @@ fn audit_input( audit: &Bound<'_, PyAny>, reference_class: &Bound<'_, PyAny>, ) -> PyResult { - let file: Bound<'_, PyAny> = audit.getattr("audit_file")?; - let block: Bound<'_, PyAny> = audit.getattr("audit_block")?; - let target_name: Bound<'_, PyAny> = audit.getattr("attached_target_name")?; + let file: Bound<'_, PyAny> = native_field(audit, "audit_file")?; + let block: Bound<'_, PyAny> = native_field(audit, "audit_block")?; + let target_name: Bound<'_, PyAny> = native_field(audit, "attached_target_name")?; let attached_target: Option<(String, String)> = if target_name.is_none() { None } else { Some(( - audit.getattr("attached_target_kind")?.str()?.extract()?, + native_field(audit, "attached_target_kind")? + .str()? + .extract()?, target_name.extract()?, )) }; @@ -155,37 +158,37 @@ fn audit_input( )); } Ok(AuditSqlInput { - name: audit.getattr("name")?.extract()?, - definition_name: audit.getattr("definition_name")?.extract()?, + name: native_field(audit, "name")?.extract()?, + definition_name: native_field(audit, "definition_name")?.extract()?, attached_target, - attached_column: audit.getattr("attached_column_name")?.extract()?, + attached_column: native_field(audit, "attached_column_name")?.extract()?, built_in: first_part.as_deref() == Some(BUILT_IN_AUDIT_ROOT), - path: file.getattr("relative_path")?.str()?.extract()?, - contents: Arc::from(file.getattr("contents")?.extract::()?), + path: native_field(&file, "relative_path")?.str()?.extract()?, + contents: Arc::from(native_field(&file, "contents")?.extract::()?), bodies, - opts_out: opts_out(&block.getattr("header_values")?)?, + opts_out: opts_out(&native_field(&block, "header_values")?)?, relations: match audit_reference_rows(audit)? { Some(rows) => row_relations(rows), - None => relations(&audit.getattr("references")?, reference_class)?, + None => relations(&native_field(audit, "references")?, reference_class)?, }, }) } fn sql_test_input(test: &Bound<'_, PyAny>) -> PyResult { - let file: Bound<'_, PyAny> = test.getattr("test_file")?; - let block: Bound<'_, PyAny> = test.getattr("test_block")?; + let file: Bound<'_, PyAny> = native_field(test, "test_file")?; + let block: Bound<'_, PyAny> = native_field(test, "test_block")?; Ok(SqlTestSqlInput { - name: test.getattr("name")?.extract()?, + name: native_field(test, "name")?.extract()?, expected_model_names: test .getattr("expected_model_names")? .try_iter()? .map(|name| name?.extract()) .collect::>()?, - path: file.getattr("relative_path")?.str()?.extract()?, - contents: Arc::from(file.getattr("contents")?.extract::()?), - sql_body: test.getattr("sql_body")?.extract()?, - authored_body: block.getattr("sql_body")?.extract()?, - opts_out: opts_out(&block.getattr("header_values")?)?, + path: native_field(&file, "relative_path")?.str()?.extract()?, + contents: Arc::from(native_field(&file, "contents")?.extract::()?), + sql_body: native_field(test, "sql_body")?.extract()?, + authored_body: native_field(&block, "sql_body")?.extract()?, + opts_out: opts_out(&native_field(&block, "header_values")?)?, }) } @@ -193,7 +196,7 @@ fn model_input( model: &Bound<'_, PyAny>, reference_class: &Bound<'_, PyAny>, ) -> PyResult { - let values: Bound<'_, PyAny> = model.getattr("config")?.getattr("values")?; + let values: Bound<'_, PyAny> = native_field(model, "config")?.getattr("values")?; let mut hooks: Vec = Vec::new(); for key in MODEL_HOOK_KEYS { let entries: Bound<'_, PyAny> = values.call_method1("get", (key,))?; @@ -226,9 +229,9 @@ fn model_input( } } Ok(HookModelInput { - name: model.getattr("name")?.extract()?, - path: model.getattr("relative_path")?.str()?.extract()?, - authored_sql: Arc::from(model.getattr("authored_sql")?.extract::()?), + name: native_field(model, "name")?.extract()?, + path: native_field(model, "relative_path")?.str()?.extract()?, + authored_sql: Arc::from(native_field(model, "authored_sql")?.extract::()?), analysis_disabled: opts_out(&values)?, hooks, }) @@ -292,10 +295,10 @@ fn relations( if !reference.is_instance(reference_class)? { continue; } - let kind: String = reference.getattr("ref_kind")?.str()?.extract()?; + let kind: String = native_field(&reference, "ref_kind")?.str()?.extract()?; names.extend(relation_name( &kind, - reference.getattr("ref_name")?.extract()?, + native_field(&reference, "ref_name")?.extract()?, )); } names.sort_unstable(); diff --git a/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/stage.rs b/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/stage.rs index 52d5c5f15a..b944fb5140 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/stage.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/stage.rs @@ -10,6 +10,7 @@ use crate::bindings::_helpers::analysis::model_reads::output_proven; use crate::bindings::_helpers::analysis_session::session::NativeModelAnalysisSession; use crate::bindings::_helpers::boundary::keyword_request::Request; use crate::bindings::_helpers::boundary::panics::NativeCompilerError; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::semantic_checks::completion::completed_diagnostics; use crate::bindings::_helpers::semantic_checks::metadata::metadata_diagnostics; use crate::bindings::_helpers::semantic_checks::stage_objects::StageClasses; @@ -52,7 +53,7 @@ fn complete_semantic_project<'py>( resource_sql_analysis, &catalog.inner, )?; - let mut diagnostics: Vec> = items(&recovered.getattr("diagnostics")?)?; + let mut diagnostics: Vec> = items(&native_field(&recovered, "diagnostics")?)?; diagnostics.extend(metadata); let combined: Bound<'py, PyAny> = classes.project_with( &recovered, @@ -84,11 +85,11 @@ fn semantic_metadata_diagnostics<'py>( } fn project_catalog<'py>(project: &Bound<'py, PyAny>) -> PyResult> { - let binding_catalog: Bound<'py, PyAny> = project.getattr("binding_catalog")?; + let binding_catalog: Bound<'py, PyAny> = native_field(project, "binding_catalog")?; if binding_catalog.is_none() { return Err(NativeCompilerError::new_err(NO_CATALOG_MESSAGE)); } - binding_catalog.getattr("native")?.extract() + native_field(&binding_catalog, "native")?.extract() } /// Models whose output names and lineage are the finished session's, so need no payload. @@ -101,9 +102,9 @@ fn session_fact_models( }; let kept: HashSet = session.fact_models().into_iter().collect(); let mut models: HashSet = HashSet::new(); - for model in project.getattr("models")?.try_iter()? { + for model in native_field(project, "models")?.try_iter()? { let model: Bound<'_, PyAny> = model?; - let name: String = model.getattr("name")?.extract()?; + let name: String = native_field(&model, "name")?.extract()?; if !kept.contains(&name) { continue; } 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 183d6014c9..8396178b1b 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 @@ -2,6 +2,7 @@ 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, RecordId}; use crate::bindings::_helpers::records::registry; use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; @@ -40,7 +41,7 @@ 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 = types.getattr("CompiledResourceType")?; + let resource = native_field(&types, "CompiledResourceType")?; let kinds = registry::class(py, RecordId::SqlReferenceKind)?; Ok(Self { diagnostic: registry::class(py, RecordId::CompilerDiagnostic)?, @@ -48,11 +49,11 @@ impl<'py> StageClasses<'py> { model_test_payload: registry::class(py, RecordId::CompiledModelSqlTestPayload)?, compile_phase: registry::class(types.py(), RecordId::DiagnosticPhase)? .getattr("COMPILE")?, - error: severity.getattr("ERROR")?, - warning: severity.getattr("WARNING")?, - model_resource: resource.getattr("MODEL")?, - source_resource: resource.getattr("SOURCE")?, - sql_test_resource: resource.getattr("SQL_TEST")?, + 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)?, @@ -61,8 +62,8 @@ impl<'py> StageClasses<'py> { 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: kinds.getattr("UDF")?, - table_function_kind: kinds.getattr("TABLE_FUNCTION")?, + udf_kind: native_field(&kinds, "UDF")?, + table_function_kind: native_field(&kinds, "TABLE_FUNCTION")?, }) } 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 2b722c58c2..ca4dff59d9 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 @@ -16,6 +16,7 @@ use crate::bindings::_helpers::analysis::model_reads::{inferred_names, lineage_i use crate::bindings::_helpers::analysis_session::session::NativeModelAnalysisSession; use crate::bindings::_helpers::boundary::panics::compiler_error; use crate::bindings::_helpers::compiled_project::compiled_model_view::LineageInput; +use crate::bindings::_helpers::records::fields::field as native_field; 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::{ @@ -44,7 +45,7 @@ pub(crate) fn recovered_types<'py>( shared: &StageShared<'_>, ) -> PyResult> { let py: Python<'py> = classes.py(); - let models: Vec> = items(&project.getattr("models")?)?; + let models: Vec> = items(&native_field(project, "models")?)?; if !blocking_binding_found(&models)? { return Ok(project.clone()); } @@ -58,14 +59,14 @@ pub(crate) fn recovered_types<'py>( } } } - let diagnostics: Vec> = items(&project.getattr("diagnostics")?)?; + let diagnostics: Vec> = items(&native_field(project, "diagnostics")?)?; let mut owners: Vec = Vec::with_capacity(diagnostics.len()); for item in &diagnostics { owners.push(DiagnosticOwner { id: id_of(&ids, item)?, - code: item.getattr("code")?.extract()?, - is_model: item.getattr("resource_type")?.eq(&classes.model_resource)?, - resource_name: item.getattr("resource_name")?.extract()?, + code: native_field(item, "code")?.extract()?, + is_model: native_field(item, "resource_type")?.eq(&classes.model_resource)?, + resource_name: native_field(item, "resource_name")?.extract()?, }); } let mut recovery_models: Vec = Vec::with_capacity(models.len()); @@ -78,7 +79,7 @@ pub(crate) fn recovered_types<'py>( )?); } let request = TypeRecoveryRequest { - dialect: project.getattr("sql_analysis_dialect")?.extract()?, + dialect: native_field(project, "sql_analysis_dialect")?.extract()?, models: recovery_models, diagnostics: owners, }; @@ -100,7 +101,7 @@ pub(crate) fn recovered_types<'py>( let mut kept: Vec> = Vec::with_capacity(outcome.kept.len()); for (index, note) in outcome.kept { let original: &Bound<'py, PyAny> = &diagnostics[index]; - let mut notes: Vec> = items(&original.getattr("notes")?)?; + let mut notes: Vec> = items(&native_field(original, "notes")?)?; if let Some(note) = note { notes.push(PyString::new(py, ¬e).into_any()); } @@ -142,14 +143,14 @@ pub(crate) fn diagnostic_ids<'py>( models: &[Bound<'py, PyAny>], ) -> PyResult> { let ids: Bound<'py, PyDict> = PyDict::new(project.py()); - for item in project.getattr("diagnostics")?.try_iter()? { + for item in native_field(project, "diagnostics")?.try_iter()? { let item: Bound<'py, PyAny> = item?; if !ids.contains(&item)? { ids.set_item(&item, ids.len())?; } } for model in models { - for item in model.getattr("binding_diagnostics")?.try_iter()? { + for item in native_field(model, "binding_diagnostics")?.try_iter()? { let item: Bound<'py, PyAny> = item?; if !ids.contains(&item)? { ids.set_item(&item, ids.len())?; @@ -175,8 +176,8 @@ pub(crate) fn lineage_outputs(lineage: Vec) -> Vec fn blocking_binding_found(models: &[Bound<'_, PyAny>]) -> PyResult { for model in models { - for item in model.getattr("binding_diagnostics")?.try_iter()? { - if item?.getattr("is_error")?.is_truthy()? { + for item in native_field(model, "binding_diagnostics")?.try_iter()? { + if native_field(&item?, "is_error")?.is_truthy()? { return Ok(true); } } @@ -190,16 +191,16 @@ fn recovery_model( (ids, raw_ids): (&Bound<'_, PyDict>, &Bound<'_, PyDict>), shared: &StageShared<'_>, ) -> PyResult { - let name: String = model.getattr("name")?.extract()?; + let name: String = native_field(model, "name")?.extract()?; let from_session: bool = shared.session_models.contains(&name); - let bindings: Vec> = items(&model.getattr("binding_diagnostics")?)?; + let bindings: Vec> = items(&native_field(model, "binding_diagnostics")?)?; let mut model_bindings: Vec = Vec::with_capacity(bindings.len()); for item in &bindings { model_bindings.push(ModelBinding { id: id_of(ids, item)?, - code: item.getattr("code")?.extract()?, - message: item.getattr("message")?.extract()?, - is_error: item.getattr("is_error")?.extract()?, + code: native_field(item, "code")?.extract()?, + message: native_field(item, "message")?.extract()?, + is_error: native_field(item, "is_error")?.extract()?, }); } let mut raw_bindings: Vec = Vec::new(); @@ -224,12 +225,12 @@ fn recovery_model( (inferred_names(model)?, lineage_inputs(model)?) }; let mut references: Vec = Vec::new(); - for reference in model.getattr("references")?.try_iter()? { - references.push(reference?.getattr("ref_name")?.extract()?); + for reference in native_field(model, "references")?.try_iter()? { + references.push(native_field(&reference?, "ref_name")?.extract()?); } Ok(RecoveryModel { name, - query_sql: model.getattr("query_sql")?.extract()?, + query_sql: native_field(model, "query_sql")?.extract()?, inferred_columns, references, lineage: lineage_outputs(lineage), @@ -264,7 +265,7 @@ fn revalidated<'py>( columns.set_item(column, UNKNOWN_RECOVERED_TYPE)?; } } - let project_models: Bound<'py, PyAny> = project.getattr("models")?; + let project_models: Bound<'py, PyAny> = native_field(project, "models")?; let mut revised: Vec> = Vec::with_capacity(models.len()); for index in models { let request: Bound<'py, PyAny> = @@ -276,12 +277,12 @@ fn revalidated<'py>( )? .get_item(0)?; let mut rows: Vec = Vec::new(); - for item in result.getattr("diagnostics")?.try_iter()? { + for item in native_field(&result, "diagnostics")?.try_iter()? { let item: Bound<'py, PyAny> = item?; rows.push(( - item.getattr("code")?.extract()?, - item.getattr("start")?.extract()?, - item.getattr("end")?.extract()?, + native_field(&item, "code")?.extract()?, + native_field(&item, "start")?.extract()?, + native_field(&item, "end")?.extract()?, )); } revised.push(rows); 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 a274ad1f7b..3e59eaa646 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/semantic_shapes/coverage.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/semantic_shapes/coverage.rs @@ -8,6 +8,7 @@ use pyo3::{pyfunction, wrap_pyfunction}; use crate::bindings::_helpers::analysis::model_reads::{inferred_rows, star_flags}; use crate::bindings::_helpers::compiled_project::compiled_model_view::InferredRow; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::semantic_shapes::shapes::project_shapes; const TYPE_CHECKED_DIALECTS: [&str; 4] = ["duckdb", "snowflake", "postgresql", "bigquery"]; @@ -35,17 +36,17 @@ pub(crate) fn semantic_coverage<'py>(project: &Bound<'py, PyAny>) -> PyResult> = Vec::with_capacity(models.len()); let mut blocked_by: HashMap> = HashMap::new(); for model in &models { - let name: String = model.getattr("name")?.extract()?; + let name: String = native_field(model, "name")?.extract()?; let mut rows: Vec<(String, String)> = Vec::new(); - for row in model.getattr("reference_rows")?.try_iter()? { + for row in native_field(model, "reference_rows")?.try_iter()? { let row: Bound<'py, PyAny> = row?; rows.push(( row.get_item(0)?.str()?.extract()?, row.get_item(1)?.extract()?, )); } - for item in model.getattr("binding_diagnostics")?.try_iter()? { - if item?.getattr("is_error")?.is_truthy()? { + for item in native_field(model, "binding_diagnostics")?.try_iter()? { + if native_field(&item?, "is_error")?.is_truthy()? { blocked_by.insert(name.clone(), HashSet::from([name.clone()])); break; } @@ -71,7 +72,7 @@ pub(crate) fn semantic_coverage<'py>(project: &Bound<'py, PyAny>) -> PyResult = project.getattr("sql_analysis_dialect")?; + let dialect: Bound<'py, PyAny> = native_field(project, "sql_analysis_dialect")?; let dialect: String = if dialect.is_truthy()? { dialect.extract()? } else { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/semantic_shapes/declarations.rs b/crates/sqlbuild-python/src/bindings/_helpers/semantic_shapes/declarations.rs index 5d1b6239e2..ba956e8688 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/semantic_shapes/declarations.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/semantic_shapes/declarations.rs @@ -7,6 +7,7 @@ use pyo3::types::{PyDict, PyTuple}; use pyo3::{pyfunction, wrap_pyfunction}; use crate::bindings::_helpers::boundary::keyword_request::Request; +use crate::bindings::_helpers::records::fields::field as native_field; const UNKNOWN: &str = "UNKNOWN"; @@ -14,7 +15,7 @@ const UNKNOWN: &str = "UNKNOWN"; pub(crate) fn function_names(functions: &Bound<'_, PyAny>) -> PyResult> { let mut names: BTreeSet = BTreeSet::new(); for function in functions.try_iter()? { - let name: String = function?.getattr("name")?.extract()?; + let name: String = native_field(&function?, "name")?.extract()?; names.insert(format!("__sqlbuild_udf_{name}")); names.insert(format!("__sqlbuild_table_function_{name}")); names.insert(name); @@ -35,15 +36,15 @@ pub(crate) fn declared_types( } for function in functions.try_iter()? { let function: Bound<'_, PyAny> = function?; - for argument in function.getattr("arguments")?.try_iter()? { - types.insert(argument?.getattr("type")?.extract()?); + for argument in native_field(&function, "arguments")?.try_iter()? { + types.insert(native_field(&argument?, "type")?.extract()?); } - let returns: Bound<'_, PyAny> = function.getattr("return_columns")?; + let returns: Bound<'_, PyAny> = native_field(&function, "return_columns")?; for column in returns.try_iter()? { - types.insert(column?.getattr("type")?.extract()?); + types.insert(native_field(&column?, "type")?.extract()?); } if !returns.is_truthy()? { - types.insert(function.getattr("returns")?.extract()?); + types.insert(native_field(&function, "returns")?.extract()?); } } types.remove(UNKNOWN); 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 4c51f02c5f..88c68f1fc8 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/semantic_shapes/shapes.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/semantic_shapes/shapes.rs @@ -14,6 +14,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; @@ -57,7 +58,7 @@ pub(crate) fn model_inputs<'py>( ) -> PyResult> { let py: Python<'py> = model.py(); let inputs: Bound<'py, PyDict> = PyDict::new(py); - for name in relation_row_names(&model.getattr("reference_rows")?)? { + for name in relation_row_names(&native_field(model, "reference_rows")?)? { let shape: Bound<'py, PyAny> = match shapes.get_item(&name)? { Some(shape) => shape, None => PyDict::new(py).into_any(), @@ -76,7 +77,7 @@ 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> = model.getattr("config")?.getattr("values")?; + let values: Bound<'py, PyAny> = native_field(model, "config")?.getattr("values")?; let snapshot: Bound<'py, PyAny> = registry::class(py, RecordId::MaterializationType)?.getattr("SNAPSHOT")?; if values @@ -107,7 +108,7 @@ fn binding_shape<'py>( profile: &Bound<'py, PyAny>, ) -> PyResult> { let py: Python<'py> = model.py(); - let dialect: Bound<'py, PyAny> = profile.getattr("sql_analysis_dialect")?; + 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 @@ -117,22 +118,22 @@ fn binding_shape<'py>( { return Ok(columns.clone()); } - let mut catalog: Bound<'py, PyAny> = profile.getattr("binding_catalog")?; + let mut catalog: Bound<'py, PyAny> = native_field(profile, "binding_catalog")?; if !catalog.is_truthy()? { let created: BindingCatalog = BindingCatalog::create( py, ( or_generic(&dialect)?.extract()?, false, - profile.getattr("semantic_known_functions")?.extract()?, - profile.getattr("semantic_known_types")?.extract()?, + native_field(profile, "semantic_known_functions")?.extract()?, + native_field(profile, "semantic_known_types")?.extract()?, ), inputs.as_any(), )?; catalog = Bound::new(py, created)?.into_any(); } let request: Bound<'py, PyDict> = PyDict::new(py); - request.set_item("sql", model.getattr("query_sql")?)?; + request.set_item("sql", native_field(model, "query_sql")?)?; request.set_item("dialect", or_generic(&dialect)?)?; request.set_item("stubs", PyDict::new(py))?; request.set_item("placeholders", PyDict::new(py))?; @@ -185,8 +186,8 @@ fn declared_columns<'py>( for column in columns.try_iter()? { let column: Bound<'py, PyAny> = column?; shape.set_item( - column.getattr("name")?, - or_unknown(&column.getattr("type")?)?, + native_field(&column, "name")?, + or_unknown(&native_field(&column, "type")?)?, )?; } Ok(shape) @@ -204,13 +205,13 @@ pub(crate) fn project_profile<'py>( let arguments: Bound<'py, PyDict> = PyDict::new(py); arguments.set_item( "sql_analysis_dialect", - project.getattr("sql_analysis_dialect")?, + native_field(project, "sql_analysis_dialect")?, )?; host::entry(py, AdapterHost::ExpressionInferenceProfile)?.call((), Some(&arguments))? } }; let arguments: Bound<'py, PyDict> = PyDict::new(py); - arguments.set_item("binding_catalog", project.getattr("binding_catalog")?)?; + arguments.set_item("binding_catalog", native_field(project, "binding_catalog")?)?; py.import(DATACLASSES_MODULE)? .getattr("replace")? .call((profile,), Some(&arguments)) @@ -224,23 +225,29 @@ pub(crate) fn project_shapes<'py>( let py: Python<'py> = project.py(); let profile: Bound<'py, PyAny> = project_profile(project, profile)?; let shapes: Bound<'py, PyDict> = PyDict::new(py); - for seed in project.getattr("seeds")?.try_iter()? { + for seed in native_field(project, "seeds")?.try_iter()? { let seed: Bound<'py, PyAny> = seed?; shapes.set_item( - seed.getattr("name")?, - declared_columns(py, &seed.getattr("schema_entry")?.getattr("columns")?)?, + native_field(&seed, "name")?, + declared_columns( + py, + &native_field(&seed, "schema_entry")?.getattr("columns")?, + )?, )?; } - for function in project.getattr("functions")?.try_iter()? { + for function in native_field(project, "functions")?.try_iter()? { let function: Bound<'py, PyAny> = function?; - let returns: Bound<'py, PyAny> = function.getattr("return_columns")?; + let returns: Bound<'py, PyAny> = native_field(&function, "return_columns")?; if returns.is_truthy()? { let shape: Bound<'py, PyDict> = PyDict::new(py); for column in returns.try_iter()? { let column: Bound<'py, PyAny> = column?; - shape.set_item(column.getattr("name")?, column.getattr("type")?)?; + shape.set_item( + native_field(&column, "name")?, + native_field(&column, "type")?, + )?; } - let name: String = function.getattr("name")?.extract()?; + let name: String = native_field(&function, "name")?.extract()?; shapes.set_item(format!("{TABLE_FUNCTION_PREFIX}{name}"), shape)?; } } @@ -248,18 +255,18 @@ pub(crate) fn project_shapes<'py>( registry::class(py, RecordId::ContractPolicy)?.getattr("ENFORCED")?; let mut expression_names: Vec> = Vec::new(); let mut expressions: Vec> = Vec::new(); - for source in project.getattr("sources")?.try_iter()? { + for source in native_field(project, "sources")?.try_iter()? { let source: Bound<'py, PyAny> = source?; - let entry: Bound<'py, PyAny> = source.getattr("source_entry")?; - if entry.getattr("contract")?.eq(&enforced)? { + let entry: Bound<'py, PyAny> = native_field(&source, "source_entry")?; + if native_field(&entry, "contract")?.eq(&enforced)? { shapes.set_item( - source.getattr("name")?, - declared_columns(py, &entry.getattr("columns")?)?, + native_field(&source, "name")?, + declared_columns(py, &native_field(&entry, "columns")?)?, )?; } else { - let expression: Bound<'py, PyAny> = entry.getattr("expression")?; + let expression: Bound<'py, PyAny> = native_field(&entry, "expression")?; if expression.is_truthy()? { - expression_names.push(source.getattr("name")?); + expression_names.push(native_field(&source, "name")?); expressions.push(expression); } } @@ -297,7 +304,7 @@ fn model_shapes<'py>( for model in &pending { let declared: Option> = declared_rows(model)?; let inferred: Option> = inferred_rows(model)?; - let rows: Bound<'py, PyAny> = model.getattr("reference_rows")?; + 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")? @@ -317,7 +324,7 @@ fn model_shapes<'py>( }; shape.set_item(name, column_type)?; } - shapes.set_item(model.getattr("name")?, shape)?; + shapes.set_item(native_field(model, "name")?, shape)?; continue; } let (has_star, star_resolved): (bool, bool) = star_flags(model)?; @@ -328,7 +335,7 @@ fn model_shapes<'py>( let inputs: Bound<'py, PyDict> = model_inputs(model, shapes)?; let columns: Bound<'py, PyDict> = typed_columns(py, inferred)?; shapes.set_item( - model.getattr("name")?, + native_field(model, "name")?, published_shape(model, &columns, &inputs, profile)?, )?; } else { @@ -373,8 +380,8 @@ fn binding_relation_names<'py>( PyTuple::new( py, [ - reference.getattr("ref_kind")?.str()?.into_any(), - reference.getattr("ref_name")?, + native_field(&reference, "ref_kind")?.str()?.into_any(), + native_field(&reference, "ref_name")?, ], )? .into_any(), 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 f6afb5970a..d1c3d30b2c 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 @@ -14,6 +14,7 @@ use crate::bindings::_helpers::analysis::schema_request::{ }; use crate::bindings::_helpers::attachments::cursor_intrinsics::analysis_cursor_sql; use crate::bindings::_helpers::boundary::keyword_request::Request; +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::semantic_shapes::declarations::{declared_types, function_names}; @@ -52,7 +53,7 @@ pub(crate) fn source_binding_diagnostics<'py>( return Ok(PyTuple::empty(py)); } let profile: Object<'py> = request.get("profile")?; - let raw_dialect: Object<'py> = profile.getattr("sql_analysis_dialect")?; + let raw_dialect: Object<'py> = native_field(&profile, "sql_analysis_dialect")?; let key: Object<'py> = if raw_dialect.is_truthy()? { raw_dialect.clone() } else { @@ -89,8 +90,8 @@ pub(crate) fn source_binding_diagnostics<'py>( .map(|column| { let column: Object<'py> = column?; Ok(( - column.getattr("name")?.extract()?, - column.getattr("type")?.extract()?, + native_field(&column, "name")?.extract()?, + native_field(&column, "type")?.extract()?, )) }) .collect::>()?; @@ -103,7 +104,7 @@ pub(crate) fn source_binding_diagnostics<'py>( } shapes.set_item(name, quoted)?; } - let binding_catalog: Object<'py> = project.getattr("binding_catalog")?; + let binding_catalog: Object<'py> = native_field(&project, "binding_catalog")?; let mut physical_catalog: Object<'py> = if binding_catalog.is_none() { binding_catalog } else { @@ -132,22 +133,22 @@ fn rebound_models<'py>( ) -> PyResult>> { let py: Python<'py> = project.py(); let by_name: Bound<'py, PyDict> = PyDict::new(py); - for model in project.getattr("models")?.try_iter()? { + for model in native_field(project, "models")?.try_iter()? { let model: Object<'py> = model?; - by_name.set_item(model.getattr("name")?, &model)?; + by_name.set_item(native_field(&model, "name")?, &model)?; } let graph: Bound<'py, PyDict> = PyDict::new(py); - for model in project.getattr("models")?.try_iter()? { + for model in native_field(project, "models")?.try_iter()? { let model: Object<'py> = model?; let mut upstream: Vec> = Vec::new(); - for reference in model.getattr("references")?.try_iter()? { + for reference in native_field(&model, "references")?.try_iter()? { let reference: Object<'py> = reference?; - let name: Object<'py> = reference.getattr("ref_name")?; - if reference.getattr("ref_kind")?.eq(REF_KIND)? && by_name.contains(&name)? { + let name: Object<'py> = native_field(&reference, "ref_name")?; + if native_field(&reference, "ref_kind")?.eq(REF_KIND)? && by_name.contains(&name)? { upstream.push(name); } } - graph.set_item(model.getattr("name")?, PyTuple::new(py, upstream)?)?; + graph.set_item(native_field(&model, "name")?, PyTuple::new(py, upstream)?)?; } let order: Object<'py> = py .import(GRAPHLIB_MODULE)? @@ -166,14 +167,16 @@ fn rebound_models<'py>( .getattr("config")? .getattr("values")? .call_method1("get", ("sql_analysis",))?; - if !selected_keys.contains(model.getattr("key")?)? || opted.is(PyBool::new(py, false)) { + if !selected_keys.contains(native_field(&model, "key")?)? + || opted.is(PyBool::new(py, false)) + { continue; } let mut reads_changed: bool = false; - for reference in model.getattr("references")?.try_iter()? { + for reference in native_field(&model, "references")?.try_iter()? { let reference: Object<'py> = reference?; - let kind: String = reference.getattr("ref_kind")?.str()?.extract()?; - let upstream: String = reference.getattr("ref_name")?.extract()?; + let kind: String = native_field(&reference, "ref_kind")?.str()?.extract()?; + let upstream: String = native_field(&reference, "ref_name")?.extract()?; if (kind == SOURCE_KIND && changed.contains(&upstream)) || (kind == REF_KIND && affected.contains(&upstream)) { @@ -184,7 +187,7 @@ fn rebound_models<'py>( if !reads_changed { continue; } - let model_name: String = model.getattr("name")?.extract()?; + let model_name: String = native_field(&model, "name")?.extract()?; affected.insert(model_name.clone()); models.push(model.clone()); let inputs: Bound<'py, PyDict> = model_inputs(&model, shapes)?; @@ -199,10 +202,10 @@ fn rebound_models<'py>( let query: Object<'py> = call( &query_class, &[ - ("sql", model.getattr("query_sql")?), + ("sql", native_field(&model, "query_sql")?), ("mode", PyString::new(py, "reanalysis").into_any()), ("placeholders", placeholders(&model)?), - ("references", model.getattr("references")?), + ("references", native_field(&model, "references")?), ], )?; let analysis: Object<'py> = call( @@ -215,7 +218,7 @@ fn rebound_models<'py>( ], )? .get_item(0)?; - let inferred: Object<'py> = analysis.getattr("columns")?; + let inferred: Object<'py> = native_field(&analysis, "columns")?; let mut all_inputs_closed: bool = true; for shape in inputs.values() { if !shape.is_truthy()? { @@ -224,18 +227,18 @@ fn rebound_models<'py>( } } if inferred.is_truthy()? - && (!analysis.getattr("has_star")?.is_truthy()? || all_inputs_closed) + && (!native_field(&analysis, "has_star")?.is_truthy()? || all_inputs_closed) { let columns: Bound<'py, PyDict> = PyDict::new(py); for column in inferred.try_iter()? { let column: Object<'py> = column?; - let column_type: Object<'py> = column.getattr("type")?; + let column_type: Object<'py> = native_field(&column, "type")?; let column_type: Object<'py> = if column_type.is_truthy()? { column_type } else { PyString::new(py, UNKNOWN).into_any() }; - columns.set_item(column.getattr("name")?, column_type)?; + columns.set_item(native_field(&column, "name")?, column_type)?; } shapes.set_item( &model_name, @@ -256,17 +259,19 @@ fn validated_diagnostics<'py>( dialect: &Object<'py>, ) -> PyResult> { let py: Python<'py> = project.py(); - let functions: Object<'py> = project.getattr("functions")?; + let functions: Object<'py> = native_field(project, "functions")?; let known_functions: Object<'py> = PyTuple::new(py, function_names(&functions)?)?.into_any(); let known_types: Object<'py> = PyTuple::new(py, declared_types(&functions, shapes.as_any())?)?.into_any(); - let ignores: bool = - binding_catalog::ignores_quoted_case(&project.getattr("effective_connection")?, dialect)?; + let ignores: bool = binding_catalog::ignores_quoted_case( + &native_field(project, "effective_connection")?, + dialect, + )?; let mut requests: Vec> = Vec::with_capacity(models.len()); for model in models { let query_sql: String = analysis_cursor_sql( py, - &model.getattr("query_sql")?.extract::()?, + &native_field(model, "query_sql")?.extract::()?, &model .getattr("config")? .getattr("values")? @@ -295,37 +300,37 @@ fn validated_diagnostics<'py>( .try_iter()? { let (path, expansion): (Object<'py>, Object<'py>) = item?.extract()?; - expansions.set_item(path.getattr("stem")?, expansion)?; + expansions.set_item(native_field(&path, "stem")?, expansion)?; } let skipped: Object<'py> = PyString::new(py, BINDING_UNKNOWN_TABLE_INTERNAL_CODE).into_any(); let types = py.import(COMPILE_TYPES_MODULE)?; 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> = types.getattr("CompiledResourceType")?.getattr("MODEL")?; + let model_type: Object<'py> = native_field(&types, "CompiledResourceType")?.getattr("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?; - let expansion: Option> = expansions.get_item(model.getattr("name")?)?; + let expansion: Option> = expansions.get_item(native_field(model, "name")?)?; let mut mapper: Option> = None; - for diagnostic in result.getattr("diagnostics")?.try_iter()? { + for diagnostic in native_field(&result, "diagnostics")?.try_iter()? { let diagnostic: Object<'py> = diagnostic?; - if diagnostic.getattr("code")?.eq(&skipped)? { + if native_field(&diagnostic, "code")?.eq(&skipped)? { continue; } let built: Object<'py> = match mapper.take() { Some(built) => built, - None => positions(model, &request.getattr("sql")?, expansion.as_ref())?, + None => positions(model, &native_field(request, "sql")?, expansion.as_ref())?, }; - let position: Object<'py> = built.getattr("position")?; + let position: Object<'py> = native_field(&built, "position")?; mapper = Some(built); let (line, column): (Object<'py>, Object<'py>) = call( &position, &[ - ("start", diagnostic.getattr("start")?), - ("line", diagnostic.getattr("line")?), - ("column", diagnostic.getattr("column")?), - ("message", diagnostic.getattr("message")?), + ("start", native_field(&diagnostic, "start")?), + ("line", native_field(&diagnostic, "line")?), + ("column", native_field(&diagnostic, "column")?), + ("message", native_field(&diagnostic, "message")?), ], )? .extract()?; @@ -335,13 +340,13 @@ fn validated_diagnostics<'py>( ("phase", phase.clone()), ( "severity", - severity_class.call1((diagnostic.getattr("severity")?,))?, + severity_class.call1((native_field(&diagnostic, "severity")?,))?, ), - ("code", diagnostic.getattr("code")?), - ("message", diagnostic.getattr("message")?), + ("code", native_field(&diagnostic, "code")?), + ("message", native_field(&diagnostic, "message")?), ("resource_type", model_type.clone()), - ("resource_name", model.getattr("name")?), - ("path", model.getattr("relative_path")?), + ("resource_name", native_field(model, "name")?), + ("path", native_field(model, "relative_path")?), ("line", line), ("column", column), ], @@ -358,12 +363,12 @@ fn positions<'py>( expansion: Option<&Object<'py>>, ) -> PyResult> { let py: Python<'py> = model.py(); - let authored_query: Object<'py> = model.getattr("authored_query_sql")?; + let authored_query: Object<'py> = native_field(model, "authored_query_sql")?; let mut passes: Vec> = Vec::new(); let expanded: Object<'py> = match expansion { None => authored_query.clone(), Some(expansion) => { - for spans in expansion.getattr("passes")?.try_iter()? { + for spans in native_field(expansion, "passes")?.try_iter()? { let mut rows: Vec> = Vec::new(); for span in spans?.try_iter()? { let span: Object<'py> = span?; @@ -376,11 +381,11 @@ fn positions<'py>( } passes.push(PyTuple::new(py, rows)?.into_any()); } - expansion.getattr("expanded_sql")? + native_field(expansion, "expanded_sql")? } }; let request: Bound<'py, PyDict> = PyDict::new(py); - request.set_item("authored", model.getattr("authored_sql")?)?; + request.set_item("authored", native_field(model, "authored_sql")?)?; request.set_item("query", authored_query)?; request.set_item("expanded", expanded)?; request.set_item("cleaned", cleaned)?; @@ -444,8 +449,10 @@ fn placeholders<'py>(model: &Object<'py>) -> PyResult> { } fn ignores_quoted_case<'py>(project: &Object<'py>, dialect: &Object<'py>) -> PyResult> { - let ignores: bool = - binding_catalog::ignores_quoted_case(&project.getattr("effective_connection")?, dialect)?; + let ignores: bool = binding_catalog::ignores_quoted_case( + &native_field(project, "effective_connection")?, + dialect, + )?; Ok(PyBool::new(project.py(), ignores).to_owned().into_any()) } 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 ce358240fb..6948cafe2f 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 @@ -13,6 +13,7 @@ use sqlbuild_analysis::compiler::models::{ use sqlbuild_core::panics::main::native_failure::native_failure; use crate::bindings::_helpers::boundary::panics::value_error; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::sqltext::lexical_syntax::LexicalSyntaxInput; use crate::bindings::types::CompilerDetach; @@ -68,9 +69,9 @@ pub(crate) fn assemble_compiled_sql_tests( } fn assembly_model(model: &Bound<'_, PyAny>) -> PyResult { - let macro_deps: Vec = model.getattr("macro_deps")?.extract()?; + let macro_deps: Vec = native_field(model, "macro_deps")?.extract()?; let unscanned_macro_source = if macro_deps.is_empty() { - Some(model.getattr("macro_source_sql")?.extract::()?) + Some(native_field(model, "macro_source_sql")?.extract::()?) .filter(|sql| sql.contains(MACRO_TOKEN)) } else { None @@ -89,9 +90,9 @@ fn assembly_model(model: &Bound<'_, PyAny>) -> PyResult { .map(|reference| { let reference = reference?; Ok(SqlTestAssemblyReference { - kind: reference.getattr("ref_kind")?.extract()?, - name: reference.getattr("ref_name")?.extract()?, - package: reference.getattr("ref_package")?.extract()?, + kind: native_field(&reference, "ref_kind")?.extract()?, + name: native_field(&reference, "ref_name")?.extract()?, + package: native_field(&reference, "ref_package")?.extract()?, }) }) .collect::>()?, @@ -99,49 +100,51 @@ fn assembly_model(model: &Bound<'_, PyAny>) -> PyResult { } fn assembly_test(test: &Bound<'_, PyAny>) -> PyResult { - let test_file = test.getattr("test_file")?; - let test_block = test.getattr("test_block")?; - let relative_path = test_file.getattr("relative_path")?; - let payload = test.getattr("payload")?; + let test_file = native_field(test, "test_file")?; + let test_block = native_field(test, "test_block")?; + let relative_path = native_field(&test_file, "relative_path")?; + let payload = native_field(test, "payload")?; let payload = if payload.hasattr("actual_cte")? { SqlTestAssemblyPayload::Direct { - mode: payload.getattr("mode")?.extract()?, - tested_resource_names: payload.getattr("tested_resource_names")?.extract()?, + mode: native_field(&payload, "mode")?.extract()?, + tested_resource_names: native_field(&payload, "tested_resource_names")?.extract()?, } } else { SqlTestAssemblyPayload::Model(SqlTestAssemblyModelPayload { authored_ctes: ctes(&payload, "authored_ctes")?, expected_ctes: ctes(&payload, "expected_ctes")?, assertion_ctes: ctes(&payload, "assertion_ctes")?, - expected_model_names: payload.getattr("expected_model_names")?.extract()?, + expected_model_names: native_field(&payload, "expected_model_names")?.extract()?, assertion_target_model_names: payload .getattr("assertion_target_model_names")? .extract()?, reference_target_model_names: payload .getattr("reference_target_model_names")? .extract()?, - mock_model_names: payload.getattr("mock_model_names")?.extract()?, + mock_model_names: native_field(&payload, "mock_model_names")?.extract()?, }) }; Ok(SqlTestAssemblyTest { - block_name: test_block.getattr("name")?.extract()?, - file_stem: test_file.getattr("file_path")?.getattr("stem")?.extract()?, + block_name: native_field(&test_block, "name")?.extract()?, + file_stem: native_field(&test_file, "file_path")? + .getattr("stem")? + .extract()?, relative_path: relative_path.call_method0("as_posix")?.extract()?, - relative_stem: relative_path.getattr("stem")?.extract()?, - contents: test_file.getattr("contents")?.extract()?, - block_sql: test_block.getattr("sql_body")?.extract()?, - block_index: test_block.getattr("test_index")?.extract()?, - sql_body: test.getattr("sql_body")?.extract()?, - case_name: test.getattr("case_name")?.extract()?, + relative_stem: native_field(&relative_path, "stem")?.extract()?, + contents: native_field(&test_file, "contents")?.extract()?, + block_sql: native_field(&test_block, "sql_body")?.extract()?, + 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")? .try_iter()? .map(|parameter| { let parameter = parameter?; Ok(( - parameter.getattr("name")?.extract()?, - parameter.getattr("value_type")?.extract()?, - parameter.getattr("nullable")?.extract()?, + native_field(¶meter, "name")?.extract()?, + native_field(¶meter, "value_type")?.extract()?, + native_field(¶meter, "nullable")?.extract()?, )) }) .collect::>()?, @@ -164,8 +167,8 @@ fn ctes(owner: &Bound<'_, PyAny>, attribute: &str) -> PyResult> .map(|value| { let value = value?; Ok(SqlTestCte { - name: value.getattr("name")?.extract()?, - sql_body: value.getattr("sql_body")?.extract()?, + name: native_field(&value, "name")?.extract()?, + sql_body: native_field(&value, "sql_body")?.extract()?, }) }) .collect() @@ -173,8 +176,10 @@ 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 = value.getattr("logical_type")?.getattr("kind")?.extract()?; - let payload = value.getattr("value")?; + let kind: String = native_field(value, "logical_type")? + .getattr("kind")? + .extract()?; + let payload = native_field(value, "value")?; Ok(match kind.as_str() { "string" => SqlTestParameterValue::String(payload.extract()?), "integer" => SqlTestParameterValue::Integer(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 618ed0562d..bd9a742f8a 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 @@ -12,6 +12,7 @@ use sqlbuild_analysis::compiler::models::{SqlTestExtractionFailure, SqlTestReque use sqlbuild_analysis::sql_test_attachment::main::authored_test_offset::authored_test_offset; use sqlbuild_sqltext::sql_scan::models::LexicalSyntax; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::ErrorId; use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::sqltext::lexical_syntax::LexicalSyntaxInput; @@ -26,9 +27,9 @@ pub(crate) fn prefixed_ctes<'py>( let result = PyDict::new(ctes.py()); for cte in ctes.try_iter()? { let cte = cte?; - let name: String = cte.getattr("name")?.extract()?; + let name: String = native_field(&cte, "name")?.extract()?; if let Some(name) = name.strip_prefix(prefix) { - result.set_item(name, cte.getattr("sql_body")?)?; + result.set_item(name, native_field(&cte, "sql_body")?)?; } } Ok(result) @@ -52,13 +53,13 @@ impl AuthoredBody { ) -> PyResult { Ok(Self { sql: sql.extract()?, - contents: file.getattr("contents")?.extract()?, + contents: native_field(file, "contents")?.extract()?, relative_path: file .getattr("relative_path")? .call_method0("as_posix")? .extract()?, - file_label: file.getattr("relative_path")?.str()?.extract()?, - span: owner.getattr("sql_body_span")?.extract()?, + file_label: native_field(file, "relative_path")?.str()?.extract()?, + span: native_field(owner, "sql_body_span")?.extract()?, }) } } @@ -127,11 +128,11 @@ fn validate_authored_sql_test_ctes( let mut blocks: Vec = Vec::new(); for test_file in test_files.try_iter()? { let test_file: Bound<'_, PyAny> = test_file?; - for block in test_file.getattr("blocks")?.try_iter()? { + for block in native_field(&test_file, "blocks")?.try_iter()? { let block: Bound<'_, PyAny> = block?; blocks.push(AuthoredBody::read( &block, - &block.getattr("sql_body")?, + &native_field(&block, "sql_body")?, &test_file, )?); } @@ -142,7 +143,7 @@ fn validate_authored_sql_test_ctes( let scenario_file: Bound<'_, PyAny> = scenario_file?; scenarios.push(AuthoredBody::read( &scenario_file, - &scenario_file.getattr("sql_body")?, + &native_field(&scenario_file, "sql_body")?, &scenario_file, )?); } 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 71b4cf56b0..bac9ffcbf3 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,6 +5,7 @@ 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::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::{ @@ -63,7 +64,7 @@ impl<'py> Classes<'py> { let types: Bound<'py, PyModule> = py.import(COMPILE_TYPES_MODULE)?; Ok(Self { direct_input: registry::class(py, RecordId::CompileDirectLogicSqlTestInputPayload)?, - resource_type: types.getattr("CompiledResourceType")?, + resource_type: native_field(&types, "CompiledResourceType")?, phase: registry::class(py, RecordId::DiagnosticPhase)?.getattr("COMPILE")?, severity: registry::class(py, RecordId::DiagnosticSeverity)?.getattr("ERROR")?, assembly: registry::class(py, RecordId::NativeSqlTestAssembly)?, @@ -129,9 +130,9 @@ fn compiled_payload<'py>( copied(payload, &DIRECT_PAYLOAD_FIELDS)?, )?; let resource_type: Bound<'py, PyAny> = classes.model(RecordId::CompiledSqlTestResource)?; - let mode: Bound<'py, PyAny> = payload.getattr("mode")?; + let mode: Bound<'py, PyAny> = native_field(payload, "mode")?; let mut resources: Vec> = Vec::new(); - for name in payload.getattr("tested_resource_names")?.try_iter()? { + for name in native_field(payload, "tested_resource_names")?.try_iter()? { resources.push(keyword_call( &resource_type, vec![("kind", mode.clone()), ("name", name?)], @@ -163,7 +164,8 @@ fn keyed_diagnostic<'py>( (line, column, end_line, end_column, message, help): DiagnosticRow, ) -> PyResult> { let py: Python<'py> = classes.py(); - let path: Bound<'py, PyAny> = test_input.getattr("test_file")?.getattr("relative_path")?; + let path: Bound<'py, PyAny> = + native_field(test_input, "test_file")?.getattr("relative_path")?; let key: Bound<'py, PyTuple> = PyTuple::new( py, [ @@ -212,31 +214,41 @@ fn compiled_test<'py>( .iter() .map(|(resource_type, dep)| classes.object_key(resource_type, dep)) .collect::>()?; - let (payload, payload_fields) = compiled_payload(classes, &test_input.getattr("payload")?)?; - let test_file: Bound<'py, PyAny> = test_input.getattr("test_file")?; - let test_block: Bound<'py, PyAny> = test_input.getattr("test_block")?; + let (payload, payload_fields) = + compiled_payload(classes, &native_field(test_input, "payload")?)?; + let test_file: Bound<'py, PyAny> = native_field(test_input, "test_file")?; + let test_block: Bound<'py, PyAny> = native_field(test_input, "test_block")?; let mut fields: Vec<(&str, Bound<'py, PyAny>)> = vec![ ("key", classes.object_key("sql_test", &name)?), ("scope_deps", PyTuple::new(py, scope_deps)?.into_any()), ("name", text(py, &name)), ("test_file", test_file.clone()), ("test_block", test_block.clone()), - ("sql_body", test_input.getattr("sql_body")?), - ("mode", test_input.getattr("mode")?), + ("sql_body", native_field(test_input, "sql_body")?), + ("mode", native_field(test_input, "mode")?), ("payload", payload), - ("source_path", test_file.getattr("relative_path")?), - ("ownership_root", test_file.getattr("ownership_root")?), - ("block_index", test_block.getattr("test_index")?), - ("explicit_name", test_block.getattr("name")?), - ("parent_name", test_input.getattr("parent_name")?), - ("case_name", test_input.getattr("case_name")?), - ("case_index", test_input.getattr("case_index")?), + ("source_path", native_field(&test_file, "relative_path")?), + ( + "ownership_root", + native_field(&test_file, "ownership_root")?, + ), + ("block_index", native_field(&test_block, "test_index")?), + ("explicit_name", native_field(&test_block, "name")?), + ("parent_name", native_field(test_input, "parent_name")?), + ("case_name", native_field(test_input, "case_name")?), + ("case_index", native_field(test_input, "case_index")?), ( "case_fingerprint", case_fingerprint.into_pyobject(py)?.into_any(), ), - ("parameter_schema", test_input.getattr("parameter_schema")?), - ("parameter_values", test_input.getattr("parameter_values")?), + ( + "parameter_schema", + native_field(test_input, "parameter_schema")?, + ), + ( + "parameter_values", + native_field(test_input, "parameter_values")?, + ), ( "target_model_names", PyTuple::new(py, target_model_names)?.into_any(), @@ -285,9 +297,9 @@ fn assemble_native_sql_tests<'py>( py: Python<'py>, inputs: &Bound<'py, PyAny>, ) -> PyResult> { - let test_inputs: Vec> = inputs.getattr("test_inputs")?.extract()?; + let test_inputs: Vec> = native_field(inputs, "test_inputs")?.extract()?; let request = AssemblyRequest { - model_inputs: inputs.getattr("model_inputs")?.extract()?, + model_inputs: native_field(inputs, "model_inputs")?.extract()?, test_inputs: test_inputs.clone(), lexical_syntax: inputs .getattr("sql_lexical_syntax")? 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 536be5e414..5245f950ce 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,6 +16,7 @@ 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::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::{ @@ -46,8 +47,9 @@ pub(super) fn uses_cursor_intrinsics(py: Python<'_>, sql: &str) -> PyResult) -> PyResult { - let block = test.getattr("test_block")?; - Ok(!block.getattr("cursor_start")?.is_none() || !block.getattr("cursor_end")?.is_none()) + let block = native_field(test, "test_block")?; + Ok(!native_field(&block, "cursor_start")?.is_none() + || !native_field(&block, "cursor_end")?.is_none()) } /// SQL with intrinsics rendered from the test's declared window, or the wide default window. @@ -66,7 +68,7 @@ pub(super) fn render_test_cursor_intrinsics<'py>( let (start, end) = test_window(&cursor, test)?; let literal = |value: &Bound<'py, PyAny>| -> PyResult { call_with( - &adapter.getattr("render_cursor_bound_literal")?, + &native_field(adapter, "render_cursor_bound_literal")?, &[ ("value", PyString::new(py, &render_value(value)?).into_any()), ("cursor_type", cursor.cursor_type.clone()), @@ -111,7 +113,9 @@ fn test_window<'py>( cursor: &ModelCursor<'py>, test: Option<&Bound<'py, PyAny>>, ) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> { - let block = test.map(|test| test.getattr("test_block")).transpose()?; + let block = test + .map(|test| native_field(test, "test_block")) + .transpose()?; let declared = |name: &str| -> PyResult>> { let Some(block) = &block else { return Ok(None); @@ -234,7 +238,7 @@ fn ordering(left: &Bound<'_, PyAny>, right: &Bound<'_, PyAny>) -> PyResult(model: &Bound<'py, PyAny>) -> PyResult> { let py = model.py(); - let values = model.getattr("config")?.getattr("values")?; + let values = native_field(model, "config")?.getattr("values")?; let declared = config_str(&values, "cursor_type")?.filter(|value| { value .extract::() @@ -243,14 +247,14 @@ fn model_cursor<'py>(model: &Bound<'py, PyAny>) -> PyResult> { let cursor_type = registry::class(py, RecordId::CursorType)?.call1(( declared.unwrap_or_else(|| PyString::new(py, TIMESTAMP_CURSOR_TYPE).into_any()), ))?; - let type_name: String = cursor_type.getattr("value")?.extract()?; + 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 session = CursorSession::new(py)?; session .grain(&grain)? - .map(|known| PyResult::Ok((grain.getattr("value")?.extract()?, known))) + .map(|known| PyResult::Ok((native_field(&grain, "value")?.extract()?, known))) .transpose()? } _ => None, @@ -267,7 +271,9 @@ fn model_cursor<'py>(model: &Bound<'py, PyAny>) -> PyResult> { fn test_label(test: &Bound<'_, PyAny>) -> PyResult { Ok(format!( "SQL test '{}' in '{}'", - test.getattr("name")?.str()?, - test.getattr("test_file")?.getattr("relative_path")?.str()? + native_field(test, "name")?.str()?, + native_field(test, "test_file")? + .getattr("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 fce670a9e9..ac00c76f44 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 @@ -17,6 +17,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::sql_test_glue::constants::{COMPILE_TYPES, FIXTURE_PREFIXES}; /// Report every statically provable fixture defect or return the completed fixture SQL groups. @@ -30,11 +31,11 @@ pub(super) fn validated_fixtures<'py>( let (chain, [refs, sources, seeds, expected]) = request; let adapter = inputs.1; let resource_types = imported(py, COMPILE_TYPES, "CompiledResourceType")?; - let model = resource_types.getattr("MODEL")?; + let model = native_field(&resource_types, "MODEL")?; let kinds = [ model.clone(), - resource_types.getattr("SOURCE")?, - resource_types.getattr("SEED")?, + native_field(&resource_types, "SOURCE")?, + native_field(&resource_types, "SEED")?, ]; let groups = PyTuple::new( py, @@ -48,7 +49,7 @@ pub(super) fn validated_fixtures<'py>( let completion = complete_fixtures(context, adapter, (&chain, groups.as_any()))?; let mut errors: BTreeSet = BTreeSet::new(); for diagnostic in &completion.diagnostics { - let key = diagnostic.getattr("key")?; + let key = native_field(diagnostic, "key")?; errors.insert(format!( "{}: {}", fixture_location( @@ -56,12 +57,12 @@ pub(super) fn validated_fixtures<'py>( &key.get_item(0)?, &key.get_item(1)?.extract::()? )?, - diagnostic.getattr("message")?.extract::()? + native_field(diagnostic, "message")?.extract::()? )); } let inferred_by_fixture = &completion.inferred_by_fixture; let completed_expected = expected.copy()?; - let sql_test = resource_types.getattr("SQL_TEST")?; + let sql_test = native_field(&resource_types, "SQL_TEST")?; let type_groups = [ (&kinds[0], &refs), (&kinds[1], &sources), @@ -106,7 +107,7 @@ pub(super) fn validated_fixtures<'py>( &name, complete_typed_null_columns(&completed_sql, &inference, types, adapter)?, )?; - Some(inference.getattr("columns")?) + Some(native_field(&inference, "columns")?) } } else { let key = PyTuple::new(py, [kind, &key_name])?; @@ -127,20 +128,20 @@ pub(super) fn validated_fixtures<'py>( .unwrap_or_default(); for column in columns.try_iter()? { let column = column?; - let column_name = column.getattr("name")?; + let column_name = native_field(&column, "name")?; let folded_name: String = column_name.call_method0("casefold")?.extract()?; let expected_type: Option = types .iter() .find(|(column, _)| *column == folded_name) .map(|(_, sql_type)| sql_type.clone()); - let inferred_type: Option = column.getattr("type")?.extract()?; + let inferred_type: Option = native_field(&column, "type")?.extract()?; if !collection_mismatch(expected_type.as_deref(), inferred_type.as_deref()) { continue; } let label = if expected_output { "expected output".to_owned() } else { - format!("mock {}", kind.getattr("value")?.extract::()?) + format!("mock {}", native_field(kind, "value")?.extract::()?) }; errors.insert(format!( "{}: {label} '{name}' column '{}' has incompatible type {}; resource type is {}. Use an explicit CAST, ARRAY_CONSTRUCT, or PARSE_JSON as appropriate", @@ -153,7 +154,7 @@ pub(super) fn validated_fixtures<'py>( } } if !errors.is_empty() { - let test_name: String = test.getattr("name")?.extract()?; + let test_name: String = native_field(test, "name")?.extract()?; return Ok(Err(errors .into_iter() .map(|error| format!("SQL test '{test_name}': {error}")) @@ -183,14 +184,14 @@ pub(super) fn fixture_location( resource_type: &Bound<'_, PyAny>, name: &str, ) -> PyResult { - let value: String = resource_type.getattr("value")?.extract()?; + let value: String = native_field(resource_type, "value")?.extract()?; let prefix = FIXTURE_PREFIXES .iter() .find_map(|(kind, prefix)| (*kind == value).then_some(*prefix)) .ok_or_else(|| pyo3::exceptions::PyKeyError::new_err(value))?; - let test_file = test.getattr("test_file")?; + let test_file = native_field(test, "test_file")?; let line = fixture_line( - &test_file.getattr("contents")?.extract::()?, + &native_field(&test_file, "contents")?.extract::()?, &format!("{prefix}{name}"), ) .map_err(value_error)?; 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 25b43046fc..d2960b60ba 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 @@ -7,6 +7,7 @@ use crate::bindings::_helpers::analysis::model_reads; use crate::bindings::_helpers::compile_driver::python_calls::{KeywordArguments, call_with}; use crate::bindings::_helpers::planner_strategy::fixture_context::planning_context; use crate::bindings::_helpers::planner_strategy::models::RelationFixturePlanningContext; +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}; @@ -89,7 +90,7 @@ pub(crate) fn assemble_sql_test_plans<'py>( } let mut plans = plans.into_iter(); let models = PyDict::new(py); - for model in project.getattr("models")?.try_iter()? { + for model in native_field(project, "models")?.try_iter()? { let model = model?; models.set_item(model_reads::name_object(&model)?, model)?; } @@ -111,7 +112,7 @@ pub(crate) fn assemble_sql_test_plans<'py>( "missing SQL-test plan", )); }; - let mut errors: Vec = plan.getattr("error_messages")?.extract()?; + let mut errors: Vec = native_field(&plan, "error_messages")?.extract()?; errors.extend(super::plan_entries::expected_diagnostics( test, &plan, @@ -139,7 +140,7 @@ pub(crate) fn assemble_sql_test_plans<'py>( &assembly.sql_analysis_enabled, )?, ), - ("warnings", plan.getattr("warnings")?), + ("warnings", native_field(&plan, "warnings")?), ], )?); } 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 8a51d31475..a7b8394942 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 @@ -6,6 +6,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::{ }; use crate::bindings::_helpers::planner_phases::objects::object; use crate::bindings::_helpers::planner_strategy::fixture_context::count_chain_resolution; +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::sql_test_glue::models::SqlTestPlanOptions; @@ -25,7 +26,7 @@ pub(super) fn chains<'py>( let request = call_with( ®istry::class(py, RecordId::NativeSqlTestChainRequest)?, &[ - ("models", project.getattr("models")?), + ("models", native_field(project, "models")?), ("tests", tests.clone()), ( "lexical_syntax", @@ -82,7 +83,7 @@ pub(crate) fn planned<'py>( let request = call_with( ®istry::class(py, RecordId::NativeSqlTestPlanningRequest)?, &[ - ("models", project.getattr("models")?), + ("models", native_field(project, "models")?), ("rendered_model_sql", model_sql.into_any()), ("functions", functions.into_any()), ("tests", tests.clone()), @@ -150,7 +151,7 @@ pub(crate) fn planned<'py>( for row in native.get_item(0)?.try_iter()? { let plan = super::plan_rows::plan(&row?)?; if artifact { - let sql = plan.getattr("sql")?; + let sql = native_field(&plan, "sql")?; if sql.is_none() { return Err(PyErr::from_value( registry::error(py, ErrorId::NativeSqlTestPlanning)? @@ -161,8 +162,8 @@ pub(crate) fn planned<'py>( ®istry::class(py, RecordId::NativeSqlTestArtifact)?, &[ ("sql", sql), - ("model_names", plan.getattr("model_names")?), - ("error_messages", plan.getattr("error_messages")?), + ("model_names", native_field(&plan, "model_names")?), + ("error_messages", native_field(&plan, "error_messages")?), ], )?); } else { @@ -241,7 +242,7 @@ pub(crate) fn sql_test_plan_error_messages<'py>( let mut seen: std::collections::HashSet = std::collections::HashSet::new(); for warning in warnings.try_iter()? { let warning = warning?; - let message: String = warning.getattr("message")?.extract()?; + let message: String = native_field(&warning, "message")?.extract()?; if warning .getattr("severity")? .getattr("value")? 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 7d60c531ae..e2c7fb12fd 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 @@ -11,6 +11,7 @@ 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; @@ -18,9 +19,12 @@ 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(&test.getattr("payload")?.getattr("authored_ctes")?, prefix)? - .keys() - .extract()? + prefixed_ctes( + &native_field(test, "payload")?.getattr("authored_ctes")?, + prefix, + )? + .keys() + .extract()? } else { Vec::new() }; @@ -37,7 +41,7 @@ fn function_deps<'py>( let py = test.py(); let deps = PyDict::new(py); if model_payload(test)? { - let names: Vec = plan.getattr("model_names")?.extract()?; + let names: Vec = native_field(plan, "model_names")?.extract()?; let mocked: std::collections::HashSet = inputs.0.extract::>()?.into_iter().collect(); let models = inputs.1; @@ -45,14 +49,16 @@ fn function_deps<'py>( let Some(model) = models.get_item(name)? else { continue; }; - for dep in model.getattr("deps")?.try_iter()? { + for dep in native_field(&model, "deps")?.try_iter()? { let dep = dep?; - let resource: String = dep.getattr("resource_type")?.getattr("value")?.extract()?; + let resource: String = native_field(&dep, "resource_type")? + .getattr("value")? + .extract()?; if resource != UDF && resource != TABLE_FN { continue; } if resource == TABLE_FN - && mocked.contains(&dep.getattr("name")?.extract::()?) + && mocked.contains(&native_field(&dep, "name")?.extract::()?) { continue; } @@ -60,23 +66,25 @@ fn function_deps<'py>( } } } else { - let payload = test.getattr("payload")?; - let mode: String = payload.getattr("mode")?.getattr("value")?.extract()?; + let payload = native_field(test, "payload")?; + let mode: String = native_field(&payload, "mode")? + .getattr("value")? + .extract()?; if mode == UDF || mode == TABLE_FN { let names: std::collections::HashSet = payload .getattr("tested_resource_names")? .extract::>()? .into_iter() .collect(); - for function in project.getattr("functions")?.try_iter()? { + for function in native_field(project, "functions")?.try_iter()? { let function = function?; - let key = function.getattr("key")?; + let key = native_field(&function, "key")?; if key .getattr("resource_type")? .getattr("value")? .extract::()? == mode - && names.contains(&function.getattr("name")?.extract::()?) + && names.contains(&native_field(&function, "name")?.extract::()?) { deps.set_item(key, py.None())?; } @@ -125,8 +133,8 @@ pub(super) fn entry<'py>( "function_deps", function_deps(test, plan, inputs.0, (&mocks, inputs.1))?, )?; - fields.set_item("chain", plan.getattr("chain")?)?; - fields.set_item("assertions", plan.getattr("assertions")?)?; + 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)) } @@ -142,14 +150,14 @@ pub(super) fn expected_diagnostics( }; let py = test.py(); let resource_types = imported(py, COMPILE_TYPES, "CompiledResourceType")?; - for step in plan.getattr("chain")?.try_iter()? { + for step in native_field(plan, "chain")?.try_iter()? { let step = step?; - let name = step.getattr("model_name")?; + let name = native_field(&step, "model_name")?; let model_name: String = name.extract()?; let available = context .authoritative_columns .get(&(MODEL_REFERENCE_KIND.to_owned(), model_name)); - let expected = step.getattr("expected_columns")?; + let expected = native_field(&step, "expected_columns")?; let Some(available) = available else { continue; }; @@ -165,12 +173,12 @@ pub(super) fn expected_diagnostics( }; let location = super::fixture_validation::fixture_location( test, - &resource_types.getattr("SQL_TEST")?, + &native_field(&resource_types, "SQL_TEST")?, &name.extract::()?, )?; result.push(format!( "SQL test '{}': {}: {}", - test.getattr("name")?.extract::()?, + native_field(test, "name")?.extract::()?, location, message )); 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 86881e4082..04b327563a 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::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::plan_values::string; @@ -15,10 +16,10 @@ fn cte_call_location<'py>( test: &Bound<'py, PyAny>, (cte, call): (&Bound<'py, PyAny>, &Bound<'py, PyAny>), ) -> PyResult> { - let test_file: Bound<'py, PyAny> = test.getattr("test_file")?; + let test_file: Bound<'py, PyAny> = native_field(test, "test_file")?; let call: Option = call.extract()?; let located: CteLocation = sql_test_cte_location( - &test_file.getattr("contents")?.extract::()?, + &native_field(&test_file, "contents")?.extract::()?, &test .getattr("test_block")? .getattr("sql_body")? @@ -27,7 +28,7 @@ fn cte_call_location<'py>( call.as_deref(), ); let arguments: Bound<'py, PyDict> = PyDict::new(py); - arguments.set_item("path", test_file.getattr("relative_path")?)?; + arguments.set_item("path", native_field(&test_file, "relative_path")?)?; arguments.set_item("line", located.line)?; arguments.set_item("column", located.column)?; arguments.set_item("end_line", located.end_line)?; @@ -46,7 +47,7 @@ pub(super) fn planning_error( let mut found = None; for test in tests.try_iter()? { let test = test?; - if test.getattr("name")?.eq(payload.get_item("testName")?)? + if native_field(&test, "name")?.eq(payload.get_item("testName")?)? && test .getattr("test_file")? .getattr("relative_path")? 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 4d68666e41..39a7915d13 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 @@ -5,6 +5,7 @@ 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::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; @@ -17,9 +18,9 @@ pub(super) fn validated_test<'py>( context: &RelationFixturePlanningContext, ) -> PyResult, Vec>> { let py = test.py(); - let payload = test.getattr("payload")?; - let ctes = payload.getattr("authored_ctes")?; - let expected = payload.getattr("expected_ctes")?; + let payload = native_field(test, "payload")?; + let ctes = native_field(&payload, "authored_ctes")?; + let expected = native_field(&payload, "expected_ctes")?; let completed = match validated_fixtures( test, ( @@ -41,11 +42,12 @@ pub(super) fn validated_test<'py>( let mut authored: Vec> = Vec::new(); for cte in ctes.try_iter()? { let cte = cte?; - let name: String = cte.getattr("name")?.extract()?; + let name: String = native_field(&cte, "name")?.extract()?; let mut replacement = cte.clone(); for (index, prefix) in ["__ref__", "__source__", "__seed__"].iter().enumerate() { if let Some(name) = name.strip_prefix(prefix) { - let sql = completed[index].call_method1("get", (name, cte.getattr("sql_body")?))?; + let sql = completed[index] + .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))?; @@ -57,12 +59,12 @@ pub(super) fn validated_test<'py>( let mut expected_ctes: Vec> = Vec::new(); for cte in expected.try_iter()? { let cte = cte?; - let name: String = cte.getattr("name")?.extract()?; + let name: String = native_field(&cte, "name")?.extract()?; let name = name.strip_prefix("__expected__").unwrap_or(&name); let kwargs = PyDict::new(py); kwargs.set_item( "sql_body", - completed[3].call_method1("get", (name, cte.getattr("sql_body")?))?, + completed[3].call_method1("get", (name, native_field(&cte, "sql_body")?))?, )?; expected_ctes.push(replace.call((&cte,), Some(&kwargs))?); } 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 ed52d26a49..2d264c9324 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 @@ -6,6 +6,7 @@ use crate::bindings::_helpers::sql_test_glue::plan_values::{model_payload, strin use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyErr, PyResult}; use pyo3::types::{PyDict, PyDictMethods, PyTuple}; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::records::ids::ErrorId; use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::sql_test_glue::constants::SENTINEL; @@ -25,7 +26,7 @@ pub(super) fn models<'py>( ) -> PyResult> { let py = project.py(); let result = PyDict::new(py); - for (index, model) in project.getattr("models")?.try_iter()?.enumerate() { + for (index, model) in native_field(project, "models")?.try_iter()?.enumerate() { let model = model?; let sql = model_reads::query_sql(&model)?; if mentions(&sql.extract::()?) { @@ -45,7 +46,7 @@ pub(super) fn overrides<'py>( ) -> PyResult> { let py = project.py(); let models = PyDict::new(py); - for model in project.getattr("models")?.try_iter()? { + for model in native_field(project, "models")?.try_iter()? { let model = model?; models.set_item(model_reads::name_object(&model)?, model)?; } @@ -142,13 +143,13 @@ pub(super) fn functions<'py>( ) -> PyResult> { let py = project.py(); let mut result: Vec> = Vec::new(); - for function in project.getattr("functions")?.try_iter()? { + for function in native_field(project, "functions")?.try_iter()? { let function = function?; - let target = function.getattr("destination")?.getattr("qualified_name")?; + let target = native_field(&function, "destination")?.getattr("qualified_name")?; if target.is_none() { continue; } - let mut fields = vec![function.getattr("name")?]; + let mut fields = vec![native_field(&function, "name")?]; for method in ["render_udf_call", "render_table_function_call"] { let rendered: String = call_with( &adapter.getattr(method)?, 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 970a81e3e3..06cca996b6 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,6 +4,7 @@ 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::RecordId; use crate::bindings::_helpers::records::registry; @@ -12,7 +13,7 @@ pub(super) fn string<'py>(py: Python<'py>, value: &str) -> Bound<'py, PyAny> { } pub(super) fn model_payload(test: &Bound<'_, PyAny>) -> PyResult { - test.getattr("payload")?.is_instance(®istry::class( + native_field(test, "payload")?.is_instance(®istry::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 35c35da692..39a41e690e 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 @@ -15,6 +15,7 @@ use sqlbuild_analysis::compiler::models::{ use crate::bindings::_helpers::analysis::model_reads; use crate::bindings::_helpers::boundary::panics::value_error; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::sqltext::lexical_syntax::LexicalSyntaxInput; use crate::bindings::types::CompilerDetach; @@ -178,13 +179,13 @@ fn plan_model( /// The names of the models among a compiled model's dependencies, in dependency order. fn model_dependencies(model: &Bound<'_, PyAny>) -> PyResult> { let mut names: Vec = Vec::new(); - for dependency in model.getattr("deps")?.try_iter()? { + for dependency in native_field(model, "deps")?.try_iter()? { let dependency = dependency?; if dependency .getattr("resource_type")? .eq(MODEL_RESOURCE_TYPE)? { - names.push(dependency.getattr("name")?.extract()?); + names.push(native_field(&dependency, "name")?.extract()?); } } Ok(names) @@ -212,12 +213,12 @@ fn plan_test( test: &Bound<'_, PyAny>, rendered_overrides: Option>, ) -> PyResult { - let payload = test.getattr("payload")?; + let payload = native_field(test, "payload")?; let payload: SqlTestPlanPayload = if payload.hasattr("actual_cte")? { SqlTestPlanPayload::Direct { - mode: payload.getattr("mode")?.extract()?, - actual_cte: cte(&payload.getattr("actual_cte")?)?, - expected_cte: cte(&payload.getattr("expected_cte")?)?, + mode: native_field(&payload, "mode")?.extract()?, + actual_cte: cte(&native_field(&payload, "actual_cte")?)?, + expected_cte: cte(&native_field(&payload, "expected_cte")?)?, helper_ctes: ctes(&payload, "helper_ctes")?, } } else { @@ -225,19 +226,19 @@ fn plan_test( authored_ctes: ctes(&payload, "authored_ctes")?, model_query_overrides: match rendered_overrides { Some(overrides) => overrides, - None => payload.getattr("model_query_overrides")?.extract()?, + None => native_field(&payload, "model_query_overrides")?.extract()?, }, expected_ctes: ctes(&payload, "expected_ctes")?, - expected_model_names: payload.getattr("expected_model_names")?.extract()?, + expected_model_names: native_field(&payload, "expected_model_names")?.extract()?, assertion_ctes: ctes(&payload, "assertion_ctes")?, - read_helper_names: Some(test.getattr("read_helper_names")?.extract()?), + read_helper_names: Some(native_field(test, "read_helper_names")?.extract()?), reference_target_model_names: Some( - test.getattr("reference_target_model_names")?.extract()?, + native_field(test, "reference_target_model_names")?.extract()?, ), } }; Ok(SqlTestPlanTest { - name: test.getattr("name")?.extract()?, + name: native_field(test, "name")?.extract()?, file_label: test .getattr("test_file")? .getattr("relative_path")? @@ -257,8 +258,8 @@ fn ctes(owner: &Bound<'_, PyAny>, attribute: &str) -> PyResult> fn cte(value: &Bound<'_, PyAny>) -> PyResult { Ok(SqlTestCte { - name: value.getattr("name")?.extract()?, - sql_body: value.getattr("sql_body")?.extract()?, + name: native_field(value, "name")?.extract()?, + sql_body: native_field(value, "sql_body")?.extract()?, }) } 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 4650dca5ee..744ebb6b0b 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/sqltext/sql_references.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/sqltext/sql_references.rs @@ -13,6 +13,7 @@ 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::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::sqltext::lexical_syntax::LexicalSyntaxInput; @@ -98,7 +99,7 @@ fn scanned<'py>( sql: &str, syntax: &Bound<'py, PyAny>, ) -> PyResult> { - let input: LexicalSyntaxInput = syntax.getattr("native_mapping")?.extract()?; + let input: LexicalSyntaxInput = native_field(syntax, "native_mapping")?.extract()?; let syntax: LexicalSyntax = input.into(); let scan = match compiler_guard(|| Ok(extract_sql_references(sql, &syntax)))? { ReferenceExtraction::Extracted(scan) => scan, @@ -191,14 +192,14 @@ pub(crate) fn extract_references<'py>( registry::class(py, RecordId::DiagnosticSeverity)?.getattr("ERROR")?, ), ("code", text(py, REFERENCE_CALL_SYNTAX_CODE)?), - ("message", call.getattr("message")?), - ("help", call.getattr("help")?), + ("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, "", call.getattr("call")?), + (REFERENCE_CALL_SYNTAX_CODE, "", native_field(&call, "call")?), )?; keywords.set_item("diagnostic", diagnostic)?; report.call((), Some(&keywords))?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/state_rows/events.rs b/crates/sqlbuild-python/src/bindings/_helpers/state_rows/events.rs index e79ff7e3d4..465a195eb1 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/state_rows/events.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/state_rows/events.rs @@ -18,6 +18,7 @@ use sqlbuild_state::events::models::{EventRelation, StateCell, StateColumnType, use crate::bindings::_helpers::boundary::keyword_request::Request; 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::state_rows::timestamps::{date_time_typed, presumed_utc}; fn emit_error(error: JsonEmitError) -> PyErr { @@ -26,9 +27,9 @@ fn emit_error(error: JsonEmitError) -> PyErr { pub(crate) fn relation(value: &Bound<'_, PyAny>) -> PyResult { Ok(EventRelation { - database: value.getattr("database")?.extract()?, - schema: value.getattr("schema")?.extract()?, - name: value.getattr("name")?.extract()?, + database: native_field(value, "database")?.extract()?, + schema: native_field(value, "schema")?.extract()?, + name: native_field(value, "name")?.extract()?, }) } @@ -70,7 +71,7 @@ fn column_id(request: Option<&Bound<'_, PyDict>>) -> PyResult { #[pyfunction(name = "deterministic_old_name_view_event_id")] #[pyo3(signature = (*, event_type, migration_event_id))] fn old_name_id(event_type: &Bound<'_, PyAny>, migration_event_id: &str) -> PyResult { - let event_type: String = event_type.getattr("value")?.extract()?; + let event_type: String = native_field(event_type, "value")?.extract()?; old_name_view_event_id(&event_type, migration_event_id).map_err(emit_error) } 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 df4e44520c..372caafccd 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{NativeAttr, RecordId}; use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::state_rows::timestamps::{ @@ -84,7 +85,7 @@ pub(crate) fn normalize<'py>( "Timestamp source freshness values must be datetime values", )); } - let aware: bool = !value.getattr("tzinfo")?.is_none(); + let aware: bool = !native_field(value, "tzinfo")?.is_none(); let local: LocalDateTime = if aware { local_date_time(&in_utc(value)?)? } else { @@ -160,9 +161,9 @@ impl TimestampParser for StateTimestamps<'_> { fn comparable(record: &Bound<'_, PyAny>) -> PyResult { Ok(ComparableRecord { - data_version_hash: record.getattr("data_version_hash")?.extract()?, - value_kind: record.getattr("value_kind")?.extract()?, - data_version: record.getattr("data_version")?.extract()?, + data_version_hash: native_field(record, "data_version_hash")?.extract()?, + value_kind: native_field(record, "value_kind")?.extract()?, + data_version: native_field(record, "data_version")?.extract()?, }) } @@ -214,12 +215,12 @@ pub(crate) fn age_policy<'py>( } let statuses: Bound<'py, PyAny> = registry::class(py, RecordId::SourceFreshnessAgeStatus)?; if !date_time_typed(data_version)? { - return statuses.getattr("UNKNOWN").map(Some); + return native_field(&statuses, "UNKNOWN").map(Some); } let version: LocalDateTime = aware_utc(data_version, "data_version")?; let observed: LocalDateTime = aware_utc(observed_at, "observed_at")?; - let error_after: Option = policy.getattr("error_after")?.extract()?; - let warn_after: Option = policy.getattr("warn_after")?.extract()?; + let error_after: Option = native_field(policy, "error_after")?.extract()?; + let warn_after: Option = native_field(policy, "warn_after")?.extract()?; let status: AgeStatus = age_status( (&version, &observed), error_after.as_deref(), 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 8cfb088388..1efd78978c 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 @@ -12,6 +12,7 @@ use sqlbuild_state::events::models::RecordedRenameKey; use crate::bindings::_helpers::boundary::keyword_request::Request; use crate::bindings::_helpers::compile_driver::python_calls::call_with; +use crate::bindings::_helpers::records::fields::field as native_field; use crate::bindings::_helpers::state_rows::constants::{ COLUMN_KIND, LOGGING_MODULE, MIGRATION_LOGGER, MIGRATION_WRITE_ATTEMPTS, MIGRATION_WRITE_RETRY_BASE_SECONDS, MIGRATION_WRITE_RETRY_MESSAGE, MODEL_KIND, OLD_NAME_KIND, @@ -86,7 +87,7 @@ pub(crate) fn stored_old_name_columns<'py>( ) -> PyResult> { let py: Python<'py> = adapter.py(); let columns: Bound<'py, PyAny> = call_with( - &adapter.getattr("get_columns")?, + &native_field(adapter, "get_columns")?, &[ ("connection", connection.clone()), ("database", database), @@ -99,23 +100,23 @@ pub(crate) fn stored_old_name_columns<'py>( )?; columns .try_iter()? - .map(|column| column?.getattr("name")?.extract()) + .map(|column| native_field(&column?, "name")?.extract()) .collect() } fn subject(table: EventTable, event: &Bound<'_, PyAny>) -> PyResult { let text = |value: Bound<'_, PyAny>| -> PyResult { Ok(value.str()?.to_string()) }; Ok(match table { - EventTable::Model => text(event.getattr("destination_model")?)?, + EventTable::Model => text(native_field(event, "destination_model")?)?, EventTable::Column => format!( "{}.{}", - text(event.getattr("model_name")?)?, - text(event.getattr("destination_column")?)? + text(native_field(event, "model_name")?)?, + text(native_field(event, "destination_column")?)? ), EventTable::OldName => format!( "{} old name {}", - text(event.getattr("destination_model")?)?, - text(event.getattr("old")?.getattr("name")?)? + text(native_field(event, "destination_model")?)?, + text(native_field(event, "old")?.getattr("name")?)? ), }) } @@ -249,7 +250,7 @@ fn read_renames<'py>(request: Option<&Bound<'py, PyDict>>) -> PyResult = request.py(); let relation: Bound<'py, PyAny> = request.get("relation")?; let since = presumed_utc(&request.get("since")?)?; - let schema: Bound<'py, PyAny> = relation.getattr("schema")?; + let schema: Bound<'py, PyAny> = native_field(&relation, "schema")?; let schema: Bound<'py, PyAny> = if schema.is_none() { PyString::new(py, "").into_any() } else { @@ -258,7 +259,7 @@ fn read_renames<'py>(request: Option<&Bound<'py, PyDict>>) -> PyResult = read_events( &access(&request)?, &layout(COLUMN_KIND)?, - (relation.getattr("database")?, schema), + (native_field(&relation, "database")?, schema), None, )?; let keys: Vec = events @@ -266,7 +267,7 @@ fn read_renames<'py>(request: Option<&Bound<'py, PyDict>>) -> PyResult( /// The history order of a stored event: its creation time in UTC, then its ID. pub(crate) fn event_order(event: &Bound<'_, PyAny>) -> PyResult { Ok(EventOrder { - created_at: presumed_utc(&event.getattr("created_at")?)?, - event_id: event.getattr("event_id")?.extract()?, + created_at: presumed_utc(&native_field(event, "created_at")?)?, + event_id: native_field(event, "event_id")?.extract()?, }) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/state_rows/migration_projections.rs b/crates/sqlbuild-python/src/bindings/_helpers/state_rows/migration_projections.rs index 0da6a9176b..d62f930987 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/state_rows/migration_projections.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/state_rows/migration_projections.rs @@ -12,6 +12,7 @@ use sqlbuild_state::events::models::{ }; 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::RecordId; use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::state_rows::constants::OLD_NAME_VIEW_CONFIG_KEY; @@ -43,8 +44,8 @@ pub(crate) fn newest_move<'py>( .map(|event| { Ok(MoveEventKey { order: event_order(event)?, - origin: event_relation(&event.getattr("origin")?)?, - destination: event_relation(&event.getattr("destination")?)?, + origin: event_relation(&native_field(event, "origin")?)?, + destination: event_relation(&native_field(event, "destination")?)?, }) }) .collect::>()?; @@ -64,9 +65,9 @@ pub(crate) fn newest_column<'py>( .map(|event| { Ok(ColumnEventKey { order: event_order(event)?, - relation: event_relation(&event.getattr("relation")?)?, - origin_column: event.getattr("origin_column")?.extract()?, - destination_column: event.getattr("destination_column")?.extract()?, + relation: event_relation(&native_field(event, "relation")?)?, + origin_column: native_field(event, "origin_column")?.extract()?, + destination_column: native_field(event, "destination_column")?.extract()?, }) }) .collect::>()?; @@ -77,7 +78,9 @@ pub(crate) fn newest_column<'py>( } fn step(fact: &Bound<'_, PyAny>) -> PyResult { - let value: String = fact.getattr("event_type")?.getattr("value")?.extract()?; + let value: String = native_field(fact, "event_type")? + .getattr("value")? + .extract()?; match value.as_str() { "required" => Ok(OldNameStep::Required), "origin_archived" => Ok(OldNameStep::OriginArchived), @@ -103,7 +106,7 @@ pub(crate) fn old_name_views<'py>( .map(|fact| { Ok(OldNameFactKey { order: event_order(fact)?, - migration_event_id: fact.getattr("migration_event_id")?.extract()?, + migration_event_id: native_field(fact, "migration_event_id")?.extract()?, step: step(fact)?, }) }) 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 c0af2ecbc0..7576ecfba1 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 @@ -18,6 +18,7 @@ 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::cursor::values::DATETIME_MODULE; +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; @@ -105,9 +106,9 @@ fn row_values<'py>( let relation = |name: &str| -> PyResult<[Bound<'py, PyAny>; 3]> { let relation: Bound<'py, PyAny> = event.getattr(name)?; Ok([ - relation.getattr("database")?, - relation.getattr("schema")?, - relation.getattr("name")?, + native_field(&relation, "database")?, + native_field(&relation, "schema")?, + native_field(&relation, "name")?, ]) }; let mut values: Vec> = vec![field("event_id")?, field("target_name")?]; @@ -168,16 +169,16 @@ pub(crate) fn write_statements<'py>( ) -> PyResult<(String, String)> { let py: Python<'py> = event.py(); let relation: Bound<'py, PyAny> = event.getattr(layout.relation_attribute)?; - let schema: Bound<'py, PyAny> = relation.getattr("schema")?; + let schema: Bound<'py, PyAny> = native_field(&relation, "schema")?; if schema.is_none() { return Err(state_error(py, layout.event_message)); } let table: String = qualified_table( layout, render_qualified_name, - (relation.getattr("database")?, schema), + (native_field(&relation, "database")?, schema), )?; - let event_id: String = event.getattr("event_id")?.extract()?; + let event_id: String = native_field(event, "event_id")?.extract()?; let values: Vec> = row_values(layout.table, event)?; let insert: String = typed_insert_sql(py, &table, (layout.columns, &values))?; Ok((event_exists_sql(&table, &event_id), insert)) @@ -243,7 +244,7 @@ fn timestamp<'py>(raw: &Bound<'py, PyAny>) -> PyResult> { } else { imported(py, DATETIME_MODULE, "datetime")?.call_method1("fromisoformat", (raw.str()?,))? }; - if !parsed.getattr("tzinfo")?.is_none() { + if !native_field(&parsed, "tzinfo")?.is_none() { return Ok(parsed); } let utc: Bound<'py, PyDict> = PyDict::new(py); diff --git a/crates/sqlbuild-python/src/bindings/_helpers/state_rows/timestamps.rs b/crates/sqlbuild-python/src/bindings/_helpers/state_rows/timestamps.rs index b679778b32..49f5016608 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/state_rows/timestamps.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/state_rows/timestamps.rs @@ -5,6 +5,7 @@ use sqlbuild_cursor::algebra::models::LocalDateTime; 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::fields::field as native_field; /// Whether `value` is a `datetime`. pub(crate) fn date_time_typed(value: &Bound<'_, PyAny>) -> PyResult { @@ -13,7 +14,7 @@ pub(crate) fn date_time_typed(value: &Bound<'_, PyAny>) -> PyResult { /// Whether `value` has a `tzinfo` that reports an offset. pub(crate) fn offset_aware(value: &Bound<'_, PyAny>) -> PyResult { - Ok(!value.getattr("tzinfo")?.is_none() && !value.call_method0("utcoffset")?.is_none()) + Ok(!native_field(value, "tzinfo")?.is_none() && !value.call_method0("utcoffset")?.is_none()) } /// `value.astimezone(UTC)`. diff --git a/crates/sqlbuild-python/src/bindings/_helpers/type_system/normalization.rs b/crates/sqlbuild-python/src/bindings/_helpers/type_system/normalization.rs index 3f37bbd680..6007f4e19d 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/type_system/normalization.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/type_system/normalization.rs @@ -8,6 +8,7 @@ use sqlbuild_analysis::type_system::main::normalize_type::normalize_type; use sqlbuild_analysis::type_system::models::{NormalizedType, TypeNormalization}; use crate::bindings::_helpers::boundary::panics::compiler_guard; +use crate::bindings::_helpers::records::fields::field as native_field; /// `(name, family, precision, scale, length)` of one normalized type. type NormalizedRow = ( @@ -43,7 +44,7 @@ pub(crate) fn normalization_error(python: Python<'_>, error: &TypeNormalizationE TypeNormalizationError::UnknownDialect(_) => PyValueError::new_err(error.message()), TypeNormalizationError::Generation(_) => python .import("polyglot_sql") - .and_then(|module| module.getattr("PolyglotError")) + .and_then(|module| native_field(&module, "PolyglotError")) .and_then(|class| class.call1((error.message(),))) .map_or_else(|import_error| import_error, PyErr::from_value), } 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 c40910c9e4..a3b11eb88b 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::fields::field as native_field; use crate::bindings::_helpers::records::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; @@ -176,20 +177,20 @@ 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 { - let project_dir: Bound<'_, PyAny> = request.getattr("project_dir")?; + let project_dir: Bound<'_, PyAny> = native_field(request, "project_dir")?; let base: PathBuf = if project_dir.is_none() { current_dir(py)? } else { project_dir.extract()? }; - let display_path: String = request.getattr("target_path")?.str()?.extract()?; + let display_path: String = native_field(request, "target_path")?.str()?.extract()?; let target_path: PathBuf = PathBuf::from(&display_path); let target_dir: PathBuf = if target_path.is_absolute() { target_path } else { base.join(target_path) }; - let template: String = request.getattr("template")?.str()?.extract()?; + let template: String = native_field(request, "template")?.str()?.extract()?; create_playground(&PlaygroundRequest { templates_root: package_dir(py)?.join(TEMPLATES_DIRECTORY), target_dir: target_dir.clone(),