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
22 changes: 19 additions & 3 deletions crates/sqlbuild-rules/src/lint_text/tests/test_lint_text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ const CROSS_JOIN_FILE: &str = "select o.order_id\nfrom orders as o\n-- sqb: igno
const NORMALISE_NAME_MACRO: &str = "MACRO (\n description \"Normalise a name\",\n arguments (column_name VARCHAR),\n returns VARCHAR,\n);\n\nREGEXP_REPLACE(\n TRIM(UPPER(\n -- Collapse repeated spaces\n REGEXP_REPLACE(\n @column_name,\n '[[:space:]]+',\n ' '\n ))),\n '(^| )([A-Z]) ',\n '\\\\1\\\\2'\n)\n";
const ESCAPED_QUOTE_MACRO: &str = "MACRO (arguments (v VARCHAR), returns VARCHAR);\n\nCONCAT(\n @v,\n 'O\\'Brien\n kept indent',\n 'tail'\n)\n";
const ESCAPED_QUOTE_FORMATTED: &str = "MACRO (arguments (v VARCHAR), returns VARCHAR);\n\nCONCAT(@v, 'O\\'Brien\n kept indent', 'tail')\n";
const E_STRING_MACRO: &str = "MACRO (arguments (v VARCHAR), returns VARCHAR);\n\nCONCAT(\n @v,\n E'O\\'Brien\n kept indent',\n 'tail'\n)\n";
const E_STRING_FORMATTED: &str = "MACRO (arguments (v VARCHAR), returns VARCHAR);\n\nCONCAT(@v, E'O\\'Brien\n kept indent', 'tail')\n";
const NESTED_COMMENT_MACRO: &str = "MACRO (arguments (v VARCHAR), returns VARCHAR);\n\nCOALESCE(\n @v, /* outer /* inner */ it's\n kept */\n 'x'\n)\n";
const NESTED_COMMENT_FORMATTED: &str = "MACRO (arguments (v VARCHAR), returns VARCHAR);\n\nCOALESCE(\n @v, /* outer /* inner */ it's\n kept */ 'x'\n)\n";
const BLOCK_COMMENT_MACRO: &str = "MACRO (arguments (v VARCHAR), returns VARCHAR);\n\nCOALESCE(\n @v, /* first\n second */\n 'x'\n)\n";
const BLOCK_COMMENT_FORMATTED: &str = "MACRO (arguments (v VARCHAR), returns VARCHAR);\n\nCOALESCE(\n @v, /* first\n second */ 'x'\n)\n";
const NORMALISE_NAME_FORMATTED: &str = "MACRO (\n description \"Normalise a name\",\n arguments (column_name VARCHAR),\n returns VARCHAR,\n);\n\nREGEXP_REPLACE(\n TRIM(\n UPPER(\n -- Collapse repeated spaces\n REGEXP_REPLACE(@column_name, '[[:space:]]+', ' ')\n )\n ),\n '(^| )([A-Z]) ',\n '\\\\1\\\\2'\n)\n";
Expand Down Expand Up @@ -1011,22 +1015,34 @@ fn given_expression_macro_bodies_when_formatting_twice_then_every_dialect_lays_t
let test_cases = [
ExpressionMacroFormatTestCase {
description: "a body with a comment between nested calls",
dialects: &["duckdb", "snowflake"],
dialects: &["duckdb", "snowflake", "databricks"],
contents: NORMALISE_NAME_MACRO.to_string(),
expected_formatted: NORMALISE_NAME_FORMATTED.to_string(),
},
ExpressionMacroFormatTestCase {
description: "a body nesting fifteen calls, authored flush left",
dialects: &["duckdb", "snowflake"],
dialects: &["duckdb", "snowflake", "databricks"],
contents: nested_macro(15, ""),
expected_formatted: nested_macro(15, " "),
},
ExpressionMacroFormatTestCase {
description: "a multi-line string after a backslash-escaped quote",
dialects: &["snowflake"],
dialects: &["snowflake", "databricks"],
contents: ESCAPED_QUOTE_MACRO.to_string(),
expected_formatted: ESCAPED_QUOTE_FORMATTED.to_string(),
},
ExpressionMacroFormatTestCase {
description: "a multi-line E-string after a backslash-escaped quote",
dialects: &["duckdb"],
contents: E_STRING_MACRO.to_string(),
expected_formatted: E_STRING_FORMATTED.to_string(),
},
ExpressionMacroFormatTestCase {
description: "a nested block comment holding an apostrophe",
dialects: &["duckdb", "databricks"],
contents: NESTED_COMMENT_MACRO.to_string(),
expected_formatted: NESTED_COMMENT_FORMATTED.to_string(),
},
ExpressionMacroFormatTestCase {
description: "a block comment with indented lines",
dialects: &["duckdb", "snowflake"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ pub fn dialect_lexical_syntax(dialect: &str) -> LexicalSyntax {
line_comment_prefixes: lines(&["--", "# ", "#!", "//"]),
..LexicalSyntax::default()
},
"databricks" | "duckdb" => LexicalSyntax {
"databricks" => LexicalSyntax {
backslash_escape_quotes: strings(&["'", "\""]),
raw_string_prefix: true,
nested_block_comments: true,
line_comment_prefixes: lines(&["--"]),
..LexicalSyntax::default()
},
Expand Down
42 changes: 42 additions & 0 deletions crates/sqlbuild-sqltext/src/sql_scan/tests/test_dialect_scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,48 @@ fn given_dialect_lexical_syntax_when_scanning_non_code_then_dialect_decides_boun
syntax: single_quote_backslashes(),
expected_first_range: Some((0, 4)),
},
DialectFirstNonCodeRangeTestCase {
description: "a Databricks string continues past a backslash-escaped quote",
sql: r"'a\'b' x",
syntax: dialect_lexical_syntax("databricks"),
expected_first_range: Some((0, 6)),
},
DialectFirstNonCodeRangeTestCase {
description: "Databricks backslash escapes hold in double-quoted strings",
sql: r#""a\"b" x"#,
syntax: dialect_lexical_syntax("databricks"),
expected_first_range: Some((0, 6)),
},
DialectFirstNonCodeRangeTestCase {
description: "a Databricks raw string ends at the quote after a backslash",
sql: r"r'a\' x",
syntax: dialect_lexical_syntax("databricks"),
expected_first_range: Some((1, 5)),
},
DialectFirstNonCodeRangeTestCase {
description: "Databricks block comments nest",
sql: "/* a /* b */ c */ x",
syntax: dialect_lexical_syntax("databricks"),
expected_first_range: Some((0, 17)),
},
DialectFirstNonCodeRangeTestCase {
description: "a DuckDB E-string continues past a backslash-escaped quote",
sql: r"E'a\'b' x",
syntax: dialect_lexical_syntax("duckdb"),
expected_first_range: Some((1, 7)),
},
DialectFirstNonCodeRangeTestCase {
description: "a plain DuckDB string ends at the quote after a backslash",
sql: r"'a\' x",
syntax: dialect_lexical_syntax("duckdb"),
expected_first_range: Some((0, 4)),
},
DialectFirstNonCodeRangeTestCase {
description: "DuckDB block comments nest",
sql: "/* a /* b */ c */ x",
syntax: dialect_lexical_syntax("duckdb"),
expected_first_range: Some((0, 17)),
},
];
for test_case in test_cases {
let ranges: Vec<(usize, usize)> = dialect_non_code_ranges(test_case.sql, &test_case.syntax);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -924,13 +924,14 @@ class SqlReadabilityTestCase:

@dataclass(frozen=True)
class ExpressionMacroFormatTestCase:
"""A Snowflake expression macro in one location, and the file formatting leaves."""
"""An expression macro in one location, and the file formatting leaves on an adapter."""

description: str
macro_directory: str
consumer_folders: tuple[str, ...]
authored_macro: str
expected_macro: str
adapter: str = "snowflake"


@dataclass(frozen=True)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -348,13 +348,35 @@ def test_given_multibyte_text_before_restorations_when_formatting_then_output_is
_NORMALISE_NAME_HEADER
+ "CONCAT(@column_name, 'O\\'Brien\n kept indent', 'tail')\n",
),
ExpressionMacroFormatTestCase(
"Databricks multi-line string after a backslash-escaped quote",
"models/staging/_sqlbuild/_macros/sql",
("staging",),
_NORMALISE_NAME_HEADER
+ "CONCAT(\n @column_name,\n 'O\\'Brien\n kept indent',\n 'tail'\n)\n",
_NORMALISE_NAME_HEADER
+ "CONCAT(@column_name, 'O\\'Brien\n kept indent', 'tail')\n",
adapter="databricks",
),
ExpressionMacroFormatTestCase(
"DuckDB multi-line E-string after a backslash-escaped quote",
"models/staging/_sqlbuild/_macros/sql",
("staging",),
_NORMALISE_NAME_HEADER
+ "CONCAT(\n @column_name,\n E'O\\'Brien\n kept indent',\n 'tail'\n)\n",
_NORMALISE_NAME_HEADER
+ "CONCAT(@column_name, E'O\\'Brien\n kept indent', 'tail')\n",
adapter="duckdb",
),
],
ids=lambda case: case.description,
)
def test_given_snowflake_expression_macro_when_formatting_then_body_is_laid_out_once(
def test_given_expression_macro_when_formatting_then_body_is_laid_out_once(
test_case: ExpressionMacroFormatTestCase, tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
(tmp_path / "sqlbuild_project.toml").write_text('name = "orders"\nadapter = "snowflake"\n')
(tmp_path / "sqlbuild_project.toml").write_text(
f'name = "orders"\nadapter = "{test_case.adapter}"\n'
)
macro: Path = tmp_path / test_case.macro_directory / "normalise_name.sql"
macro.parent.mkdir(parents=True)
macro.write_text(test_case.authored_macro)
Expand Down
Loading