Skip to content

Commit bbcfd7a

Browse files
authored
refactor: read native record fields directly across the bindings (#1123)
* 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. * 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. * 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. * 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. * refactor: read native record fields directly instead of through the Python attribute protocol
1 parent 6f3bee6 commit bbcfd7a

490 files changed

Lines changed: 8881 additions & 6836 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//! A dynamic pivot proof as `json.dumps(asdict(proof), sort_keys=True, separators=(",", ":"))`.
2+
3+
use sqlbuild_core::json::main::dumps::dumps;
4+
use sqlbuild_core::json::models::{JsonDialect, JsonValue, StdlibJsonOptions};
5+
6+
use crate::assembly::analysis_session::models::ContractProof;
7+
8+
/// The proof's canonical JSON text, as the SQL rule cache keys a model's contract.
9+
pub fn contract_proof_json(proof: &ContractProof) -> String {
10+
let text = |value: &str| JsonValue::String(value.to_owned());
11+
let optional = |value: Option<&String>| value.map_or(JsonValue::Null, |value| text(value));
12+
let fixed: Vec<JsonValue> = proof
13+
.fixed_columns
14+
.iter()
15+
.map(|column| {
16+
JsonValue::Object(vec![
17+
("name".to_owned(), text(&column.name)),
18+
("nullability".to_owned(), text(&column.nullability)),
19+
("type".to_owned(), optional(column.data_type.as_ref())),
20+
])
21+
})
22+
.collect();
23+
let families: Vec<JsonValue> = proof
24+
.families
25+
.iter()
26+
.map(|(name, inferred_type)| {
27+
JsonValue::Object(vec![
28+
("inferred_type".to_owned(), optional(inferred_type.as_ref())),
29+
("name".to_owned(), text(name)),
30+
])
31+
})
32+
.collect();
33+
let value: JsonValue = JsonValue::Object(vec![
34+
(
35+
"bare_dynamic_pivot".to_owned(),
36+
JsonValue::Bool(proof.bare_dynamic_pivot),
37+
),
38+
(
39+
"failure_reason".to_owned(),
40+
optional(proof.failure_reason.as_ref()),
41+
),
42+
("families".to_owned(), JsonValue::Array(families)),
43+
("fixed_columns".to_owned(), JsonValue::Array(fixed)),
44+
(
45+
"input_relations".to_owned(),
46+
JsonValue::Array(
47+
proof
48+
.input_relations
49+
.iter()
50+
.map(|name| text(name))
51+
.collect(),
52+
),
53+
),
54+
(
55+
"output_proven".to_owned(),
56+
JsonValue::Bool(proof.output_proven),
57+
),
58+
]);
59+
let options: StdlibJsonOptions = StdlibJsonOptions {
60+
sort_keys: true,
61+
..StdlibJsonOptions::new(None).with_separators(",", ":")
62+
};
63+
dumps(&value, &JsonDialect::Stdlib(options)).unwrap_or_default()
64+
}

‎crates/sqlbuild-analysis/src/assembly/analysis_session/main/mod.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod attach_analysis_cache;
2+
pub mod contract_proof_json;
23
pub mod expression_shapes;
34
pub mod finish_analysis_session;
45
pub mod finished_fact_models;

‎crates/sqlbuild-analysis/src/assembly/analysis_session/tests.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
mod analysis_cache;
33
#[path = "tests/test_analysis_session.rs"]
44
mod analysis_session;
5+
#[path = "tests/test_contract_proof_json.rs"]
6+
mod contract_proof_json;
57
#[path = "tests/test_expression_shapes.rs"]
68
mod expression_shapes;
79
#[path = "tests/helpers.rs"]
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
use crate::assembly::analysis_session::main::contract_proof_json::contract_proof_json;
2+
use crate::assembly::analysis_session::tests::test_types::ContractProofJsonTestCase;
3+
4+
use crate::assembly::analysis_session::models::{ColumnFact, ContractProof};
5+
6+
#[test]
7+
fn given_contract_proof_when_encoding_then_matches_python_sorted_compact_json() {
8+
let test_cases = [
9+
ContractProofJsonTestCase {
10+
description: "a proven pivot escapes non-ASCII text as Python does",
11+
proof: ContractProof {
12+
output_proven: true,
13+
fixed_columns: vec![ColumnFact {
14+
name: "a\u{e9}\"".to_owned(),
15+
data_type: None,
16+
nullability: "non_null".to_owned(),
17+
}],
18+
families: vec![("f".to_owned(), Some("INT".to_owned()))],
19+
input_relations: vec!["x".to_owned()],
20+
failure_reason: None,
21+
bare_dynamic_pivot: false,
22+
},
23+
expected_json: concat!(
24+
r#"{"bare_dynamic_pivot":false,"failure_reason":null,"#,
25+
r#""families":[{"inferred_type":"INT","name":"f"}],"#,
26+
r#""fixed_columns":[{"name":"a\u00e9\"","nullability":"non_null","type":null}],"#,
27+
r#""input_relations":["x"],"output_proven":true}"#
28+
),
29+
},
30+
ContractProofJsonTestCase {
31+
description: "a failed proof keeps its reason and empty collections",
32+
proof: ContractProof {
33+
output_proven: false,
34+
fixed_columns: Vec::new(),
35+
families: vec![("g".to_owned(), None)],
36+
input_relations: Vec::new(),
37+
failure_reason: Some("no schema".to_owned()),
38+
bare_dynamic_pivot: true,
39+
},
40+
expected_json: concat!(
41+
r#"{"bare_dynamic_pivot":true,"failure_reason":"no schema","#,
42+
r#""families":[{"inferred_type":null,"name":"g"}],"fixed_columns":[],"#,
43+
r#""input_relations":[],"output_proven":false}"#
44+
),
45+
},
46+
];
47+
48+
for test_case in test_cases {
49+
let json: String = contract_proof_json(&test_case.proof);
50+
51+
assert_eq!(json, test_case.expected_json, "{}", test_case.description);
52+
}
53+
}

‎crates/sqlbuild-analysis/src/assembly/analysis_session/tests/test_types.rs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::assembly::analysis_session::models::PivotOutcome;
1+
use crate::assembly::analysis_session::models::{ContractProof, PivotOutcome};
22

33
/// A model as `(name, sql, source names, model refs)`.
44
pub(crate) type ModelSpec = (
@@ -183,3 +183,9 @@ pub(crate) struct NativeDialectTestCase {
183183
pub(crate) dialect: Option<&'static str>,
184184
pub(crate) expected_dialect: Option<&'static str>,
185185
}
186+
187+
pub(crate) struct ContractProofJsonTestCase {
188+
pub(crate) description: &'static str,
189+
pub(crate) proof: ContractProof,
190+
pub(crate) expected_json: &'static str,
191+
}

‎crates/sqlbuild-python/src/bindings/_helpers/analysis/binding_catalog.rs‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use sqlbuild_analysis::semantic_validation::types::{BindingRequest, DiagnosticRo
1111

1212
use crate::bindings::_helpers::analysis::conversions;
1313
use crate::bindings::_helpers::boundary::panics::compiler_error;
14+
use crate::bindings::_helpers::records::fields::field as native_field;
1415
use crate::bindings::_helpers::records::ids::RecordId;
1516
use crate::bindings::_helpers::records::registry;
1617
use crate::bindings::models::{CatalogInput, ProjectCatalog};
@@ -88,7 +89,7 @@ impl BindingCatalog {
8889
let schemas: &Bound<'_, PyDict> = self.schemas.bind(py);
8990
let additions: Bound<'_, PyDict> = PyDict::new(py);
9091
for request in requests {
91-
let schema: Bound<'_, PyAny> = request.getattr("schema")?;
92+
let schema: Bound<'_, PyAny> = native_field(request, "schema")?;
9293
for item in schema.call_method0("items")?.try_iter()? {
9394
let (name, columns): (Bound<'_, PyAny>, Bound<'_, PyAny>) = item?.extract()?;
9495
if columns.is_truthy()? && !schemas.contains(&name)? {
@@ -103,7 +104,7 @@ impl BindingCatalog {
103104
}
104105
let mut prepared: Vec<BindingRequest> = Vec::with_capacity(requests.len());
105106
for request in requests {
106-
let schema: Bound<'_, PyAny> = request.getattr("schema")?;
107+
let schema: Bound<'_, PyAny> = native_field(request, "schema")?;
107108
let mut names: Vec<String> = Vec::new();
108109
for name in schema.try_iter()? {
109110
names.push(name?.extract()?);
@@ -123,7 +124,7 @@ impl BindingCatalog {
123124
}
124125
closed.push((name, truthy));
125126
}
126-
let sql: String = request.getattr("sql")?.extract()?;
127+
let sql: String = native_field(request, "sql")?.extract()?;
127128
prepared.push((sql, closed, conversions::relations(overrides.as_any())?));
128129
}
129130
Ok(prepared)
@@ -293,14 +294,14 @@ pub(crate) fn schema_validations<'py>(
293294
let mut groups: Vec<(Py<BindingCatalog>, bool, Vec<usize>)> = Vec::new();
294295
for (index, request) in requests.iter().enumerate() {
295296
let key: CatalogKey = (
296-
native_dialect(&request.getattr("dialect")?)?,
297+
native_dialect(&native_field(request, "dialect")?)?,
297298
request
298299
.getattr("quoted_identifiers_ignore_case")?
299300
.extract()?,
300-
request.getattr("known_functions")?.extract()?,
301-
request.getattr("known_types")?.extract()?,
301+
native_field(request, "known_functions")?.extract()?,
302+
native_field(request, "known_types")?.extract()?,
302303
);
303-
let explicit: Bound<'py, PyAny> = request.getattr("catalog")?;
304+
let explicit: Bound<'py, PyAny> = native_field(request, "catalog")?;
304305
let own: bool = !explicit.is_none();
305306
let catalog: Py<BindingCatalog> = if own {
306307
explicit.extract()?
@@ -309,7 +310,7 @@ pub(crate) fn schema_validations<'py>(
309310
} else {
310311
let catalog: Py<BindingCatalog> = Py::new(
311312
py,
312-
BindingCatalog::create(py, key.clone(), &request.getattr("schema")?)?,
313+
BindingCatalog::create(py, key.clone(), &native_field(request, "schema")?)?,
313314
)?;
314315
created.push((key, catalog.clone_ref(py)));
315316
catalog

‎crates/sqlbuild-python/src/bindings/_helpers/analysis/explicit_references.rs‎

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ use crate::bindings::_helpers::boundary::panics::compiler_error;
2828
use crate::bindings::_helpers::compile_driver::python_calls::imported;
2929
use crate::bindings::_helpers::native_host::host;
3030
use crate::bindings::_helpers::native_host::user_code::UserCodeHost;
31+
use crate::bindings::_helpers::records::fields::field as native_field;
3132
use crate::bindings::_helpers::records::ids::{ErrorId, NativeAttr, RecordId};
3233
use crate::bindings::_helpers::records::registry;
3334
use crate::bindings::types::CompilerDetach;
@@ -65,8 +66,8 @@ impl<'py> PythonSqlOwner<'py> {
6566
Ok(Self {
6667
label,
6768
kind,
68-
function: node.getattr("function")?,
69-
relative_path: node.getattr("relative_path")?,
69+
function: native_field(node, "function")?,
70+
relative_path: native_field(node, "relative_path")?,
7071
own_refs: Vec::new(),
7172
upstream_loader_by_source: Vec::new(),
7273
})
@@ -117,7 +118,7 @@ fn explicit_reference_report<'py>(
117118
}
118119
let request = ReferenceRequest {
119120
enforce_explicit,
120-
dialect: project.getattr("sql_analysis_dialect")?.extract()?,
121+
dialect: native_field(project, "sql_analysis_dialect")?.extract()?,
121122
explicit_help: if enforce_explicit {
122123
references_help(HARD_CODED_ALLOWED)
123124
} else {
@@ -158,7 +159,7 @@ fn python_sql_owners<'py>(
158159
for (label, attribute) in NODE_OWNERS {
159160
for node in discovered_inputs.getattr(attribute)?.try_iter()? {
160161
let node: Bound<'py, PyAny> = node?;
161-
let name: String = node.getattr("name")?.extract()?;
162+
let name: String = native_field(&node, "name")?.extract()?;
162163
owners.push(PythonSqlOwner::new(
163164
format!("{label}:{name}"),
164165
NODE_KIND,
@@ -169,21 +170,21 @@ fn python_sql_owners<'py>(
169170
owners.extend(loader_owners(project, discovered_inputs)?);
170171
let hook_names = registry::native(py, NativeAttr::ModelPythonHookNames)?;
171172
let mut attached: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
172-
for model in project.getattr("models")?.try_iter()? {
173+
for model in native_field(project, "models")?.try_iter()? {
173174
let model: Bound<'py, PyAny> = model?;
174175
let keywords: Bound<'py, PyDict> = PyDict::new(py);
175176
keywords.set_item("model", &model)?;
176-
let model_name: String = model.getattr("name")?.extract()?;
177+
let model_name: String = native_field(&model, "name")?.extract()?;
177178
for hook_name in hook_names.call((), Some(&keywords))?.try_iter()? {
178179
attached
179180
.entry(hook_name?.extract()?)
180181
.or_default()
181182
.insert(model_name.clone());
182183
}
183184
}
184-
for hook in project.getattr("hook_functions")?.try_iter()? {
185+
for hook in native_field(project, "hook_functions")?.try_iter()? {
185186
let hook: Bound<'py, PyAny> = hook?;
186-
let name: String = hook.getattr("name")?.extract()?;
187+
let name: String = native_field(&hook, "name")?.extract()?;
187188
let mut owner = PythonSqlOwner::new(format!("hook:{name}"), HOOK_KIND, &hook)?;
188189
owner.own_refs = attached
189190
.get(&name)
@@ -203,7 +204,7 @@ fn loader_owners<'py>(
203204
) -> PyResult<Vec<PythonSqlOwner<'py>>> {
204205
let py: Python<'py> = project.py();
205206
let mut sources_by_loader: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
206-
for source in project.getattr("sources")?.try_iter()? {
207+
for source in native_field(project, "sources")?.try_iter()? {
207208
let source: Bound<'py, PyAny> = source?;
208209
let loader: Option<String> = source
209210
.getattr("source_entry")?
@@ -213,16 +214,16 @@ fn loader_owners<'py>(
213214
sources_by_loader
214215
.entry(loader)
215216
.or_default()
216-
.insert(source.getattr("name")?.extract()?);
217+
.insert(native_field(&source, "name")?.extract()?);
217218
}
218219
}
219220
let read_definition = host::entry(py, UserCodeHost::ReadLoaderDefinition)?;
220221
let mut owners: Vec<PythonSqlOwner<'py>> = Vec::new();
221-
for loader in discovered_inputs.getattr("loader_functions")?.try_iter()? {
222+
for loader in native_field(discovered_inputs, "loader_functions")?.try_iter()? {
222223
let loader: Bound<'py, PyAny> = loader?;
223-
let name: String = loader.getattr("name")?.extract()?;
224+
let name: String = native_field(&loader, "name")?.extract()?;
224225
let mut upstream: Vec<(String, String)> = Vec::new();
225-
for dependency in loader.getattr("depends_on")?.try_iter()? {
226+
for dependency in native_field(&loader, "depends_on")?.try_iter()? {
226227
let dependency: Bound<'py, PyAny> = dependency?;
227228
if !dependency.is_callable() {
228229
continue;
@@ -231,7 +232,7 @@ fn loader_owners<'py>(
231232
if definition.is_none() {
232233
continue;
233234
}
234-
let definition_name: String = definition.getattr("name")?.extract()?;
235+
let definition_name: String = native_field(&definition, "name")?.extract()?;
235236
let shown: String = match dependency.getattr_opt("__name__")? {
236237
Some(shown) => shown.extract()?,
237238
None => definition_name.clone(),
@@ -341,7 +342,7 @@ fn python_report<'py>(
341342
}
342343
let literal_relation: Bound<'py, PyAny> =
343344
registry::class(reference_models.py(), RecordId::LiteralSqlRelation)?;
344-
let relation_name: Bound<'py, PyAny> = reference_models.getattr("RelationName")?;
345+
let relation_name: Bound<'py, PyAny> = native_field(&reference_models, "RelationName")?;
345346
let mut unmatched: Vec<Bound<'py, PyAny>> = Vec::with_capacity(report.unmatched.len());
346347
for row in &report.unmatched {
347348
let owner: &PythonSqlOwner<'py> = &owners[row.owner];
@@ -392,8 +393,8 @@ pub(crate) fn hard_coded_relation_remedy(
392393
upstream_loader_by_source: Option<&Bound<'_, PyAny>>,
393394
) -> PyResult<String> {
394395
let target: ResourceKey = (
395-
r#ref.getattr("kind")?.getattr("value")?.extract()?,
396-
r#ref.getattr("name")?.extract()?,
396+
native_field(r#ref, "kind")?.getattr("value")?.extract()?,
397+
native_field(r#ref, "name")?.extract()?,
397398
);
398399
let mut upstream_loaders: Vec<(String, String)> = Vec::new();
399400
if let Some(loaders) = upstream_loader_by_source.filter(|loaders| !loaders.is_none()) {

0 commit comments

Comments
 (0)