Skip to content

Commit 692b0e2

Browse files
MagicalTuxclaude
andcommitted
fix(exec): eagerly reject a bad column on the LHS of an IN (SELECT)
The tested expression of `x [NOT] IN (SELECT …)` is a column reference of the enclosing query, so a bad one (`nope IN (SELECT …)`) is a prepare-time `no such column` in SQLite — raised even over an empty or fully filtered input, and even when the `IN (SELECT)` is nested inside another subquery's WHERE. The shallow-column walker used by the eager validators visited the operand of `InList` but had no arm for `InSelect`, so the LHS fell through to the catch-all and was only resolved lazily, per row — missing the error when no row is reached. Add an `InSelect` arm that visits the tested expression (the subquery body is a separate scope, validated by the existing recursion). A valid or aliased LHS (`count(*) IN (…)`, an output alias in HAVING/ORDER) is still accepted. Verified vs sqlite3 3.50.4 (`tests/eager_in_select_lhs.rs`). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 257924e commit 692b0e2

2 files changed

Lines changed: 72 additions & 0 deletions

File tree

src/exec/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26894,6 +26894,10 @@ fn walk_shallow_columns(e: &Expr, f: &mut impl FnMut(Option<&str>, Option<&str>,
2689426894
walk_shallow_columns(a, f);
2689526895
}
2689626896
}
26897+
// The tested expression of `x [NOT] IN (SELECT …)` is a shallow column of
26898+
// *this* scope (the subquery body is validated separately); visit it so a
26899+
// bad LHS (`nope IN (SELECT …)`) is caught, like `InList`'s LHS.
26900+
Expr::InSelect { expr, .. } => walk_shallow_columns(expr, f),
2689726901
Expr::Between {
2689826902
expr, low, high, ..
2689926903
} => {

tests/eager_in_select_lhs.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
//! The tested expression of `x [NOT] IN (SELECT …)` is a column reference of the
2+
//! enclosing scope, so a bad one (`nope IN (SELECT …)`) is a prepare-time
3+
//! `no such column` in SQLite — raised even over an empty/filtered input, and
4+
//! even when the `IN (SELECT)` is itself nested inside another subquery's body.
5+
//! graphite's shallow-column walker skipped the `InSelect` operand entirely (only
6+
//! `InList`'s operand was visited), so this was caught only lazily. Verified
7+
//! against sqlite3 3.50.4.
8+
9+
#![cfg(feature = "std")]
10+
11+
use std::process::Command;
12+
13+
fn sqlite3_available() -> bool {
14+
Command::new("sqlite3").arg("--version").output().is_ok()
15+
}
16+
17+
fn rejects(bin: &str, base: &str, sql: &str) -> bool {
18+
let full = format!("{base} {sql}");
19+
let out = Command::new(bin)
20+
.arg(":memory:")
21+
.arg(&full)
22+
.output()
23+
.unwrap();
24+
let mut s = String::from_utf8_lossy(&out.stdout).into_owned();
25+
s.push_str(&String::from_utf8_lossy(&out.stderr));
26+
s.to_lowercase().contains("error")
27+
}
28+
29+
#[test]
30+
fn in_select_lhs_column_validation_matches_sqlite() {
31+
if !sqlite3_available() {
32+
eprintln!("sqlite3 CLI not found; skipping");
33+
return;
34+
}
35+
let g = env!("CARGO_BIN_EXE_graphitesql");
36+
let empty = "CREATE TABLE t(a, b); CREATE TABLE u(c, d);";
37+
let full = "CREATE TABLE t(a, b); INSERT INTO t VALUES(1, 2); CREATE TABLE u(c, d);";
38+
let cases = [
39+
// Bad LHS of an IN (SELECT) — outer and nested inside another subquery.
40+
(empty, "SELECT x FROM t WHERE nope IN (SELECT c FROM u)"),
41+
(empty, "SELECT a FROM t WHERE nope NOT IN (SELECT c FROM u)"),
42+
(
43+
empty,
44+
"SELECT a FROM t WHERE a IN (SELECT c FROM u WHERE nope IN (SELECT d FROM u))",
45+
),
46+
// Valid LHS references must still be accepted (no false positive):
47+
(full, "SELECT a FROM t WHERE a IN (SELECT c FROM u)"),
48+
(
49+
full,
50+
"SELECT a AS x FROM t GROUP BY x HAVING x IN (SELECT c FROM u)",
51+
),
52+
(
53+
full,
54+
"SELECT count(*) FROM t GROUP BY a HAVING count(*) IN (SELECT c FROM u)",
55+
),
56+
(
57+
full,
58+
"SELECT a FROM t WHERE b IN (SELECT c FROM u WHERE d IN (SELECT c FROM u))",
59+
),
60+
];
61+
for (base, q) in cases {
62+
assert_eq!(
63+
rejects("sqlite3", base, q),
64+
rejects(g, base, q),
65+
"reject-parity for `{q}`"
66+
);
67+
}
68+
}

0 commit comments

Comments
 (0)