diff --git a/Makefile b/Makefile index ee2f15e0d..096c0a679 100644 --- a/Makefile +++ b/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: diff --git a/cli/app.rs b/cli/app.rs index 0276ae245..496a56b2a 100644 --- a/cli/app.rs +++ b/cli/app.rs @@ -646,7 +646,6 @@ fn get_io(db: &str) -> anyhow::Result> { 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."#; diff --git a/cli/main.rs b/cli/main.rs index 3671d47c9..9977f6540 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -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<()> { diff --git a/core/translate/emitter.rs b/core/translate/emitter.rs index ce2b4d0b2..38311b9d9 100644 --- a/core/translate/emitter.rs +++ b/core/translate/emitter.rs @@ -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, + 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, - aggregates: &Vec, + 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, + result_columns: &[ResultSetColumn], group_by: &GroupBy, order_by: Option<&Vec<(ast::Expr, Direction)>>, - aggregates: &Vec, + aggregates: &[Aggregate], limit: Option, 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, - result_columns: &Vec, - aggregates: &Vec, + 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, + order_by: &[(ast::Expr, Direction)], + result_columns: &[ResultSetColumn], limit: Option, 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, + order_by: &[(ast::Expr, Direction)], + result_columns: &[ResultSetColumn], result_column_indexes_in_orderby_sorter: &mut HashMap, 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, + order_by: &[(ast::Expr, Direction)], + result_columns: &[ResultSetColumn], ) -> Option> { let mut result_column_remapping: Option> = 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 } diff --git a/core/translate/mod.rs b/core/translate/mod.rs index cb2463239..ef06e1467 100644 --- a/core/translate/mod.rs +++ b/core/translate/mod.rs @@ -392,7 +392,7 @@ fn update_pragma(name: &str, value: i64, header: Rc>, 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) } diff --git a/core/util.rs b/core/util.rs index ed1e60010..4b8a7f43b 100644 --- a/core/util.rs +++ b/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, schema: &mut Schema, io: Arc bool { + match (num_str.parse::(), other.parse::()) { + (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\"")); + } +} diff --git a/testing/shelltests.py b/testing/shelltests.py index 22c9ed122..f36972e25 100755 --- a/testing/shelltests.py +++ b/testing/shelltests.py @@ -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 diff --git a/vendored/sqlite3-parser/src/parser/ast/mod.rs b/vendored/sqlite3-parser/src/parser/ast/mod.rs index da1798cff..4ff8746f3 100644 --- a/vendored/sqlite3-parser/src/parser/ast/mod.rs +++ b/vendored/sqlite3-parser/src/parser/ast/mod.rs @@ -648,6 +648,21 @@ impl From 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 {