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
4 changes: 4 additions & 0 deletions crates/sqlquack-semantics/src/bigquery/profile/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ impl DialectRules for BigQueryRules {
false
}

fn derived_tables_read_left_items(&self) -> bool {
false
}

fn as_of_keeps_unmatched_left(&self) -> bool {
false
}
Expand Down
4 changes: 4 additions & 0 deletions crates/sqlquack-semantics/src/clickhouse/profile/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ impl DialectRules for ClickHouseRules {
false
}

fn derived_tables_read_left_items(&self) -> bool {
false
}

fn as_of_keeps_unmatched_left(&self) -> bool {
false
}
Expand Down
4 changes: 4 additions & 0 deletions crates/sqlquack-semantics/src/databricks/profile/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ impl DialectRules for DatabricksRules {
true
}

fn derived_tables_read_left_items(&self) -> bool {
false
}

fn as_of_keeps_unmatched_left(&self) -> bool {
false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -829,6 +829,8 @@ julian,scalar,1,false
kahan_sum,aggregate,1,false
kurtosis,aggregate,1,false
kurtosis_pop,aggregate,1,false
lag,window,1,false
lag,window,2,false
lag,window,3,false
lance_fts,table,,false
lance_hybrid_search,table,,false
Expand All @@ -838,6 +840,8 @@ last_day,scalar,1,false
last_value,window,1,false
lcase,scalar,1,false
lcm,scalar,2,false
lead,window,1,false
lead,window,2,false
lead,window,3,false
least,scalar,1,true
least_common_multiple,scalar,2,false
Expand Down
4 changes: 4 additions & 0 deletions crates/sqlquack-semantics/src/duckdb/profile/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ impl DialectRules for DuckDbRules {
false
}

fn derived_tables_read_left_items(&self) -> bool {
true
}

fn as_of_keeps_unmatched_left(&self) -> bool {
false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ use crate::lineage::tests::test_types::QueryLineageTestCase;
#[test]
fn given_a_query_when_tracing_lineage_then_each_output_names_its_sources_and_transform() {
let test_cases = [
QueryLineageTestCase {
description: "a derived table without LATERAL reads the item before it, as DuckDB runs it",
sql: "SELECT k.n FROM orders AS p, (SELECT p.status AS n) AS k",
dialect: Dialect::DuckDB,
profile: &DUCKDB,
expected_outputs: &[("n", TransformKind::Direct, &["orders.status"])],
expected_open_star: false,
},
QueryLineageTestCase {
description: "DuckDB names a pivoted column by its value's text, without quotes",
sql: "SELECT * FROM (SELECT customer_id, status, amount FROM orders) PIVOT (SUM(amount) FOR status IN ('open', 'shipped'))",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -419,3 +419,232 @@ fn given_duckdb_syntax_its_parser_emits_as_calls_when_checking_then_the_forms_du
);
}
}

#[test]
fn given_from_items_that_read_each_other_when_resolving_names_then_each_reads_only_the_items_before_it()
{
// Each verdict is DuckDB 2.0.0's, which also runs a derived table without LATERAL as lateral.
let test_cases = [
QueryFindingsTestCase {
description: "a comma-joined LATERAL whose nested derived table reads the item before it",
sql: "SELECT k.n FROM orders AS p, \
LATERAL (SELECT f.u AS n FROM (SELECT UNNEST(p.items) AS u) AS f) AS k",
expected_findings: &[],
},
QueryFindingsTestCase {
description: "the same body under CROSS JOIN LATERAL",
sql: "SELECT k.n FROM orders AS p \
CROSS JOIN LATERAL (SELECT f.u AS n FROM (SELECT UNNEST(p.items) AS u) AS f) AS k",
expected_findings: &[],
},
QueryFindingsTestCase {
description: "a scalar subquery whose derived table reads the outer query",
sql: "SELECT (SELECT f.u FROM (SELECT p.items AS u) AS f) AS n FROM orders AS p",
expected_findings: &[],
},
QueryFindingsTestCase {
description: "a LATERAL body reading the item after it",
sql: "SELECT k.n FROM orders AS p, LATERAL (SELECT q.id AS n) AS k, orders AS q",
expected_findings: &[(
"E222",
"Unknown table or alias 'q' referenced by column 'id'",
45,
)],
},
QueryFindingsTestCase {
description: "a derived table nested in a LATERAL reading the item after it",
sql: "SELECT k.n FROM orders AS p, \
LATERAL (SELECT f.u AS n FROM (SELECT q.id AS u) AS f) AS k, orders AS q",
expected_findings: &[(
"E222",
"Unknown table or alias 'q' referenced by column 'id'",
67,
)],
},
QueryFindingsTestCase {
description: "a LATERAL body reading itself",
sql: "SELECT k.n FROM orders AS p, LATERAL (SELECT k.n AS n) AS k",
expected_findings: &[(
"E222",
"Unknown table or alias 'k' referenced by column 'n'",
45,
)],
},
QueryFindingsTestCase {
description: "a derived table without LATERAL reading the item before it",
sql: "SELECT k.n FROM orders AS p, (SELECT p.items AS n) AS k",
expected_findings: &[],
},
QueryFindingsTestCase {
description: "a derived table nested in one without LATERAL reading the item before it",
sql: "SELECT k.n FROM orders AS p CROSS JOIN (SELECT f.u AS n FROM (SELECT p.id AS u) AS f) AS k",
expected_findings: &[],
},
QueryFindingsTestCase {
description: "a derived table without LATERAL reading the item after it",
sql: "SELECT k.n FROM orders AS p, (SELECT q.id AS n) AS k, orders AS q",
expected_findings: &[(
"E222",
"Unknown table or alias 'q' referenced by column 'id'",
37,
)],
},
QueryFindingsTestCase {
description: "a derived table without LATERAL after LEFT JOIN reading the item before it",
sql: "SELECT k.n FROM orders AS p LEFT JOIN (SELECT p.id AS n) AS k ON TRUE",
expected_findings: &[],
},
QueryFindingsTestCase {
description: "a derived table without LATERAL after RIGHT JOIN, which DuckDB never runs as lateral",
sql: "SELECT k.n FROM orders AS p RIGHT JOIN (SELECT p.id AS n) AS k ON TRUE",
expected_findings: &[(
"E222",
"Unknown table or alias 'p' referenced by column 'id'",
47,
)],
},
QueryFindingsTestCase {
description: "a derived table without LATERAL after FULL JOIN, which DuckDB never runs as lateral",
sql: "SELECT k.n FROM orders AS p FULL JOIN (SELECT p.id AS n) AS k ON TRUE",
expected_findings: &[(
"E222",
"Unknown table or alias 'p' referenced by column 'id'",
46,
)],
},
];
for test_case in test_cases {
assert_eq!(
duckdb_parser_findings(test_case.sql),
owned_findings(test_case.expected_findings),
"{}",
test_case.description
);
}
}

#[test]
fn given_a_lambda_in_a_grouped_select_when_checking_grouping_then_only_outer_columns_must_be_grouped()
{
// Each verdict is DuckDB 2.0.0's.
let test_cases = [
QueryFindingsTestCase {
description: "a lambda parameter, which is no column",
sql: "SELECT o.customer_id, len(list_filter(ARRAY_AGG(o.status), lambda e: e <> \
'null')) AS c FROM orders AS o GROUP BY o.customer_id",
expected_findings: &[],
},
QueryFindingsTestCase {
description: "a lambda parameter cast over a JSON list of an aggregate",
sql: "SELECT o.customer_id, len(list_filter(CAST(to_json(ARRAY_AGG(o.status)) AS \
JSON[]), lambda e: CAST(e AS VARCHAR) <> 'null')) AS c FROM orders AS o GROUP BY \
o.customer_id",
expected_findings: &[],
},
QueryFindingsTestCase {
description: "a grouped outer column inside the lambda body",
sql: "SELECT o.customer_id, len(list_filter(ARRAY_AGG(o.status), lambda e: e <> \
CAST(o.customer_id AS VARCHAR))) AS c FROM orders AS o GROUP BY o.customer_id",
expected_findings: &[],
},
QueryFindingsTestCase {
description: "an ungrouped outer column inside the lambda body",
sql: "SELECT o.customer_id, len(list_filter(ARRAY_AGG(o.status), lambda e: e <> \
o.status)) AS c FROM orders AS o GROUP BY o.customer_id",
expected_findings: &[(
"E230",
"Column must be grouped or used in an aggregate function",
76,
)],
},
QueryFindingsTestCase {
description: "a parameter's name read outside its lambda is a column",
sql: "SELECT o.customer_id, concat(list_filter(ARRAY_AGG(o.status), lambda status: \
status <> 'x')[1], status) AS c FROM orders AS o GROUP BY o.customer_id",
expected_findings: &[(
"E230",
"Column must be grouped or used in an aggregate function",
96,
)],
},
QueryFindingsTestCase {
description: "a lambda field the FROM item of that name also has is that item's column",
sql: "SELECT list_transform([{'amount': 99}], lambda o: o.amount) AS v FROM orders AS o GROUP BY o.status",
expected_findings: &[(
"E230",
"Column must be grouped or used in an aggregate function",
52,
)],
},
QueryFindingsTestCase {
description: "a lambda field only the parameter has",
sql: "SELECT list_transform([{'m': 99}], lambda o: o.m) AS v FROM orders AS o GROUP BY o.status",
expected_findings: &[],
},
QueryFindingsTestCase {
description: "a JSON arrow is no lambda, so its column must be grouped",
sql: "SELECT o.items -> o.status AS v FROM orders AS o GROUP BY o.status",
expected_findings: &[(
"E230",
"Column must be grouped or used in an aggregate function",
9,
)],
},
];
for test_case in test_cases {
assert_eq!(
duckdb_parser_findings(test_case.sql),
owned_findings(test_case.expected_findings),
"{}",
test_case.description
);
}
}

#[test]
fn given_duckdbs_parser_reads_window_calls_when_checking_arity_then_every_count_duckdb_binds_is_known()
{
// Each verdict is DuckDB 2.0.0's: LAG and LEAD bind one to three arguments, not four.
let test_cases = [
QueryFindingsTestCase {
description: "LAG and LEAD with one, two and three arguments",
sql: "SELECT LAG(amount) OVER (ORDER BY id) AS a, LAG(amount, 2) OVER (ORDER BY id) AS b, \
LAG(amount, 1, 0) OVER (ORDER BY id) AS c, LEAD(amount) OVER (ORDER BY id) AS d, \
LEAD(amount, 1) OVER (ORDER BY id) AS e FROM orders",
expected_findings: &[],
},
QueryFindingsTestCase {
description: "a short LEAD inside QUALIFY, COALESCE and CASE",
sql: "SELECT COALESCE(LEAD(amount) OVER (ORDER BY id), 0) AS a, \
CASE WHEN LAG(status) OVER (ORDER BY id) IS NULL THEN 1 END AS b \
FROM orders QUALIFY LAG(amount) OVER (ORDER BY id) > 1",
expected_findings: &[],
},
QueryFindingsTestCase {
description: "the other window functions at the counts DuckDB binds",
sql: "SELECT NTH_VALUE(amount, 2) OVER (ORDER BY id) AS a, NTILE(4) OVER (ORDER BY id) AS b, \
FIRST_VALUE(amount) OVER (ORDER BY id) AS c, LAST_VALUE(amount) OVER (ORDER BY id) AS d, \
PERCENT_RANK() OVER (ORDER BY id) AS e, CUME_DIST() OVER (ORDER BY id) AS f, \
ROW_NUMBER() OVER () AS g, RANK() OVER (ORDER BY id) AS h, \
DENSE_RANK() OVER (ORDER BY id) AS i FROM orders",
expected_findings: &[],
},
QueryFindingsTestCase {
description: "LAG with four arguments is still refused",
sql: "SELECT LAG(amount, 1, 0, 2) OVER (ORDER BY id) AS a FROM orders",
expected_findings: &[(
"E203",
"Invalid arity for function 'LAG': got 4, expected 1, 2, 3",
11,
)],
},
];
for test_case in test_cases {
assert_eq!(
duckdb_parser_findings(test_case.sql),
owned_findings(test_case.expected_findings),
"{}",
test_case.description
);
}
}
Loading
Loading