Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
54 changes: 53 additions & 1 deletion corpora/polyglot-check/expected_differences.toml
Original file line number Diff line number Diff line change
Expand Up @@ -782,10 +782,52 @@ note = "Snowflake accepts a bare trailing LIMIT (recorded as accepted) and reads
[[rejection]]
id = "malformed_typed_type_text"
sql = "SELECT __polyglot_typed(amount, 'NOT A TYPE (') FROM orders"
dialects = ["Snowflake", "DuckDB"]
dialects = ["DuckDB"]
sqlquack_error = "Unexpected token after data type"
note = "malformed typed-expression type text errors instead of degrading to an ordinary call"

[[rejection]]
id = "snowflake_reserved_type_name"
sql = "SELECT __polyglot_typed(amount, 'NOT A TYPE (') FROM orders"
dialects = ["Snowflake"]
sqlquack_error = "Reserved word NOT cannot be an unquoted type name"
note = "the same malformed type text fails earlier in Snowflake, where NOT is reserved and cannot name a type"

[[rejection]]
id = "snowflake_cte_without_as"
sql = "WITH t (SELECT 1 AS c) SELECT c FROM t"
dialects = ["Snowflake"]
sqlquack_error = "Expected AS before the CTE body"
note = "Snowflake needs AS between a CTE's name and its body: it refuses WITH v1 ( SELECT 1 ) SELECT 1 with syntax error 1003 (case sf.query.with.select.k8b29420d51a4)"

[[rejection]]
id = "snowflake_cte_without_as_prose"
sql = " with the correct AST shape."
dialects = ["Snowflake"]
sqlquack_error = "Expected AS before the CTE body"
note = "prose read as a CTE without AS; polyglot fails here too, with another error"

[[rejection]]
id = "snowflake_reserved_cte_name"
sql = "WITH all AS (SELECT 1 AS count) SELECT all.count FROM all"
dialects = ["Snowflake"]
sqlquack_error = "Reserved word ALL cannot be an unquoted object name"
note = "ALL is a Snowflake reserved word, so it cannot name a CTE unquoted; Snowflake refuses WITH SELECT AS ( SELECT 1 ) SELECT 1 with syntax error 1003"

[[rejection]]
id = "snowflake_reserved_truncate_target"
sql = "TRUNCATE TABLE table"
dialects = ["Snowflake"]
sqlquack_error = "Reserved word TABLE cannot be an unquoted object name"
note = "TABLE is a Snowflake reserved word, so it cannot name a table unquoted"

[[rejection]]
id = "snowflake_empty_call_argument"
sql = "SELECT ROUND(SCALE => 1, EXPR => 2.25, , ROUNDING_MODE => 'HALF_TO_EVEN') AS value"
dialects = ["Snowflake"]
sqlquack_error = "Expected a function argument between commas"
note = "Snowflake refuses an empty argument between commas with syntax error 1003; polyglot drops it"

[[rejection]]
id = "snowflake_bare_values_cte"
sql = "WITH RECURSIVE T(n) AS (VALUES (1) UNION ALL SELECT n + 1 FROM t WHERE n < 100) SELECT SUM(n) FROM t"
Expand Down Expand Up @@ -895,6 +937,16 @@ id = "star_exclude_quoted_name"
marker = "EXCLUDE(\""
note = "SELECT * EXCLUDE (\"note\") excludes the case-sensitive column note, not NOTE; polyglot drops the quotes of a star's EXCLUDE names"

# Wrapper nodes SQLQuack keeps where polyglot drops them. The check counts an entry only when the
# statement, without whitespace and ignoring case, contains `marker`, and both trees are equal once
# every `{ "<node>": { "this": … } }` in SQLQuack's tree is replaced by its operand.

[[kept_node]]
id = "unary_plus"
marker = "+"
node = "pos"
note = "SELECT + 1 keeps its unary plus, a documented Snowflake operator; polyglot drops it and keeps only the operand; crates/sqlquack-syntax/tests/parser/test_fixes.rs pins the tree"

