diff --git a/crates/sqlquack-core/src/diagnostics/constants.rs b/crates/sqlquack-core/src/diagnostics/constants.rs index 1757bdcd4..a6d95d832 100644 --- a/crates/sqlquack-core/src/diagnostics/constants.rs +++ b/crates/sqlquack-core/src/diagnostics/constants.rs @@ -22,6 +22,11 @@ pub const ZONED_LOCAL_TIME_NOTE: &str = "a Snowflake TIMESTAMP_TZ stores each va pub const ZONED_LOCAL_TIME_HELP: &str = "convert the value to a wall time first: \ `CONVERT_TIMEZONE('UTC', col)::TIMESTAMP_NTZ` for UTC, or \ `CONVERT_TIMEZONE(, col)::TIMESTAMP_NTZ` for local day boundaries in a known zone"; +/// The delimiters of a dbt Jinja expression or statement, which Snowflake never parses. +pub const JINJA_MARKERS: [&str; 2] = ["{{", "{%"]; +/// The help for SQL that does not parse where it holds Jinja: it was pasted before dbt compiled it. +pub const JINJA_HELP: &str = "this looks like uncompiled dbt Jinja; run `dbt compile` and check \ + the SQL it writes under target/compiled"; /// The prefix of a complexity guard's message, before the rest of its `E_GUARD_*` name. pub const GUARD_PREFIX: &str = "E_GUARD_"; /// The longest source line shown in full; longer lines show a window around the span. @@ -62,7 +67,8 @@ pub const CODES: &[CodeEntry] = &[ converted. The span points where the parser stopped and the label carries the \ parser's message. Check the SQL at that point against Snowflake's grammar; if \ Snowflake accepts the statement, SQLQuack's parser is missing the syntax and the \ - statement should be reported with the label.", + statement should be reported with the label. Where the line holds dbt Jinja, the help says \ + to compile the model with dbt first, since Snowflake does not parse Jinja either.", example: r#"echo "SELECT a, FROM orders WHERE (b = 1" | sqlquack check"#, }, CodeEntry { diff --git a/crates/sqlquack-core/src/diagnostics/main/diagnose.rs b/crates/sqlquack-core/src/diagnostics/main/diagnose.rs index 200f1c349..aec20d345 100644 --- a/crates/sqlquack-core/src/diagnostics/main/diagnose.rs +++ b/crates/sqlquack-core/src/diagnostics/main/diagnose.rs @@ -6,8 +6,9 @@ use crate::diagnostics::_helpers::coded::coded; use crate::diagnostics::_helpers::line_index::LineIndex; use crate::diagnostics::constants::{ CASE_ONLY_MATCH, CASE_ONLY_MATCH_HELP, CONVERSION_BUDGET, DIVERGENCE_REFUSED, GUARD_PREFIX, - MISSING_RULE, NOT_EXPRESSIBLE, PARSE_FIDELITY, TYPE_CONDITION, UNPARSED_PREFIX, - UNRESOLVED_NAME, ZONED_LOCAL_TIME, ZONED_LOCAL_TIME_HELP, ZONED_LOCAL_TIME_NOTE, + JINJA_HELP, JINJA_MARKERS, MISSING_RULE, NOT_EXPRESSIBLE, PARSE_FIDELITY, TYPE_CONDITION, + UNPARSED_PREFIX, UNRESOLVED_NAME, ZONED_LOCAL_TIME, ZONED_LOCAL_TIME_HELP, + ZONED_LOCAL_TIME_NOTE, }; use crate::diagnostics::models::Diagnostic; use crate::refusals::models::{Refusal, RefusalCategory}; @@ -25,6 +26,9 @@ pub fn diagnose(refusals: &[Refusal], source: &str) -> Vec { { diagnostic.help = Some(CASE_ONLY_MATCH_HELP.to_owned()); } + if refusal.category == RefusalCategory::ParseFidelity && holds_jinja(refusal, source) { + diagnostic.help = Some(JINJA_HELP.to_owned()); + } if refusal.category == RefusalCategory::TypeCondition && refusal.reason.contains(ZONED_LOCAL_TIME) { @@ -44,6 +48,15 @@ pub fn diagnose(refusals: &[Refusal], source: &str) -> Vec { diagnostics } +/// Whether the line the parser stopped on, or the whole source without a location, holds Jinja. +fn holds_jinja(refusal: &Refusal, source: &str) -> bool { + let text = refusal + .location + .and_then(|location| source.lines().nth(location.line.saturating_sub(1))) + .unwrap_or(source); + JINJA_MARKERS.iter().any(|marker| text.contains(marker)) +} + /// The code a refusal maps to, and the label left once its code's own wording is removed. fn code_and_label(refusal: &Refusal) -> (&'static str, String) { let code = match refusal.category { diff --git a/crates/sqlquack/tests/cli/test_convert.rs b/crates/sqlquack/tests/cli/test_convert.rs index 304766dd3..44a0295e4 100644 --- a/crates/sqlquack/tests/cli/test_convert.rs +++ b/crates/sqlquack/tests/cli/test_convert.rs @@ -1724,6 +1724,34 @@ fn given_a_refused_divergence_when_converting_then_its_help_names_it_and_only_fi } } +#[test] +fn given_uncompiled_dbt_jinja_when_converting_then_the_help_points_to_dbt_compile() { + let test_cases = [ + DivergenceHelpTestCase { + description: "a pasted model with Jinja expressions", + sql: "select {{ dbt_utils.generate_surrogate_key(['order_id']) }} as k \ + from {{ ref('stg_orders') }}", + flags: &[], + expected_help: "this looks like uncompiled dbt Jinja; run `dbt compile` and check the \ + SQL it writes under target/compiled", + }, + DivergenceHelpTestCase { + description: "a parse failure without Jinja keeps no help", + sql: "SELECT a, FROM orders WHERE (b = 1", + flags: &[], + expected_help: "", + }, + ]; + for test_case in test_cases { + assert_eq!( + first_refusal_help(test_case.sql, test_case.flags), + test_case.expected_help, + "{}", + test_case.description + ); + } +} + #[test] fn given_a_malformed_schema_when_converting_then_the_error_names_what_is_wrong_in_schema_terms() { let test_cases = [ diff --git a/website/src/content/docs/docs/diagnostics/reference.mdx b/website/src/content/docs/docs/diagnostics/reference.mdx index 4b3d01460..19573112d 100644 --- a/website/src/content/docs/docs/diagnostics/reference.mdx +++ b/website/src/content/docs/docs/diagnostics/reference.mdx @@ -42,7 +42,7 @@ Why a statement was not converted. **the SQL does not parse** (`parse_fidelity`) -SQLQuack's Snowflake parser could not read the SQL, so nothing in it was converted. The span points where the parser stopped and the label carries the parser's message. Check the SQL at that point against Snowflake's grammar; if Snowflake accepts the statement, SQLQuack's parser is missing the syntax and the statement should be reported with the label. +SQLQuack's Snowflake parser could not read the SQL, so nothing in it was converted. The span points where the parser stopped and the label carries the parser's message. Check the SQL at that point against Snowflake's grammar; if Snowflake accepts the statement, SQLQuack's parser is missing the syntax and the statement should be reported with the label. Where the line holds dbt Jinja, the help says to compile the model with dbt first, since Snowflake does not parse Jinja either. **Example:**