Skip to content

Commit db8c27a

Browse files
committed
Merge branch 'fix/receipt-contradictions' into fix/r68-contradictions
2 parents a38ef65 + ff4f6d0 commit db8c27a

100 files changed

Lines changed: 12506 additions & 43 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.

‎.github/workflows/website.yml‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: Website
2+
3+
# Builds the site and checks its links and branding. Manual only, and it never deploys: the site is
4+
# not public yet. Add push and pull_request triggers, and a deploy job, once publishing is approved.
5+
on:
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: read
10+
11+
concurrency:
12+
group: website-${{ github.ref }}
13+
cancel-in-progress: true
14+
15+
jobs:
16+
build:
17+
name: Build the website
18+
runs-on: ubuntu-24.04
19+
defaults:
20+
run:
21+
working-directory: website
22+
steps:
23+
- uses: actions/checkout@v4
24+
25+
- uses: actions/setup-node@v4
26+
with:
27+
node-version: 22
28+
cache: npm
29+
cache-dependency-path: website/package-lock.json
30+
31+
- name: Check the diagnostics reference matches the registry
32+
run: python3 scripts/generate_diagnostics.py --check
33+
34+
- name: Install dependencies
35+
run: npm ci
36+
37+
- name: Build
38+
run: npm run build
39+
40+
- name: Check links
41+
run: python3 scripts/check_links.py dist

‎Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ sha2 = "0.10"
5454
sqlquack = { path = "crates/sqlquack" }
5555
sqlquack-compare = { path = "crates/sqlquack-compare" }
5656
sqlquack-core = { path = "crates/sqlquack-core" }
57-
sqlquack-duckdb = { path = "crates/sqlquack-duckdb" }
57+
sqlquack-duckdb = { path = "crates/sqlquack-duckdb", default-features = false }
5858
sqlquack-evidence = { path = "crates/sqlquack-evidence" }
5959
sqlquack-policy = { path = "crates/sqlquack-policy" }
6060
sqlquack-rules = { path = "crates/sqlquack-rules" }

‎crates/sqlquack-duckdb/Cargo.toml‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,20 @@ homepage.workspace = true
99
publish.workspace = true
1010
rust-version.workspace = true
1111

12+
# `engine` links native DuckDB for check_printed's parse-and-bind check. Without it, the building
13+
# blocks and the lexical checks still compile, for targets such as wasm32 that bind elsewhere.
14+
[features]
15+
default = ["engine"]
16+
engine = ["dep:duckdb"]
17+
1218
[dependencies]
13-
duckdb.workspace = true
19+
duckdb = { workspace = true, optional = true }
1420
serde_json.workspace = true
1521
sqlquack-core.workspace = true
1622
sqlquack-snowflake.workspace = true
1723

