Skip to content

Commit df143dd

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix/translation-absent-forms
2 parents 0a4afe5 + d19c7a5 commit df143dd

72 files changed

Lines changed: 1876 additions & 102 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ A `VALUES` list of more than 1,000 rows is refused for now.
107107
session parameters yet and convert under `utc-v1`.
108108

109109
`convert`, `function`, `census` and `index` read `sqlquack.toml` (or `--policy`); without one they
110-
exit 2 unless `--builtin-policy` asks for the built-in default, which refuses every known divergence.
110+
use the built-in policy, which refuses every known divergence, as `--builtin-policy` asks for.
111111
They share the policy flags (`--policy`, `--builtin-policy`, `--strict`, `--utc-session`,
112112
`--confirm-declared-offsets`, `--python-runtime` and `--loud-divergences-only`, which relies only
113113
on divergences whose converted SQL raises rather than returns another value). Every command with a report takes
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# The names Snowflake gives unaliased ROUND and current-time items, as a playground query writes them.
2+
# The recorded column names are the receipts for naming them.
3+
category = "relational"
4+
construct = "node:alias"
5+
session_profile = "utc-v1"
6+
7+
[[case]]
8+
id = "unaliased_round"
9+
description = "Unaliased ROUND with one and two arguments, in upper and lower case and with inner spaces, over a NULL, a half and a negative."
10+
columns = [{ name = "n", type = "NUMBER(10,2)" }]
11+
rows = [
12+
["1.25"],
13+
["1.25"],
14+
["-2.55"],
15+
["NULL"],
16+
]
17+
query = "SELECT ROUND(n, 1), ROUND(n), round( n, 0 ) FROM input"
18+
order = "ordered"
19+
tags = ["null", "boundary", "duplicates"]
20+
21+
[[case]]
22+
id = "unaliased_round_no_rows"
23+
description = "Unaliased ROUND over no rows still names its columns."
24+
columns = [{ name = "n", type = "NUMBER(10,2)" }]
25+
rows = [
26+
["1.00"],
27+
]
28+
query = "SELECT ROUND(n, 1), ROUND(n) FROM input WHERE n > 5"
29+
order = "ordered"
30+
tags = ["empty-input"]
31+
32+
[[case]]
33+
id = "unaliased_current_time"
34+
description = "Unaliased CURRENT_DATE(), CURRENT_DATE and CURRENT_TIMESTAMP(): only the column names and types are the receipt; the values read the clock, so it is typing evidence."
35+
evidence = "typing"
36+
columns = [{ name = "n", type = "NUMBER(10,2)" }]
37+
rows = [
38+
["1.00"],
39+
]
40+
query = "SELECT CURRENT_DATE(), CURRENT_DATE, CURRENT_TIMESTAMP() FROM input"
41+
order = "ordered"
42+
tags = ["boundary"]
43+
44+
[[case]]
45+
id = "unaliased_distinct_aggregates"
46+
description = "Unaliased SUM, MAX and AVG over DISTINCT, over a duplicate and a NULL. The aggregates are one query and the row expressions another, since one select list cannot hold both without GROUP BY; each item is written as given."
47+
columns = [{ name = "n", type = "NUMBER(10,2)" }, { name = "g", type = "VARCHAR" }]
48+
rows = [
49+
["1.50", "'12'"],
50+
["1.50", "'12'"],
51+
["NULL", "NULL"],
52+
["-2.25", "'-3'"],
53+
]
54+
query = "SELECT SUM(DISTINCT n), MAX(DISTINCT n), AVG(DISTINCT n) FROM input"
55+
order = "ordered"
56+
tags = ["null", "duplicates"]
57+
58+
[[case]]
59+
id = "unaliased_distinct_aggregates_no_rows"
60+
description = "The unaliased DISTINCT aggregates over no rows still name their columns."
61+
columns = [{ name = "n", type = "NUMBER(10,2)" }, { name = "g", type = "VARCHAR" }]
62+
rows = [
63+
["1.00", "'1'"],
64+
]
65+
query = "SELECT SUM(DISTINCT n), MAX(DISTINCT n), AVG(DISTINCT n) FROM input WHERE n > 5"
66+
order = "ordered"
67+
tags = ["empty-input"]
68+
69+
[[case]]
70+
id = "unaliased_row_expressions"
71+
description = "Unaliased COALESCE with a column and with literals only, CAST of numeric text to INTEGER, and lower with a space before its closing parenthesis, over a duplicate and a NULL."
72+
columns = [{ name = "n", type = "NUMBER(10,2)" }, { name = "g", type = "VARCHAR" }]
73+
rows = [
74+
["1.50", "'12'"],
75+
["1.50", "'12'"],
76+
["NULL", "NULL"],
77+
["-2.25", "'-3'"],
78+
]
79+
query = "SELECT COALESCE(n, 0, 1), COALESCE(1, 2), CAST(g AS INTEGER), lower(g ) FROM input"
80+
order = "bag"
81+
tags = ["null", "duplicates"]
82+
83+
[[case]]
84+
id = "unaliased_row_expressions_no_rows"
85+
description = "The unaliased row expressions over no rows still name their columns."
86+
columns = [{ name = "n", type = "NUMBER(10,2)" }, { name = "g", type = "VARCHAR" }]
87+
rows = [
88+
["1.00", "'1'"],
89+
]
90+
query = "SELECT COALESCE(n, 0, 1), COALESCE(1, 2), CAST(g AS INTEGER), lower(g ) FROM input WHERE n > 5"
91+
order = "bag"
92+
tags = ["empty-input"]

