Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,7 @@ jobs:
run: make test-e2e-diagnostic-compile-performance

compile_performance_ratio:
name: Same-runner compile ratio (${{ matrix.kind }} 3000 models)
name: Same-runner compile ratio (${{ matrix.label || matrix.kind }} 3000 models)
needs: [changes, lockfile]
if: needs.changes.outputs.performance == 'true'
runs-on: ubuntu-24.04
Expand All @@ -598,6 +598,12 @@ jobs:
max_ratio: "1.10"
- kind: fresh
max_ratio: "1.10"
- kind: dense
label: native-preview contracts vs python
base_engine: python
head_engine: native-preview
gate_phase: contracts_cpu_ms
max_ratio: "1.00"
steps:
- uses: actions/checkout@v4
- name: Check out the comparison base
Expand All @@ -624,7 +630,27 @@ jobs:
- name: Install base dependencies
working-directory: base
run: uv sync --locked --all-groups --all-extras
- name: Compare python and native-preview contract evaluation CPU on head
if: matrix.gate_phase
env:
KIND: ${{ matrix.kind }}
MAX_RATIO: ${{ matrix.max_ratio }}
BASE_ENGINE: ${{ matrix.base_engine }}
HEAD_ENGINE: ${{ matrix.head_engine }}
GATE_PHASE: ${{ matrix.gate_phase }}
run: |
uv run python -m scripts.compare_compile_performance \
--kind "${KIND}" \
--modes cold \
--max-ratio "${MAX_RATIO}" \
--models 3000 \
--base-python .venv/bin/python \
--head-python .venv/bin/python \
--base-engine "${BASE_ENGINE}" \
--head-engine "${HEAD_ENGINE}" \
--gate-phase "${GATE_PHASE}"
- name: Compare base and head cold, warm and one-model edit compiles
if: ${{ !matrix.gate_phase }}
env:
KIND: ${{ matrix.kind }}
MAX_RATIO: ${{ matrix.max_ratio }}
Expand Down
12 changes: 6 additions & 6 deletions crates/sqlbuild-analysis/src/contracts/_helpers/columns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use std::collections::{HashMap, HashSet};

use crate::contracts::_helpers::dynamic::dynamic_column_diagnostics;
use crate::contracts::_helpers::type_comparison::{TypeComparison, types_equal};
use crate::contracts::_helpers::type_comparison::{TypeComparer, TypeComparison};
use crate::contracts::constants::{
CONTRACT_ENFORCED, CONTRACT_NONE, DYNAMIC_OUTPUT_NOT_PROVEN_CODE, EXTRA_COLUMN_CODE,
EXTRA_COLUMN_HELP, EXTRA_COLUMN_NAMED_SCHEMA_HELP, MISSING_COLUMN_CODE,
Expand Down Expand Up @@ -45,7 +45,7 @@ pub(crate) fn declared_shape_validation_is_active(model: &ContractModel, implici
pub(crate) fn model_contract_diagnostics(
model: &ContractModel,
validate_declared_shape: bool,
dialect: &str,
types: &TypeComparer<'_>,
) -> Result<Vec<ContractDiagnostic>, ContractDeferral> {
let contract_enforced: bool = model.contract.as_deref() == Some(CONTRACT_ENFORCED);
let Some(schema) = model
Expand All @@ -59,7 +59,7 @@ pub(crate) fn model_contract_diagnostics(
Vec::new()
});
};
let mut diagnostics: Vec<ContractDiagnostic> = dynamic_column_diagnostics(model, dialect)?;
let mut diagnostics: Vec<ContractDiagnostic> = dynamic_column_diagnostics(model, types)?;
let dynamic_output_unproven: bool = diagnostics
.iter()
.any(|diagnostic| diagnostic.code == DYNAMIC_OUTPUT_NOT_PROVEN_CODE);
Expand Down Expand Up @@ -107,7 +107,7 @@ pub(crate) fn model_contract_diagnostics(
type_enforcement: schema.type_enforcement,
contract_enforced,
unchecked: &unchecked,
dialect,
types,
};
diagnostics.extend(column_type(&checked, &check)?);
}
Expand All @@ -118,7 +118,7 @@ struct TypeCheck<'a> {
type_enforcement: bool,
contract_enforced: bool,
unchecked: &'a HashSet<&'a str>,
dialect: &'a str,
types: &'a TypeComparer<'a>,
}

