diff --git a/COMPAT.md b/COMPAT.md index 99db218fa..879b022f8 100644 --- a/COMPAT.md +++ b/COMPAT.md @@ -58,7 +58,7 @@ Turso aims to be fully compatible with SQLite, with opt-in features not supporte | COMMIT TRANSACTION | Partial | Transaction names are not supported. | | CREATE INDEX | Partial | Only for columns (not arbitrary expressions) | | CREATE TABLE | Partial | | -| CREATE TABLE ... STRICT | Yes | | +| CREATE TABLE ... STRICT | Partial | Strict schema mode is experimental. | | CREATE TRIGGER | No | | | CREATE VIEW | Yes | | | CREATE VIRTUAL TABLE | Yes | | diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs index 99a331337..9085a9b1b 100644 --- a/bindings/python/src/lib.rs +++ b/bindings/python/src/lib.rs @@ -317,7 +317,7 @@ impl Drop for Connection { #[allow(clippy::arc_with_non_send_sync)] #[pyfunction(signature = (path))] pub fn connect(path: &str) -> Result { - match turso_core::Connection::from_uri(path, true, false, false) { + match turso_core::Connection::from_uri(path, true, false, false, false) { Ok((io, conn)) => Ok(Connection { conn, _io: io }), Err(e) => Err(PyErr::new::(format!( "Failed to create connection: {e:?}" diff --git a/cli/app.rs b/cli/app.rs index 910e68608..08c6bd5e2 100644 --- a/cli/app.rs +++ b/cli/app.rs @@ -66,6 +66,8 @@ pub struct Opts { pub experimental_views: bool, #[clap(long, help = "Enable experimental indexing feature")] pub experimental_indexes: Option, + #[clap(long, help = "Enable experimental strict schema mode")] + pub experimental_strict: bool, #[clap(short = 't', long, help = "specify output file for log traces")] pub tracing_output: Option, #[clap(long, help = "Start MCP server instead of interactive shell")] @@ -107,6 +109,7 @@ impl Limbo { indexes_enabled, opts.experimental_mvcc, opts.experimental_views, + opts.experimental_strict, )? } else { let flags = if opts.readonly { @@ -118,9 +121,11 @@ impl Limbo { &db_file, opts.vfs.as_ref(), flags, - indexes_enabled, - opts.experimental_mvcc, - opts.experimental_views, + turso_core::DatabaseOpts::new() + .with_mvcc(opts.experimental_mvcc) + .with_indexes(indexes_enabled) + .with_views(opts.experimental_views) + .with_strict(opts.experimental_strict), )?; let conn = db.connect()?; (io, conn) diff --git a/cli/mcp_server.rs b/cli/mcp_server.rs index f80b86474..838d4f1d5 100644 --- a/cli/mcp_server.rs +++ b/cli/mcp_server.rs @@ -8,7 +8,7 @@ use std::sync::mpsc; use std::sync::{Arc, Mutex}; use std::thread; use std::time::Duration; -use turso_core::{Connection, Database, OpenFlags, StepResult, Value as DbValue}; +use turso_core::{Connection, Database, DatabaseOpts, OpenFlags, StepResult, Value as DbValue}; #[derive(Debug, Serialize, Deserialize)] struct JsonRpcRequest { @@ -408,7 +408,7 @@ impl TursoMcpServer { // Open the new database connection let conn = if path == ":memory:" || path.contains([':', '?', '&', '#']) { - match Connection::from_uri(&path, true, false, false) { + match Connection::from_uri(&path, true, false, false, false) { Ok((_io, c)) => c, Err(e) => return format!("Failed to open database '{path}': {e}"), } @@ -417,9 +417,7 @@ impl TursoMcpServer { &path, None::<&str>, OpenFlags::default(), - true, - false, - false, + DatabaseOpts::new().with_indexes(true), ) { Ok((_io, db)) => match db.connect() { Ok(c) => c, diff --git a/core/lib.rs b/core/lib.rs index 5b30f936d..fac579257 100644 --- a/core/lib.rs +++ b/core/lib.rs @@ -99,6 +99,52 @@ pub use util::IOExt; use vdbe::builder::QueryMode; use vdbe::builder::TableRefIdCounter; +/// Configuration for database features +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DatabaseOpts { + pub enable_mvcc: bool, + pub enable_indexes: bool, + pub enable_views: bool, + pub enable_strict: bool, +} + +impl Default for DatabaseOpts { + fn default() -> Self { + Self { + enable_mvcc: false, + enable_indexes: true, + enable_views: false, + enable_strict: false, + } + } +} + +impl DatabaseOpts { + pub fn new() -> Self { + Self::default() + } + + pub fn with_mvcc(mut self, enable: bool) -> Self { + self.enable_mvcc = enable; + self + } + + pub fn with_indexes(mut self, enable: bool) -> Self { + self.enable_indexes = enable; + self + } + + pub fn with_views(mut self, enable: bool) -> Self { + self.enable_views = enable; + self + } + + pub fn with_strict(mut self, enable: bool) -> Self { + self.enable_strict = enable; + self + } +} + pub type Result = std::result::Result; #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -144,7 +190,7 @@ pub struct Database { init_lock: Arc>, open_flags: OpenFlags, builtin_syms: RefCell, - experimental_views: bool, + opts: DatabaseOpts, n_connections: AtomicUsize, } @@ -216,9 +262,9 @@ impl Database { io, path, OpenFlags::default(), - enable_mvcc, - enable_indexes, - false, + DatabaseOpts::new() + .with_mvcc(enable_mvcc) + .with_indexes(enable_indexes), ) } @@ -227,21 +273,11 @@ impl Database { io: Arc, path: &str, flags: OpenFlags, - enable_mvcc: bool, - enable_indexes: bool, - enable_views: bool, + opts: DatabaseOpts, ) -> Result> { let file = io.open_file(path, flags, true)?; let db_file = Arc::new(DatabaseFile::new(file)); - Self::open_with_flags( - io, - path, - db_file, - flags, - enable_mvcc, - enable_indexes, - enable_views, - ) + Self::open_with_flags(io, path, db_file, flags, opts) } #[allow(clippy::arc_with_non_send_sync)] @@ -257,9 +293,9 @@ impl Database { path, db_file, OpenFlags::default(), - enable_mvcc, - enable_indexes, - false, + DatabaseOpts::new() + .with_mvcc(enable_mvcc) + .with_indexes(enable_indexes), ) } @@ -269,9 +305,7 @@ impl Database { path: &str, db_file: Arc, flags: OpenFlags, - enable_mvcc: bool, - enable_indexes: bool, - enable_views: bool, + opts: DatabaseOpts, ) -> Result> { // turso-sync-engine create 2 databases with different names in the same IO if MemoryIO is used // in this case we need to bypass registry (as this is MemoryIO DB) but also preserve original distinction in names (e.g. :memory:-draft and :memory:-synced) @@ -282,9 +316,7 @@ impl Database { &format!("{path}-wal"), db_file, flags, - enable_mvcc, - enable_indexes, - enable_views, + opts, ); } @@ -304,15 +336,13 @@ impl Database { &format!("{path}-wal"), db_file, flags, - enable_mvcc, - enable_indexes, - enable_views, + opts, )?; registry.insert(canonical_path, Arc::downgrade(&db)); Ok(db) } - #[allow(clippy::arc_with_non_send_sync, clippy::too_many_arguments)] + #[allow(clippy::arc_with_non_send_sync)] #[cfg(all(feature = "fs", feature = "conn_raw_api"))] pub fn open_with_flags_bypass_registry( io: Arc, @@ -320,36 +350,23 @@ impl Database { wal_path: &str, db_file: Arc, flags: OpenFlags, - enable_mvcc: bool, - enable_indexes: bool, - enable_views: bool, + opts: DatabaseOpts, ) -> Result> { - Self::open_with_flags_bypass_registry_internal( - io, - path, - wal_path, - db_file, - flags, - enable_mvcc, - enable_indexes, - enable_views, - ) + Self::open_with_flags_bypass_registry_internal(io, path, wal_path, db_file, flags, opts) } - #[allow(clippy::arc_with_non_send_sync, clippy::too_many_arguments)] + #[allow(clippy::arc_with_non_send_sync)] fn open_with_flags_bypass_registry_internal( io: Arc, path: &str, wal_path: &str, db_file: Arc, flags: OpenFlags, - enable_mvcc: bool, - enable_indexes: bool, - enable_views: bool, + opts: DatabaseOpts, ) -> Result> { let maybe_shared_wal = WalFileShared::open_shared_if_exists(&io, wal_path)?; - let mv_store = if enable_mvcc { + let mv_store = if opts.enable_mvcc { Some(Arc::new(MvStore::new( mvcc::LocalClock::new(), mvcc::persistent_storage::Storage::new_noop(), @@ -372,11 +389,12 @@ impl Database { } else { BufferPool::DEFAULT_ARENA_SIZE }; + // opts is now passed as parameter let db = Arc::new(Database { mv_store, path: path.to_string(), wal_path: wal_path.to_string(), - schema: Mutex::new(Arc::new(Schema::new(enable_indexes))), + schema: Mutex::new(Arc::new(Schema::new(opts.enable_indexes))), _shared_page_cache: shared_page_cache.clone(), maybe_shared_wal: RwLock::new(maybe_shared_wal), db_file, @@ -385,7 +403,7 @@ impl Database { open_flags: flags, db_state: Arc::new(AtomicDbState::new(db_state)), init_lock: Arc::new(Mutex::new(())), - experimental_views: enable_views, + opts, buffer_pool: BufferPool::begin_init(&io, arena_size), n_connections: AtomicUsize::new(0), }); @@ -617,9 +635,7 @@ impl Database { path: &str, vfs: Option, flags: OpenFlags, - indexes: bool, - mvcc: bool, - views: bool, + opts: DatabaseOpts, ) -> Result<(Arc, Arc)> where S: AsRef + std::fmt::Display, @@ -646,7 +662,7 @@ impl Database { } }, }; - let db = Self::open_file_with_flags(io.clone(), path, flags, mvcc, indexes, views)?; + let db = Self::open_file_with_flags(io.clone(), path, flags, opts)?; Ok((io, db)) } None => { @@ -654,7 +670,7 @@ impl Database { MEMORY_PATH => Arc::new(MemoryIO::new()), _ => Arc::new(PlatformIO::new()?), }; - let db = Self::open_file_with_flags(io.clone(), path, flags, mvcc, indexes, views)?; + let db = Self::open_file_with_flags(io.clone(), path, flags, opts)?; Ok((io, db)) } } @@ -695,7 +711,11 @@ impl Database { } pub fn experimental_views_enabled(&self) -> bool { - self.experimental_views + self.opts.enable_views + } + + pub fn experimental_strict_enabled(&self) -> bool { + self.opts.enable_strict } } @@ -1247,6 +1267,7 @@ impl Connection { use_indexes: bool, mvcc: bool, views: bool, + strict: bool, ) -> Result<(Arc, Arc)> { use crate::util::MEMORY_PATH; let opts = OpenOptions::parse(uri)?; @@ -1257,9 +1278,11 @@ impl Connection { io.clone(), MEMORY_PATH, flags, - mvcc, - use_indexes, - views, + DatabaseOpts::new() + .with_mvcc(mvcc) + .with_indexes(use_indexes) + .with_views(views) + .with_strict(strict), )?; let conn = db.connect()?; return Ok((io, conn)); @@ -1268,9 +1291,11 @@ impl Connection { &opts.path, opts.vfs.as_ref(), flags, - use_indexes, - mvcc, - views, + DatabaseOpts::new() + .with_mvcc(mvcc) + .with_indexes(use_indexes) + .with_views(views) + .with_strict(strict), )?; if let Some(modeof) = opts.modeof { let perms = std::fs::metadata(modeof)?; @@ -1287,24 +1312,12 @@ impl Connection { } #[cfg(feature = "fs")] - fn from_uri_attached( - uri: &str, - use_indexes: bool, - use_mvcc: bool, - use_views: bool, - ) -> Result> { + fn from_uri_attached(uri: &str, db_opts: DatabaseOpts) -> Result> { let mut opts = OpenOptions::parse(uri)?; // FIXME: for now, only support read only attach opts.mode = OpenMode::ReadOnly; let flags = opts.get_flags()?; - let (_io, db) = Database::open_new( - &opts.path, - opts.vfs.as_ref(), - flags, - use_indexes, - use_mvcc, - use_views, - )?; + let (_io, db) = Database::open_new(&opts.path, opts.vfs.as_ref(), flags, db_opts)?; if let Some(modeof) = opts.modeof { let perms = std::fs::metadata(modeof)?; std::fs::set_permissions(&opts.path, perms.permissions())?; @@ -1736,6 +1749,10 @@ impl Connection { self._db.experimental_views_enabled() } + pub fn experimental_strict_enabled(&self) -> bool { + self._db.experimental_strict_enabled() + } + /// Query the current value(s) of `pragma_name` associated to /// `pragma_value`. /// @@ -1832,8 +1849,14 @@ impl Connection { .indexes_enabled(); let use_mvcc = self._db.mv_store.is_some(); let use_views = self._db.experimental_views_enabled(); + let use_strict = self._db.experimental_strict_enabled(); - let db = Self::from_uri_attached(path, use_indexes, use_mvcc, use_views)?; + let db_opts = DatabaseOpts::new() + .with_mvcc(use_mvcc) + .with_indexes(use_indexes) + .with_views(use_views) + .with_strict(use_strict); + let db = Self::from_uri_attached(path, db_opts)?; let pager = Rc::new(db.init_pager(None)?); self.attached_databases diff --git a/core/translate/mod.rs b/core/translate/mod.rs index 6175aba72..288580494 100644 --- a/core/translate/mod.rs +++ b/core/translate/mod.rs @@ -189,6 +189,7 @@ pub fn translate_inner( if_not_exists, schema, syms, + connection, program, )?, ast::Stmt::CreateTrigger { .. } => bail_parse_error!("CREATE TRIGGER not supported yet"), diff --git a/core/translate/pragma.rs b/core/translate/pragma.rs index b04d0e87e..5e0338411 100644 --- a/core/translate/pragma.rs +++ b/core/translate/pragma.rs @@ -289,6 +289,7 @@ fn update_pragma( true, schema, syms, + &connection, program, )?; } diff --git a/core/translate/schema.rs b/core/translate/schema.rs index b40e19216..46c60fdfc 100644 --- a/core/translate/schema.rs +++ b/core/translate/schema.rs @@ -30,6 +30,7 @@ use crate::{bail_parse_error, Result}; use turso_ext::VTabKind; use turso_parser::ast::fmt::ToTokens; +#[allow(clippy::too_many_arguments)] pub fn translate_create_table( tbl_name: ast::QualifiedName, temporary: bool, @@ -37,11 +38,22 @@ pub fn translate_create_table( if_not_exists: bool, schema: &Schema, syms: &SymbolTable, + connection: &Arc, mut program: ProgramBuilder, ) -> Result { if temporary { bail_parse_error!("TEMPORARY table not supported yet"); } + + // Check for STRICT mode without experimental flag + if let ast::CreateTableBody::ColumnsAndConstraints { options, .. } = &body { + if options.contains(ast::TableOptions::STRICT) && !connection.experimental_strict_enabled() + { + bail_parse_error!( + "STRICT tables are an experimental feature. Enable them with --experimental-strict flag" + ); + } + } let opts = ProgramBuilderOpts { num_cursors: 1, approx_num_insns: 30, diff --git a/scripts/limbo-sqlite3 b/scripts/limbo-sqlite3 index 9054eca05..3f9f67cdf 100755 --- a/scripts/limbo-sqlite3 +++ b/scripts/limbo-sqlite3 @@ -7,7 +7,7 @@ PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" TURSODB="$PROJECT_ROOT/target/debug/tursodb" # Add experimental features for testing -EXPERIMENTAL_FLAGS="--experimental-views" +EXPERIMENTAL_FLAGS="--experimental-views --experimental-strict" # if RUST_LOG is non-empty, enable tracing output if [ -n "$RUST_LOG" ]; then diff --git a/sync/engine/src/database_sync_engine.rs b/sync/engine/src/database_sync_engine.rs index 9b49d3614..456af757f 100644 --- a/sync/engine/src/database_sync_engine.rs +++ b/sync/engine/src/database_sync_engine.rs @@ -133,9 +133,7 @@ impl DatabaseSyncEngine { main_db_path, db_file.clone(), OpenFlags::Create, - false, - true, - false, + turso_core::DatabaseOpts::new().with_indexes(true), ) .unwrap(); let tape_opts = DatabaseTapeOpts { @@ -179,9 +177,7 @@ impl DatabaseSyncEngine { &self.revert_db_wal_path, self.db_file.clone(), OpenFlags::Create, - false, - true, - false, + turso_core::DatabaseOpts::new().with_indexes(true), )?; let conn = db.connect()?; conn.wal_auto_checkpoint_disable(); diff --git a/tests/integration/common.rs b/tests/integration/common.rs index e15f022a0..48ea009dd 100644 --- a/tests/integration/common.rs +++ b/tests/integration/common.rs @@ -29,9 +29,7 @@ impl TempDatabase { io.clone(), path.to_str().unwrap(), turso_core::OpenFlags::default(), - false, - enable_indexes, - false, + turso_core::DatabaseOpts::new().with_indexes(enable_indexes), ) .unwrap(); Self { path, io, db } @@ -55,9 +53,7 @@ impl TempDatabase { io.clone(), db_path.to_str().unwrap(), flags, - false, - enable_indexes, - false, + turso_core::DatabaseOpts::new().with_indexes(enable_indexes), ) .unwrap(); Self { @@ -85,9 +81,7 @@ impl TempDatabase { io.clone(), path.to_str().unwrap(), turso_core::OpenFlags::default(), - false, - enable_indexes, - false, + turso_core::DatabaseOpts::new().with_indexes(enable_indexes), ) .unwrap(); diff --git a/tests/integration/functions/test_wal_api.rs b/tests/integration/functions/test_wal_api.rs index 069b0b084..4f3444cc8 100644 --- a/tests/integration/functions/test_wal_api.rs +++ b/tests/integration/functions/test_wal_api.rs @@ -870,9 +870,7 @@ fn test_db_share_same_file() { path.to_str().unwrap(), db_file.clone(), turso_core::OpenFlags::Create, - false, - true, - false, + turso_core::DatabaseOpts::new().with_indexes(true), ) .unwrap(); let conn1 = db1.connect().unwrap(); @@ -898,9 +896,7 @@ fn test_db_share_same_file() { &format!("{}-wal-copy", path.to_str().unwrap()), db_file.clone(), turso_core::OpenFlags::empty(), - false, - true, - false, + turso_core::DatabaseOpts::new().with_indexes(true), ) .unwrap(); let conn2 = db2.connect().unwrap(); diff --git a/tests/integration/query_processing/encryption.rs b/tests/integration/query_processing/encryption.rs index 19d9c342c..169677e7c 100644 --- a/tests/integration/query_processing/encryption.rs +++ b/tests/integration/query_processing/encryption.rs @@ -130,7 +130,7 @@ fn test_per_page_encryption() -> anyhow::Result<()> { "file:{}?cipher=aegis256&hexkey=b1bbfda4f589dc9daaf004fe21111e00dc00c98237102f5c7002a5669fc76327", db_path.to_str().unwrap() ); - let (_io, conn) = turso_core::Connection::from_uri(&uri, true, false, false)?; + let (_io, conn) = turso_core::Connection::from_uri(&uri, true, false, false, false)?; let mut row_count = 0; run_query_on_row(&tmp_db, &conn, "SELECT * FROM test", |row: &Row| { assert_eq!(row.get::(0).unwrap(), 1); diff --git a/tests/integration/query_processing/test_write_path.rs b/tests/integration/query_processing/test_write_path.rs index 60cd6495a..222d2deb4 100644 --- a/tests/integration/query_processing/test_write_path.rs +++ b/tests/integration/query_processing/test_write_path.rs @@ -706,9 +706,7 @@ fn test_wal_bad_frame() -> anyhow::Result<()> { io.clone(), db_path.to_str().unwrap(), turso_core::OpenFlags::default(), - false, - false, - false, + turso_core::DatabaseOpts::new().with_indexes(false), ) .unwrap(); let tmp_db = TempDatabase {