diff --git a/.github/workflows/java.yml b/.github/workflows/java.yml new file mode 100644 index 000000000..7c3c6b7ba --- /dev/null +++ b/.github/workflows/java.yml @@ -0,0 +1,38 @@ +name: Java Tests + +on: + push: + branches: + - main + tags: + - v* + pull_request: + branches: + - main + +env: + working-directory: bindings/java + +jobs: + test: + runs-on: ubuntu-latest + + defaults: + run: + working-directory: ${{ env.working-directory }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust(stable) + uses: dtolnay/rust-toolchain@stable + + - name: Set up JDK + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '11' + + - name: Run Java tests + run: make test diff --git a/COMPAT.md b/COMPAT.md index 83888f87c..4ddabe10f 100644 --- a/COMPAT.md +++ b/COMPAT.md @@ -281,8 +281,8 @@ Feature support of [sqlite expr syntax](https://www.sqlite.org/lang_expr.html). | jsonb_replace(json,path,value,...) | | | | json_set(json,path,value,...) | | | | jsonb_set(json,path,value,...) | | | -| json_type(json) | | | -| json_type(json,path) | | | +| json_type(json) | Yes | | +| json_type(json,path) | Yes | | | json_valid(json) | | | | json_valid(json,flags) | | | | json_quote(value) | | | diff --git a/Cargo.lock b/Cargo.lock index 69129c585..915456422 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1192,6 +1192,8 @@ dependencies = [ "js-sys", "limbo_core", "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[package]] @@ -2495,6 +2497,19 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.99" diff --git a/bindings/java/Makefile b/bindings/java/Makefile index e91fcc923..5d091145d 100644 --- a/bindings/java/Makefile +++ b/bindings/java/Makefile @@ -1,7 +1,7 @@ -java_run: lib - export LIMBO_SYSTEM_PATH=../../target/debug && ./gradlew run +.PHONY: test build_test -.PHONY: lib +test: build_test + ./gradlew test -lib: - cargo build +build_test: + CARGO_TARGET_DIR=src/test/resources/limbo cargo build diff --git a/bindings/java/build.gradle.kts b/bindings/java/build.gradle.kts index 331b4831f..f1349859d 100644 --- a/bindings/java/build.gradle.kts +++ b/bindings/java/build.gradle.kts @@ -13,6 +13,7 @@ repositories { dependencies { testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("org.assertj:assertj-core:3.27.0") } application { @@ -28,4 +29,6 @@ application { tasks.test { useJUnitPlatform() + // In order to find rust built file under resources, we need to set it as system path + systemProperty("java.library.path", "${System.getProperty("java.library.path")}:$projectDir/src/test/resources/limbo/debug") } diff --git a/bindings/java/rs_src/errors.rs b/bindings/java/rs_src/errors.rs index 490fdfbd9..7924ffb68 100644 --- a/bindings/java/rs_src/errors.rs +++ b/bindings/java/rs_src/errors.rs @@ -6,7 +6,7 @@ pub struct CustomError { } /// This struct defines error codes that correspond to the constants defined in the -/// Java package `org.github.tursodatabase.exceptions.ErrorCode`. +/// Java package `org.github.tursodatabase.LimboErrorCode`. /// /// These error codes are used to handle and represent specific error conditions /// that may occur within the Rust code and need to be communicated to the Java side. @@ -14,8 +14,7 @@ pub struct CustomError { pub struct ErrorCode; impl ErrorCode { - pub const CONNECTION_FAILURE: i32 = -1; - + // TODO: change CONNECTION_FAILURE_STATEMENT_IS_DML to appropriate error code number pub const STATEMENT_IS_DML: i32 = -1; } diff --git a/bindings/java/rs_src/lib.rs b/bindings/java/rs_src/lib.rs index 4ba9ba2c4..4dfadd743 100644 --- a/bindings/java/rs_src/lib.rs +++ b/bindings/java/rs_src/lib.rs @@ -1,66 +1,6 @@ mod connection; mod cursor; mod errors; +mod limbo_db; mod macros; mod utils; - -use crate::connection::Connection; -use crate::errors::ErrorCode; -use jni::errors::JniError; -use jni::objects::{JClass, JString}; -use jni::sys::jlong; -use jni::JNIEnv; -use std::sync::{Arc, Mutex}; - -/// Establishes a connection to the database specified by the given path. -/// -/// This function is called from the Java side to create a connection to the database. -/// It returns a pointer to the `Connection` object, which can be used in subsequent -/// native function calls. -/// -/// # Arguments -/// -/// * `env` - The JNI environment pointer. -/// * `_class` - The Java class calling this function. -/// * `path` - A `JString` representing the path to the database file. -/// -/// # Returns -/// -/// A `jlong` representing the pointer to the newly created `Connection` object, -/// or [ErrorCode::CONNECTION_FAILURE] if the connection could not be established. -#[no_mangle] -pub extern "system" fn Java_org_github_tursodatabase_limbo_Limbo_connect<'local>( - mut env: JNIEnv<'local>, - _class: JClass<'local>, - path: JString<'local>, -) -> jlong { - connect_internal(&mut env, path).unwrap_or_else(|_| ErrorCode::CONNECTION_FAILURE as jlong) -} - -#[allow(improper_ctypes_definitions, clippy::arc_with_non_send_sync)] // TODO: remove -fn connect_internal<'local>( - env: &mut JNIEnv<'local>, - path: JString<'local>, -) -> Result { - let io = Arc::new(limbo_core::PlatformIO::new().map_err(|e| { - println!("IO initialization failed: {:?}", e); - JniError::Unknown - })?); - - let path: String = env - .get_string(&path) - .expect("Failed to convert JString to Rust String") - .into(); - let db = limbo_core::Database::open_file(io.clone(), &path).map_err(|e| { - println!("Failed to open database: {:?}", e); - JniError::Unknown - })?; - - let conn = db.connect().clone(); - let connection = Connection { - conn: Arc::new(Mutex::new(conn)), - io, - }; - - Ok(Box::into_raw(Box::new(connection)) as jlong) -} diff --git a/bindings/java/rs_src/limbo_db.rs b/bindings/java/rs_src/limbo_db.rs new file mode 100644 index 000000000..e03e76fa3 --- /dev/null +++ b/bindings/java/rs_src/limbo_db.rs @@ -0,0 +1,89 @@ +use jni::objects::{JByteArray, JObject}; +use jni::sys::{jint, jlong}; +use jni::JNIEnv; +use limbo_core::Database; +use std::sync::Arc; + +const ERROR_CODE_ETC: i32 = 9999; + +#[no_mangle] +#[allow(clippy::arc_with_non_send_sync)] +pub extern "system" fn Java_org_github_tursodatabase_core_LimboDB__1open_1utf8<'local>( + mut env: JNIEnv<'local>, + obj: JObject<'local>, + file_name_byte_arr: JByteArray<'local>, + _open_flags: jint, +) -> jlong { + let io = match limbo_core::PlatformIO::new() { + Ok(io) => Arc::new(io), + Err(e) => { + set_err_msg_and_throw_exception(&mut env, obj, ERROR_CODE_ETC, e.to_string()); + return -1; + } + }; + + let path = match env + .convert_byte_array(file_name_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, ERROR_CODE_ETC, e.to_string()); + return -1; + } + }, + Err(e) => { + set_err_msg_and_throw_exception(&mut env, obj, ERROR_CODE_ETC, e.to_string()); + return -1; + } + }; + + let db = match Database::open_file(io.clone(), &path) { + Ok(db) => db, + Err(e) => { + set_err_msg_and_throw_exception(&mut env, obj, ERROR_CODE_ETC, e.to_string()); + return -1; + } + }; + + Box::into_raw(Box::new(db)) as jlong +} + +#[no_mangle] +pub extern "system" fn Java_org_github_tursodatabase_core_LimboDB_throwJavaException<'local>( + mut env: JNIEnv<'local>, + obj: JObject<'local>, + error_code: jint, +) { + set_err_msg_and_throw_exception( + &mut env, + obj, + error_code, + "throw java exception".to_string(), + ); +} + +fn set_err_msg_and_throw_exception<'local>( + env: &mut JNIEnv<'local>, + obj: JObject<'local>, + err_code: i32, + err_msg: String, +) { + let error_message_bytes = env + .byte_array_from_slice(err_msg.as_bytes()) + .expect("Failed to convert to byte array"); + match env.call_method( + obj, + "throwLimboException", + "(I[B)V", + &[err_code.into(), (&error_message_bytes).into()], + ) { + Ok(_) => { + // do nothing because above method will always return Err + } + Err(_e) => { + // do nothing because our java app will handle Err + } + } +} diff --git a/bindings/java/src/main/java/org/github/tursodatabase/LimboErrorCode.java b/bindings/java/src/main/java/org/github/tursodatabase/LimboErrorCode.java new file mode 100644 index 000000000..0c65ba04f --- /dev/null +++ b/bindings/java/src/main/java/org/github/tursodatabase/LimboErrorCode.java @@ -0,0 +1,34 @@ +package org.github.tursodatabase; + +public enum LimboErrorCode { + UNKNOWN_ERROR(-1, "Unknown error"), + ETC(9999, "Unclassified error"); + + public final int code; + public final String message; + + /** + * @param code Error code + * @param message Message for the error. + */ + LimboErrorCode(int code, String message) { + this.code = code; + this.message = message; + } + + public static LimboErrorCode getErrorCode(int errorCode) { + for (LimboErrorCode limboErrorCode: LimboErrorCode.values()) { + if (errorCode == limboErrorCode.code) return limboErrorCode; + } + + return UNKNOWN_ERROR; + } + + @Override + public String toString() { + return "LimboErrorCode{" + + "code=" + code + + ", message='" + message + '\'' + + '}'; + } +} diff --git a/bindings/java/src/main/java/org/github/tursodatabase/Main.java b/bindings/java/src/main/java/org/github/tursodatabase/Main.java deleted file mode 100644 index de9b94e36..000000000 --- a/bindings/java/src/main/java/org/github/tursodatabase/Main.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.github.tursodatabase; - -import org.github.tursodatabase.limbo.Connection; -import org.github.tursodatabase.limbo.Cursor; -import org.github.tursodatabase.limbo.Limbo; - -/** - * TODO: Remove Main class. We can use test code to verify behaviors. - */ -public class Main { - public static void main(String[] args) throws Exception { - Limbo limbo = Limbo.create(); - Connection connection = limbo.getConnection("database.db"); - - Cursor cursor = connection.cursor(); - cursor.execute("SELECT * FROM example_table;"); - System.out.println("result: " + cursor.fetchOne()); - } -} diff --git a/bindings/java/src/main/java/org/github/tursodatabase/NativeInvocation.java b/bindings/java/src/main/java/org/github/tursodatabase/NativeInvocation.java new file mode 100644 index 000000000..70fd6c100 --- /dev/null +++ b/bindings/java/src/main/java/org/github/tursodatabase/NativeInvocation.java @@ -0,0 +1,15 @@ +package org.github.tursodatabase; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to mark methods that are called by native functions. + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.METHOD) +public @interface NativeInvocation { +} diff --git a/bindings/java/src/main/java/org/github/tursodatabase/VisibleForTesting.java b/bindings/java/src/main/java/org/github/tursodatabase/VisibleForTesting.java new file mode 100644 index 000000000..1afd119c3 --- /dev/null +++ b/bindings/java/src/main/java/org/github/tursodatabase/VisibleForTesting.java @@ -0,0 +1,14 @@ +package org.github.tursodatabase; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to mark methods that use larger visibility for testing purposes. + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.METHOD) +public @interface VisibleForTesting { +} diff --git a/bindings/java/src/main/java/org/github/tursodatabase/core/DB.java b/bindings/java/src/main/java/org/github/tursodatabase/core/AbstractDB.java similarity index 87% rename from bindings/java/src/main/java/org/github/tursodatabase/core/DB.java rename to bindings/java/src/main/java/org/github/tursodatabase/core/AbstractDB.java index 4d82a7a92..62bd86de3 100644 --- a/bindings/java/src/main/java/org/github/tursodatabase/core/DB.java +++ b/bindings/java/src/main/java/org/github/tursodatabase/core/AbstractDB.java @@ -1,5 +1,9 @@ package org.github.tursodatabase.core; +import org.github.tursodatabase.LimboErrorCode; +import org.github.tursodatabase.NativeInvocation; +import org.github.tursodatabase.exceptions.LimboException; + import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.util.concurrent.atomic.AtomicBoolean; @@ -10,18 +14,14 @@ import java.util.concurrent.atomic.AtomicBoolean; * are not only to provide functionality, but to handle contractual * differences between the JDBC specification and the Limbo API. */ -public abstract class DB { +public abstract class AbstractDB { private final String url; private final String fileName; private final AtomicBoolean closed = new AtomicBoolean(true); - public DB(String url, String fileName) throws SQLException { + public AbstractDB(String url, String filaName) throws SQLException { this.url = url; - this.fileName = fileName; - } - - public String getUrl() { - return url; + this.fileName = filaName; } public boolean isClosed() { @@ -36,7 +36,7 @@ public abstract class DB { /** * Executes an SQL statement. * - * @param sql SQL statement to be executed. + * @param sql SQL statement to be executed. * @param autoCommit Whether to auto-commit the transaction. * @throws SQLException if a database access error occurs. */ @@ -47,17 +47,16 @@ public abstract class DB { /** * Creates an SQLite interface to a database for the given connection. - * @see SQLite Open Flags * - * @param fileName The database. * @param openFlags Flags for opening the database. * @throws SQLException if a database access error occurs. */ - public final synchronized void open(String fileName, int openFlags) throws SQLException { - // TODO: add implementation - throw new SQLFeatureNotSupportedException(); + public final synchronized void open(int openFlags) throws SQLException { + _open(fileName, openFlags); } + protected abstract void _open(String fileName, int openFlags) throws SQLException; + /** * Closes a database connection and finalizes any remaining statements before the closing * operation. @@ -95,13 +94,13 @@ public abstract class DB { /** * Creates an SQLite interface to a database with the provided open flags. - * @see SQLite Open Flags * - * @param filename The database to open. + * @param fileName The database to open. * @param openFlags Flags for opening the database. + * @return pointer to database instance * @throws SQLException if a database access error occurs. */ - protected abstract void _open(String filename, int openFlags) throws SQLException; + protected abstract long _open_utf8(byte[] fileName, int openFlags) throws SQLException; /** * Closes the SQLite interface to a database. 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 095da6910..80c3fbe8b 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,65 +1,69 @@ package org.github.tursodatabase.core; +import org.github.tursodatabase.LimboErrorCode; +import org.github.tursodatabase.NativeInvocation; +import org.github.tursodatabase.VisibleForTesting; +import org.github.tursodatabase.exceptions.LimboException; + +import java.nio.charset.StandardCharsets; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; /** * This class provides a thin JNI layer over the SQLite3 C API. */ -public final class LimboDB extends DB { - /** - * SQLite connection handle. - */ - private long pointer = 0; +public final class LimboDB extends AbstractDB { + + // Pointer to database instance + private long dbPtr; + private boolean isOpen; private static boolean isLoaded; - private static boolean loadSucceeded; static { if ("The Android Project".equals(System.getProperty("java.vm.vendor"))) { - System.loadLibrary("sqlitejdbc"); - isLoaded = true; - loadSucceeded = true; + // TODO } else { // continue with non Android execution path isLoaded = false; - loadSucceeded = false; } } + // url example: "jdbc:sqlite:{fileName} + + /** + * @param url e.g. "jdbc:sqlite:fileName + * @param fileName e.g. path to file + */ + public static LimboDB create(String url, String fileName) throws SQLException { + return new LimboDB(url, fileName); + } + // TODO: receive config as argument - public LimboDB(String url, String fileName) throws SQLException { + private LimboDB(String url, String fileName) throws SQLException { super(url, fileName); } /** * Loads the SQLite interface backend. - * - * @return True if the SQLite JDBC driver is successfully loaded; false otherwise. */ - public static boolean load() throws Exception { - if (isLoaded) return loadSucceeded; + public void load() { + if (isLoaded) return; try { System.loadLibrary("_limbo_java"); - loadSucceeded = true; + } finally { isLoaded = true; } - return loadSucceeded; } // WRAPPER FUNCTIONS //////////////////////////////////////////// - @Override - protected synchronized void _open(String file, int openFlags) throws SQLException { - // TODO: add implementation - throw new SQLFeatureNotSupportedException(); - } - // TODO: add support for JNI - synchronized native void _open_utf8(byte[] fileUtf8, int openFlags) throws SQLException; + @Override + protected synchronized native long _open_utf8(byte[] file, int openFlags) throws SQLException; // TODO: add support for JNI @Override @@ -78,6 +82,15 @@ public final class LimboDB extends DB { @Override public native void interrupt(); + @Override + protected void _open(String fileName, int openFlags) throws SQLException { + if (isOpen) { + throwLimboException(LimboErrorCode.UNKNOWN_ERROR.code, "Already opened"); + } + dbPtr = _open_utf8(stringToUtf8ByteArray(fileName), openFlags); + isOpen = true; + } + @Override protected synchronized SafeStmtPtr prepare(String sql) throws SQLException { // TODO: add implementation @@ -91,4 +104,52 @@ public final class LimboDB extends DB { // TODO: add support for JNI @Override public synchronized native int step(long stmt); + + @VisibleForTesting + native void throwJavaException(int errorCode) throws SQLException; + + /** + * Throws formatted SQLException with error code and message. + * + * @param errorCode Error code. + * @param errorMessageBytes Error message. + */ + @NativeInvocation + private void throwLimboException(int errorCode, byte[] errorMessageBytes) throws SQLException { + String errorMessage = utf8ByteBufferToString(errorMessageBytes); + throwLimboException(errorCode, errorMessage); + } + + /** + * Throws formatted SQLException with error code and message. + * + * @param errorCode Error code. + * @param errorMessage Error message. + */ + public void throwLimboException(int errorCode, String errorMessage) throws SQLException { + LimboErrorCode code = LimboErrorCode.getErrorCode(errorCode); + String msg; + if (code == LimboErrorCode.UNKNOWN_ERROR) { + msg = String.format("%s:%s (%s)", code, errorCode, errorMessage); + } else { + msg = String.format("%s (%s)", code, errorMessage); + } + + throw new LimboException(msg, code); + } + + private static String utf8ByteBufferToString(byte[] buffer) { + if (buffer == null) { + return null; + } + + return new String(buffer, StandardCharsets.UTF_8); + } + + private static byte[] stringToUtf8ByteArray(String str) { + if (str == null) { + return null; + } + return str.getBytes(StandardCharsets.UTF_8); + } } diff --git a/bindings/java/src/main/java/org/github/tursodatabase/exceptions/LimboException.java b/bindings/java/src/main/java/org/github/tursodatabase/exceptions/LimboException.java new file mode 100644 index 000000000..d4526a818 --- /dev/null +++ b/bindings/java/src/main/java/org/github/tursodatabase/exceptions/LimboException.java @@ -0,0 +1,18 @@ +package org.github.tursodatabase.exceptions; + +import org.github.tursodatabase.LimboErrorCode; + +import java.sql.SQLException; + +public class LimboException extends SQLException { + private final LimboErrorCode resultCode; + + public LimboException(String message, LimboErrorCode resultCode) { + super(message, null, resultCode.code & 0xff); + this.resultCode = resultCode; + } + + public LimboErrorCode getResultCode() { + return resultCode; + } +} diff --git a/bindings/java/src/main/java/org/github/tursodatabase/limbo/Connection.java b/bindings/java/src/main/java/org/github/tursodatabase/limbo/Connection.java deleted file mode 100644 index aaf0f2ca2..000000000 --- a/bindings/java/src/main/java/org/github/tursodatabase/limbo/Connection.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.github.tursodatabase.limbo; - -import java.lang.Exception; - -/** - * Represents a connection to the database. - * TODO: Deprecate classes under limbo package. We leave this source code for reference. - */ -public class Connection { - - // Pointer to the connection object - private final long connectionPtr; - - public Connection(long connectionPtr) { - this.connectionPtr = connectionPtr; - } - - /** - * Creates a new cursor object using this connection. - * - * @return A new Cursor object. - * @throws Exception If the cursor cannot be created. - */ - public Cursor cursor() throws Exception { - long cursorId = cursor(connectionPtr); - return new Cursor(cursorId); - } - - private native long cursor(long connectionPtr); - - /** - * Closes the connection to the database. - * - * @throws Exception If there is an error closing the connection. - */ - public void close() throws Exception { - close(connectionPtr); - } - - private native void close(long connectionPtr); - - /** - * Commits the current transaction. - * - * @throws Exception If there is an error during commit. - */ - public void commit() throws Exception { - try { - commit(connectionPtr); - } catch (Exception e) { - System.out.println("caught exception: " + e); - } - } - - private native void commit(long connectionPtr) throws Exception; - - /** - * Rolls back the current transaction. - * - * @throws Exception If there is an error during rollback. - */ - public void rollback() throws Exception { - rollback(connectionPtr); - } - - private native void rollback(long connectionPtr) throws Exception; -} diff --git a/bindings/java/src/main/java/org/github/tursodatabase/limbo/Cursor.java b/bindings/java/src/main/java/org/github/tursodatabase/limbo/Cursor.java deleted file mode 100644 index eaec47f04..000000000 --- a/bindings/java/src/main/java/org/github/tursodatabase/limbo/Cursor.java +++ /dev/null @@ -1,86 +0,0 @@ -package org.github.tursodatabase.limbo; - -/** - * Represents a database cursor. - * TODO: Deprecate classes under limbo package. We leave this source code for reference. - */ -public class Cursor { - private long cursorPtr; - - public Cursor(long cursorPtr) { - this.cursorPtr = cursorPtr; - } - - // TODO: support parameters - public Cursor execute(String sql) { - var result = execute(cursorPtr, sql); - System.out.println("resut: " + result); - return this; - } - - private static native int execute(long cursorPtr, String sql); - - public Object fetchOne() throws Exception { - Object result = fetchOne(cursorPtr); - return processSingleResult(result); - } - - private static native Object fetchOne(long cursorPtr); - - public Object fetchAll() throws Exception { - Object result = fetchAll(cursorPtr); - return processArrayResult(result); - } - - private static native Object fetchAll(long cursorPtr); - - private Object processSingleResult(Object result) throws Exception { - if (result instanceof Object[]) { - System.out.println("The result is of type: Object[]"); - for (Object element : (Object[]) result) { - printElementType(element); - } - return result; - } else { - printElementType(result); - return result; - } - } - - private Object processArrayResult(Object result) throws Exception { - if (result instanceof Object[][]) { - System.out.println("The result is of type: Object[][]"); - Object[][] array = (Object[][]) result; - for (Object[] row : array) { - for (Object element : row) { - printElementType(element); - } - } - return array; - } else { - throw new Exception("result should be of type Object[][]. Maybe internal logic has error."); - } - } - - private void printElementType(Object element) { - if (element instanceof String) { - System.out.println("String: " + element); - } else if (element instanceof Integer) { - System.out.println("Integer: " + element); - } else if (element instanceof Double) { - System.out.println("Double: " + element); - } else if (element instanceof Boolean) { - System.out.println("Boolean: " + element); - } else if (element instanceof Long) { - System.out.println("Long: " + element); - } else if (element instanceof byte[]) { - System.out.print("byte[]: "); - for (byte b : (byte[]) element) { - System.out.print(b + " "); - } - System.out.println(); - } else { - System.out.println("Unknown type: " + element); - } - } -} diff --git a/bindings/java/src/main/java/org/github/tursodatabase/limbo/Limbo.java b/bindings/java/src/main/java/org/github/tursodatabase/limbo/Limbo.java deleted file mode 100644 index 941ec4656..000000000 --- a/bindings/java/src/main/java/org/github/tursodatabase/limbo/Limbo.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.github.tursodatabase.limbo; - -import org.github.tursodatabase.exceptions.ErrorCode; - -import java.lang.Exception; - -/** - * TODO: Deprecate classes under limbo package. We leave this source code for reference. - */ -public class Limbo { - - private static volatile boolean initialized; - - private Limbo() { - if (!initialized) { - System.loadLibrary("_limbo_java"); - initialized = true; - } - } - - public static Limbo create() { - return new Limbo(); - } - - public Connection getConnection(String path) throws Exception { - long connectionId = connect(path); - if (connectionId == ErrorCode.CONNECTION_FAILURE) { - throw new Exception("Failed to initialize connection"); - } - return new Connection(connectionId); - } - - private static native long connect(String path); -} diff --git a/bindings/java/src/test/java/org/github/tursodatabase/TestUtils.java b/bindings/java/src/test/java/org/github/tursodatabase/TestUtils.java new file mode 100644 index 000000000..0d7e64488 --- /dev/null +++ b/bindings/java/src/test/java/org/github/tursodatabase/TestUtils.java @@ -0,0 +1,13 @@ +package org.github.tursodatabase; + +import java.io.IOException; +import java.nio.file.Files; + +public class TestUtils { + /** + * Create temporary file and returns the path. + */ + public static String createTempFile() throws IOException { + return Files.createTempFile("limbo_test_db", null).toAbsolutePath().toString(); + } +} diff --git a/bindings/java/src/test/java/org/github/tursodatabase/core/LimboDBTest.java b/bindings/java/src/test/java/org/github/tursodatabase/core/LimboDBTest.java new file mode 100644 index 000000000..feeeff060 --- /dev/null +++ b/bindings/java/src/test/java/org/github/tursodatabase/core/LimboDBTest.java @@ -0,0 +1,48 @@ +package org.github.tursodatabase.core; + +import org.github.tursodatabase.LimboErrorCode; +import org.github.tursodatabase.TestUtils; +import org.github.tursodatabase.exceptions.LimboException; +import org.junit.jupiter.api.Test; + +import java.sql.SQLException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class LimboDBTest { + + @Test + void db_should_open_normally() throws Exception { + String dbPath = TestUtils.createTempFile(); + LimboDB db = LimboDB.create("jdbc:sqlite" + dbPath, dbPath); + db.load(); + db.open(0); + } + + @Test + void should_throw_exception_when_opened_twice() throws Exception { + String dbPath = TestUtils.createTempFile(); + LimboDB db = LimboDB.create("jdbc:sqlite:" + dbPath, dbPath); + db.load(); + db.open(0); + + assertThatThrownBy(() -> db.open(0)).isInstanceOf(SQLException.class); + } + + @Test + void throwJavaException_should_throw_appropriate_java_exception() throws Exception { + String dbPath = TestUtils.createTempFile(); + LimboDB db = LimboDB.create("jdbc:sqlite:" + dbPath, dbPath); + db.load(); + + final int limboExceptionCode = LimboErrorCode.ETC.code; + try { + db.throwJavaException(limboExceptionCode); + } catch (Exception e) { + assertThat(e).isInstanceOf(LimboException.class); + LimboException limboException = (LimboException) e; + assertThat(limboException.getResultCode().code).isEqualTo(limboExceptionCode); + } + } +} diff --git a/bindings/wasm/.gitignore b/bindings/wasm/.gitignore new file mode 100644 index 000000000..0f8678b46 --- /dev/null +++ b/bindings/wasm/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +*.wasm diff --git a/bindings/wasm/Cargo.toml b/bindings/wasm/Cargo.toml index 1a45707e0..7fcf66f27 100644 --- a/bindings/wasm/Cargo.toml +++ b/bindings/wasm/Cargo.toml @@ -15,3 +15,10 @@ console_error_panic_hook = "0.1.7" js-sys = "0.3.72" limbo_core = { path = "../../core", default-features = false } wasm-bindgen = "0.2" +wasm-bindgen-futures = "0.4" +web-sys = "0.3" + +[features] +web = [] +nodejs = [] +default = ["nodejs"] diff --git a/bindings/wasm/README.md b/bindings/wasm/README.md index 7703ab4a8..34652e801 100644 --- a/bindings/wasm/README.md +++ b/bindings/wasm/README.md @@ -4,6 +4,39 @@ This source tree contains Limbo Wasm bindings. ## Building +For nodejs ``` ./scripts/build ``` +For web + +``` +./scripts/build web +``` + +# Browser Support + +Adding experimental support for limbo in the browser. This is done by adding support for OPFS as a VFS. + +To see a basic example of this `npm run dev` and navigate to `http://localhost:5173/limbo-opfs-test.html` and open the console. + +## Design + +This design mirrors sqlite's approach for OPFS support. It has a sync api in `opfs.js` which communicates with `opfs-sync-proxy.js` via `SharedArrayBuffer` and `Atomics.wait`. This allows us to live the VFS api in `lib.rs` unchanged. + +You can see `limbo-opfs-test.html` for basic usage. + +## UTs + +There are OPFS specific unit tests and then some basic limbo unit tests. These are run via `npm test` or `npx vitest`. + +For more info and log output you can run `npx vitest:ui` but you can get some parallel execution of test cases which cause issues. + + +## TODO + +-[] Add a wrapper js that provides a clean interface to the `limbo-worker.js` +-[] Add more tests for opfs.js operations +-[] Add error return handling +-[] Make sure posix flags for open are handled instead of just being ignored (this requires creating a mapping of behaviors from posix to opfs as far as makes sense) + diff --git a/bindings/wasm/html/index.html b/bindings/wasm/html/index.html new file mode 100644 index 000000000..0097d61f8 --- /dev/null +++ b/bindings/wasm/html/index.html @@ -0,0 +1,9 @@ + + + + + + diff --git a/bindings/wasm/html/limbo-opfs-test.html b/bindings/wasm/html/limbo-opfs-test.html new file mode 100644 index 000000000..ec82f9dbf --- /dev/null +++ b/bindings/wasm/html/limbo-opfs-test.html @@ -0,0 +1,83 @@ + + + + Limbo Test + + + + + + diff --git a/bindings/wasm/html/limbo-test.html b/bindings/wasm/html/limbo-test.html new file mode 100644 index 000000000..8651f7f77 --- /dev/null +++ b/bindings/wasm/html/limbo-test.html @@ -0,0 +1,11 @@ + + + + Limbo Test + + + + + diff --git a/bindings/wasm/lib.rs b/bindings/wasm/lib.rs index f24efa234..1a1569a09 100644 --- a/bindings/wasm/lib.rs +++ b/bindings/wasm/lib.rs @@ -47,7 +47,9 @@ impl Database { } #[wasm_bindgen] - pub fn exec(&self, _sql: &str) {} + pub fn exec(&self, _sql: &str) { + let _res = self.conn.execute(_sql).unwrap(); + } #[wasm_bindgen] pub fn prepare(&self, _sql: &str) -> Statement { @@ -352,10 +354,39 @@ impl limbo_core::DatabaseStorage for DatabaseStorage { } } +#[cfg(all(feature = "web", feature = "nodejs"))] +compile_error!("Features 'web' and 'nodejs' cannot be enabled at the same time"); + +#[cfg(feature = "web")] +#[wasm_bindgen(module = "/src/web-vfs.js")] +extern "C" { + type VFS; + #[wasm_bindgen(constructor)] + fn new() -> VFS; + + #[wasm_bindgen(method)] + fn open(this: &VFS, path: &str, flags: &str) -> i32; + + #[wasm_bindgen(method)] + fn close(this: &VFS, fd: i32) -> bool; + + #[wasm_bindgen(method)] + fn pwrite(this: &VFS, fd: i32, buffer: &[u8], offset: usize) -> i32; + + #[wasm_bindgen(method)] + fn pread(this: &VFS, fd: i32, buffer: &mut [u8], offset: usize) -> i32; + + #[wasm_bindgen(method)] + fn size(this: &VFS, fd: i32) -> u64; + + #[wasm_bindgen(method)] + fn sync(this: &VFS, fd: i32); +} + +#[cfg(feature = "nodejs")] #[wasm_bindgen(module = "/vfs.js")] extern "C" { type VFS; - #[wasm_bindgen(constructor)] fn new() -> VFS; diff --git a/bindings/wasm/package-lock.json b/bindings/wasm/package-lock.json new file mode 100644 index 000000000..5028485f2 --- /dev/null +++ b/bindings/wasm/package-lock.json @@ -0,0 +1,1457 @@ +{ + "name": "limbo-wasm", + "version": "0.0.11", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "limbo-wasm", + "version": "0.0.11", + "license": "MIT", + "devDependencies": { + "@playwright/test": "^1.49.1", + "@vitest/ui": "^2.1.8", + "happy-dom": "^16.3.0", + "playwright": "^1.49.1", + "vite": "^6.0.7", + "vite-plugin-wasm": "^3.4.1", + "vitest": "^2.1.8", + "wasm-pack": "^0.13.1" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@playwright/test": { + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz", + "integrity": "sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.49.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.28", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", + "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.30.0.tgz", + "integrity": "sha512-617pd92LhdA9+wpixnzsyhVft3szYiN16aNUMzVkf2N+yAk8UXY226Bfp36LvxYTUt7MO/ycqGFjQgJ0wlMaWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.8.tgz", + "integrity": "sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.8", + "@vitest/utils": "2.1.8", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.8.tgz", + "integrity": "sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.8.tgz", + "integrity": "sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.8", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.8.tgz", + "integrity": "sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.8", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.8.tgz", + "integrity": "sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/ui": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-2.1.8.tgz", + "integrity": "sha512-5zPJ1fs0ixSVSs5+5V2XJjXLmNzjugHRyV11RqxYVR+oMcogZ9qTuSfKW+OcTV0JeFNznI83BNylzH6SSNJ1+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.8", + "fflate": "^0.8.2", + "flatted": "^3.3.1", + "pathe": "^1.1.2", + "sirv": "^3.0.0", + "tinyglobby": "^0.2.10", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "2.1.8" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.8.tgz", + "integrity": "sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.8", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/axios": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", + "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.14.8" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-install": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/binary-install/-/binary-install-1.1.0.tgz", + "integrity": "sha512-rkwNGW+3aQVSZoD0/o3mfPN6Yxh3Id0R/xzTVBVVpGNlVz8EGwusksxRlbk/A5iKTZt9zkMn3qIqmAt3vpfbzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "axios": "^0.26.1", + "rimraf": "^3.0.2", + "tar": "^6.1.11" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz", + "integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-module-lexer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz", + "integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", + "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, + "node_modules/flatted": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", + "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/happy-dom": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-16.3.0.tgz", + "integrity": "sha512-Q71RaIhyS21vhW17Tpa5W36yqQXIlE1TZ0A0Gguts8PShUSQE/7fBgxYGxgm3+5y0gF6afdlAVHLQqgrIcfRzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "webidl-conversions": "^7.0.0", + "whatwg-mimetype": "^3.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/loupe": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz", + "integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz", + "integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.49.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz", + "integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/postcss": { + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.30.0.tgz", + "integrity": "sha512-sDnr1pcjTgUT69qBksNF1N1anwfbyYG6TBQ22b03bII8EdiUQ7J0TlozVaTMjT/eEJAO49e1ndV7t+UZfL1+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.30.0", + "@rollup/rollup-android-arm64": "4.30.0", + "@rollup/rollup-darwin-arm64": "4.30.0", + "@rollup/rollup-darwin-x64": "4.30.0", + "@rollup/rollup-freebsd-arm64": "4.30.0", + "@rollup/rollup-freebsd-x64": "4.30.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.30.0", + "@rollup/rollup-linux-arm-musleabihf": "4.30.0", + "@rollup/rollup-linux-arm64-gnu": "4.30.0", + "@rollup/rollup-linux-arm64-musl": "4.30.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.30.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.30.0", + "@rollup/rollup-linux-riscv64-gnu": "4.30.0", + "@rollup/rollup-linux-s390x-gnu": "4.30.0", + "@rollup/rollup-linux-x64-gnu": "4.30.0", + "@rollup/rollup-linux-x64-musl": "4.30.0", + "@rollup/rollup-win32-arm64-msvc": "4.30.0", + "@rollup/rollup-win32-ia32-msvc": "4.30.0", + "@rollup/rollup-win32-x64-msvc": "4.30.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/sirv": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.0.tgz", + "integrity": "sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", + "integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.10.tgz", + "integrity": "sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/tinypool": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", + "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/vite": { + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.7.tgz", + "integrity": "sha512-RDt8r/7qx9940f8FcOIAH9PTViRrghKaK2K1jY3RaAURrEUbm9Du1mJ72G+jlhtG3WwodnfzY8ORQZbBavZEAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.24.2", + "postcss": "^8.4.49", + "rollup": "^4.23.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.8.tgz", + "integrity": "sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vite-node/node_modules/vite": { + "version": "5.4.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", + "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-plugin-wasm": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/vite-plugin-wasm/-/vite-plugin-wasm-3.4.1.tgz", + "integrity": "sha512-ja3nSo2UCkVeitltJGkS3pfQHAanHv/DqGatdI39ja6McgABlpsZ5hVgl6wuR8Qx5etY3T5qgDQhOWzc5RReZA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "vite": "^2 || ^3 || ^4 || ^5 || ^6" + } + }, + "node_modules/vitest": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.8.tgz", + "integrity": "sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.8", + "@vitest/mocker": "2.1.8", + "@vitest/pretty-format": "^2.1.8", + "@vitest/runner": "2.1.8", + "@vitest/snapshot": "2.1.8", + "@vitest/spy": "2.1.8", + "@vitest/utils": "2.1.8", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.8", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.8", + "@vitest/ui": "2.1.8", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.8.tgz", + "integrity": "sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.8", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "5.4.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", + "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/wasm-pack": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/wasm-pack/-/wasm-pack-0.13.1.tgz", + "integrity": "sha512-P9exD4YkjpDbw68xUhF3MDm/CC/3eTmmthyG5bHJ56kalxOTewOunxTke4SyF8MTXV6jUtNjXggPgrGmMtczGg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "binary-install": "^1.0.1" + }, + "bin": { + "wasm-pack": "run.js" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/bindings/wasm/package.json b/bindings/wasm/package.json index 2a74b138e..7fcc1e9e4 100644 --- a/bindings/wasm/package.json +++ b/bindings/wasm/package.json @@ -16,5 +16,21 @@ "limbo_wasm.d.ts" ], "main": "limbo_wasm.js", - "types": "limbo_wasm.d.ts" + "types": "limbo_wasm.d.ts", + "type": "module", + "scripts": { + "dev": "vite", + "test": "vitest --sequence.shuffle=false", + "test:ui": "vitest --ui" + }, + "devDependencies": { + "@playwright/test": "^1.49.1", + "@vitest/ui": "^2.1.8", + "happy-dom": "^16.3.0", + "playwright": "^1.49.1", + "vite": "^6.0.7", + "vite-plugin-wasm": "^3.4.1", + "vitest": "^2.1.8", + "wasm-pack": "^0.13.1" + } } diff --git a/bindings/wasm/playwright.config.js b/bindings/wasm/playwright.config.js new file mode 100644 index 000000000..39a997296 --- /dev/null +++ b/bindings/wasm/playwright.config.js @@ -0,0 +1,13 @@ +// Using Playwright (recommended) +import { expect, test } from "@playwright/test"; + +// playwright.config.js +export default { + use: { + headless: true, + // Required for SharedArrayBuffer + launchOptions: { + args: ["--cross-origin-isolated"], + }, + }, +}; diff --git a/bindings/wasm/scripts/build b/bindings/wasm/scripts/build index 23f2b8b93..4cd7a48d2 100755 --- a/bindings/wasm/scripts/build +++ b/bindings/wasm/scripts/build @@ -1,4 +1,12 @@ #!/bin/bash -wasm-pack build --no-pack --target nodejs +# get target as argument from cli, defaults to nodejs if no argument is supplied +TARGET=${1:-nodejs} +FEATURE="nodejs" + +if [ "$TARGET" = "web" ]; then + FEATURE="web" +fi + +npx wasm-pack build --no-pack --target $TARGET --no-default-features --features $FEATURE cp package.json pkg/package.json diff --git a/bindings/wasm/src/limbo-worker.js b/bindings/wasm/src/limbo-worker.js new file mode 100644 index 000000000..d8598303f --- /dev/null +++ b/bindings/wasm/src/limbo-worker.js @@ -0,0 +1,74 @@ +import { VFS } from "./opfs.js"; +import init, { Database } from "./../pkg/limbo_wasm.js"; + +let db = null; +let currentStmt = null; + +async function initVFS() { + const vfs = new VFS(); + await vfs.ready; + self.vfs = vfs; + return vfs; +} + +async function initAll() { + await initVFS(); + await init(); +} + +initAll().then(() => { + self.postMessage({ type: "ready" }); + + self.onmessage = (e) => { + try { + switch (e.data.op) { + case "createDb": { + db = new Database(e.data.path); + self.postMessage({ type: "success", op: "createDb" }); + break; + } + case "exec": { + log(e.data.sql); + db.exec(e.data.sql); + self.postMessage({ type: "success", op: "exec" }); + break; + } + case "prepare": { + currentStmt = db.prepare(e.data.sql); + const results = currentStmt.raw().all(); + self.postMessage({ type: "result", result: results }); + break; + } + case "get": { + const row = currentStmt?.raw().get(); + self.postMessage({ type: "result", result: row }); + break; + } + } + } catch (err) { + self.postMessage({ type: "error", error: err.toString() }); + } + }; +}).catch((error) => { + self.postMessage({ type: "error", error: error.toString() }); +}); + +// logLevel: +// +// 0 = no logging output +// 1 = only errors +// 2 = warnings and errors +// 3 = debug, warnings, and errors +const logLevel = 1; + +const loggers = { + 0: console.error.bind(console), + 1: console.warn.bind(console), + 2: console.log.bind(console), +}; +const logImpl = (level, ...args) => { + if (logLevel > level) loggers[level]("OPFS asyncer:", ...args); +}; +const log = (...args) => logImpl(2, ...args); +const warn = (...args) => logImpl(1, ...args); +const error = (...args) => logImpl(0, ...args); diff --git a/bindings/wasm/src/opfs-interface.js b/bindings/wasm/src/opfs-interface.js new file mode 100644 index 000000000..f237040a4 --- /dev/null +++ b/bindings/wasm/src/opfs-interface.js @@ -0,0 +1,67 @@ +export class VFSInterface { + constructor(workerUrl) { + this.worker = new Worker(workerUrl, { type: "module" }); + this.nextMessageId = 1; + this.pendingRequests = new Map(); + + this.worker.onmessage = (event) => { + console.log("interface onmessage: ", event.data); + let { id, result, error } = event.data; + const resolver = this.pendingRequests.get(id); + if (event.data?.buffer && event.data?.size) { + result = { size: event.data.size, buffer: event.data.buffer }; + } + + if (resolver) { + this.pendingRequests.delete(id); + if (error) { + resolver.reject(new Error(error)); + } else { + resolver.resolve(result); + } + } + }; + } + + _sendMessage(method, args) { + const id = this.nextMessageId++; + return new Promise((resolve, reject) => { + this.pendingRequests.set(id, { resolve, reject }); + this.worker.postMessage({ id, method, args }); + }); + } + + async open(path, flags) { + return await this._sendMessage("open", { path, flags }); + } + + async close(fd) { + return await this._sendMessage("close", { fd }); + } + + async pwrite(fd, buffer, offset) { + return await this._sendMessage("pwrite", { fd, buffer, offset }, [ + buffer.buffer, + ]); + } + + async pread(fd, buffer, offset) { + console.log("interface in buffer: ", [...buffer]); + const result = await this._sendMessage("pread", { + fd, + buffer: buffer, + offset, + }, []); + console.log("interface out buffer: ", [...buffer]); + buffer.set(new Uint8Array(result.buffer)); + return buffer.length; + } + + async size(fd) { + return await this._sendMessage("size", { fd }); + } + + async sync(fd) { + return await this._sendMessage("sync", { fd }); + } +} diff --git a/bindings/wasm/src/opfs-sync-proxy.js b/bindings/wasm/src/opfs-sync-proxy.js new file mode 100644 index 000000000..6bac0a759 --- /dev/null +++ b/bindings/wasm/src/opfs-sync-proxy.js @@ -0,0 +1,136 @@ +// opfs-sync-proxy.js +let transferBuffer, statusBuffer, statusArray, statusView; +let transferArray; +let rootDir = null; +const handles = new Map(); +let nextFd = 1; + +self.postMessage("ready"); + +onmessage = async (e) => { + log("handle message: ", e.data); + if (e.data.cmd === "init") { + log("init"); + transferBuffer = e.data.transferBuffer; + statusBuffer = e.data.statusBuffer; + + transferArray = new Uint8Array(transferBuffer); + statusArray = new Int32Array(statusBuffer); + statusView = new DataView(statusBuffer); + + self.postMessage("done"); + return; + } + + const result = await handleCommand(e.data); + sendResult(result); +}; + +self.onerror = (error) => { + console.error("opfssync error: ", error); + // Don't close, keep running + return true; // Prevents default error handling +}; + +function handleCommand(msg) { + log(`handle message: ${msg.cmd}`); + switch (msg.cmd) { + case "open": + return handleOpen(msg.path); + case "close": + return handleClose(msg.fd); + case "read": + return handleRead(msg.fd, msg.offset, msg.size); + case "write": + return handleWrite(msg.fd, msg.buffer, msg.offset); + case "size": + return handleSize(msg.fd); + case "sync": + return handleSync(msg.fd); + } +} + +async function handleOpen(path) { + if (!rootDir) { + rootDir = await navigator.storage.getDirectory(); + } + const fd = nextFd++; + + const handle = await rootDir.getFileHandle(path, { create: true }); + const syncHandle = await handle.createSyncAccessHandle(); + + handles.set(fd, syncHandle); + return { fd }; +} + +function handleClose(fd) { + const handle = handles.get(fd); + handle.close(); + handles.delete(fd); + return { success: true }; +} + +function handleRead(fd, offset, size) { + const handle = handles.get(fd); + const readBuffer = new ArrayBuffer(size); + const readSize = handle.read(readBuffer, { at: offset }); + log("opfssync read: size: ", readBuffer.byteLength); + + const tmp = new Uint8Array(readBuffer); + log("opfssync read buffer: ", [...tmp]); + + transferArray.set(tmp); + + return { success: true, length: readSize }; +} + +function handleWrite(fd, buffer, offset) { + log("opfssync buffer size:", buffer.byteLength); + log("opfssync write buffer: ", [...buffer]); + const handle = handles.get(fd); + const size = handle.write(buffer, { at: offset }); + return { success: true, length: size }; +} + +function handleSize(fd) { + const handle = handles.get(fd); + return { success: true, length: handle.getSize() }; +} + +function handleSync(fd) { + const handle = handles.get(fd); + handle.flush(); + return { success: true }; +} + +function sendResult(result) { + if (result?.fd) { + statusView.setInt32(4, result.fd, true); + } else { + log("opfs-sync-proxy: result.length: ", result.length); + statusView.setInt32(4, result?.length || 0, true); + } + + Atomics.store(statusArray, 0, 1); + Atomics.notify(statusArray, 0); +} + +// logLevel: +// +// 0 = no logging output +// 1 = only errors +// 2 = warnings and errors +// 3 = debug, warnings, and errors +const logLevel = 1; + +const loggers = { + 0: console.error.bind(console), + 1: console.warn.bind(console), + 2: console.log.bind(console), +}; +const logImpl = (level, ...args) => { + if (logLevel > level) loggers[level]("OPFS asyncer:", ...args); +}; +const log = (...args) => logImpl(2, ...args); +const warn = (...args) => logImpl(1, ...args); +const error = (...args) => logImpl(0, ...args); diff --git a/bindings/wasm/src/opfs-worker.js b/bindings/wasm/src/opfs-worker.js new file mode 100644 index 000000000..b9a1c0ee3 --- /dev/null +++ b/bindings/wasm/src/opfs-worker.js @@ -0,0 +1,57 @@ +import { VFS } from "./opfs.js"; + +const vfs = new VFS(); + +onmessage = async function (e) { + if (!vfs.isReady) { + console.log("opfs ready: ", vfs.isReady); + await vfs.ready; + console.log("opfs ready: ", vfs.isReady); + } + + const { id, method, args } = e.data; + + console.log(`interface onmessage method: ${method}`); + try { + let result; + switch (method) { + case "open": + result = vfs.open(args.path, args.flags); + break; + case "close": + result = vfs.close(args.fd); + break; + case "pread": { + const buffer = new Uint8Array(args.buffer); + result = vfs.pread(args.fd, buffer, args.offset); + self.postMessage( + { id, size: result, error: null, buffer }, + ); + console.log("read size: ", result); + console.log("read buffer: ", [...buffer]); + return; + } + case "pwrite": { + result = vfs.pwrite(args.fd, args.buffer, args.offset); + console.log("write size: ", result); + break; + } + case "size": + result = vfs.size(args.fd); + break; + case "sync": + result = vfs.sync(args.fd); + break; + default: + throw new Error(`Unknown method: ${method}`); + } + + self.postMessage( + { id, result, error: null }, + ); + } catch (error) { + self.postMessage({ id, result: null, error: error.message }); + } +}; + +console.log("opfs-worker.js"); diff --git a/bindings/wasm/src/opfs.js b/bindings/wasm/src/opfs.js new file mode 100644 index 000000000..01c6d2175 --- /dev/null +++ b/bindings/wasm/src/opfs.js @@ -0,0 +1,154 @@ +// First file: VFS class +class VFS { + constructor() { + this.transferBuffer = new SharedArrayBuffer(1024 * 1024); // 1mb + this.statusBuffer = new SharedArrayBuffer(8); // Room for status + size + + this.statusArray = new Int32Array(this.statusBuffer); + this.statusView = new DataView(this.statusBuffer); + + this.worker = new Worker( + new URL("./opfs-sync-proxy.js", import.meta.url), + { type: "module" }, + ); + + this.isReady = false; + this.ready = new Promise((resolve, reject) => { + this.worker.addEventListener("message", async (e) => { + if (e.data === "ready") { + await this.initWorker(); + this.isReady = true; + resolve(); + } + }, { once: true }); + this.worker.addEventListener("error", reject, { once: true }); + }); + + this.worker.onerror = (e) => { + console.error("Sync proxy worker error:", e.message); + }; + } + + initWorker() { + return new Promise((resolve) => { + this.worker.addEventListener("message", (e) => { + log("eventListener: ", e.data); + resolve(); + }, { once: true }); + + this.worker.postMessage({ + cmd: "init", + transferBuffer: this.transferBuffer, + statusBuffer: this.statusBuffer, + }); + }); + } + + open(path) { + Atomics.store(this.statusArray, 0, 0); + this.worker.postMessage({ cmd: "open", path }); + Atomics.wait(this.statusArray, 0, 0); + + const result = this.statusView.getInt32(4, true); + log("opfs.js open result: ", result); + log("opfs.js open result type: ", typeof result); + + return result; + } + + close(fd) { + Atomics.store(this.statusArray, 0, 0); + this.worker.postMessage({ cmd: "close", fd }); + Atomics.wait(this.statusArray, 0, 0); + return true; + } + + pread(fd, buffer, offset) { + let bytesRead = 0; + + while (bytesRead < buffer.byteLength) { + const chunkSize = Math.min( + this.transferBuffer.byteLength, + buffer.byteLength - bytesRead, + ); + + Atomics.store(this.statusArray, 0, 0); + this.worker.postMessage({ + cmd: "read", + fd, + offset: offset + bytesRead, + size: chunkSize, + }); + + Atomics.wait(this.statusArray, 0, 0); + const readSize = this.statusView.getInt32(4, true); + buffer.set( + new Uint8Array(this.transferBuffer, 0, readSize), + bytesRead, + ); + log("opfs pread buffer: ", [...buffer]); + + bytesRead += readSize; + if (readSize < chunkSize) break; + } + + return bytesRead; + } + + pwrite(fd, buffer, offset) { + log("write buffer size: ", buffer.byteLength); + Atomics.store(this.statusArray, 0, 0); + this.worker.postMessage({ + cmd: "write", + fd, + buffer: buffer, + offset: offset, + }); + + Atomics.wait(this.statusArray, 0, 0); + log( + "opfs pwrite length statusview: ", + this.statusView.getInt32(4, true), + ); + return this.statusView.getInt32(4, true); + } + + size(fd) { + Atomics.store(this.statusArray, 0, 0); + this.worker.postMessage({ cmd: "size", fd }); + Atomics.wait(this.statusArray, 0, 0); + + const result = this.statusView.getInt32(4, true); + log("opfs.js size result: ", result); + log("opfs.js size result type: ", typeof result); + return BigInt(result); + } + + sync(fd) { + Atomics.store(this.statusArray, 0, 0); + this.worker.postMessage({ cmd: "sync", fd }); + Atomics.wait(this.statusArray, 0, 0); + } +} + +// logLevel: +// +// 0 = no logging output +// 1 = only errors +// 2 = warnings and errors +// 3 = debug, warnings, and errors +const logLevel = 1; + +const loggers = { + 0: console.error.bind(console), + 1: console.warn.bind(console), + 2: console.log.bind(console), +}; +const logImpl = (level, ...args) => { + if (logLevel > level) loggers[level]("OPFS asyncer:", ...args); +}; +const log = (...args) => logImpl(2, ...args); +const warn = (...args) => logImpl(1, ...args); +const error = (...args) => logImpl(0, ...args); + +export { VFS }; diff --git a/bindings/wasm/src/web-vfs.js b/bindings/wasm/src/web-vfs.js new file mode 100644 index 000000000..a24bc75d5 --- /dev/null +++ b/bindings/wasm/src/web-vfs.js @@ -0,0 +1,29 @@ +export class VFS { + constructor() { + return self.vfs; + } + + open(path, flags) { + return self.vfs.open(path); + } + + close(fd) { + return self.vfs.close(fd); + } + + pread(fd, buffer, offset) { + return self.vfs.pread(fd, buffer, offset); + } + + pwrite(fd, buffer, offset) { + return self.vfs.pwrite(fd, buffer, offset); + } + + size(fd) { + return self.vfs.size(fd); + } + + sync(fd) { + return self.vfs.sync(fd); + } +} diff --git a/bindings/wasm/test/helpers.js b/bindings/wasm/test/helpers.js new file mode 100644 index 000000000..cc9de4ca2 --- /dev/null +++ b/bindings/wasm/test/helpers.js @@ -0,0 +1,23 @@ +import { createServer } from "vite"; +import { chromium } from "playwright"; + +export async function setupTestEnvironment(port) { + const server = await createServer({ + configFile: "./vite.config.js", + root: ".", + server: { port }, + }); + await server.listen(); + const browser = await chromium.launch(); + const context = await browser.newContext(); + const page = await context.newPage(); + globalThis.__page__ = page; + + return { server, browser, context, page }; +} + +export async function teardownTestEnvironment({ server, browser, context }) { + await context.close(); + await browser.close(); + await server.close(); +} diff --git a/bindings/wasm/test/limbo.test.js b/bindings/wasm/test/limbo.test.js new file mode 100644 index 000000000..f7f34bee8 --- /dev/null +++ b/bindings/wasm/test/limbo.test.js @@ -0,0 +1,72 @@ +import { afterAll, beforeAll, beforeEach, expect, test } from "vitest"; +import { setupTestEnvironment, teardownTestEnvironment } from "./helpers.js"; + +let testEnv; + +beforeAll(async () => { + testEnv = await setupTestEnvironment(5174); +}); + +beforeEach(async () => { + const { page } = testEnv; + await page.goto("http://localhost:5174/limbo-test.html"); +}); + +afterAll(async () => { + await teardownTestEnvironment(testEnv); +}); + +test("basic database operations", async () => { + const { page } = testEnv; + const result = await page.evaluate(async () => { + const worker = new Worker("./src/limbo-worker.js", { type: "module" }); + + const waitForMessage = (type, op) => + new Promise((resolve, reject) => { + const handler = (e) => { + if (e.data.type === type && (!op || e.data.op === op)) { + worker.removeEventListener("message", handler); + resolve(e.data); + } else if (e.data.type === "error") { + worker.removeEventListener("message", handler); + reject(e.data.error); + } + }; + worker.addEventListener("message", handler); + }); + + try { + await waitForMessage("ready"); + worker.postMessage({ op: "createDb", path: "test.db" }); + await waitForMessage("success", "createDb"); + + worker.postMessage({ + op: "exec", + sql: + "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT);", + }); + await waitForMessage("success", "exec"); + + worker.postMessage({ + op: "exec", + sql: "INSERT INTO users VALUES (1, 'Alice', 'alice@example.org');", + }); + await waitForMessage("success", "exec"); + + worker.postMessage({ + op: "prepare", + sql: "SELECT * FROM users;", + }); + + const results = await waitForMessage("result"); + return results; + } catch (error) { + return { error: error.message }; + } + }); + + if (result.error) throw new Error(`Test failed: ${result.error}`); + expect(result.result).toHaveLength(1); + expect(result.result[0]).toEqual([1, "Alice", "alice@example.org"]); +}); + diff --git a/bindings/wasm/test/opfs.test.js b/bindings/wasm/test/opfs.test.js new file mode 100644 index 000000000..ea96add44 --- /dev/null +++ b/bindings/wasm/test/opfs.test.js @@ -0,0 +1,140 @@ +// test/opfs.test.js +import { afterAll, beforeAll, beforeEach, expect, test } from "vitest"; +import { setupTestEnvironment, teardownTestEnvironment } from "./helpers"; + +let testEnv; + +beforeAll(async () => { + testEnv = await setupTestEnvironment(5173); +}); + +beforeEach(async () => { + const { page } = testEnv; + await page.goto("http://localhost:5173/index.html"); + await page.waitForFunction(() => window.VFSInterface !== undefined); +}); + +afterAll(async () => { + await teardownTestEnvironment(testEnv); +}); + +test("basic read/write functionality", async () => { + const { page } = testEnv; + const result = await page.evaluate(async () => { + const vfs = new window.VFSInterface("/src/opfs-worker.js"); + let fd; + try { + fd = await vfs.open("test.txt", {}); + const writeData = new Uint8Array([1, 2, 3, 4]); + const bytesWritten = await vfs.pwrite(fd, writeData, 0); + const readData = new Uint8Array(4); + const bytesRead = await vfs.pread(fd, readData, 0); + await vfs.close(fd); + return { fd, bytesWritten, bytesRead, readData: Array.from(readData) }; + } catch (error) { + if (fd !== undefined) await vfs.close(fd); + return { error: error.message }; + } + }); + + if (result.error) throw new Error(`Test failed: ${result.error}`); + expect(result.fd).toBe(1); + expect(result.bytesWritten).toBe(4); + expect(result.bytesRead).toBe(4); + expect(result.readData).toEqual([1, 2, 3, 4]); +}); + +test("larger data read/write", async () => { + const { page } = testEnv; + const result = await page.evaluate(async () => { + const vfs = new window.VFSInterface("/src/opfs-worker.js"); + let fd; + try { + fd = await vfs.open("large.txt", {}); + const writeData = new Uint8Array(1024).map((_, i) => i % 256); + const bytesWritten = await vfs.pwrite(fd, writeData, 0); + const readData = new Uint8Array(1024); + const bytesRead = await vfs.pread(fd, readData, 0); + await vfs.close(fd); + return { bytesWritten, bytesRead, readData: Array.from(readData) }; + } catch (error) { + if (fd !== undefined) await vfs.close(fd); + return { error: error.message }; + } + }); + + if (result.error) throw new Error(`Test failed: ${result.error}`); + expect(result.bytesWritten).toBe(1024); + expect(result.bytesRead).toBe(1024); + expect(result.readData).toEqual( + Array.from({ length: 1024 }, (_, i) => i % 256), + ); +}); + +test("partial reads and writes", async () => { + const { page } = testEnv; + const result = await page.evaluate(async () => { + const vfs = new window.VFSInterface("/src/opfs-worker.js"); + let fd; + try { + fd = await vfs.open("partial.txt", {}); + + const writeData1 = new Uint8Array([1, 2, 3, 4]); + const writeData2 = new Uint8Array([5, 6, 7, 8]); + await vfs.pwrite(fd, writeData1, 0); + await vfs.pwrite(fd, writeData2, 4); + + const readData1 = new Uint8Array(2); + const readData2 = new Uint8Array(4); + const readData3 = new Uint8Array(2); + + await vfs.pread(fd, readData1, 0); + await vfs.pread(fd, readData2, 2); + await vfs.pread(fd, readData3, 6); + + await vfs.close(fd); + return { + readData1: Array.from(readData1), + readData2: Array.from(readData2), + readData3: Array.from(readData3), + }; + } catch (error) { + if (fd !== undefined) await vfs.close(fd); + return { error: error.message }; + } + }); + + if (result.error) throw new Error(`Test failed: ${result.error}`); + expect(result.readData1).toEqual([1, 2]); + expect(result.readData2).toEqual([3, 4, 5, 6]); + expect(result.readData3).toEqual([7, 8]); +}); + +test("file size operations", async () => { + const { page } = testEnv; + const result = await page.evaluate(async () => { + const vfs = new window.VFSInterface("/src/opfs-worker.js"); + let fd; + try { + fd = await vfs.open("size.txt", {}); + const writeData1 = new Uint8Array([1, 2, 3, 4]); + await vfs.pwrite(fd, writeData1, 0); + const size1 = await vfs.size(fd); + + const writeData2 = new Uint8Array([5, 6, 7, 8]); + await vfs.pwrite(fd, writeData2, 4); + const size2 = await vfs.size(fd); + + await vfs.close(fd); + return { size1, size2 }; + } catch (error) { + if (fd !== undefined) await vfs.close(fd); + return { error: error.message }; + } + }); + + if (result.error) throw new Error(`Test failed: ${result.error}`); + expect(Number(result.size1)).toBe(4); + expect(Number(result.size2)).toBe(8); +}); + diff --git a/bindings/wasm/test/setup.js b/bindings/wasm/test/setup.js new file mode 100644 index 000000000..e69de29bb diff --git a/bindings/wasm/vite.config.js b/bindings/wasm/vite.config.js new file mode 100644 index 000000000..d86194560 --- /dev/null +++ b/bindings/wasm/vite.config.js @@ -0,0 +1,29 @@ +import { defineConfig } from "vite"; +import wasm from "vite-plugin-wasm"; + +export default defineConfig({ + publicDir: "./html", + root: ".", + plugins: [wasm()], + test: { + globals: true, + environment: "happy-dom", + setupFiles: ["./test/setup.js"], + include: ["test/*.test.js"], + }, + server: { + headers: { + "Cross-Origin-Embedder-Policy": "require-corp", + "Cross-Origin-Opener-Policy": "same-origin", + "Cross-Origin-Resource-Policy": "cross-origin", + }, + }, + worker: { + format: "es", + rollupOptions: { + output: { + format: "es", + }, + }, + }, +}); diff --git a/core/Cargo.toml b/core/Cargo.toml index 0aab086b4..c0c579152 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -14,7 +14,7 @@ name = "limbo_core" path = "lib.rs" [features] -default = ["fs", "json", "uuid"] +default = ["fs", "json", "uuid", "io_uring"] fs = [] json = [ "dep:jsonb", @@ -22,11 +22,12 @@ json = [ "dep:pest_derive", ] uuid = ["dep:uuid"] +io_uring = ["dep:io-uring"] [target.'cfg(target_os = "linux")'.dependencies] -io-uring = "0.6.1" +io-uring = { version = "0.6.1", optional = true } -[target.'cfg(target_os = "macos")'.dependencies] +[target.'cfg(target_family = "unix")'.dependencies] polling = "3.7.2" rustix = "0.38.34" diff --git a/core/error.rs b/core/error.rs index 85473ff68..646e85825 100644 --- a/core/error.rs +++ b/core/error.rs @@ -19,12 +19,12 @@ pub enum LimboError { EnvVarError(#[from] std::env::VarError), #[error("I/O error: {0}")] IOError(#[from] std::io::Error), - #[cfg(target_os = "linux")] + #[cfg(all(target_os = "linux", feature = "io_uring"))] #[error("I/O error: {0}")] - LinuxIOError(String), + UringIOError(String), #[error("Locking error: {0}")] LockingError(String), - #[cfg(target_os = "macos")] + #[cfg(target_family = "unix")] #[error("I/O error: {0}")] RustixIOError(#[from] rustix::io::Errno), #[error("Parse error: {0}")] diff --git a/core/function.rs b/core/function.rs index 7385b237f..94d752eb5 100644 --- a/core/function.rs +++ b/core/function.rs @@ -27,6 +27,7 @@ pub enum JsonFunc { JsonArray, JsonExtract, JsonArrayLength, + JsonType, } #[cfg(feature = "json")] @@ -40,6 +41,7 @@ impl Display for JsonFunc { Self::JsonArray => "json_array".to_string(), Self::JsonExtract => "json_extract".to_string(), Self::JsonArrayLength => "json_array_length".to_string(), + Self::JsonType => "json_type".to_string(), } ) } @@ -371,6 +373,8 @@ impl Func { "json_array" => Ok(Self::Json(JsonFunc::JsonArray)), #[cfg(feature = "json")] "json_extract" => Ok(Func::Json(JsonFunc::JsonExtract)), + #[cfg(feature = "json")] + "json_type" => Ok(Func::Json(JsonFunc::JsonType)), "unixepoch" => Ok(Self::Scalar(ScalarFunc::UnixEpoch)), "julianday" => Ok(Self::Scalar(ScalarFunc::JulianDay)), "hex" => Ok(Self::Scalar(ScalarFunc::Hex)), diff --git a/core/io/linux.rs b/core/io/io_uring.rs similarity index 93% rename from core/io/linux.rs rename to core/io/io_uring.rs index 70de8ef79..54a02ee61 100644 --- a/core/io/linux.rs +++ b/core/io/io_uring.rs @@ -14,15 +14,15 @@ const MAX_IOVECS: usize = 128; const SQPOLL_IDLE: u32 = 1000; #[derive(Debug, Error)] -enum LinuxIOError { +enum UringIOError { IOUringCQError(i32), } // Implement the Display trait to customize error messages -impl fmt::Display for LinuxIOError { +impl fmt::Display for UringIOError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - LinuxIOError::IOUringCQError(code) => write!( + UringIOError::IOUringCQError(code) => write!( f, "IOUring completion queue error occurred with code {}", code @@ -31,8 +31,8 @@ impl fmt::Display for LinuxIOError { } } -pub struct LinuxIO { - inner: Rc>, +pub struct UringIO { + inner: Rc>, } struct WrappedIOUring { @@ -42,13 +42,13 @@ struct WrappedIOUring { key: u64, } -struct InnerLinuxIO { +struct InnerUringIO { ring: WrappedIOUring, iovecs: [iovec; MAX_IOVECS], next_iovec: usize, } -impl LinuxIO { +impl UringIO { pub fn new() -> Result { let ring = match io_uring::IoUring::builder() .setup_sqpoll(SQPOLL_IDLE) @@ -57,7 +57,7 @@ impl LinuxIO { Ok(ring) => ring, Err(_) => io_uring::IoUring::new(MAX_IOVECS as u32)?, }; - let inner = InnerLinuxIO { + let inner = InnerUringIO { ring: WrappedIOUring { ring, pending_ops: 0, @@ -76,7 +76,7 @@ impl LinuxIO { } } -impl InnerLinuxIO { +impl InnerUringIO { pub fn get_iovec(&mut self, buf: *const u8, len: usize) -> &iovec { let iovec = &mut self.iovecs[self.next_iovec]; iovec.iov_base = buf as *mut std::ffi::c_void; @@ -125,7 +125,7 @@ impl WrappedIOUring { } } -impl IO for LinuxIO { +impl IO for UringIO { fn open_file(&self, path: &str, flags: OpenFlags, direct: bool) -> Result> { trace!("open_file(path = {})", path); let file = std::fs::File::options() @@ -142,14 +142,14 @@ impl IO for LinuxIO { Err(error) => debug!("Error {error:?} returned when setting O_DIRECT flag to read file. The performance of the system may be affected"), }; } - let linux_file = Rc::new(LinuxFile { + let uring_file = Rc::new(UringFile { io: self.inner.clone(), file, }); if std::env::var(common::ENV_DISABLE_FILE_LOCK).is_err() { - linux_file.lock_file(true)?; + uring_file.lock_file(true)?; } - Ok(linux_file) + Ok(uring_file) } fn run_once(&self) -> Result<()> { @@ -165,9 +165,9 @@ impl IO for LinuxIO { while let Some(cqe) = ring.get_completion() { let result = cqe.result(); if result < 0 { - return Err(LimboError::LinuxIOError(format!( + return Err(LimboError::UringIOError(format!( "{} cqe: {:?}", - LinuxIOError::IOUringCQError(result), + UringIOError::IOUringCQError(result), cqe ))); } @@ -191,12 +191,12 @@ impl IO for LinuxIO { } } -pub struct LinuxFile { - io: Rc>, +pub struct UringFile { + io: Rc>, file: std::fs::File, } -impl File for LinuxFile { +impl File for UringFile { fn lock_file(&self, exclusive: bool) -> Result<()> { let fd = self.file.as_raw_fd(); let flock = flock { @@ -306,7 +306,7 @@ impl File for LinuxFile { } } -impl Drop for LinuxFile { +impl Drop for UringFile { fn drop(&mut self) { self.unlock_file().expect("Failed to unlock file"); } @@ -319,6 +319,6 @@ mod tests { #[test] fn test_multiple_processes_cannot_open_file() { - common::tests::test_multiple_processes_cannot_open_file(LinuxIO::new); + common::tests::test_multiple_processes_cannot_open_file(UringIO::new); } } diff --git a/core/io/mod.rs b/core/io/mod.rs index 3bed97b16..7e910f4af 100644 --- a/core/io/mod.rs +++ b/core/io/mod.rs @@ -164,14 +164,14 @@ impl Buffer { } cfg_block! { - #[cfg(target_os = "linux")] { - mod linux; - pub use linux::LinuxIO as PlatformIO; + #[cfg(all(target_os = "linux", feature = "io_uring"))] { + mod io_uring; + pub use io_uring::UringIO as PlatformIO; } - #[cfg(target_os = "macos")] { - mod darwin; - pub use darwin::DarwinIO as PlatformIO; + #[cfg(any(all(target_os = "linux",not(feature = "io_uring")), target_os = "macos"))] { + mod unix; + pub use unix::UnixIO as PlatformIO; } #[cfg(target_os = "windows")] { diff --git a/core/io/darwin.rs b/core/io/unix.rs similarity index 97% rename from core/io/darwin.rs rename to core/io/unix.rs index c052b572f..c86e05ee4 100644 --- a/core/io/darwin.rs +++ b/core/io/unix.rs @@ -14,13 +14,13 @@ use std::collections::HashMap; use std::io::{Read, Seek, Write}; use std::rc::Rc; -pub struct DarwinIO { +pub struct UnixIO { poller: Rc>, events: Rc>, callbacks: Rc>>, } -impl DarwinIO { +impl UnixIO { pub fn new() -> Result { Ok(Self { poller: Rc::new(RefCell::new(Poller::new()?)), @@ -30,7 +30,7 @@ impl DarwinIO { } } -impl IO for DarwinIO { +impl IO for UnixIO { fn open_file(&self, path: &str, flags: OpenFlags, _direct: bool) -> Result> { trace!("open_file(path = {})", path); let file = std::fs::File::options() @@ -40,15 +40,15 @@ impl IO for DarwinIO { .create(matches!(flags, OpenFlags::Create)) .open(path)?; - let darwin_file = Rc::new(DarwinFile { + let unix_file = Rc::new(UnixFile { file: Rc::new(RefCell::new(file)), poller: self.poller.clone(), callbacks: self.callbacks.clone(), }); if std::env::var(common::ENV_DISABLE_FILE_LOCK).is_err() { - darwin_file.lock_file(true)?; + unix_file.lock_file(true)?; } - Ok(darwin_file) + Ok(unix_file) } fn run_once(&self) -> Result<()> { @@ -127,13 +127,13 @@ enum CompletionCallback { ), } -pub struct DarwinFile { +pub struct UnixFile { file: Rc>, poller: Rc>, callbacks: Rc>>, } -impl File for DarwinFile { +impl File for UnixFile { fn lock_file(&self, exclusive: bool) -> Result<()> { let fd = self.file.borrow().as_raw_fd(); let flock = flock { @@ -279,7 +279,7 @@ impl File for DarwinFile { } } -impl Drop for DarwinFile { +impl Drop for UnixFile { fn drop(&mut self) { self.unlock_file().expect("Failed to unlock file"); } @@ -291,6 +291,6 @@ mod tests { #[test] fn test_multiple_processes_cannot_open_file() { - common::tests::test_multiple_processes_cannot_open_file(DarwinIO::new); + common::tests::test_multiple_processes_cannot_open_file(UnixIO::new); } } diff --git a/core/json/mod.rs b/core/json/mod.rs index eda97bf2b..8ee36b33a 100644 --- a/core/json/mod.rs +++ b/core/json/mod.rs @@ -121,17 +121,13 @@ pub fn json_array_length( json_value: &OwnedValue, json_path: Option<&OwnedValue>, ) -> crate::Result { - let path = match json_path { - Some(OwnedValue::Text(t)) => Some(t.value.to_string()), - Some(OwnedValue::Integer(i)) => Some(i.to_string()), - Some(OwnedValue::Float(f)) => Some(f.to_string()), - _ => None::, - }; - let json = get_json_value(json_value)?; - let arr_val = if let Some(path) = path { - &json_extract_single(&json, path.as_str())? + let arr_val = if let Some(path) = json_path { + match json_extract_single(&json, path)? { + Some(val) => val, + None => return Ok(OwnedValue::Null), + } } else { &json }; @@ -161,10 +157,13 @@ pub fn json_extract(value: &OwnedValue, paths: &[OwnedValue]) -> crate::Result { - let extracted = json_extract_single(&json, p.value.as_str())?; + OwnedValue::Null => { + return Ok(OwnedValue::Null); + } + _ => { + let extracted = json_extract_single(&json, path)?.unwrap_or_else(|| &Val::Null); - if paths.len() == 1 && extracted == Val::Null { + if paths.len() == 1 && extracted == &Val::Null { return Ok(OwnedValue::Null); } @@ -173,8 +172,6 @@ pub fn json_extract(value: &OwnedValue, paths: &[OwnedValue]) -> crate::Result return Ok(OwnedValue::Null), - _ => crate::bail_constraint_error!("JSON path error near: {:?}", path.to_string()), } } @@ -186,8 +183,49 @@ pub fn json_extract(value: &OwnedValue, paths: &[OwnedValue]) -> crate::Result crate::Result { - let json_path = json_path(path)?; +pub fn json_type(value: &OwnedValue, path: Option<&OwnedValue>) -> crate::Result { + if let OwnedValue::Null = value { + return Ok(OwnedValue::Null); + } + + let json = get_json_value(value)?; + + let json = if let Some(path) = path { + match json_extract_single(&json, path)? { + Some(val) => val, + None => return Ok(OwnedValue::Null), + } + } else { + &json + }; + + let val = match json { + Val::Null => "null", + Val::Bool(v) => { + if *v { + "true" + } else { + "false" + } + } + Val::Integer(_) => "integer", + Val::Float(_) => "real", + Val::String(_) => "text", + Val::Array(_) => "array", + Val::Object(_) => "object", + }; + + Ok(OwnedValue::Text(LimboText::json(Rc::new(val.to_string())))) +} + +/// Returns the value at the given JSON path. If the path does not exist, it returns None. +/// If the path is an invalid path, returns an error. +fn json_extract_single<'a>(json: &'a Val, path: &OwnedValue) -> crate::Result> { + let json_path = match path { + OwnedValue::Text(t) => json_path(t.value.as_str())?, + OwnedValue::Null => return Ok(None), + _ => crate::bail_constraint_error!("JSON path error near: {:?}", path.to_string()), + }; let mut current_element = &Val::Null; @@ -204,12 +242,10 @@ fn json_extract_single(json: &Val, path: &str) -> crate::Result { if let Some(value) = map.get(key) { current_element = value; } else { - return Ok(Val::Null); + return Ok(None); } } - _ => { - return Ok(Val::Null); - } + _ => return Ok(None), } } PathElement::ArrayLocator(idx) => match current_element { @@ -223,16 +259,15 @@ fn json_extract_single(json: &Val, path: &str) -> crate::Result { if idx < array.len() as i32 { current_element = &array[idx as usize]; } else { - return Ok(Val::Null); + return Ok(None); } } - _ => { - return Ok(Val::Null); - } + _ => return Ok(None), }, } } - Ok(current_element.clone()) + + Ok(Some(¤t_element)) } #[cfg(test)] diff --git a/core/storage/btree.rs b/core/storage/btree.rs index 481866ee7..c84a0c2c0 100644 --- a/core/storage/btree.rs +++ b/core/storage/btree.rs @@ -78,6 +78,7 @@ macro_rules! return_if_locked { enum WriteState { Start, BalanceStart, + BalanceNonRoot, BalanceGetParentPage, BalanceMoveUp, Finish, @@ -730,9 +731,10 @@ impl BTreeCursor { } } WriteState::BalanceStart + | WriteState::BalanceNonRoot | WriteState::BalanceMoveUp | WriteState::BalanceGetParentPage => { - return_if_io!(self.balance_leaf()); + return_if_io!(self.balance()); } WriteState::Finish => { self.write_info.state = WriteState::Start; @@ -882,7 +884,7 @@ impl BTreeCursor { /// This is a naive algorithm that doesn't try to distribute cells evenly by content. /// It will try to split the page in half by keys not by content. /// Sqlite tries to have a page at least 40% full. - fn balance_leaf(&mut self) -> Result> { + fn balance(&mut self) -> Result> { let state = &self.write_info.state; match state { WriteState::BalanceStart => { @@ -906,7 +908,31 @@ impl BTreeCursor { self.balance_root(); return Ok(CursorResult::Ok(())); } - debug!("Balancing leaf. leaf={}", current_page.get().id); + + self.write_info.state = WriteState::BalanceNonRoot; + self.balance_non_root() + } + WriteState::BalanceNonRoot + | WriteState::BalanceGetParentPage + | WriteState::BalanceMoveUp => self.balance_non_root(), + + _ => unreachable!("invalid balance leaf state {:?}", state), + } + } + + fn balance_non_root(&mut self) -> Result> { + let state = &self.write_info.state; + match state { + WriteState::Start => todo!(), + WriteState::BalanceStart => todo!(), + WriteState::BalanceNonRoot => { + // drop divider cells and find right pointer + // NOTE: since we are doing a simple split we only finding the pointer we want to update (right pointer). + // Right pointer means cell that points to the last page, as we don't really want to drop this one. This one + // can be a "rightmost pointer" or a "cell". + // we always asumme there is a parent + let current_page = self.stack.top(); + debug!("balance_non_root(page={})", current_page.get().id); // Copy of page used to reference cell bytes. // This needs to be saved somewhere safe so taht references still point to here, @@ -1186,8 +1212,7 @@ impl BTreeCursor { let _ = self.write_info.page_copy.take(); Ok(CursorResult::Ok(())) } - - _ => unreachable!("invalid balance leaf state {:?}", state), + WriteState::Finish => todo!(), } } diff --git a/core/translate/emitter.rs b/core/translate/emitter.rs index 7b07f1a7a..8fdff8cd1 100644 --- a/core/translate/emitter.rs +++ b/core/translate/emitter.rs @@ -104,12 +104,9 @@ fn prologue<'a>( let mut program = ProgramBuilder::new(); let init_label = program.allocate_label(); - program.emit_insn_with_label_dependency( - Insn::Init { - target_pc: init_label, - }, - init_label, - ); + program.emit_insn(Insn::Init { + target_pc: init_label, + }); let start_offset = program.offset(); @@ -151,8 +148,6 @@ fn epilogue( target_pc: start_offset, }); - program.resolve_deferred_labels(); - Ok(()) } @@ -218,12 +213,9 @@ pub fn emit_query<'a>( let after_main_loop_label = program.allocate_label(); t_ctx.label_main_loop_end = Some(after_main_loop_label); if plan.contains_constant_false_condition { - program.emit_insn_with_label_dependency( - Insn::Goto { - target_pc: after_main_loop_label, - }, - after_main_loop_label, - ); + program.emit_insn(Insn::Goto { + target_pc: after_main_loop_label, + }); } // Allocate registers for result columns @@ -281,12 +273,9 @@ fn emit_program_for_delete( // No rows will be read from source table loops if there is a constant false condition eg. WHERE 0 let after_main_loop_label = program.allocate_label(); if plan.contains_constant_false_condition { - program.emit_insn_with_label_dependency( - Insn::Goto { - target_pc: after_main_loop_label, - }, - after_main_loop_label, - ); + program.emit_insn(Insn::Goto { + target_pc: after_main_loop_label, + }); } // Initialize cursors and other resources needed for query execution @@ -356,13 +345,10 @@ fn emit_delete_insns<'a>( dest: limit_reg, }); program.mark_last_insn_constant(); - program.emit_insn_with_label_dependency( - Insn::DecrJumpZero { - reg: limit_reg, - target_pc: t_ctx.label_main_loop_end.unwrap(), - }, - t_ctx.label_main_loop_end.unwrap(), - ) + program.emit_insn(Insn::DecrJumpZero { + reg: limit_reg, + target_pc: t_ctx.label_main_loop_end.unwrap(), + }) } Ok(()) diff --git a/core/translate/expr.rs b/core/translate/expr.rs index b4fb5e87e..be4cbf534 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -13,11 +13,52 @@ use crate::Result; use super::emitter::Resolver; use super::plan::{TableReference, TableReferenceType}; -#[derive(Default, Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy)] pub struct ConditionMetadata { pub jump_if_condition_is_true: bool, pub jump_target_when_true: BranchOffset, pub jump_target_when_false: BranchOffset, + pub parent_op: Option, +} + +fn emit_cond_jump(program: &mut ProgramBuilder, cond_meta: ConditionMetadata, reg: usize) { + if cond_meta.jump_if_condition_is_true { + program.emit_insn(Insn::If { + reg, + target_pc: cond_meta.jump_target_when_true, + null_reg: reg, + }); + } else { + program.emit_insn(Insn::IfNot { + reg, + target_pc: cond_meta.jump_target_when_false, + null_reg: reg, + }); + } +} +macro_rules! emit_cmp_insn { + ( + $program:expr, + $cond:expr, + $op_true:ident, + $op_false:ident, + $lhs:expr, + $rhs:expr + ) => {{ + if $cond.jump_if_condition_is_true { + $program.emit_insn(Insn::$op_true { + lhs: $lhs, + rhs: $rhs, + target_pc: $cond.jump_target_when_true, + }); + } else { + $program.emit_insn(Insn::$op_false { + lhs: $lhs, + rhs: $rhs, + target_pc: $cond.jump_target_when_false, + }); + } + }}; } pub fn translate_condition_expr( @@ -38,6 +79,8 @@ pub fn translate_condition_expr( lhs, ConditionMetadata { jump_if_condition_is_true: false, + // Mark that the parent op for sub-expressions is AND + parent_op: Some(ast::Operator::And), ..condition_metadata }, resolver, @@ -46,170 +89,91 @@ pub fn translate_condition_expr( program, referenced_tables, rhs, - condition_metadata, + ConditionMetadata { + parent_op: Some(ast::Operator::And), + ..condition_metadata + }, resolver, ); } ast::Expr::Binary(lhs, ast::Operator::Or, rhs) => { - let jump_target_when_false = program.allocate_label(); - let _ = translate_condition_expr( - program, - referenced_tables, - lhs, - ConditionMetadata { - // If the first condition is true, we don't need to evaluate the second condition. + if matches!(condition_metadata.parent_op, Some(ast::Operator::And)) { + // we are inside a bigger AND expression, so we do NOT jump to parent's 'true' if LHS or RHS is true. + // we only short-circuit the parent's false label if LHS and RHS are both false. + let local_true_label = program.allocate_label(); + let local_false_label = program.allocate_label(); + + // evaluate LHS in normal OR fashion, short-circuit local if true + let lhs_metadata = ConditionMetadata { + jump_if_condition_is_true: true, + jump_target_when_true: local_true_label, + jump_target_when_false: local_false_label, + parent_op: Some(ast::Operator::Or), + }; + translate_condition_expr(program, referenced_tables, lhs, lhs_metadata, resolver)?; + + // if lhs was false, we land here: + program.resolve_label(local_false_label, program.offset()); + + // evaluate rhs with normal OR: short-circuit if true, go to local_true + let rhs_metadata = ConditionMetadata { + jump_if_condition_is_true: true, + jump_target_when_true: local_true_label, + jump_target_when_false: condition_metadata.jump_target_when_false, + // if rhs is also false => parent's false + parent_op: Some(ast::Operator::Or), + }; + translate_condition_expr(program, referenced_tables, rhs, rhs_metadata, resolver)?; + + // if we get here, both lhs+rhs are false: explicit jump to parent's false + program.emit_insn(Insn::Goto { + target_pc: condition_metadata.jump_target_when_false, + }); + // local_true: we do not jump to parent's "true" label because the parent is AND, + // so we want to keep evaluating the rest + program.resolve_label(local_true_label, program.offset()); + } else { + let jump_target_when_false = program.allocate_label(); + + let lhs_metadata = ConditionMetadata { jump_if_condition_is_true: true, jump_target_when_false, + parent_op: Some(ast::Operator::Or), ..condition_metadata - }, - resolver, - ); - program.resolve_label(jump_target_when_false, program.offset()); - let _ = translate_condition_expr( - program, - referenced_tables, - rhs, - condition_metadata, - resolver, - ); + }; + + translate_condition_expr(program, referenced_tables, lhs, lhs_metadata, resolver)?; + + // if LHS was false, we land here: + program.resolve_label(jump_target_when_false, program.offset()); + let rhs_metadata = ConditionMetadata { + parent_op: Some(ast::Operator::Or), + ..condition_metadata + }; + translate_condition_expr(program, referenced_tables, rhs, rhs_metadata, resolver)?; + } } ast::Expr::Binary(lhs, op, rhs) => { - let lhs_reg = program.alloc_register(); - let _ = translate_expr(program, Some(referenced_tables), lhs, lhs_reg, resolver); - if let ast::Expr::Literal(_) = lhs.as_ref() { - program.mark_last_insn_constant() - } - let rhs_reg = program.alloc_register(); - let _ = translate_expr(program, Some(referenced_tables), rhs, rhs_reg, resolver); - if let ast::Expr::Literal(_) = rhs.as_ref() { - program.mark_last_insn_constant() - } + let lhs_reg = translate_and_mark(program, Some(referenced_tables), lhs, resolver)?; + let rhs_reg = translate_and_mark(program, Some(referenced_tables), rhs, resolver)?; match op { ast::Operator::Greater => { - if condition_metadata.jump_if_condition_is_true { - program.emit_insn_with_label_dependency( - Insn::Gt { - lhs: lhs_reg, - rhs: rhs_reg, - target_pc: condition_metadata.jump_target_when_true, - }, - condition_metadata.jump_target_when_true, - ) - } else { - program.emit_insn_with_label_dependency( - Insn::Le { - lhs: lhs_reg, - rhs: rhs_reg, - target_pc: condition_metadata.jump_target_when_false, - }, - condition_metadata.jump_target_when_false, - ) - } + emit_cmp_insn!(program, condition_metadata, Gt, Le, lhs_reg, rhs_reg) } ast::Operator::GreaterEquals => { - if condition_metadata.jump_if_condition_is_true { - program.emit_insn_with_label_dependency( - Insn::Ge { - lhs: lhs_reg, - rhs: rhs_reg, - target_pc: condition_metadata.jump_target_when_true, - }, - condition_metadata.jump_target_when_true, - ) - } else { - program.emit_insn_with_label_dependency( - Insn::Lt { - lhs: lhs_reg, - rhs: rhs_reg, - target_pc: condition_metadata.jump_target_when_false, - }, - condition_metadata.jump_target_when_false, - ) - } + emit_cmp_insn!(program, condition_metadata, Ge, Lt, lhs_reg, rhs_reg) } ast::Operator::Less => { - if condition_metadata.jump_if_condition_is_true { - program.emit_insn_with_label_dependency( - Insn::Lt { - lhs: lhs_reg, - rhs: rhs_reg, - target_pc: condition_metadata.jump_target_when_true, - }, - condition_metadata.jump_target_when_true, - ) - } else { - program.emit_insn_with_label_dependency( - Insn::Ge { - lhs: lhs_reg, - rhs: rhs_reg, - target_pc: condition_metadata.jump_target_when_false, - }, - condition_metadata.jump_target_when_false, - ) - } + emit_cmp_insn!(program, condition_metadata, Lt, Ge, lhs_reg, rhs_reg) } ast::Operator::LessEquals => { - if condition_metadata.jump_if_condition_is_true { - program.emit_insn_with_label_dependency( - Insn::Le { - lhs: lhs_reg, - rhs: rhs_reg, - target_pc: condition_metadata.jump_target_when_true, - }, - condition_metadata.jump_target_when_true, - ) - } else { - program.emit_insn_with_label_dependency( - Insn::Gt { - lhs: lhs_reg, - rhs: rhs_reg, - target_pc: condition_metadata.jump_target_when_false, - }, - condition_metadata.jump_target_when_false, - ) - } + emit_cmp_insn!(program, condition_metadata, Le, Gt, lhs_reg, rhs_reg) } ast::Operator::Equals => { - if condition_metadata.jump_if_condition_is_true { - program.emit_insn_with_label_dependency( - Insn::Eq { - lhs: lhs_reg, - rhs: rhs_reg, - target_pc: condition_metadata.jump_target_when_true, - }, - condition_metadata.jump_target_when_true, - ) - } else { - program.emit_insn_with_label_dependency( - Insn::Ne { - lhs: lhs_reg, - rhs: rhs_reg, - target_pc: condition_metadata.jump_target_when_false, - }, - condition_metadata.jump_target_when_false, - ) - } + emit_cmp_insn!(program, condition_metadata, Eq, Ne, lhs_reg, rhs_reg) } ast::Operator::NotEquals => { - if condition_metadata.jump_if_condition_is_true { - program.emit_insn_with_label_dependency( - Insn::Ne { - lhs: lhs_reg, - rhs: rhs_reg, - target_pc: condition_metadata.jump_target_when_true, - }, - condition_metadata.jump_target_when_true, - ) - } else { - program.emit_insn_with_label_dependency( - Insn::Eq { - lhs: lhs_reg, - rhs: rhs_reg, - target_pc: condition_metadata.jump_target_when_false, - }, - condition_metadata.jump_target_when_false, - ) - } + emit_cmp_insn!(program, condition_metadata, Ne, Eq, lhs_reg, rhs_reg) } ast::Operator::Is => todo!(), ast::Operator::IsNot => todo!(), @@ -227,25 +191,7 @@ pub fn translate_condition_expr( value: int_value, dest: reg, }); - if condition_metadata.jump_if_condition_is_true { - program.emit_insn_with_label_dependency( - Insn::If { - reg, - target_pc: condition_metadata.jump_target_when_true, - null_reg: reg, - }, - condition_metadata.jump_target_when_true, - ) - } else { - program.emit_insn_with_label_dependency( - Insn::IfNot { - reg, - target_pc: condition_metadata.jump_target_when_false, - null_reg: reg, - }, - condition_metadata.jump_target_when_false, - ) - } + emit_cond_jump(program, condition_metadata, reg); } else { crate::bail_parse_error!("unsupported literal type in condition"); } @@ -256,25 +202,7 @@ pub fn translate_condition_expr( value: string.clone(), dest: reg, }); - if condition_metadata.jump_if_condition_is_true { - program.emit_insn_with_label_dependency( - Insn::If { - reg, - target_pc: condition_metadata.jump_target_when_true, - null_reg: reg, - }, - condition_metadata.jump_target_when_true, - ) - } else { - program.emit_insn_with_label_dependency( - Insn::IfNot { - reg, - target_pc: condition_metadata.jump_target_when_false, - null_reg: reg, - }, - condition_metadata.jump_target_when_false, - ) - } + emit_cond_jump(program, condition_metadata, reg); } unimpl => todo!("literal {:?} not implemented", unimpl), }, @@ -302,20 +230,14 @@ pub fn translate_condition_expr( // Note that we are already breaking up our WHERE clauses into a series of terms at "AND" boundaries, so right now we won't be running into cases where jumping on true would be incorrect, // but once we have e.g. parenthesization and more complex conditions, not having this 'if' here would introduce a bug. if condition_metadata.jump_if_condition_is_true { - program.emit_insn_with_label_dependency( - Insn::Goto { - target_pc: condition_metadata.jump_target_when_true, - }, - condition_metadata.jump_target_when_true, - ); + program.emit_insn(Insn::Goto { + target_pc: condition_metadata.jump_target_when_true, + }); } } else { - program.emit_insn_with_label_dependency( - Insn::Goto { - target_pc: condition_metadata.jump_target_when_false, - }, - condition_metadata.jump_target_when_false, - ); + program.emit_insn(Insn::Goto { + target_pc: condition_metadata.jump_target_when_false, + }); } return Ok(()); } @@ -349,35 +271,26 @@ pub fn translate_condition_expr( translate_expr(program, Some(referenced_tables), expr, rhs_reg, resolver)?; // If this is not the last condition, we need to jump to the 'jump_target_when_true' label if the condition is true. if !last_condition { - program.emit_insn_with_label_dependency( - Insn::Eq { - lhs: lhs_reg, - rhs: rhs_reg, - target_pc: jump_target_when_true, - }, - jump_target_when_true, - ); + program.emit_insn(Insn::Eq { + lhs: lhs_reg, + rhs: rhs_reg, + target_pc: jump_target_when_true, + }); } else { // If this is the last condition, we need to jump to the 'jump_target_when_false' label if there is no match. - program.emit_insn_with_label_dependency( - Insn::Ne { - lhs: lhs_reg, - rhs: rhs_reg, - target_pc: condition_metadata.jump_target_when_false, - }, - condition_metadata.jump_target_when_false, - ); + program.emit_insn(Insn::Ne { + lhs: lhs_reg, + rhs: rhs_reg, + target_pc: condition_metadata.jump_target_when_false, + }); } } // If we got here, then the last condition was a match, so we jump to the 'jump_target_when_true' label if 'jump_if_condition_is_true'. // If not, we can just fall through without emitting an unnecessary instruction. if condition_metadata.jump_if_condition_is_true { - program.emit_insn_with_label_dependency( - Insn::Goto { - target_pc: condition_metadata.jump_target_when_true, - }, - condition_metadata.jump_target_when_true, - ); + program.emit_insn(Insn::Goto { + target_pc: condition_metadata.jump_target_when_true, + }); } } else { // If it's a NOT IN expression, we need to jump to the 'jump_target_when_false' label if any of the conditions are true. @@ -385,24 +298,18 @@ pub fn translate_condition_expr( let rhs_reg = program.alloc_register(); let _ = translate_expr(program, Some(referenced_tables), expr, rhs_reg, resolver)?; - program.emit_insn_with_label_dependency( - Insn::Eq { - lhs: lhs_reg, - rhs: rhs_reg, - target_pc: condition_metadata.jump_target_when_false, - }, - condition_metadata.jump_target_when_false, - ); + program.emit_insn(Insn::Eq { + lhs: lhs_reg, + rhs: rhs_reg, + target_pc: condition_metadata.jump_target_when_false, + }); } // If we got here, then none of the conditions were a match, so we jump to the 'jump_target_when_true' label if 'jump_if_condition_is_true'. // If not, we can just fall through without emitting an unnecessary instruction. if condition_metadata.jump_if_condition_is_true { - program.emit_insn_with_label_dependency( - Insn::Goto { - target_pc: condition_metadata.jump_target_when_true, - }, - condition_metadata.jump_target_when_true, - ); + program.emit_insn(Insn::Goto { + target_pc: condition_metadata.jump_target_when_true, + }); } } @@ -421,18 +328,8 @@ pub fn translate_condition_expr( match op { ast::LikeOperator::Like | ast::LikeOperator::Glob => { let pattern_reg = program.alloc_register(); - let column_reg = program.alloc_register(); let mut constant_mask = 0; - let _ = translate_expr( - program, - Some(referenced_tables), - lhs, - column_reg, - resolver, - )?; - if let ast::Expr::Literal(_) = lhs.as_ref() { - program.mark_last_insn_constant(); - } + let _ = translate_and_mark(program, Some(referenced_tables), lhs, resolver); let _ = translate_expr( program, Some(referenced_tables), @@ -440,7 +337,7 @@ pub fn translate_condition_expr( pattern_reg, resolver, )?; - if let ast::Expr::Literal(_) = rhs.as_ref() { + if matches!(rhs.as_ref(), ast::Expr::Literal(_)) { program.mark_last_insn_constant(); constant_mask = 1; } @@ -463,56 +360,34 @@ pub fn translate_condition_expr( ast::LikeOperator::Regexp => todo!(), } if !*not { - if condition_metadata.jump_if_condition_is_true { - program.emit_insn_with_label_dependency( - Insn::If { - reg: cur_reg, - target_pc: condition_metadata.jump_target_when_true, - null_reg: cur_reg, - }, - condition_metadata.jump_target_when_true, - ); - } else { - program.emit_insn_with_label_dependency( - Insn::IfNot { - reg: cur_reg, - target_pc: condition_metadata.jump_target_when_false, - null_reg: cur_reg, - }, - condition_metadata.jump_target_when_false, - ); - } + emit_cond_jump(program, condition_metadata, cur_reg); } else if condition_metadata.jump_if_condition_is_true { - program.emit_insn_with_label_dependency( - Insn::IfNot { - reg: cur_reg, - target_pc: condition_metadata.jump_target_when_true, - null_reg: cur_reg, - }, - condition_metadata.jump_target_when_true, - ); + program.emit_insn(Insn::IfNot { + reg: cur_reg, + target_pc: condition_metadata.jump_target_when_true, + null_reg: cur_reg, + }); } else { - program.emit_insn_with_label_dependency( - Insn::If { - reg: cur_reg, - target_pc: condition_metadata.jump_target_when_false, - null_reg: cur_reg, - }, - condition_metadata.jump_target_when_false, - ); + program.emit_insn(Insn::If { + reg: cur_reg, + target_pc: condition_metadata.jump_target_when_false, + null_reg: cur_reg, + }); } } ast::Expr::Parenthesized(exprs) => { - // TODO: this is probably not correct; multiple expressions in a parenthesized expression - // are reserved for special cases like `(a, b) IN ((1, 2), (3, 4))`. - for expr in exprs { + if exprs.len() == 1 { let _ = translate_condition_expr( program, referenced_tables, - expr, + &exprs[0], condition_metadata, resolver, ); + } else { + crate::bail_parse_error!( + "parenthesized condtional should have exactly one expression" + ); } } _ => todo!("op {:?} not implemented", expr), @@ -707,23 +582,17 @@ pub fn translate_expr( translate_expr(program, referenced_tables, when_expr, expr_reg, resolver)?; match base_reg { // CASE 1 WHEN 0 THEN 0 ELSE 1 becomes 1==0, Ne branch to next clause - Some(base_reg) => program.emit_insn_with_label_dependency( - Insn::Ne { - lhs: base_reg, - rhs: expr_reg, - target_pc: next_case_label, - }, - next_case_label, - ), + Some(base_reg) => program.emit_insn(Insn::Ne { + lhs: base_reg, + rhs: expr_reg, + target_pc: next_case_label, + }), // CASE WHEN 0 THEN 0 ELSE 1 becomes ifnot 0 branch to next clause - None => program.emit_insn_with_label_dependency( - Insn::IfNot { - reg: expr_reg, - target_pc: next_case_label, - null_reg: 1, - }, - next_case_label, - ), + None => program.emit_insn(Insn::IfNot { + reg: expr_reg, + target_pc: next_case_label, + null_reg: 1, + }), }; // THEN... translate_expr( @@ -733,12 +602,9 @@ pub fn translate_expr( target_register, resolver, )?; - program.emit_insn_with_label_dependency( - Insn::Goto { - target_pc: return_label, - }, - return_label, - ); + program.emit_insn(Insn::Goto { + target_pc: return_label, + }); // This becomes either the next WHEN, or in the last WHEN/THEN, we're // assured to have at least one instruction corresponding to the ELSE immediately follow. program.preassign_label_to_next_insn(next_case_label); @@ -879,7 +745,7 @@ pub fn translate_expr( }); Ok(target_register) } - JsonFunc::JsonArrayLength => { + JsonFunc::JsonArrayLength | JsonFunc::JsonType => { let args = if let Some(args) = args { if args.len() > 2 { crate::bail_parse_error!( @@ -925,9 +791,9 @@ pub fn translate_expr( unreachable!("this is always ast::Expr::Cast") } ScalarFunc::Changes => { - if let Some(_) = args { + if args.is_some() { crate::bail_parse_error!( - "{} fucntion with more than 0 arguments", + "{} function with more than 0 arguments", srf ); } @@ -984,13 +850,10 @@ pub fn translate_expr( resolver, )?; if index < args.len() - 1 { - program.emit_insn_with_label_dependency( - Insn::NotNull { - reg, - target_pc: label_coalesce_end, - }, - label_coalesce_end, - ); + program.emit_insn(Insn::NotNull { + reg, + target_pc: label_coalesce_end, + }); } } program.preassign_label_to_next_insn(label_coalesce_end); @@ -1085,7 +948,7 @@ pub fn translate_expr( )?; program.emit_insn(Insn::NotNull { reg: temp_reg, - target_pc: program.offset() + 2, + target_pc: program.offset().add(2u32), }); translate_expr( @@ -1120,14 +983,11 @@ pub fn translate_expr( resolver, )?; 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, - ); + program.emit_insn(Insn::IfNot { + reg: temp_reg, + target_pc: jump_target_when_false, + null_reg: 1, + }); translate_expr( program, referenced_tables, @@ -1136,12 +996,9 @@ pub fn translate_expr( resolver, )?; let jump_target_result = program.allocate_label(); - program.emit_insn_with_label_dependency( - Insn::Goto { - target_pc: jump_target_result, - }, - jump_target_result, - ); + program.emit_insn(Insn::Goto { + target_pc: jump_target_result, + }); program.resolve_label(jump_target_when_false, program.offset()); translate_expr( program, @@ -1169,12 +1026,8 @@ pub fn translate_expr( ); }; for arg in args { - let reg = program.alloc_register(); let _ = - translate_expr(program, referenced_tables, arg, reg, resolver)?; - if let ast::Expr::Literal(_) = arg { - program.mark_last_insn_constant() - } + translate_and_mark(program, referenced_tables, arg, resolver); } program.emit_insn(Insn::Function { // Only constant patterns for LIKE are supported currently, so this @@ -1212,12 +1065,11 @@ pub fn translate_expr( srf.to_string() ); }; - - let regs = program.alloc_register(); - translate_expr(program, referenced_tables, &args[0], regs, resolver)?; + let reg = + translate_and_mark(program, referenced_tables, &args[0], resolver)?; program.emit_insn(Insn::Function { constant_mask: 0, - start_reg: regs, + start_reg: reg, dest: target_register, func: func_ctx, }); @@ -1243,12 +1095,10 @@ pub fn translate_expr( if let Some(args) = args { for arg in args.iter() { // register containing result of each argument expression - let target_reg = program.alloc_register(); - _ = translate_expr( + let _ = translate_and_mark( program, referenced_tables, arg, - target_reg, resolver, )?; } @@ -1280,15 +1130,14 @@ pub fn translate_expr( let str_reg = program.alloc_register(); let start_reg = program.alloc_register(); let length_reg = program.alloc_register(); - - translate_expr( + let str_reg = translate_expr( program, referenced_tables, &args[0], str_reg, resolver, )?; - translate_expr( + let _ = translate_expr( program, referenced_tables, &args[1], @@ -1304,7 +1153,6 @@ pub fn translate_expr( resolver, )?; } - program.emit_insn(Insn::Function { constant_mask: 0, start_reg: str_reg, @@ -1324,8 +1172,8 @@ pub fn translate_expr( } else { crate::bail_parse_error!("hex function with no arguments",); }; - let regs = program.alloc_register(); - translate_expr(program, referenced_tables, &args[0], regs, resolver)?; + let regs = + translate_and_mark(program, referenced_tables, &args[0], resolver)?; program.emit_insn(Insn::Function { constant_mask: 0, start_reg: regs, @@ -1365,12 +1213,10 @@ pub fn translate_expr( if let Some(args) = args { for arg in args.iter() { // register containing result of each argument expression - let target_reg = program.alloc_register(); - _ = translate_expr( + let _ = translate_and_mark( program, referenced_tables, arg, - target_reg, resolver, )?; } @@ -1384,7 +1230,7 @@ pub fn translate_expr( Ok(target_register) } ScalarFunc::TotalChanges => { - if let Some(_) = args { + if args.is_some() { crate::bail_parse_error!( "{} fucntion with more than 0 arguments", srf.to_string() @@ -1420,11 +1266,7 @@ pub fn translate_expr( }; for arg in args.iter() { - let reg = program.alloc_register(); - translate_expr(program, referenced_tables, arg, reg, resolver)?; - if let ast::Expr::Literal(_) = arg { - program.mark_last_insn_constant(); - } + translate_and_mark(program, referenced_tables, arg, resolver)?; } program.emit_insn(Insn::Function { constant_mask: 0, @@ -1446,12 +1288,7 @@ pub fn translate_expr( crate::bail_parse_error!("min function with no arguments"); }; for arg in args { - let reg = program.alloc_register(); - let _ = - translate_expr(program, referenced_tables, arg, reg, resolver)?; - if let ast::Expr::Literal(_) = arg { - program.mark_last_insn_constant() - } + translate_and_mark(program, referenced_tables, arg, resolver)?; } program.emit_insn(Insn::Function { @@ -1474,12 +1311,7 @@ pub fn translate_expr( crate::bail_parse_error!("max function with no arguments"); }; for arg in args { - let reg = program.alloc_register(); - let _ = - translate_expr(program, referenced_tables, arg, reg, resolver)?; - if let ast::Expr::Literal(_) = arg { - program.mark_last_insn_constant() - } + translate_and_mark(program, referenced_tables, arg, resolver)?; } program.emit_insn(Insn::Function { @@ -1515,7 +1347,7 @@ pub fn translate_expr( resolver, )?; let second_reg = program.alloc_register(); - translate_expr( + let _ = translate_expr( program, referenced_tables, &args[1], @@ -1566,34 +1398,30 @@ pub fn translate_expr( srf.to_string() ); }; - let str_reg = program.alloc_register(); let pattern_reg = program.alloc_register(); let replacement_reg = program.alloc_register(); - - translate_expr( + let _ = translate_expr( program, referenced_tables, &args[0], str_reg, resolver, )?; - translate_expr( + let _ = translate_expr( program, referenced_tables, &args[1], pattern_reg, resolver, )?; - - translate_expr( + let _ = translate_expr( program, referenced_tables, &args[2], replacement_reg, resolver, )?; - program.emit_insn(Insn::Function { constant_mask: 0, start_reg: str_reg, @@ -1660,12 +1488,12 @@ pub fn translate_expr( }; let mut start_reg = None; if let Some(arg) = args.first() { - let reg = program.alloc_register(); - start_reg = Some(reg); - translate_expr(program, referenced_tables, arg, reg, resolver)?; - if let ast::Expr::Literal(_) = arg { - program.mark_last_insn_constant() - } + start_reg = Some(translate_and_mark( + program, + referenced_tables, + arg, + resolver, + )?); } program.emit_insn(Insn::Function { constant_mask: 0, @@ -1707,10 +1535,8 @@ pub fn translate_expr( crate::bail_parse_error!("{} function with no arguments", math_func); }; - let reg = program.alloc_register(); - - translate_expr(program, referenced_tables, &args[0], reg, resolver)?; - + let reg = + translate_and_mark(program, referenced_tables, &args[0], resolver)?; program.emit_insn(Insn::Function { constant_mask: 0, start_reg: reg, @@ -1732,20 +1558,12 @@ pub fn translate_expr( } else { crate::bail_parse_error!("{} function with no arguments", math_func); }; - let reg1 = program.alloc_register(); + let _ = + translate_expr(program, referenced_tables, &args[0], reg1, resolver)?; let reg2 = program.alloc_register(); - - translate_expr(program, referenced_tables, &args[0], reg1, resolver)?; - if let ast::Expr::Literal(_) = &args[0] { - program.mark_last_insn_constant(); - } - - translate_expr(program, referenced_tables, &args[1], reg2, resolver)?; - if let ast::Expr::Literal(_) = &args[1] { - program.mark_last_insn_constant(); - } - + let _ = + translate_expr(program, referenced_tables, &args[1], reg2, resolver)?; program.emit_insn(Insn::Function { constant_mask: 0, start_reg: target_register + 1, @@ -1769,7 +1587,6 @@ pub fn translate_expr( }; let regs = program.alloc_registers(args.len()); - for (i, arg) in args.iter().enumerate() { translate_expr(program, referenced_tables, arg, regs + i, resolver)?; } @@ -2016,7 +1833,6 @@ fn translate_variable_sized_function_parameter_list( for arg in args.iter() { translate_expr(program, referenced_tables, arg, current_reg, resolver)?; - current_reg += 1; } @@ -2033,7 +1849,7 @@ fn wrap_eval_jump_expr( value: 1, // emit True by default dest: target_register, }); - program.emit_insn_with_label_dependency(insn, if_true_label); + program.emit_insn(insn); program.emit_insn(Insn::Integer { value: 0, // emit False if we reach this point (no jump) dest: target_register, @@ -2049,6 +1865,20 @@ pub fn maybe_apply_affinity(col_type: Type, target_register: usize, program: &mu } } +pub fn translate_and_mark( + program: &mut ProgramBuilder, + referenced_tables: Option<&[TableReference]>, + expr: &ast::Expr, + resolver: &Resolver, +) -> Result { + let target_register = program.alloc_register(); + translate_expr(program, referenced_tables, expr, target_register, resolver)?; + if matches!(expr, ast::Expr::Literal(_)) { + program.mark_last_insn_constant(); + } + Ok(target_register) +} + /// Get an appropriate name for an expression. /// If the query provides an alias (e.g. `SELECT a AS b FROM t`), use that (e.g. `b`). /// If the expression is a column from a table, use the column name (e.g. `a`). diff --git a/core/translate/group_by.rs b/core/translate/group_by.rs index a38a9d26c..c20466f27 100644 --- a/core/translate/group_by.rs +++ b/core/translate/group_by.rs @@ -93,13 +93,10 @@ pub fn init_group_by( program.add_comment(program.offset(), "go to clear accumulator subroutine"); let reg_subrtn_acc_clear_return_offset = program.alloc_register(); - program.emit_insn_with_label_dependency( - Insn::Gosub { - target_pc: label_subrtn_acc_clear, - return_reg: reg_subrtn_acc_clear_return_offset, - }, - label_subrtn_acc_clear, - ); + program.emit_insn(Insn::Gosub { + target_pc: label_subrtn_acc_clear, + return_reg: reg_subrtn_acc_clear_return_offset, + }); t_ctx.reg_agg_start = Some(reg_agg_exprs_start); @@ -187,15 +184,12 @@ pub fn emit_group_by<'a>( }); // Sort the sorter based on the group by columns - program.emit_insn_with_label_dependency( - Insn::SorterSort { - cursor_id: sort_cursor, - pc_if_empty: label_grouping_loop_end, - }, - label_grouping_loop_end, - ); + program.emit_insn(Insn::SorterSort { + cursor_id: sort_cursor, + pc_if_empty: label_grouping_loop_end, + }); - program.defer_label_resolution(label_grouping_loop_start, program.offset() as usize); + program.resolve_label(label_grouping_loop_start, program.offset()); // Read a row from the sorted data in the sorter into the pseudo cursor program.emit_insn(Insn::SorterData { cursor_id: sort_cursor, @@ -229,14 +223,11 @@ pub fn emit_group_by<'a>( "start new group if comparison is not equal", ); // If we are at a new group, continue. If we are at the same group, jump to the aggregation step (i.e. accumulate more values into the aggregations) - program.emit_insn_with_label_dependency( - Insn::Jump { - target_pc_lt: program.offset() + 1, - target_pc_eq: agg_step_label, - target_pc_gt: program.offset() + 1, - }, - agg_step_label, - ); + program.emit_insn(Insn::Jump { + target_pc_lt: program.offset().add(1u32), + target_pc_eq: agg_step_label, + target_pc_gt: program.offset().add(1u32), + }); // New group, move current group by columns into the comparison register program.emit_insn(Insn::Move { @@ -249,32 +240,23 @@ pub fn emit_group_by<'a>( program.offset(), "check if ended group had data, and output if so", ); - program.emit_insn_with_label_dependency( - Insn::Gosub { - target_pc: label_subrtn_acc_output, - return_reg: reg_subrtn_acc_output_return_offset, - }, - label_subrtn_acc_output, - ); + program.emit_insn(Insn::Gosub { + target_pc: label_subrtn_acc_output, + return_reg: reg_subrtn_acc_output_return_offset, + }); program.add_comment(program.offset(), "check abort flag"); - program.emit_insn_with_label_dependency( - Insn::IfPos { - reg: reg_abort_flag, - target_pc: label_group_by_end, - decrement_by: 0, - }, - label_group_by_end, - ); + program.emit_insn(Insn::IfPos { + reg: reg_abort_flag, + target_pc: label_group_by_end, + decrement_by: 0, + }); program.add_comment(program.offset(), "goto clear accumulator subroutine"); - program.emit_insn_with_label_dependency( - Insn::Gosub { - target_pc: label_subrtn_acc_clear, - return_reg: reg_subrtn_acc_clear_return_offset, - }, - label_subrtn_acc_clear, - ); + program.emit_insn(Insn::Gosub { + target_pc: label_subrtn_acc_clear, + return_reg: reg_subrtn_acc_clear_return_offset, + }); // Accumulate the values into the aggregations program.resolve_label(agg_step_label, program.offset()); @@ -299,14 +281,11 @@ pub fn emit_group_by<'a>( program.offset(), "don't emit group columns if continuing existing group", ); - program.emit_insn_with_label_dependency( - Insn::If { - target_pc: label_acc_indicator_set_flag_true, - reg: reg_data_in_acc_flag, - null_reg: 0, // unused in this case - }, - label_acc_indicator_set_flag_true, - ); + program.emit_insn(Insn::If { + target_pc: label_acc_indicator_set_flag_true, + reg: reg_data_in_acc_flag, + null_reg: 0, // unused in this case + }); // Read the group by columns for a finished group for i in 0..group_by.exprs.len() { @@ -326,32 +305,23 @@ pub fn emit_group_by<'a>( dest: reg_data_in_acc_flag, }); - program.emit_insn_with_label_dependency( - Insn::SorterNext { - cursor_id: sort_cursor, - pc_if_next: label_grouping_loop_start, - }, - label_grouping_loop_start, - ); + program.emit_insn(Insn::SorterNext { + cursor_id: sort_cursor, + pc_if_next: label_grouping_loop_start, + }); program.resolve_label(label_grouping_loop_end, program.offset()); program.add_comment(program.offset(), "emit row for final group"); - program.emit_insn_with_label_dependency( - Insn::Gosub { - target_pc: label_subrtn_acc_output, - return_reg: reg_subrtn_acc_output_return_offset, - }, - label_subrtn_acc_output, - ); + program.emit_insn(Insn::Gosub { + target_pc: label_subrtn_acc_output, + return_reg: reg_subrtn_acc_output_return_offset, + }); program.add_comment(program.offset(), "group by finished"); - program.emit_insn_with_label_dependency( - Insn::Goto { - target_pc: label_group_by_end, - }, - label_group_by_end, - ); + program.emit_insn(Insn::Goto { + target_pc: label_group_by_end, + }); program.emit_insn(Insn::Integer { value: 1, dest: reg_abort_flag, @@ -363,19 +333,13 @@ pub fn emit_group_by<'a>( program.resolve_label(label_subrtn_acc_output, program.offset()); program.add_comment(program.offset(), "output group by row subroutine start"); - program.emit_insn_with_label_dependency( - Insn::IfPos { - reg: reg_data_in_acc_flag, - target_pc: label_agg_final, - decrement_by: 0, - }, - label_agg_final, - ); + program.emit_insn(Insn::IfPos { + reg: reg_data_in_acc_flag, + target_pc: label_agg_final, + decrement_by: 0, + }); let group_by_end_without_emitting_row_label = program.allocate_label(); - program.defer_label_resolution( - group_by_end_without_emitting_row_label, - program.offset() as usize, - ); + program.resolve_label(group_by_end_without_emitting_row_label, program.offset()); program.emit_insn(Insn::Return { return_reg: reg_subrtn_acc_output_return_offset, }); @@ -417,7 +381,8 @@ pub fn emit_group_by<'a>( ConditionMetadata { jump_if_condition_is_true: false, jump_target_when_false: group_by_end_without_emitting_row_label, - jump_target_when_true: i64::MAX, // unused + jump_target_when_true: BranchOffset::Placeholder, // not used. FIXME: this is a bug. HAVING can have e.g. HAVING a OR b. + parent_op: None, }, &t_ctx.resolver, )?; diff --git a/core/translate/insert.rs b/core/translate/insert.rs index 3213e89cd..b9f73ba8c 100644 --- a/core/translate/insert.rs +++ b/core/translate/insert.rs @@ -7,6 +7,7 @@ use sqlite3_parser::ast::{ use crate::error::SQLITE_CONSTRAINT_PRIMARYKEY; use crate::util::normalize_ident; +use crate::vdbe::BranchOffset; use crate::{ schema::{Column, Schema, Table}, storage::sqlite3_ondisk::DatabaseHeader, @@ -40,12 +41,9 @@ pub fn translate_insert( let mut program = ProgramBuilder::new(); let resolver = Resolver::new(syms); let init_label = program.allocate_label(); - program.emit_insn_with_label_dependency( - Insn::Init { - target_pc: init_label, - }, - init_label, - ); + program.emit_insn(Insn::Init { + target_pc: init_label, + }); let start_offset = program.offset(); // open table @@ -104,7 +102,7 @@ pub fn translate_insert( let record_register = program.alloc_register(); let halt_label = program.allocate_label(); - let mut loop_start_offset = 0; + let mut loop_start_offset = BranchOffset::Offset(0); let inserting_multiple_rows = values.len() > 1; @@ -112,14 +110,11 @@ pub fn translate_insert( if inserting_multiple_rows { let yield_reg = program.alloc_register(); let jump_on_definition_label = program.allocate_label(); - program.emit_insn_with_label_dependency( - Insn::InitCoroutine { - yield_reg, - jump_on_definition: jump_on_definition_label, - start_offset: program.offset() + 1, - }, - jump_on_definition_label, - ); + program.emit_insn(Insn::InitCoroutine { + yield_reg, + jump_on_definition: jump_on_definition_label, + start_offset: program.offset().add(1u32), + }); for value in values { populate_column_registers( @@ -133,7 +128,7 @@ pub fn translate_insert( )?; program.emit_insn(Insn::Yield { yield_reg, - end_offset: 0, + end_offset: halt_label, }); } program.emit_insn(Insn::EndCoroutine { yield_reg }); @@ -149,13 +144,10 @@ pub fn translate_insert( // FIXME: rollback is not implemented. E.g. if you insert 2 rows and one fails to unique constraint violation, // the other row will still be inserted. loop_start_offset = program.offset(); - program.emit_insn_with_label_dependency( - Insn::Yield { - yield_reg, - end_offset: halt_label, - }, - halt_label, - ); + program.emit_insn(Insn::Yield { + yield_reg, + end_offset: halt_label, + }); } else { // Single row - populate registers directly program.emit_insn(Insn::OpenWriteAsync { @@ -194,13 +186,10 @@ pub fn translate_insert( program.emit_insn(Insn::SoftNull { reg }); } // the user provided rowid value might itself be NULL. If it is, we create a new rowid on the next instruction. - program.emit_insn_with_label_dependency( - Insn::NotNull { - reg: rowid_reg, - target_pc: check_rowid_is_integer_label.unwrap(), - }, - check_rowid_is_integer_label.unwrap(), - ); + program.emit_insn(Insn::NotNull { + reg: rowid_reg, + target_pc: check_rowid_is_integer_label.unwrap(), + }); } // Create new rowid if a) not provided by user or b) provided by user but is NULL @@ -220,14 +209,11 @@ pub fn translate_insert( // When the DB allocates it there are no need for separate uniqueness checks. if has_user_provided_rowid { let make_record_label = program.allocate_label(); - program.emit_insn_with_label_dependency( - Insn::NotExists { - cursor: cursor_id, - rowid_reg, - target_pc: make_record_label, - }, - make_record_label, - ); + program.emit_insn(Insn::NotExists { + cursor: cursor_id, + rowid_reg, + target_pc: make_record_label, + }); let rowid_column_name = if let Some(index) = rowid_alias_index { table.column_index_to_name(index).unwrap() } else { @@ -276,7 +262,6 @@ pub fn translate_insert( program.emit_insn(Insn::Goto { target_pc: start_offset, }); - program.resolve_deferred_labels(); Ok(program.build(database_header, connection)) } diff --git a/core/translate/main_loop.rs b/core/translate/main_loop.rs index c11cc17c4..e95b3dc22 100644 --- a/core/translate/main_loop.rs +++ b/core/translate/main_loop.rs @@ -194,7 +194,7 @@ pub fn open_loop( // In case the subquery is an inner loop, it needs to be reinitialized on each iteration of the outer loop. program.emit_insn(Insn::InitCoroutine { yield_reg, - jump_on_definition: 0, + jump_on_definition: BranchOffset::Offset(0), start_offset: coroutine_implementation_start, }); let LoopLabels { @@ -205,18 +205,15 @@ pub fn open_loop( .labels_main_loop .get(id) .expect("subquery has no loop labels"); - program.defer_label_resolution(loop_start, program.offset() as usize); + program.resolve_label(loop_start, program.offset()); // A subquery within the main loop of a parent query has no cursor, so instead of advancing the cursor, // it emits a Yield which jumps back to the main loop of the subquery itself to retrieve the next row. // When the subquery coroutine completes, this instruction jumps to the label at the top of the termination_label_stack, // which in this case is the end of the Yield-Goto loop in the parent query. - program.emit_insn_with_label_dependency( - Insn::Yield { - yield_reg, - end_offset: loop_end, - }, - loop_end, - ); + program.emit_insn(Insn::Yield { + yield_reg, + end_offset: loop_end, + }); // These are predicates evaluated outside of the subquery, // so they are translated here. @@ -228,6 +225,7 @@ pub fn open_loop( jump_if_condition_is_true: false, jump_target_when_true, jump_target_when_false: next, + parent_op: None, }; translate_condition_expr( program, @@ -276,6 +274,7 @@ pub fn open_loop( jump_if_condition_is_true: false, jump_target_when_true, jump_target_when_false, + parent_op: None, }; for predicate in predicates.iter() { translate_condition_expr( @@ -291,10 +290,7 @@ pub fn open_loop( if *outer { let lj_meta = t_ctx.meta_left_joins.get(id).unwrap(); - program.defer_label_resolution( - lj_meta.label_match_flag_set_true, - program.offset() as usize, - ); + program.resolve_label(lj_meta.label_match_flag_set_true, program.offset()); program.emit_insn(Insn::Integer { value: 1, dest: lj_meta.reg_match_flag, @@ -326,7 +322,7 @@ pub fn open_loop( .labels_main_loop .get(id) .expect("scan has no loop labels"); - program.emit_insn_with_label_dependency( + program.emit_insn( if iter_dir .as_ref() .is_some_and(|dir| *dir == IterationDirection::Backwards) @@ -341,9 +337,8 @@ pub fn open_loop( pc_if_empty: loop_end, } }, - loop_end, ); - program.defer_label_resolution(loop_start, program.offset() as usize); + program.resolve_label(loop_start, program.offset()); if let Some(preds) = predicates { for expr in preds { @@ -352,6 +347,7 @@ pub fn open_loop( jump_if_condition_is_true: false, jump_target_when_true, jump_target_when_false: next, + parent_op: None, }; translate_condition_expr( program, @@ -420,28 +416,25 @@ pub fn open_loop( _ => unreachable!(), } // If we try to seek to a key that is not present in the table/index, we exit the loop entirely. - program.emit_insn_with_label_dependency( - match cmp_op { - ast::Operator::Equals | ast::Operator::GreaterEquals => Insn::SeekGE { - is_index: index_cursor_id.is_some(), - cursor_id: index_cursor_id.unwrap_or(table_cursor_id), - start_reg: cmp_reg, - num_regs: 1, - target_pc: loop_end, - }, - ast::Operator::Greater - | ast::Operator::Less - | ast::Operator::LessEquals => Insn::SeekGT { - is_index: index_cursor_id.is_some(), - cursor_id: index_cursor_id.unwrap_or(table_cursor_id), - start_reg: cmp_reg, - num_regs: 1, - target_pc: loop_end, - }, - _ => unreachable!(), + program.emit_insn(match cmp_op { + ast::Operator::Equals | ast::Operator::GreaterEquals => Insn::SeekGE { + is_index: index_cursor_id.is_some(), + cursor_id: index_cursor_id.unwrap_or(table_cursor_id), + start_reg: cmp_reg, + num_regs: 1, + target_pc: loop_end, }, - loop_end, - ); + ast::Operator::Greater | ast::Operator::Less | ast::Operator::LessEquals => { + Insn::SeekGT { + is_index: index_cursor_id.is_some(), + cursor_id: index_cursor_id.unwrap_or(table_cursor_id), + start_reg: cmp_reg, + num_regs: 1, + target_pc: loop_end, + } + } + _ => unreachable!(), + }); if *cmp_op == ast::Operator::Less || *cmp_op == ast::Operator::LessEquals { translate_expr( program, @@ -452,7 +445,7 @@ pub fn open_loop( )?; } - program.defer_label_resolution(loop_start, program.offset() as usize); + program.resolve_label(loop_start, program.offset()); // TODO: We are currently only handling ascending indexes. // For conditions like index_key > 10, we have already seeked to the first key greater than 10, and can just scan forward. // For conditions like index_key < 10, we are at the beginning of the index, and will scan forward and emit IdxGE(10) with a conditional jump to the end. @@ -466,56 +459,44 @@ pub fn open_loop( match cmp_op { ast::Operator::Equals | ast::Operator::LessEquals => { if let Some(index_cursor_id) = index_cursor_id { - program.emit_insn_with_label_dependency( - Insn::IdxGT { - cursor_id: index_cursor_id, - start_reg: cmp_reg, - num_regs: 1, - target_pc: loop_end, - }, - loop_end, - ); + program.emit_insn(Insn::IdxGT { + cursor_id: index_cursor_id, + start_reg: cmp_reg, + num_regs: 1, + target_pc: loop_end, + }); } else { let rowid_reg = program.alloc_register(); program.emit_insn(Insn::RowId { cursor_id: table_cursor_id, dest: rowid_reg, }); - program.emit_insn_with_label_dependency( - Insn::Gt { - lhs: rowid_reg, - rhs: cmp_reg, - target_pc: loop_end, - }, - loop_end, - ); + program.emit_insn(Insn::Gt { + lhs: rowid_reg, + rhs: cmp_reg, + target_pc: loop_end, + }); } } ast::Operator::Less => { if let Some(index_cursor_id) = index_cursor_id { - program.emit_insn_with_label_dependency( - Insn::IdxGE { - cursor_id: index_cursor_id, - start_reg: cmp_reg, - num_regs: 1, - target_pc: loop_end, - }, - loop_end, - ); + program.emit_insn(Insn::IdxGE { + cursor_id: index_cursor_id, + start_reg: cmp_reg, + num_regs: 1, + target_pc: loop_end, + }); } else { let rowid_reg = program.alloc_register(); program.emit_insn(Insn::RowId { cursor_id: table_cursor_id, dest: rowid_reg, }); - program.emit_insn_with_label_dependency( - Insn::Ge { - lhs: rowid_reg, - rhs: cmp_reg, - target_pc: loop_end, - }, - loop_end, - ); + program.emit_insn(Insn::Ge { + lhs: rowid_reg, + rhs: cmp_reg, + target_pc: loop_end, + }); } } _ => {} @@ -538,14 +519,11 @@ pub fn open_loop( src_reg, &t_ctx.resolver, )?; - program.emit_insn_with_label_dependency( - Insn::SeekRowid { - cursor_id: table_cursor_id, - src_reg, - target_pc: next, - }, - next, - ); + program.emit_insn(Insn::SeekRowid { + cursor_id: table_cursor_id, + src_reg, + target_pc: next, + }); } if let Some(predicates) = predicates { for predicate in predicates.iter() { @@ -554,6 +532,7 @@ pub fn open_loop( jump_if_condition_is_true: false, jump_target_when_true, jump_target_when_false: next, + parent_op: None, }; translate_condition_expr( program, @@ -748,12 +727,9 @@ pub fn close_loop( // A subquery has no cursor to call NextAsync on, so it just emits a Goto // to the Yield instruction, which in turn jumps back to the main loop of the subquery, // so that the next row from the subquery can be read. - program.emit_insn_with_label_dependency( - Insn::Goto { - target_pc: loop_labels.loop_start, - }, - loop_labels.loop_start, - ); + program.emit_insn(Insn::Goto { + target_pc: loop_labels.loop_start, + }); } SourceOperator::Join { id, @@ -771,7 +747,7 @@ pub fn close_loop( // If the left join match flag has been set to 1, we jump to the next row on the outer table, // i.e. continue to the next row of t1 in our example. program.resolve_label(lj_meta.label_match_flag_check_value, program.offset()); - let jump_offset = program.offset() + 3; + let jump_offset = program.offset().add(3u32); program.emit_insn(Insn::IfPos { reg: lj_meta.reg_match_flag, target_pc: jump_offset, @@ -799,12 +775,9 @@ pub fn close_loop( // and we will end up back in the IfPos instruction above, which will then // check the match flag again, and since it is now 1, we will jump to the // next row in the left table. - program.emit_insn_with_label_dependency( - Insn::Goto { - target_pc: lj_meta.label_match_flag_set_true, - }, - lj_meta.label_match_flag_set_true, - ); + program.emit_insn(Insn::Goto { + target_pc: lj_meta.label_match_flag_set_true, + }); assert!(program.offset() == jump_offset); } @@ -830,21 +803,15 @@ pub fn close_loop( .as_ref() .is_some_and(|dir| *dir == IterationDirection::Backwards) { - program.emit_insn_with_label_dependency( - Insn::PrevAwait { - cursor_id, - pc_if_next: loop_labels.loop_start, - }, - loop_labels.loop_start, - ); + program.emit_insn(Insn::PrevAwait { + cursor_id, + pc_if_next: loop_labels.loop_start, + }); } else { - program.emit_insn_with_label_dependency( - Insn::NextAwait { - cursor_id, - pc_if_next: loop_labels.loop_start, - }, - loop_labels.loop_start, - ); + program.emit_insn(Insn::NextAwait { + cursor_id, + pc_if_next: loop_labels.loop_start, + }); } } SourceOperator::Search { @@ -866,13 +833,10 @@ pub fn close_loop( }; program.emit_insn(Insn::NextAsync { cursor_id }); - program.emit_insn_with_label_dependency( - Insn::NextAwait { - cursor_id, - pc_if_next: loop_labels.loop_start, - }, - loop_labels.loop_start, - ); + program.emit_insn(Insn::NextAwait { + cursor_id, + pc_if_next: loop_labels.loop_start, + }); } SourceOperator::Nothing { .. } => {} }; diff --git a/core/translate/mod.rs b/core/translate/mod.rs index f8adb0e09..92b661ce1 100644 --- a/core/translate/mod.rs +++ b/core/translate/mod.rs @@ -388,12 +388,9 @@ fn translate_create_table( if schema.get_table(tbl_name.name.0.as_str()).is_some() { if if_not_exists { let init_label = program.allocate_label(); - program.emit_insn_with_label_dependency( - Insn::Init { - target_pc: init_label, - }, - init_label, - ); + program.emit_insn(Insn::Init { + target_pc: init_label, + }); let start_offset = program.offset(); program.emit_insn(Insn::Halt { err_code: 0, @@ -414,12 +411,9 @@ fn translate_create_table( let parse_schema_label = program.allocate_label(); let init_label = program.allocate_label(); - program.emit_insn_with_label_dependency( - Insn::Init { - target_pc: init_label, - }, - init_label, - ); + program.emit_insn(Insn::Init { + target_pc: init_label, + }); let start_offset = program.offset(); // TODO: ReadCookie // TODO: If @@ -544,12 +538,9 @@ fn translate_pragma( ) -> Result { let mut program = ProgramBuilder::new(); let init_label = program.allocate_label(); - program.emit_insn_with_label_dependency( - Insn::Init { - target_pc: init_label, - }, - init_label, - ); + program.emit_insn(Insn::Init { + target_pc: init_label, + }); let start_offset = program.offset(); let mut write = false; match body { @@ -581,7 +572,6 @@ fn translate_pragma( program.emit_insn(Insn::Goto { target_pc: start_offset, }); - program.resolve_deferred_labels(); Ok(program.build(database_header, connection)) } diff --git a/core/translate/optimizer.rs b/core/translate/optimizer.rs index 5b64f7b2d..d6caba85e 100644 --- a/core/translate/optimizer.rs +++ b/core/translate/optimizer.rs @@ -24,7 +24,7 @@ pub fn optimize_plan(plan: &mut Plan) -> Result<()> { */ fn optimize_select_plan(plan: &mut SelectPlan) -> Result<()> { optimize_subqueries(&mut plan.source)?; - eliminate_between(&mut plan.source, &mut plan.where_clause)?; + rewrite_exprs_select(plan)?; if let ConstantConditionEliminationResult::ImpossibleCondition = eliminate_constants(&mut plan.source, &mut plan.where_clause)? { @@ -55,7 +55,7 @@ fn optimize_select_plan(plan: &mut SelectPlan) -> Result<()> { } fn optimize_delete_plan(plan: &mut DeletePlan) -> Result<()> { - eliminate_between(&mut plan.source, &mut plan.where_clause)?; + rewrite_exprs_delete(plan)?; if let ConstantConditionEliminationResult::ImpossibleCondition = eliminate_constants(&mut plan.source, &mut plan.where_clause)? { @@ -603,14 +603,45 @@ fn push_scan_direction(operator: &mut SourceOperator, direction: &Direction) { } } -fn eliminate_between( - operator: &mut SourceOperator, - where_clauses: &mut Option>, -) -> Result<()> { - if let Some(predicates) = where_clauses { - *predicates = predicates.drain(..).map(convert_between_expr).collect(); +fn rewrite_exprs_select(plan: &mut SelectPlan) -> Result<()> { + rewrite_source_operator_exprs(&mut plan.source)?; + for rc in plan.result_columns.iter_mut() { + rewrite_expr(&mut rc.expr)?; + } + for agg in plan.aggregates.iter_mut() { + rewrite_expr(&mut agg.original_expr)?; + } + if let Some(predicates) = &mut plan.where_clause { + for expr in predicates { + rewrite_expr(expr)?; + } + } + if let Some(group_by) = &mut plan.group_by { + for expr in group_by.exprs.iter_mut() { + rewrite_expr(expr)?; + } + } + if let Some(order_by) = &mut plan.order_by { + for (expr, _) in order_by.iter_mut() { + rewrite_expr(expr)?; + } } + Ok(()) +} + +fn rewrite_exprs_delete(plan: &mut DeletePlan) -> Result<()> { + rewrite_source_operator_exprs(&mut plan.source)?; + if let Some(predicates) = &mut plan.where_clause { + for expr in predicates { + rewrite_expr(expr)?; + } + } + + Ok(()) +} + +fn rewrite_source_operator_exprs(operator: &mut SourceOperator) -> Result<()> { match operator { SourceOperator::Join { left, @@ -618,29 +649,37 @@ fn eliminate_between( predicates, .. } => { - eliminate_between(left, where_clauses)?; - eliminate_between(right, where_clauses)?; + rewrite_source_operator_exprs(left)?; + rewrite_source_operator_exprs(right)?; if let Some(predicates) = predicates { - *predicates = predicates.drain(..).map(convert_between_expr).collect(); + for expr in predicates.iter_mut() { + rewrite_expr(expr)?; + } } - } - SourceOperator::Scan { - predicates: Some(preds), - .. - } => { - *preds = preds.drain(..).map(convert_between_expr).collect(); - } - SourceOperator::Search { - predicates: Some(preds), - .. - } => { - *preds = preds.drain(..).map(convert_between_expr).collect(); - } - _ => (), - } - Ok(()) + Ok(()) + } + SourceOperator::Scan { predicates, .. } | SourceOperator::Search { predicates, .. } => { + if let Some(predicates) = predicates { + for expr in predicates.iter_mut() { + rewrite_expr(expr)?; + } + } + + Ok(()) + } + SourceOperator::Subquery { predicates, .. } => { + if let Some(predicates) = predicates { + for expr in predicates.iter_mut() { + rewrite_expr(expr)?; + } + } + + Ok(()) + } + SourceOperator::Nothing { .. } => Ok(()), + } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -735,9 +774,17 @@ impl Optimizable for ast::Expr { rhs.check_index_scan(table_index, referenced_tables, available_indexes)?; if rhs_index.is_some() { // swap lhs and rhs + let swapped_operator = match *op { + ast::Operator::Equals => ast::Operator::Equals, + ast::Operator::Greater => ast::Operator::Less, + ast::Operator::GreaterEquals => ast::Operator::LessEquals, + ast::Operator::Less => ast::Operator::Greater, + ast::Operator::LessEquals => ast::Operator::GreaterEquals, + _ => unreachable!(), + }; let lhs_new = rhs.take_ownership(); let rhs_new = lhs.take_ownership(); - *self = Self::Binary(Box::new(lhs_new), *op, Box::new(rhs_new)); + *self = Self::Binary(Box::new(lhs_new), swapped_operator, Box::new(rhs_new)); return Ok(rhs_index); } Ok(None) @@ -747,16 +794,6 @@ impl Optimizable for ast::Expr { } fn check_constant(&self) -> Result> { match self { - Self::Id(id) => { - // true and false are special constants that are effectively aliases for 1 and 0 - if id.0.eq_ignore_ascii_case("true") { - return Ok(Some(ConstantPredicate::AlwaysTrue)); - } - if id.0.eq_ignore_ascii_case("false") { - return Ok(Some(ConstantPredicate::AlwaysFalse)); - } - Ok(None) - } Self::Literal(lit) => match lit { ast::Literal::Null => Ok(Some(ConstantPredicate::AlwaysFalse)), ast::Literal::Numeric(b) => { @@ -967,8 +1004,20 @@ pub fn try_extract_index_search_expression( } } -fn convert_between_expr(expr: ast::Expr) -> ast::Expr { +fn rewrite_expr(expr: &mut ast::Expr) -> Result<()> { match expr { + ast::Expr::Id(id) => { + // Convert "true" and "false" to 1 and 0 + if id.0.eq_ignore_ascii_case("true") { + *expr = ast::Expr::Literal(ast::Literal::Numeric(1.to_string())); + return Ok(()); + } + if id.0.eq_ignore_ascii_case("false") { + *expr = ast::Expr::Literal(ast::Literal::Numeric(0.to_string())); + return Ok(()); + } + Ok(()) + } ast::Expr::Between { lhs, not, @@ -976,53 +1025,62 @@ fn convert_between_expr(expr: ast::Expr) -> ast::Expr { end, } => { // Convert `y NOT BETWEEN x AND z` to `x > y OR y > z` - let (lower_op, upper_op) = if not { + let (lower_op, upper_op) = if *not { (ast::Operator::Greater, ast::Operator::Greater) } else { // Convert `y BETWEEN x AND z` to `x <= y AND y <= z` (ast::Operator::LessEquals, ast::Operator::LessEquals) }; - let lower_bound = ast::Expr::Binary(start, lower_op, lhs.clone()); - let upper_bound = ast::Expr::Binary(lhs, upper_op, end); + rewrite_expr(start)?; + rewrite_expr(lhs)?; + rewrite_expr(end)?; - if not { - ast::Expr::Binary( + let start = start.take_ownership(); + let lhs = lhs.take_ownership(); + let end = end.take_ownership(); + + let lower_bound = ast::Expr::Binary(Box::new(start), lower_op, Box::new(lhs.clone())); + let upper_bound = ast::Expr::Binary(Box::new(lhs), upper_op, Box::new(end)); + + if *not { + *expr = ast::Expr::Binary( Box::new(lower_bound), ast::Operator::Or, Box::new(upper_bound), - ) + ); } else { - ast::Expr::Binary( + *expr = ast::Expr::Binary( Box::new(lower_bound), ast::Operator::And, Box::new(upper_bound), - ) + ); } + Ok(()) } - ast::Expr::Parenthesized(mut exprs) => { - ast::Expr::Parenthesized(exprs.drain(..).map(convert_between_expr).collect()) + ast::Expr::Parenthesized(ref mut exprs) => { + for subexpr in exprs.iter_mut() { + rewrite_expr(subexpr)?; + } + let exprs = std::mem::take(exprs); + *expr = ast::Expr::Parenthesized(exprs); + Ok(()) } // Process other expressions recursively - ast::Expr::Binary(lhs, op, rhs) => ast::Expr::Binary( - Box::new(convert_between_expr(*lhs)), - op, - Box::new(convert_between_expr(*rhs)), - ), - ast::Expr::FunctionCall { - name, - distinctness, - args, - order_by, - filter_over, - } => ast::Expr::FunctionCall { - name, - distinctness, - args: args.map(|args| args.into_iter().map(convert_between_expr).collect()), - order_by, - filter_over, - }, - _ => expr, + ast::Expr::Binary(lhs, _, rhs) => { + rewrite_expr(lhs)?; + rewrite_expr(rhs)?; + Ok(()) + } + ast::Expr::FunctionCall { args, .. } => { + if let Some(args) = args { + for arg in args.iter_mut() { + rewrite_expr(arg)?; + } + } + Ok(()) + } + _ => Ok(()), } } diff --git a/core/translate/order_by.rs b/core/translate/order_by.rs index d05fccd60..b58e7ec21 100644 --- a/core/translate/order_by.rs +++ b/core/translate/order_by.rs @@ -110,15 +110,12 @@ pub fn emit_order_by( num_fields: num_columns_in_sorter, }); - program.emit_insn_with_label_dependency( - Insn::SorterSort { - cursor_id: sort_cursor, - pc_if_empty: sort_loop_end_label, - }, - sort_loop_end_label, - ); + program.emit_insn(Insn::SorterSort { + cursor_id: sort_cursor, + pc_if_empty: sort_loop_end_label, + }); - program.defer_label_resolution(sort_loop_start_label, program.offset() as usize); + program.resolve_label(sort_loop_start_label, program.offset()); program.emit_insn(Insn::SorterData { cursor_id: sort_cursor, dest_reg: reg_sorter_data, @@ -140,13 +137,10 @@ pub fn emit_order_by( emit_result_row_and_limit(program, t_ctx, plan, start_reg, Some(sort_loop_end_label))?; - program.emit_insn_with_label_dependency( - Insn::SorterNext { - cursor_id: sort_cursor, - pc_if_next: sort_loop_start_label, - }, - sort_loop_start_label, - ); + program.emit_insn(Insn::SorterNext { + cursor_id: sort_cursor, + pc_if_next: sort_loop_start_label, + }); program.resolve_label(sort_loop_end_label, program.offset()); diff --git a/core/translate/planner.rs b/core/translate/planner.rs index 1f0bf687b..64a0ffb04 100644 --- a/core/translate/planner.rs +++ b/core/translate/planner.rs @@ -113,18 +113,18 @@ pub fn bind_column_references( crate::bail_parse_error!("Column {} is ambiguous", id.0); } let col = table.columns().get(col_idx.unwrap()).unwrap(); - match_result = Some((tbl_idx, col_idx.unwrap(), col.primary_key)); + match_result = Some((tbl_idx, col_idx.unwrap(), col.is_rowid_alias)); } } if match_result.is_none() { crate::bail_parse_error!("Column {} not found", id.0); } - let (tbl_idx, col_idx, is_primary_key) = match_result.unwrap(); + let (tbl_idx, col_idx, is_rowid_alias) = match_result.unwrap(); *expr = ast::Expr::Column { database: None, // TODO: support different databases table: tbl_idx, column: col_idx, - is_rowid_alias: is_primary_key, + is_rowid_alias, }; Ok(()) } @@ -294,7 +294,7 @@ fn parse_from_clause_table( }; subplan.query_type = SelectQueryType::Subquery { yield_reg: usize::MAX, // will be set later in bytecode emission - coroutine_implementation_start: BranchOffset::MAX, // will be set later in bytecode emission + coroutine_implementation_start: BranchOffset::Placeholder, // will be set later in bytecode emission }; let identifier = maybe_alias .map(|a| match a { @@ -544,6 +544,14 @@ fn parse_join( pub fn parse_limit(limit: Limit) -> Option { if let Expr::Literal(ast::Literal::Numeric(n)) = limit.expr { n.parse().ok() + } else if let Expr::Id(id) = limit.expr { + if id.0.eq_ignore_ascii_case("true") { + Some(1) + } else if id.0.eq_ignore_ascii_case("false") { + Some(0) + } else { + None + } } else { None } diff --git a/core/translate/result_row.rs b/core/translate/result_row.rs index 4901cc9ec..5c76f3008 100644 --- a/core/translate/result_row.rs +++ b/core/translate/result_row.rs @@ -54,7 +54,7 @@ pub fn emit_result_row_and_limit( SelectQueryType::Subquery { yield_reg, .. } => { program.emit_insn(Insn::Yield { yield_reg: *yield_reg, - end_offset: 0, + end_offset: BranchOffset::Offset(0), }); } } @@ -71,13 +71,10 @@ pub fn emit_result_row_and_limit( dest: t_ctx.reg_limit.unwrap(), }); program.mark_last_insn_constant(); - program.emit_insn_with_label_dependency( - Insn::DecrJumpZero { - reg: t_ctx.reg_limit.unwrap(), - target_pc: label_on_limit_reached.unwrap(), - }, - label_on_limit_reached.unwrap(), - ); + program.emit_insn(Insn::DecrJumpZero { + reg: t_ctx.reg_limit.unwrap(), + target_pc: label_on_limit_reached.unwrap(), + }); } Ok(()) } diff --git a/core/translate/subquery.rs b/core/translate/subquery.rs index cb0527a6d..206da45ba 100644 --- a/core/translate/subquery.rs +++ b/core/translate/subquery.rs @@ -72,7 +72,7 @@ pub fn emit_subquery<'a>( t_ctx: &mut TranslateCtx<'a>, ) -> Result { let yield_reg = program.alloc_register(); - let coroutine_implementation_start_offset = program.offset() + 1; + let coroutine_implementation_start_offset = program.offset().add(1u32); match &mut plan.query_type { SelectQueryType::Subquery { yield_reg: y, @@ -100,14 +100,11 @@ pub fn emit_subquery<'a>( resolver: Resolver::new(t_ctx.resolver.symbol_table), }; let subquery_body_end_label = program.allocate_label(); - program.emit_insn_with_label_dependency( - Insn::InitCoroutine { - yield_reg, - jump_on_definition: subquery_body_end_label, - start_offset: coroutine_implementation_start_offset, - }, - subquery_body_end_label, - ); + program.emit_insn(Insn::InitCoroutine { + yield_reg, + jump_on_definition: subquery_body_end_label, + start_offset: coroutine_implementation_start_offset, + }); // Normally we mark each LIMIT value as a constant insn that is emitted only once, but in the case of a subquery, // we need to initialize it every time the subquery is run; otherwise subsequent runs of the subquery will already // have the LIMIT counter at 0, and will never return rows. diff --git a/core/vdbe/builder.rs b/core/vdbe/builder.rs index 561765738..40f936cd1 100644 --- a/core/vdbe/builder.rs +++ b/core/vdbe/builder.rs @@ -11,23 +11,20 @@ use super::{BranchOffset, CursorID, Insn, InsnReference, Program, Table}; #[allow(dead_code)] pub struct ProgramBuilder { next_free_register: usize, - next_free_label: BranchOffset, + next_free_label: i32, next_free_cursor_id: usize, insns: Vec, // for temporarily storing instructions that will be put after Transaction opcode constant_insns: Vec, - // Each label has a list of InsnReferences that must - // be resolved. Lists are indexed by: label.abs() - 1 - unresolved_labels: Vec>, next_insn_label: Option, // Cursors that are referenced by the program. Indexed by CursorID. pub cursor_ref: Vec<(Option, Option)>, - // List of deferred label resolutions. Each entry is a pair of (label, insn_reference). - deferred_label_resolutions: Vec<(BranchOffset, InsnReference)>, + // Hashmap of label to insn reference. Resolved in build(). + label_to_resolved_offset: HashMap, // Bitmask of cursors that have emitted a SeekRowid instruction. seekrowid_emitted_bitmask: u64, // map of instruction index to manual comment (used in EXPLAIN) - comments: HashMap, + comments: HashMap, } impl ProgramBuilder { @@ -37,11 +34,10 @@ impl ProgramBuilder { next_free_label: 0, next_free_cursor_id: 0, insns: Vec::new(), - unresolved_labels: Vec::new(), next_insn_label: None, cursor_ref: Vec::new(), constant_insns: Vec::new(), - deferred_label_resolutions: Vec::new(), + label_to_resolved_offset: HashMap::new(), seekrowid_emitted_bitmask: 0, comments: HashMap::new(), } @@ -71,20 +67,17 @@ impl ProgramBuilder { cursor } - fn _emit_insn(&mut self, insn: Insn) { + pub fn emit_insn(&mut self, insn: Insn) { + if let Some(label) = self.next_insn_label { + self.label_to_resolved_offset + .insert(label.to_label_value(), self.insns.len() as InsnReference); + self.next_insn_label = None; + } self.insns.push(insn); } - pub fn emit_insn(&mut self, insn: Insn) { - self._emit_insn(insn); - if let Some(label) = self.next_insn_label { - self.next_insn_label = None; - self.resolve_label(label, (self.insns.len() - 1) as BranchOffset); - } - } - pub fn add_comment(&mut self, insn_index: BranchOffset, comment: &'static str) { - self.comments.insert(insn_index, comment); + self.comments.insert(insn_index.to_offset_int(), comment); } // Emit an instruction that will be put at the end of the program (after Transaction statement). @@ -99,19 +92,13 @@ impl ProgramBuilder { self.insns.append(&mut self.constant_insns); } - pub fn emit_insn_with_label_dependency(&mut self, insn: Insn, label: BranchOffset) { - self._emit_insn(insn); - self.add_label_dependency(label, (self.insns.len() - 1) as BranchOffset); - } - pub fn offset(&self) -> BranchOffset { - self.insns.len() as BranchOffset + BranchOffset::Offset(self.insns.len() as InsnReference) } pub fn allocate_label(&mut self) -> BranchOffset { self.next_free_label -= 1; - self.unresolved_labels.push(Vec::new()); - self.next_free_label + BranchOffset::Label(self.next_free_label) } // Effectively a GOTO without the need to emit an explicit GOTO instruction. @@ -121,232 +108,187 @@ impl ProgramBuilder { self.next_insn_label = Some(label); } - fn label_to_index(&self, label: BranchOffset) -> usize { - (label.abs() - 1) as usize - } - - pub fn add_label_dependency(&mut self, label: BranchOffset, insn_reference: BranchOffset) { - assert!(insn_reference >= 0); - assert!(label < 0); - let label_index = self.label_to_index(label); - assert!(label_index < self.unresolved_labels.len()); - let insn_reference = insn_reference as InsnReference; - let label_references = &mut self.unresolved_labels[label_index]; - label_references.push(insn_reference); - } - - pub fn defer_label_resolution(&mut self, label: BranchOffset, insn_reference: InsnReference) { - self.deferred_label_resolutions - .push((label, insn_reference)); + pub fn resolve_label(&mut self, label: BranchOffset, to_offset: BranchOffset) { + assert!(matches!(label, BranchOffset::Label(_))); + assert!(matches!(to_offset, BranchOffset::Offset(_))); + self.label_to_resolved_offset + .insert(label.to_label_value(), to_offset.to_offset_int()); } /// Resolve unresolved labels to a specific offset in the instruction list. /// - /// This function updates all instructions that reference the given label - /// to point to the specified offset. It ensures that the label and offset - /// are valid and updates the target program counter (PC) of each instruction - /// that references the label. - /// - /// # Arguments - /// - /// * `label` - The label to resolve. - /// * `to_offset` - The offset to which the labeled instructions should be resolved to. - pub fn resolve_label(&mut self, label: BranchOffset, to_offset: BranchOffset) { - assert!(label < 0); - assert!(to_offset >= 0); - let label_index = self.label_to_index(label); - assert!( - label_index < self.unresolved_labels.len(), - "Forbidden resolve of an unexistent label!" - ); - - let label_references = &mut self.unresolved_labels[label_index]; - for insn_reference in label_references.iter() { - let insn = &mut self.insns[*insn_reference]; + /// This function scans all instructions and resolves any labels to their corresponding offsets. + /// It ensures that all labels are resolved correctly and updates the target program counter (PC) + /// of each instruction that references a label. + pub fn resolve_labels(&mut self) { + let resolve = |pc: &mut BranchOffset, insn_name: &str| { + if let BranchOffset::Label(label) = pc { + let to_offset = *self.label_to_resolved_offset.get(label).unwrap_or_else(|| { + panic!("Reference to undefined label in {}: {}", insn_name, label) + }); + *pc = BranchOffset::Offset(to_offset); + } + }; + for insn in self.insns.iter_mut() { match insn { Insn::Init { target_pc } => { - assert!(*target_pc < 0); - *target_pc = to_offset; + resolve(target_pc, "Init"); } Insn::Eq { lhs: _lhs, rhs: _rhs, target_pc, } => { - assert!(*target_pc < 0); - *target_pc = to_offset; + resolve(target_pc, "Eq"); } Insn::Ne { lhs: _lhs, rhs: _rhs, target_pc, } => { - assert!(*target_pc < 0); - *target_pc = to_offset; + resolve(target_pc, "Ne"); } Insn::Lt { lhs: _lhs, rhs: _rhs, target_pc, } => { - assert!(*target_pc < 0); - *target_pc = to_offset; + resolve(target_pc, "Lt"); } Insn::Le { lhs: _lhs, rhs: _rhs, target_pc, } => { - assert!(*target_pc < 0); - *target_pc = to_offset; + resolve(target_pc, "Le"); } Insn::Gt { lhs: _lhs, rhs: _rhs, target_pc, } => { - assert!(*target_pc < 0); - *target_pc = to_offset; + resolve(target_pc, "Gt"); } Insn::Ge { lhs: _lhs, rhs: _rhs, target_pc, } => { - assert!(*target_pc < 0); - *target_pc = to_offset; + resolve(target_pc, "Ge"); } Insn::If { reg: _reg, target_pc, null_reg: _, } => { - assert!(*target_pc < 0); - *target_pc = to_offset; + resolve(target_pc, "If"); } Insn::IfNot { reg: _reg, target_pc, null_reg: _, } => { - assert!(*target_pc < 0); - *target_pc = to_offset; + resolve(target_pc, "IfNot"); } Insn::RewindAwait { cursor_id: _cursor_id, pc_if_empty, } => { - assert!(*pc_if_empty < 0); - *pc_if_empty = to_offset; + resolve(pc_if_empty, "RewindAwait"); } Insn::LastAwait { cursor_id: _cursor_id, pc_if_empty, } => { - assert!(*pc_if_empty < 0); - *pc_if_empty = to_offset; + resolve(pc_if_empty, "LastAwait"); } Insn::Goto { target_pc } => { - assert!(*target_pc < 0); - *target_pc = to_offset; + resolve(target_pc, "Goto"); } Insn::DecrJumpZero { reg: _reg, target_pc, } => { - assert!(*target_pc < 0); - *target_pc = to_offset; + resolve(target_pc, "DecrJumpZero"); } Insn::SorterNext { cursor_id: _cursor_id, pc_if_next, } => { - assert!(*pc_if_next < 0); - *pc_if_next = to_offset; + resolve(pc_if_next, "SorterNext"); } Insn::SorterSort { pc_if_empty, .. } => { - assert!(*pc_if_empty < 0); - *pc_if_empty = to_offset; + resolve(pc_if_empty, "SorterSort"); } Insn::NotNull { reg: _reg, target_pc, } => { - assert!(*target_pc < 0); - *target_pc = to_offset; + resolve(target_pc, "NotNull"); } Insn::IfPos { target_pc, .. } => { - assert!(*target_pc < 0); - *target_pc = to_offset; + resolve(target_pc, "IfPos"); } Insn::NextAwait { pc_if_next, .. } => { - assert!(*pc_if_next < 0); - *pc_if_next = to_offset; + resolve(pc_if_next, "NextAwait"); } Insn::PrevAwait { pc_if_next, .. } => { - assert!(*pc_if_next < 0); - *pc_if_next = to_offset; + resolve(pc_if_next, "PrevAwait"); } Insn::InitCoroutine { yield_reg: _, jump_on_definition, start_offset: _, } => { - *jump_on_definition = to_offset; + resolve(jump_on_definition, "InitCoroutine"); } Insn::NotExists { cursor: _, rowid_reg: _, target_pc, } => { - *target_pc = to_offset; + resolve(target_pc, "NotExists"); } Insn::Yield { yield_reg: _, end_offset, } => { - *end_offset = to_offset; + resolve(end_offset, "Yield"); } Insn::SeekRowid { target_pc, .. } => { - assert!(*target_pc < 0); - *target_pc = to_offset; + resolve(target_pc, "SeekRowid"); } Insn::Gosub { target_pc, .. } => { - assert!(*target_pc < 0); - *target_pc = to_offset; + resolve(target_pc, "Gosub"); } - Insn::Jump { target_pc_eq, .. } => { - // FIXME: this current implementation doesnt scale for insns that - // have potentially multiple label dependencies. - assert!(*target_pc_eq < 0); - *target_pc_eq = to_offset; + Insn::Jump { + target_pc_eq, + target_pc_lt, + target_pc_gt, + } => { + resolve(target_pc_eq, "Jump"); + resolve(target_pc_lt, "Jump"); + resolve(target_pc_gt, "Jump"); } Insn::SeekGE { target_pc, .. } => { - assert!(*target_pc < 0); - *target_pc = to_offset; + resolve(target_pc, "SeekGE"); } Insn::SeekGT { target_pc, .. } => { - assert!(*target_pc < 0); - *target_pc = to_offset; + resolve(target_pc, "SeekGT"); } Insn::IdxGE { target_pc, .. } => { - assert!(*target_pc < 0); - *target_pc = to_offset; + resolve(target_pc, "IdxGE"); } Insn::IdxGT { target_pc, .. } => { - assert!(*target_pc < 0); - *target_pc = to_offset; + resolve(target_pc, "IdxGT"); } Insn::IsNull { src: _, target_pc } => { - assert!(*target_pc < 0); - *target_pc = to_offset; - } - _ => { - todo!("missing resolve_label for {:?}", insn); + resolve(target_pc, "IsNull"); } + _ => continue, } } - label_references.clear(); + self.label_to_resolved_offset.clear(); } // translate table to cursor id @@ -361,23 +303,12 @@ impl ProgramBuilder { .unwrap() } - pub fn resolve_deferred_labels(&mut self) { - for i in 0..self.deferred_label_resolutions.len() { - let (label, insn_reference) = self.deferred_label_resolutions[i]; - self.resolve_label(label, insn_reference as BranchOffset); - } - self.deferred_label_resolutions.clear(); - } - pub fn build( - self, + mut self, database_header: Rc>, connection: Weak, ) -> Program { - assert!( - self.deferred_label_resolutions.is_empty(), - "deferred_label_resolutions is not empty when build() is called, did you forget to call resolve_deferred_labels()?" - ); + self.resolve_labels(); assert!( self.constant_insns.is_empty(), "constant_insns is not empty when build() is called, did you forget to call emit_constant_insns()?" diff --git a/core/vdbe/datetime.rs b/core/vdbe/datetime.rs index 37e66ae69..ec995290c 100644 --- a/core/vdbe/datetime.rs +++ b/core/vdbe/datetime.rs @@ -1432,10 +1432,17 @@ mod tests { #[test] fn test_subsec_modifier() { - let now = Utc::now().naive_utc(); - let expected = now.format("%H:%M:%S%.3f").to_string(); + let now = Utc::now().naive_utc().time(); let result = exec_datetime(&[text("now"), text("subsec")], DateTimeOutput::Time); - assert_eq!(result, text(&expected)); + let tolerance = TimeDelta::milliseconds(1); + let result = + chrono::NaiveTime::parse_from_str(&result.to_string(), "%H:%M:%S%.3f").unwrap(); + assert!( + (now - result).num_milliseconds().abs() <= tolerance.num_milliseconds(), + "Expected: {}, Actual: {}", + now, + result + ); } #[test] @@ -1505,11 +1512,10 @@ mod tests { #[test] fn test_combined_modifiers() { let now = Utc::now().naive_utc(); - let dt = now - TimeDelta::days(1) + let expected = now - TimeDelta::days(1) + TimeDelta::hours(5) + TimeDelta::minutes(30) + TimeDelta::seconds(15); - let expected = dt.format("%Y-%m-%d %H:%M:%S%.3f").to_string(); let result = exec_datetime( &[ text("now"), @@ -1521,7 +1527,16 @@ mod tests { ], DateTimeOutput::DateTime, ); - assert_eq!(result, text(&expected)); + let tolerance = TimeDelta::milliseconds(1); + let result = + chrono::NaiveDateTime::parse_from_str(&result.to_string(), "%Y-%m-%d %H:%M:%S%.3f") + .unwrap(); + assert!( + (result - expected).num_milliseconds().abs() <= tolerance.num_milliseconds(), + "Expected: {}, Actual: {}", + expected, + result + ); } #[test] diff --git a/core/vdbe/explain.rs b/core/vdbe/explain.rs index a17ababcf..133172b78 100644 --- a/core/vdbe/explain.rs +++ b/core/vdbe/explain.rs @@ -13,11 +13,11 @@ pub fn insn_to_str( Insn::Init { target_pc } => ( "Init", 0, - *target_pc as i32, + target_pc.to_debug_int(), 0, OwnedValue::build_text(Rc::new("".to_string())), 0, - format!("Start at {}", target_pc), + format!("Start at {}", target_pc.to_debug_int()), ), Insn::Add { lhs, rhs, dest } => ( "Add", @@ -114,11 +114,11 @@ pub fn insn_to_str( Insn::NotNull { reg, target_pc } => ( "NotNull", *reg as i32, - *target_pc as i32, + target_pc.to_debug_int(), 0, OwnedValue::build_text(Rc::new("".to_string())), 0, - format!("r[{}]!=NULL -> goto {}", reg, target_pc), + format!("r[{}]!=NULL -> goto {}", reg, target_pc.to_debug_int()), ), Insn::Compare { start_reg_a, @@ -145,9 +145,9 @@ pub fn insn_to_str( target_pc_gt, } => ( "Jump", - *target_pc_lt as i32, - *target_pc_eq as i32, - *target_pc_gt as i32, + target_pc_lt.to_debug_int(), + target_pc_eq.to_debug_int(), + target_pc_gt.to_debug_int(), OwnedValue::build_text(Rc::new("".to_string())), 0, "".to_string(), @@ -178,13 +178,16 @@ pub fn insn_to_str( } => ( "IfPos", *reg as i32, - *target_pc as i32, + target_pc.to_debug_int(), 0, OwnedValue::build_text(Rc::new("".to_string())), 0, format!( "r[{}]>0 -> r[{}]-={}, goto {}", - reg, reg, decrement_by, target_pc + reg, + reg, + decrement_by, + target_pc.to_debug_int() ), ), Insn::Eq { @@ -195,10 +198,15 @@ pub fn insn_to_str( "Eq", *lhs as i32, *rhs as i32, - *target_pc as i32, + target_pc.to_debug_int(), OwnedValue::build_text(Rc::new("".to_string())), 0, - format!("if r[{}]==r[{}] goto {}", lhs, rhs, target_pc), + format!( + "if r[{}]==r[{}] goto {}", + lhs, + rhs, + target_pc.to_debug_int() + ), ), Insn::Ne { lhs, @@ -208,10 +216,15 @@ pub fn insn_to_str( "Ne", *lhs as i32, *rhs as i32, - *target_pc as i32, + target_pc.to_debug_int(), OwnedValue::build_text(Rc::new("".to_string())), 0, - format!("if r[{}]!=r[{}] goto {}", lhs, rhs, target_pc), + format!( + "if r[{}]!=r[{}] goto {}", + lhs, + rhs, + target_pc.to_debug_int() + ), ), Insn::Lt { lhs, @@ -221,10 +234,10 @@ pub fn insn_to_str( "Lt", *lhs as i32, *rhs as i32, - *target_pc as i32, + target_pc.to_debug_int(), OwnedValue::build_text(Rc::new("".to_string())), 0, - format!("if r[{}]r[{}] goto {}", lhs, rhs, target_pc), + format!("if r[{}]>r[{}] goto {}", lhs, rhs, target_pc.to_debug_int()), ), Insn::Ge { lhs, @@ -260,10 +278,15 @@ pub fn insn_to_str( "Ge", *lhs as i32, *rhs as i32, - *target_pc as i32, + target_pc.to_debug_int(), OwnedValue::build_text(Rc::new("".to_string())), 0, - format!("if r[{}]>=r[{}] goto {}", lhs, rhs, target_pc), + format!( + "if r[{}]>=r[{}] goto {}", + lhs, + rhs, + target_pc.to_debug_int() + ), ), Insn::If { reg, @@ -272,11 +295,11 @@ pub fn insn_to_str( } => ( "If", *reg as i32, - *target_pc as i32, + target_pc.to_debug_int(), *null_reg as i32, OwnedValue::build_text(Rc::new("".to_string())), 0, - format!("if r[{}] goto {}", reg, target_pc), + format!("if r[{}] goto {}", reg, target_pc.to_debug_int()), ), Insn::IfNot { reg, @@ -285,11 +308,11 @@ pub fn insn_to_str( } => ( "IfNot", *reg as i32, - *target_pc as i32, + target_pc.to_debug_int(), *null_reg as i32, OwnedValue::build_text(Rc::new("".to_string())), 0, - format!("if !r[{}] goto {}", reg, target_pc), + format!("if !r[{}] goto {}", reg, target_pc.to_debug_int()), ), Insn::OpenReadAsync { cursor_id, @@ -347,7 +370,7 @@ pub fn insn_to_str( } => ( "RewindAwait", *cursor_id as i32, - *pc_if_empty as i32, + pc_if_empty.to_debug_int(), 0, OwnedValue::build_text(Rc::new("".to_string())), 0, @@ -431,7 +454,7 @@ pub fn insn_to_str( } => ( "NextAwait", *cursor_id as i32, - *pc_if_next as i32, + pc_if_next.to_debug_int(), 0, OwnedValue::build_text(Rc::new("".to_string())), 0, @@ -461,7 +484,7 @@ pub fn insn_to_str( Insn::Goto { target_pc } => ( "Goto", 0, - *target_pc as i32, + target_pc.to_debug_int(), 0, OwnedValue::build_text(Rc::new("".to_string())), 0, @@ -473,7 +496,7 @@ pub fn insn_to_str( } => ( "Gosub", *return_reg as i32, - *target_pc as i32, + target_pc.to_debug_int(), 0, OwnedValue::build_text(Rc::new("".to_string())), 0, @@ -562,7 +585,7 @@ pub fn insn_to_str( "SeekRowid", *cursor_id as i32, *src_reg as i32, - *target_pc as i32, + target_pc.to_debug_int(), OwnedValue::build_text(Rc::new("".to_string())), 0, format!( @@ -572,7 +595,7 @@ pub fn insn_to_str( .0 .as_ref() .unwrap_or(&format!("cursor {}", cursor_id)), - target_pc + target_pc.to_debug_int() ), ), Insn::DeferredSeek { @@ -596,7 +619,7 @@ pub fn insn_to_str( } => ( "SeekGT", *cursor_id as i32, - *target_pc as i32, + target_pc.to_debug_int(), *start_reg as i32, OwnedValue::build_text(Rc::new("".to_string())), 0, @@ -611,7 +634,7 @@ pub fn insn_to_str( } => ( "SeekGE", *cursor_id as i32, - *target_pc as i32, + target_pc.to_debug_int(), *start_reg as i32, OwnedValue::build_text(Rc::new("".to_string())), 0, @@ -625,7 +648,7 @@ pub fn insn_to_str( } => ( "IdxGT", *cursor_id as i32, - *target_pc as i32, + target_pc.to_debug_int(), *start_reg as i32, OwnedValue::build_text(Rc::new("".to_string())), 0, @@ -639,7 +662,7 @@ pub fn insn_to_str( } => ( "IdxGE", *cursor_id as i32, - *target_pc as i32, + target_pc.to_debug_int(), *start_reg as i32, OwnedValue::build_text(Rc::new("".to_string())), 0, @@ -648,11 +671,11 @@ pub fn insn_to_str( Insn::DecrJumpZero { reg, target_pc } => ( "DecrJumpZero", *reg as i32, - *target_pc as i32, + target_pc.to_debug_int(), 0, OwnedValue::build_text(Rc::new("".to_string())), 0, - format!("if (--r[{}]==0) goto {}", reg, target_pc), + format!("if (--r[{}]==0) goto {}", reg, target_pc.to_debug_int()), ), Insn::AggStep { func, @@ -742,7 +765,7 @@ pub fn insn_to_str( } => ( "SorterSort", *cursor_id as i32, - *pc_if_empty as i32, + pc_if_empty.to_debug_int(), 0, OwnedValue::build_text(Rc::new("".to_string())), 0, @@ -754,7 +777,7 @@ pub fn insn_to_str( } => ( "SorterNext", *cursor_id as i32, - *pc_if_next as i32, + pc_if_next.to_debug_int(), 0, OwnedValue::build_text(Rc::new("".to_string())), 0, @@ -792,8 +815,8 @@ pub fn insn_to_str( } => ( "InitCoroutine", *yield_reg as i32, - *jump_on_definition as i32, - *start_offset as i32, + jump_on_definition.to_debug_int(), + start_offset.to_debug_int(), OwnedValue::build_text(Rc::new("".to_string())), 0, "".to_string(), @@ -813,7 +836,7 @@ pub fn insn_to_str( } => ( "Yield", *yield_reg as i32, - *end_offset as i32, + end_offset.to_debug_int(), 0, OwnedValue::build_text(Rc::new("".to_string())), 0, @@ -898,7 +921,7 @@ pub fn insn_to_str( } => ( "NotExists", *cursor as i32, - *target_pc as i32, + target_pc.to_debug_int(), *rowid_reg as i32, OwnedValue::build_text(Rc::new("".to_string())), 0, @@ -968,11 +991,11 @@ pub fn insn_to_str( Insn::IsNull { src, target_pc } => ( "IsNull", *src as i32, - *target_pc as i32, + target_pc.to_debug_int(), 0, OwnedValue::build_text(Rc::new("".to_string())), 0, - format!("if (r[{}]==NULL) goto {}", src, target_pc), + format!("if (r[{}]==NULL) goto {}", src, target_pc.to_debug_int()), ), Insn::ParseSchema { db, where_clause } => ( "ParseSchema", diff --git a/core/vdbe/mod.rs b/core/vdbe/mod.rs index a0b42abb6..73557303e 100644 --- a/core/vdbe/mod.rs +++ b/core/vdbe/mod.rs @@ -41,7 +41,7 @@ use crate::vdbe::insn::Insn; #[cfg(feature = "json")] use crate::{ function::JsonFunc, json::get_json, json::json_array, json::json_array_length, - json::json_extract, + json::json_extract, json::json_type, }; use crate::{Connection, Result, Rows, TransactionState, DATABASE_VERSION}; use datetime::{exec_date, exec_datetime_full, exec_julianday, exec_time, exec_unixepoch}; @@ -58,13 +58,75 @@ use std::cell::RefCell; use std::collections::{BTreeMap, HashMap}; use std::rc::{Rc, Weak}; -pub type BranchOffset = i64; +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +/// Represents a target for a jump instruction. +/// Stores 32-bit ints to keep the enum word-sized. +pub enum BranchOffset { + /// A label is a named location in the program. + /// If there are references to it, it must always be resolved to an Offset + /// via program.resolve_label(). + Label(i32), + /// An offset is a direct index into the instruction list. + Offset(InsnReference), + /// A placeholder is a temporary value to satisfy the compiler. + /// It must be set later. + Placeholder, +} + +impl BranchOffset { + /// Returns true if the branch offset is a label. + pub fn is_label(&self) -> bool { + matches!(self, BranchOffset::Label(_)) + } + + /// Returns true if the branch offset is an offset. + pub fn is_offset(&self) -> bool { + matches!(self, BranchOffset::Offset(_)) + } + + /// Returns the offset value. Panics if the branch offset is a label or placeholder. + pub fn to_offset_int(&self) -> InsnReference { + match self { + BranchOffset::Label(v) => unreachable!("Unresolved label: {}", v), + BranchOffset::Offset(v) => *v, + BranchOffset::Placeholder => unreachable!("Unresolved placeholder"), + } + } + + /// Returns the label value. Panics if the branch offset is an offset or placeholder. + pub fn to_label_value(&self) -> i32 { + match self { + BranchOffset::Label(v) => *v, + BranchOffset::Offset(_) => unreachable!("Offset cannot be converted to label value"), + BranchOffset::Placeholder => unreachable!("Unresolved placeholder"), + } + } + + /// Returns the branch offset as a signed integer. + /// Used in explain output, where we don't want to panic in case we have an unresolved + /// label or placeholder. + pub fn to_debug_int(&self) -> i32 { + match self { + BranchOffset::Label(v) => *v, + BranchOffset::Offset(v) => *v as i32, + BranchOffset::Placeholder => i32::MAX, + } + } + + /// Adds an integer value to the branch offset. + /// Returns a new branch offset. + /// Panics if the branch offset is a label or placeholder. + pub fn add>(self, n: N) -> BranchOffset { + BranchOffset::Offset(self.to_offset_int() + n.into()) + } +} + pub type CursorID = usize; pub type PageIdx = usize; // Index of insn in list of insns -type InsnReference = usize; +type InsnReference = u32; pub enum StepResult<'a> { Done, @@ -101,7 +163,7 @@ impl RegexCache { /// The program state describes the environment in which the program executes. pub struct ProgramState { - pub pc: BranchOffset, + pub pc: InsnReference, cursors: RefCell>>, registers: Vec, last_compare: Option, @@ -151,7 +213,7 @@ pub struct Program { pub insns: Vec, pub cursor_ref: Vec<(Option, Option
)>, pub database_header: Rc>, - pub comments: HashMap, + pub comments: HashMap, pub connection: Weak, pub auto_commit: bool, } @@ -189,8 +251,8 @@ impl Program { let mut cursors = state.cursors.borrow_mut(); match insn { Insn::Init { target_pc } => { - assert!(*target_pc >= 0); - state.pc = *target_pc; + assert!(target_pc.is_offset()); + state.pc = target_pc.to_offset_int(); } Insn::Add { lhs, rhs, dest } => { state.registers[*dest] = @@ -278,6 +340,9 @@ impl Program { target_pc_eq, target_pc_gt, } => { + assert!(target_pc_lt.is_offset()); + assert!(target_pc_eq.is_offset()); + assert!(target_pc_gt.is_offset()); let cmp = state.last_compare.take(); if cmp.is_none() { return Err(LimboError::InternalError( @@ -289,8 +354,7 @@ impl Program { std::cmp::Ordering::Equal => *target_pc_eq, std::cmp::Ordering::Greater => *target_pc_gt, }; - assert!(target_pc >= 0); - state.pc = target_pc; + state.pc = target_pc.to_offset_int(); } Insn::Move { source_reg, @@ -313,12 +377,12 @@ impl Program { target_pc, decrement_by, } => { - assert!(*target_pc >= 0); + assert!(target_pc.is_offset()); let reg = *reg; let target_pc = *target_pc; match &state.registers[reg] { OwnedValue::Integer(n) if *n > 0 => { - state.pc = target_pc; + state.pc = target_pc.to_offset_int(); state.registers[reg] = OwnedValue::Integer(*n - *decrement_by as i64); } OwnedValue::Integer(_) => { @@ -332,7 +396,7 @@ impl Program { } } Insn::NotNull { reg, target_pc } => { - assert!(*target_pc >= 0); + assert!(target_pc.is_offset()); let reg = *reg; let target_pc = *target_pc; match &state.registers[reg] { @@ -340,7 +404,7 @@ impl Program { state.pc += 1; } _ => { - state.pc = target_pc; + state.pc = target_pc.to_offset_int(); } } } @@ -350,17 +414,17 @@ impl Program { rhs, target_pc, } => { - assert!(*target_pc >= 0); + assert!(target_pc.is_offset()); let lhs = *lhs; let rhs = *rhs; let target_pc = *target_pc; match (&state.registers[lhs], &state.registers[rhs]) { (_, OwnedValue::Null) | (OwnedValue::Null, _) => { - state.pc = target_pc; + state.pc = target_pc.to_offset_int(); } _ => { if state.registers[lhs] == state.registers[rhs] { - state.pc = target_pc; + state.pc = target_pc.to_offset_int(); } else { state.pc += 1; } @@ -372,17 +436,17 @@ impl Program { rhs, target_pc, } => { - assert!(*target_pc >= 0); + assert!(target_pc.is_offset()); let lhs = *lhs; let rhs = *rhs; let target_pc = *target_pc; match (&state.registers[lhs], &state.registers[rhs]) { (_, OwnedValue::Null) | (OwnedValue::Null, _) => { - state.pc = target_pc; + state.pc = target_pc.to_offset_int(); } _ => { if state.registers[lhs] != state.registers[rhs] { - state.pc = target_pc; + state.pc = target_pc.to_offset_int(); } else { state.pc += 1; } @@ -394,17 +458,17 @@ impl Program { rhs, target_pc, } => { - assert!(*target_pc >= 0); + assert!(target_pc.is_offset()); let lhs = *lhs; let rhs = *rhs; let target_pc = *target_pc; match (&state.registers[lhs], &state.registers[rhs]) { (_, OwnedValue::Null) | (OwnedValue::Null, _) => { - state.pc = target_pc; + state.pc = target_pc.to_offset_int(); } _ => { if state.registers[lhs] < state.registers[rhs] { - state.pc = target_pc; + state.pc = target_pc.to_offset_int(); } else { state.pc += 1; } @@ -416,17 +480,17 @@ impl Program { rhs, target_pc, } => { - assert!(*target_pc >= 0); + assert!(target_pc.is_offset()); let lhs = *lhs; let rhs = *rhs; let target_pc = *target_pc; match (&state.registers[lhs], &state.registers[rhs]) { (_, OwnedValue::Null) | (OwnedValue::Null, _) => { - state.pc = target_pc; + state.pc = target_pc.to_offset_int(); } _ => { if state.registers[lhs] <= state.registers[rhs] { - state.pc = target_pc; + state.pc = target_pc.to_offset_int(); } else { state.pc += 1; } @@ -438,17 +502,17 @@ impl Program { rhs, target_pc, } => { - assert!(*target_pc >= 0); + assert!(target_pc.is_offset()); let lhs = *lhs; let rhs = *rhs; let target_pc = *target_pc; match (&state.registers[lhs], &state.registers[rhs]) { (_, OwnedValue::Null) | (OwnedValue::Null, _) => { - state.pc = target_pc; + state.pc = target_pc.to_offset_int(); } _ => { if state.registers[lhs] > state.registers[rhs] { - state.pc = target_pc; + state.pc = target_pc.to_offset_int(); } else { state.pc += 1; } @@ -460,17 +524,17 @@ impl Program { rhs, target_pc, } => { - assert!(*target_pc >= 0); + assert!(target_pc.is_offset()); let lhs = *lhs; let rhs = *rhs; let target_pc = *target_pc; match (&state.registers[lhs], &state.registers[rhs]) { (_, OwnedValue::Null) | (OwnedValue::Null, _) => { - state.pc = target_pc; + state.pc = target_pc.to_offset_int(); } _ => { if state.registers[lhs] >= state.registers[rhs] { - state.pc = target_pc; + state.pc = target_pc.to_offset_int(); } else { state.pc += 1; } @@ -482,9 +546,9 @@ impl Program { target_pc, null_reg, } => { - assert!(*target_pc >= 0); + assert!(target_pc.is_offset()); if exec_if(&state.registers[*reg], &state.registers[*null_reg], false) { - state.pc = *target_pc; + state.pc = target_pc.to_offset_int(); } else { state.pc += 1; } @@ -494,9 +558,9 @@ impl Program { target_pc, null_reg, } => { - assert!(*target_pc >= 0); + assert!(target_pc.is_offset()); if exec_if(&state.registers[*reg], &state.registers[*null_reg], true) { - state.pc = *target_pc; + state.pc = target_pc.to_offset_int(); } else { state.pc += 1; } @@ -539,10 +603,11 @@ impl Program { cursor_id, pc_if_empty, } => { + assert!(pc_if_empty.is_offset()); let cursor = cursors.get_mut(cursor_id).unwrap(); cursor.wait_for_completion()?; if cursor.is_empty() { - state.pc = *pc_if_empty; + state.pc = pc_if_empty.to_offset_int(); } else { state.pc += 1; } @@ -551,10 +616,11 @@ impl Program { cursor_id, pc_if_empty, } => { + assert!(pc_if_empty.is_offset()); let cursor = cursors.get_mut(cursor_id).unwrap(); cursor.wait_for_completion()?; if cursor.is_empty() { - state.pc = *pc_if_empty; + state.pc = pc_if_empty.to_offset_int(); } else { state.pc += 1; } @@ -620,11 +686,11 @@ impl Program { cursor_id, pc_if_next, } => { - assert!(*pc_if_next >= 0); + assert!(pc_if_next.is_offset()); let cursor = cursors.get_mut(cursor_id).unwrap(); cursor.wait_for_completion()?; if !cursor.is_empty() { - state.pc = *pc_if_next; + state.pc = pc_if_next.to_offset_int(); } else { state.pc += 1; } @@ -633,11 +699,11 @@ impl Program { cursor_id, pc_if_next, } => { - assert!(*pc_if_next >= 0); + assert!(pc_if_next.is_offset()); let cursor = cursors.get_mut(cursor_id).unwrap(); cursor.wait_for_completion()?; if !cursor.is_empty() { - state.pc = *pc_if_next; + state.pc = pc_if_next.to_offset_int(); } else { state.pc += 1; } @@ -705,24 +771,22 @@ impl Program { state.pc += 1; } Insn::Goto { target_pc } => { - assert!(*target_pc >= 0); - state.pc = *target_pc; + assert!(target_pc.is_offset()); + state.pc = target_pc.to_offset_int(); } Insn::Gosub { target_pc, return_reg, } => { - assert!(*target_pc >= 0); - state.registers[*return_reg] = OwnedValue::Integer(state.pc + 1); - state.pc = *target_pc; + assert!(target_pc.is_offset()); + state.registers[*return_reg] = OwnedValue::Integer((state.pc + 1) as i64); + state.pc = target_pc.to_offset_int(); } Insn::Return { return_reg } => { if let OwnedValue::Integer(pc) = state.registers[*return_reg] { - if pc < 0 { - return Err(LimboError::InternalError( - "Return register is negative".to_string(), - )); - } + let pc: u32 = pc + .try_into() + .unwrap_or_else(|_| panic!("Return register is negative: {}", pc)); state.pc = pc; } else { return Err(LimboError::InternalError( @@ -779,11 +843,12 @@ impl Program { src_reg, target_pc, } => { + assert!(target_pc.is_offset()); let cursor = cursors.get_mut(cursor_id).unwrap(); let rowid = match &state.registers[*src_reg] { OwnedValue::Integer(rowid) => *rowid as u64, OwnedValue::Null => { - state.pc = *target_pc; + state.pc = target_pc.to_offset_int(); continue; } other => { @@ -794,7 +859,7 @@ impl Program { }; let found = return_if_io!(cursor.seek(SeekKey::TableRowId(rowid), SeekOp::EQ)); if !found { - state.pc = *target_pc; + state.pc = target_pc.to_offset_int(); } else { state.pc += 1; } @@ -813,6 +878,7 @@ impl Program { target_pc, is_index, } => { + assert!(target_pc.is_offset()); if *is_index { let cursor = cursors.get_mut(cursor_id).unwrap(); let record_from_regs: OwnedRecord = @@ -821,7 +887,7 @@ impl Program { cursor.seek(SeekKey::IndexKey(&record_from_regs), SeekOp::GE) ); if !found { - state.pc = *target_pc; + state.pc = target_pc.to_offset_int(); } else { state.pc += 1; } @@ -844,7 +910,7 @@ impl Program { let found = return_if_io!(cursor.seek(SeekKey::TableRowId(rowid), SeekOp::GE)); if !found { - state.pc = *target_pc; + state.pc = target_pc.to_offset_int(); } else { state.pc += 1; } @@ -857,6 +923,7 @@ impl Program { target_pc, is_index, } => { + assert!(target_pc.is_offset()); if *is_index { let cursor = cursors.get_mut(cursor_id).unwrap(); let record_from_regs: OwnedRecord = @@ -865,7 +932,7 @@ impl Program { cursor.seek(SeekKey::IndexKey(&record_from_regs), SeekOp::GT) ); if !found { - state.pc = *target_pc; + state.pc = target_pc.to_offset_int(); } else { state.pc += 1; } @@ -888,7 +955,7 @@ impl Program { let found = return_if_io!(cursor.seek(SeekKey::TableRowId(rowid), SeekOp::GT)); if !found { - state.pc = *target_pc; + state.pc = target_pc.to_offset_int(); } else { state.pc += 1; } @@ -900,7 +967,7 @@ impl Program { num_regs, target_pc, } => { - assert!(*target_pc >= 0); + assert!(target_pc.is_offset()); let cursor = cursors.get_mut(cursor_id).unwrap(); let record_from_regs: OwnedRecord = make_owned_record(&state.registers, start_reg, num_regs); @@ -909,12 +976,12 @@ impl Program { if idx_record.values[..idx_record.values.len() - 1] >= *record_from_regs.values { - state.pc = *target_pc; + state.pc = target_pc.to_offset_int(); } else { state.pc += 1; } } else { - state.pc = *target_pc; + state.pc = target_pc.to_offset_int(); } } Insn::IdxGT { @@ -923,6 +990,7 @@ impl Program { num_regs, target_pc, } => { + assert!(target_pc.is_offset()); let cursor = cursors.get_mut(cursor_id).unwrap(); let record_from_regs: OwnedRecord = make_owned_record(&state.registers, start_reg, num_regs); @@ -931,21 +999,21 @@ impl Program { if idx_record.values[..idx_record.values.len() - 1] > *record_from_regs.values { - state.pc = *target_pc; + state.pc = target_pc.to_offset_int(); } else { state.pc += 1; } } else { - state.pc = *target_pc; + state.pc = target_pc.to_offset_int(); } } Insn::DecrJumpZero { reg, target_pc } => { - assert!(*target_pc >= 0); + assert!(target_pc.is_offset()); match state.registers[*reg] { OwnedValue::Integer(n) => { let n = n - 1; if n == 0 { - state.pc = *target_pc; + state.pc = target_pc.to_offset_int(); } else { state.registers[*reg] = OwnedValue::Integer(n); state.pc += 1; @@ -1250,18 +1318,18 @@ impl Program { cursor.rewind()?; state.pc += 1; } else { - state.pc = *pc_if_empty; + state.pc = pc_if_empty.to_offset_int(); } } Insn::SorterNext { cursor_id, pc_if_next, } => { - assert!(*pc_if_next >= 0); + assert!(pc_if_next.is_offset()); let cursor = cursors.get_mut(cursor_id).unwrap(); return_if_io!(cursor.next()); if !cursor.is_empty() { - state.pc = *pc_if_next; + state.pc = pc_if_next.to_offset_int(); } else { state.pc += 1; } @@ -1313,17 +1381,25 @@ impl Program { } } #[cfg(feature = "json")] - crate::function::Func::Json(JsonFunc::JsonArrayLength) => { + crate::function::Func::Json( + func @ (JsonFunc::JsonArrayLength | JsonFunc::JsonType), + ) => { let json_value = &state.registers[*start_reg]; let path_value = if arg_count > 1 { Some(&state.registers[*start_reg + 1]) } else { None }; - let json_array_length = json_array_length(json_value, path_value); + let func_result = match func { + JsonFunc::JsonArrayLength => { + json_array_length(json_value, path_value) + } + JsonFunc::JsonType => json_type(json_value, path_value), + _ => unreachable!(), + }; - match json_array_length { - Ok(length) => state.registers[*dest] = length, + match func_result { + Ok(result) => state.registers[*dest] = result, Err(e) => return Err(e), } } @@ -1726,18 +1802,23 @@ impl Program { jump_on_definition, start_offset, } => { - assert!(*jump_on_definition >= 0); - state.registers[*yield_reg] = OwnedValue::Integer(*start_offset); + assert!(jump_on_definition.is_offset()); + let start_offset = start_offset.to_offset_int(); + state.registers[*yield_reg] = OwnedValue::Integer(start_offset as i64); state.ended_coroutine.insert(*yield_reg, false); - state.pc = if *jump_on_definition == 0 { + let jump_on_definition = jump_on_definition.to_offset_int(); + state.pc = if jump_on_definition == 0 { state.pc + 1 } else { - *jump_on_definition + jump_on_definition }; } Insn::EndCoroutine { yield_reg } => { if let OwnedValue::Integer(pc) = state.registers[*yield_reg] { state.ended_coroutine.insert(*yield_reg, true); + let pc: u32 = pc + .try_into() + .unwrap_or_else(|_| panic!("EndCoroutine: pc overflow: {}", pc)); state.pc = pc - 1; // yield jump is always next to yield. Here we substract 1 to go back to yield instruction } else { unreachable!(); @@ -1753,12 +1834,15 @@ impl Program { .get(yield_reg) .expect("coroutine not initialized") { - state.pc = *end_offset; + state.pc = end_offset.to_offset_int(); } else { + let pc: u32 = pc + .try_into() + .unwrap_or_else(|_| panic!("Yield: pc overflow: {}", pc)); // swap the program counter with the value in the yield register // this is the mechanism that allows jumping back and forth between the coroutine and the caller (state.pc, state.registers[*yield_reg]) = - (pc, OwnedValue::Integer(state.pc + 1)); + (pc, OwnedValue::Integer((state.pc + 1) as i64)); } } else { unreachable!( @@ -1842,7 +1926,7 @@ impl Program { if exists { state.pc += 1; } else { - state.pc = *target_pc; + state.pc = target_pc.to_offset_int(); } } // this cursor may be reused for next insert @@ -1894,7 +1978,7 @@ impl Program { } Insn::IsNull { src, target_pc } => { if matches!(state.registers[*src], OwnedValue::Null) { - state.pc = *target_pc; + state.pc = target_pc.to_offset_int(); } else { state.pc += 1; } @@ -1976,7 +2060,7 @@ fn trace_insn(program: &Program, addr: InsnReference, insn: &Insn) { addr, insn, String::new(), - program.comments.get(&(addr as BranchOffset)).copied() + program.comments.get(&(addr as u32)).copied() ) ); } @@ -1987,7 +2071,7 @@ fn print_insn(program: &Program, addr: InsnReference, insn: &Insn, indent: Strin addr, insn, indent, - program.comments.get(&(addr as BranchOffset)).copied(), + program.comments.get(&(addr as u32)).copied(), ); println!("{}", s); } diff --git a/testing/json.test b/testing/json.test index 5340f7049..fd2cdb54c 100755 --- a/testing/json.test +++ b/testing/json.test @@ -221,3 +221,51 @@ do_execsql_test json_array_length_via_bad_prop { do_execsql_test json_array_length_nested { SELECT json_array_length('{"one":[[1,2,3],2,3]}', '$.one[0]'); } {{3}} + +do_execsql_test json_type_no_path { + select json_type('{"a":[2,3.5,true,false,null,"x"]}') +} {{object}} + +do_execsql_test json_type_root_path { + select json_type('{"a":[2,3.5,true,false,null,"x"]}','$') +} {{object}} + +do_execsql_test json_type_array { + select json_type('{"a":[2,3.5,true,false,null,"x"]}','$.a') +} {{array}} + +do_execsql_test json_type_integer { + select json_type('{"a":[2,3.5,true,false,null,"x"]}','$.a[0]') +} {{integer}} + +do_execsql_test json_type_real { + select json_type('{"a":[2,3.5,true,false,null,"x"]}','$.a[1]') +} {{real}} + +do_execsql_test json_type_true { + select json_type('{"a":[2,3.5,true,false,null,"x"]}','$.a[2]') +} {{true}} + +do_execsql_test json_type_false { + select json_type('{"a":[2,3.5,true,false,null,"x"]}','$.a[3]') +} {{false}} + +do_execsql_test json_type_null { + select json_type('{"a":[2,3.5,true,false,null,"x"]}','$.a[4]') +} {{null}} + +do_execsql_test json_type_text { + select json_type('{"a":[2,3.5,true,false,null,"x"]}','$.a[5]') +} {{text}} + +do_execsql_test json_type_NULL { + select json_type('{"a":[2,3.5,true,false,null,"x"]}','$.a[6]') +} {{}} + +do_execsql_test json_type_cast { + select json_type(1) +} {{integer}} + +do_execsql_test json_type_null_arg { + select json_type(null) +} {{}} diff --git a/testing/select.test b/testing/select.test index c6d403a6a..49f8021bc 100755 --- a/testing/select.test +++ b/testing/select.test @@ -11,6 +11,14 @@ do_execsql_test select-const-2 { SELECT 2 } {2} +do_execsql_test select-true { + SELECT true +} {1} + +do_execsql_test select-false { + SELECT false +} {0} + do_execsql_test select-text-escape-1 { SELECT '''a' } {'a} @@ -31,6 +39,15 @@ do_execsql_test select-limit-0 { SELECT id FROM users LIMIT 0; } {} +# ORDER BY id here because sqlite uses age_idx here and we (yet) don't so force it to evaluate in ID order +do_execsql_test select-limit-true { + SELECT id FROM users ORDER BY id LIMIT true; +} {1} + +do_execsql_test select-limit-false { + SELECT id FROM users ORDER BY id LIMIT false; +} {} + do_execsql_test realify { select price from products limit 1; } {79.0} diff --git a/testing/where.test b/testing/where.test index 8a568d0fc..c613f784b 100755 --- a/testing/where.test +++ b/testing/where.test @@ -338,3 +338,30 @@ do_execsql_test between-price-range-with-names { AND (name = 'sweatshirt' OR name = 'sneakers'); } {5|sweatshirt|74.0 8|sneakers|82.0} + +do_execsql_test where-between-true-and-2 { + select id from users where id between true and 2; +} {1 +2} + +do_execsql_test nested-parens-conditionals-or-and-or { + SELECT count(*) FROM users WHERE ((age > 25 OR age < 18) AND (city = 'Boston' OR state = 'MA')); +} {146} + +do_execsql_test nested-parens-conditionals-and-or-and { + SELECT * FROM users WHERE (((age > 18 AND city = 'New Mario') OR age = 92) AND city = 'Lake Paul'); +} {{9989|Timothy|Harrison|woodsmichael@example.net|+1-447-830-5123|782 Wright Harbors|Lake Paul|ID|52330|92}} + + +do_execsql_test nested-parens-conditionals-and-double-or { + SELECT * FROM users WHERE ((age > 30 OR age < 20) AND (state = 'NY' OR state = 'CA')) AND first_name glob 'An*' order by id; +} {{1738|Angelica|Pena|jacksonjonathan@example.net|(867)536-1578x039|663 Jacqueline Estate Apt. 652|Clairehaven|NY|64172|74 +1811|Andrew|Mckee|jchen@example.net|359.939.9548|19809 Blair Junction Apt. 438|New Lawrencefort|NY|26240|42 +3773|Andrew|Peterson|cscott@example.com|(405)410-4972x90408|90513 Munoz Radial Apt. 786|Travisfurt|CA|52951|43 +3875|Anthony|Cordova|ocross@example.org|+1-356-999-4070x557|77081 Aguilar Turnpike|Michaelfurt|CA|73353|37 +4909|Andrew|Carson|michelle31@example.net|823.423.1516|78514 Luke Springs|Lake Crystal|CA|49481|74 +5498|Anna|Hall|elizabethheath@example.org|9778473725|5803 Taylor Tunnel|New Nicholaston|NY|21825|14 +6340|Angela|Freeman|juankelly@example.net|501.372.4720|3912 Ricardo Mission|West Nancyville|NY|60823|34 +8171|Andrea|Lee|dgarrison@example.com|001-594-430-0646|452 Anthony Stravenue|Sandraville|CA|28572|12 +9110|Anthony|Barrett|steven05@example.net|(562)928-9177x8454|86166 Foster Inlet Apt. 284|North Jeffreyburgh|CA|80147|97 +9279|Annette|Lynn|joanne37@example.com|(272)700-7181|2676 Laura Points Apt. 683|Tristanville|NY|48646|91}}