struct TypedColumn<'a> {
Expand Down Expand Up @@ -271,7 +271,7 @@ fn column_type(
help: UNKNOWN_TYPE_HELP.to_owned(),
}));
};
if types_equal(declared_type, inferred_type, check.dialect)? == TypeComparison::Equal {
if check.types.types_equal(declared_type, inferred_type)? == TypeComparison::Equal {
return Ok(None);
}
Ok(Some(ContractDiagnostic {
Expand Down
6 changes: 3 additions & 3 deletions crates/sqlbuild-analysis/src/contracts/_helpers/dynamic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use std::collections::HashMap;

use crate::contracts::_helpers::type_comparison::{TypeComparison, types_equal};
use crate::contracts::_helpers::type_comparison::{TypeComparer, TypeComparison};
use crate::contracts::constants::{
DYNAMIC_FAMILY_TYPE_MISMATCH_HELP, DYNAMIC_FAMILY_UNKNOWN_TYPE_HELP,
DYNAMIC_OUTPUT_NOT_PROVEN_CODE, DYNAMIC_OUTPUT_NOT_PROVEN_HELP, NO_COMPILER_EVIDENCE,
Expand All @@ -16,7 +16,7 @@ use crate::contracts::models::{
/// The dynamic column contract diagnostics of one model, before its fixed columns.
pub(crate) fn dynamic_column_diagnostics(
model: &ContractModel,
dialect: &str,
types: &TypeComparer<'_>,
) -> Result<Vec<ContractDiagnostic>, ContractDeferral> {
let Some(schema) = model
.schema
Expand Down Expand Up @@ -49,7 +49,7 @@ pub(crate) fn dynamic_column_diagnostics(
));
continue;
};
if types_equal(declared, inferred, dialect)? == TypeComparison::Different {
if types.types_equal(declared, inferred)? == TypeComparison::Different {
diagnostics.push(family_diagnostic(
TYPE_MISMATCH_CODE,
ContractSeverity::Error,
Expand Down
58 changes: 41 additions & 17 deletions crates/sqlbuild-analysis/src/contracts/_helpers/type_comparison.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
//! Python's `types_equal` over the native type system, deferring where it cannot answer.

use std::cell::RefCell;
use std::collections::HashMap;

use crate::contracts::models::ContractDeferral;
use crate::type_system::main::normalize_type::normalize_type;
use crate::type_system::models::{NormalizedType, TypeNormalization};
Expand All @@ -11,23 +14,44 @@ pub(crate) enum TypeComparison {
Different,
}

/// Compare two types as Python's `types_equal`, or defer when either type has no native answer.
pub(crate) fn types_equal(
left: &str,
right: &str,
dialect: &str,
) -> Result<TypeComparison, ContractDeferral> {
let left: NormalizedType = normalized(left, dialect)?;
let right: NormalizedType = normalized(right, dialect)?;
Ok(if left == right {
TypeComparison::Equal
} else {
TypeComparison::Different
})
/// One request's type comparisons, normalizing each type string once as Python's cache does.
pub(crate) struct TypeComparer<'a> {
dialect: &'a str,
normalized: RefCell<HashMap<String, Option<NormalizedType>>>,
}

fn normalized(type_sql: &str, dialect: &str) -> Result<NormalizedType, ContractDeferral> {
normalize_type(type_sql, dialect)
.map(|TypeNormalization { normalized, .. }| normalized)
.ok_or(ContractDeferral::TypeNormalization)
impl<'a> TypeComparer<'a> {
pub(crate) fn new(dialect: &'a str) -> Self {
Self {
dialect,
normalized: RefCell::new(HashMap::new()),
}
}

/// Compare two types as Python's `types_equal`, or defer when either has no native answer.
pub(crate) fn types_equal(
&self,
left: &str,
right: &str,
) -> Result<TypeComparison, ContractDeferral> {
let left: NormalizedType = self.normalized(left)?;
let right: NormalizedType = self.normalized(right)?;
Ok(if left == right {
TypeComparison::Equal
} else {
TypeComparison::Different
})
}

fn normalized(&self, type_sql: &str) -> Result<NormalizedType, ContractDeferral> {
if let Some(known) = self.normalized.borrow().get(type_sql) {
return known.clone().ok_or(ContractDeferral::TypeNormalization);
}
let normalized: Option<NormalizedType> = normalize_type(type_sql, self.dialect)
.map(|TypeNormalization { normalized, .. }| normalized);
self.normalized
.borrow_mut()
.insert(type_sql.to_owned(), normalized.clone());
normalized.ok_or(ContractDeferral::TypeNormalization)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
use crate::contracts::_helpers::columns::{
declared_shape_validation_is_active, model_contract_diagnostics, requires_contract_evaluation,
};
use crate::contracts::_helpers::type_comparison::TypeComparer;
use crate::contracts::models::{ContractOutcome, ContractRequest};

/// One outcome per requested model; models that need no evaluation have no diagnostics.
#[must_use]
pub fn evaluate_model_contracts(request: &ContractRequest) -> Vec<ContractOutcome> {
let implicit: bool = request.implicit_column_contracts;
let types = TypeComparer::new(&request.dialect);
request
.models
.iter()
Expand All @@ -19,7 +21,7 @@ pub fn evaluate_model_contracts(request: &ContractRequest) -> Vec<ContractOutcom
match model_contract_diagnostics(
model,
declared_shape_validation_is_active(model, implicit),
&request.dialect,
&types,
) {
Ok(diagnostics) => ContractOutcome::Diagnostics(diagnostics),
Err(deferral) => ContractOutcome::Deferred(deferral),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use crate::contracts::main::evaluate_model_contracts::evaluate_model_contracts;
use crate::contracts::models::{ContractOutcome, ContractRequest};
use crate::contracts::models::{ContractModel, ContractOutcome, ContractRequest};
use crate::contracts::tests::helpers::{
declared, dynamic_model, inferred, model, outcome_lines, proof, schema, with_inferred,
};
use crate::contracts::tests::test_types::ContractTestCase;
use crate::contracts::tests::test_types::{ContractTestCase, SharedTypesTestCase};

#[test]
fn given_model_contracts_when_evaluating_then_diagnostics_follow_python_order() {
Expand Down Expand Up @@ -210,3 +210,60 @@ fn given_model_contracts_when_evaluating_then_diagnostics_follow_python_order()
);
}
}

#[test]
fn given_models_sharing_types_when_evaluating_together_then_each_matches_its_own_request() {
let enforced = |declared_type: &str, inferred_type: &str| {
with_inferred(
model(
Some("enforced"),
Some(schema(
vec![declared("amount", Some(declared_type), false)],
false,
)),
),
vec![inferred("amount", Some(inferred_type), false)],
)
};
let test_cases = [SharedTypesTestCase {
description: "repeated equal, mismatched and unanswerable types",
models: vec![
enforced("INT", "INTEGER"),
enforced("INT", "VARCHAR"),
enforced("TÉXT", "VARCHAR"),
enforced("INT", "INTEGER"),
enforced("INT", "VARCHAR"),
enforced("TÉXT", "VARCHAR"),
],
expected_lines: &[
"K002 error declared:0 column 'amount' inferred as VARCHAR but declared type is INT [amount: inferred VARCHAR]",
"deferred:type_normalization",
"K002 error declared:0 column 'amount' inferred as VARCHAR but declared type is INT [amount: inferred VARCHAR]",
"deferred:type_normalization",
],
}];
for test_case in test_cases {
let request = |models: Vec<ContractModel>| ContractRequest {
dialect: "duckdb".to_owned(),
implicit_column_contracts: true,
models,
};
let together: Vec<String> = evaluate_model_contracts(&request(test_case.models.clone()))
.iter()
.flat_map(outcome_lines)
.collect();
let alone: Vec<String> = test_case
.models
.iter()
.flat_map(|model| evaluate_model_contracts(&request(vec![model.clone()])))
.flat_map(|outcome| outcome_lines(&outcome))
.collect();

assert_eq!(together, alone, "{}", test_case.description);
assert_eq!(
together, test_case.expected_lines,
"{}",
test_case.description
);
}
}
7 changes: 7 additions & 0 deletions crates/sqlbuild-analysis/src/contracts/tests/test_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ pub(crate) struct ContractTestCase {
pub(crate) expected_lines: &'static [&'static str],
}

pub(crate) struct SharedTypesTestCase {
pub(crate) description: &'static str,
pub(crate) models: Vec<ContractModel>,
/// Every model's lines in order, as each model's own single-model request yields them.
pub(crate) expected_lines: &'static [&'static str],
}

pub(crate) struct PromotionTestCase {
pub(crate) description: &'static str,
pub(crate) request: PromotionRequest,
Expand Down
5 changes: 4 additions & 1 deletion crates/sqlbuild-analysis/src/type_system/_helpers/parsed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,11 @@ fn python_str(value: &Value) -> Option<String> {
}
}

/// Whether the wheel hands Python this value as an expression object rather than data.
/// Whether the wheel hands Python an expression object; every variant holds data, so not a scalar.
fn is_python_expression(value: &Value) -> bool {
if !matches!(value, Value::Object(_) | Value::Array(_)) {
return false;
}
if let Ok(_expression) = expression_from_value(value.clone()) {
return true;
}
Expand Down
Loading
Loading