From 183ea8e362801a78b892a4e53761601196f135dd Mon Sep 17 00:00:00 2001 From: Alex Miller Date: Sat, 7 Dec 2024 21:04:03 -0800 Subject: [PATCH 01/15] Implement support for iif(). In sqlite, iif() looks like: sqlite> create table iiftest(a int, b int, c int); sqlite> explain select iif(a,b,c) from iiftest; addr opcode p1 p2 p3 p4 p5 comment ---- ------------- ---- ---- ---- ------------- -- ------------- 0 Init 0 11 0 0 Start at 11 1 OpenRead 0 2 0 3 0 root=2 iDb=0; iiftest 2 Rewind 0 10 0 0 3 Column 0 0 2 0 r[2]= cursor 0 column 0 4 IfNot 2 7 1 0 5 Column 0 1 1 0 r[1]= cursor 0 column 1 6 Goto 0 8 0 0 7 Column 0 2 1 0 r[1]= cursor 0 column 2 8 ResultRow 1 1 0 0 output=r[1] 9 Next 0 3 0 1 10 Halt 0 0 0 0 11 Transaction 0 0 1 0 1 usesStmtJournal=0 12 Goto 0 1 0 0 And with this change, in limbo it looks like: addr opcode p1 p2 p3 p4 p5 comment ---- ----------------- ---- ---- ---- ------------- -- ------- 0 Init 0 14 0 0 Start at 14 1 OpenReadAsync 0 2 0 0 table=iiftest, root=2 2 OpenReadAwait 0 0 0 0 3 RewindAsync 0 0 0 0 4 RewindAwait 0 13 0 0 Rewind table iiftest 5 Column 0 0 2 0 r[2]=iiftest.a 6 IfNot 2 9 1 0 if !r[2] goto 9 7 Column 0 1 1 0 r[1]=iiftest.b 8 Goto 0 10 0 0 9 Column 0 2 1 0 r[1]=iiftest.c 10 ResultRow 1 1 0 0 output=r[1] 11 NextAsync 0 0 0 0 12 NextAwait 0 5 0 0 13 Halt 0 0 0 0 14 Transaction 0 0 0 0 15 Goto 0 1 0 0 --- COMPAT.md | 2 +- core/function.rs | 3 ++ core/translate/expr.rs | 52 +++++++++++++++++++++++++++++++++++ core/vdbe/mod.rs | 1 + testing/scalar-functions.test | 8 ++++++ 5 files changed, 65 insertions(+), 1 deletion(-) diff --git a/COMPAT.md b/COMPAT.md index e909c794e..7501c5f7d 100644 --- a/COMPAT.md +++ b/COMPAT.md @@ -76,7 +76,7 @@ This document describes the SQLite compatibility status of Limbo: | glob(X,Y) | Yes | | | hex(X) | Yes | | | ifnull(X,Y) | Yes | | -| iif(X,Y,Z) | No | | +| iif(X,Y,Z) | Yes | | | instr(X,Y) | Yes | | | last_insert_rowid() | No | | | length(X) | Yes | | diff --git a/core/function.rs b/core/function.rs index c0baf807a..9591b1e01 100644 --- a/core/function.rs +++ b/core/function.rs @@ -56,6 +56,7 @@ pub enum ScalarFunc { ConcatWs, Glob, IfNull, + Iif, Instr, Like, Abs, @@ -96,6 +97,7 @@ impl Display for ScalarFunc { ScalarFunc::ConcatWs => "concat_ws".to_string(), ScalarFunc::Glob => "glob".to_string(), ScalarFunc::IfNull => "ifnull".to_string(), + ScalarFunc::Iif => "iif".to_string(), ScalarFunc::Instr => "instr".to_string(), ScalarFunc::Like => "like(2)".to_string(), ScalarFunc::Abs => "abs".to_string(), @@ -174,6 +176,7 @@ impl Func { "concat_ws" => Ok(Func::Scalar(ScalarFunc::ConcatWs)), "glob" => Ok(Func::Scalar(ScalarFunc::Glob)), "ifnull" => Ok(Func::Scalar(ScalarFunc::IfNull)), + "iif" => Ok(Func::Scalar(ScalarFunc::Iif)), "instr" => Ok(Func::Scalar(ScalarFunc::Instr)), "like" => Ok(Func::Scalar(ScalarFunc::Like)), "abs" => Ok(Func::Scalar(ScalarFunc::Abs)), diff --git a/core/translate/expr.rs b/core/translate/expr.rs index 6c0b4437d..e7324db64 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -967,6 +967,58 @@ pub fn translate_expr( Ok(target_register) } + ScalarFunc::Iif => { + let args = if args.as_ref().map(|args| args.len() != 3).unwrap_or(true) + { + crate::bail_parse_error!( + "{} requires exactly 3 arguments", + srf.to_string() + ); + } else { + args.as_ref().unwrap() + }; + let temp_reg = program.alloc_register(); + translate_expr( + program, + referenced_tables, + &args[0], + temp_reg, + precomputed_exprs_to_registers, + )?; + let jump_target_when_false = program.allocate_label(); + program.emit_insn_with_label_dependency( + Insn::IfNot { + reg: temp_reg, + target_pc: jump_target_when_false, + null_reg: 1, + }, + jump_target_when_false, + ); + translate_expr( + program, + referenced_tables, + &args[1], + target_register, + precomputed_exprs_to_registers, + )?; + let jump_target_result = program.allocate_label(); + program.emit_insn_with_label_dependency( + Insn::Goto { + target_pc: jump_target_result, + }, + jump_target_result, + ); + program.preassign_label_to_next_insn(jump_target_when_false); + translate_expr( + program, + referenced_tables, + &args[2], + target_register, + precomputed_exprs_to_registers, + )?; + program.preassign_label_to_next_insn(jump_target_result); + Ok(target_register) + } ScalarFunc::Glob | ScalarFunc::Like => { let args = if let Some(args) = args { if args.len() < 2 { diff --git a/core/vdbe/mod.rs b/core/vdbe/mod.rs index 906f43f73..ff5134f81 100644 --- a/core/vdbe/mod.rs +++ b/core/vdbe/mod.rs @@ -2105,6 +2105,7 @@ impl Program { state.registers[*dest] = result; } ScalarFunc::IfNull => {} + ScalarFunc::Iif => {} ScalarFunc::Instr => { let reg_value = &state.registers[*start_reg]; let pattern_value = &state.registers[*start_reg + 1]; diff --git a/testing/scalar-functions.test b/testing/scalar-functions.test index 64a9b9e5f..b0488bfae 100755 --- a/testing/scalar-functions.test +++ b/testing/scalar-functions.test @@ -75,6 +75,14 @@ do_execsql_test ifnull-2 { select ifnull(null, 2); } {2} +do_execsql_test iif-1 { + select iif(1, 1, 0); +} {1} + +do_execsql_test iif-2 { + select iif(0, 1, 0); +} {0} + do_execsql_test instr-str { select instr('limbo', 'im'); } {2} From 27ef95e0ab9e5c6069680edce807cef1148f5036 Mon Sep 17 00:00:00 2001 From: Alex Miller Date: Sat, 7 Dec 2024 21:15:04 -0800 Subject: [PATCH 02/15] LLM recommended a better way to write args matching --- core/translate/expr.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/core/translate/expr.rs b/core/translate/expr.rs index e7324db64..79a9d8ad1 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -968,14 +968,12 @@ pub fn translate_expr( Ok(target_register) } ScalarFunc::Iif => { - let args = if args.as_ref().map(|args| args.len() != 3).unwrap_or(true) - { - crate::bail_parse_error!( + let args = match args { + Some(args) if args.len() == 3 => args, + _ => crate::bail_parse_error!( "{} requires exactly 3 arguments", srf.to_string() - ); - } else { - args.as_ref().unwrap() + ), }; let temp_reg = program.alloc_register(); translate_expr( From e85df1c895e3b9bcf70927186cc62d3c39cf8fac Mon Sep 17 00:00:00 2001 From: Alex Miller Date: Tue, 10 Dec 2024 19:36:54 -0800 Subject: [PATCH 03/15] resolve labels to current offset. make test clearer. --- core/translate/expr.rs | 4 ++-- testing/scalar-functions.test | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/core/translate/expr.rs b/core/translate/expr.rs index 79a9d8ad1..6b1e81ce6 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -1006,7 +1006,7 @@ pub fn translate_expr( }, jump_target_result, ); - program.preassign_label_to_next_insn(jump_target_when_false); + program.resolve_label(jump_target_when_false, program.offset()); translate_expr( program, referenced_tables, @@ -1014,7 +1014,7 @@ pub fn translate_expr( target_register, precomputed_exprs_to_registers, )?; - program.preassign_label_to_next_insn(jump_target_result); + program.resolve_label(jump_target_result, program.offset()); Ok(target_register) } ScalarFunc::Glob | ScalarFunc::Like => { diff --git a/testing/scalar-functions.test b/testing/scalar-functions.test index b0488bfae..177fe88b6 100755 --- a/testing/scalar-functions.test +++ b/testing/scalar-functions.test @@ -75,13 +75,13 @@ do_execsql_test ifnull-2 { select ifnull(null, 2); } {2} -do_execsql_test iif-1 { - select iif(1, 1, 0); -} {1} +do_execsql_test iif-true { + select iif(1, 'pass', 'fail'); +} {pass} -do_execsql_test iif-2 { - select iif(0, 1, 0); -} {0} +do_execsql_test iif-false { + select iif(0, 'fail', 'pass'); +} {pass} do_execsql_test instr-str { select instr('limbo', 'im'); From d5391dc716b0513b22d9d0e732cba80c698a50c6 Mon Sep 17 00:00:00 2001 From: PThorpe92 Date: Tue, 10 Dec 2024 19:17:43 -0500 Subject: [PATCH 04/15] Add vdbe bitwise operators: and, or, not --- .gitignore | 3 + COMPAT.md | 6 +- core/translate/expr.rs | 53 +++++ core/translate/planner.rs | 5 + core/vdbe/explain.rs | 27 +++ core/vdbe/mod.rs | 209 +++++++++++++++++- testing/math.test | 104 +++++++++ vendored/sqlite3-parser/src/parser/ast/fmt.rs | 1 + vendored/sqlite3-parser/src/parser/ast/mod.rs | 3 + 9 files changed, 407 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 78c2410ab..bcfe79f81 100644 --- a/.gitignore +++ b/.gitignore @@ -19,5 +19,8 @@ env dist/ .tmp/ + +/testing/testing.db-wal + # OS .DS_Store diff --git a/COMPAT.md b/COMPAT.md index 0bacfc0e3..9fb496da3 100644 --- a/COMPAT.md +++ b/COMPAT.md @@ -252,9 +252,9 @@ Feature support of [sqlite expr syntax](https://www.sqlite.org/lang_expr.html). | AggStep | Yes | | And | No | | AutoCommit | No | -| BitAnd | No | -| BitNot | No | -| BitOr | No | +| BitAnd | Yes | +| BitNot | Yes | +| BitOr | Yes | | Blob | Yes | | Checkpoint | No | | Clear | No | diff --git a/core/translate/expr.rs b/core/translate/expr.rs index 0d84815a5..1e8c4d4d6 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -694,6 +694,20 @@ pub fn translate_expr( dest: target_register, }); } + ast::Operator::BitwiseAnd => { + program.emit_insn(Insn::BitAnd { + lhs: e1_reg, + rhs: e2_reg, + dest: target_register, + }); + } + ast::Operator::BitwiseOr => { + program.emit_insn(Insn::BitOr { + lhs: e1_reg, + rhs: e2_reg, + dest: target_register, + }); + } other_unimplemented => todo!("{:?}", other_unimplemented), } Ok(target_register) @@ -1615,6 +1629,45 @@ pub fn translate_expr( } Ok(target_register) } + (UnaryOperator::BitwiseNot, ast::Expr::Literal(ast::Literal::Numeric(num_val))) => { + let maybe_int = num_val.parse::(); + if let Ok(maybe_int) = maybe_int { + program.emit_insn(Insn::Integer { + value: !maybe_int, + dest: target_register, + }); + Ok(target_register) + } else { + let num_val = num_val.parse::()? as i64; + program.emit_insn(Insn::Integer { + value: !num_val, + dest: target_register, + }); + Ok(target_register) + } + } + (UnaryOperator::BitwiseNot, ast::Expr::Literal(ast::Literal::Null)) => { + program.emit_insn(Insn::Null { + dest: target_register, + dest_end: None, + }); + Ok(target_register) + } + (UnaryOperator::BitwiseNot, _) => { + let reg = program.alloc_register(); + translate_expr( + program, + referenced_tables, + expr, + reg, + precomputed_exprs_to_registers, + )?; + program.emit_insn(Insn::BitNot { + reg, + dest: target_register, + }); + Ok(target_register) + } _ => todo!(), }, ast::Expr::Variable(_) => todo!(), diff --git a/core/translate/planner.rs b/core/translate/planner.rs index 7f58a1d5d..b67630c3f 100644 --- a/core/translate/planner.rs +++ b/core/translate/planner.rs @@ -70,6 +70,11 @@ fn resolve_aggregates(expr: &ast::Expr, aggs: &mut Vec) -> bool { contains_aggregates |= resolve_aggregates(rhs, aggs); contains_aggregates } + ast::Expr::Unary(_, expr) => { + let mut contains_aggregates = false; + contains_aggregates |= resolve_aggregates(expr, aggs); + contains_aggregates + } // TODO: handle other expressions that may contain aggregates _ => false, } diff --git a/core/vdbe/explain.rs b/core/vdbe/explain.rs index 9b704fcf6..c88ac8b91 100644 --- a/core/vdbe/explain.rs +++ b/core/vdbe/explain.rs @@ -55,6 +55,33 @@ pub fn insn_to_str( 0, format!("r[{}]=r[{}]/r[{}]", dest, lhs, rhs), ), + Insn::BitAnd { lhs, rhs, dest } => ( + "BitAnd", + *lhs as i32, + *rhs as i32, + *dest as i32, + OwnedValue::Text(Rc::new("".to_string())), + 0, + format!("r[{}]=r[{}]&r[{}]", dest, lhs, rhs), + ), + Insn::BitOr { lhs, rhs, dest } => ( + "BitOr", + *lhs as i32, + *rhs as i32, + *dest as i32, + OwnedValue::Text(Rc::new("".to_string())), + 0, + format!("r[{}]=r[{}]|r[{}]", dest, lhs, rhs), + ), + Insn::BitNot { reg, dest } => ( + "BitNot", + *reg as i32, + *dest as i32, + 0, + OwnedValue::Text(Rc::new("".to_string())), + 0, + format!("r[{}]=~r[{}]", dest, reg), + ), Insn::Null { dest, dest_end } => ( "Null", 0, diff --git a/core/vdbe/mod.rs b/core/vdbe/mod.rs index ca6309a11..2454261c0 100644 --- a/core/vdbe/mod.rs +++ b/core/vdbe/mod.rs @@ -119,6 +119,23 @@ pub enum Insn { start_reg_b: usize, count: usize, }, + // Place the result of rhs bitwise AND lhs in third register. + BitAnd { + lhs: usize, + rhs: usize, + dest: usize, + }, + // Place the result of rhs bitwise OR lhs in third register. + BitOr { + lhs: usize, + rhs: usize, + dest: usize, + }, + // Place the result of bitwise NOT register P1 in dest register. + BitNot { + reg: usize, + dest: usize, + }, // Jump to the instruction at address P1, P2, or P3 depending on whether in the most recent Compare instruction the P1 vector was less than, equal to, or greater than the P2 vector, respectively. Jump { target_pc_lt: BranchOffset, @@ -838,7 +855,7 @@ impl Program { } (OwnedValue::Integer(i), OwnedValue::Float(f)) | (OwnedValue::Float(f), OwnedValue::Integer(i)) => { - state.registers[dest] = OwnedValue::Float(*i as f64 * *f as f64); + state.registers[dest] = OwnedValue::Float(*i as f64 * { *f }); } (OwnedValue::Null, _) | (_, OwnedValue::Null) => { state.registers[dest] = OwnedValue::Null; @@ -1011,6 +1028,196 @@ impl Program { } state.pc += 1; } + Insn::BitAnd { lhs, rhs, dest } => { + let lhs = *lhs; + let rhs = *rhs; + let dest = *dest; + match (&state.registers[lhs], &state.registers[rhs]) { + // handle 0 and null cases + (OwnedValue::Null, _) | (_, OwnedValue::Null) => { + state.registers[dest] = OwnedValue::Null; + } + (_, OwnedValue::Integer(0)) + | (OwnedValue::Integer(0), _) + | (_, OwnedValue::Float(0.0)) + | (OwnedValue::Float(0.0), _) => { + state.registers[dest] = OwnedValue::Integer(0); + } + (OwnedValue::Integer(lh), OwnedValue::Integer(rh)) => { + state.registers[dest] = OwnedValue::Integer(lh & rh); + } + (OwnedValue::Float(lh), OwnedValue::Float(rh)) => { + state.registers[dest] = OwnedValue::Integer(*lh as i64 & *rh as i64); + } + (OwnedValue::Float(lh), OwnedValue::Integer(rh)) => { + state.registers[dest] = OwnedValue::Integer(*lh as i64 & rh); + } + (OwnedValue::Integer(lh), OwnedValue::Float(rh)) => { + state.registers[dest] = OwnedValue::Integer(lh & *rh as i64); + } + (OwnedValue::Agg(aggctx), other) | (other, OwnedValue::Agg(aggctx)) => { + match other { + OwnedValue::Agg(aggctx2) => { + match (aggctx.final_value(), aggctx2.final_value()) { + (OwnedValue::Integer(lh), OwnedValue::Integer(rh)) => { + state.registers[dest] = OwnedValue::Integer(lh & rh); + } + (OwnedValue::Float(lh), OwnedValue::Float(rh)) => { + state.registers[dest] = + OwnedValue::Integer(*lh as i64 & *rh as i64); + } + (OwnedValue::Integer(lh), OwnedValue::Float(rh)) => { + state.registers[dest] = + OwnedValue::Integer(lh & *rh as i64); + } + (OwnedValue::Float(lh), OwnedValue::Integer(rh)) => { + state.registers[dest] = + OwnedValue::Integer(*lh as i64 & rh); + } + _ => { + unimplemented!( + "{:?} {:?}", + aggctx.final_value(), + aggctx2.final_value() + ); + } + } + } + other => match (aggctx.final_value(), other) { + (OwnedValue::Null, _) | (_, OwnedValue::Null) => { + state.registers[dest] = OwnedValue::Null; + } + (_, OwnedValue::Integer(0)) + | (OwnedValue::Integer(0), _) + | (_, OwnedValue::Float(0.0)) + | (OwnedValue::Float(0.0), _) => { + state.registers[dest] = OwnedValue::Integer(0); + } + (OwnedValue::Integer(lh), OwnedValue::Integer(rh)) => { + state.registers[dest] = OwnedValue::Integer(lh & rh); + } + (OwnedValue::Float(lh), OwnedValue::Integer(rh)) => { + state.registers[dest] = + OwnedValue::Integer(*lh as i64 & rh); + } + (OwnedValue::Integer(lh), OwnedValue::Float(rh)) => { + state.registers[dest] = + OwnedValue::Integer(lh & *rh as i64); + } + _ => { + unimplemented!("{:?} {:?}", aggctx.final_value(), other); + } + }, + } + } + _ => { + unimplemented!("{:?} {:?}", state.registers[lhs], state.registers[rhs]); + } + } + state.pc += 1; + } + Insn::BitOr { lhs, rhs, dest } => { + let lhs = *lhs; + let rhs = *rhs; + let dest = *dest; + match (&state.registers[lhs], &state.registers[rhs]) { + (OwnedValue::Null, _) | (_, OwnedValue::Null) => { + state.registers[dest] = OwnedValue::Null; + } + (OwnedValue::Integer(lh), OwnedValue::Integer(rh)) => { + state.registers[dest] = OwnedValue::Integer(lh | rh); + } + (OwnedValue::Float(lh), OwnedValue::Integer(rh)) => { + state.registers[dest] = OwnedValue::Integer(*lh as i64 | rh); + } + (OwnedValue::Integer(lh), OwnedValue::Float(rh)) => { + state.registers[dest] = OwnedValue::Integer(lh | *rh as i64); + } + (OwnedValue::Float(lh), OwnedValue::Float(rh)) => { + state.registers[dest] = OwnedValue::Integer(*lh as i64 | *rh as i64); + } + (OwnedValue::Agg(aggctx), other) | (other, OwnedValue::Agg(aggctx)) => { + match other { + OwnedValue::Agg(aggctx2) => { + let final_lhs = aggctx.final_value(); + let final_rhs = aggctx2.final_value(); + match (final_lhs, final_rhs) { + (OwnedValue::Integer(lh), OwnedValue::Integer(rh)) => { + state.registers[dest] = OwnedValue::Integer(lh | rh); + } + (OwnedValue::Float(lh), OwnedValue::Float(rh)) => { + state.registers[dest] = + OwnedValue::Integer(*lh as i64 | *rh as i64); + } + (OwnedValue::Integer(lh), OwnedValue::Float(rh)) => { + state.registers[dest] = + OwnedValue::Integer(lh | *rh as i64); + } + (OwnedValue::Float(lh), OwnedValue::Integer(rh)) => { + state.registers[dest] = + OwnedValue::Integer(*lh as i64 | rh); + } + _ => { + unimplemented!("{:?} {:?}", final_lhs, final_rhs); + } + } + } + other => match (aggctx.final_value(), other) { + (OwnedValue::Null, _) | (_, OwnedValue::Null) => { + state.registers[dest] = OwnedValue::Null; + } + (OwnedValue::Integer(lh), OwnedValue::Integer(rh)) => { + state.registers[dest] = OwnedValue::Integer(lh | rh); + } + (OwnedValue::Float(lh), OwnedValue::Integer(rh)) => { + state.registers[dest] = + OwnedValue::Integer(*lh as i64 | rh); + } + (OwnedValue::Integer(lh), OwnedValue::Float(rh)) => { + state.registers[dest] = + OwnedValue::Integer(lh | *rh as i64); + } + _ => { + unimplemented!("{:?} {:?}", aggctx.final_value(), other); + } + }, + } + } + _ => { + unimplemented!("{:?} {:?}", state.registers[lhs], state.registers[rhs]); + } + } + state.pc += 1; + } + Insn::BitNot { reg, dest } => { + let reg = *reg; + let dest = *dest; + match &state.registers[reg] { + OwnedValue::Integer(i) => state.registers[dest] = OwnedValue::Integer(!i), + OwnedValue::Float(f) => { + state.registers[dest] = OwnedValue::Integer(!{ *f as i64 }) + } + OwnedValue::Null => { + state.registers[dest] = OwnedValue::Null; + } + OwnedValue::Agg(aggctx) => match aggctx.final_value() { + OwnedValue::Integer(i) => { + state.registers[dest] = OwnedValue::Integer(!i); + } + OwnedValue::Float(f) => { + state.registers[dest] = OwnedValue::Integer(!{ *f as i64 }); + } + OwnedValue::Null => { + state.registers[dest] = OwnedValue::Null; + } + _ => unimplemented!("{:?}", aggctx), + }, + _ => { + unimplemented!("{:?}", state.registers[reg]); + } + } + state.pc += 1; + } Insn::Null { dest, dest_end } => { if let Some(dest_end) = dest_end { for i in *dest..=*dest_end { diff --git a/testing/math.test b/testing/math.test index c567550e2..45b38f7d6 100644 --- a/testing/math.test +++ b/testing/math.test @@ -261,3 +261,107 @@ do_execsql_test divide-agg-int-agg-float { do_execsql_test divide-agg-float-agg-int { SELECT min(price) / min(id) from products } {1.0} + + +do_execsql_test bitwise-and-int-null { + SELECT 1234 & NULL +} {} + +do_execsql_test bitwise-and-int-int { + SELECT 1234 & 1234 +} {1234} + +do_execsql_test bitwise-and-int-float { + SELECT 660 & 261.8 +} {4} + +do_execsql_test bitwise-and-float-float { + SELECT 660.63 & 261.8 +} {4} + +do_execsql_test bitwise-and-float-int-rev { + SELECT 261.8 & 660 +} {4} + +do_execsql_test bitwise-and-float-int-rev { + SELECT SUM(id) from products +} {66} + +do_execsql_test bitwise-and-int-agg-int { + SELECT 8261 & sum(id) from products +} {64} + +do_execsql_test bitwise-and-int-agg-float { + SELECT 1036.6 & sum(id) from products +} {0} + +do_execsql_test bitwise-and-int-agg-int-agg { + SELECT sum(id) & sum(id) from products +} {66} + + +do_execsql_test bitwise-or-int-null { + SELECT 1234 | NULL +} {} + +do_execsql_test bitwise-or-null-int { + SELECT NULL | 1234 +} {} + +do_execsql_test bitwise-or-int-int { + SELECT 4321 | 1234 +} {5363} + +do_execsql_test bitwise-or-int-float { + SELECT 660 | 1234.0 +} {1750} + +do_execsql_test bitwise-or-int-agg { + SELECT 18823 | sum(id) from products +} {18887} + +do_execsql_test bitwise-or-float-float { + SELECT 1234.6 | 5432.2 +} {5626} + +do_execsql_test bitwise-and-int-agg-int-agg { + SELECT sum(id) | sum(id) from products +} {66} + + +do_execsql_test bitwise-not-null { + SELECT ~NULL +} {} + +do_execsql_test bitwise-not-int { + SELECT ~1234 +} {-1235} + +do_execsql_test bitwise-not-float { + SELECT ~823.34 +} {-824} + +do_execsql_test bitwise-not-scalar-float { + SELECT ~abs(693.9) +} {-694} + +do_execsql_test bitwise-not-scalar-int { + SELECT ~abs(7566) +} {-7567} + +do_execsql_test bitwise-not-agg-int { + SELECT ~sum(693) +} {-694} + +do_execsql_test bitwise-not-agg-and-agg { + SELECT ~sum(693) & sum(-302) +} {-958} + +do_execsql_test bitwise-not-agg-int { + SELECT ~sum(693) +} {-694} + +do_execsql_test bitwise-not-zero { + SELECT ~0 +} {-1} + diff --git a/vendored/sqlite3-parser/src/parser/ast/fmt.rs b/vendored/sqlite3-parser/src/parser/ast/fmt.rs index 80f87eefb..39c929409 100644 --- a/vendored/sqlite3-parser/src/parser/ast/fmt.rs +++ b/vendored/sqlite3-parser/src/parser/ast/fmt.rs @@ -792,6 +792,7 @@ impl ToTokens for Operator { Self::ArrowRightShift => s.append(TK_PTR, Some("->>")), Self::BitwiseAnd => s.append(TK_BITAND, None), Self::BitwiseOr => s.append(TK_BITOR, None), + Self::BitwiseNot => s.append(TK_BITNOT, None), Self::Concat => s.append(TK_CONCAT, None), Self::Equals => s.append(TK_EQ, None), Self::Divide => s.append(TK_SLASH, None), diff --git a/vendored/sqlite3-parser/src/parser/ast/mod.rs b/vendored/sqlite3-parser/src/parser/ast/mod.rs index 690f5e71c..da1798cff 100644 --- a/vendored/sqlite3-parser/src/parser/ast/mod.rs +++ b/vendored/sqlite3-parser/src/parser/ast/mod.rs @@ -583,6 +583,8 @@ pub enum Operator { BitwiseAnd, /// `|` BitwiseOr, + /// `~` + BitwiseNot, /// String concatenation (`||`) Concat, /// `=` or `==` @@ -630,6 +632,7 @@ impl From for Operator { x if x == TK_NE as YYCODETYPE => Self::NotEquals, x if x == TK_BITAND as YYCODETYPE => Self::BitwiseAnd, x if x == TK_BITOR as YYCODETYPE => Self::BitwiseOr, + x if x == TK_BITNOT as YYCODETYPE => Self::BitwiseNot, x if x == TK_LSHIFT as YYCODETYPE => Self::LeftShift, x if x == TK_RSHIFT as YYCODETYPE => Self::RightShift, x if x == TK_PLUS as YYCODETYPE => Self::Add, From 16595f39f5ae72ddf96e25523acdbbfd55bab37f Mon Sep 17 00:00:00 2001 From: PThorpe92 Date: Wed, 11 Dec 2024 11:29:06 -0500 Subject: [PATCH 05/15] Add support for unary op negation of aggregates --- core/translate/expr.rs | 32 ++++++++++++++++++++++++++++---- testing/agg-functions.test | 9 +++++++++ testing/math.test | 4 ---- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/core/translate/expr.rs b/core/translate/expr.rs index 1e8c4d4d6..b92b892ac 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -1627,30 +1627,54 @@ pub fn translate_expr( dest: target_register, }); } + program.mark_last_insn_constant(); + Ok(target_register) + } + (UnaryOperator::Negative, _) => { + let reg = program.alloc_register(); + translate_expr( + program, + referenced_tables, + expr, + reg, + precomputed_exprs_to_registers, + )?; + let zero_reg = program.alloc_register(); + program.emit_insn(Insn::Integer { + value: -1, + dest: zero_reg, + }); + program.mark_last_insn_constant(); + program.emit_insn(Insn::Multiply { + lhs: zero_reg, + rhs: reg, + dest: target_register, + }); Ok(target_register) } (UnaryOperator::BitwiseNot, ast::Expr::Literal(ast::Literal::Numeric(num_val))) => { let maybe_int = num_val.parse::(); - if let Ok(maybe_int) = maybe_int { + if let Ok(val) = maybe_int { program.emit_insn(Insn::Integer { - value: !maybe_int, + value: !val, dest: target_register, }); - Ok(target_register) } else { let num_val = num_val.parse::()? as i64; program.emit_insn(Insn::Integer { value: !num_val, dest: target_register, }); - Ok(target_register) } + program.mark_last_insn_constant(); + Ok(target_register) } (UnaryOperator::BitwiseNot, ast::Expr::Literal(ast::Literal::Null)) => { program.emit_insn(Insn::Null { dest: target_register, dest_end: None, }); + program.mark_last_insn_constant(); Ok(target_register) } (UnaryOperator::BitwiseNot, _) => { diff --git a/testing/agg-functions.test b/testing/agg-functions.test index fbc007b62..e5594efa5 100755 --- a/testing/agg-functions.test +++ b/testing/agg-functions.test @@ -74,3 +74,12 @@ do_execsql_test select-string-agg-with-delimiter { do_execsql_test select-string-agg-with-column-delimiter { SELECT string_agg(name, id) FROM products; } {hat2cap3shirt4sweater5sweatshirt6shorts7jeans8sneakers9boots10coat11accessories} + +do_execsql_test select-agg-unary { + SELECT -max(age) FROM users; +} {-100} + +do_execsql_test select-agg-binary-unary { + SELECT min(age) + -max(age) FROM users; +} {-99} + diff --git a/testing/math.test b/testing/math.test index 45b38f7d6..af477fca4 100644 --- a/testing/math.test +++ b/testing/math.test @@ -283,10 +283,6 @@ do_execsql_test bitwise-and-float-int-rev { SELECT 261.8 & 660 } {4} -do_execsql_test bitwise-and-float-int-rev { - SELECT SUM(id) from products -} {66} - do_execsql_test bitwise-and-int-agg-int { SELECT 8261 & sum(id) from products } {64} From c083e594571244355ee54a8e5113e8c1767fa18c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=84=A0=EC=9A=B0?= Date: Thu, 12 Dec 2024 13:47:14 +0900 Subject: [PATCH 06/15] Enhance README.md --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 151699975..ae88c937e 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Install `limbo` with: -``` +```shell curl --proto '=https' --tlsv1.2 -LsSf \ https://github.com/penberg/limbo/releases/latest/download/limbo-installer.sh | sh ``` @@ -96,6 +96,12 @@ print(res.fetchone()) ## Developing +Build `limbo`: + +```shell +cargo run --package limbo --bin limbo database.db +``` + Run tests: ```console From 8e6184c932847270e6e6f70f004160992efb65be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=84=A0=EC=9A=B0?= Date: Thu, 12 Dec 2024 16:05:23 +0900 Subject: [PATCH 07/15] Enhance README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ae88c937e..bb62a6288 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ print(res.fetchone()) ## Developing -Build `limbo`: +Build and run `limbo` cli: ```shell cargo run --package limbo --bin limbo database.db From 2e754e37f189751f565fd8c820a10c4553f55727 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Thu, 12 Dec 2024 09:08:51 +0200 Subject: [PATCH 08/15] perf: Update Cargo.lock files We already bumped up clap to fix up a Github dependabot complaint, but let's also update the lock files... --- perf/latency/limbo/Cargo.lock | 56 ++++--------- perf/latency/rusqlite/Cargo.lock | 140 ++++++++++++++++++++++++------- 2 files changed, 126 insertions(+), 70 deletions(-) diff --git a/perf/latency/limbo/Cargo.lock b/perf/latency/limbo/Cargo.lock index 466be0f72..540687f3d 100644 --- a/perf/latency/limbo/Cargo.lock +++ b/perf/latency/limbo/Cargo.lock @@ -32,20 +32,6 @@ dependencies = [ "libc", ] -[[package]] -name = "anstream" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon 2.1.0", - "colorchoice", - "utf8parse", -] - [[package]] name = "anstream" version = "0.6.11" @@ -55,7 +41,7 @@ dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", - "anstyle-wincon 3.0.2", + "anstyle-wincon", "colorchoice", "utf8parse", ] @@ -84,16 +70,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "anstyle-wincon" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" -dependencies = [ - "anstyle", - "windows-sys 0.48.0", -] - [[package]] name = "anstyle-wincon" version = "3.0.2" @@ -192,9 +168,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.2" +version = "4.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a13b88d2c62ff462f88e4a121f17a82c1af05693a2f192b5c38d14de73c19f6" +checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc" dependencies = [ "clap_builder", "clap_derive", @@ -202,11 +178,11 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.2" +version = "4.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08" +checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99" dependencies = [ - "anstream 0.5.0", + "anstream", "anstyle", "clap_lex", "strsim", @@ -214,9 +190,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.4.2" +version = "4.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" +checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" dependencies = [ "heck", "proc-macro2", @@ -226,9 +202,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.5.1" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "colorchoice" @@ -321,7 +297,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9eeb342678d785662fd2514be38c459bb925f02b68dd2a3e0f21d7ef82d979dd" dependencies = [ - "anstream 0.6.11", + "anstream", "anstyle", "env_filter", "humantime", @@ -411,9 +387,9 @@ dependencies = [ [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" @@ -545,7 +521,7 @@ dependencies = [ [[package]] name = "limbo_core" -version = "0.0.5" +version = "0.0.8" dependencies = [ "cfg_block", "chrono", @@ -944,9 +920,9 @@ dependencies = [ [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" diff --git a/perf/latency/rusqlite/Cargo.lock b/perf/latency/rusqlite/Cargo.lock index 158403519..740a551c1 100644 --- a/perf/latency/rusqlite/Cargo.lock +++ b/perf/latency/rusqlite/Cargo.lock @@ -27,23 +27,24 @@ checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" [[package]] name = "anstream" -version = "0.5.0" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.2" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" @@ -60,17 +61,17 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" dependencies = [ - "windows-sys", + "windows-sys 0.48.0", ] [[package]] name = "anstyle-wincon" -version = "2.1.0" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -105,9 +106,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.4.2" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a13b88d2c62ff462f88e4a121f17a82c1af05693a2f192b5c38d14de73c19f6" +checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" dependencies = [ "clap_builder", "clap_derive", @@ -115,9 +116,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.2" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08" +checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" dependencies = [ "anstream", "anstyle", @@ -127,9 +128,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.4.2" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck", "proc-macro2", @@ -139,9 +140,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.5.1" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "colorchoice" @@ -234,9 +235,15 @@ dependencies = [ [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "libsqlite3-sys" @@ -302,9 +309,9 @@ checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -349,9 +356,9 @@ checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" @@ -394,7 +401,16 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -403,13 +419,29 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -418,38 +450,86 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" From 4e40d9ecee9ca0491a03ca0fe00217a497897200 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Thu, 12 Dec 2024 10:47:11 +0200 Subject: [PATCH 09/15] Update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb03055f7..45965182f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Added +* Add support for iif() function (Alex Miller) + * Add suport for last_insert_rowid() function (Krishna Vishal) * Add support JOIN USING and NATURAL JOIN (Jussi Saurio) From 07220374657b898effa1825571ea96415d22e4cf Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Thu, 12 Dec 2024 10:59:10 +0200 Subject: [PATCH 10/15] github: Fix stale workflow access token --- .github/workflows/stale.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 9d09eaef1..ad9d1bbe7 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -14,6 +14,7 @@ jobs: - name: Close stale pull requests uses: actions/stale@v6 with: + repo-token: ${{ secrets.STALE_GH_TOKEN }} operations-per-run: 1000 ascending: true stale-pr-message: 'This pull request has been marked as stale due to inactivity. It will be closed in 7 days if no further activity occurs.' From 03288e51706652df5bf57dd018f0eefc03c54ecd Mon Sep 17 00:00:00 2001 From: Li Yazhou Date: Thu, 12 Dec 2024 21:21:36 -0600 Subject: [PATCH 11/15] add impl about scalar function soundex with test --- COMPAT.md | 3 +- core/function.rs | 3 + core/translate/expr.rs | 1 + core/vdbe/mod.rs | 145 +++++++++++++++++++++++++++++++++- testing/scalar-functions.test | 4 + 5 files changed, 152 insertions(+), 4 deletions(-) diff --git a/COMPAT.md b/COMPAT.md index 913b0366d..d25e67a0c 100644 --- a/COMPAT.md +++ b/COMPAT.md @@ -5,6 +5,7 @@ This document describes the SQLite compatibility status of Limbo: - [SQLite Compatibility](#sqlite-compatibility) - [Limitations](#limitations) - [SQL statements](#sql-statements) + - [SELECT Expressions](#select-expressions) - [SQL functions](#sql-functions) - [Scalar functions](#scalar-functions) - [Aggregate functions](#aggregate-functions) @@ -138,7 +139,7 @@ Feature support of [sqlite expr syntax](https://www.sqlite.org/lang_expr.html). | rtrim(X) | Yes | | | rtrim(X,Y) | Yes | | | sign(X) | Yes | | -| soundex(X) | No | | +| soundex(X) | Yes | | | sqlite_compileoption_get(N) | No | | | sqlite_compileoption_used(X) | No | | | sqlite_offset(X) | No | | diff --git a/core/function.rs b/core/function.rs index 9ece12992..0659da73f 100644 --- a/core/function.rs +++ b/core/function.rs @@ -76,6 +76,7 @@ pub enum ScalarFunc { Sign, Substr, Substring, + Soundex, Date, Time, Typeof, @@ -119,6 +120,7 @@ impl Display for ScalarFunc { ScalarFunc::Sign => "sign".to_string(), ScalarFunc::Substr => "substr".to_string(), ScalarFunc::Substring => "substring".to_string(), + ScalarFunc::Soundex => "soundex".to_string(), ScalarFunc::Date => "date".to_string(), ScalarFunc::Time => "time".to_string(), ScalarFunc::Typeof => "typeof".to_string(), @@ -210,6 +212,7 @@ impl Func { "hex" => Ok(Func::Scalar(ScalarFunc::Hex)), "unhex" => Ok(Func::Scalar(ScalarFunc::Unhex)), "zeroblob" => Ok(Func::Scalar(ScalarFunc::ZeroBlob)), + "soundex" => Ok(Func::Scalar(ScalarFunc::Soundex)), _ => Err(()), } } diff --git a/core/translate/expr.rs b/core/translate/expr.rs index 8a13e1344..d15103a9f 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -1169,6 +1169,7 @@ pub fn translate_expr( | ScalarFunc::Quote | ScalarFunc::RandomBlob | ScalarFunc::Sign + | ScalarFunc::Soundex | ScalarFunc::ZeroBlob => { let args = if let Some(args) = args { if args.len() != 1 { diff --git a/core/vdbe/mod.rs b/core/vdbe/mod.rs index ebd25360a..b3b40379d 100644 --- a/core/vdbe/mod.rs +++ b/core/vdbe/mod.rs @@ -2149,6 +2149,7 @@ impl Program { | ScalarFunc::Quote | ScalarFunc::RandomBlob | ScalarFunc::Sign + | ScalarFunc::Soundex | ScalarFunc::ZeroBlob => { let reg_value = state.registers[*start_reg].borrow_mut(); let result = match scalar_func { @@ -2163,6 +2164,7 @@ impl Program { ScalarFunc::Quote => Some(exec_quote(reg_value)), ScalarFunc::RandomBlob => Some(exec_randomblob(reg_value)), ScalarFunc::ZeroBlob => Some(exec_zeroblob(reg_value)), + ScalarFunc::Soundex => Some(exec_soundex(reg_value)), _ => unreachable!(), }; state.registers[*dest] = result.unwrap_or(OwnedValue::Null); @@ -2666,6 +2668,96 @@ fn exec_sign(reg: &OwnedValue) -> Option { Some(OwnedValue::Integer(sign)) } +/// Generates the Soundex code for a given word +pub fn exec_soundex(reg: &OwnedValue) -> OwnedValue { + let s = match reg { + OwnedValue::Null => return OwnedValue::Text(Rc::new("?000".to_string())), + OwnedValue::Text(s) => { + // return ?000 if non ASCII alphabet character is found + if !s.chars().all(|c| c.is_ascii_alphabetic()) { + return OwnedValue::Text(Rc::new("?000".to_string())); + } + s.clone() + } + _ => return OwnedValue::Text(Rc::new("?000".to_string())), // For unsupported types, return NULL + }; + + // Remove numbers and spaces + let word: String = s + .chars() + .filter(|c| !c.is_digit(10)) + .collect::() + .replace(" ", ""); + if word.is_empty() { + return OwnedValue::Text(Rc::new("0000".to_string())); + } + + let soundex_code = |c| match c { + 'b' | 'f' | 'p' | 'v' => Some('1'), + 'c' | 'g' | 'j' | 'k' | 'q' | 's' | 'x' | 'z' => Some('2'), + 'd' | 't' => Some('3'), + 'l' => Some('4'), + 'm' | 'n' => Some('5'), + 'r' => Some('6'), + _ => None, + }; + + // Convert the word to lowercase for consistent lookups + let word = word.to_lowercase(); + let first_letter = word.chars().next().unwrap(); + + // Remove all occurrences of 'h' and 'w' except the first letter + let code: String = word + .chars() + .skip(1) + .filter(|&ch| ch != 'h' && ch != 'w') + .fold(first_letter.to_string(), |mut acc, ch| { + acc.push(ch); + acc + }); + + // Replace consonants with digits based on Soundex mapping + let tmp: String = code + .chars() + .map(|ch| match soundex_code(ch) { + Some(code) => code.to_string(), + None => ch.to_string(), + }) + .collect(); + + // Remove adjacent same digits + let tmp = tmp.chars().fold(String::new(), |mut acc, ch| { + if acc.chars().last() != Some(ch) { + acc.push(ch); + } + acc + }); + + // Remove all occurrences of a, e, i, o, u, y except the first letter + let mut result = tmp + .chars() + .enumerate() + .filter(|(i, ch)| *i == 0 || !matches!(ch, 'a' | 'e' | 'i' | 'o' | 'u' | 'y')) + .map(|(_, ch)| ch) + .collect::(); + + // If the first symbol is a digit, replace it with the saved first letter + if let Some(first_digit) = result.chars().next() { + if first_digit.is_digit(10) { + result.replace_range(0..1, &first_letter.to_string()); + } + } + + // Append zeros if the result contains less than 4 characters + while result.len() < 4 { + result.push('0'); + } + + // Retain the first 4 characters and convert to uppercase + result.truncate(4); + OwnedValue::Text(Rc::new(result.to_uppercase())) +} + fn exec_abs(reg: &OwnedValue) -> Option { match reg { OwnedValue::Integer(x) => { @@ -3255,9 +3347,9 @@ mod tests { use super::{ exec_abs, exec_char, exec_hex, exec_if, exec_instr, exec_length, exec_like, exec_lower, exec_ltrim, exec_max, exec_min, exec_nullif, exec_quote, exec_random, exec_randomblob, - exec_round, exec_rtrim, exec_sign, exec_substring, exec_trim, exec_typeof, exec_unhex, - exec_unicode, exec_upper, exec_zeroblob, execute_sqlite_version, get_new_rowid, AggContext, - Cursor, CursorResult, LimboError, OwnedRecord, OwnedValue, Result, + exec_round, exec_rtrim, exec_sign, exec_soundex, exec_substring, exec_trim, exec_typeof, + exec_unhex, exec_unicode, exec_upper, exec_zeroblob, execute_sqlite_version, get_new_rowid, + AggContext, Cursor, CursorResult, LimboError, OwnedRecord, OwnedValue, Result, }; use mockall::{mock, predicate}; use rand::{rngs::mock::StepRng, thread_rng}; @@ -3587,6 +3679,53 @@ mod tests { assert_eq!(exec_rtrim(&input_str, Some(pattern_str)), expected_str); } + #[test] + fn test_soundex() { + let input_str = OwnedValue::Text(Rc::new(String::from("Pfister"))); + let expected_str = OwnedValue::Text(Rc::new(String::from("P236"))); + assert_eq!(exec_soundex(&input_str), expected_str); + + let input_str = OwnedValue::Text(Rc::new(String::from("husobee"))); + let expected_str = OwnedValue::Text(Rc::new(String::from("H210"))); + assert_eq!(exec_soundex(&input_str), expected_str); + + let input_str = OwnedValue::Text(Rc::new(String::from("Tymczak"))); + let expected_str = OwnedValue::Text(Rc::new(String::from("T522"))); + assert_eq!(exec_soundex(&input_str), expected_str); + + let input_str = OwnedValue::Text(Rc::new(String::from("Ashcraft"))); + let expected_str = OwnedValue::Text(Rc::new(String::from("A261"))); + assert_eq!(exec_soundex(&input_str), expected_str); + + let input_str = OwnedValue::Text(Rc::new(String::from("Robert"))); + let expected_str = OwnedValue::Text(Rc::new(String::from("R163"))); + assert_eq!(exec_soundex(&input_str), expected_str); + + let input_str = OwnedValue::Text(Rc::new(String::from("Rupert"))); + let expected_str = OwnedValue::Text(Rc::new(String::from("R163"))); + assert_eq!(exec_soundex(&input_str), expected_str); + + let input_str = OwnedValue::Text(Rc::new(String::from("Rubin"))); + let expected_str = OwnedValue::Text(Rc::new(String::from("R150"))); + assert_eq!(exec_soundex(&input_str), expected_str); + + let input_str = OwnedValue::Text(Rc::new(String::from("Kant"))); + let expected_str = OwnedValue::Text(Rc::new(String::from("K530"))); + assert_eq!(exec_soundex(&input_str), expected_str); + + let input_str = OwnedValue::Text(Rc::new(String::from("Knuth"))); + let expected_str = OwnedValue::Text(Rc::new(String::from("K530"))); + assert_eq!(exec_soundex(&input_str), expected_str); + + let input_str = OwnedValue::Text(Rc::new(String::from("x"))); + let expected_str = OwnedValue::Text(Rc::new(String::from("X000"))); + assert_eq!(exec_soundex(&input_str), expected_str); + + let input_str = OwnedValue::Text(Rc::new(String::from("闪电五连鞭"))); + let expected_str = OwnedValue::Text(Rc::new(String::from("?000"))); + assert_eq!(exec_soundex(&input_str), expected_str); + } + #[test] fn test_upper_case() { let input_str = OwnedValue::Text(Rc::new(String::from("Limbo"))); diff --git a/testing/scalar-functions.test b/testing/scalar-functions.test index ae96ddad8..80878cba8 100755 --- a/testing/scalar-functions.test +++ b/testing/scalar-functions.test @@ -767,3 +767,7 @@ do_execsql_test cast-in-where { select age from users where age = cast('45' as integer) limit 1; } {45} +# TODO: sqlite seems not enable soundex() by default unless build it with SQLITE_SOUNDEX enabled. +# do_execsql_test soundex-text { +# select soundex('Pfister'), soundex('husobee'), soundex('Tymczak'), soundex('Ashcraft'), soundex('Robert'), soundex('Rupert'), soundex('Rubin'), soundex('Kant'), soundex('Knuth'), soundex('x'), soundex(''); +# } {P236|H210|T522|A261|R163|R163|R150|K530|K530|X000|0000} \ No newline at end of file From 33f3eb02c8b3bc6112cb78378316932201869b29 Mon Sep 17 00:00:00 2001 From: krishvishal Date: Thu, 12 Dec 2024 19:46:01 +0530 Subject: [PATCH 12/15] Change `octet_length` to yes in COMPAT.md - Its added in https://github.com/tursodatabase/limbo/pull/430 --- COMPAT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/COMPAT.md b/COMPAT.md index 913b0366d..a34e6200c 100644 --- a/COMPAT.md +++ b/COMPAT.md @@ -127,7 +127,7 @@ Feature support of [sqlite expr syntax](https://www.sqlite.org/lang_expr.html). | max(X,Y,...) | Yes | | | min(X,Y,...) | Yes | | | nullif(X,Y) | Yes | | -| octet_length(X) | No | | +| octet_length(X) | Yes | | | printf(FORMAT,...) | No | | | quote(X) | Yes | | | random() | Yes | | From 4dc71bc9ad298f92a052413db4a16570ec4246a2 Mon Sep 17 00:00:00 2001 From: PThorpe92 Date: Wed, 11 Dec 2024 17:31:49 -0500 Subject: [PATCH 13/15] Add buffered cli input to allow for incomplete statements --- cli/main.rs | 59 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 12 deletions(-) diff --git a/cli/main.rs b/cli/main.rs index 605e3cd2a..389fac710 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -71,23 +71,49 @@ fn main() -> anyhow::Result<()> { } println!("Limbo v{}", env!("CARGO_PKG_VERSION")); println!("Enter \".help\" for usage hints."); + const PROMPT: &str = "limbo> "; + let mut input_buff = String::new(); + let mut prompt = PROMPT.to_string(); loop { - let readline = rl.readline("limbo> "); + let readline = rl.readline(&prompt); match readline { Ok(line) => { + let line = line.trim(); + if input_buff.is_empty() { + if line.is_empty() { + continue; + } else if line.starts_with('.') { + if let Err(e) = handle_dot_command(io.clone(), &conn, line) { + eprintln!("{}", e); + } + rl.add_history_entry(line.to_owned())?; + interrupt_count.store(0, Ordering::SeqCst); + continue; + } + } + if line.ends_with(';') { + input_buff.push_str(line); + input_buff.split(';').for_each(|stmt| { + if let Err(e) = + query(io.clone(), &conn, stmt, &opts.output_mode, &interrupt_count) + { + eprintln!("{}", e); + } + }); + input_buff.clear(); + prompt = PROMPT.to_string(); + } else { + input_buff.push_str(line); + input_buff.push(' '); + prompt = match calc_parens_offset(&input_buff) { + n if n < 0 => String::from(")x!...>"), + 0 => String::from(" ...> "), + n if n < 10 => format!("(x{}...> ", n), + _ => String::from("(.....> "), + }; + } rl.add_history_entry(line.to_owned())?; interrupt_count.store(0, Ordering::SeqCst); - if line.trim().starts_with('.') { - handle_dot_command(io.clone(), &conn, &line)?; - } else { - query( - io.clone(), - &conn, - &line, - &opts.output_mode, - &interrupt_count, - )?; - } } Err(ReadlineError::Interrupted) => { // At prompt, increment interrupt count @@ -97,6 +123,7 @@ fn main() -> anyhow::Result<()> { break; } println!("Use .quit to exit or press Ctrl-C again to force quit."); + input_buff.clear(); continue; } Err(ReadlineError::Eof) => { @@ -113,6 +140,14 @@ fn main() -> anyhow::Result<()> { Ok(()) } +fn calc_parens_offset(input: &str) -> i32 { + input.chars().fold(0, |acc, c| match c { + '(' => acc + 1, + ')' => acc - 1, + _ => acc, + }) +} + fn display_help_message() { let help_message = r#" Limbo SQL Shell Help From afbf9eb9da68e81a46d56b003250bcd3bcde7dd4 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Thu, 12 Dec 2024 18:47:42 +0200 Subject: [PATCH 14/15] Update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45965182f..ced4db62d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Added +* Improve CLI (Preston Thorpe) + * Add support for iif() function (Alex Miller) * Add suport for last_insert_rowid() function (Krishna Vishal) From 3023d228c7831ff7178b8253d7f0df87fa0220c3 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Thu, 12 Dec 2024 18:49:05 +0200 Subject: [PATCH 15/15] Limbo 0.0.9 --- CHANGELOG.md | 2 +- Cargo.lock | 14 +++++++------- Cargo.toml | 2 +- bindings/wasm/package.json | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ced4db62d..86fdb9755 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 0.0.9 - 2024-12-12 ### Added diff --git a/Cargo.lock b/Cargo.lock index 312d00abe..ced65e88c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -405,7 +405,7 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core_tester" -version = "0.0.8" +version = "0.0.9" dependencies = [ "anyhow", "clap", @@ -1125,7 +1125,7 @@ dependencies = [ [[package]] name = "limbo" -version = "0.0.8" +version = "0.0.9" dependencies = [ "anyhow", "clap", @@ -1139,7 +1139,7 @@ dependencies = [ [[package]] name = "limbo-wasm" -version = "0.0.8" +version = "0.0.9" dependencies = [ "console_error_panic_hook", "js-sys", @@ -1149,7 +1149,7 @@ dependencies = [ [[package]] name = "limbo_core" -version = "0.0.8" +version = "0.0.9" dependencies = [ "cfg_block", "chrono", @@ -1184,7 +1184,7 @@ dependencies = [ [[package]] name = "limbo_sim" -version = "0.0.8" +version = "0.0.9" dependencies = [ "anarchist-readable-name-generator-lib", "env_logger 0.10.2", @@ -1197,7 +1197,7 @@ dependencies = [ [[package]] name = "limbo_sqlite3" -version = "0.0.8" +version = "0.0.9" dependencies = [ "cbindgen", "env_logger 0.11.5", @@ -1641,7 +1641,7 @@ dependencies = [ [[package]] name = "py-limbo" -version = "0.0.8" +version = "0.0.9" dependencies = [ "anyhow", "limbo_core", diff --git a/Cargo.toml b/Cargo.toml index 87675cc55..a8beb431a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ members = [ exclude = ["perf/latency/limbo"] [workspace.package] -version = "0.0.8" +version = "0.0.9" authors = ["the Limbo authors"] edition = "2021" license = "MIT" diff --git a/bindings/wasm/package.json b/bindings/wasm/package.json index c49f0be7c..18b398647 100644 --- a/bindings/wasm/package.json +++ b/bindings/wasm/package.json @@ -3,7 +3,7 @@ "collaborators": [ "the Limbo authors" ], - "version": "0.0.8", + "version": "0.0.9", "license": "MIT", "repository": { "type": "git",