# Statements polyglot keeps as one opaque command where SQLQuack parses them into structured nodes.
# The check counts an entry only when polyglot's tree is a single command, SQLQuack's tree starts
# with `sqlquack_node`, and the statement, without whitespace and ignoring case, contains `marker`.
Expand Down
3 changes: 2 additions & 1 deletion crates/sqlquack-polyglot-check/src/cli/_helpers/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ pub(crate) fn report(label: &str, summary: &RunSummary, counts_only: bool) -> St
}
let _ = writeln!(
text,
"{label}: {} entries, {} same trees, {} same errors, {} fixed, {} rejected, {} kept quoting, {} structured, {} differences, {} without lossless \
"{label}: {} entries, {} same trees, {} same errors, {} fixed, {} rejected, {} kept quoting, {} kept nodes, {} structured, {} differences, {} without lossless \
tokens, {} cast type names unlike polyglot's, {} faithful calls kept, {} unlisted, {} \
unexplained, {} located parses unlike faithful; parse time polyglot {:.1} ms, sqlquack \
{:.1} ms",
Expand All @@ -35,6 +35,7 @@ pub(crate) fn report(label: &str, summary: &RunSummary, counts_only: bool) -> St
summary.fixed.values().sum::<usize>(),
summary.rejected.values().sum::<usize>(),
summary.kept_quoting.values().sum::<usize>(),
summary.kept_nodes.values().sum::<usize>(),
summary.structured.values().sum::<usize>(),
summary.differences.len(),
summary.tokens_lost.len(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub(crate) mod nodes;
pub(crate) mod quoting;
54 changes: 54 additions & 0 deletions crates/sqlquack-polyglot-check/src/expected/_helpers/nodes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//! Trees that differ only by a wrapper node SQLQuack keeps and polyglot drops.

use serde_json::Value;

use crate::expected::constants::{OK_PREFIX, THIS_KEY};

/// Whether both rendered parses are trees that are equal once SQLQuack's `node` wrappers go.
pub(crate) fn differ_only_by_kept_node(polyglot: &str, sqlquack: &str, node: &str) -> bool {
let (Some(theirs), Some(ours)) = (
polyglot.strip_prefix(OK_PREFIX),
sqlquack.strip_prefix(OK_PREFIX),
) else {
return false;
};
match (
serde_json::from_str::<Value>(theirs),
serde_json::from_str::<Value>(ours),
) {
(Ok(theirs), Ok(ours)) => theirs != ours && without_node(ours, node) == theirs,
(Err(_), _) | (_, Err(_)) => false,
}
}

/// `value` with every `{ node: { "this": inner } }` replaced by `inner`.
fn without_node(value: Value, node: &str) -> Value {
match value {
Value::Object(mut object) => {
let wrapped: bool = object.len() == 1
&& object
.get(node)
.and_then(Value::as_object)
.is_some_and(|inner| inner.len() == 1 && inner.contains_key(THIS_KEY));
match object.remove(node).filter(|_| wrapped) {
Some(Value::Object(mut inner)) => {
without_node(inner.remove(THIS_KEY).unwrap_or(Value::Null), node)
}
Some(other) => other,
None => Value::Object(
object
.into_iter()
.map(|(key, value)| (key, without_node(value, node)))
.collect(),
),
}
}
Value::Array(items) => Value::Array(
items
.into_iter()
.map(|value| without_node(value, node))
.collect(),
),
other => other,
}
}
2 changes: 2 additions & 0 deletions crates/sqlquack-polyglot-check/src/expected/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ pub const OK_PREFIX: &str = "ok:";
pub const COMMAND_TREE: &str = "[{\"command\":";
/// The field naming an identifier in a serialised tree.
pub const NAME_KEY: &str = "name";
/// The field holding the operand of a wrapper node such as unary plus.
pub const THIS_KEY: &str = "this";
/// The field recording whether an identifier was quoted.
pub const QUOTED_KEY: &str = "quoted";
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub fn load_expected(path: &Path) -> Result<ExpectedDifferences, String> {
fixes: file.fixes,
rejections: file.rejections,
kept_quotings: file.kept_quotings,
kept_nodes: file.kept_nodes,
structured: file.structured,
..ExpectedDifferences::default()
};
Expand Down
29 changes: 29 additions & 0 deletions crates/sqlquack-polyglot-check/src/expected/models.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Expected differences.

use crate::expected::_helpers::nodes::differ_only_by_kept_node;
use crate::expected::_helpers::quoting::{compact, differ_only_by_kept_quoting};
use crate::expected::constants::{COMMAND_TREE, ERROR_PREFIX, OK_PREFIX};
use std::collections::{BTreeMap, BTreeSet};
Expand All @@ -22,6 +23,9 @@ pub struct ExpectedFile {
/// One entry per reviewed name whose quoting polyglot drops and SQLQuack keeps.
#[serde(default, rename = "kept_quoting")]
pub kept_quotings: Vec<KeptQuoting>,
/// One entry per reviewed wrapper node SQLQuack keeps and polyglot drops.
#[serde(default, rename = "kept_node")]
pub kept_nodes: Vec<KeptNode>,
/// One entry per reviewed statement polyglot keeps as a command and SQLQuack structures.
#[serde(default, rename = "structured")]
pub structured: Vec<StructuredStatement>,
Expand Down Expand Up @@ -53,6 +57,20 @@ pub struct KeptQuoting {
pub note: String,
}

/// A statement both parsers parse, where SQLQuack keeps a wrapper node, such as unary plus, polyglot drops.
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct KeptNode {
/// A lower-case identifier.
pub id: String,
/// Text the statement contains, with whitespace removed and ignoring case, such as `+`.
pub marker: String,
/// The wrapper node SQLQuack keeps, such as `pos`; its only field is `this`.
pub node: String,
/// Why SQLQuack keeps it; without its wrappers the trees may differ in nothing.
pub note: String,
}

/// A statement SQLQuack rejects where polyglot parses it or fails differently, recognised by its exact text and dialect.
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)]
Expand Down Expand Up @@ -106,6 +124,8 @@ pub struct ExpectedDifferences {
pub rejections: Vec<ParserRejection>,
/// The reviewed names whose quoting polyglot drops.
pub kept_quotings: Vec<KeptQuoting>,
/// The reviewed wrapper nodes SQLQuack keeps and polyglot drops.
pub kept_nodes: Vec<KeptNode>,
/// The reviewed statements polyglot keeps as a command and SQLQuack structures.
pub structured: Vec<StructuredStatement>,
}
Expand Down Expand Up @@ -159,6 +179,15 @@ impl ExpectedDifferences {
.filter(|_| differ_only_by_kept_quoting(polyglot, sqlquack))
}

/// The entry explaining where SQLQuack keeps a wrapper node polyglot drops.
pub fn kept_node_for(&self, sql: &str, polyglot: &str, sqlquack: &str) -> Option<&KeptNode> {
let compact = compact(sql);
self.kept_nodes.iter().find(|entry| {
compact.contains(&entry.marker.to_ascii_uppercase())
&& differ_only_by_kept_node(polyglot, sqlquack, &entry.node)
})
}

/// The entry explaining where SQLQuack structures a statement polyglot keeps as a command.
pub fn structured_for(
&self,
Expand Down
16 changes: 10 additions & 6 deletions crates/sqlquack-polyglot-check/src/runs/main/run_corpus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,22 +59,26 @@ pub fn run_corpus(entries: &[CorpusEntry], expected: &ExpectedDifferences) -> Ru
let dialect = format!("{:?}", entry.dialect);
let rejection = expected.rejection_for(&entry.sql, &dialect, &sqlquack);
let kept_quoting = expected.kept_quoting_for(&entry.sql, &polyglot, &sqlquack);
let kept_node = expected.kept_node_for(&entry.sql, &polyglot, &sqlquack);
let structured = expected.structured_for(&entry.sql, &polyglot, &sqlquack);
match (fix, rejection, kept_quoting, structured) {
(Some(fix), _, _, _) => *summary.fixed.entry(fix.id.clone()).or_default() += 1,
(None, Some(rejection), _, _) => {
match (fix, rejection, kept_quoting, kept_node, structured) {
(Some(fix), ..) => *summary.fixed.entry(fix.id.clone()).or_default() += 1,
(None, Some(rejection), ..) => {
*summary.rejected.entry(rejection.id.clone()).or_default() += 1;
}
(None, None, Some(kept_quoting), _) => {
(None, None, Some(kept_quoting), ..) => {
*summary
.kept_quoting
.entry(kept_quoting.id.clone())
.or_default() += 1;
}
(None, None, None, Some(structured)) => {
(None, None, None, Some(kept_node), _) => {
*summary.kept_nodes.entry(kept_node.id.clone()).or_default() += 1;
}
(None, None, None, None, Some(structured)) => {
*summary.structured.entry(structured.id.clone()).or_default() += 1;
}
(None, None, None, None) => summary.differences.push(Finding {
(None, None, None, None, None) => summary.differences.push(Finding {
from,
detail: format!("polyglot: {polyglot}\nsqlquack: {sqlquack}"),
}),
Expand Down
2 changes: 2 additions & 0 deletions crates/sqlquack-polyglot-check/src/runs/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ pub struct RunSummary {
pub rejected: BTreeMap<String, usize>,
/// Entries whose trees differ only by quoting polyglot drops, as a reviewed entry explains.
pub kept_quoting: BTreeMap<String, usize>,
/// Entries whose trees differ only by wrapper nodes polyglot drops, as a reviewed entry explains.
pub kept_nodes: BTreeMap<String, usize>,
/// Entries polyglot keeps as a command and SQLQuack structures, as a reviewed entry explains.
pub structured: BTreeMap<String, usize>,
/// Entries whose results differ.
Expand Down
2 changes: 2 additions & 0 deletions crates/sqlquack-polyglot-check/tests/runs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
mod helpers;
#[path = "runs/test_exact.rs"]
mod test_exact;
#[path = "runs/test_kept_nodes.rs"]
mod test_kept_nodes;
#[path = "runs/test_kept_quoting.rs"]
mod test_kept_quoting;
#[path = "runs/test_rejections.rs"]
Expand Down
8 changes: 7 additions & 1 deletion crates/sqlquack-polyglot-check/tests/runs/test_exact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ fn given_committed_corpora_when_parsed_by_both_parsers_then_results_are_identica
expected_outcome: ExactOutcome {
loaded: true,
rejected: BTreeMap::from([
("malformed_typed_type_text".to_owned(), 2),
("malformed_typed_type_text".to_owned(), 1),
("snowflake_bare_values_cte".to_owned(), 1),
("snowflake_cte_without_as_prose".to_owned(), 1),
("snowflake_reserved_type_name".to_owned(), 1),
("snowflake_by_name_on_except".to_owned(), 1),
("snowflake_by_name_on_intersect".to_owned(), 1),
("snowflake_empty_select_list".to_owned(), 1),
Expand Down Expand Up @@ -54,6 +56,10 @@ fn given_committed_corpora_when_parsed_by_both_parsers_then_results_are_identica
expected_outcome: ExactOutcome {
loaded: true,
rejected: BTreeMap::from([
("snowflake_cte_without_as".to_owned(), 1),
("snowflake_empty_call_argument".to_owned(), 1),
("snowflake_reserved_cte_name".to_owned(), 1),
("snowflake_reserved_truncate_target".to_owned(), 1),
(
"snowflake_empty_select_list_trailing_order_by".to_owned(),
1,
Expand Down
Loading