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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
//! Cost-based join-order for THREE OR MORE tables (the N-table generalisation of
//! the two-table rowid/index inner swaps). sqlite drives a plain equi-INNER join
//! from a table it must SCAN and pulls the rowid-/index-seekable tables into the
//! inner positions, so an unordered query's rows come out in the chosen driver's
//! scan order. graphite reorders the same way (`ntable_join_order`) — only the row
//! ORDER changes; the RESULT SET and the `SELECT *` / `t.*` column order stay in
//! DECLARED `FROM` order.
//!
//! These checks are differential against real sqlite3 3.50.4 (skipped if the
//! binary is absent) and assert the rows (and, where graphite's plan is expected
//! to match byte-for-byte, the EXPLAIN QUERY PLAN), on both `set_use_vdbe(true)`
//! (default) and `(false)`. For a shape graphite deliberately leaves in
//! declaration order (a single-table WHERE restriction, which shifts sqlite's
//! driver choice by a selectivity we do not model), only the row *multiset* is
//! asserted equal — a wrong order is never emitted, but an unreordered one is
//! acceptable.
#![cfg(feature = "std")]
use graphitesql::{Connection, Value};
use std::process::Command;
fn sqlite3(script: &str) -> String {
let o = Command::new("sqlite3")
.arg(":memory:")
.arg(script)
.output()
.unwrap();
String::from_utf8_lossy(&o.stdout).trim_end().to_string()
}
fn have_sqlite3() -> bool {
Command::new("sqlite3").arg("--version").output().is_ok()
}
fn fmt(v: &Value) -> String {
match v {
Value::Null => String::new(),
Value::Integer(i) => i.to_string(),
Value::Real(r) => {
if *r == (*r as i64) as f64 {
format!("{:.1}", r)
} else {
format!("{r}")
}
}
Value::Text(s) => s.clone(),
Value::Blob(b) => String::from_utf8_lossy(b).into_owned(),
}
}
fn graphite_rows(c: &Connection, sql: &str) -> String {
c.query(sql)
.unwrap()
.rows
.iter()
.map(|row| row.iter().map(fmt).collect::<Vec<_>>().join("|"))
.collect::<Vec<_>>()
.join("\n")
}
fn graphite_plan(c: &Connection, sql: &str) -> Vec<String> {
c.query(&format!("EXPLAIN QUERY PLAN {sql}"))
.unwrap()
.rows
.iter()
.map(|row| match row.last() {
Some(Value::Text(s)) => s.clone(),
other => panic!("plan detail not text: {other:?}"),
})
.collect()
}
fn sqlite_plan(setup: &str, sql: &str) -> Vec<String> {
let out = sqlite3(&format!("{setup}\nEXPLAIN QUERY PLAN {sql};"));
out.lines()
.filter(|l| *l != "QUERY PLAN")
.map(|l| l.trim_start_matches(['|', '-', '`', ' ']).to_string())
.collect()
}
fn graphite(setup: &str, use_vdbe: bool) -> Connection {
let mut c = Connection::open_memory().unwrap();
c.set_use_vdbe(use_vdbe);
for stmt in setup.split(';') {
if !stmt.trim().is_empty() {
c.execute(stmt).unwrap();
}
}
c
}
/// Assert graphite's rows for `sql` equal sqlite's EXACTLY (order included), on
/// BOTH VDBE modes. Use for shapes graphite reorders to match sqlite's drive
/// order.
fn assert_rows(setup: &str, sql: &str) {
let want = sqlite3(&format!("{setup}\n{sql};"));
for &vdbe in &[true, false] {
let c = graphite(setup, vdbe);
let got = graphite_rows(&c, sql);
assert_eq!(got, want, "rows diverged (use_vdbe={vdbe}) for `{sql}`");
}
}
/// Assert graphite's rows equal sqlite's as a MULTISET (sorted lines), on both
/// VDBE modes. Use for shapes graphite deliberately leaves in declaration order
/// (row order may then differ from sqlite; only the set must match).
fn assert_rows_multiset(setup: &str, sql: &str) {
let mut want: Vec<String> = sqlite3(&format!("{setup}\n{sql};"))
.lines()
.map(str::to_string)
.collect();
want.sort();
for &vdbe in &[true, false] {
let c = graphite(setup, vdbe);
let mut got: Vec<String> = graphite_rows(&c, sql).lines().map(str::to_string).collect();
got.sort();
assert_eq!(got, want, "row set diverged (use_vdbe={vdbe}) for `{sql}`");
}
}
fn assert_plan(setup: &str, sql: &str) {
let want = sqlite_plan(setup, sql);
let c = graphite(setup, true);
let got = graphite_plan(&c, sql);
assert_eq!(got, want, "plan diverged for `{sql}`");
}
/// The reported bug: a rowid hub `u` sought from `v` (SCAN) and joined with `w`.
/// Asymmetric data makes the drive order observable.
const HUB: &str = "CREATE TABLE u(x INTEGER PRIMARY KEY,y);\
CREATE TABLE v(p,q);\
CREATE TABLE w(r,s);\
CREATE INDEX iv ON v(p);\
CREATE INDEX iw ON w(r);\
INSERT INTO u VALUES(3,30),(1,10),(2,20);\
INSERT INTO v VALUES(2,200),(1,100),(3,300);\
INSERT INTO w VALUES(3,3000),(1,1000),(2,2000);";
#[test]
fn hub_case_rows_and_plan() {
if !have_sqlite3() {
eprintln!("sqlite3 not found; skipping");
return;
}
// sqlite drives from v (SCAN v), seeks the rowid hub u, then joins w — rows in
// v's scan order (2,1,3). The full ROWS match sqlite exactly (the acceptance
// criterion); the driver + rowid-hub SEARCH nodes match too. The LAST node —
// the tail table `w` — is a seek-vs-scan LABEL nuance (sqlite hash-joins the
// tiny tail; graphite seeks its index): rows are identical either way, so only
// the driver + hub prefix is asserted, not the whole plan (mirrors how the
// two-table slice leaves the LEFT-join inner-node text unasserted).
let sql = "SELECT u.x,v.q,w.s FROM u JOIN v ON u.x=v.p JOIN w ON u.x=w.r";
assert_rows(HUB, sql);
// Confirm the drive order concretely: rows in v's scan order, not u's.
let c = graphite(HUB, true);
assert_eq!(graphite_rows(&c, sql), "2|200|2000\n1|100|1000\n3|300|3000");
let plan = graphite_plan(&c, sql);
assert_eq!(plan[0], "SCAN v");
assert_eq!(plan[1], "SEARCH u USING INTEGER PRIMARY KEY (rowid=?)");
}
#[test]
fn chain_case_rows_and_plan() {
if !have_sqlite3() {
eprintln!("sqlite3 not found; skipping");
return;
}
// A chain u—v—w (v.q=w.r), so w connects through v, not u. sqlite still drives
// from v, seeks u by rowid, seeks w by index — plan AND rows match exactly.
let setup = "CREATE TABLE u(x INTEGER PRIMARY KEY,y);\
CREATE TABLE v(p,q);\
CREATE TABLE w(r,s);\
CREATE INDEX iv ON v(p);\
CREATE INDEX iw ON w(r);\
INSERT INTO u VALUES(3,30),(1,10),(2,20);\
INSERT INTO v VALUES(2,20),(1,10),(3,30);\
INSERT INTO w VALUES(30,3000),(10,1000),(20,2000);";
let sql = "SELECT u.x,v.q,w.s FROM u JOIN v ON u.x=v.p JOIN w ON v.q=w.r";
assert_plan(setup, sql);
assert_rows(setup, sql);
}
#[test]
fn select_star_keeps_declared_column_order() {
if !have_sqlite3() {
eprintln!("sqlite3 not found; skipping");
return;
}
// Even though v drives, `SELECT *` expands in DECLARED order u,v,w — and each
// row's values follow that layout.
assert_rows(HUB, "SELECT * FROM u JOIN v ON u.x=v.p JOIN w ON u.x=w.r");
assert_rows(
HUB,
"SELECT u.*, w.*, v.* FROM u JOIN v ON u.x=v.p JOIN w ON u.x=w.r",
);
}
#[test]
fn four_table_case() {
if !have_sqlite3() {
eprintln!("sqlite3 not found; skipping");
return;
}
// Four tables, all hanging off the rowid hub u. sqlite drives a scanned table
// and pulls u (rowid) inner; the rows come out in the driver's scan order.
let setup = "CREATE TABLE u(x INTEGER PRIMARY KEY,y);\
CREATE TABLE v(p,q);\
CREATE TABLE w(r,s);\
CREATE TABLE z(a,b);\
CREATE INDEX iv ON v(p);\
CREATE INDEX iw ON w(r);\
CREATE INDEX iz ON z(a);\
INSERT INTO u VALUES(3,30),(1,10),(2,20);\
INSERT INTO v VALUES(2,200),(1,100),(3,300);\
INSERT INTO w VALUES(3,3000),(1,1000),(2,2000);\
INSERT INTO z VALUES(2,22),(1,11),(3,33);";
let sql = "SELECT u.x,v.q,w.s,z.b FROM u JOIN v ON u.x=v.p JOIN w ON u.x=w.r JOIN z ON u.x=z.a";
// The rows come out in the driver's scan order (matching sqlite exactly).
assert_rows(setup, sql);
// The chosen driver is a scanned table, and the rowid hub u is a rowid inner.
let c = graphite(setup, true);
let plan = graphite_plan(&c, sql);
assert!(plan[0].starts_with("SCAN "), "driver is a SCAN: {plan:?}");
assert!(
plan.iter()
.any(|l| l == "SEARCH u USING INTEGER PRIMARY KEY (rowid=?)"),
"u is the rowid inner: {plan:?}"
);
}
#[test]
fn order_by_stays_sorted() {
if !have_sqlite3() {
eprintln!("sqlite3 not found; skipping");
return;
}
// An explicit ORDER BY wins regardless of the drive direction, on both modes.
assert_rows(
HUB,
"SELECT u.x,v.q,w.s FROM u JOIN v ON u.x=v.p JOIN w ON u.x=w.r ORDER BY u.x",
);
assert_rows(
HUB,
"SELECT u.x,v.q,w.s FROM u JOIN v ON u.x=v.p JOIN w ON u.x=w.r ORDER BY w.s DESC",
);
}
#[test]
fn aggregate_and_distinct_over_join() {
if !have_sqlite3() {
eprintln!("sqlite3 not found; skipping");
return;
}
// Aggregates collapse the row set — the reorder must not change the RESULT.
assert_rows(
HUB,
"SELECT count(*),sum(w.s) FROM u JOIN v ON u.x=v.p JOIN w ON u.x=w.r",
);
// DISTINCT over the join: the set is order-independent, so compare as a set.
assert_rows_multiset(
HUB,
"SELECT DISTINCT v.q FROM u JOIN v ON u.x=v.p JOIN w ON u.x=w.r",
);
}
#[test]
fn where_filter_result_set_matches() {
if !have_sqlite3() {
eprintln!("sqlite3 not found; skipping");
return;
}
// A single-table WHERE restriction shifts sqlite's driver choice by selectivity
// (a cost factor graphite does not model), so graphite declines the reorder and
// runs in declaration order. The RESULT SET still matches sqlite exactly (order
// may differ — only the set is asserted).
assert_rows_multiset(
HUB,
"SELECT u.x,v.q,w.s FROM u JOIN v ON u.x=v.p JOIN w ON u.x=w.r WHERE w.s>1000",
);
// A cross-table WHERE equi-predicate is not a single-table restriction, so the
// reorder may still fire; either way the set matches.
assert_rows_multiset(
HUB,
"SELECT u.x,v.q,w.s FROM u JOIN v ON u.x=v.p JOIN w ON u.x=w.r WHERE v.q>u.y",
);
}
#[test]
fn comma_join_form_reorders_identically() {
if !have_sqlite3() {
eprintln!("sqlite3 not found; skipping");
return;
}
// The comma form promotes its WHERE equalities to ONs and reorders identically.
let sql = "SELECT u.x,v.q,w.s FROM u,v,w WHERE u.x=v.p AND u.x=w.r";
assert_rows(HUB, sql);
}
// ---- Negative / unchanged cases ---------------------------------------------
#[test]
fn two_table_still_handled_by_existing_slices() {
if !have_sqlite3() {
eprintln!("sqlite3 not found; skipping");
return;
}
// A two-table join is NOT the N-table path (it needs >= 3 tables); the existing
// rowid/index inner swaps own it, unchanged. Plan + rows match sqlite.
let sql = "SELECT u.x,v.q FROM u JOIN v ON u.x=v.p";
assert_plan(HUB, sql);
assert_rows(HUB, sql);
}
#[test]
fn left_join_never_reordered() {
if !have_sqlite3() {
eprintln!("sqlite3 not found; skipping");
return;
}
// A LEFT join anywhere in the chain fixes the order semantically — the N-table
// reorder declines (it requires every join to be plain INNER). Rows must match
// sqlite's fixed order; assert only the multiset (graphite keeps declaration
// order, sqlite's outer-join order is likewise fixed but the null-padding makes
// exact order comparison brittle across planners).
let sql = "SELECT u.x,v.q,w.s FROM u JOIN v ON u.x=v.p LEFT JOIN w ON u.x=w.r";
assert_rows_multiset(HUB, sql);
// u remains the driver (declaration order — no reorder).
let c = graphite(HUB, true);
assert_eq!(graphite_plan(&c, sql)[0], "SCAN u");
}
#[test]
fn natural_and_using_never_reordered() {
if !have_sqlite3() {
eprintln!("sqlite3 not found; skipping");
return;
}
// NATURAL / USING joins are excluded from the reorder (they coalesce columns and
// fix order). The RESULT SET must still match sqlite.
let setup = "CREATE TABLE u(x INTEGER PRIMARY KEY,k);\
CREATE TABLE v(k,q);\
CREATE TABLE w(k,s);\
INSERT INTO u VALUES(1,10),(2,20),(3,30);\
INSERT INTO v VALUES(20,200),(10,100),(30,300);\
INSERT INTO w VALUES(30,3000),(10,1000),(20,2000);";
assert_rows_multiset(setup, "SELECT * FROM u JOIN v USING(k) JOIN w USING(k)");
assert_rows_multiset(setup, "SELECT * FROM u NATURAL JOIN v NATURAL JOIN w");
}
#[test]
fn cross_product_no_equijoin_unchanged() {
if !have_sqlite3() {
eprintln!("sqlite3 not found; skipping");
return;
}
// No connecting equi-join between one table and the rest → the reorder declines
// (a cross-product step it will not introduce). Result set matches sqlite.
let setup = "CREATE TABLE u(x INTEGER PRIMARY KEY,y);\
CREATE TABLE v(p,q);\
CREATE TABLE w(r,s);\
CREATE INDEX iv ON v(p);\
INSERT INTO u VALUES(1,10),(2,20);\
INSERT INTO v VALUES(1,100),(2,200);\
INSERT INTO w VALUES(7,7000),(8,8000);";
// w is a bare cross product (no ON tying it to u/v).
assert_rows_multiset(
setup,
"SELECT u.x,v.q,w.s FROM u JOIN v ON u.x=v.p CROSS JOIN w",
);
}