diff --git a/CHANGELOG.md b/CHANGELOG.md index fee38e12c..847deec42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -303,7 +303,7 @@ - `ORDER BY` support for nullable sorting columns and qualified identifiers (Jussi Saurio) -- Fix `.schema` command crash in the CLI ([#212](https://github.com/penberg/limbo/issues/212) (Jussi Saurio) +- Fix `.schema` command crash in the CLI ([#212](https://github.com/tursodatabase/limbo/issues/212) (Jussi Saurio) ## 0.0.2 - 2024-07-24 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cad33a8db..ac4566c00 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -63,7 +63,7 @@ cargo bench --bench benchmark -- --profile-time=5 ## Finding things to work on -The issue tracker has issues tagged with [good first issue](https://github.com/penberg/limbo/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22), +The issue tracker has issues tagged with [good first issue](https://github.com/tursodatabase/limbo/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22), which are considered to be things to work on to get going. If you're interested in working on one of them, comment on the issue tracker, and we're happy to help you get going. ## Submitting your work diff --git a/Cargo.lock b/Cargo.lock index 819311184..a57d30b7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -399,29 +399,6 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" -[[package]] -name = "cli-table" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b53f9241f288a7b12c56565f04aaeaeeab6b8923d42d99255d4ca428b4d97f89" -dependencies = [ - "cli-table-derive", - "csv", - "termcolor", - "unicode-width", -] - -[[package]] -name = "cli-table-derive" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e83a93253aaae7c74eb7428ce4faa6e219ba94886908048888701819f82fb94" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "clipboard-win" version = "4.5.0" @@ -449,6 +426,17 @@ dependencies = [ "memchr", ] +[[package]] +name = "comfy-table" +version = "7.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a65ebfec4fb190b6f90e944a817d60499ee0744e582530e2c9900a22e591d9a" +dependencies = [ + "crossterm", + "unicode-segmentation", + "unicode-width 0.2.0", +] + [[package]] name = "comma" version = "1.0.0" @@ -492,8 +480,6 @@ version = "0.0.14" dependencies = [ "anyhow", "assert_cmd", - "clap", - "dirs", "env_logger 0.10.2", "limbo_core", "log", @@ -501,7 +487,6 @@ dependencies = [ "rand_chacha 0.9.0", "rexpect", "rusqlite", - "rustyline", "tempfile", ] @@ -595,6 +580,28 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags 2.8.0", + "crossterm_winapi", + "parking_lot", + "rustix", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crunchy" version = "0.2.2" @@ -711,12 +718,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" -[[package]] -name = "downcast" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" - [[package]] name = "either" version = "1.13.0" @@ -891,12 +892,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fragile" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" - [[package]] name = "fsevent-sys" version = "4.1.0" @@ -1564,7 +1559,7 @@ version = "0.0.14" dependencies = [ "anyhow", "clap", - "cli-table", + "comfy-table", "csv", "ctrlc", "dirs", @@ -1607,7 +1602,6 @@ name = "limbo_core" version = "0.0.14" dependencies = [ "built", - "bumpalo", "cfg_block", "chrono", "criterion", @@ -1632,7 +1626,6 @@ dependencies = [ "log", "miette", "mimalloc", - "mockall", "parking_lot", "pest", "pest_derive", @@ -1647,7 +1640,6 @@ dependencies = [ "rusqlite", "rustix", "serde", - "sieve-cache", "sqlite3-parser", "strum", "tempfile", @@ -1673,7 +1665,6 @@ name = "limbo_ext" version = "0.0.14" dependencies = [ "limbo_macros", - "log", ] [[package]] @@ -1707,7 +1698,6 @@ name = "limbo_regexp" version = "0.0.14" dependencies = [ "limbo_ext", - "log", "mimalloc", "regex", ] @@ -1717,7 +1707,6 @@ name = "limbo_series" version = "0.0.14" dependencies = [ "limbo_ext", - "log", "mimalloc", "quickcheck", "quickcheck_macros", @@ -1848,7 +1837,7 @@ dependencies = [ "terminal_size", "textwrap", "thiserror 1.0.69", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] @@ -1898,32 +1887,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "mockall" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2" -dependencies = [ - "cfg-if", - "downcast", - "fragile", - "mockall_derive", - "predicates", - "predicates-tree", -] - -[[package]] -name = "mockall_derive" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898" -dependencies = [ - "cfg-if", - "proc-macro2", - "quote", - "syn 2.0.96", -] - [[package]] name = "nibble_vec" version = "0.1.0" @@ -2704,7 +2667,7 @@ dependencies = [ "radix_trie", "scopeguard", "unicode-segmentation", - "unicode-width", + "unicode-width 0.1.14", "utf8parse", "winapi", ] @@ -2786,12 +2749,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "sieve-cache" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51bf3a9dccf2c079bf1465d449a485c85b36443caf765f2f127bfec28b180f75" - [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -3033,7 +2990,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" dependencies = [ "unicode-linebreak", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] @@ -3201,6 +3158,12 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + [[package]] name = "unindent" version = "0.2.3" diff --git a/bindings/go/Cargo.toml b/bindings/go/Cargo.toml index b73902c5f..e268d0a27 100644 --- a/bindings/go/Cargo.toml +++ b/bindings/go/Cargo.toml @@ -17,7 +17,7 @@ io_uring = ["limbo_core/io_uring"] [dependencies] -limbo_core = { path = "../../core/" } +limbo_core = { path = "../../core" } [target.'cfg(target_os = "linux")'.dependencies] -limbo_core = { path = "../../core/", features = ["io_uring"] } +limbo_core = { path = "../../core", features = ["io_uring"] } diff --git a/bindings/java/Cargo.toml b/bindings/java/Cargo.toml index 9b78b1597..fd38d1e65 100644 --- a/bindings/java/Cargo.toml +++ b/bindings/java/Cargo.toml @@ -12,6 +12,6 @@ crate-type = ["cdylib"] path = "rs_src/lib.rs" [dependencies] -limbo_core = { path = "../../core" } +limbo_core = { path = "../../core", features = ["io_uring"] } jni = "0.21.1" thiserror = "2.0.9" diff --git a/bindings/java/rs_src/limbo_db.rs b/bindings/java/rs_src/limbo_db.rs index 16cb3d66b..65f6dafc0 100644 --- a/bindings/java/rs_src/limbo_db.rs +++ b/bindings/java/rs_src/limbo_db.rs @@ -21,7 +21,6 @@ impl LimboDB { Box::into_raw(Box::new(self)) as jlong } - #[allow(dead_code)] pub fn drop(ptr: jlong) { let _boxed = unsafe { Box::from_raw(ptr as *mut LimboDB) }; } @@ -97,6 +96,15 @@ pub extern "system" fn Java_org_github_tursodatabase_core_LimboDB_connect0<'loca conn.to_ptr() } +#[no_mangle] +pub extern "system" fn Java_org_github_tursodatabase_core_LimboDB_close0<'local>( + _env: JNIEnv<'local>, + _obj: JObject<'local>, + db_pointer: jlong, +) { + LimboDB::drop(db_pointer); +} + #[no_mangle] pub extern "system" fn Java_org_github_tursodatabase_core_LimboDB_throwJavaException<'local>( mut env: JNIEnv<'local>, diff --git a/bindings/java/src/main/java/org/github/tursodatabase/core/AbstractDB.java b/bindings/java/src/main/java/org/github/tursodatabase/core/AbstractDB.java deleted file mode 100644 index 4906acd9c..000000000 --- a/bindings/java/src/main/java/org/github/tursodatabase/core/AbstractDB.java +++ /dev/null @@ -1,75 +0,0 @@ -package org.github.tursodatabase.core; - -import java.sql.SQLException; -import java.sql.SQLFeatureNotSupportedException; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * Interface to Limbo. It provides some helper functions used by other parts of the driver. The goal - * of the helper functions here are not only to provide functionality, but to handle contractual - * differences between the JDBC specification and the Limbo API. - */ -public abstract class AbstractDB { - protected final String url; - protected final String filePath; - private final AtomicBoolean closed = new AtomicBoolean(true); - - public AbstractDB(String url, String filePath) { - this.url = url; - this.filePath = filePath; - } - - public boolean isClosed() { - return closed.get(); - } - - /** Aborts any pending operation and returns at its earliest opportunity. */ - public abstract void interrupt() throws SQLException; - - /** - * Creates an SQLite interface to a database for the given connection. - * - * @param openFlags Flags for opening the database. - * @throws SQLException if a database access error occurs. - */ - public final synchronized void open(int openFlags) throws SQLException { - open0(filePath, openFlags); - } - - protected abstract void open0(String fileName, int openFlags) throws SQLException; - - /** - * Closes a database connection and finalizes any remaining statements before the closing - * operation. - * - * @throws SQLException if a database access error occurs. - */ - public final synchronized void close() throws SQLException { - // TODO: add implementation - throw new SQLFeatureNotSupportedException(); - } - - /** - * Connects to a database. - * - * @return Pointer to the connection. - */ - public abstract long connect() throws SQLException; - - /** - * Creates an SQLite interface to a database with the provided open flags. - * - * @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 long openUtf8(byte[] fileName, int openFlags) throws SQLException; - - /** - * Closes the SQLite interface to a database. - * - * @throws SQLException if a database access error occurs. - */ - protected abstract void close0() throws SQLException; -} diff --git a/bindings/java/src/main/java/org/github/tursodatabase/core/LimboConnection.java b/bindings/java/src/main/java/org/github/tursodatabase/core/LimboConnection.java index 597029ce9..c87706c6e 100644 --- a/bindings/java/src/main/java/org/github/tursodatabase/core/LimboConnection.java +++ b/bindings/java/src/main/java/org/github/tursodatabase/core/LimboConnection.java @@ -14,7 +14,7 @@ public class LimboConnection { private static final Logger logger = LoggerFactory.getLogger(LimboConnection.class); private final long connectionPtr; - private final AbstractDB database; + private final LimboDB database; private boolean closed; public LimboConnection(String url, String filePath) throws SQLException { @@ -32,7 +32,7 @@ public class LimboConnection { this.connectionPtr = this.database.connect(); } - private static AbstractDB open(String url, String filePath, Properties properties) + private static LimboDB open(String url, String filePath, Properties properties) throws SQLException { return LimboDBFactory.open(url, filePath, properties); } @@ -55,7 +55,7 @@ public class LimboConnection { return closed; } - public AbstractDB getDatabase() { + public LimboDB getDatabase() { return 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 e1d619239..64b005d35 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 @@ -7,7 +7,6 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.sql.SQLException; -import java.util.concurrent.locks.ReentrantLock; import org.github.tursodatabase.LimboErrorCode; import org.github.tursodatabase.annotations.NativeInvocation; import org.github.tursodatabase.annotations.VisibleForTesting; @@ -16,14 +15,15 @@ import org.github.tursodatabase.utils.Logger; import org.github.tursodatabase.utils.LoggerFactory; /** This class provides a thin JNI layer over the SQLite3 C API. */ -public final class LimboDB extends AbstractDB { +public final class LimboDB implements AutoCloseable { private static final Logger logger = LoggerFactory.getLogger(LimboDB.class); // Pointer to database instance private long dbPointer; private boolean isOpen; + private final String url; + private final String filePath; private static boolean isLoaded; - private ReentrantLock dbLock = new ReentrantLock(); static { if ("The Android Project".equals(System.getProperty("java.vm.vendor"))) { @@ -176,23 +176,26 @@ public final class LimboDB extends AbstractDB { // TODO: receive config as argument private LimboDB(String url, String filePath) { - super(url, filePath); + this.url = url; + this.filePath = filePath; } // TODO: add support for JNI - @Override - protected native long openUtf8(byte[] file, int openFlags) throws SQLException; - - // TODO: add support for JNI - @Override - protected native void close0() throws SQLException; - - // TODO: add support for JNI - @Override public native void interrupt(); - @Override - protected void open0(String filePath, int openFlags) throws SQLException { + public boolean isClosed() { + return !this.isOpen; + } + + public boolean isOpen() { + return this.isOpen; + } + + public void open(int openFlags) throws SQLException { + open0(filePath, openFlags); + } + + private void open0(String filePath, int openFlags) throws SQLException { if (isOpen) { throw LimboExceptionUtils.buildLimboException( LimboErrorCode.LIMBO_ETC.code, "Already opened"); @@ -209,13 +212,24 @@ public final class LimboDB extends AbstractDB { isOpen = true; } - @Override + private native long openUtf8(byte[] file, int openFlags) throws SQLException; + public long connect() throws SQLException { return connect0(dbPointer); } private native long connect0(long databasePtr) throws SQLException; + @Override + public void close() throws Exception { + if (!isOpen) return; + + close0(dbPointer); + isOpen = false; + } + + private native void close0(long databasePtr) throws SQLException; + @VisibleForTesting native void throwJavaException(int errorCode) throws SQLException; diff --git a/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4Connection.java b/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4Connection.java index aac34e755..2b734c2df 100644 --- a/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4Connection.java +++ b/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4Connection.java @@ -13,6 +13,8 @@ public class JDBC4Connection implements Connection { private final LimboConnection connection; + private Map> typeMap = new HashMap<>(); + public JDBC4Connection(String url, String filePath) throws SQLException { this.connection = new LimboConnection(url, filePath); } @@ -47,22 +49,8 @@ public class JDBC4Connection implements Connection { } @Override - public PreparedStatement prepareStatement(String sql) throws SQLException { - return new JDBC4PreparedStatement(this, sql); - } - - @Override - @SkipNullableCheck - public CallableStatement prepareCall(String sql) throws SQLException { - // TODO - return null; - } - - @Override - @SkipNullableCheck public String nativeSQL(String sql) throws SQLException { - // TODO - return ""; + return sql; } @Override @@ -115,13 +103,10 @@ public class JDBC4Connection implements Connection { } @Override - public void setCatalog(String catalog) throws SQLException { - // TODO - } + public void setCatalog(String catalog) throws SQLException {} @Override public String getCatalog() throws SQLException { - // TODO return ""; } @@ -148,41 +133,30 @@ public class JDBC4Connection implements Connection { // TODO } - @Override - @SkipNullableCheck - public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) - throws SQLException { - // TODO - return null; - } - - @Override - @SkipNullableCheck - public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) - throws SQLException { - // TODO - return null; - } - @Override public Map> getTypeMap() throws SQLException { - // TODO - return new HashMap<>(); + return this.typeMap; } @Override public void setTypeMap(Map> map) throws SQLException { - // TODO - } - - @Override - public void setHoldability(int holdability) throws SQLException { - // TODO + synchronized (this) { + this.typeMap = map; + } } @Override public int getHoldability() throws SQLException { - return 0; + checkOpen(); + return ResultSet.CLOSE_CURSORS_AT_COMMIT; + } + + @Override + public void setHoldability(int holdability) throws SQLException { + checkOpen(); + if (holdability != ResultSet.CLOSE_CURSORS_AT_COMMIT) { + throw new SQLException("Limbo only supports CLOSE_CURSORS_AT_COMMIT"); + } } @Override @@ -210,76 +184,95 @@ public class JDBC4Connection implements Connection { } @Override - @SkipNullableCheck - public PreparedStatement prepareStatement( - String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) - throws SQLException { - // TODO - return null; + public CallableStatement prepareCall(String sql) throws SQLException { + return prepareCall( + sql, + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, + ResultSet.CLOSE_CURSORS_AT_COMMIT); + } + + @Override + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) + throws SQLException { + return prepareCall(sql, resultSetType, resultSetConcurrency, ResultSet.CLOSE_CURSORS_AT_COMMIT); } @Override - @SkipNullableCheck public CallableStatement prepareCall( String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { - // TODO - return null; + throw new SQLException("Limbo does not support stored procedures"); + } + + @Override + public PreparedStatement prepareStatement(String sql) throws SQLException { + return prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + } + + @Override + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) + throws SQLException { + return prepareStatement( + sql, resultSetType, resultSetConcurrency, ResultSet.CLOSE_CURSORS_AT_COMMIT); + } + + @Override + public PreparedStatement prepareStatement( + String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) + throws SQLException { + checkOpen(); + checkCursor(resultSetType, resultSetConcurrency, resultSetHoldability); + return new JDBC4PreparedStatement(this, sql); } @Override - @SkipNullableCheck public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { - // TODO - return null; + return prepareStatement(sql); } @Override - @SkipNullableCheck public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { - // TODO - return null; + // TODO: maybe we can enhance this functionality by using columnIndexes + return prepareStatement(sql); } @Override - @SkipNullableCheck public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { - // TODO - return null; + // TODO: maybe we can enhance this functionality by using columnNames + return prepareStatement(sql); } @Override - @SkipNullableCheck public Clob createClob() throws SQLException { - // TODO - return null; + throw new SQLFeatureNotSupportedException("createClob not supported"); } @Override - @SkipNullableCheck public Blob createBlob() throws SQLException { - // TODO - return null; + throw new SQLFeatureNotSupportedException("createBlob not supported"); } @Override - @SkipNullableCheck public NClob createNClob() throws SQLException { - // TODO - return null; + throw new SQLFeatureNotSupportedException("createNClob not supported"); } @Override @SkipNullableCheck public SQLXML createSQLXML() throws SQLException { - // TODO - return null; + throw new SQLFeatureNotSupportedException("createSQLXML not supported"); } @Override public boolean isValid(int timeout) throws SQLException { - // TODO - return false; + if (isClosed()) { + return false; + } + + try (Statement statement = createStatement()) { + return statement.execute("select 1;"); + } } @Override @@ -333,7 +326,11 @@ public class JDBC4Connection implements Connection { @Override public void abort(Executor executor) throws SQLException { - // TODO + if (isClosed()) { + return; + } + + close(); } @Override diff --git a/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4PreparedStatement.java b/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4PreparedStatement.java index fa487f014..b04ed71c4 100644 --- a/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4PreparedStatement.java +++ b/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4PreparedStatement.java @@ -23,6 +23,7 @@ import java.sql.Time; import java.sql.Timestamp; import java.util.Calendar; import org.github.tursodatabase.annotations.SkipNullableCheck; +import org.github.tursodatabase.core.LimboResultSet; public class JDBC4PreparedStatement extends JDBC4Statement implements PreparedStatement { @@ -45,7 +46,11 @@ public class JDBC4PreparedStatement extends JDBC4Statement implements PreparedSt @Override public int executeUpdate() throws SQLException { - // TODO + requireNonNull(this.statement); + final LimboResultSet resultSet = statement.getResultSet(); + resultSet.consumeAll(); + + // TODO: return updated count return 0; } diff --git a/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4Statement.java b/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4Statement.java index 260b7cbcb..fa11544bc 100644 --- a/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4Statement.java +++ b/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4Statement.java @@ -252,10 +252,8 @@ public class JDBC4Statement implements Statement { } @Override - @SkipNullableCheck - public Connection getConnection() throws SQLException { - // TODO - return null; + public Connection getConnection() { + return connection; } @Override @@ -273,32 +271,32 @@ public class JDBC4Statement implements Statement { @Override public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException { - // TODO - return 0; + // TODO: enhance + return executeUpdate(sql); } @Override public int executeUpdate(String sql, int[] columnIndexes) throws SQLException { - // TODO - return 0; + // TODO: enhance + return executeUpdate(sql); } @Override public int executeUpdate(String sql, String[] columnNames) throws SQLException { - // TODO - return 0; + // TODO: enhance + return executeUpdate(sql); } @Override public boolean execute(String sql, int autoGeneratedKeys) throws SQLException { - // TODO - return false; + // TODO: enhance + return execute(sql); } @Override public boolean execute(String sql, int[] columnIndexes) throws SQLException { - // TODO - return false; + // TODO: enhance + return execute(sql); } @Override 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 index ca75ac4c7..e2fcc8204 100644 --- a/bindings/java/src/test/java/org/github/tursodatabase/core/LimboDBTest.java +++ b/bindings/java/src/test/java/org/github/tursodatabase/core/LimboDBTest.java @@ -2,6 +2,7 @@ package org.github.tursodatabase.core; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertFalse; import java.sql.SQLException; import org.github.tursodatabase.LimboErrorCode; @@ -13,16 +14,27 @@ public class LimboDBTest { @Test void db_should_open_normally() throws Exception { - String dbPath = TestUtils.createTempFile(); LimboDB.load(); + String dbPath = TestUtils.createTempFile(); LimboDB db = LimboDB.create("jdbc:sqlite" + dbPath, dbPath); db.open(0); } @Test - void should_throw_exception_when_opened_twice() throws Exception { - String dbPath = TestUtils.createTempFile(); + void db_should_close_normally() throws Exception { LimboDB.load(); + String dbPath = TestUtils.createTempFile(); + LimboDB db = LimboDB.create("jdbc:sqlite" + dbPath, dbPath); + db.open(0); + db.close(); + + assertFalse(db.isOpen()); + } + + @Test + void should_throw_exception_when_opened_twice() throws Exception { + LimboDB.load(); + String dbPath = TestUtils.createTempFile(); LimboDB db = LimboDB.create("jdbc:sqlite:" + dbPath, dbPath); db.open(0); @@ -31,8 +43,8 @@ public class LimboDBTest { @Test void throwJavaException_should_throw_appropriate_java_exception() throws Exception { - String dbPath = TestUtils.createTempFile(); LimboDB.load(); + String dbPath = TestUtils.createTempFile(); LimboDB db = LimboDB.create("jdbc:sqlite:" + dbPath, dbPath); final int limboExceptionCode = LimboErrorCode.LIMBO_ETC.code; diff --git a/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ConnectionTest.java b/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ConnectionTest.java index 1bc4fb526..6302bc899 100644 --- a/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ConnectionTest.java +++ b/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ConnectionTest.java @@ -84,4 +84,15 @@ class JDBC4ConnectionTest { connection.createStatement( ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, -1)); } + + @Test + void isValid_should_return_true_on_open_connection() throws SQLException { + assertTrue(connection.isValid(10)); + } + + @Test + void isValid_should_return_false_on_closed_connection() throws SQLException { + connection.close(); + assertFalse(connection.isValid(10)); + } } diff --git a/bindings/python/Cargo.toml b/bindings/python/Cargo.toml index 6157fa579..43639f94c 100644 --- a/bindings/python/Cargo.toml +++ b/bindings/python/Cargo.toml @@ -16,7 +16,7 @@ extension-module = ["pyo3/extension-module"] [dependencies] anyhow = "1.0" -limbo_core = { path = "../../core" } +limbo_core = { path = "../../core", features = ["io_uring"] } pyo3 = { version = "0.22.4", features = ["anyhow"] } [build-dependencies] diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml index 3dd269b0e..4ed6066ce 100644 --- a/bindings/python/pyproject.toml +++ b/bindings/python/pyproject.toml @@ -40,8 +40,8 @@ dev = [ ] [project.urls] -Homepage = "https://github.com/penberg/limbo" -Source = "https://github.com/penberg/limbo" +Homepage = "https://github.com/tursodatabase/limbo" +Source = "https://github.com/tursodatabase/limbo" [tool.maturin] bindings = 'pyo3' diff --git a/bindings/rust/Cargo.toml b/bindings/rust/Cargo.toml index bdf101c56..a5c3a3829 100644 --- a/bindings/rust/Cargo.toml +++ b/bindings/rust/Cargo.toml @@ -9,7 +9,7 @@ license.workspace = true repository.workspace = true [dependencies] -limbo_core = { path = "../../core" } +limbo_core = { path = "../../core", features = ["io_uring"] } thiserror = "2.0.9" [dev-dependencies] diff --git a/bindings/wasm/package.json b/bindings/wasm/package.json index 2265a7799..d7c799bb9 100644 --- a/bindings/wasm/package.json +++ b/bindings/wasm/package.json @@ -7,7 +7,7 @@ "license": "MIT", "repository": { "type": "git", - "url": "https://github.com/penberg/limbo" + "url": "https://github.com/tursodatabase/limbo" }, "type": "module", "main": "./node/dist/index.cjs", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 1886627e4..d73256e93 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -21,7 +21,7 @@ path = "main.rs" [dependencies] anyhow = "1.0.75" clap = { version = "4.5", features = ["derive"] } -cli-table = "0.4.7" +comfy-table = "7.1.4" dirs = "5.0.1" env_logger = "0.10.1" limbo_core = { path = "../core" } @@ -31,4 +31,5 @@ csv = "1.3.1" miette = { version = "7.4.0", features = ["fancy"] } [features] +default = ["io_uring"] io_uring = ["limbo_core/io_uring"] diff --git a/cli/app.rs b/cli/app.rs index 7fa7b70f0..45dd14896 100644 --- a/cli/app.rs +++ b/cli/app.rs @@ -2,8 +2,7 @@ use crate::{ import::{ImportFile, IMPORT_HELP}, opcodes_dictionary::OPCODE_DESCRIPTIONS, }; -use cli_table::format::{Border, HorizontalLine, Separator, VerticalLine}; -use cli_table::{Cell, Style, Table}; +use comfy_table::{Attribute, Cell, CellAlignment, ContentArrangement, Row, Table}; use limbo_core::{Database, LimboError, Statement, StepResult, Value}; use clap::{Parser, ValueEnum}; @@ -670,35 +669,42 @@ impl Limbo { println!("Query interrupted."); return Ok(()); } - let mut table_rows: Vec> = vec![]; + let mut table = Table::new(); + table + .set_content_arrangement(ContentArrangement::Dynamic) + .set_truncation_indicator("…") + .apply_modifier("││──├─┼┤│─┼├┤┬┴┌┐└┘"); if rows.num_columns() > 0 { - let columns = (0..rows.num_columns()) + let header = (0..rows.num_columns()) .map(|i| { - rows.get_column_name(i) - .map(|name| name.cell().bold(true)) - .unwrap_or_else(|| " ".cell()) + let name = rows.get_column_name(i).cloned().unwrap_or_default(); + Cell::new(name).add_attribute(Attribute::Bold) }) .collect::>(); - table_rows.push(columns); + table.set_header(header); } loop { match rows.step() { Ok(StepResult::Row) => { - let row = rows.row().unwrap(); - table_rows.push( - row.values - .iter() - .map(|value| match value.to_value() { - Value::Null => self.opts.null_value.clone().cell(), - Value::Integer(i) => i.to_string().cell(), - Value::Float(f) => f.to_string().cell(), - Value::Text(s) => s.cell(), - Value::Blob(b) => { - format!("{}", String::from_utf8_lossy(b)).cell() - } - }) - .collect(), - ); + let record = rows.row().unwrap(); + let mut row = Row::new(); + row.max_height(1); + for value in &record.values { + let (content, alignment) = match value.to_value() { + Value::Null => { + (self.opts.null_value.clone(), CellAlignment::Left) + } + Value::Integer(i) => (i.to_string(), CellAlignment::Right), + Value::Float(f) => (f.to_string(), CellAlignment::Right), + Value::Text(s) => (s.to_string(), CellAlignment::Left), + Value::Blob(b) => ( + String::from_utf8_lossy(b).to_string(), + CellAlignment::Left, + ), + }; + row.add_cell(Cell::new(content).set_alignment(alignment)); + } + table.add_row(row); } Ok(StepResult::IO) => { self.io.run_once()?; @@ -718,7 +724,10 @@ impl Limbo { } } } - self.print_table(table_rows); + + if table.header().is_some() { + let _ = self.write_fmt(format_args!("{}", table)); + } } }, Ok(None) => {} @@ -734,40 +743,6 @@ impl Limbo { Ok(()) } - fn print_table(&mut self, table_rows: Vec>) { - if table_rows.is_empty() { - return; - } - - let horizontal_line = HorizontalLine::new('┌', '┐', '┬', '─'); - let horizontal_line_mid = HorizontalLine::new('├', '┤', '┼', '─'); - let horizontal_line_bottom = HorizontalLine::new('└', '┘', '┴', '─'); - let vertical_line = VerticalLine::new('│'); - - let border = Border::builder() - .top(horizontal_line) - .bottom(horizontal_line_bottom) - .left(vertical_line.clone()) - .right(vertical_line.clone()) - .build(); - - let separator = Separator::builder() - .column(Some(vertical_line)) - .row(Some(horizontal_line_mid)) - .build(); - - if let Ok(table) = table_rows - .table() - .border(border) - .separator(separator) - .display() - { - let _ = self.write_fmt(format_args!("{}", table)); - } else { - let _ = self.writeln("Error displaying table."); - } - } - fn display_schema(&mut self, table: Option<&str>) -> anyhow::Result<()> { let sql = match table { Some(table_name) => format!( diff --git a/core/Cargo.toml b/core/Cargo.toml index 687f4ff19..933654d78 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -14,7 +14,7 @@ name = "limbo_core" path = "lib.rs" [features] -default = ["fs", "json", "uuid", "io_uring", "time"] +default = ["fs", "json", "uuid", "time"] fs = [] json = [ "dep:jsonb", @@ -47,7 +47,6 @@ fallible-iterator = "0.3.0" hex = "0.4.3" libc = "0.2.155" log = "0.4.20" -sieve-cache = "0.1.4" sqlite3-parser = { path = "../vendored/sqlite3-parser" } thiserror = "1.0.61" getrandom = { version = "0.2.15", features = ["js"] } @@ -61,7 +60,6 @@ serde = { version = "1.0", features = ["derive"] } pest = { version = "2.0", optional = true } pest_derive = { version = "2.0", optional = true } rand = "0.8.5" -bumpalo = { version = "3.16.0", features = ["collections", "boxed"] } limbo_macros = { path = "../macros" } limbo_uuid = { path = "../extensions/uuid", optional = true, features = ["static"] } limbo_regexp = { path = "../extensions/regexp", optional = true, features = ["static"] } @@ -88,7 +86,6 @@ criterion = { version = "0.5", features = [ "async", "async_futures", ] } -mockall = "0.13.0" rstest = "0.18.2" rusqlite = "0.29.0" tempfile = "3.8.0" diff --git a/core/benches/benchmark.rs b/core/benches/benchmark.rs index 21b75424d..d2aef982b 100644 --- a/core/benches/benchmark.rs +++ b/core/benches/benchmark.rs @@ -12,7 +12,7 @@ fn rusqlite_open() -> rusqlite::Connection { } fn bench(criterion: &mut Criterion) { - // https://github.com/penberg/limbo/issues/174 + // https://github.com/tursodatabase/limbo/issues/174 // The rusqlite benchmark crashes on Mac M1 when using the flamegraph features let enable_rusqlite = std::env::var("DISABLE_RUSQLITE_BENCHMARK").is_err(); diff --git a/core/ext/mod.rs b/core/ext/mod.rs index 67fd78491..8ac0bdcc2 100644 --- a/core/ext/mod.rs +++ b/core/ext/mod.rs @@ -127,7 +127,7 @@ impl Database { let Stmt::CreateTable { body, .. } = stmt else { return ResultCode::Error; }; - let Ok(columns) = columns_from_create_table_body(body) else { + let Ok(columns) = columns_from_create_table_body(*body) else { return ResultCode::Error; }; let vtab_module = self.vtab_modules.get(name).unwrap().clone(); @@ -153,6 +153,7 @@ impl Database { } pub fn register_builtins(&self) -> Result<(), String> { + #[allow(unused_variables)] let ext_api = self.build_limbo_ext(); #[cfg(feature = "uuid")] if unsafe { !limbo_uuid::register_extension_static(&ext_api).is_ok() } { diff --git a/core/io/mod.rs b/core/io/mod.rs index f88a5d554..50b4551bf 100644 --- a/core/io/mod.rs +++ b/core/io/mod.rs @@ -166,14 +166,17 @@ impl Buffer { cfg_block! { #[cfg(all(target_os = "linux", feature = "io_uring"))] { mod io_uring; + #[cfg(feature = "fs")] pub use io_uring::UringIO; mod unix; + #[cfg(feature = "fs")] pub use unix::UnixIO; pub use io_uring::UringIO as PlatformIO; } #[cfg(any(all(target_os = "linux",not(feature = "io_uring")), target_os = "macos"))] { mod unix; + #[cfg(feature = "fs")] pub use unix::UnixIO; pub use unix::UnixIO as PlatformIO; } diff --git a/core/io/unix.rs b/core/io/unix.rs index effd94bf5..d06b71770 100644 --- a/core/io/unix.rs +++ b/core/io/unix.rs @@ -22,6 +22,7 @@ pub struct UnixIO { } impl UnixIO { + #[cfg(feature = "fs")] pub fn new() -> Result { debug!("Using IO backend 'syscall'"); Ok(Self { diff --git a/core/schema.rs b/core/schema.rs index f4a6aee2b..aa38cd191 100644 --- a/core/schema.rs +++ b/core/schema.rs @@ -150,7 +150,7 @@ impl BTreeTable { let cmd = parser.next()?; match cmd { Some(Cmd::Stmt(Stmt::CreateTable { tbl_name, body, .. })) => { - create_table(tbl_name, body, root_page) + create_table(tbl_name, *body, root_page) } _ => todo!("Expected CREATE TABLE statement"), } diff --git a/core/storage/database.rs b/core/storage/database.rs index e59519f38..7e30c9215 100644 --- a/core/storage/database.rs +++ b/core/storage/database.rs @@ -1,4 +1,6 @@ -use crate::{error::LimboError, io::Completion, Buffer, Result}; +#[cfg(feature = "fs")] +use crate::error::LimboError; +use crate::{io::Completion, Buffer, Result}; use std::{cell::RefCell, rc::Rc}; /// DatabaseStorage is an interface a database file that consists of pages. diff --git a/core/translate/delete.rs b/core/translate/delete.rs index ffad33d73..b5ce85fdc 100644 --- a/core/translate/delete.rs +++ b/core/translate/delete.rs @@ -13,7 +13,7 @@ pub fn translate_delete( query_mode: QueryMode, schema: &Schema, tbl_name: &QualifiedName, - where_clause: Option, + where_clause: Option>, limit: Option>, syms: &SymbolTable, ) -> Result { @@ -35,7 +35,7 @@ pub fn translate_delete( pub fn prepare_delete_plan( schema: &Schema, tbl_name: &QualifiedName, - where_clause: Option, + where_clause: Option>, limit: Option>, ) -> Result { let table = match schema.get_table(tbl_name.name.0.as_str()) { @@ -53,7 +53,12 @@ pub fn prepare_delete_plan( let mut where_predicates = vec![]; // Parse the WHERE clause - parse_where(where_clause, &table_references, None, &mut where_predicates)?; + parse_where( + where_clause.map(|e| *e), + &table_references, + None, + &mut where_predicates, + )?; // Parse the LIMIT/OFFSET clause let (resolved_limit, resolved_offset) = limit.map_or(Ok((None, None)), |l| parse_limit(*l))?; diff --git a/core/translate/expr.rs b/core/translate/expr.rs index 8ddb580df..36acfa935 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -159,6 +159,7 @@ macro_rules! expect_arguments_min { }}; } +#[allow(unused_macros)] macro_rules! expect_arguments_even { ( $args:expr, diff --git a/core/translate/mod.rs b/core/translate/mod.rs index 79791866b..7ff780e2b 100644 --- a/core/translate/mod.rs +++ b/core/translate/mod.rs @@ -34,6 +34,7 @@ use crate::{bail_parse_error, Connection, LimboError, Result, SymbolTable}; use insert::translate_insert; use select::translate_select; use sqlite3_parser::ast::{self, fmt::ToTokens}; +use sqlite3_parser::ast::{Delete, Insert}; use std::cell::RefCell; use std::fmt::Display; use std::rc::{Rc, Weak}; @@ -51,7 +52,7 @@ pub fn translate( let mut change_cnt_on = false; let program = match stmt { - ast::Stmt::AlterTable(_, _) => bail_parse_error!("ALTER TABLE not supported yet"), + ast::Stmt::AlterTable(_) => bail_parse_error!("ALTER TABLE not supported yet"), ast::Stmt::Analyze(_) => bail_parse_error!("ANALYZE not supported yet"), ast::Stmt::Attach { .. } => bail_parse_error!("ATTACH not supported yet"), ast::Stmt::Begin(_, _) => bail_parse_error!("BEGIN not supported yet"), @@ -67,19 +68,20 @@ pub fn translate( bail_parse_error!("TEMPORARY table not supported yet"); } - translate_create_table(query_mode, tbl_name, body, if_not_exists, schema)? + translate_create_table(query_mode, tbl_name, *body, if_not_exists, schema)? } ast::Stmt::CreateTrigger { .. } => bail_parse_error!("CREATE TRIGGER not supported yet"), ast::Stmt::CreateView { .. } => bail_parse_error!("CREATE VIEW not supported yet"), ast::Stmt::CreateVirtualTable { .. } => { bail_parse_error!("CREATE VIRTUAL TABLE not supported yet") } - ast::Stmt::Delete { - tbl_name, - where_clause, - limit, - .. - } => { + ast::Stmt::Delete(delete) => { + let Delete { + tbl_name, + where_clause, + limit, + .. + } = *delete; change_cnt_on = true; translate_delete(query_mode, schema, &tbl_name, where_clause, limit, syms)? } @@ -92,7 +94,7 @@ pub fn translate( query_mode, &schema, &name, - body, + body.map(|b| *b), database_header.clone(), pager, )?, @@ -103,14 +105,15 @@ pub fn translate( ast::Stmt::Select(select) => translate_select(query_mode, schema, *select, syms)?, ast::Stmt::Update { .. } => bail_parse_error!("UPDATE not supported yet"), ast::Stmt::Vacuum(_, _) => bail_parse_error!("VACUUM not supported yet"), - ast::Stmt::Insert { - with, - or_conflict, - tbl_name, - columns, - body, - returning, - } => { + ast::Stmt::Insert(insert) => { + let Insert { + with, + or_conflict, + tbl_name, + columns, + body, + returning, + } = *insert; change_cnt_on = true; translate_insert( query_mode, diff --git a/core/translate/select.rs b/core/translate/select.rs index 2a055afd2..22168ab7a 100644 --- a/core/translate/select.rs +++ b/core/translate/select.rs @@ -11,8 +11,8 @@ use crate::util::normalize_ident; use crate::vdbe::builder::{ProgramBuilderOpts, QueryMode}; use crate::SymbolTable; use crate::{schema::Schema, vdbe::builder::ProgramBuilder, Result}; -use sqlite3_parser::ast::ResultColumn; use sqlite3_parser::ast::{self}; +use sqlite3_parser::ast::{ResultColumn, SelectInner}; pub fn translate_select( query_mode: QueryMode, @@ -42,13 +42,14 @@ pub fn prepare_select_plan( syms: &SymbolTable, ) -> Result { match *select.body.select { - ast::OneSelect::Select { - mut columns, - from, - where_clause, - group_by, - .. - } => { + ast::OneSelect::Select(select_inner) => { + let SelectInner { + mut columns, + from, + where_clause, + group_by, + .. + } = *select_inner; let col_count = columns.len(); if col_count == 0 { crate::bail_parse_error!("SELECT without columns is not allowed"); @@ -305,7 +306,7 @@ pub fn prepare_select_plan( exprs: group_by.exprs, having: if let Some(having) = group_by.having { let mut predicates = vec![]; - break_predicate_at_and_boundaries(having, &mut predicates); + break_predicate_at_and_boundaries(*having, &mut predicates); for expr in predicates.iter_mut() { bind_column_references( expr, diff --git a/extensions/core/Cargo.toml b/extensions/core/Cargo.toml index 3194bcadb..7dc60b498 100644 --- a/extensions/core/Cargo.toml +++ b/extensions/core/Cargo.toml @@ -12,5 +12,4 @@ default = [] static = [] [dependencies] -log = "0.4.20" limbo_macros = { path = "../../macros" } diff --git a/extensions/regexp/Cargo.toml b/extensions/regexp/Cargo.toml index c8288e601..9702db5f1 100644 --- a/extensions/regexp/Cargo.toml +++ b/extensions/regexp/Cargo.toml @@ -17,7 +17,6 @@ crate-type = ["cdylib", "lib"] [dependencies] limbo_ext = { path = "../core", features = ["static"] } regex = "1.11.1" -log = "0.4.20" [target.'cfg(not(target_family = "wasm"))'.dependencies] mimalloc = { version = "*", default-features = false } diff --git a/extensions/series/Cargo.toml b/extensions/series/Cargo.toml index 823e95472..33f45cff2 100644 --- a/extensions/series/Cargo.toml +++ b/extensions/series/Cargo.toml @@ -15,7 +15,6 @@ crate-type = ["cdylib", "lib"] [dependencies] limbo_ext = { path = "../core", features = ["static"] } -log = "0.4.20" [target.'cfg(not(target_family = "wasm"))'.dependencies] mimalloc = { version = "*", default-features = false } diff --git a/simulator/generation/plan.rs b/simulator/generation/plan.rs index d06cdfff3..cf41c1060 100644 --- a/simulator/generation/plan.rs +++ b/simulator/generation/plan.rs @@ -47,9 +47,9 @@ impl InteractionPlan { .map(|i| i.interactions()) .collect::>(); - let (mut i, mut j1, mut j2) = (0, 0, 0); + let (mut i, mut j) = (0, 0); - while i < interactions.len() && j1 < plan.len() { + while i < interactions.len() && j < plan.len() { if interactions[i].starts_with("-- begin") || interactions[i].starts_with("-- end") || interactions[i].is_empty() @@ -58,28 +58,30 @@ impl InteractionPlan { continue; } - if interactions[i].contains(plan[j1][j2].to_string().as_str()) { - i += 1; - if j2 + 1 < plan[j1].len() { - j2 += 1; - } else { - j1 += 1; - j2 = 0; - } - } else { - plan[j1].remove(j2); + // interactions[i] is the i'th line in the human readable plan + // plan[j][k] is the k'th interaction in the j'th property + let mut k = 0; - if plan[j1].is_empty() { - plan.remove(j1); - j2 = 0; + while k < plan[j].len() { + if i >= interactions.len() { + let _ = plan.split_off(j + 1); + let _ = plan[j].split_off(k); + break; + } + + if interactions[i].contains(plan[j][k].to_string().as_str()) { + i += 1; + k += 1; + } else { + plan[j].remove(k); } } - } - if j1 < plan.len() { - if j2 < plan[j1].len() { - let _ = plan[j1].split_off(j2); + + if plan[j].is_empty() { + plan.remove(j); + } else { + j += 1; } - let _ = plan.split_off(j1); } plan diff --git a/simulator/main.rs b/simulator/main.rs index 82c39b809..2eb463529 100644 --- a/simulator/main.rs +++ b/simulator/main.rs @@ -233,6 +233,10 @@ fn run_simulator( }) .collect::>(); + // Write the shrunk plan to a file + let mut f = std::fs::File::create(&paths.shrunk_plan).unwrap(); + f.write_all(shrunk_plans[0].to_string().as_bytes()).unwrap(); + let last_execution = Arc::new(Mutex::new(*last_execution)); let shrunk = SandboxedResult::from( @@ -270,11 +274,6 @@ fn run_simulator( log::error!("shrinking failed, the error was not properly reproduced"); } } - - // Write the shrunk plan to a file - let shrunk_plan = std::fs::read(&paths.shrunk_plan).unwrap(); - let mut f = std::fs::File::create(&paths.shrunk_plan).unwrap(); - f.write_all(&shrunk_plan).unwrap(); } } } diff --git a/simulator/runner/execution.rs b/simulator/runner/execution.rs index 6544928a1..6342dff3a 100644 --- a/simulator/runner/execution.rs +++ b/simulator/runner/execution.rs @@ -62,6 +62,7 @@ pub(crate) fn execute_plans( ) -> ExecutionResult { let mut history = ExecutionHistory::new(); let now = std::time::Instant::now(); + env.clear_poison(); let mut env = env.lock().unwrap(); for _tick in 0..env.opts.ticks { // Pick the connection to interact with diff --git a/tests/Cargo.toml b/tests/Cargo.toml index f3e67d73a..a17c58910 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -16,11 +16,8 @@ path = "integration/mod.rs" [dependencies] anyhow = "1.0.75" -clap = { version = "4.5", features = ["derive"] } -dirs = "5.0.1" env_logger = "0.10.1" limbo_core = { path = "../core" } -rustyline = "12.0.0" rusqlite = { version = "0.29", features = ["bundled"] } tempfile = "3.0.7" log = "0.4.22" diff --git a/vendored/sqlite3-parser/src/lexer/sql/test.rs b/vendored/sqlite3-parser/src/lexer/sql/test.rs index 2ba066bdd..c0923403b 100644 --- a/vendored/sqlite3-parser/src/lexer/sql/test.rs +++ b/vendored/sqlite3-parser/src/lexer/sql/test.rs @@ -3,7 +3,7 @@ use fallible_iterator::FallibleIterator; use super::{Error, Parser}; use crate::parser::ast::fmt::ToTokens; use crate::parser::{ - ast::{Cmd, Name, ParameterInfo, QualifiedName, Stmt}, + ast::{Cmd, ParameterInfo, Stmt}, ParserError, }; @@ -73,20 +73,12 @@ fn vtab_args() -> Result<(), Error> { body TEXT CHECK(length(body)<10240) );"; let r = parse_cmd(sql); - let Cmd::Stmt(Stmt::CreateVirtualTable { - tbl_name: QualifiedName { - name: Name(tbl_name), - .. - }, - module_name: Name(module_name), - args: Some(args), - .. - }) = r - else { + let Cmd::Stmt(Stmt::CreateVirtualTable(create_virtual_table)) = r else { panic!("unexpected AST") }; - assert_eq!(tbl_name, "mail"); - assert_eq!(module_name, "fts3"); + assert_eq!(create_virtual_table.tbl_name.name, "mail"); + assert_eq!(create_virtual_table.module_name.0, "fts3"); + let args = create_virtual_table.args.as_ref().unwrap(); assert_eq!(args.len(), 2); assert_eq!(args[0], "subject VARCHAR(256) NOT NULL"); assert_eq!(args[1], "body TEXT CHECK(length(body)<10240)"); diff --git a/vendored/sqlite3-parser/src/parser/ast/check.rs b/vendored/sqlite3-parser/src/parser/ast/check.rs index e1e0eecd3..3df2c1c97 100644 --- a/vendored/sqlite3-parser/src/parser/ast/check.rs +++ b/vendored/sqlite3-parser/src/parser/ast/check.rs @@ -56,20 +56,29 @@ impl Stmt { /// Like `sqlite3_column_count` but more limited pub fn column_count(&self) -> ColumnCount { match self { - Self::Delete { - returning: Some(returning), - .. - } => column_count(returning), - Self::Insert { - returning: Some(returning), - .. - } => column_count(returning), + Self::Delete(delete) => { + let Delete { returning, .. } = &**delete; + match returning { + Some(returning) => column_count(returning), + None => ColumnCount::None, + } + } + Self::Insert(insert) => { + let Insert { returning, .. } = &**insert; + match returning { + Some(returning) => column_count(returning), + None => ColumnCount::None, + } + } Self::Pragma(..) => ColumnCount::Dynamic, Self::Select(s) => s.column_count(), - Self::Update { - returning: Some(returning), - .. - } => column_count(returning), + Self::Update(update) => { + let Update { returning, .. } = &**update; + match returning { + Some(returning) => column_count(returning), + None => ColumnCount::None, + } + } _ => ColumnCount::None, } } @@ -94,22 +103,28 @@ impl Stmt { /// check for extra rules pub fn check(&self) -> Result<(), ParserError> { match self { - Self::AlterTable(old_name, AlterTableBody::RenameTo(new_name)) => { - if *new_name == old_name.name { - return Err(custom_err!( - "there is already another table or index with this name: {}", - new_name - )); - } - Ok(()) - } - Self::AlterTable(.., AlterTableBody::AddColumn(cd)) => { - for c in cd { - if let ColumnConstraint::PrimaryKey { .. } = c { - return Err(custom_err!("Cannot add a PRIMARY KEY column")); - } else if let ColumnConstraint::Unique(..) = c { - return Err(custom_err!("Cannot add a UNIQUE column")); + Self::AlterTable(alter_table) => { + let (old_name, body) = &**alter_table; + match body { + AlterTableBody::RenameTo(new_name) => { + if *new_name == old_name.name { + return Err(custom_err!( + "there is already another table or index with this name: {}", + new_name + )); + } } + AlterTableBody::AddColumn(cd) => { + for c in cd { + if let ColumnConstraint::PrimaryKey { .. } = c { + return Err(custom_err!("Cannot add a PRIMARY KEY column")); + } + if let ColumnConstraint::Unique(..) = c { + return Err(custom_err!("Cannot add a UNIQUE column")); + } + } + } + _ => {} } Ok(()) } @@ -153,31 +168,47 @@ impl Stmt { _ => Ok(()), } } - Self::Delete { - order_by: Some(_), - limit: None, - .. - } => Err(custom_err!("ORDER BY without LIMIT on DELETE")), - Self::Insert { - columns: Some(columns), - body: InsertBody::Select(select, ..), - .. - } => match select.body.select.column_count() { - ColumnCount::Fixed(n) if n != columns.len() => { - Err(custom_err!("{} values for {} columns", n, columns.len())) + Self::Delete(delete) => { + let Delete { + order_by, limit, .. + } = &**delete; + if let Some(_) = order_by { + if limit.is_none() { + return Err(custom_err!("ORDER BY without LIMIT on DELETE")); + } } - _ => Ok(()), - }, - Self::Insert { - columns: Some(columns), - body: InsertBody::DefaultValues, - .. - } => Err(custom_err!("0 values for {} columns", columns.len())), - Self::Update { - order_by: Some(_), - limit: None, - .. - } => Err(custom_err!("ORDER BY without LIMIT on UPDATE")), + Ok(()) + } + Self::Insert(insert) => { + let Insert { columns, body, .. } = &**insert; + if columns.is_none() { + return Ok(()); + } + let columns = columns.as_ref().unwrap(); + match &*body { + InsertBody::Select(select, ..) => match select.body.select.column_count() { + ColumnCount::Fixed(n) if n != columns.len() => { + Err(custom_err!("{} values for {} columns", n, columns.len())) + } + _ => Ok(()), + }, + InsertBody::DefaultValues => { + Err(custom_err!("0 values for {} columns", columns.len())) + } + } + } + Self::Update(update) => { + let Update { + order_by, limit, .. + } = &**update; + if let Some(_) = order_by { + if limit.is_none() { + return Err(custom_err!("ORDER BY without LIMIT on UPDATE")); + } + } + + Ok(()) + } _ => Ok(()), } } @@ -295,7 +326,10 @@ impl OneSelect { /// Like `sqlite3_column_count` but more limited pub fn column_count(&self) -> ColumnCount { match self { - Self::Select { columns, .. } => column_count(columns), + Self::Select(select) => { + let SelectInner { columns, .. } = &**select; + column_count(columns) + } Self::Values(values) => { assert!(!values.is_empty()); // TODO Validate ColumnCount::Fixed(values[0].len()) diff --git a/vendored/sqlite3-parser/src/parser/ast/fmt.rs b/vendored/sqlite3-parser/src/parser/ast/fmt.rs index 34a7fa3f0..3c264d607 100644 --- a/vendored/sqlite3-parser/src/parser/ast/fmt.rs +++ b/vendored/sqlite3-parser/src/parser/ast/fmt.rs @@ -119,7 +119,8 @@ impl Display for Cmd { impl ToTokens for Stmt { fn to_tokens(&self, s: &mut S) -> Result<(), S::Error> { match self { - Self::AlterTable(tbl_name, body) => { + Self::AlterTable(alter_table) => { + let (tbl_name, body) = &**alter_table; s.append(TK_ALTER, None)?; s.append(TK_TABLE, None)?; tbl_name.to_tokens(s)?; @@ -211,17 +212,18 @@ impl ToTokens for Stmt { tbl_name.to_tokens(s)?; body.to_tokens(s) } - Self::CreateTrigger { - temporary, - if_not_exists, - trigger_name, - time, - event, - tbl_name, - for_each_row, - when_clause, - commands, - } => { + Self::CreateTrigger(trigger) => { + let CreateTrigger { + temporary, + if_not_exists, + trigger_name, + time, + event, + tbl_name, + for_each_row, + when_clause, + commands, + } = &**trigger; s.append(TK_CREATE, None)?; if *temporary { s.append(TK_TEMP, None)?; @@ -281,12 +283,13 @@ impl ToTokens for Stmt { s.append(TK_AS, None)?; select.to_tokens(s) } - Self::CreateVirtualTable { - if_not_exists, - tbl_name, - module_name, - args, - } => { + Self::CreateVirtualTable(create_virtual_table) => { + let CreateVirtualTable { + if_not_exists, + tbl_name, + module_name, + args, + } = &**create_virtual_table; s.append(TK_CREATE, None)?; s.append(TK_VIRTUAL, None)?; s.append(TK_TABLE, None)?; @@ -304,15 +307,16 @@ impl ToTokens for Stmt { } s.append(TK_RP, None) } - Self::Delete { - with, - tbl_name, - indexed, - where_clause, - returning, - order_by, - limit, - } => { + Self::Delete(delete) => { + let Delete { + with, + tbl_name, + indexed, + where_clause, + returning, + order_by, + limit, + } = &**delete; if let Some(with) = with { with.to_tokens(s)?; } @@ -392,14 +396,15 @@ impl ToTokens for Stmt { } view_name.to_tokens(s) } - Self::Insert { - with, - or_conflict, - tbl_name, - columns, - body, - returning, - } => { + Self::Insert(insert) => { + let Insert { + with, + or_conflict, + tbl_name, + columns, + body, + returning, + } = &**insert; if let Some(with) = with { with.to_tokens(s)?; } @@ -465,18 +470,19 @@ impl ToTokens for Stmt { name.to_tokens(s) } Self::Select(select) => select.to_tokens(s), - Self::Update { - with, - or_conflict, - tbl_name, - indexed, - sets, - from, - where_clause, - returning, - order_by, - limit, - } => { + Self::Update(update) => { + let Update { + with, + or_conflict, + tbl_name, + indexed, + sets, + from, + where_clause, + returning, + order_by, + limit, + } = &**update; if let Some(with) = with { with.to_tokens(s)?; } @@ -891,14 +897,15 @@ impl Display for CompoundOperator { impl ToTokens for OneSelect { fn to_tokens(&self, s: &mut S) -> Result<(), S::Error> { match self { - Self::Select { - distinctness, - columns, - from, - where_clause, - group_by, - window_clause, - } => { + Self::Select(select) => { + let SelectInner { + distinctness, + columns, + from, + where_clause, + group_by, + window_clause, + } = &**select; s.append(TK_SELECT, None)?; if let Some(ref distinctness) = distinctness { distinctness.to_tokens(s)?; @@ -1649,13 +1656,14 @@ impl ToTokens for TriggerEvent { impl ToTokens for TriggerCmd { fn to_tokens(&self, s: &mut S) -> Result<(), S::Error> { match self { - Self::Update { - or_conflict, - tbl_name, - sets, - from, - where_clause, - } => { + Self::Update(update) => { + let TriggerCmdUpdate { + or_conflict, + tbl_name, + sets, + from, + where_clause, + } = &**update; s.append(TK_UPDATE, None)?; if let Some(or_conflict) = or_conflict { s.append(TK_OR, None)?; @@ -1674,14 +1682,15 @@ impl ToTokens for TriggerCmd { } Ok(()) } - Self::Insert { - or_conflict, - tbl_name, - col_names, - select, - upsert, - returning, - } => { + Self::Insert(insert) => { + let TriggerCmdInsert { + or_conflict, + tbl_name, + col_names, + select, + upsert, + returning, + } = &**insert; if let Some(ResolveType::Replace) = or_conflict { s.append(TK_REPLACE, None)?; } else { @@ -1708,14 +1717,11 @@ impl ToTokens for TriggerCmd { } Ok(()) } - Self::Delete { - tbl_name, - where_clause, - } => { + Self::Delete(delete) => { s.append(TK_DELETE, None)?; s.append(TK_FROM, None)?; - tbl_name.to_tokens(s)?; - if let Some(where_clause) = where_clause { + delete.tbl_name.to_tokens(s)?; + if let Some(where_clause) = &delete.where_clause { s.append(TK_WHERE, None)?; where_clause.to_tokens(s)?; } diff --git a/vendored/sqlite3-parser/src/parser/ast/mod.rs b/vendored/sqlite3-parser/src/parser/ast/mod.rs index f149322b3..3ea9d5992 100644 --- a/vendored/sqlite3-parser/src/parser/ast/mod.rs +++ b/vendored/sqlite3-parser/src/parser/ast/mod.rs @@ -71,18 +71,18 @@ pub(crate) enum ExplainKind { #[derive(Clone, Debug, PartialEq, Eq)] pub enum Stmt { /// `ALTER TABLE`: table name, body - AlterTable(QualifiedName, AlterTableBody), + AlterTable(Box<(QualifiedName, AlterTableBody)>), /// `ANALYSE`: object name Analyze(Option), /// `ATTACH DATABASE` Attach { /// filename // TODO distinction between ATTACH and ATTACH DATABASE - expr: Expr, + expr: Box, /// schema name - db_name: Expr, + db_name: Box, /// password - key: Option, + key: Option>, }, /// `BEGIN`: tx type, tx name Begin(Option, Option), @@ -95,13 +95,13 @@ pub enum Stmt { /// `IF NOT EXISTS` if_not_exists: bool, /// index name - idx_name: QualifiedName, + idx_name: Box, /// table name tbl_name: Name, /// indexed columns or expressions columns: Vec, /// partial index - where_clause: Option, + where_clause: Option>, }, /// `CREATE TABLE` CreateTable { @@ -112,29 +112,10 @@ pub enum Stmt { /// table name tbl_name: QualifiedName, /// table body - body: CreateTableBody, + body: Box, }, /// `CREATE TRIGGER` - CreateTrigger { - /// `TEMPORARY` - temporary: bool, - /// `IF NOT EXISTS` - if_not_exists: bool, - /// trigger name - trigger_name: QualifiedName, - /// `BEFORE`/`AFTER`/`INSTEAD OF` - time: Option, - /// `DELETE`/`INSERT`/`UPDATE` - event: TriggerEvent, - /// table name - tbl_name: QualifiedName, - /// `FOR EACH ROW` - for_each_row: bool, - /// `WHEN` - when_clause: Option, - /// statements - commands: Vec, - }, + CreateTrigger(Box), /// `CREATE VIEW` CreateView { /// `TEMPORARY` @@ -149,35 +130,11 @@ pub enum Stmt { select: Box), /// `UPDATE` - Update { - /// CTE - with: Option, - /// `OR` - or_conflict: Option, - /// table name - tbl_name: QualifiedName, - /// `INDEXED` - indexed: Option, - /// `SET` assignments - sets: Vec, - /// `FROM` - from: Option, - /// `WHERE` clause - where_clause: Option, - /// `RETURNING` - returning: Option>, - /// `ORDER BY` - order_by: Option>, - /// `LIMIT` - limit: Option>, - }, + Update(Box), /// `VACUUM`: database name, into expr - Vacuum(Option, Option), + Vacuum(Option, Option>), +} + +/// `CREATE VIRTUAL TABLE` +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CreateVirtualTable { + /// `IF NOT EXISTS` + pub if_not_exists: bool, + /// table name + pub tbl_name: QualifiedName, + /// module name + pub module_name: Name, + /// args + pub args: Option>, // TODO smol str +} + +/// `CREATE TRIGGER +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CreateTrigger { + /// `TEMPORARY` + pub temporary: bool, + /// `IF NOT EXISTS` + pub if_not_exists: bool, + /// trigger name + pub trigger_name: QualifiedName, + /// `BEFORE`/`AFTER`/`INSTEAD OF` + pub time: Option, + /// `DELETE`/`INSERT`/`UPDATE` + pub event: TriggerEvent, + /// table name + pub tbl_name: QualifiedName, + /// `FOR EACH ROW` + pub for_each_row: bool, + /// `WHEN` + pub when_clause: Option, + /// statements + pub commands: Vec, +} + +/// `INSERT` +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Insert { + /// CTE + pub with: Option, + /// `OR` + pub or_conflict: Option, // TODO distinction between REPLACE and INSERT OR REPLACE + /// table name + pub tbl_name: QualifiedName, + /// `COLUMNS` + pub columns: Option, + /// `VALUES` or `SELECT` + pub body: InsertBody, + /// `RETURNING` + pub returning: Option>, +} + +/// `UPDATE` clause +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Update { + /// CTE + pub with: Option, + /// `OR` + pub or_conflict: Option, + /// table name + pub tbl_name: QualifiedName, + /// `INDEXED` + pub indexed: Option, + /// `SET` assignments + pub sets: Vec, + /// `FROM` + pub from: Option, + /// `WHERE` clause + pub where_clause: Option>, + /// `RETURNING` + pub returning: Option>, + /// `ORDER BY` + pub order_by: Option>, + /// `LIMIT` + pub limit: Option>, +} + +/// `DELETE` +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Delete { + /// CTE + pub with: Option, + /// `FROM` table name + pub tbl_name: QualifiedName, + /// `INDEXED` + pub indexed: Option, + /// `WHERE` clause + pub where_clause: Option>, + /// `RETURNING` + pub returning: Option>, + /// `ORDER BY` + pub order_by: Option>, + /// `LIMIT` + pub limit: Option>, } /// SQL expression @@ -771,24 +791,28 @@ pub enum CompoundOperator { #[derive(Clone, Debug, PartialEq, Eq)] pub enum OneSelect { /// `SELECT` - Select { - /// `DISTINCT` - distinctness: Option, - /// columns - columns: Vec, - /// `FROM` clause - from: Option, - /// `WHERE` clause - where_clause: Option, - /// `GROUP BY` - group_by: Option, - /// `WINDOW` definition - window_clause: Option>, - }, + Select(Box), /// `VALUES` Values(Vec>), } +#[derive(Clone, Debug, PartialEq, Eq)] +/// `SELECT` core +pub struct SelectInner { + /// `DISTINCT` + pub distinctness: Option, + /// columns + pub columns: Vec, + /// `FROM` clause + pub from: Option, + /// `WHERE` clause + pub where_clause: Option, + /// `GROUP BY` + pub group_by: Option, + /// `WINDOW` definition + pub window_clause: Option>, +} + /// `SELECT` ... `FROM` clause // https://sqlite.org/syntax/join-clause.html #[derive(Clone, Debug, PartialEq, Eq)] @@ -1005,7 +1029,7 @@ pub struct GroupBy { /// expressions pub exprs: Vec, /// `HAVING` - pub having: Option, // HAVING clause on a non-aggregate query + pub having: Option>, // HAVING clause on a non-aggregate query } /// identifier or one of several keywords or `INDEXED` @@ -1632,44 +1656,56 @@ pub enum TriggerEvent { #[derive(Clone, Debug, PartialEq, Eq)] pub enum TriggerCmd { /// `UPDATE` - Update { - /// `OR` - or_conflict: Option, - /// table name - tbl_name: Name, - /// `SET` assignments - sets: Vec, - /// `FROM` - from: Option, - /// `WHERE` clause - where_clause: Option, - }, + Update(Box), /// `INSERT` - Insert { - /// `OR` - or_conflict: Option, - /// table name - tbl_name: Name, - /// `COLUMNS` - col_names: Option, - /// `SELECT` or `VALUES` - select: Box), } +/// `UPDATE` trigger command +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TriggerCmdUpdate { + /// `OR` + pub or_conflict: Option, + /// table name + pub tbl_name: Name, + /// `SET` assignments + pub sets: Vec, + /// `FROM` + pub from: Option, + /// `WHERE` clause + pub where_clause: Option, +} + +/// `INSERT` trigger command +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TriggerCmdInsert { + /// `OR` + pub or_conflict: Option, + /// table name + pub tbl_name: Name, + /// `COLUMNS` + pub col_names: Option, + /// `SELECT` or `VALUES` + pub select: Box