‎coverage/translation/results-baseline.json‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6718,6 +6718,13 @@
67186718
"relational/unaliased_output_names.toml#unaliased_group_count": "matching",
67196719
"relational/unaliased_output_names.toml#unaliased_literals": "matching",
67206720
"relational/unaliased_output_names.toml#unaliased_over_no_rows": "matching",
6721+
"relational/unaliased_round_current.toml#unaliased_current_time": "shape_only",
6722+
"relational/unaliased_round_current.toml#unaliased_distinct_aggregates": "refused",
6723+
"relational/unaliased_round_current.toml#unaliased_distinct_aggregates_no_rows": "refused",
6724+
"relational/unaliased_round_current.toml#unaliased_round": "matching",
6725+
"relational/unaliased_round_current.toml#unaliased_round_no_rows": "matching",
6726+
"relational/unaliased_round_current.toml#unaliased_row_expressions": "matching",
6727+
"relational/unaliased_round_current.toml#unaliased_row_expressions_no_rows": "matching",
67216728
"relational/union_by_name.toml#union_all_by_name_reordered": "matching",
67226729
"relational/union_by_name.toml#union_by_name_case_insensitive": "matching",
67236730
"relational/union_by_name.toml#union_by_name_chain": "matching",

‎crates/sqlquack-core/src/diagnostics/constants.rs‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ pub const CODES: &[CodeEntry] = &[
155155
help: Some(
156156
"the help names the divergence and the fixes that work in the current mode: allow \
157157
it in the policy file, drop --strict or --loud-divergences-only, or confirm its \
158-
assumption; --builtin-policy refuses every one",
158+
assumption; the built-in policy refuses every one",
159159
),
160160
explanation: "The construct at the span converts only by relying on the known \
161161
divergence the label names, where DuckDB may raise or differ from Snowflake as the \
@@ -175,8 +175,9 @@ pub const CODES: &[CodeEntry] = &[
175175
a converted query must return the same column names. SQLQuack emits Snowflake's name \
176176
only for the shapes recorded results verify, named by their text upper-cased as \
177177
written: literals, columns and COUNT, SUM, MAX, AVG, LOWER, UPPER and COALESCE of \
178-
columns, a column plus, minus, times, divided by or concatenated with a literal, and \
179-
a column cast to INTEGER, written with single spaces and at most one line break. \
178+
columns, ROUND of a column, a column plus, minus, times, divided by or concatenated \
179+
with a literal, a column cast to INTEGER, and CURRENT_DATE, CURRENT_DATE() or \
180+
CURRENT_TIMESTAMP(), written with single spaces and at most one line break. \
180181
Other items, such as paths, CASE, nested calls or other functions, are refused \
181182
rather than converted with a different column name. An alias fixes the name in both \
182183
engines.",
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
//! Where lines break: before a token, and after one that asks for a break.
2+
3+
use crate::layout::_helpers::plan::Plan;
4+
use crate::layout::_helpers::tokens::{Token, TokenKind};
5+
use crate::layout::constants::{
6+
BY_CLAUSE_WORDS, CLAUSE_WORDS, INDENT, INLINE_WIDTH, JOIN_MODIFIERS, NON_CALL_WORDS,
7+
SELECT_MODIFIERS, SET_OPERATION_MODIFIERS, SET_OPERATIONS,
8+
};
9+
use crate::layout::models::{Case, Clause, Frame, Walk};
10+
11+
/// The walk over a statement, which breaks its lines when `broken`.
12+
pub(crate) fn start(broken: bool) -> Walk {
13+
Walk {
14+
frames: vec![Frame {
15+
query: true,
16+
broken,
17+
inline: !broken,
18+
call: false,
19+
base: 0,
20+
close: 0,
21+
clause: Clause::Other,
22+
item_pending: false,
23+
}],
24+
cases: Vec::new(),
25+
line_indent: 0,
26+
forced: None,
27+
laid_out: String::new(),
28+
}
29+
}
30+
31+
impl Walk {
32+
/// Appends `tokens[index]`, on a new line at `level` or after its own spacing.
33+
pub(crate) fn push_token(&mut self, tokens: &[Token<'_>], index: usize, level: Option<usize>) {
34+
let token = &tokens[index];
35+
match (index, level) {
36+
(0, _) => {}
37+
(_, Some(level)) => {
38+
self.laid_out.push('\n');
39+
self.laid_out.push_str(&INDENT.repeat(level));
40+
self.line_indent = level;
41+
}
42+
(_, None) if token.spaced => self.laid_out.push(' '),
43+
(_, None) => {}
44+
}
45+
self.laid_out.push_str(token.text);
46+
}
47+
48+
/// The indentation level of the new line `tokens[index]` starts, or `None` to keep its line.
49+
pub(crate) fn line_break(&mut self, tokens: &[Token<'_>], index: usize) -> Option<usize> {
50+
if let Some((_, level)) = self.forced.filter(|(at, _)| *at == index) {
51+
return Some(level);
52+
}
53+
let token = &tokens[index];
54+
let depth = self.frames.len();
55+
let case = self.cases.last().filter(|case| case.depth == depth);
56+
let frame = self.frames.last_mut()?;
57+
if token.is_symbol(")") {
58+
return (depth > 1 && frame.query && frame.broken).then_some(frame.close);
59+
}
60+
if frame.inline {
61+
return None;
62+
}
63+
if let Some(level) = case.and_then(|case| case_break(token, case)) {
64+
return Some(level);
65+
}
66+
if !frame.query || !frame.broken {
67+
return None;
68+
}
69+
if frame.item_pending {
70+
return frame.first_item_break(tokens, index);
71+
}
72+
(starts_clause(tokens, index) || is_set_operation(tokens, index)).then_some(frame.base)
73+
}
74+
75+
/// Opens or closes what `tokens[index]` opens or closes, and the break it asks for after it.
76+
pub(crate) fn after_token(
77+
&mut self,
78+
tokens: &[Token<'_>],
79+
index: usize,
80+
plan: &Plan,
81+
) -> Option<(usize, usize)> {
82+
let token = &tokens[index];
83+
match token {
84+
token if token.is_symbol("(") => self.open_frame(tokens, index, plan),
85+
token if token.is_symbol(")") => {
86+
if self.frames.len() > 1 {
87+
self.frames.pop();
88+
}
89+
None
90+
}
91+
token if token.is("CASE") => {
92+
let frame = self.frames.last()?;
93+
let broken = !frame.inline
94+
&& !frame.call
95+
&& plan.width(index, plan.case_end(index)) > INLINE_WIDTH;
96+
self.cases.push(Case {
97+
broken,
98+
indent: self.line_indent,
99+
depth: self.frames.len(),
100+
});
101+
None
102+
}
103+
token if token.is("END") => {
104+
self.cases.pop();
105+
None
106+
}
107+
token if token.kind == TokenKind::LineComment => Some((index + 1, self.line_indent)),
108+
_ => self.frames.last_mut()?.clause_break(tokens, index),
109+
}
110+
}
111+
112+
/// Opens the frame of the parenthesis at `tokens[index]`, breaking inside a long query.
113+
fn open_frame(
114+
&mut self,
115+
tokens: &[Token<'_>],
116+
index: usize,
117+
plan: &Plan,
118+
) -> Option<(usize, usize)> {
119+
let frame = self.frames.last()?;
120+
let query = plan.opens_query(tokens, index);
121+
let broken = query && !frame.inline && plan.width(index, plan.close(index)) > INLINE_WIDTH;
122+
let base = self.line_indent + 1;
123+
let opened = Frame {
124+
query,
125+
broken,
126+
inline: frame.inline || (query && !broken),
127+
call: !query && (frame.call || calls(tokens, index)),
128+
base,
129+
close: self.line_indent,
130+
clause: Clause::Other,
131+
item_pending: false,
132+
};
133+
self.frames.push(opened);
134+
broken.then_some((index + 1, base))
135+
}
136+
}
137+
138+
impl Frame {
139+
/// The line the select list's first item starts, after any `DISTINCT` or `DISTINCT ON (…)`.
140+
fn first_item_break(&mut self, tokens: &[Token<'_>], index: usize) -> Option<usize> {
141+
let token = &tokens[index];
142+
let modifier = SELECT_MODIFIERS.iter().any(|word| token.is(word))
143+
|| (token.is_symbol("(") && tokens[index - 1].is("ON"));
144+
if modifier {
145+
return None;
146+
}
147+
self.item_pending = false;
148+
Some(self.base + 1)
149+
}
150+
151+
/// The clause `tokens[index]` moves a broken query into, and the break its commas ask for.
152+
fn clause_break(&mut self, tokens: &[Token<'_>], index: usize) -> Option<(usize, usize)> {
153+
let token = &tokens[index];
154+
if !self.query || !self.broken {
155+
return None;
156+
}
157+
match token {
158+
token if token.is("SELECT") => {
159+
(self.clause, self.item_pending) = (Clause::Select, true);
160+
None
161+
}
162+
token if token.is("WITH") && self.clause == Clause::Other => {
163+
self.clause = Clause::With;
164+
None
165+
}
166+
token if token.is_symbol(",") => match self.clause {
167+
Clause::Select => Some((index + 1, self.base + 1)),
168+
Clause::With => Some((index + 1, self.base)),
169+
Clause::Other => None,
170+
},
171+
_ if is_set_operation(tokens, index) => {
172+
self.clause = Clause::Other;
173+
Some((after_modifiers(tokens, index), self.base))
174+
}
175+
_ if starts_clause(tokens, index) => {
176+
self.clause = Clause::Other;
177+
None
178+
}
179+
_ => None,
180+
}
181+
}
182+
}
183+
184+
/// The level of the line `token` starts within `case`, when the `CASE` breaks.
185+
fn case_break(token: &Token<'_>, case: &Case) -> Option<usize> {
186+
match case.broken {
187+
true if token.is("WHEN") || token.is("ELSE") => Some(case.indent + 1),
188+
true if token.is("END") => Some(case.indent),
189+
_ => None,
190+
}
191+
}
192+
193+
/// The index of the first token after the set operation at `index` and its modifiers.
194+
fn after_modifiers(tokens: &[Token<'_>], index: usize) -> usize {
195+
(index + 1..tokens.len())
196+
.find(|at| !is_set_operation_modifier(&tokens[*at]))
197+
.unwrap_or(tokens.len())
198+
}
199+
200+
/// Whether `token` is `ALL`, `DISTINCT`, `BY` or `NAME` after a set operation.
201+
fn is_set_operation_modifier(token: &Token<'_>) -> bool {
202+
SET_OPERATION_MODIFIERS.iter().any(|word| token.is(word))
203+
}
204+
205+
/// Whether `token` can come before `JOIN` in a join.
206+
fn is_join_modifier(token: &Token<'_>) -> bool {
207+
JOIN_MODIFIERS.iter().any(|word| token.is(word))
208+
}
209+
210+
/// Whether `tokens[index]` starts a clause: a clause word, `GROUP BY`, `ORDER BY` or a join.
211+
fn starts_clause(tokens: &[Token<'_>], index: usize) -> bool {
212+
let token = &tokens[index];
213+
let previous = index.checked_sub(1).map(|at| &tokens[at]);
214+
let next = tokens.get(index + 1);
215+
if token.is("FROM") && previous.is_some_and(|previous| previous.is("DISTINCT")) {
216+
return false;
217+
}
218+
let joins = (token.is("JOIN") || is_join_modifier(token))
219+
&& !previous.is_some_and(is_join_modifier)
220+
&& tokens[index..]
221+
.iter()
222+
.find(|token| !is_join_modifier(token))
223+
.is_some_and(|token| token.is("JOIN"));
224+
CLAUSE_WORDS.iter().any(|word| token.is(word))
225+
|| (BY_CLAUSE_WORDS.iter().any(|word| token.is(word))
226+
&& next.is_some_and(|next| next.is("BY")))
227+
|| joins
228+
}
229+
230+
/// Whether `tokens[index]` is a set operation, not the `EXCEPT` of a `* EXCEPT (…)` column list.
231+
fn is_set_operation(tokens: &[Token<'_>], index: usize) -> bool {
232+
SET_OPERATIONS.iter().any(|word| tokens[index].is(word))
233+
&& !(tokens[index].is("EXCEPT") && index > 0 && tokens[index - 1].is_symbol("*"))
234+
}
235+
236+
/// Whether the parenthesis at `tokens[index]` calls a function rather than grouping.
237+
fn calls(tokens: &[Token<'_>], index: usize) -> bool {
238+
let Some(previous) = index.checked_sub(1).map(|at| &tokens[at]) else {
239+
return false;
240+
};
241+
match previous.kind {
242+
TokenKind::Word => !NON_CALL_WORDS.iter().any(|word| previous.is(word)),
243+
TokenKind::Quoted => true,
244+
_ => previous.is_symbol("]") || previous.is_symbol(")"),
245+
}
246+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
//! Tokens, the facts about them, and where lines break.
2+
3+
pub(crate) mod breaks;
4+
pub(crate) mod plan;
5+
pub(crate) mod tokens;

0 commit comments

Comments
 (0)