core: Make strict schema support experimental

It's not tested properly so let's mark it as experimental for now.

Fixes #2775
This commit is contained in:
Pekka Enberg
2025-09-02 15:19:18 +03:00
parent 8f7e43b32b
commit 12cf4d2e72
14 changed files with 136 additions and 112 deletions

View File

@@ -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 | |

View File

@@ -317,7 +317,7 @@ impl Drop for Connection {
#[allow(clippy::arc_with_non_send_sync)]
#[pyfunction(signature = (path))]
pub fn connect(path: &str) -> Result<Connection> {
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::<ProgrammingError, _>(format!(
"Failed to create connection: {e:?}"

View File

@@ -66,6 +66,8 @@ pub struct Opts {
pub experimental_views: bool,
#[clap(long, help = "Enable experimental indexing feature")]
pub experimental_indexes: Option<bool>,
#[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<String>,
#[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)

View File

@@ -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,

View File

@@ -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<T, E = LimboError> = std::result::Result<T, E>;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
@@ -144,7 +190,7 @@ pub struct Database {
init_lock: Arc<Mutex<()>>,
open_flags: OpenFlags,
builtin_syms: RefCell<SymbolTable>,
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<dyn IO>,
path: &str,
flags: OpenFlags,
enable_mvcc: bool,
enable_indexes: bool,
enable_views: bool,
opts: DatabaseOpts,
) -> Result<Arc<Database>> {
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<dyn DatabaseStorage>,
flags: OpenFlags,
enable_mvcc: bool,
enable_indexes: bool,
enable_views: bool,
opts: DatabaseOpts,
) -> Result<Arc<Database>> {
// 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<dyn IO>,
@@ -320,36 +350,23 @@ impl Database {
wal_path: &str,
db_file: Arc<dyn DatabaseStorage>,
flags: OpenFlags,
enable_mvcc: bool,
enable_indexes: bool,
enable_views: bool,
opts: DatabaseOpts,
) -> Result<Arc<Database>> {
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<dyn IO>,
path: &str,
wal_path: &str,
db_file: Arc<dyn DatabaseStorage>,
flags: OpenFlags,
enable_mvcc: bool,
enable_indexes: bool,
enable_views: bool,
opts: DatabaseOpts,
) -> Result<Arc<Database>> {
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<S>,
flags: OpenFlags,
indexes: bool,
mvcc: bool,
views: bool,
opts: DatabaseOpts,
) -> Result<(Arc<dyn IO>, Arc<Database>)>
where
S: AsRef<str> + 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<dyn IO>, Arc<Connection>)> {
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<Arc<Database>> {
fn from_uri_attached(uri: &str, db_opts: DatabaseOpts) -> Result<Arc<Database>> {
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

View File

@@ -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"),

View File

@@ -289,6 +289,7 @@ fn update_pragma(
true,
schema,
syms,
&connection,
program,
)?;
}

View File

@@ -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<crate::Connection>,
mut program: ProgramBuilder,
) -> Result<ProgramBuilder> {
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,

View File

@@ -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

View File

@@ -133,9 +133,7 @@ impl<P: ProtocolIO, Ctx> DatabaseSyncEngine<P, Ctx> {
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<P: ProtocolIO, Ctx> DatabaseSyncEngine<P, Ctx> {
&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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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::<i64>(0).unwrap(), 1);

View File

@@ -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 {