Skip to content
Merged
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
44 changes: 0 additions & 44 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@ sqlbuild-scopes = { path = "crates/sqlbuild-scopes" }
sqlbuild-sqltext = { path = "crates/sqlbuild-sqltext" }
sqlbuild-state = { path = "crates/sqlbuild-state" }
stacker = "0.1"
sqlparser = { version = "0.58", features = ["visitor"] }
syn = { version = "2", features = ["full", "visit", "extra-traits"] }
tempfile = "3"
toml = "0.9"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,17 @@ pub(crate) fn normalize_analysis_sql(
normalize(sql, dialect, &stubs, &placeholders).map(|mapped| mapped.sql)
}

pub(crate) fn normalize_dialect_sql_mapped(
sql: &str,
dialect: &str,
) -> Result<(String, Vec<usize>), String> {
let input = MappedSql::new(sql);
if !dialect.eq_ignore_ascii_case("snowflake") {
return Ok((input.sql, input.offsets));
}
normalize_variant(input).map(|mapped| (mapped.sql, mapped.offsets))
}

pub(crate) fn normalize_dialect_sql(sql: &str, dialect: &str) -> Result<String, String> {
if !dialect.eq_ignore_ascii_case("snowflake") {
return Ok(sql.to_owned());
Expand Down
1 change: 1 addition & 0 deletions crates/sqlbuild-analysis/src/semantic_validation/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub mod new_analysis_pool;
pub mod normalize;
pub mod normalize_batch;
pub mod normalize_dialect;
pub mod normalize_mapped;
use crate::semantic_validation::models::ProjectCatalog;

use polyglot_sql::{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
use crate::semantic_validation::models::MappedNormalization;

/// The dialect normalization analysis applies to `sql`, mapped back to its input characters.
pub fn normalize_mapped(sql: &str, dialect: &str) -> Result<MappedNormalization, String> {
crate::semantic_validation::_helpers::normalization::normalize_dialect_sql_mapped(sql, dialect)
.map(|(sql, offsets)| MappedNormalization { sql, offsets })
}
7 changes: 7 additions & 0 deletions crates/sqlbuild-analysis/src/semantic_validation/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ pub struct NormalizationInput {
pub placeholders: HashMap<String, String>,
}

/// Normalized analysis SQL with, per normalized character, the input character it came from.
#[derive(Debug)]
pub struct MappedNormalization {
pub sql: String,
pub offsets: Vec<usize>,
}

#[derive(Debug)]
pub struct PositionInput {
pub authored: String,
Expand Down
1 change: 0 additions & 1 deletion crates/sqlbuild-rules/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ sqlbuild-core.workspace = true
sqlbuild-scopes.workspace = true
sqlbuild-sqltext.workspace = true
stacker.workspace = true
sqlparser.workspace = true
toml.workspace = true

[build-dependencies]
Expand Down
1 change: 0 additions & 1 deletion crates/sqlbuild-rules/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ pub(crate) const ENFORCED_CONTRACT: &str = "enforced";
pub(crate) const SELECT_STAR_MODEL_RULE_CODE: &str = "SQBRMODEL102";
pub(crate) const BOOLEAN_TYPE: &str = "BOOLEAN";
pub(crate) const DATE_TYPE: &str = "DATE";
pub(crate) const NEGATION_OPERATOR: &str = "-";
pub(crate) const DECLARATION_DOMAIN_COMPONENTS: usize = 3;
pub(crate) const RULE_CODE_NUMBER_LENGTH: usize = 3;
pub(crate) const EMPTY_FIXTURE_QUERY_TOKEN_COUNT: usize = 6;
Expand Down
4 changes: 3 additions & 1 deletion crates/sqlbuild-rules/src/engine/_helpers/evaluation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::rules::main::{
resolve_threshold_overrides, select, sql_test_coverage,
};
use crate::rules::models::{
ModelEvaluationRequest, ProjectEvaluationRequest, ResolvedThresholdOverride,
ModelEvaluationRequest, ProjectEvaluationRequest, QueryShapes, ResolvedThresholdOverride,
};
use fensu_policy::apply_suppressions;
use fensu_policy::lifecycle::errors::LifecycleError;
Expand Down Expand Up @@ -188,6 +188,7 @@ fn evaluate_pending_models(
pending_models: Vec<PendingModelRule<'_>>,
context: ModelRuleBatchContext<'_>,
) -> Result<Vec<CompletedModelRule>, String> {
let shapes = QueryShapes::default();
let evaluate_pending = |pending: PendingModelRule<'_>| CompletedModelRule {
index: pending.index,
identity: pending.identity,
Expand All @@ -203,6 +204,7 @@ fn evaluate_pending_models(
selected: context.selected,
request: context.request,
threshold_overrides: context.threshold_overrides,
shapes: &shapes,
})
.and_then(|faults| scoped_faults(context.selected, faults, RuleScope::evaluates_models))
},
Expand Down
2 changes: 2 additions & 0 deletions crates/sqlbuild-rules/src/engine/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ mod evaluation;
mod helpers;
#[path = "tests/test_incremental_rules.rs"]
mod incremental_rules;
#[path = "tests/test_query_facts.rs"]
mod query_facts;
#[path = "tests/test_scope_facts.rs"]
mod scope_facts;
#[path = "tests/test_speculative_rows.rs"]
Expand Down
51 changes: 50 additions & 1 deletion crates/sqlbuild-rules/src/engine/tests/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use crate::models::{
use crate::engine::_helpers::evaluation::evaluate_json;
use crate::engine::_helpers::evaluation::evaluate_request as evaluate_parsed;
use crate::engine::_helpers::evaluation::parse_parts;
use crate::engine::tests::test_types;

pub(crate) fn request(project_dir: &TempDir, config: &Value) -> String {
json!({
Expand Down Expand Up @@ -327,11 +328,29 @@ pub(crate) fn empty_input_rule_evaluation(
tests: Value,
allowed_tests: Value,
cache_enabled: bool,
) -> Result<Vec<String>, String> {
dialect_empty_input_rule_evaluation(
"duckdb",
project_dir,
select,
tests,
allowed_tests,
cache_enabled,
)
}

pub(crate) fn dialect_empty_input_rule_evaluation(
dialect: &str,
project_dir: &TempDir,
select: Value,
tests: Value,
allowed_tests: Value,
cache_enabled: bool,
) -> Result<Vec<String>, String> {
let request = json!({
"version": 1,
"project_dir": project_dir.path(),
"dialect": "duckdb",
"dialect": dialect,
"config": {
"select": select,
"thresholds": {"min_tests_per_model": 1},
Expand Down Expand Up @@ -588,3 +607,33 @@ pub(crate) fn drop_at_once(
drop(speculative);
Ok(false)
}
pub(crate) fn query_facts_faults(
test_case: &test_types::QueryFactsTestCase,
) -> Result<Vec<(String, u64, u64, String)>, String> {
let project_dir = TempDir::new().map_err(|error| error.to_string())?;
let config = json!({"select": test_case.select, "cache": {"enabled": false}});
let mut request: Value =
serde_json::from_str(&request(&project_dir, &config)).map_err(|error| error.to_string())?;
request["dialect"] = json!(test_case.dialect);
request["models"][0]["query_sql"] = json!(test_case.query_sql);
request["models"][0]["authored_sql"] = json!(test_case.query_sql);
request["models"][0]["enum_columns"] = json!(test_case.enum_columns);
request["models"][0]["config"] = json!({"contract": "enforced"});
request["models"][0]["columns"] = test_case.columns.clone();
let result: Value = serde_json::from_str(&evaluate_json(&request.to_string())?)
.map_err(|error| error.to_string())?;
let faults = result["faults"]
.as_array()
.ok_or("faults must be an array")?;
Ok(faults
.iter()
.map(|fault| {
(
fault["code"].as_str().unwrap_or_default().to_owned(),
fault["line"].as_u64().unwrap_or_default(),
fault["column"].as_u64().unwrap_or_default(),
fault["message"].as_str().unwrap_or_default().to_owned(),
)
})
.collect())
}
115 changes: 114 additions & 1 deletion crates/sqlbuild-rules/src/engine/tests/test_empty_input_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,34 @@ fn given_sql_test_shapes_when_evaluating_empty_input_rule_then_flags_only_filler
allowed_tests: json!([]),
expected_messages: &[FLAGGED_MESSAGE],
},
test_types::EmptyInputTestRuleTestCase {
description: "a cast to a parameterized type under a false predicate is an empty mock",
test: helpers::empty_input_test_fact(
"orders__empty_inputs_produce_no_rows",
&[(
"__source__raw_orders",
"SELECT CAST(NULL AS DECIMAL(18, 2)) AS amount WHERE 1 = 0",
)],
&[],
&[NO_ROWS_ASSERTION],
),
allowed_tests: json!([]),
expected_messages: &[FLAGGED_MESSAGE],
},
test_types::EmptyInputTestRuleTestCase {
description: "a cast to a parameterized type keeps a bare existence check",
test: helpers::empty_input_test_fact(
"orders__empty_inputs_produce_no_rows",
&[EMPTY_ORDERS],
&[],
&[(
"__assert__empty_inputs_produce_no_rows",
"SELECT CAST(order_id AS VARCHAR(10)) AS unexpected_row FROM __ref(\"orders\")",
)],
),
allowed_tests: json!([]),
expected_messages: &[FLAGGED_MESSAGE],
},
test_types::EmptyInputTestRuleTestCase {
description: "input rows with expected output pass",
test: helpers::empty_input_test_fact(
Expand Down Expand Up @@ -202,7 +230,7 @@ fn given_sql_test_shapes_when_evaluating_empty_input_rule_then_flags_only_filler
),
allowed_tests: json!([]),
expected_messages: &[
"SQBRTEST203 tests/unit/test_orders__empty_inputs_produce_no_rows.sql: Rule SQBRTEST203 could not be evaluated for test block 1: could not parse SQL-test fixture: sql parser error: sql parser error: Expected: an expression, found: EOF; generic fallback also failed: sql parser error: Expected: an expression, found: EOF",
"SQBRTEST203 tests/unit/test_orders__empty_inputs_produce_no_rows.sql: Rule SQBRTEST203 could not be evaluated for test block 1: could not parse SQL-test fixture: Parse error at line 1, column 42: Unexpected token: Limit",
],
},
test_types::EmptyInputTestRuleTestCase {
Expand Down Expand Up @@ -389,3 +417,88 @@ fn given_cached_minimum_tests_when_filler_becomes_real_test_then_model_cache_is_
}
Ok(())
}

#[test]
fn given_sql_server_conversions_when_evaluating_empty_input_rule_then_type_arguments_are_not_calls()
-> Result<(), String> {
let project_dir = TempDir::new().map_err(|error| error.to_string())?;
let empty_orders = (
"__source__raw_orders",
"SELECT NULL AS order_id WHERE 1 = 0",
);
let test_cases = [
test_types::EmptyInputTestRuleTestCase {
description: "a CONVERT to a parameterized type under a false predicate is an empty mock",
test: helpers::empty_input_test_fact(
"orders__empty_inputs_produce_no_rows",
&[(
"__source__raw_orders",
"SELECT CONVERT(DECIMAL(18, 2), NULL) AS amount WHERE 1 = 0",
)],
&[],
&[NO_ROWS_ASSERTION],
),
allowed_tests: json!([]),
expected_messages: &[FLAGGED_MESSAGE],
},
test_types::EmptyInputTestRuleTestCase {
description: "a CONVERT with a style argument is an empty mock",
test: helpers::empty_input_test_fact(
"orders__empty_inputs_produce_no_rows",
&[(
"__source__raw_orders",
"SELECT CONVERT(NUMERIC(10, 2), NULL, 1) AS amount WHERE 1 = 0",
)],
&[],
&[NO_ROWS_ASSERTION],
),
allowed_tests: json!([]),
expected_messages: &[FLAGGED_MESSAGE],
},
test_types::EmptyInputTestRuleTestCase {
description: "a TRY_CONVERT to a parameterized type keeps a bare existence check",
test: helpers::empty_input_test_fact(
"orders__empty_inputs_produce_no_rows",
&[empty_orders],
&[],
&[(
"__assert__empty_inputs_produce_no_rows",
"SELECT TRY_CONVERT(VARCHAR(10), order_id) AS unexpected_row FROM __ref(\"orders\")",
)],
),
allowed_tests: json!([]),
expected_messages: &[FLAGGED_MESSAGE],
},
test_types::EmptyInputTestRuleTestCase {
description: "a function call in a CONVERT value argument still counts",
test: helpers::empty_input_test_fact(
"orders__empty_inputs_produce_no_rows",
&[(
"__source__raw_orders",
"SELECT CONVERT(VARCHAR(10), COUNT(*)) AS amount WHERE 1 = 0",
)],
&[],
&[NO_ROWS_ASSERTION],
),
allowed_tests: json!([]),
expected_messages: &[],
},
];

for test_case in test_cases {
let messages = helpers::dialect_empty_input_rule_evaluation(
"mssql",
&project_dir,
json!(["SQBRTEST203"]),
json!([test_case.test]),
test_case.allowed_tests,
false,
)?;
assert_eq!(
messages, test_case.expected_messages,
"{}",
test_case.description
);
}
Ok(())
}
Loading
Loading