24+
[dev-dependencies]
25+
duckdb.workspace = true
26+
1827
[lints]
1928
workspace = true
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//! Binding printed SQL in DuckDB, when the crate is built with the engine.
2+
3+
#[cfg(feature = "engine")]
4+
use crate::checking::_helpers::engine::with_engine;
5+
#[cfg(feature = "engine")]
6+
use crate::checking::_helpers::tables::{function_definitions, table_definitions};
7+
#[cfg(feature = "engine")]
8+
use crate::checking::constants::PARSER_ERROR;
9+
#[cfg(feature = "engine")]
10+
use crate::checking::errors::CheckStage;
11+
use crate::checking::errors::PrintedSqlFailure;
12+
use crate::checking::types::{FunctionLookup, RelationLookup};
13+
14+
/// DuckDB parses `sql` and binds it over the relations and functions `names` reads.
15+
#[cfg(feature = "engine")]
16+
pub(crate) fn bound(
17+
sql: &str,
18+
names: &[String],
19+
(lookup, functions): (RelationLookup<'_>, FunctionLookup<'_>),
20+
) -> Result<(), PrintedSqlFailure> {
21+
with_engine(|engine| {
22+
let mut definitions = table_definitions(names, lookup);
23+
definitions.extend(function_definitions(names, functions));
24+
engine.keep_only(&definitions.iter().map(|(name, _)| name.as_str()).collect())?;
25+
for (name, statement) in &definitions {
26+
engine.define_table(name, statement)?;
27+
}
28+
engine.connection.prepare(sql).map(drop).map_err(|error| {
29+
let message = error.to_string();
30+
let stage = match message.starts_with(PARSER_ERROR) {
31+
true => CheckStage::Parse,
32+
false => CheckStage::Bind,
33+
};
34+
PrintedSqlFailure { stage, message }
35+
})
36+
})
37+
}
38+
39+
/// Without the engine the caller binds the SQL itself, so only the lexical checks apply.
40+
#[cfg(not(feature = "engine"))]
41+
pub(crate) fn bound(
42+
_sql: &str,
43+
_names: &[String],
44+
_lookups: (RelationLookup<'_>, FunctionLookup<'_>),
45+
) -> Result<(), PrintedSqlFailure> {
46+
Ok(())
47+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
pub(crate) mod binding;
12
pub(crate) mod dotted_parameters;
3+
#[cfg(feature = "engine")]
24
pub(crate) mod engine;
35
pub(crate) mod lexing;
6+
#[cfg(feature = "engine")]
47
pub(crate) mod tables;

‎crates/sqlquack-duckdb/src/checking/main/check_printed.rs‎

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
//! `check_printed`: DuckDB parses printed SQL as one statement and binds it.
22
3+
use crate::checking::_helpers::binding::bound;
34
use crate::checking::_helpers::dotted_parameters::dotted_parameters;
4-
use crate::checking::_helpers::engine::with_engine;
55
use crate::checking::_helpers::lexing::lexed;
6-
use crate::checking::_helpers::tables::{function_definitions, table_definitions};
7-
use crate::checking::constants::PARSER_ERROR;
86
use crate::checking::errors::{CheckStage, PrintedSqlFailure};
97
use crate::checking::types::{FunctionLookup, RelationLookup};
108

@@ -28,22 +26,7 @@ pub fn check_printed(
2826
));
2927
}
3028
let names: Vec<String> = lexed.names.into_iter().collect();
31-
with_engine(|engine| {
32-
let mut definitions = table_definitions(&names, lookup);
33-
definitions.extend(function_definitions(&names, functions));
34-
engine.keep_only(&definitions.iter().map(|(name, _)| name.as_str()).collect())?;
35-
for (name, statement) in &definitions {
36-
engine.define_table(name, statement)?;
37-
}
38-
engine.connection.prepare(sql).map(drop).map_err(|error| {
39-
let message = error.to_string();
40-
let stage = match message.starts_with(PARSER_ERROR) {
41-
true => CheckStage::Parse,
42-
false => CheckStage::Bind,
43-
};
44-
failure(stage, message)
45-
})
46-
})
29+
bound(sql, &names, (lookup, functions))
4730
}
4831

