diff --git a/.github/workflows/push_only.yml b/.github/workflows/push_only.yml new file mode 100644 index 000000000..1c10bbb62 --- /dev/null +++ b/.github/workflows/push_only.yml @@ -0,0 +1,42 @@ +name: Benchmarks+Nyrkiö + +# Pull request support isn't integrated to the github-action-benchmark so run only post-merge +on: + push: + branches: [ "main", "master", "notmain", "add-nyrkio" ] + +env: + CARGO_TERM_COLOR: never + +jobs: + bench: + runs-on: ubuntu-latest + environment: test + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v4 + with: + node-version: 20 + # cache: 'npm' + # - name: Install dependencies + # run: npm install && npm run build + + - name: Bench + run: cargo bench 2>&1 | tee output.txt + + - name: Analyze benchmark result with Nyrkiö + uses: nyrkio/github-action-benchmark@HEAD + with: + name: turso + tool: criterion + output-file-path: output.txt + fail-on-alert: true + # Nyrkiö configuration + nyrkio-enable: true + # Get yours from https://nyrkio.com/docs/getting-started + nyrkio-token: ${{ secrets.NYRKIO_JWT_TOKEN }} + + # Old way... + # Explicitly set this to null. We don't want threshold based alerts today. + external-data-json-path: null + gh-repository: null diff --git a/COMPAT.md b/COMPAT.md index fcd72b633..03798d24c 100644 --- a/COMPAT.md +++ b/COMPAT.md @@ -408,7 +408,7 @@ Modifiers: | AggFinal | Yes | | AggStep | Yes | | AggStep | Yes | -| And | No | +| And | Yes | | AutoCommit | No | | BitAnd | Yes | | BitNot | Yes | @@ -501,7 +501,7 @@ Modifiers: | OpenWrite | No | | OpenWriteAsync | Yes | | OpenWriteAwait | Yes | -| Or | No | +| Or | Yes | | Pagecount | No | | Param | No | | ParseSchema | No | diff --git a/README.md b/README.md index 5b4b7bd30..85fb2f1ea 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@

- Chat on Discord + Chat on Discord

