1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
//! When `*` (or `tbl.*`) expansion over an *unaliased self-join* surfaces a
//! column that two sources share by both name and qualifier, SQLite cannot tell
//! the copies apart and rejects the query — naming the column by its source's
//! origin: `<db>.<table>.<col>` for a real table (`main.t.a`, a temp table that
//! shadows it → `temp.t.a`, an attached `aux.t.a`), or `*.<alias>.<col>` for a
//! derived table / CTE that has no database (`*.x.a`). graphite previously
//! emitted just `<source>.<col>` (`t.a`, `x.a`); it now matches the origin
//! prefix. An *explicit* ambiguous reference (`SELECT a` / `SELECT x.a`) keeps
//! its bare/`table.col` spelling, unchanged.
//!
//! Verified against the sqlite3 3.50.4 CLI.
#![cfg(feature = "std")]
use std::process::Command;
fn sqlite3_available() -> bool {
Command::new("sqlite3").arg("--version").output().is_ok()
}
/// The `ambiguous column name: …` tail of the error, regardless of how the CLI
/// wraps it (prepare errors vs the parse-error `near line N:` prefix). For a
/// query that does not error, returns its first non-empty output line so the
/// valid cases still compare equal.
fn err_tail(bin: &str, sql: &str) -> String {
let out = Command::new(bin).arg(":memory:").arg(sql).output().unwrap();
let text = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
for line in text.lines() {
if let Some(pos) = line.find("ambiguous column name:") {
return line[pos..].trim_end().to_string();
}
}
text.lines()
.map(str::trim_end)
.find(|l| !l.is_empty())
.unwrap_or("")
.to_string()
}
#[test]
fn wildcard_ambiguity_names_source_origin() {
if !sqlite3_available() {
eprintln!("sqlite3 CLI not found; skipping");
return;
}
let g = env!("CARGO_BIN_EXE_graphitesql");
for sql in [
// Real table self-join → `main.t.col`.
"CREATE TABLE t(a); SELECT * FROM t, t",
"CREATE TABLE t(a,b); SELECT * FROM t, t",
"CREATE TABLE t(a,b); SELECT t.* FROM t, t",
// Temp table (shadows main) → `temp.t.col`.
"CREATE TEMP TABLE t(a,b); SELECT * FROM t, t",
"CREATE TABLE t(a,b); CREATE TEMP TABLE t(a,b); SELECT * FROM t, t",
// CTE / subquery → `*.alias.col`.
"WITH x AS (SELECT 1) SELECT * FROM x, x",
"WITH x AS (SELECT 1 a) SELECT * FROM x, x",
"CREATE TABLE t(a,b); WITH x AS (SELECT * FROM t) SELECT * FROM x, x",
"SELECT * FROM (SELECT 1 a) x, (SELECT 2 a) x",
// Explicit references keep their bare / table.col spelling.
"WITH x AS (SELECT 1 a) SELECT a FROM x, x",
"WITH x AS (SELECT 1 a) SELECT x.a FROM x, x",
"CREATE TABLE t(a); SELECT a FROM t, t",
"CREATE TABLE t(a); CREATE TABLE u(a); SELECT * FROM t,u WHERE a=1",
// Aliased self-join is unambiguous — both run fine.
"CREATE TABLE t(a); SELECT * FROM t t1, t t2",
// Same name in *different databases* is NOT ambiguous: the origin
// `(db, table, name)` differs, so `*` keeps every column (regression
// guard for A-misc-2 — graphite used to reject these as `main.t.a`).
"ATTACH ':memory:' AS aux; CREATE TABLE aux.t(a,c); INSERT INTO aux.t VALUES(3,4); \
CREATE TABLE t(a,b); INSERT INTO t VALUES(1,2); SELECT * FROM t, aux.t",
"ATTACH ':memory:' AS aux; CREATE TABLE aux.t(a,c); INSERT INTO aux.t VALUES(3,4); \
CREATE TABLE t(a,b); INSERT INTO t VALUES(1,2); SELECT * FROM aux.t, t",
"CREATE TABLE t(a,b); INSERT INTO t VALUES(1,2); \
CREATE TEMP TABLE u(a,c); INSERT INTO u VALUES(9,8); SELECT * FROM t, temp.u",
// …but a real self-join still collides even with an unrelated other-db
// table present, and an attached self-join names that db's origin.
"ATTACH ':memory:' AS aux; CREATE TABLE aux.t(a,c); \
CREATE TABLE t(a,b); SELECT * FROM t, t, aux.t",
"ATTACH ':memory:' AS aux; CREATE TABLE aux.t(a,b); SELECT * FROM aux.t, aux.t",
// Explicit `main.` self-join is the same origin as the unqualified one.
"CREATE TABLE t(a,b); SELECT * FROM t, main.t",
] {
assert_eq!(err_tail("sqlite3", sql), err_tail(g, sql), "for {sql}");
}
}