4932
fn failure(stage: CheckStage, message: String) -> PrintedSqlFailure {

‎crates/sqlquack-rules/Cargo.toml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ homepage.workspace = true
99
publish.workspace = true
1010
rust-version.workspace = true
1111

12+
# `engine`: DuckDB parses and binds every converted statement. Off, the converter skips that check,
13+
# and the caller binds the SQL itself (the browser playground does, in DuckDB-WASM).
14+
[features]
15+
default = ["engine"]
16+
engine = ["sqlquack-duckdb/engine"]
17+
1218
[dependencies]
1319
serde.workspace = true
1420
serde_json.workspace = true
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//! Every current receipt against the parser, whether or not a catalogued case cites it (decision D141).
2+
3+
use std::collections::{BTreeMap, BTreeSet};
4+
5+
use crate::check::_helpers::receipts::snowflake_syntax;
6+
use crate::check::models::{CaseResult, LocalVerdict, SnowflakeSyntax};
7+
use crate::formats::models::{Receipt, SyntaxCoverage};
8+
9+
/// Whether the parser's verdict contradicts what Snowflake's parser did with the same statement.
10+
fn contradicts(snowflake: SnowflakeSyntax, local: &LocalVerdict) -> bool {
11+
match snowflake {
12+
SnowflakeSyntax::Parsed => !matches!(local, LocalVerdict::Accepted { .. }),
13+
SnowflakeSyntax::Refused => matches!(local, LocalVerdict::Accepted { .. }),
14+
}
15+
}
16+
17+
/// The receipt case ids the parser contradicts by SQL, setup and session, and those without SQL.
18+
pub(crate) fn receipt_contradictions(
19+
coverage: &SyntaxCoverage,
20+
cases: &BTreeMap<&str, CaseResult>,
21+
parse: &dyn Fn(&str) -> LocalVerdict,
22+
) -> (BTreeSet<String>, BTreeSet<String>) {
23+
let mut statements: BTreeMap<(&str, &str, &str), Vec<&Receipt>> = BTreeMap::new();
24+
for receipt in &coverage.receipts {
25+
statements
26+
.entry((
27+
receipt.sql_sha256.as_str(),
28+
receipt.setup_sha256.as_str(),
29+
receipt.session_sha256.as_str(),
30+
))
31+
.or_default()
32+
.push(receipt);
33+
}
34+
let mut judged: BTreeMap<&str, LocalVerdict> = cases
35+
.values()
36+
.map(|case| (case.sql_sha256.as_str(), case.local.clone()))
37+
.collect();
38+
let case_sql: BTreeMap<&str, &str> = coverage
39+
.cases
40+
.values()
41+
.flatten()
42+
.map(|case| (case.sql_sha256.as_str(), case.sql.as_str()))
43+
.collect();
44+
let mut contradicted: BTreeSet<String> = BTreeSet::new();
45+
let mut unjudged: BTreeSet<String> = BTreeSet::new();
46+
for ((sql_sha256, _, _), receipts) in &statements {
47+
let Some(snowflake) = snowflake_syntax(receipts) else {
48+
continue;
49+
};
50+
let sql: Option<&str> = case_sql
51+
.get(sql_sha256)
52+
.copied()
53+
.or_else(|| coverage.receipt_sql.get(*sql_sha256).map(String::as_str));
54+
let Some(sql) = sql else {
55+
unjudged.extend(receipts.iter().map(|receipt| receipt.case_id.clone()));
56+
continue;
57+
};
58+
let local: &LocalVerdict = judged.entry(sql_sha256).or_insert_with(|| parse(sql));
59+
if contradicts(snowflake, local) {
60+
contradicted.extend(receipts.iter().map(|receipt| receipt.case_id.clone()));
61+
}
62+
}
63+
(contradicted, unjudged)
64+
}

‎crates/sqlquack-syntax-coverage/src/check/_helpers/mod.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
pub(crate) mod contradictions;
12
pub(crate) mod fingerprints;
23
pub(crate) mod forms;
34
pub(crate) mod join;

‎crates/sqlquack-syntax-coverage/src/check/_helpers/receipts.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ fn agreement(phase: Phase, local: &LocalVerdict, terminates: bool) -> Result<boo
6363
}
6464

6565
/// What Snowflake's parser did by the syntax receipts of the case's statement; none unless all agree.
66-
fn snowflake_syntax(statement_receipts: &[&Receipt]) -> Option<SnowflakeSyntax> {
66+
pub(crate) fn snowflake_syntax(statement_receipts: &[&Receipt]) -> Option<SnowflakeSyntax> {
6767
let verdicts: BTreeSet<Option<SnowflakeSyntax>> = statement_receipts
6868
.iter()
6969
.filter(|receipt| receipt.measure.is_none())

0 commit comments

Comments
 (0)