--- diff --git a/bindings/java/rs_src/limbo_connection.rs b/bindings/java/rs_src/limbo_connection.rs index 1399d8b42..dd54e9087 100644 --- a/bindings/java/rs_src/limbo_connection.rs +++ b/bindings/java/rs_src/limbo_connection.rs @@ -9,16 +9,19 @@ use jni::sys::jlong; use jni::JNIEnv; use limbo_core::Connection; use std::rc::Rc; +use std::sync::Arc; #[derive(Clone)] #[allow(dead_code)] pub struct LimboConnection { + // Because java's LimboConnection is 1:1 mapped to limbo connection, we can use Rc pub(crate) conn: Rc, - pub(crate) io: Rc, + // Because io is shared across multiple `LimboConnection`s, wrap it with Arc + pub(crate) io: Arc, } impl LimboConnection { - pub fn new(conn: Rc, io: Rc) -> Self { + pub fn new(conn: Rc, io: Arc) -> Self { LimboConnection { conn, io } } @@ -69,7 +72,7 @@ pub extern "system" fn Java_org_github_tursodatabase_core_LimboConnection_prepar }; match connection.conn.prepare(sql) { - Ok(stmt) => LimboStatement::new(stmt).to_ptr(), + Ok(stmt) => LimboStatement::new(stmt, connection.clone()).to_ptr(), Err(e) => { set_err_msg_and_throw_exception( &mut env, diff --git a/bindings/java/rs_src/limbo_db.rs b/bindings/java/rs_src/limbo_db.rs index 09d8afa75..16cb3d66b 100644 --- a/bindings/java/rs_src/limbo_db.rs +++ b/bindings/java/rs_src/limbo_db.rs @@ -5,16 +5,16 @@ use jni::objects::{JByteArray, JObject}; use jni::sys::{jint, jlong}; use jni::JNIEnv; use limbo_core::Database; -use std::rc::Rc; use std::sync::Arc; struct LimboDB { db: Arc, + io: Arc, } impl LimboDB { - pub fn new(db: Arc) -> Self { - LimboDB { db } + pub fn new(db: Arc, io: Arc) -> Self { + LimboDB { db, io } } pub fn to_ptr(self) -> jlong { @@ -76,14 +76,13 @@ pub extern "system" fn Java_org_github_tursodatabase_core_LimboDB_openUtf8<'loca } }; - LimboDB::new(db).to_ptr() + LimboDB::new(db, io).to_ptr() } #[no_mangle] pub extern "system" fn Java_org_github_tursodatabase_core_LimboDB_connect0<'local>( mut env: JNIEnv<'local>, obj: JObject<'local>, - file_path_byte_arr: JByteArray<'local>, db_pointer: jlong, ) -> jlong { let db = match to_limbo_db(db_pointer) { @@ -94,41 +93,7 @@ pub extern "system" fn Java_org_github_tursodatabase_core_LimboDB_connect0<'loca } }; - let path = match env - .convert_byte_array(file_path_byte_arr) - .map_err(|e| e.to_string()) - { - Ok(bytes) => match String::from_utf8(bytes) { - Ok(s) => s, - Err(e) => { - set_err_msg_and_throw_exception(&mut env, obj, LIMBO_ETC, e.to_string()); - return 0; - } - }, - Err(e) => { - set_err_msg_and_throw_exception(&mut env, obj, LIMBO_ETC, e.to_string()); - return 0; - } - }; - - let io: Rc = match path.as_str() { - ":memory:" => match limbo_core::MemoryIO::new() { - Ok(io) => Rc::new(io), - Err(e) => { - set_err_msg_and_throw_exception(&mut env, obj, LIMBO_ETC, e.to_string()); - return 0; - } - }, - _ => match limbo_core::PlatformIO::new() { - Ok(io) => Rc::new(io), - Err(e) => { - set_err_msg_and_throw_exception(&mut env, obj, LIMBO_ETC, e.to_string()); - return 0; - } - }, - }; - let conn = LimboConnection::new(db.db.connect(), io); - + let conn = LimboConnection::new(db.db.connect(), db.io.clone()); conn.to_ptr() } diff --git a/bindings/java/rs_src/limbo_statement.rs b/bindings/java/rs_src/limbo_statement.rs index cdd8a5c75..7de4b2c19 100644 --- a/bindings/java/rs_src/limbo_statement.rs +++ b/bindings/java/rs_src/limbo_statement.rs @@ -1,5 +1,6 @@ use crate::errors::Result; use crate::errors::{LimboError, LIMBO_ETC}; +use crate::limbo_connection::LimboConnection; use crate::utils::set_err_msg_and_throw_exception; use jni::objects::{JObject, JValue}; use jni::sys::jlong; @@ -7,6 +8,7 @@ use jni::JNIEnv; use limbo_core::{Statement, StepResult}; pub const STEP_RESULT_ID_ROW: i32 = 10; +#[allow(dead_code)] pub const STEP_RESULT_ID_IO: i32 = 20; pub const STEP_RESULT_ID_DONE: i32 = 30; pub const STEP_RESULT_ID_INTERRUPT: i32 = 40; @@ -15,11 +17,12 @@ pub const STEP_RESULT_ID_ERROR: i32 = 60; pub struct LimboStatement { pub(crate) stmt: Statement, + pub(crate) connection: LimboConnection, } impl LimboStatement { - pub fn new(stmt: Statement) -> Self { - LimboStatement { stmt } + pub fn new(stmt: Statement, connection: LimboConnection) -> Self { + LimboStatement { stmt, connection } } pub fn to_ptr(self) -> jlong { @@ -50,30 +53,38 @@ pub extern "system" fn Java_org_github_tursodatabase_core_LimboStatement_step<'l Ok(stmt) => stmt, Err(e) => { set_err_msg_and_throw_exception(&mut env, obj, LIMBO_ETC, e.to_string()); - - return JObject::null(); + return to_limbo_step_result(&mut env, STEP_RESULT_ID_ERROR, None); } }; - match stmt.stmt.step() { - Ok(StepResult::Row(row)) => match row_to_obj_array(&mut env, &row) { - Ok(row) => to_limbo_step_result(&mut env, STEP_RESULT_ID_ROW, Some(row)), - Err(e) => { - set_err_msg_and_throw_exception(&mut env, obj, LIMBO_ETC, e.to_string()); - to_limbo_step_result(&mut env, STEP_RESULT_ID_ERROR, None) + loop { + let step_result = match stmt.stmt.step() { + Ok(result) => result, + Err(_) => return to_limbo_step_result(&mut env, STEP_RESULT_ID_ERROR, None), + }; + + match step_result { + StepResult::Row(row) => { + return match row_to_obj_array(&mut env, &row) { + Ok(row) => to_limbo_step_result(&mut env, STEP_RESULT_ID_ROW, Some(row)), + Err(e) => { + set_err_msg_and_throw_exception(&mut env, obj, LIMBO_ETC, e.to_string()); + to_limbo_step_result(&mut env, STEP_RESULT_ID_ERROR, None) + } + } } - }, - Ok(StepResult::IO) => match env.new_object_array(0, "java/lang/Object", JObject::null()) { - Ok(row) => to_limbo_step_result(&mut env, STEP_RESULT_ID_IO, Some(row.into())), - Err(e) => { - set_err_msg_and_throw_exception(&mut env, obj, LIMBO_ETC, e.to_string()); - to_limbo_step_result(&mut env, STEP_RESULT_ID_ERROR, None) + StepResult::IO => { + if let Err(e) = stmt.connection.io.run_once() { + set_err_msg_and_throw_exception(&mut env, obj, LIMBO_ETC, e.to_string()); + return to_limbo_step_result(&mut env, STEP_RESULT_ID_ERROR, None); + } } - }, - Ok(StepResult::Done) => to_limbo_step_result(&mut env, STEP_RESULT_ID_DONE, None), - Ok(StepResult::Interrupt) => to_limbo_step_result(&mut env, STEP_RESULT_ID_INTERRUPT, None), - Ok(StepResult::Busy) => to_limbo_step_result(&mut env, STEP_RESULT_ID_BUSY, None), - _ => to_limbo_step_result(&mut env, STEP_RESULT_ID_ERROR, None), + StepResult::Done => return to_limbo_step_result(&mut env, STEP_RESULT_ID_DONE, None), + StepResult::Interrupt => { + return to_limbo_step_result(&mut env, STEP_RESULT_ID_INTERRUPT, None) + } + StepResult::Busy => return to_limbo_step_result(&mut env, STEP_RESULT_ID_BUSY, None), + } } } diff --git a/bindings/java/src/main/java/org/github/tursodatabase/core/LimboDB.java b/bindings/java/src/main/java/org/github/tursodatabase/core/LimboDB.java index 89d13b8cf..ad6ee68a0 100644 --- a/bindings/java/src/main/java/org/github/tursodatabase/core/LimboDB.java +++ b/bindings/java/src/main/java/org/github/tursodatabase/core/LimboDB.java @@ -1,5 +1,9 @@ package org.github.tursodatabase.core; +import static org.github.tursodatabase.utils.ByteArrayUtils.stringToUtf8ByteArray; + +import java.sql.SQLException; +import java.util.concurrent.locks.ReentrantLock; import org.github.tursodatabase.LimboErrorCode; import org.github.tursodatabase.annotations.NativeInvocation; @@ -8,12 +12,6 @@ import org.github.tursodatabase.utils.LimboExceptionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.sql.SQLException; -import java.sql.SQLFeatureNotSupportedException; -import java.util.concurrent.locks.ReentrantLock; - -import static org.github.tursodatabase.utils.ByteArrayUtils.stringToUtf8ByteArray; - /** * This class provides a thin JNI layer over the SQLite3 C API. */ @@ -39,7 +37,7 @@ public final class LimboDB extends AbstractDB { * Loads the SQLite interface backend. */ public static void load() { - if (isLoaded) return; + if (isLoaded) {return;} try { System.loadLibrary("_limbo_java"); @@ -49,7 +47,7 @@ public final class LimboDB extends AbstractDB { } /** - * @param url e.g. "jdbc:sqlite:fileName + * @param url e.g. "jdbc:sqlite:fileName * @param filePath e.g. path to file */ public static LimboDB create(String url, String filePath) throws SQLException { @@ -86,7 +84,9 @@ public final class LimboDB extends AbstractDB { byte[] filePathBytes = stringToUtf8ByteArray(filePath); if (filePathBytes == null) { - throw LimboExceptionUtils.buildLimboException(LimboErrorCode.LIMBO_ETC.code, "File path cannot be converted to byteArray. File name: " + filePath); + throw LimboExceptionUtils.buildLimboException( + LimboErrorCode.LIMBO_ETC.code, + "File path cannot be converted to byteArray. File name: " + filePath); } dbPointer = openUtf8(filePathBytes, openFlags); @@ -95,14 +95,10 @@ public final class LimboDB extends AbstractDB { @Override public long connect() throws SQLException { - byte[] filePathBytes = stringToUtf8ByteArray(filePath); - if (filePathBytes == null) { - throw LimboExceptionUtils.buildLimboException(LimboErrorCode.LIMBO_ETC.code, "File path cannot be converted to byteArray. File name: " + filePath); - } - return connect0(filePathBytes, dbPointer); + return connect0(dbPointer); } - private native long connect0(byte[] path, long databasePtr) throws SQLException; + private native long connect0(long databasePtr) throws SQLException; @VisibleForTesting native void throwJavaException(int errorCode) throws SQLException; @@ -110,7 +106,7 @@ public final class LimboDB extends AbstractDB { /** * Throws formatted SQLException with error code and message. * - * @param errorCode Error code. + * @param errorCode Error code. * @param errorMessageBytes Error message. */ @NativeInvocation(invokedFrom = "limbo_db.rs") diff --git a/bindings/java/src/main/java/org/github/tursodatabase/core/LimboResultSet.java b/bindings/java/src/main/java/org/github/tursodatabase/core/LimboResultSet.java index 19d730727..882d2b78b 100644 --- a/bindings/java/src/main/java/org/github/tursodatabase/core/LimboResultSet.java +++ b/bindings/java/src/main/java/org/github/tursodatabase/core/LimboResultSet.java @@ -64,6 +64,11 @@ public class LimboResultSet { row++; } + if (lastStepResult.isInInvalidState()) { + open = false; + throw new SQLException("step() returned invalid result: " + lastStepResult); + } + pastLastRow = lastStepResult.isDone(); if (pastLastRow) { open = false; diff --git a/bindings/java/src/main/java/org/github/tursodatabase/core/LimboStepResult.java b/bindings/java/src/main/java/org/github/tursodatabase/core/LimboStepResult.java index 7870cbeab..27a8dfc05 100644 --- a/bindings/java/src/main/java/org/github/tursodatabase/core/LimboStepResult.java +++ b/bindings/java/src/main/java/org/github/tursodatabase/core/LimboStepResult.java @@ -13,6 +13,7 @@ public class LimboStepResult { private static final int STEP_RESULT_ID_IO = 20; private static final int STEP_RESULT_ID_DONE = 30; private static final int STEP_RESULT_ID_INTERRUPT = 40; + // Indicates that the database file could not be written because of concurrent activity by some other connection private static final int STEP_RESULT_ID_BUSY = 50; private static final int STEP_RESULT_ID_ERROR = 60; @@ -41,6 +42,14 @@ public class LimboStepResult { return stepResultId == STEP_RESULT_ID_DONE; } + public boolean isInInvalidState() { + // current implementation doesn't allow STEP_RESULT_ID_IO to be returned + return stepResultId == STEP_RESULT_ID_IO || + stepResultId == STEP_RESULT_ID_INTERRUPT || + stepResultId == STEP_RESULT_ID_BUSY || + stepResultId == STEP_RESULT_ID_ERROR; + } + @Override public String toString() { return "LimboStepResult{" + diff --git a/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ResultSetTest.java b/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ResultSetTest.java index e717232a8..88a499b9d 100644 --- a/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ResultSetTest.java +++ b/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ResultSetTest.java @@ -9,7 +9,6 @@ import java.util.Properties; import org.github.tursodatabase.TestUtils; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; class JDBC4ResultSetTest { @@ -27,7 +26,6 @@ class JDBC4ResultSetTest { } @Test - @Disabled("https://github.com/tursodatabase/limbo/pull/743#issuecomment-2600746904") void invoking_next_before_the_last_row_should_return_true() throws Exception { stmt.executeUpdate("CREATE TABLE users (id INT PRIMARY KEY, username TEXT);"); stmt.executeUpdate("INSERT INTO users VALUES (1, 'sinwoo');"); @@ -41,7 +39,6 @@ class JDBC4ResultSetTest { } @Test - @Disabled("https://github.com/tursodatabase/limbo/pull/743#issuecomment-2600746904") void invoking_next_after_the_last_row_should_return_false() throws Exception { stmt.executeUpdate("CREATE TABLE users (id INT PRIMARY KEY, username TEXT);"); stmt.executeUpdate("INSERT INTO users VALUES (1, 'sinwoo');"); diff --git a/core/lib.rs b/core/lib.rs index f093762fb..d75510f7c 100644 --- a/core/lib.rs +++ b/core/lib.rs @@ -295,7 +295,6 @@ impl Connection { pub(crate) fn run_cmd(self: &Rc, cmd: Cmd) -> Result> { let db = self.db.clone(); let syms: &SymbolTable = &db.syms.borrow(); - match cmd { Cmd::Stmt(stmt) => { let program = Rc::new(translate::translate( @@ -466,6 +465,10 @@ impl Statement { Ok(Rows::new(stmt)) } + pub fn columns(&self) -> &[String] { + &self.program.columns + } + pub fn parameters(&self) -> ¶meters::Parameters { &self.program.parameters } @@ -513,6 +516,10 @@ impl Rows { pub fn next_row(&mut self) -> Result> { self.stmt.step() } + + pub fn columns(&self) -> &[String] { + self.stmt.columns() + } } pub(crate) struct SymbolTable { diff --git a/core/storage/page_cache.rs b/core/storage/page_cache.rs index e21433af1..27ea8244e 100644 --- a/core/storage/page_cache.rs +++ b/core/storage/page_cache.rs @@ -6,7 +6,7 @@ use super::pager::PageRef; // In limbo, page cache is shared by default, meaning that multiple frames from WAL can reside in // the cache, meaning, we need a way to differentiate between pages cached in different -// connections. For this we include the max_frame that will read a connection from so that if two +// connections. For this we include the max_frame that a connection will read from so that if two // connections have different max_frames, they might or not have different frame read from WAL. // // WAL was introduced after Shared cache in SQLite, so this is why these two features don't work diff --git a/core/storage/wal.rs b/core/storage/wal.rs index 5d40d5a1d..0de3b7590 100644 --- a/core/storage/wal.rs +++ b/core/storage/wal.rs @@ -187,7 +187,7 @@ pub enum CheckpointStatus { // min_frame and max_frame is the range of frames that can be safely transferred from WAL to db // file. // current_page is a helper to iterate through all the pages that might have a frame in the safe -// range. This is inneficient for now. +// range. This is inefficient for now. struct OngoingCheckpoint { page: PageRef, state: CheckpointState, @@ -228,13 +228,13 @@ pub struct WalFileShared { max_frame: u64, nbackfills: u64, // Frame cache maps a Page to all the frames it has stored in WAL in ascending order. - // This is do to easily find the frame it must checkpoint each connection if a checkpoint is + // This is to easily find the frame it must checkpoint each connection if a checkpoint is // necessary. // One difference between SQLite and limbo is that we will never support multi process, meaning // we don't need WAL's index file. So we can do stuff like this without shared memory. - // TODO: this will need refactoring because this is incredible memory inneficient. + // TODO: this will need refactoring because this is incredible memory inefficient. frame_cache: HashMap>, - // Another memory inneficient array made to just keep track of pages that are in frame_cache. + // Another memory inefficient array made to just keep track of pages that are in frame_cache. pages_in_frames: Vec, last_checksum: (u32, u32), // Check of last frame in WAL, this is a cumulative checksum over all frames in the WAL file: Rc, diff --git a/core/translate/emitter.rs b/core/translate/emitter.rs index 13daa85ed..939a287f0 100644 --- a/core/translate/emitter.rs +++ b/core/translate/emitter.rs @@ -175,7 +175,11 @@ fn emit_program_for_select( // Finalize program epilogue(program, init_label, start_offset)?; - + program.columns = plan + .result_columns + .iter() + .map(|rc| rc.name.clone()) + .collect::>(); Ok(()) } @@ -286,7 +290,11 @@ fn emit_program_for_delete( // Finalize program epilogue(program, init_label, start_offset)?; - + program.columns = plan + .result_columns + .iter() + .map(|rc| rc.name.clone()) + .collect::>(); Ok(()) } diff --git a/core/translate/expr.rs b/core/translate/expr.rs index 598a6f858..d1fea2ee2 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -605,6 +605,20 @@ pub fn translate_expr( dest: target_register, }); } + ast::Operator::And => { + program.emit_insn(Insn::And { + lhs: e1_reg, + rhs: e2_reg, + dest: target_register, + }); + } + ast::Operator::Or => { + program.emit_insn(Insn::Or { + lhs: e1_reg, + rhs: e2_reg, + dest: target_register, + }); + } ast::Operator::BitwiseAnd => { program.emit_insn(Insn::BitAnd { lhs: e1_reg, diff --git a/core/vdbe/builder.rs b/core/vdbe/builder.rs index 08b35d9e3..0af4d1182 100644 --- a/core/vdbe/builder.rs +++ b/core/vdbe/builder.rs @@ -30,6 +30,7 @@ pub struct ProgramBuilder { // map of instruction index to manual comment (used in EXPLAIN) comments: HashMap, pub parameters: Parameters, + pub columns: Vec, } #[derive(Debug, Clone)] @@ -60,6 +61,7 @@ impl ProgramBuilder { seekrowid_emitted_bitmask: 0, comments: HashMap::new(), parameters: Parameters::new(), + columns: Vec::new(), } } @@ -352,6 +354,7 @@ impl ProgramBuilder { parameters: self.parameters, n_change: Cell::new(0), change_cnt_on, + columns: self.columns, } } } diff --git a/core/vdbe/explain.rs b/core/vdbe/explain.rs index 80f419ed7..89967608a 100644 --- a/core/vdbe/explain.rs +++ b/core/vdbe/explain.rs @@ -1120,6 +1120,24 @@ pub fn insn_to_str( 0, format!("r[{}]=r[{}] + r[{}]", dest, lhs, rhs), ), + Insn::And { lhs, rhs, dest } => ( + "And", + *rhs as i32, + *lhs as i32, + *dest as i32, + OwnedValue::build_text(Rc::new("".to_string())), + 0, + format!("r[{}]=(r[{}] && r[{}])", dest, lhs, rhs), + ), + Insn::Or { lhs, rhs, dest } => ( + "Or", + *rhs as i32, + *lhs as i32, + *dest as i32, + OwnedValue::build_text(Rc::new("".to_string())), + 0, + format!("r[{}]=(r[{}] || r[{}])", dest, lhs, rhs), + ), }; format!( "{:<4} {:<17} {:<4} {:<4} {:<4} {:<13} {:<2} {}", diff --git a/core/vdbe/insn.rs b/core/vdbe/insn.rs index f3f5e36a7..f17a1a354 100644 --- a/core/vdbe/insn.rs +++ b/core/vdbe/insn.rs @@ -551,6 +551,18 @@ pub enum Insn { rhs: usize, dest: usize, }, + /// Take the logical AND of the values in registers P1 and P2 and write the result into register P3. + And { + lhs: usize, + rhs: usize, + dest: usize, + }, + /// Take the logical OR of the values in register P1 and P2 and store the answer in register P3. + Or { + lhs: usize, + rhs: usize, + dest: usize, + }, } fn cast_text_to_numerical(value: &str) -> OwnedValue { @@ -955,3 +967,167 @@ pub fn exec_concat(lhs: &OwnedValue, rhs: &OwnedValue) -> OwnedValue { (OwnedValue::Record(_), _) | (_, OwnedValue::Record(_)) => unreachable!(), } } + +pub fn exec_and(mut lhs: &OwnedValue, mut rhs: &OwnedValue) -> OwnedValue { + if let OwnedValue::Agg(agg) = lhs { + lhs = agg.final_value(); + } + if let OwnedValue::Agg(agg) = rhs { + rhs = agg.final_value(); + } + + match (lhs, rhs) { + (_, OwnedValue::Integer(0)) + | (OwnedValue::Integer(0), _) + | (_, OwnedValue::Float(0.0)) + | (OwnedValue::Float(0.0), _) => OwnedValue::Integer(0), + (OwnedValue::Null, _) | (_, OwnedValue::Null) => OwnedValue::Null, + (OwnedValue::Text(lhs), OwnedValue::Text(rhs)) => exec_and( + &cast_text_to_numerical(&lhs.value), + &cast_text_to_numerical(&rhs.value), + ), + (OwnedValue::Text(text), other) | (other, OwnedValue::Text(text)) => { + exec_and(&cast_text_to_numerical(&text.value), other) + } + _ => OwnedValue::Integer(1), + } +} + +pub fn exec_or(mut lhs: &OwnedValue, mut rhs: &OwnedValue) -> OwnedValue { + if let OwnedValue::Agg(agg) = lhs { + lhs = agg.final_value(); + } + if let OwnedValue::Agg(agg) = rhs { + rhs = agg.final_value(); + } + + match (lhs, rhs) { + (OwnedValue::Null, OwnedValue::Null) + | (OwnedValue::Null, OwnedValue::Float(0.0)) + | (OwnedValue::Float(0.0), OwnedValue::Null) + | (OwnedValue::Null, OwnedValue::Integer(0)) + | (OwnedValue::Integer(0), OwnedValue::Null) => OwnedValue::Null, + (OwnedValue::Float(0.0), OwnedValue::Integer(0)) + | (OwnedValue::Integer(0), OwnedValue::Float(0.0)) + | (OwnedValue::Float(0.0), OwnedValue::Float(0.0)) + | (OwnedValue::Integer(0), OwnedValue::Integer(0)) => OwnedValue::Integer(0), + (OwnedValue::Text(lhs), OwnedValue::Text(rhs)) => exec_or( + &cast_text_to_numerical(&lhs.value), + &cast_text_to_numerical(&rhs.value), + ), + (OwnedValue::Text(text), other) | (other, OwnedValue::Text(text)) => { + exec_or(&cast_text_to_numerical(&text.value), other) + } + _ => OwnedValue::Integer(1), + } +} + +#[cfg(test)] +mod tests { + use std::rc::Rc; + + use crate::{ + types::{LimboText, OwnedValue}, + vdbe::insn::exec_or, + }; + + use super::exec_and; + + #[test] + fn test_exec_and() { + let inputs = vec![ + (OwnedValue::Integer(0), OwnedValue::Null), + (OwnedValue::Null, OwnedValue::Integer(1)), + (OwnedValue::Null, OwnedValue::Null), + (OwnedValue::Float(0.0), OwnedValue::Null), + (OwnedValue::Integer(1), OwnedValue::Float(2.2)), + ( + OwnedValue::Integer(0), + OwnedValue::Text(LimboText::new(Rc::new("string".to_string()))), + ), + ( + OwnedValue::Integer(0), + OwnedValue::Text(LimboText::new(Rc::new("1".to_string()))), + ), + ( + OwnedValue::Integer(1), + OwnedValue::Text(LimboText::new(Rc::new("1".to_string()))), + ), + ]; + let outpus = [ + OwnedValue::Integer(0), + OwnedValue::Null, + OwnedValue::Null, + OwnedValue::Integer(0), + OwnedValue::Integer(1), + OwnedValue::Integer(0), + OwnedValue::Integer(0), + OwnedValue::Integer(1), + ]; + + assert_eq!( + inputs.len(), + outpus.len(), + "Inputs and Outputs should have same size" + ); + for (i, (lhs, rhs)) in inputs.iter().enumerate() { + assert_eq!( + exec_and(lhs, rhs), + outpus[i], + "Wrong AND for lhs: {}, rhs: {}", + lhs, + rhs + ); + } + } + + #[test] + fn test_exec_or() { + let inputs = vec![ + (OwnedValue::Integer(0), OwnedValue::Null), + (OwnedValue::Null, OwnedValue::Integer(1)), + (OwnedValue::Null, OwnedValue::Null), + (OwnedValue::Float(0.0), OwnedValue::Null), + (OwnedValue::Integer(1), OwnedValue::Float(2.2)), + (OwnedValue::Float(0.0), OwnedValue::Integer(0)), + ( + OwnedValue::Integer(0), + OwnedValue::Text(LimboText::new(Rc::new("string".to_string()))), + ), + ( + OwnedValue::Integer(0), + OwnedValue::Text(LimboText::new(Rc::new("1".to_string()))), + ), + ( + OwnedValue::Integer(0), + OwnedValue::Text(LimboText::new(Rc::new("".to_string()))), + ), + ]; + let outpus = [ + OwnedValue::Null, + OwnedValue::Integer(1), + OwnedValue::Null, + OwnedValue::Null, + OwnedValue::Integer(1), + OwnedValue::Integer(0), + OwnedValue::Integer(0), + OwnedValue::Integer(1), + OwnedValue::Integer(0), + ]; + + assert_eq!( + inputs.len(), + outpus.len(), + "Inputs and Outputs should have same size" + ); + for (i, (lhs, rhs)) in inputs.iter().enumerate() { + assert_eq!( + exec_or(lhs, rhs), + outpus[i], + "Wrong OR for lhs: {}, rhs: {}", + lhs, + rhs + ); + } + } +} diff --git a/core/vdbe/mod.rs b/core/vdbe/mod.rs index 55f009314..dae7f8806 100644 --- a/core/vdbe/mod.rs +++ b/core/vdbe/mod.rs @@ -48,8 +48,9 @@ use datetime::{ exec_date, exec_datetime_full, exec_julianday, exec_strftime, exec_time, exec_unixepoch, }; use insn::{ - exec_add, exec_bit_and, exec_bit_not, exec_bit_or, exec_boolean_not, exec_concat, exec_divide, - exec_multiply, exec_remainder, exec_shift_left, exec_shift_right, exec_subtract, + exec_add, exec_and, exec_bit_and, exec_bit_not, exec_bit_or, exec_boolean_not, exec_concat, + exec_divide, exec_multiply, exec_or, exec_remainder, exec_shift_left, exec_shift_right, + exec_subtract, }; use likeop::{construct_like_escape_arg, exec_glob, exec_like_with_escape}; use rand::distributions::{Distribution, Uniform}; @@ -286,6 +287,7 @@ pub struct Program { pub auto_commit: bool, pub n_change: Cell, pub change_cnt_on: bool, + pub columns: Vec, } impl Program { @@ -2354,6 +2356,16 @@ impl Program { exec_concat(&state.registers[*lhs], &state.registers[*rhs]); state.pc += 1; } + Insn::And { lhs, rhs, dest } => { + state.registers[*dest] = + exec_and(&state.registers[*lhs], &state.registers[*rhs]); + state.pc += 1; + } + Insn::Or { lhs, rhs, dest } => { + state.registers[*dest] = + exec_or(&state.registers[*lhs], &state.registers[*rhs]); + state.pc += 1; + } } } } diff --git a/tests/integration/common.rs b/tests/integration/common.rs index 86f4b7b3f..07c840b23 100644 --- a/tests/integration/common.rs +++ b/tests/integration/common.rs @@ -67,3 +67,45 @@ pub(crate) fn compare_string(a: &String, b: &String) { } } } + +#[cfg(test)] +mod tests { + use super::TempDatabase; + + #[test] + fn test_statement_columns() -> anyhow::Result<()> { + let _ = env_logger::try_init(); + let tmp_db = + TempDatabase::new("create table test (foo integer, bar integer, baz integer);"); + let conn = tmp_db.connect_limbo(); + + let stmt = conn.prepare("select * from test;")?; + + let columns = stmt.columns(); + assert_eq!(columns.len(), 3); + assert_eq!(&columns[0], "foo"); + assert_eq!(&columns[1], "bar"); + assert_eq!(&columns[2], "baz"); + + let stmt = conn.prepare("select foo, bar from test;")?; + + let columns = stmt.columns(); + assert_eq!(columns.len(), 2); + assert_eq!(&columns[0], "foo"); + assert_eq!(&columns[1], "bar"); + + let stmt = conn.prepare("delete from test;")?; + let columns = stmt.columns(); + assert_eq!(columns.len(), 0); + + let stmt = conn.prepare("insert into test (foo, bar, baz) values (1, 2, 3);")?; + let columns = stmt.columns(); + assert_eq!(columns.len(), 0); + + let stmt = conn.prepare("delete from test where foo = 1")?; + let columns = stmt.columns(); + assert_eq!(columns.len(), 0); + + Ok(()) + } +}