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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
//! `EXPLAIN QUERY PLAN` over a VIEW source. A view has no b-tree of its own:
//! SQLite flattens the view body into the outer plan exactly as it does a derived
//! table — `SELECT * FROM v` over `CREATE VIEW v AS SELECT a,b FROM t` reads as the
//! body's own `SCAN t` (a covering index when only its columns are needed), and an
//! outer `WHERE` tightens that into a `SEARCH`.
//!
//! graphite previously crashed EQP on *any* view source with a malformed
//! `no such table: <view>` (the view name fell through to a base-table lookup). It
//! now rewrites `FROM v` into `FROM (<view body>) AS v` and reuses the derived-table
//! flatten machinery: the flattenable shapes render byte-exactly, and the rest
//! (an aggregate / join / compound view body, or a view combined with a join —
//! which SQLite cost-reorders) decline cleanly instead of crashing. Verified vs 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()
}
/// `EXPLAIN QUERY PLAN sql` → `#`-joined bare node labels.
fn plan(bin: &str, base: &str, sql: &str) -> String {
let full = format!("{base} EXPLAIN QUERY PLAN {sql}");
let out = Command::new(bin)
.arg(":memory:")
.arg(&full)
.output()
.unwrap();
String::from_utf8_lossy(&out.stdout)
.lines()
.filter(|l| !l.trim().is_empty() && !l.starts_with("QUERY PLAN"))
.map(|l| l.trim_start_matches(|c: char| " |`*+_-".contains(c)))
.collect::<Vec<_>>()
.join("#")
}
/// graphite's stderr error line, prefix-stripped, for the decline cases.
fn err(bin: &str, base: &str, sql: &str) -> String {
let full = format!("{base} EXPLAIN QUERY PLAN {sql}");
let out = Command::new(bin)
.arg(":memory:")
.arg(&full)
.output()
.unwrap();
String::from_utf8_lossy(&out.stderr).trim().to_string()
}
const BASE: &str = "CREATE TABLE t(a INTEGER PRIMARY KEY, b, c); CREATE INDEX tb ON t(b); \
CREATE TABLE u(x,y); \
CREATE VIEW v AS SELECT a,b FROM t; \
CREATE VIEW v2 AS SELECT * FROM t; \
CREATE VIEW vw AS SELECT a,b FROM t WHERE c>0; \
CREATE VIEW vagg AS SELECT count(*) c FROM t; \
CREATE VIEW vj AS SELECT t.a,u.y FROM t JOIN u ON t.a=u.x;";
#[test]
fn flattenable_view_matches_sqlite() {
if !sqlite3_available() {
eprintln!("sqlite3 CLI not found; skipping");
return;
}
let g = env!("CARGO_BIN_EXE_graphitesql");
for q in [
"SELECT * FROM v",
"SELECT * FROM v WHERE b=5",
"SELECT * FROM v2",
"SELECT * FROM v2 WHERE b=5",
"SELECT a FROM v WHERE b=5",
"SELECT * FROM v WHERE a=3",
// The view name is the bind qualifier once flattened, alias or not.
"SELECT * FROM v AS x WHERE x.b=5",
"SELECT v.a FROM v WHERE v.b=5",
// An inner WHERE in the view body carries through.
"SELECT * FROM vw",
// An aggregate view body materializes as a CO-ROUTINE (see the
// aggregate-body co-routine rendering).
"SELECT * FROM vagg",
] {
assert_eq!(plan("sqlite3", BASE, q), plan(g, BASE, q), "plan for {q}");
}
}
#[test]
fn non_flattenable_view_shapes_decline_without_crashing() {
// The pre-fix bug surfaced as a malformed `no such table: <view>`. A join view
// body (SQLite cost-reorders) and a view combined with a join must now decline
// cleanly instead.
let g = env!("CARGO_BIN_EXE_graphitesql");
for q in [
"SELECT * FROM vj", // join view body
"SELECT * FROM v JOIN u ON v.a=u.x", // view combined with a join
"SELECT * FROM u JOIN v ON v.a=u.x", // view in the join position
"SELECT * FROM u, v", // comma-join with a view
] {
let got = err(g, BASE, q);
assert!(
!got.contains("no such table"),
"{q} regressed to the malformed crash: {got:?}"
);
assert!(
got.contains("EXPLAIN QUERY PLAN for this query shape"),
"{q} should decline as unsupported, got {got:?}"
);
}
}