From e3db1555e9467acadafe1b02c4c90ac4307793d7 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Fri, 16 Oct 2026 20:00:49 +0100 Subject: [PATCH 1/5] refactor(rules): read SQL rule model inputs from native facts SQL rules read each compiled model's relative path, sql_analysis opt-out, authored and expanded SQL and dynamic pivot proof from the native facts and analysis instead of Python attributes. The proof's cache-key text is encoded natively as the same sorted compact JSON json.dumps produced, so SQL rule cache keys are unchanged; hand-built models keep reading their record fields. --- .../main/contract_proof_json.rs | 64 +++++++++ .../src/assembly/analysis_session/main/mod.rs | 1 + .../src/assembly/analysis_session/tests.rs | 2 + .../tests/test_contract_proof_json.rs | 53 ++++++++ .../analysis_session/tests/test_types.rs | 8 +- .../compiled_project/compiled_model_view.rs | 52 ++++++++ .../src/bindings/_helpers/rules/sql_rules.rs | 123 +++++++++++++----- 7 files changed, 268 insertions(+), 35 deletions(-) create mode 100644 crates/sqlbuild-analysis/src/assembly/analysis_session/main/contract_proof_json.rs create mode 100644 crates/sqlbuild-analysis/src/assembly/analysis_session/tests/test_contract_proof_json.rs 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/compiled_project/compiled_model_view.rs b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/compiled_model_view.rs index c4e94a2f27..76ba00de5b 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::{ @@ -30,9 +31,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 +386,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)?; 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..fbecc18793 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/sql_rules.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/sql_rules.rs @@ -19,6 +19,9 @@ 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::ids::{ErrorId, RecordId}; use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::rules::constants::SAFE_RULE_FIX_CODES; @@ -186,9 +189,19 @@ 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(); +fn sql_rule_models<'py>( + project: &Bound<'py, PyAny>, + keyed: bool, +) -> PyResult>> { + let mut models: Vec> = Vec::new(); if !project .getattr("settings")? .getattr("sql_analysis")? @@ -196,42 +209,87 @@ fn sql_rule_models<'py>(project: &Bound<'py, PyAny>) -> PyResult = 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) { + 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: model.getattr("authored_sql")?.extract()?, + query_sql: model.getattr("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,7 +301,7 @@ 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")?; @@ -289,7 +347,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)? @@ -316,8 +374,7 @@ fn evaluated_sql_rules( 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::()?)); + 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 +428,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(); From 63daead295ce43b2ef9e129ce126f5200bcc0953 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Fri, 16 Oct 2026 20:36:14 +0100 Subject: [PATCH 2/5] refactor(rules): read SQL test and fixture schema inputs in place Native records now keep their class layout, so records::fields::field reads a declared field straight from the record's values without the Python attribute protocol, falling back to getattr for other objects. SQL test and scenario facts and the fixture-format schemas read their inputs this way, and discovered model files' paths, header values and column locations through the native model file record. --- .../src/bindings/_helpers/records/fields.rs | 17 ++++ .../src/bindings/_helpers/records/models.rs | 3 + .../_helpers/rules/fixture_schemas.rs | 86 ++++++++++++------ .../_helpers/rules/sql_test_objects.rs | 90 +++++++++---------- 4 files changed, 124 insertions(+), 72 deletions(-) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/records/fields.rs b/crates/sqlbuild-python/src/bindings/_helpers/records/fields.rs index 54b76127e7..ffc66850df 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/records/fields.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/records/fields.rs @@ -36,6 +36,23 @@ 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) +} + /// Field values bound from `args` and `kwargs` as a dataclass `__init__` binds them. pub(crate) fn build_values( spec: &RecordSpec, 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/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/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")?, From 4fdc1e5a2ba089eebbd614816724330555abc969 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Fri, 16 Oct 2026 21:40:45 +0100 Subject: [PATCH 3/5] refactor(rules): run rule planning and dispatch on native record reads Rule planning, dispatch, catalogue selection, finalization, results and format --fix now read native record fields in place, rebuild records with records::fields::replaced instead of dataclasses.replace, and call the rule helpers (plan, SQL rules, finish, finalize, selection, catalogue, compiled project builds) directly instead of through module attributes. Rules configurations and custom payloads encode natively as the JSON the selection and request paths parsed, falling back to Python's encoder for values only it spells (non-finite or wide numbers, non-text keys). Duplicate-literal hints read finding files natively, and project views answer settings, scenarios and scope index reads from stored fields. --- .../_helpers/compiled_project/audit_views.rs | 2 +- .../_helpers/compiled_project/project_view.rs | 5 +- .../_helpers/compiled_project/records.rs | 3 + .../_helpers/discovery/model_file_record.rs | 7 +- .../src/bindings/_helpers/records/fields.rs | 34 +++- .../src/bindings/_helpers/rules/catalogue.rs | 116 ++++++----- .../bindings/_helpers/rules/config_json.rs | 85 +++++++++ .../bindings/_helpers/rules/fact_replay.rs | 17 +- .../bindings/_helpers/rules/fix_session.rs | 180 +++++++++++------- .../src/bindings/_helpers/rules/mod.rs | 1 + .../src/bindings/_helpers/rules/results.rs | 66 ++++--- .../src/bindings/_helpers/rules/rows.rs | 54 +++--- .../bindings/_helpers/rules/rule_dispatch.rs | 167 +++++++--------- .../src/bindings/_helpers/rules/rule_plan.rs | 90 ++++----- .../src/bindings/_helpers/rules/rule_run.rs | 26 +-- .../src/bindings/_helpers/rules/selection.rs | 23 ++- .../src/bindings/_helpers/rules/sql_rules.rs | 4 +- 17 files changed, 540 insertions(+), 340 deletions(-) create mode 100644 crates/sqlbuild-python/src/bindings/_helpers/rules/config_json.rs 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/project_view.rs b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/project_view.rs index 61c1012b24..3648176dd5 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,12 @@ 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, "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..361239937b 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/records.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/records.rs @@ -463,6 +463,9 @@ fn hand_built_project<'py>( pub(crate) fn compiled_project_facts<'py>( project: &Bound<'py, PyAny>, ) -> PyResult> { + if let Ok(view) = project.downcast::() { + return Ok(view.get().owner().bind(project.py()).clone()); + } let retained: Bound<'py, PyAny> = project.getattr("native_project")?; if !retained.is_none() { return Ok(retained.downcast_into()?); 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/records/fields.rs b/crates/sqlbuild-python/src/bindings/_helpers/records/fields.rs index ffc66850df..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::{ @@ -53,6 +55,28 @@ pub(crate) fn field<'py>(value: &Bound<'py, PyAny>, name: &str) -> PyResult( + 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, @@ -234,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/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/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/fix_session.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/fix_session.rs index e8e1ecdba5..cd86b424de 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,24 @@ 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, 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 +117,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 +210,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 +232,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 +240,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 +264,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 +322,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 +374,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 +420,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 +477,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 +495,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(_) => model.getattr("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(_) => model.getattr("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 = model.getattr("contents")?; + 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")?; + 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 +558,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 +589,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 +616,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 +659,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 +669,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 +742,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/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/results.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/results.rs index 9e5c883e7c..b35b795028 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/results.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/results.rs @@ -2,33 +2,33 @@ 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, 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 +47,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 +66,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,6 +90,16 @@ 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")?, @@ -117,15 +127,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_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..388b1f8318 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,7 @@ 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::ids::RecordId; use crate::bindings::_helpers::records::registry; use crate::bindings::_helpers::rules::fact_replay::project_models; @@ -22,7 +23,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 +52,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 +85,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 +114,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")?, ), ], ) 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 fbecc18793..8a116bc572 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/sql_rules.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/sql_rules.rs @@ -95,7 +95,9 @@ 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")?; From 0a0409c1af5d3b805a4d77ff8234a717be6ec654 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Fri, 16 Oct 2026 21:51:23 +0100 Subject: [PATCH 4/5] refactor(rules): read lint, guidance and SQL rule inputs from native records SQL rule runs, lint settings and compiled expansions, rule guidance, early SQL lint planning and fix-verification facts read native record fields in place, and project views answer diagnostics reads from their stored fields. --- .../_helpers/compiled_project/project_view.rs | 1 + .../bindings/_helpers/rules/early_sql_lint.rs | 30 ++++++------- .../src/bindings/_helpers/rules/fix_facts.rs | 22 ++++----- .../src/bindings/_helpers/rules/guidance.rs | 26 +++++------ .../bindings/_helpers/rules/lint_pipeline.rs | 38 ++++++++-------- .../src/bindings/_helpers/rules/sql_rules.rs | 45 +++++++++---------- 6 files changed, 82 insertions(+), 80 deletions(-) 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 3648176dd5..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 @@ -256,6 +256,7 @@ impl NativeProjectView { "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/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/fix_facts.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/fix_facts.rs index 519b5f4f15..4ac43f0b09 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,10 @@ 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::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] @@ -31,7 +33,7 @@ pub(crate) fn compile_facts<'py>( } 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 +46,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 +70,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/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_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/sql_rules.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/sql_rules.rs index 8a116bc572..723f68adf7 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; @@ -22,12 +22,14 @@ use crate::bindings::_helpers::compile_driver::python_calls::{call_with, importe use crate::bindings::_helpers::compiled_project::compiled_model_view::{ NativeCompiledModel, SqlRuleInputs, SqlRuleKey, }; +use crate::bindings::_helpers::records::fields::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, }; @@ -55,7 +57,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); } @@ -102,38 +104,39 @@ pub(crate) fn run_planned_sql_rules<'py>( 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 { @@ -204,14 +207,10 @@ fn sql_rule_models<'py>( keyed: bool, ) -> PyResult>> { let mut models: Vec> = Vec::new(); - if !project - .getattr("settings")? - .getattr("sql_analysis")? - .is_truthy()? - { + if !field(&project_attribute(project, "settings")?, "sql_analysis")?.is_truthy()? { return Ok(models); } - for model in project.getattr("models")?.try_iter()? { + for model in project_attribute(project, "models")?.try_iter()? { let Some(entry) = sql_rule_model(model?, keyed)? else { continue; }; @@ -375,7 +374,7 @@ 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()? { + 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) { From 1132eae7a0ea32300b8bf98cad67219f2a45bd4c Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sat, 17 Oct 2026 00:15:55 +0100 Subject: [PATCH 5/5] refactor: read native record fields directly instead of through the Python attribute protocol --- .../_helpers/analysis/binding_catalog.rs | 17 +- .../_helpers/analysis/explicit_references.rs | 35 ++- .../bindings/_helpers/analysis/model_reads.rs | 59 ++-- .../_helpers/analysis/reference_facts.rs | 81 ++--- .../_helpers/analysis/required_columns.rs | 5 +- .../_helpers/analysis/sql_test_comparisons.rs | 29 +- .../analysis_session/fixture_columns.rs | 3 +- .../analysis_session/fixture_inferences.rs | 3 +- .../analysis_session/model_analyses.rs | 25 +- .../analysis_session/nullability_rules.rs | 4 +- .../analysis_session/profile_inputs.rs | 7 +- .../analysis_session/stage_functions.rs | 51 +-- .../_helpers/audit_command/command.rs | 115 ++++--- .../bindings/_helpers/auditing/identity.rs | 66 ++-- .../src/bindings/_helpers/auditing/render.rs | 3 +- .../_helpers/authored_values/python_nodes.rs | 7 +- .../_helpers/boundary/config_values.rs | 5 +- .../_helpers/build_command/command.rs | 11 +- .../_helpers/build_command/completion.rs | 46 +-- .../_helpers/build_command/defer_clone.rs | 141 +++++---- .../_helpers/build_command/execution.rs | 137 +++++---- .../_helpers/build_command/invocation.rs | 62 ++-- .../_helpers/build_command/lifecycle.rs | 107 ++++--- .../_helpers/build_command/planning.rs | 33 +- .../_helpers/build_command/policies.rs | 15 +- .../_helpers/build_progress/audit_progress.rs | 3 +- .../build_progress/build_callbacks.rs | 29 +- .../build_progress/nested_progress.rs | 3 +- .../_helpers/build_progress/plan_facts.rs | 20 +- .../_helpers/build_progress/result_text.rs | 34 +- .../_helpers/build_progress/terminal.rs | 5 +- .../build_progress/transient_status.rs | 5 +- .../bindings/_helpers/check/check_command.rs | 123 ++++---- .../src/bindings/_helpers/check/check_core.rs | 114 +++---- .../bindings/_helpers/check/check_results.rs | 27 +- .../src/bindings/_helpers/cli/argv.rs | 28 +- .../src/bindings/_helpers/cli/dispatch.rs | 17 +- .../src/bindings/_helpers/cli/entry.rs | 32 +- .../bindings/_helpers/cli/observability.rs | 72 +++-- .../src/bindings/_helpers/cli/progress.rs | 13 +- .../bindings/_helpers/cli/progress_text.rs | 41 +-- .../bindings/_helpers/clone/clone_command.rs | 58 ++-- .../_helpers/clone/clone_execution.rs | 65 ++-- .../bindings/_helpers/clone/clone_pipeline.rs | 22 +- .../bindings/_helpers/clone/clone_progress.rs | 13 +- .../_helpers/command_runtime/adapters.rs | 5 +- .../_helpers/command_runtime/connection.rs | 26 +- .../_helpers/command_runtime/invocation.rs | 35 ++- .../command_runtime/preview_target.rs | 9 +- .../command_runtime/runtime_errors.rs | 3 +- .../_helpers/command_runtime/warehouses.rs | 23 +- .../compile_attachment/cursor_roles.rs | 3 +- .../compile_attachment/declaration_scope.rs | 32 +- .../compile_attachment/effective_config.rs | 29 +- .../compile_attachment/model_config.rs | 34 +- .../compile_attachment/model_inputs.rs | 56 ++-- .../sql_expansion_context.rs | 31 +- .../compile_attachment/storage_policies.rs | 13 +- .../_helpers/compile_attachment/target.rs | 7 +- .../compile_command/prepared_artifacts.rs | 31 +- .../_helpers/compile_command/report.rs | 259 +++++++++------- .../_helpers/compile_command/reuse_attempt.rs | 31 +- .../compile_command/reuse_recording.rs | 39 ++- .../compile_command/runtime_identity.rs | 15 +- .../_helpers/compile_command/target_writer.rs | 98 +++--- .../compile_outputs/runtime_target.rs | 52 ++-- .../_helpers/compiled_project/attachment.rs | 33 +- .../_helpers/compiled_project/audit_loop.rs | 38 +-- .../compiled_model_analysis.rs | 5 +- .../compiled_project/compiled_model_view.rs | 3 +- .../compiled_project/expanded_offsets.rs | 9 +- .../_helpers/compiled_project/loop_classes.rs | 62 ++-- .../compiled_project/model_input_view.rs | 23 +- .../_helpers/compiled_project/model_loop.rs | 7 +- .../compiled_project/model_loop_macros.rs | 3 +- .../compiled_project/model_loop_visibility.rs | 5 +- .../_helpers/compiled_project/project.rs | 5 +- .../_helpers/compiled_project/records.rs | 82 ++--- .../compiled_project/resource_diagnostics.rs | 39 ++- .../_helpers/compiled_project/source_loop.rs | 40 +-- .../_helpers/compiled_project/source_views.rs | 3 +- .../compiled_project/sql_test_loop.rs | 19 +- .../_helpers/compute_logs/conversion.rs | 31 +- .../_helpers/contract_command/command.rs | 83 ++--- .../_helpers/contract_command/write.rs | 27 +- .../contracts/contract_diagnostics.rs | 15 +- .../_helpers/contracts/effective_config.rs | 35 ++- .../_helpers/contracts/spec_resolution.rs | 21 +- .../bindings/_helpers/contracts/sql_values.rs | 11 +- .../_helpers/contracts/state_literals.rs | 11 +- .../bindings/_helpers/cost_command/bounds.rs | 5 +- .../bindings/_helpers/cost_command/command.rs | 25 +- .../bindings/_helpers/cost_command/refresh.rs | 44 +-- .../bindings/_helpers/cost_command/sorting.rs | 14 +- .../_helpers/cost_report/degradation.rs | 48 +-- .../bindings/_helpers/cost_report/records.rs | 13 +- .../bindings/_helpers/cost_report/store.rs | 3 +- .../_helpers/cost_telemetry/allocation.rs | 15 +- .../_helpers/cost_telemetry/context.rs | 9 +- .../_helpers/cost_telemetry/ledger.rs | 20 +- .../_helpers/cost_telemetry/snowflake.rs | 7 +- .../bindings/_helpers/cursor/conversion.rs | 15 +- .../src/bindings/_helpers/cursor/durations.rs | 5 +- .../src/bindings/_helpers/cursor/intervals.rs | 9 +- .../src/bindings/_helpers/cursor/overrides.rs | 9 +- .../src/bindings/_helpers/cursor/scalars.rs | 5 +- .../src/bindings/_helpers/cursor/values.rs | 23 +- .../_helpers/dag_artifact/artifact.rs | 5 +- .../bindings/_helpers/dag_artifact/rows.rs | 5 +- .../_helpers/dag_artifact/selection.rs | 5 +- .../_helpers/dagster_invocation/arguments.rs | 3 +- .../_helpers/dagster_invocation/failures.rs | 18 +- .../_helpers/dagster_invocation/results.rs | 20 +- .../_helpers/dbt_command/auto_init.rs | 9 +- .../bindings/_helpers/dbt_command/command.rs | 13 +- .../src/bindings/_helpers/dbt_command/init.rs | 27 +- .../_helpers/dbt_command/init_project.rs | 44 +-- .../src/bindings/_helpers/dbt_interop/cli.rs | 9 +- .../_helpers/dbt_interop/event_stream.rs | 13 +- .../_helpers/dbt_interop/execution.rs | 20 +- .../bindings/_helpers/dbt_interop/graph.rs | 29 +- .../bindings/_helpers/dbt_interop/manifest.rs | 30 +- .../_helpers/dbt_interop/orchestration.rs | 43 +-- .../bindings/_helpers/dbt_interop/pipeline.rs | 72 +++-- .../_helpers/dbt_interop/pipeline_phases.rs | 75 +++-- .../_helpers/dbt_interop/pipeline_prologue.rs | 55 ++-- .../src/bindings/_helpers/dbt_interop/plan.rs | 53 ++-- .../_helpers/dbt_interop/plan_output.rs | 61 ++-- .../bindings/_helpers/dbt_interop/profile.rs | 3 +- .../dbt_interop/profile_connection.rs | 11 +- .../dbt_interop/reference_resolver.rs | 5 +- .../bindings/_helpers/dbt_interop/runner.rs | 12 +- .../_helpers/dbt_interop/runtime_config.rs | 19 +- .../_helpers/dbt_interop/runtime_execution.rs | 17 +- .../_helpers/dbt_interop/runtime_results.rs | 9 +- .../_helpers/dbt_interop/sqlbuild_work.rs | 68 ++-- .../_helpers/debug_command/run_debug.rs | 24 +- .../_helpers/diagnostics/console_records.rs | 5 +- .../_helpers/diagnostics/log_records.rs | 13 +- .../bindings/_helpers/diff_report/command.rs | 73 +++-- .../bindings/_helpers/diff_report/facts.rs | 93 +++--- .../bindings/_helpers/diff_report/query.rs | 128 ++++---- .../_helpers/diff_report/registration.rs | 23 +- .../bindings/_helpers/diff_report/support.rs | 7 +- .../_helpers/diff_report/tolerances.rs | 11 +- .../bindings/_helpers/discovery/guidance.rs | 5 +- .../_helpers/discovery/hook_entries.rs | 5 +- .../_helpers/discovery/imported_modules.rs | 5 +- .../_helpers/discovery/imported_providers.rs | 23 +- .../discovery/imported_python_nodes.rs | 21 +- .../bindings/_helpers/discovery/locations.rs | 5 +- .../_helpers/discovery/project_facts.rs | 23 +- .../_helpers/discovery/project_validation.rs | 5 +- .../discovery/python_function_files.rs | 21 +- .../src/bindings/_helpers/discovery/raised.rs | 5 +- .../_helpers/discovery/scope_snapshot.rs | 7 +- .../_helpers/discovery/sql_test_records.rs | 7 +- .../_helpers/event_exporting/dispatcher.rs | 23 +- .../_helpers/event_exporting/scope.rs | 33 +- .../_helpers/execution_history/conversion.rs | 71 ++--- .../_helpers/execution_history/functions.rs | 13 +- .../_helpers/execution_history/postgres.rs | 21 +- .../_helpers/execution_history/sqlite.rs | 29 +- .../exported_constants/registration.rs | 3 +- .../_helpers/fingerprints/latest_rows.rs | 9 +- .../_helpers/fingerprints/storage_io.rs | 13 +- .../_helpers/fingerprints/storage_sql.rs | 15 +- .../_helpers/format_command/command.rs | 12 +- .../_helpers/format_command/report.rs | 31 +- .../bindings/_helpers/format_command/run.rs | 14 +- .../_helpers/format_command/selection.rs | 27 +- .../_helpers/freshness/freshness_command.rs | 70 +++-- .../_helpers/freshness/freshness_observe.rs | 49 +-- .../_helpers/freshness/observation.rs | 103 ++++--- .../bindings/_helpers/freshness/planning.rs | 46 +-- .../_helpers/freshness/publication.rs | 41 +-- .../bindings/_helpers/freshness/state_io.rs | 17 +- .../src/bindings/_helpers/frontier/encoder.rs | 14 +- .../_helpers/frontier/python_types.rs | 3 +- .../_helpers/frontier/stage_capture.rs | 5 +- .../_helpers/graph/execution_gates.rs | 11 +- .../src/bindings/_helpers/graph/keys.rs | 5 +- .../_helpers/graph/python_lifecycle.rs | 8 +- .../_helpers/graph/python_node_graph.rs | 23 +- .../_helpers/graph/python_selection.rs | 24 +- .../src/bindings/_helpers/graph/views.rs | 4 +- .../_helpers/hand_built/compiled_model.rs | 91 +++--- .../bindings/_helpers/init_command/command.rs | 4 +- .../_helpers/init_command/scaffold.rs | 5 +- .../janitor/janitor_classification.rs | 8 +- .../_helpers/janitor/janitor_command.rs | 41 +-- .../_helpers/janitor/janitor_facts.rs | 35 ++- .../bindings/_helpers/janitor/janitor_keys.rs | 25 +- .../_helpers/janitor/janitor_old_names.rs | 20 +- .../_helpers/janitor/janitor_phases.rs | 39 ++- .../bindings/_helpers/janitor/janitor_plan.rs | 79 ++--- .../_helpers/janitor/janitor_planning.rs | 13 +- .../_helpers/lifecycle_events/envelopes.rs | 15 +- .../_helpers/lifecycle_events/validation.rs | 9 +- .../_helpers/lineage/column_lineage_graph.rs | 55 ++-- .../_helpers/lineage/lineage_cache.rs | 5 +- .../_helpers/lineage/lineage_command.rs | 105 ++++--- .../_helpers/lineage/lineage_index.rs | 44 +-- .../_helpers/lineage/lineage_objects.rs | 5 +- .../_helpers/lineage/project_lineage.rs | 49 +-- .../_helpers/lineage/project_schema.rs | 13 +- .../bindings/_helpers/load/ingestr_command.rs | 11 +- .../bindings/_helpers/load/load_command.rs | 107 ++++--- .../bindings/_helpers/load/load_progress.rs | 25 +- .../bindings/_helpers/load/load_selection.rs | 51 +-- .../_helpers/macro_bridge/store_keys.rs | 15 +- .../src/bindings/_helpers/manifest/builder.rs | 3 +- .../src/bindings/_helpers/manifest/rows.rs | 3 +- .../_helpers/microbatches/conversion.rs | 5 +- .../_helpers/microbatches/projection.rs | 44 +-- .../_helpers/microbatches/statements.rs | 17 +- .../_helpers/model_config/authored_nodes.rs | 3 +- .../_helpers/model_config/contract_classes.rs | 3 +- .../_helpers/model_config/described_models.rs | 18 +- .../_helpers/model_config/model_validator.rs | 11 +- .../_helpers/output_capture/dispatcher.rs | 7 +- .../_helpers/output_capture/records.rs | 12 +- .../bindings/_helpers/plan_command/plan.rs | 52 ++-- .../bindings/_helpers/plan_command/rules.rs | 49 +-- .../_helpers/plan_output/cursor_details.rs | 59 ++-- .../bindings/_helpers/plan_output/display.rs | 17 +- .../_helpers/plan_output/migrations.rs | 115 +++---- .../_helpers/plan_output/plan_json.rs | 290 ++++++++++-------- .../_helpers/plan_output/plan_text.rs | 177 ++++++----- .../_helpers/plan_output/python_identity.rs | 5 +- .../bindings/_helpers/plan_output/safety.rs | 39 +-- .../bindings/_helpers/plan_output/values.rs | 9 +- .../_helpers/planner_changes/actions.rs | 15 +- .../_helpers/planner_changes/detect.rs | 70 +++-- .../_helpers/planner_changes/renames.rs | 46 +-- .../_helpers/planner_entries/clone_plans.rs | 107 ++++--- .../_helpers/planner_entries/command_plans.rs | 93 +++--- .../_helpers/planner_entries/display_plan.rs | 9 +- .../planner_entries/execution_plan.rs | 29 +- .../_helpers/planner_entries/model_entries.rs | 75 ++--- .../_helpers/planner_entries/plan_output.rs | 189 +++++++----- .../_helpers/planner_entries/relations.rs | 31 +- .../_helpers/planner_identity/graph.rs | 31 +- .../_helpers/planner_identity/hashing.rs | 5 +- .../_helpers/planner_identity/identities.rs | 47 +-- .../_helpers/planner_identity/json_values.rs | 5 +- .../_helpers/planner_identity/metadata.rs | 77 ++--- .../_helpers/planner_identity/staleness.rs | 42 +-- .../planner_migrations/column_candidates.rs | 26 +- .../planner_migrations/column_entries.rs | 37 ++- .../planner_migrations/column_objects.rs | 3 +- .../planner_migrations/column_overlay.rs | 54 ++-- .../planner_migrations/column_planning.rs | 9 +- .../planner_migrations/column_warnings.rs | 33 +- .../_helpers/planner_migrations/columns.rs | 13 +- .../planner_migrations/compatibility.rs | 21 +- .../_helpers/planner_migrations/discovery.rs | 9 +- .../planner_migrations/discovery_events.rs | 29 +- .../planner_migrations/discovery_models.rs | 23 +- .../planner_migrations/discovery_requests.rs | 43 +-- .../planner_migrations/fingerprint.rs | 9 +- .../planner_migrations/known_input_columns.rs | 7 +- .../_helpers/planner_migrations/matching.rs | 7 +- .../planner_migrations/model_entries.rs | 53 ++-- .../planner_migrations/model_handover.rs | 18 +- .../planner_migrations/model_overlay.rs | 29 +- .../planner_migrations/model_planning.rs | 39 +-- .../planner_migrations/model_warnings.rs | 29 +- .../planner_migrations/old_name_entries.rs | 108 ++++--- .../planner_migrations/old_name_objects.rs | 36 ++- .../planner_migrations/old_name_planning.rs | 35 ++- .../planner_migrations/old_name_references.rs | 35 ++- .../_helpers/planner_migrations/old_names.rs | 31 +- .../planner_migrations/planning_boundary.rs | 24 +- .../planner_migrations/planning_execution.rs | 32 +- .../planner_migrations/planning_origin.rs | 42 +-- .../planner_migrations/projections.rs | 23 +- .../planner_migrations/state_inspection.rs | 58 ++-- .../_helpers/planner_phases/buildability.rs | 19 +- .../planner_phases/execution_changes.rs | 13 +- .../_helpers/planner_phases/full_refresh.rs | 13 +- .../_helpers/planner_phases/identities.rs | 19 +- .../_helpers/planner_phases/objects.rs | 9 +- .../planner_phases/output_assembly.rs | 45 +-- .../_helpers/planner_phases/retention.rs | 102 +++--- .../_helpers/planner_phases/scopes.rs | 33 +- .../planner_phases/warehouse_state.rs | 33 +- .../planner_resolution/cursor_bounds.rs | 13 +- .../planner_resolution/cursor_model.rs | 42 ++- .../planner_resolution/cursor_overrides.rs | 5 +- .../planner_resolution/cursor_policies.rs | 5 +- .../planner_resolution/cursor_ranges.rs | 33 +- .../planner_resolution/cursor_safety.rs | 9 +- .../planner_resolution/cursor_warnings.rs | 13 +- .../planner_resolution/cursor_windows.rs | 17 +- .../planner_resolution/future_safety.rs | 15 +- .../_helpers/planner_resolution/locations.rs | 6 +- .../planner_resolution/maximum_safety.rs | 51 +-- .../planner_resolution/orchestration.rs | 31 +- .../_helpers/planner_resolution/refs.rs | 5 +- .../_helpers/planner_resolution/sources.rs | 19 +- .../_helpers/planner_scenario/artifacts.rs | 15 +- .../bindings/_helpers/planner_scenario/cli.rs | 26 +- .../_helpers/planner_scenario/entries.rs | 26 +- .../_helpers/planner_scenario/execution.rs | 47 +-- .../_helpers/planner_scenario/fixtures.rs | 9 +- .../_helpers/planner_scenario/graph.rs | 29 +- .../_helpers/planner_scenario/relations.rs | 25 +- .../bindings/_helpers/planner_scenario/sql.rs | 13 +- .../_helpers/planner_scope/loaders.rs | 13 +- .../bindings/_helpers/planner_scope/scope.rs | 16 +- .../_helpers/planner_scope/sources.rs | 26 +- .../bindings/_helpers/planner_scope/values.rs | 20 +- .../planner_selection/model_selectors.rs | 3 +- .../_helpers/planner_selection/selectors.rs | 7 +- .../_helpers/planner_selection/unit_tests.rs | 17 +- .../planner_strategy/audit_entries.rs | 36 ++- .../_helpers/planner_strategy/audits.rs | 19 +- .../planner_strategy/cursor_inputs.rs | 33 +- .../_helpers/planner_strategy/cursor_types.rs | 5 +- .../planner_strategy/fixture_context.rs | 9 +- .../planner_strategy/fixture_metadata.rs | 5 +- .../planner_strategy/fixture_preparation.rs | 7 +- .../planner_strategy/fixture_reads.rs | 11 +- .../_helpers/planner_strategy/fixture_sql.rs | 9 +- .../planner_strategy/fixture_values.rs | 9 +- .../_helpers/planner_strategy/lineage.rs | 11 +- .../planner_strategy/microbatch_limits.rs | 41 +-- .../_helpers/planner_strategy/microbatches.rs | 12 +- .../planner_strategy/model_entries.rs | 40 +-- .../_helpers/planner_strategy/values.rs | 21 +- .../_helpers/planner_warehouse/cursors.rs | 68 ++-- .../planner_warehouse/fingerprint_cache.rs | 3 +- .../planner_warehouse/semantic_sources.rs | 42 +-- .../_helpers/planner_warehouse/snapshot.rs | 121 +++++--- .../planner_warehouse/source_columns.rs | 38 +-- .../planner_warehouse/source_deferral.rs | 85 ++--- .../planner_warehouse/source_freshness.rs | 19 +- .../planner_warehouse/source_tables.rs | 57 ++-- .../_helpers/planner_warehouse/watermarks.rs | 27 +- .../_helpers/presentation_text/functions.rs | 5 +- .../project_assembly/declared_relations.rs | 27 +- .../project_assembly/native_resources.rs | 16 +- .../project_assembly/syntax_checks.rs | 5 +- .../project_assembly/syntax_validation.rs | 9 +- .../bindings/_helpers/python_values/values.rs | 3 +- .../bindings/_helpers/query_command/dag.rs | 3 +- .../_helpers/query_command/project.rs | 8 +- .../bindings/_helpers/query_command/query.rs | 13 +- .../_helpers/record_behaviours/auditing.rs | 9 +- .../_helpers/record_behaviours/compiled.rs | 98 +++--- .../_helpers/record_behaviours/config.rs | 5 +- .../_helpers/record_behaviours/cursors.rs | 5 +- .../_helpers/record_behaviours/discovery.rs | 11 +- .../_helpers/record_behaviours/error_inits.rs | 18 +- .../_helpers/record_behaviours/lexical.rs | 5 +- .../record_behaviours/lineage_facts.rs | 20 +- .../_helpers/record_behaviours/macros.rs | 37 ++- .../_helpers/record_behaviours/migrations.rs | 30 +- .../_helpers/record_behaviours/planner.rs | 39 +-- .../record_behaviours/python_nodes.rs | 7 +- .../_helpers/record_behaviours/rule_shapes.rs | 7 +- .../_helpers/record_behaviours/scopes.rs | 5 +- .../_helpers/record_behaviours/values.rs | 17 +- .../_helpers/records/error_classes.rs | 3 +- .../bindings/_helpers/records/registration.rs | 4 +- .../_helpers/refactor/contract_compare.rs | 86 +++--- .../_helpers/refactor/contract_edits.rs | 40 +-- .../_helpers/refactor/offline_compile.rs | 27 +- .../_helpers/refactor/python_strings.rs | 5 +- .../_helpers/refactor/refactor_command.rs | 42 +-- .../_helpers/refactor/refactor_facts.rs | 137 +++++---- .../bindings/_helpers/references/relations.rs | 13 +- .../_helpers/references/source_relations.rs | 11 +- .../bindings/_helpers/render/authored_sql.rs | 5 +- .../_helpers/render/declaration_indexes.rs | 33 +- .../_helpers/render/declaration_values.rs | 90 +++--- .../_helpers/render/macro_call_classes.rs | 19 +- .../_helpers/render/macro_evaluation.rs | 7 +- .../_helpers/render/macro_expansion.rs | 36 ++- .../_helpers/render/macro_expansion_result.rs | 3 +- .../render/macro_generated_references.rs | 9 +- .../_helpers/result_output/aggregation.rs | 13 +- .../bindings/_helpers/result_output/assets.rs | 131 ++++---- .../bindings/_helpers/result_output/checks.rs | 136 ++++---- .../_helpers/result_output/documents.rs | 33 +- .../_helpers/result_output/event_writer.rs | 6 +- .../result_output/integration_envelope.rs | 3 +- .../result_output/integration_records.rs | 110 ++++--- .../_helpers/result_output/projection.rs | 3 +- .../_helpers/result_output/resource_ids.rs | 19 +- .../_helpers/result_output/terminal_index.rs | 7 +- .../_helpers/rules/custom_evaluation.rs | 28 +- .../bindings/_helpers/rules/custom_hosts.rs | 3 +- .../_helpers/rules/custom_rule_evidence.rs | 9 +- .../bindings/_helpers/rules/dsl_headers.rs | 11 +- .../bindings/_helpers/rules/fact_encoder.rs | 3 +- .../src/bindings/_helpers/rules/findings.rs | 13 +- .../src/bindings/_helpers/rules/fix_facts.rs | 9 +- .../bindings/_helpers/rules/fix_session.rs | 9 +- .../bindings/_helpers/rules/fix_verdict.rs | 20 +- .../_helpers/rules/fixture_formatting.rs | 9 +- .../bindings/_helpers/rules/lint_expansion.rs | 5 +- .../bindings/_helpers/rules/lint_headers.rs | 3 +- .../bindings/_helpers/rules/lint_inputs.rs | 14 +- .../_helpers/rules/lint_preparation.rs | 7 +- .../_helpers/rules/lint_sql_bodies.rs | 43 +-- .../src/bindings/_helpers/rules/lint_text.rs | 15 +- .../_helpers/rules/project_tree_queries.rs | 3 +- .../bindings/_helpers/rules/relation_keys.rs | 15 +- .../src/bindings/_helpers/rules/results.rs | 3 +- .../src/bindings/_helpers/rules/rule_cache.rs | 15 +- .../src/bindings/_helpers/rules/rule_run.rs | 3 +- .../src/bindings/_helpers/rules/sql_rules.rs | 7 +- .../scenario_command/capture_command.rs | 12 +- .../_helpers/scenario_command/conversions.rs | 190 ++++++------ .../_helpers/scenario_command/presentation.rs | 12 +- .../_helpers/scenario_command/setup.rs | 49 +-- .../scenario_command/snapshot_sync.rs | 24 +- .../_helpers/scopes/compile_usages.rs | 43 +-- .../bindings/_helpers/scopes/scope_cache.rs | 5 +- .../bindings/_helpers/scopes/scope_command.rs | 98 +++--- .../bindings/_helpers/scopes/scope_errors.rs | 37 ++- .../bindings/_helpers/scopes/scope_index.rs | 144 +++++---- .../bindings/_helpers/scopes/scope_loading.rs | 19 +- .../bindings/_helpers/scopes/scope_lookup.rs | 9 +- .../bindings/_helpers/scopes/scope_objects.rs | 43 +-- .../_helpers/scopes/scope_report_objects.rs | 17 +- .../bindings/_helpers/scopes/scope_reports.rs | 17 +- .../_helpers/scopes/scope_tolerant.rs | 7 +- .../_helpers/scopes/scope_validation.rs | 50 +-- .../_helpers/scopes/scope_visibility.rs | 17 +- .../bindings/_helpers/seed_command/command.rs | 114 ++++--- .../_helpers/semantic_checks/completion.rs | 60 ++-- .../_helpers/semantic_checks/inputs.rs | 30 +- .../_helpers/semantic_checks/metadata.rs | 91 +++--- .../_helpers/semantic_checks/resource_sql.rs | 67 ++-- .../_helpers/semantic_checks/stage.rs | 11 +- .../_helpers/semantic_checks/stage_objects.rs | 17 +- .../_helpers/semantic_checks/type_recovery.rs | 49 +-- .../_helpers/semantic_shapes/coverage.rs | 11 +- .../_helpers/semantic_shapes/declarations.rs | 13 +- .../_helpers/semantic_shapes/shapes.rs | 67 ++-- .../semantic_shapes/source_bindings.rs | 103 ++++--- .../_helpers/sql_test_glue/assembly.rs | 61 ++-- .../_helpers/sql_test_glue/authored_ctes.rs | 17 +- .../_helpers/sql_test_glue/compiled_tests.rs | 52 ++-- .../_helpers/sql_test_glue/cursor_window.rs | 24 +- .../sql_test_glue/fixture_validation.rs | 29 +- .../_helpers/sql_test_glue/plan_assembly.rs | 7 +- .../_helpers/sql_test_glue/plan_driver.rs | 13 +- .../_helpers/sql_test_glue/plan_entries.rs | 46 +-- .../_helpers/sql_test_glue/plan_errors.rs | 9 +- .../_helpers/sql_test_glue/plan_fixtures.rs | 16 +- .../_helpers/sql_test_glue/plan_rendering.rs | 11 +- .../_helpers/sql_test_glue/plan_values.rs | 3 +- .../_helpers/sql_test_glue/planning.rs | 27 +- .../_helpers/sqltext/sql_references.rs | 9 +- .../bindings/_helpers/state_rows/events.rs | 9 +- .../bindings/_helpers/state_rows/freshness.rs | 15 +- .../_helpers/state_rows/migration_io.rs | 21 +- .../_helpers/state_rows/migration_objects.rs | 5 +- .../state_rows/migration_projections.rs | 17 +- .../_helpers/state_rows/migration_rows.rs | 15 +- .../_helpers/state_rows/timestamps.rs | 3 +- .../_helpers/type_system/normalization.rs | 3 +- .../_helpers/workspace_commands/commands.rs | 7 +- 467 files changed, 7873 insertions(+), 6315 deletions(-) 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/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 76ba00de5b..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 @@ -22,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"; @@ -754,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/records.rs b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/records.rs index 361239937b..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); } @@ -466,7 +470,7 @@ pub(crate) fn compiled_project_facts<'py>( if let Ok(view) = project.downcast::() { return Ok(view.get().owner().bind(project.py()).clone()); } - let retained: Bound<'py, PyAny> = project.getattr("native_project")?; + let retained: Bound<'py, PyAny> = native_field(project, "native_project")?; if !retained.is_none() { return Ok(retained.downcast_into()?); } @@ -475,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()? @@ -485,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, @@ -502,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 @@ -522,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/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/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/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/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/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 4ac43f0b09..8fe9e4a7c9 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/fix_facts.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/fix_facts.rs @@ -6,6 +6,7 @@ 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; @@ -21,15 +22,15 @@ 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")?; 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 cd86b424de..f5a82effb1 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/fix_session.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/fix_session.rs @@ -22,6 +22,7 @@ use crate::bindings::_helpers::compile_driver::python_calls::{ 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::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; @@ -502,7 +503,7 @@ fn replace<'py>( 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(_) => model.getattr("contents"), + Err(_) => native_field(model, "contents"), } } @@ -510,7 +511,7 @@ fn model_contents<'py>(model: &Bound<'py, PyAny>) -> PyResult> fn model_file_path<'py>(model: &Bound<'py, PyAny>) -> PyResult> { match model.cast::() { Ok(native) => Ok(native.get().paths(model.py()).0.clone()), - Err(_) => model.getattr("file_path"), + Err(_) => native_field(model, "file_path"), } } @@ -521,8 +522,8 @@ fn edited_model<'py>( ) -> PyResult> { let py = model.py(); let Ok(native) = model.cast::() else { - let current = model.getattr("contents")?; - let start = current.call_method1("index", (model.getattr("query_sql")?,))?; + 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, 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/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_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/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 b35b795028..f4e41c3b2b 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/results.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/results.rs @@ -14,6 +14,7 @@ use sqlbuild_rules::rule_results::main::format_skipped_rules::format_skipped_rul use sqlbuild_rules::rule_results::models::{ResultCounts, RuleFinding}; 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 UTF8: &str = "utf-8"; @@ -102,7 +103,7 @@ fn file_lines(project_dir: &Bound<'_, PyAny>, path: &str) -> PyResult = 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 { 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_run.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/rule_run.rs index 388b1f8318..ab494cc30c 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/rule_run.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/rule_run.rs @@ -13,6 +13,7 @@ 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; @@ -150,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/sql_rules.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/sql_rules.rs index 723f68adf7..0d996c3266 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/sql_rules.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/sql_rules.rs @@ -23,6 +23,7 @@ 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; @@ -247,8 +248,8 @@ fn sql_rule_model(model: Bound<'_, PyAny>, keyed: bool) -> PyResult = keyed .then(|| -> PyResult { Ok(SqlRuleKey { - authored_sql: model.getattr("authored_sql")?.extract()?, - query_sql: model.getattr("query_sql")?.extract()?, + authored_sql: native_field(&model, "authored_sql")?.extract()?, + query_sql: native_field(&model, "query_sql")?.extract()?, contract: contract_text(&model)?, }) }) @@ -305,7 +306,7 @@ 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); } 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(),