mirror of
https://github.com/aljazceru/turso.git
synced 2026-02-05 00:04:23 +01:00
Merge 'Expression equality checking, some optimizations' from Preston Thorpe
This PR mainly adds custom logic to check equality in ast expressions. Not sure if this belongs in the `vendored` parser or not, let me know and I'll bring it out. Also replaces `Vec` arguments with slice refs where possible, as well as some clippy warnings in the same `emitter` file. I'll write some more tests tomorrow to make sure this is as thorough as possible. EDIT: failed test same issue referenced in #484. Marking as draft until more tests + cases added Reviewed-by: Jussi Saurio <jussi.saurio@gmail.com> Closes #488
This commit is contained in:
2
Makefile
2
Makefile
@@ -61,7 +61,7 @@ test: limbo test-compat test-sqlite3 test-shell
|
||||
.PHONY: test
|
||||
|
||||
test-shell: limbo
|
||||
./testing/shelltests.py
|
||||
SQLITE_EXEC=$(SQLITE_EXEC) ./testing/shelltests.py
|
||||
.PHONY: test-shell
|
||||
|
||||
test-compat:
|
||||
|
||||
@@ -646,7 +646,6 @@ fn get_io(db: &str) -> anyhow::Result<Arc<dyn limbo_core::IO>> {
|
||||
const HELP_MSG: &str = r#"
|
||||
Limbo SQL Shell Help
|
||||
==============
|
||||
|
||||
Welcome to the Limbo SQL Shell! You can execute any standard SQL command here.
|
||||
In addition to standard SQL commands, the following special commands are available:
|
||||
|
||||
@@ -689,12 +688,6 @@ Usage Examples:
|
||||
8. Show the current values of settings:
|
||||
.show
|
||||
|
||||
9. Set the value 'NULL' to be displayed for null values instead of empty string:
|
||||
.nullvalue "NULL"
|
||||
|
||||
Note:
|
||||
-----
|
||||
- All SQL commands must end with a semicolon (;).
|
||||
- Special commands do not require a semicolon.
|
||||
|
||||
"#;
|
||||
- Special commands do not require a semicolon."#;
|
||||
|
||||
@@ -2,7 +2,7 @@ mod app;
|
||||
mod opcodes_dictionary;
|
||||
|
||||
use rustyline::{error::ReadlineError, DefaultEditor};
|
||||
use std::sync::{atomic::Ordering, Arc};
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
#[allow(clippy::arc_with_non_send_sync)]
|
||||
fn main() -> anyhow::Result<()> {
|
||||
|
||||
@@ -5,12 +5,13 @@ use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::{Rc, Weak};
|
||||
|
||||
use sqlite3_parser::ast;
|
||||
use sqlite3_parser::ast::{self};
|
||||
|
||||
use crate::schema::{Column, PseudoTable, Table};
|
||||
use crate::storage::sqlite3_ondisk::DatabaseHeader;
|
||||
use crate::translate::plan::{IterationDirection, Search};
|
||||
use crate::types::{OwnedRecord, OwnedValue};
|
||||
use crate::util::exprs_are_equivalent;
|
||||
use crate::vdbe::builder::ProgramBuilder;
|
||||
use crate::vdbe::{BranchOffset, Insn, Program};
|
||||
use crate::{Connection, Result};
|
||||
@@ -216,7 +217,7 @@ pub fn emit_program(
|
||||
// Clean up and close the main execution loop
|
||||
close_loop(
|
||||
&mut program,
|
||||
&mut plan.source,
|
||||
&plan.source,
|
||||
&mut metadata,
|
||||
&plan.referenced_tables,
|
||||
)?;
|
||||
@@ -235,7 +236,7 @@ pub fn emit_program(
|
||||
group_by,
|
||||
plan.order_by.as_ref(),
|
||||
&plan.aggregates,
|
||||
plan.limit.clone(),
|
||||
plan.limit,
|
||||
&plan.referenced_tables,
|
||||
&mut metadata,
|
||||
)?;
|
||||
@@ -259,7 +260,7 @@ pub fn emit_program(
|
||||
&mut program,
|
||||
order_by,
|
||||
&plan.result_columns,
|
||||
plan.limit.clone(),
|
||||
plan.limit,
|
||||
&mut metadata,
|
||||
)?;
|
||||
}
|
||||
@@ -274,7 +275,7 @@ pub fn emit_program(
|
||||
/// Initialize resources needed for ORDER BY processing
|
||||
fn init_order_by(
|
||||
program: &mut ProgramBuilder,
|
||||
order_by: &Vec<(ast::Expr, Direction)>,
|
||||
order_by: &[(ast::Expr, Direction)],
|
||||
metadata: &mut Metadata,
|
||||
) -> Result<()> {
|
||||
metadata
|
||||
@@ -301,7 +302,7 @@ fn init_order_by(
|
||||
fn init_group_by(
|
||||
program: &mut ProgramBuilder,
|
||||
group_by: &GroupBy,
|
||||
aggregates: &Vec<Aggregate>,
|
||||
aggregates: &[Aggregate],
|
||||
metadata: &mut Metadata,
|
||||
) -> Result<()> {
|
||||
let agg_final_label = program.allocate_label();
|
||||
@@ -866,8 +867,8 @@ fn inner_loop_emit(
|
||||
/// See the InnerLoopEmitTarget enum for more details.
|
||||
fn inner_loop_source_emit(
|
||||
program: &mut ProgramBuilder,
|
||||
result_columns: &Vec<ResultSetColumn>,
|
||||
aggregates: &Vec<Aggregate>,
|
||||
result_columns: &[ResultSetColumn],
|
||||
aggregates: &[Aggregate],
|
||||
metadata: &mut Metadata,
|
||||
emit_target: InnerLoopEmitTarget,
|
||||
referenced_tables: &[BTreeTableReference],
|
||||
@@ -927,7 +928,7 @@ fn inner_loop_source_emit(
|
||||
order_by,
|
||||
result_columns,
|
||||
&mut metadata.result_column_indexes_in_orderby_sorter,
|
||||
&metadata.sort_metadata.as_ref().unwrap(),
|
||||
metadata.sort_metadata.as_ref().unwrap(),
|
||||
None,
|
||||
)?;
|
||||
Ok(())
|
||||
@@ -1123,12 +1124,13 @@ fn close_loop(
|
||||
/// Emits the bytecode for processing a GROUP BY clause.
|
||||
/// This is called when the main query execution loop has finished processing,
|
||||
/// and we now have data in the GROUP BY sorter.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn group_by_emit(
|
||||
program: &mut ProgramBuilder,
|
||||
result_columns: &Vec<ResultSetColumn>,
|
||||
result_columns: &[ResultSetColumn],
|
||||
group_by: &GroupBy,
|
||||
order_by: Option<&Vec<(ast::Expr, Direction)>>,
|
||||
aggregates: &Vec<Aggregate>,
|
||||
aggregates: &[Aggregate],
|
||||
limit: Option<usize>,
|
||||
referenced_tables: &[BTreeTableReference],
|
||||
metadata: &mut Metadata,
|
||||
@@ -1437,7 +1439,7 @@ fn group_by_emit(
|
||||
order_by,
|
||||
result_columns,
|
||||
&mut metadata.result_column_indexes_in_orderby_sorter,
|
||||
&metadata.sort_metadata.as_ref().unwrap(),
|
||||
metadata.sort_metadata.as_ref().unwrap(),
|
||||
Some(&precomputed_exprs_to_register),
|
||||
)?;
|
||||
}
|
||||
@@ -1474,9 +1476,9 @@ fn group_by_emit(
|
||||
/// and we can now materialize the aggregate results.
|
||||
fn agg_without_group_by_emit(
|
||||
program: &mut ProgramBuilder,
|
||||
referenced_tables: &Vec<BTreeTableReference>,
|
||||
result_columns: &Vec<ResultSetColumn>,
|
||||
aggregates: &Vec<Aggregate>,
|
||||
referenced_tables: &[BTreeTableReference],
|
||||
result_columns: &[ResultSetColumn],
|
||||
aggregates: &[Aggregate],
|
||||
metadata: &mut Metadata,
|
||||
) -> Result<()> {
|
||||
let agg_start_reg = metadata.aggregation_start_register.unwrap();
|
||||
@@ -1513,8 +1515,8 @@ fn agg_without_group_by_emit(
|
||||
/// and we can now emit rows from the ORDER BY sorter.
|
||||
fn order_by_emit(
|
||||
program: &mut ProgramBuilder,
|
||||
order_by: &Vec<(ast::Expr, Direction)>,
|
||||
result_columns: &Vec<ResultSetColumn>,
|
||||
order_by: &[(ast::Expr, Direction)],
|
||||
result_columns: &[ResultSetColumn],
|
||||
limit: Option<usize>,
|
||||
metadata: &mut Metadata,
|
||||
) -> Result<()> {
|
||||
@@ -1693,8 +1695,8 @@ fn sorter_insert(
|
||||
fn order_by_sorter_insert(
|
||||
program: &mut ProgramBuilder,
|
||||
referenced_tables: &[BTreeTableReference],
|
||||
order_by: &Vec<(ast::Expr, Direction)>,
|
||||
result_columns: &Vec<ResultSetColumn>,
|
||||
order_by: &[(ast::Expr, Direction)],
|
||||
result_columns: &[ResultSetColumn],
|
||||
result_column_indexes_in_orderby_sorter: &mut HashMap<usize, usize>,
|
||||
sort_metadata: &SortMetadata,
|
||||
precomputed_exprs_to_register: Option<&Vec<(&ast::Expr, usize)>>,
|
||||
@@ -1760,18 +1762,15 @@ fn order_by_sorter_insert(
|
||||
///
|
||||
/// If any result columns can be skipped, this returns list of 2-tuples of (SkippedResultColumnIndex: usize, ResultColumnIndexInOrderBySorter: usize)
|
||||
fn order_by_deduplicate_result_columns(
|
||||
order_by: &Vec<(ast::Expr, Direction)>,
|
||||
result_columns: &Vec<ResultSetColumn>,
|
||||
order_by: &[(ast::Expr, Direction)],
|
||||
result_columns: &[ResultSetColumn],
|
||||
) -> Option<Vec<(usize, usize)>> {
|
||||
let mut result_column_remapping: Option<Vec<(usize, usize)>> = None;
|
||||
for (i, rc) in result_columns.iter().enumerate() {
|
||||
// TODO: implement a custom equality check for expressions
|
||||
// there are lots of examples where this breaks, even simple ones like
|
||||
// sum(x) != SUM(x)
|
||||
let found = order_by
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, (expr, _))| expr == &rc.expr);
|
||||
.find(|(_, (expr, _))| exprs_are_equivalent(expr, &rc.expr));
|
||||
if let Some((j, _)) = found {
|
||||
if let Some(ref mut v) = result_column_remapping {
|
||||
v.push((i, j));
|
||||
@@ -1781,5 +1780,5 @@ fn order_by_deduplicate_result_columns(
|
||||
}
|
||||
}
|
||||
|
||||
return result_column_remapping;
|
||||
result_column_remapping
|
||||
}
|
||||
|
||||
@@ -392,7 +392,7 @@ fn update_pragma(name: &str, value: i64, header: Rc<RefCell<DatabaseHeader>>, pa
|
||||
struct TableFormatter<'a> {
|
||||
body: &'a ast::CreateTableBody,
|
||||
}
|
||||
impl<'a> Display for TableFormatter<'a> {
|
||||
impl Display for TableFormatter<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.body.to_fmt(f)
|
||||
}
|
||||
|
||||
465
core/util.rs
465
core/util.rs
@@ -1,5 +1,7 @@
|
||||
use std::{rc::Rc, sync::Arc};
|
||||
|
||||
use sqlite3_parser::ast::{Expr, FunctionTail, Literal};
|
||||
|
||||
use crate::{
|
||||
schema::{self, Schema},
|
||||
Result, RowResult, Rows, IO,
|
||||
@@ -55,3 +57,466 @@ pub fn parse_schema_rows(rows: Option<Rows>, schema: &mut Schema, io: Arc<dyn IO
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmp_numeric_strings(num_str: &str, other: &str) -> bool {
|
||||
match (num_str.parse::<f64>(), other.parse::<f64>()) {
|
||||
(Ok(num), Ok(other)) => num == other,
|
||||
_ => num_str == other,
|
||||
}
|
||||
}
|
||||
|
||||
const QUOTE_PAIRS: &[(char, char)] = &[('"', '"'), ('[', ']'), ('`', '`')];
|
||||
pub fn check_ident_equivalency(ident1: &str, ident2: &str) -> bool {
|
||||
fn strip_quotes(identifier: &str) -> &str {
|
||||
for &(start, end) in QUOTE_PAIRS {
|
||||
if identifier.starts_with(start) && identifier.ends_with(end) {
|
||||
return &identifier[1..identifier.len() - 1];
|
||||
}
|
||||
}
|
||||
identifier
|
||||
}
|
||||
strip_quotes(ident1).eq_ignore_ascii_case(strip_quotes(ident2))
|
||||
}
|
||||
|
||||
pub fn check_literal_equivalency(lhs: &Literal, rhs: &Literal) -> bool {
|
||||
match (lhs, rhs) {
|
||||
(Literal::Numeric(n1), Literal::Numeric(n2)) => cmp_numeric_strings(n1, n2),
|
||||
(Literal::String(s1), Literal::String(s2)) => check_ident_equivalency(s1, s2),
|
||||
(Literal::Blob(b1), Literal::Blob(b2)) => b1 == b2,
|
||||
(Literal::Keyword(k1), Literal::Keyword(k2)) => check_ident_equivalency(k1, k2),
|
||||
(Literal::Null, Literal::Null) => true,
|
||||
(Literal::CurrentDate, Literal::CurrentDate) => true,
|
||||
(Literal::CurrentTime, Literal::CurrentTime) => true,
|
||||
(Literal::CurrentTimestamp, Literal::CurrentTimestamp) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// This function is used to determine whether two expressions are logically
|
||||
/// equivalent in the context of queries, even if their representations
|
||||
/// differ. e.g.: `SUM(x)` and `sum(x)`, `x + y` and `y + x`
|
||||
///
|
||||
/// *Note*: doesn't attempt to evaluate/compute "constexpr" results
|
||||
pub fn exprs_are_equivalent(expr1: &Expr, expr2: &Expr) -> bool {
|
||||
match (expr1, expr2) {
|
||||
(
|
||||
Expr::Between {
|
||||
lhs: lhs1,
|
||||
not: not1,
|
||||
start: start1,
|
||||
end: end1,
|
||||
},
|
||||
Expr::Between {
|
||||
lhs: lhs2,
|
||||
not: not2,
|
||||
start: start2,
|
||||
end: end2,
|
||||
},
|
||||
) => {
|
||||
not1 == not2
|
||||
&& exprs_are_equivalent(lhs1, lhs2)
|
||||
&& exprs_are_equivalent(start1, start2)
|
||||
&& exprs_are_equivalent(end1, end2)
|
||||
}
|
||||
(Expr::Binary(lhs1, op1, rhs1), Expr::Binary(lhs2, op2, rhs2)) => {
|
||||
op1 == op2
|
||||
&& ((exprs_are_equivalent(lhs1, lhs2) && exprs_are_equivalent(rhs1, rhs2))
|
||||
|| (op1.is_commutative()
|
||||
&& exprs_are_equivalent(lhs1, rhs2)
|
||||
&& exprs_are_equivalent(rhs1, lhs2)))
|
||||
}
|
||||
(
|
||||
Expr::Case {
|
||||
base: base1,
|
||||
when_then_pairs: pairs1,
|
||||
else_expr: else1,
|
||||
},
|
||||
Expr::Case {
|
||||
base: base2,
|
||||
when_then_pairs: pairs2,
|
||||
else_expr: else2,
|
||||
},
|
||||
) => {
|
||||
base1 == base2
|
||||
&& pairs1.len() == pairs2.len()
|
||||
&& pairs1.iter().zip(pairs2).all(|((w1, t1), (w2, t2))| {
|
||||
exprs_are_equivalent(w1, w2) && exprs_are_equivalent(t1, t2)
|
||||
})
|
||||
&& else1 == else2
|
||||
}
|
||||
(
|
||||
Expr::Cast {
|
||||
expr: expr1,
|
||||
type_name: type1,
|
||||
},
|
||||
Expr::Cast {
|
||||
expr: expr2,
|
||||
type_name: type2,
|
||||
},
|
||||
) => {
|
||||
exprs_are_equivalent(expr1, expr2)
|
||||
&& match (type1, type2) {
|
||||
(Some(t1), Some(t2)) => t1.name.eq_ignore_ascii_case(&t2.name),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
(Expr::Collate(expr1, collation1), Expr::Collate(expr2, collation2)) => {
|
||||
exprs_are_equivalent(expr1, expr2) && collation1.eq_ignore_ascii_case(collation2)
|
||||
}
|
||||
(
|
||||
Expr::FunctionCall {
|
||||
name: name1,
|
||||
distinctness: distinct1,
|
||||
args: args1,
|
||||
order_by: order1,
|
||||
filter_over: filter1,
|
||||
},
|
||||
Expr::FunctionCall {
|
||||
name: name2,
|
||||
distinctness: distinct2,
|
||||
args: args2,
|
||||
order_by: order2,
|
||||
filter_over: filter2,
|
||||
},
|
||||
) => {
|
||||
name1.0.eq_ignore_ascii_case(&name2.0)
|
||||
&& distinct1 == distinct2
|
||||
&& args1 == args2
|
||||
&& order1 == order2
|
||||
&& filter1 == filter2
|
||||
}
|
||||
(
|
||||
Expr::FunctionCallStar {
|
||||
name: name1,
|
||||
filter_over: filter1,
|
||||
},
|
||||
Expr::FunctionCallStar {
|
||||
name: name2,
|
||||
filter_over: filter2,
|
||||
},
|
||||
) => {
|
||||
name1.0.eq_ignore_ascii_case(&name2.0)
|
||||
&& match (filter1, filter2) {
|
||||
(None, None) => true,
|
||||
(
|
||||
Some(FunctionTail {
|
||||
filter_clause: fc1,
|
||||
over_clause: oc1,
|
||||
}),
|
||||
Some(FunctionTail {
|
||||
filter_clause: fc2,
|
||||
over_clause: oc2,
|
||||
}),
|
||||
) => match ((fc1, fc2), (oc1, oc2)) {
|
||||
((Some(fc1), Some(fc2)), (Some(oc1), Some(oc2))) => {
|
||||
exprs_are_equivalent(fc1, fc2) && oc1 == oc2
|
||||
}
|
||||
((Some(fc1), Some(fc2)), _) => exprs_are_equivalent(fc1, fc2),
|
||||
_ => false,
|
||||
},
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
(Expr::NotNull(expr1), Expr::NotNull(expr2)) => exprs_are_equivalent(expr1, expr2),
|
||||
(Expr::IsNull(expr1), Expr::IsNull(expr2)) => exprs_are_equivalent(expr1, expr2),
|
||||
(Expr::Literal(lit1), Expr::Literal(lit2)) => check_literal_equivalency(lit1, lit2),
|
||||
(Expr::Id(id1), Expr::Id(id2)) => check_ident_equivalency(&id1.0, &id2.0),
|
||||
(Expr::Unary(op1, expr1), Expr::Unary(op2, expr2)) => {
|
||||
op1 == op2 && exprs_are_equivalent(expr1, expr2)
|
||||
}
|
||||
(Expr::Variable(var1), Expr::Variable(var2)) => var1 == var2,
|
||||
(Expr::Parenthesized(exprs1), Expr::Parenthesized(exprs2)) => {
|
||||
exprs1.len() == exprs2.len()
|
||||
&& exprs1
|
||||
.iter()
|
||||
.zip(exprs2)
|
||||
.all(|(e1, e2)| exprs_are_equivalent(e1, e2))
|
||||
}
|
||||
(Expr::Parenthesized(exprs1), exprs2) | (exprs2, Expr::Parenthesized(exprs1)) => {
|
||||
exprs1.len() == 1 && exprs_are_equivalent(&exprs1[0], exprs2)
|
||||
}
|
||||
(Expr::Qualified(tn1, cn1), Expr::Qualified(tn2, cn2)) => {
|
||||
check_ident_equivalency(&tn1.0, &tn2.0) && check_ident_equivalency(&cn1.0, &cn2.0)
|
||||
}
|
||||
(Expr::DoublyQualified(sn1, tn1, cn1), Expr::DoublyQualified(sn2, tn2, cn2)) => {
|
||||
check_ident_equivalency(&sn1.0, &sn2.0)
|
||||
&& check_ident_equivalency(&tn1.0, &tn2.0)
|
||||
&& check_ident_equivalency(&cn1.0, &cn2.0)
|
||||
}
|
||||
(
|
||||
Expr::InList {
|
||||
lhs: lhs1,
|
||||
not: not1,
|
||||
rhs: rhs1,
|
||||
},
|
||||
Expr::InList {
|
||||
lhs: lhs2,
|
||||
not: not2,
|
||||
rhs: rhs2,
|
||||
},
|
||||
) => {
|
||||
*not1 == *not2
|
||||
&& exprs_are_equivalent(lhs1, lhs2)
|
||||
&& rhs1
|
||||
.as_ref()
|
||||
.zip(rhs2.as_ref())
|
||||
.map(|(list1, list2)| {
|
||||
list1.len() == list2.len()
|
||||
&& list1
|
||||
.iter()
|
||||
.zip(list2)
|
||||
.all(|(e1, e2)| exprs_are_equivalent(e1, e2))
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
// fall back to naive equality check
|
||||
_ => expr1 == expr2,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use sqlite3_parser::ast::{self, Expr, Id, Literal, Operator::*, Type};
|
||||
#[test]
|
||||
fn test_basic_addition_exprs_are_equivalent() {
|
||||
let expr1 = Expr::Binary(
|
||||
Box::new(Expr::Literal(Literal::Numeric("826".to_string()))),
|
||||
Add,
|
||||
Box::new(Expr::Literal(Literal::Numeric("389".to_string()))),
|
||||
);
|
||||
let expr2 = Expr::Binary(
|
||||
Box::new(Expr::Literal(Literal::Numeric("389".to_string()))),
|
||||
Add,
|
||||
Box::new(Expr::Literal(Literal::Numeric("826".to_string()))),
|
||||
);
|
||||
assert!(super::exprs_are_equivalent(&expr1, &expr2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_addition_expressions_equivalent_normalized() {
|
||||
let expr1 = Expr::Binary(
|
||||
Box::new(Expr::Literal(Literal::Numeric("123.0".to_string()))),
|
||||
Add,
|
||||
Box::new(Expr::Literal(Literal::Numeric("243".to_string()))),
|
||||
);
|
||||
let expr2 = Expr::Binary(
|
||||
Box::new(Expr::Literal(Literal::Numeric("243.0".to_string()))),
|
||||
Add,
|
||||
Box::new(Expr::Literal(Literal::Numeric("123".to_string()))),
|
||||
);
|
||||
assert!(super::exprs_are_equivalent(&expr1, &expr2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_subtraction_expressions_not_equivalent() {
|
||||
let expr3 = Expr::Binary(
|
||||
Box::new(Expr::Literal(Literal::Numeric("364".to_string()))),
|
||||
Subtract,
|
||||
Box::new(Expr::Literal(Literal::Numeric("22.0".to_string()))),
|
||||
);
|
||||
let expr4 = Expr::Binary(
|
||||
Box::new(Expr::Literal(Literal::Numeric("22.0".to_string()))),
|
||||
Subtract,
|
||||
Box::new(Expr::Literal(Literal::Numeric("364".to_string()))),
|
||||
);
|
||||
assert!(!super::exprs_are_equivalent(&expr3, &expr4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_subtraction_expressions_normalized() {
|
||||
let expr3 = Expr::Binary(
|
||||
Box::new(Expr::Literal(Literal::Numeric("66.0".to_string()))),
|
||||
Subtract,
|
||||
Box::new(Expr::Literal(Literal::Numeric("22".to_string()))),
|
||||
);
|
||||
let expr4 = Expr::Binary(
|
||||
Box::new(Expr::Literal(Literal::Numeric("66".to_string()))),
|
||||
Subtract,
|
||||
Box::new(Expr::Literal(Literal::Numeric("22.0".to_string()))),
|
||||
);
|
||||
assert!(super::exprs_are_equivalent(&expr3, &expr4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_expressions_equivalent_case_insensitive_functioncalls() {
|
||||
let func1 = Expr::FunctionCall {
|
||||
name: Id("SUM".to_string()),
|
||||
distinctness: None,
|
||||
args: Some(vec![Expr::Id(Id("x".to_string()))]),
|
||||
order_by: None,
|
||||
filter_over: None,
|
||||
};
|
||||
let func2 = Expr::FunctionCall {
|
||||
name: Id("sum".to_string()),
|
||||
distinctness: None,
|
||||
args: Some(vec![Expr::Id(Id("x".to_string()))]),
|
||||
order_by: None,
|
||||
filter_over: None,
|
||||
};
|
||||
assert!(super::exprs_are_equivalent(&func1, &func2));
|
||||
|
||||
let func3 = Expr::FunctionCall {
|
||||
name: Id("SUM".to_string()),
|
||||
distinctness: Some(ast::Distinctness::Distinct),
|
||||
args: Some(vec![Expr::Id(Id("x".to_string()))]),
|
||||
order_by: None,
|
||||
filter_over: None,
|
||||
};
|
||||
assert!(!super::exprs_are_equivalent(&func1, &func3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_expressions_equivalent_identical_fn_with_distinct() {
|
||||
let sum = Expr::FunctionCall {
|
||||
name: Id("SUM".to_string()),
|
||||
distinctness: None,
|
||||
args: Some(vec![Expr::Id(Id("x".to_string()))]),
|
||||
order_by: None,
|
||||
filter_over: None,
|
||||
};
|
||||
let sum_distinct = Expr::FunctionCall {
|
||||
name: Id("SUM".to_string()),
|
||||
distinctness: Some(ast::Distinctness::Distinct),
|
||||
args: Some(vec![Expr::Id(Id("x".to_string()))]),
|
||||
order_by: None,
|
||||
filter_over: None,
|
||||
};
|
||||
assert!(!super::exprs_are_equivalent(&sum, &sum_distinct));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_expressions_equivalent_multiplicaiton() {
|
||||
let expr1 = Expr::Binary(
|
||||
Box::new(Expr::Literal(Literal::Numeric("42.0".to_string()))),
|
||||
Multiply,
|
||||
Box::new(Expr::Literal(Literal::Numeric("38".to_string()))),
|
||||
);
|
||||
let expr2 = Expr::Binary(
|
||||
Box::new(Expr::Literal(Literal::Numeric("38.0".to_string()))),
|
||||
Multiply,
|
||||
Box::new(Expr::Literal(Literal::Numeric("42".to_string()))),
|
||||
);
|
||||
assert!(super::exprs_are_equivalent(&expr1, &expr2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_expressions_both_parenthesized_equivalent() {
|
||||
let expr1 = Expr::Parenthesized(vec![Expr::Binary(
|
||||
Box::new(Expr::Literal(Literal::Numeric("683".to_string()))),
|
||||
Add,
|
||||
Box::new(Expr::Literal(Literal::Numeric("799.0".to_string()))),
|
||||
)]);
|
||||
let expr2 = Expr::Binary(
|
||||
Box::new(Expr::Literal(Literal::Numeric("799".to_string()))),
|
||||
Add,
|
||||
Box::new(Expr::Literal(Literal::Numeric("683".to_string()))),
|
||||
);
|
||||
assert!(super::exprs_are_equivalent(&expr1, &expr2));
|
||||
}
|
||||
#[test]
|
||||
fn test_expressions_parenthesized_equivalent() {
|
||||
let expr7 = Expr::Parenthesized(vec![Expr::Binary(
|
||||
Box::new(Expr::Literal(Literal::Numeric("6".to_string()))),
|
||||
Add,
|
||||
Box::new(Expr::Literal(Literal::Numeric("7".to_string()))),
|
||||
)]);
|
||||
let expr8 = Expr::Binary(
|
||||
Box::new(Expr::Literal(Literal::Numeric("6".to_string()))),
|
||||
Add,
|
||||
Box::new(Expr::Literal(Literal::Numeric("7".to_string()))),
|
||||
);
|
||||
assert!(super::exprs_are_equivalent(&expr7, &expr8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_like_expressions_equivalent() {
|
||||
let expr1 = Expr::Like {
|
||||
lhs: Box::new(Expr::Id(Id("name".to_string()))),
|
||||
not: false,
|
||||
op: ast::LikeOperator::Like,
|
||||
rhs: Box::new(Expr::Literal(Literal::String("%john%".to_string()))),
|
||||
escape: Some(Box::new(Expr::Literal(Literal::String("\\".to_string())))),
|
||||
};
|
||||
let expr2 = Expr::Like {
|
||||
lhs: Box::new(Expr::Id(Id("name".to_string()))),
|
||||
not: false,
|
||||
op: ast::LikeOperator::Like,
|
||||
rhs: Box::new(Expr::Literal(Literal::String("%john%".to_string()))),
|
||||
escape: Some(Box::new(Expr::Literal(Literal::String("\\".to_string())))),
|
||||
};
|
||||
assert!(super::exprs_are_equivalent(&expr1, &expr2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_expressions_equivalent_like_escaped() {
|
||||
let expr1 = Expr::Like {
|
||||
lhs: Box::new(Expr::Id(Id("name".to_string()))),
|
||||
not: false,
|
||||
op: ast::LikeOperator::Like,
|
||||
rhs: Box::new(Expr::Literal(Literal::String("%john%".to_string()))),
|
||||
escape: Some(Box::new(Expr::Literal(Literal::String("\\".to_string())))),
|
||||
};
|
||||
let expr2 = Expr::Like {
|
||||
lhs: Box::new(Expr::Id(Id("name".to_string()))),
|
||||
not: false,
|
||||
op: ast::LikeOperator::Like,
|
||||
rhs: Box::new(Expr::Literal(Literal::String("%john%".to_string()))),
|
||||
escape: Some(Box::new(Expr::Literal(Literal::String("#".to_string())))),
|
||||
};
|
||||
assert!(!super::exprs_are_equivalent(&expr1, &expr2));
|
||||
}
|
||||
#[test]
|
||||
fn test_expressions_equivalent_between() {
|
||||
let expr1 = Expr::Between {
|
||||
lhs: Box::new(Expr::Id(Id("age".to_string()))),
|
||||
not: false,
|
||||
start: Box::new(Expr::Literal(Literal::Numeric("18".to_string()))),
|
||||
end: Box::new(Expr::Literal(Literal::Numeric("65".to_string()))),
|
||||
};
|
||||
let expr2 = Expr::Between {
|
||||
lhs: Box::new(Expr::Id(Id("age".to_string()))),
|
||||
not: false,
|
||||
start: Box::new(Expr::Literal(Literal::Numeric("18".to_string()))),
|
||||
end: Box::new(Expr::Literal(Literal::Numeric("65".to_string()))),
|
||||
};
|
||||
assert!(super::exprs_are_equivalent(&expr1, &expr2));
|
||||
|
||||
// differing BETWEEN bounds
|
||||
let expr3 = Expr::Between {
|
||||
lhs: Box::new(Expr::Id(Id("age".to_string()))),
|
||||
not: false,
|
||||
start: Box::new(Expr::Literal(Literal::Numeric("20".to_string()))),
|
||||
end: Box::new(Expr::Literal(Literal::Numeric("65".to_string()))),
|
||||
};
|
||||
assert!(!super::exprs_are_equivalent(&expr1, &expr3));
|
||||
}
|
||||
#[test]
|
||||
fn test_cast_exprs_equivalent() {
|
||||
let cast1 = Expr::Cast {
|
||||
expr: Box::new(Expr::Literal(Literal::Numeric("123".to_string()))),
|
||||
type_name: Some(Type {
|
||||
name: "INTEGER".to_string(),
|
||||
size: None,
|
||||
}),
|
||||
};
|
||||
|
||||
let cast2 = Expr::Cast {
|
||||
expr: Box::new(Expr::Literal(Literal::Numeric("123".to_string()))),
|
||||
type_name: Some(Type {
|
||||
name: "integer".to_string(),
|
||||
size: None,
|
||||
}),
|
||||
};
|
||||
assert!(super::exprs_are_equivalent(&cast1, &cast2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ident_equivalency() {
|
||||
assert!(super::check_ident_equivalency("\"foo\"", "foo"));
|
||||
assert!(super::check_ident_equivalency("[foo]", "foo"));
|
||||
assert!(super::check_ident_equivalency("`FOO`", "foo"));
|
||||
assert!(super::check_ident_equivalency("\"foo\"", "`FOO`"));
|
||||
assert!(!super::check_ident_equivalency("\"foo\"", "[bar]"));
|
||||
assert!(!super::check_ident_equivalency("foo", "\"bar\""));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import os
|
||||
import subprocess
|
||||
|
||||
# Configuration
|
||||
sqlite_exec = "./target/debug/limbo"
|
||||
sqlite_exec = os.getenv("SQLITE_EXEC", "./target/debug/limbo")
|
||||
cwd = os.getcwd()
|
||||
|
||||
# Initial setup commands
|
||||
|
||||
@@ -648,6 +648,21 @@ impl From<YYCODETYPE> for Operator {
|
||||
}
|
||||
}
|
||||
|
||||
impl Operator {
|
||||
/// returns whether order of operations can be ignored
|
||||
pub fn is_commutative(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Operator::Add
|
||||
| Operator::Multiply
|
||||
| Operator::BitwiseAnd
|
||||
| Operator::BitwiseOr
|
||||
| Operator::Equals
|
||||
| Operator::NotEquals
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Unary operators
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum UnaryOperator {
|
||||
|
||||
Reference in New Issue
Block a user