Skip to content

Commit 630e0dc

Browse files
committed
sql: clean up tests
1 parent 63afebd commit 630e0dc

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+1786
-1790
lines changed

src/sql/mod.rs

Lines changed: 72 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,16 @@ pub mod parser;
7272
pub mod planner;
7373
pub mod types;
7474

75+
/// SQL tests are implemented as goldenscripts under src/sql/testscripts.
7576
#[cfg(test)]
7677
mod tests {
7778
use super::engine::{Catalog as _, Session};
7879
use super::parser::Parser;
79-
use super::planner::{Node, Plan, OPTIMIZERS};
80+
use super::planner::{Plan, OPTIMIZERS};
8081
use crate::encoding::format::{self, Formatter as _};
8182
use crate::sql::engine::{Engine, Local, StatementResult};
8283
use crate::sql::planner::{Planner, Scope};
83-
use crate::storage::engine::test::{self, Emit, Mirror, Operation};
84+
use crate::storage::engine::test as testengine;
8485
use crate::storage::{self, Engine as _};
8586

8687
use crossbeam::channel::Receiver;
@@ -92,24 +93,25 @@ mod tests {
9293
use test_each_file::test_each_path;
9394

9495
// Run goldenscript tests in src/sql/testscripts.
95-
test_each_path! { in "src/sql/testscripts/optimizer" as optimizer => test_goldenscript }
96+
test_each_path! { in "src/sql/testscripts/expressions" as expressions => test_goldenscript_expr }
97+
test_each_path! { in "src/sql/testscripts/optimizers" as optimizers => test_goldenscript }
9698
test_each_path! { in "src/sql/testscripts/queries" as queries => test_goldenscript }
9799
test_each_path! { in "src/sql/testscripts/schema" as schema => test_goldenscript }
98100
test_each_path! { in "src/sql/testscripts/transactions" as transactions => test_goldenscript }
99101
test_each_path! { in "src/sql/testscripts/writes" as writes => test_goldenscript }
100-
test_each_path! { in "src/sql/testscripts/expressions" as expressions => test_goldenscript_expr }
101102

102103
/// Runs SQL goldenscripts.
103104
fn test_goldenscript(path: &std::path::Path) {
104-
// Since the runner's Session can't reference an Engine in the same
105-
// struct, borrow the engine. Use both a BitCask and a Memory engine,
106-
// and mirror operations across them. Emit engine operations to op_rx.
105+
// The runner's Session can't borrow from an Engine in the same struct,
106+
// so pass an engine reference. Use both BitCask and Memory engines and
107+
// mirror operations across them. Emit engine operations to op_rx.
107108
let (op_tx, op_rx) = crossbeam::channel::unbounded();
108109
let tempdir = tempfile::TempDir::with_prefix("toydb").expect("tempdir failed");
109110
let bitcask =
110111
storage::BitCask::new(tempdir.path().join("bitcask")).expect("bitcask failed");
111112
let memory = storage::Memory::new();
112-
let engine = Local::new(Emit::new(Mirror::new(bitcask, memory), op_tx));
113+
let engine =
114+
Local::new(testengine::Emit::new(testengine::Mirror::new(bitcask, memory), op_tx));
113115
let mut runner = SQLRunner::new(&engine, op_rx);
114116

115117
goldenscript::run(&mut runner, path).expect("goldenscript failed")
@@ -124,13 +126,14 @@ mod tests {
124126
struct SQLRunner<'a> {
125127
engine: &'a TestEngine,
126128
sessions: HashMap<String, Session<'a, TestEngine>>,
127-
op_rx: Receiver<Operation>,
129+
op_rx: Receiver<testengine::Operation>,
128130
}
129131

130-
type TestEngine = Local<test::Emit<test::Mirror<storage::BitCask, storage::Memory>>>;
132+
type TestEngine =
133+
Local<testengine::Emit<testengine::Mirror<storage::BitCask, storage::Memory>>>;
131134

132135
impl<'a> SQLRunner<'a> {
133-
fn new(engine: &'a TestEngine, op_rx: Receiver<Operation>) -> Self {
136+
fn new(engine: &'a TestEngine, op_rx: Receiver<testengine::Operation>) -> Self {
134137
Self { engine, sessions: HashMap::new(), op_rx }
135138
}
136139
}
@@ -139,7 +142,7 @@ mod tests {
139142
fn run(&mut self, command: &goldenscript::Command) -> Result<String, Box<dyn Error>> {
140143
let mut output = String::new();
141144

142-
// Obtain a session for the command prefix.
145+
// Obtain a session based on the command prefix ("" if none).
143146
let prefix = command.prefix.clone().unwrap_or_default();
144147
let session = self.sessions.entry(prefix).or_insert_with(|| self.engine.session());
145148

@@ -151,12 +154,9 @@ mod tests {
151154
let mut engine = self.engine.mvcc.engine.lock().expect("mutex failed");
152155
let mut iter = engine.scan(..);
153156
while let Some((key, value)) = iter.next().transpose()? {
154-
writeln!(
155-
output,
156-
"{} [{}]",
157-
format::MVCC::<format::SQL>::key_value(&key, &value),
158-
format::Raw::key_value(&key, &value)
159-
)?;
157+
let fmtkv = format::MVCC::<format::SQL>::key_value(&key, &value);
158+
let rawkv = format::Raw::key_value(&key, &value);
159+
writeln!(output, "{fmtkv} [{rawkv}]",)?;
160160
}
161161
return Ok(output);
162162
}
@@ -175,56 +175,71 @@ mod tests {
175175
.map(|t| session.with_txn(true, |txn| txn.must_get_table(&t)))
176176
.try_collect()?
177177
};
178-
return Ok(schemas.into_iter().map(|s| s.to_string()).join("\n"));
178+
return Ok(schemas.into_iter().join("\n"));
179179
}
180180

181181
// Otherwise, fall through to SQL execution.
182182
_ => {}
183183
}
184184

185-
// The entire command is the statement to execute. There are no args.
185+
// The entire command is the SQL statement. There are no args.
186186
if !command.args.is_empty() {
187-
return Err("expressions should be given as a command with no args".into());
187+
return Err("SQL statements should be given as a command with no args".into());
188188
}
189189
let input = &command.name;
190190
let mut tags = command.tags.clone();
191191

192-
// Execute the statement.
193-
let result = session.execute(input)?;
192+
// Output the plan if requested.
193+
if tags.remove("plan") {
194+
let ast = Parser::new(input).parse()?;
195+
let plan =
196+
session.with_txn(true, |txn| Planner::new(txn).build(ast)?.optimize())?;
197+
writeln!(output, "{plan}")?;
198+
}
194199

195-
// Output optimizations if requested.
200+
// Output plan optimizations if requested.
196201
if tags.remove("opt") {
197202
if tags.contains("plan") {
198-
return Err("no point using both plan and opt".into());
203+
return Err("using both plan and opt is redundant".into());
199204
}
200205
let ast = Parser::new(input).parse()?;
201206
let plan = session.with_txn(true, |txn| Planner::new(txn).build(ast))?;
202207
let Plan::Select(mut root) = plan else {
203208
return Err("can only use opt with SELECT plans".into());
204209
};
205-
206-
let fmtplan = |name, node: &Node| format!("{name}:\n{node}").replace('\n', "\n ");
207-
writeln!(output, "{}", fmtplan("Initial", &root))?;
210+
writeln!(output, "{}", format!("Initial:\n{root}").replace('\n', "\n "))?;
208211
for (name, optimizer) in OPTIMIZERS {
209-
let old = root.clone();
212+
let prev = root.clone();
210213
root = optimizer(root)?;
211-
if root != old {
212-
writeln!(output, "{}", fmtplan(name, &root))?;
214+
if root != prev {
215+
writeln!(output, "{}", format!("{name}:\n{root}").replace('\n', "\n "))?;
213216
}
214217
}
215218
}
216219

217-
// Output the plan if requested.
218-
if tags.remove("plan") {
219-
let query = format!("EXPLAIN {input}");
220-
let StatementResult::Explain(plan) = session.execute(&query)? else {
221-
return Err("unexpected explain response".into());
222-
};
223-
writeln!(output, "{plan}")?;
220+
// Execute the statement.
221+
let result = session.execute(input)?;
222+
223+
// Output engine ops if requested.
224+
if tags.remove("ops") {
225+
while let Ok(op) = self.op_rx.try_recv() {
226+
match op {
227+
testengine::Operation::Delete { key } => {
228+
let fmtkey = format::MVCC::<format::SQL>::key(&key);
229+
let rawkey = format::Raw::key(&key);
230+
writeln!(output, "delete {fmtkey} [{rawkey}]")?;
231+
}
232+
testengine::Operation::Flush => writeln!(output, "flush")?,
233+
testengine::Operation::Set { key, value } => {
234+
let fmtkv = format::MVCC::<format::SQL>::key_value(&key, &value);
235+
let rawkv = format::Raw::key_value(&key, &value);
236+
writeln!(output, "set {fmtkv} [{rawkv}]")?;
237+
}
238+
}
239+
}
224240
}
225241

226242
// Output the result if requested. SELECT results are always output.
227-
let show_result = tags.remove("result");
228243
match result {
229244
StatementResult::Select { columns, rows } => {
230245
if tags.remove("header") {
@@ -234,37 +249,10 @@ mod tests {
234249
writeln!(output, "{}", row.into_iter().join(", "))?;
235250
}
236251
}
237-
StatementResult::Begin { state } if show_result => {
238-
let version = state.version;
239-
let kind = if state.read_only { "read-only" } else { "read-write" };
240-
let active = state.active.iter().join(",");
241-
writeln!(output, "v{version} {kind} active={{{active}}}")?;
242-
}
243-
result if show_result => writeln!(output, "{result:?}")?,
252+
result if tags.remove("result") => writeln!(output, "{result:?}")?,
244253
_ => {}
245254
}
246255

247-
// Output engine ops if requested.
248-
if tags.remove("ops") {
249-
while let Ok(op) = self.op_rx.try_recv() {
250-
match op {
251-
Operation::Delete { key } => writeln!(
252-
output,
253-
"storage delete {} [{}]",
254-
format::MVCC::<format::SQL>::key(&key),
255-
format::Raw::key(&key),
256-
)?,
257-
Operation::Flush => writeln!(output, "storage flush")?,
258-
Operation::Set { key, value } => writeln!(
259-
output,
260-
"storage set {} [{}]",
261-
format::MVCC::<format::SQL>::key_value(&key, &value),
262-
format::Raw::key_value(&key, &value),
263-
)?,
264-
}
265-
}
266-
}
267-
268256
// Reject unknown tags.
269257
if let Some(tag) = tags.iter().next() {
270258
return Err(format!("unknown tag {tag}").into());
@@ -280,14 +268,16 @@ mod tests {
280268
}
281269
}
282270

283-
/// A test runner for expressions specifically. Evaluates expressions to
284-
/// values, and can optionally emit the expression tree.
271+
/// A test runner for expressions. Evaluates expressions to values, and
272+
/// optionally emits the expression tree.
285273
struct ExpressionRunner;
286274

287275
type Catalog<'a> = <Local<storage::Memory> as Engine<'a>>::Transaction;
288276

289277
impl goldenscript::Runner for ExpressionRunner {
290278
fn run(&mut self, command: &goldenscript::Command) -> Result<String, Box<dyn Error>> {
279+
let mut output = String::new();
280+
291281
// The entire command is the expression to evaluate. There are no args.
292282
if !command.args.is_empty() {
293283
return Err("expressions should be given as a command with no args".into());
@@ -301,30 +291,31 @@ mod tests {
301291
if let Some(next) = parser.lexer.next().transpose()? {
302292
return Err(format!("unconsumed token {next}").into());
303293
}
304-
let mut expr = Planner::<Catalog>::build_expression(ast, &Scope::new())?;
305-
306-
// If requested, convert the expression to conjunctive normal form.
307-
if tags.remove("cnf") {
308-
expr = expr.into_cnf();
309-
tags.insert("expr".to_string()); // imply expr
310-
}
294+
let expr = Planner::<Catalog>::build_expression(ast, &Scope::new())?;
311295

312296
// Evaluate the expression.
313-
let mut output = String::new();
314297
let value = expr.evaluate(None)?;
315-
write!(output, "{value:?}")?;
298+
write!(output, "{value}")?;
316299

317-
// If requested, dump the parsed expression.
300+
// If requested, convert the expression to conjunctive normal form
301+
// and dump it. Assert that it produces the same result.
302+
if tags.remove("cnf") {
303+
let cnf = expr.clone().into_cnf();
304+
assert_eq!(value, cnf.evaluate(None)?, "CNF result differs");
305+
write!(output, " ← {}", cnf.format_constant())?;
306+
}
307+
308+
// If requested, debug-dump the parsed expression.
318309
if tags.remove("expr") {
319-
write!(output, " ← {}", expr.format(&Node::Nothing { columns: vec![] }))?;
310+
write!(output, " ← {:?}", expr)?;
320311
}
312+
writeln!(output)?;
321313

322314
// Reject unknown tags.
323315
if let Some(tag) = tags.iter().next() {
324316
return Err(format!("unknown tag {tag}").into());
325317
}
326318

327-
writeln!(output)?;
328319
Ok(output)
329320
}
330321
}

src/sql/testscripts/expressions/cnf

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,28 @@
33
# Noop for non-boolean expressions.
44
[cnf]> 1 + 2
55
---
6-
Integer(3) ← 1 + 2
6+
3 ← 1 + 2
77

88
# Applies De Morgan's laws.
99
[cnf]> NOT (TRUE AND FALSE)
1010
---
11-
Boolean(true) ← NOT TRUE OR NOT FALSE
11+
TRUE ← NOT TRUE OR NOT FALSE
1212

1313
[cnf]> NOT (TRUE OR FALSE)
1414
---
15-
Boolean(false) ← NOT TRUE AND NOT FALSE
15+
FALSE ← NOT TRUE AND NOT FALSE
1616

1717
# NOTs are pushed into the expression.
1818
[cnf]> NOT (TRUE AND TRUE AND TRUE OR TRUE)
1919
---
20-
Boolean(false) ← (NOT TRUE OR NOT TRUE OR NOT TRUE) AND NOT TRUE
20+
FALSE ← (NOT TRUE OR NOT TRUE OR NOT TRUE) AND NOT TRUE
2121

2222
# ORs are converted to ANDs by the distributive law.
2323
[cnf]> (TRUE AND FALSE) OR (FALSE AND TRUE)
2424
---
25-
Boolean(false) ← (TRUE OR FALSE) AND (TRUE OR TRUE) AND (FALSE OR FALSE) AND (FALSE OR TRUE)
25+
FALSE ← (TRUE OR FALSE) AND (TRUE OR TRUE) AND (FALSE OR FALSE) AND (FALSE OR TRUE)
2626

2727
# This is also true when combined with De Morgan's laws.
2828
[cnf]> NOT ((TRUE OR FALSE) AND (TRUE OR FALSE))
2929
---
30-
Boolean(false) ← (NOT TRUE OR NOT TRUE) AND (NOT TRUE OR NOT FALSE) AND (NOT FALSE OR NOT TRUE) AND (NOT FALSE OR NOT FALSE)
30+
FALSE ← (NOT TRUE OR NOT TRUE) AND (NOT TRUE OR NOT FALSE) AND (NOT FALSE OR NOT TRUE) AND (NOT FALSE OR NOT FALSE)

src/sql/testscripts/expressions/func

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44
> sqrt(1)
55
> SQRT(1)
66
---
7-
Float(1.0)
8-
Float(1.0)
7+
1.0
8+
1.0
99

1010
# A space is allowed around the arguments.
1111
> sqrt ( 1 )
1212
---
13-
Float(1.0)
13+
1.0
1414

1515
# Wrong number of arguments errors.
1616
!> sqrt()

src/sql/testscripts/expressions/func_sqrt

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,27 @@
44
[expr]> sqrt(2)
55
[expr]> sqrt(100)
66
---
7-
Float(1.4142135623730951)sqrt(2)
8-
Float(10.0)sqrt(100)
7+
1.4142135623730951 ← SquareRoot(Constant(Integer(2)))
8+
10.0 ← SquareRoot(Constant(Integer(100)))
99

1010
# Negative integers error, but 0 is valid.
1111
!> sqrt(-1)
1212
> sqrt(0)
1313
---
1414
Error: invalid input: can't take negative square root
15-
Float(0.0)
15+
0.0
1616

1717
# Floats work.
1818
> sqrt(3.14)
1919
> sqrt(100.0)
2020
---
21-
Float(1.772004514666935)
22-
Float(10.0)
21+
1.772004514666935
22+
10.0
2323

2424
# Negative floats work, but return NAN.
2525
> sqrt(-1.0)
2626
---
27-
Float(NaN)
27+
NaN
2828

2929
# Test various special float values.
3030
> sqrt(-0.0)
@@ -33,16 +33,16 @@ Float(NaN)
3333
> sqrt(INFINITY)
3434
> sqrt(-INFINITY)
3535
---
36-
Float(-0.0)
37-
Float(0.0)
38-
Float(NaN)
39-
Float(inf)
40-
Float(NaN)
36+
-0.0
37+
0.0
38+
NaN
39+
inf
40+
NaN
4141

4242
# NULL is passed through.
4343
> sqrt(NULL)
4444
---
45-
Null
45+
NULL
4646

4747
# Strings and booleans error.
4848
!> sqrt(TRUE)

0 commit comments

Comments
 (0)