pub mod sqlite;
pub use sqlite::SqliteDialect;
pub trait Dialect: Send + Sync + 'static {
fn name(&self) -> &'static str;
fn parse(&self, sql: &str) -> crate::Result<(Option<turso_parser::ast::Cmd>, usize)>;
fn parse_table_sql(
&self,
sql: &str,
root_page: i64,
) -> crate::Result<crate::schema::BTreeTable>;
fn parse_table_sql_ast(&self, sql: &str) -> crate::Result<turso_parser::ast::Stmt>;
fn table_sql_for_replay(&self, sql: &str) -> crate::Result<String>;
fn format_table_sql(
&self,
input: &str,
tbl_name: &turso_parser::ast::QualifiedName,
body: &turso_parser::ast::CreateTableBody,
) -> crate::Result<String>;
fn format_rewritten_table_sql(&self, stmt: &turso_parser::ast::Stmt) -> crate::Result<String> {
let turso_parser::ast::Stmt::CreateTable { tbl_name, body, .. } = stmt else {
return Err(crate::LimboError::InternalError(
"format_rewritten_table_sql requires CREATE TABLE".to_string(),
));
};
self.format_table_sql(&stmt.to_string(), tbl_name, body)
}
fn register_catalog(
&self,
schema: &mut crate::schema::Schema,
enable_custom_types: bool,
) -> crate::Result<()>;
fn resolve_function(&self, name: &str, arg_count: usize) -> crate::Result<Option<crate::Func>>;
fn exec_scalar_function(
&self,
_conn: &crate::Connection,
name: &str,
_args: &[crate::Value],
) -> crate::Result<crate::Value> {
Err(crate::LimboError::ParseError(format!(
"no such function: {name}"
)))
}
fn requires_custom_types(&self) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::BTreeTable;
use crate::storage::database::DatabaseFile;
use crate::sync::atomic::{AtomicUsize, Ordering};
use crate::{Database, DatabaseOpts, MemoryIO, OpenFlags, IO};
use std::sync::Arc;
#[derive(Default)]
struct TestDialect {
parse_calls: AtomicUsize,
statement_parse_calls: AtomicUsize,
}
impl Dialect for TestDialect {
fn name(&self) -> &'static str {
"test"
}
fn parse(&self, sql: &str) -> crate::Result<(Option<turso_parser::ast::Cmd>, usize)> {
self.statement_parse_calls.fetch_add(1, Ordering::SeqCst);
if let Some(sql) = sql.strip_prefix("test: ") {
let (cmd, offset) = sqlite::parse(sql)?;
Ok((cmd, "test: ".len() + offset))
} else {
sqlite::parse(sql)
}
}
fn parse_table_sql(&self, sql: &str, root_page: i64) -> crate::Result<BTreeTable> {
self.parse_calls.fetch_add(1, Ordering::SeqCst);
let sql = sql.strip_prefix("/* test */ ").unwrap_or(sql);
BTreeTable::from_sql(sql, root_page)
}
fn parse_table_sql_ast(&self, sql: &str) -> crate::Result<turso_parser::ast::Stmt> {
let sql = sql.strip_prefix("/* test */ ").unwrap_or(sql);
sqlite::parse_table_sql_ast(sql)
}
fn table_sql_for_replay(&self, sql: &str) -> crate::Result<String> {
let sql = sql.strip_prefix("/* test */ ").unwrap_or(sql);
sqlite::table_sql_for_replay(sql)
}
fn format_table_sql(
&self,
input: &str,
_tbl_name: &turso_parser::ast::QualifiedName,
_body: &turso_parser::ast::CreateTableBody,
) -> crate::Result<String> {
Ok(format!("/* test */ {input}"))
}
fn resolve_function(
&self,
name: &str,
arg_count: usize,
) -> crate::Result<Option<crate::function::Func>> {
if name.eq_ignore_ascii_case("nvl") {
return sqlite::resolve_builtin_function("coalesce", arg_count);
}
if name.eq_ignore_ascii_case("test_add_one") && arg_count == 1 {
return Ok(Some(crate::function::Func::Dialect(
"test_add_one".to_string(),
)));
}
sqlite::resolve_builtin_function(name, arg_count)
}
fn exec_scalar_function(
&self,
_conn: &crate::Connection,
name: &str,
args: &[crate::Value],
) -> crate::Result<crate::Value> {
assert_eq!(name, "test_add_one");
let crate::Value::Numeric(crate::numeric::Numeric::Integer(v)) = args[0] else {
return Err(crate::LimboError::InvalidArgument(
"test_add_one expects an integer".to_string(),
));
};
Ok(crate::Value::Numeric(crate::numeric::Numeric::Integer(
v + 1,
)))
}
fn register_catalog(
&self,
schema: &mut crate::schema::Schema,
enable_custom_types: bool,
) -> crate::Result<()> {
sqlite::register_builtin_catalog(schema, enable_custom_types)?;
let vtab = crate::VirtualTable::new_internal(
"test_catalog".to_string(),
"CREATE TABLE test_catalog (value INTEGER)".to_string(),
turso_ext::VTabKind::VirtualTable,
Arc::new(crate::sync::RwLock::new(TestCatalogTable)),
)?;
schema.add_virtual_table(Arc::new(vtab))
}
}
struct StrictTestDialect;
impl StrictTestDialect {
const PREFIX: &'static str = "strict: ";
}
impl Dialect for StrictTestDialect {
fn name(&self) -> &'static str {
"strict-test"
}
fn parse(&self, sql: &str) -> crate::Result<(Option<turso_parser::ast::Cmd>, usize)> {
sqlite::parse(sql)
}
fn parse_table_sql(&self, sql: &str, root_page: i64) -> crate::Result<BTreeTable> {
let sql = sql.strip_prefix(Self::PREFIX).unwrap_or(sql);
BTreeTable::from_sql(sql, root_page)
}
fn parse_table_sql_ast(&self, sql: &str) -> crate::Result<turso_parser::ast::Stmt> {
let sql = sql.strip_prefix(Self::PREFIX).unwrap_or(sql);
sqlite::parse_table_sql_ast(sql)
}
fn table_sql_for_replay(&self, sql: &str) -> crate::Result<String> {
let sql = sql.strip_prefix(Self::PREFIX).unwrap_or(sql);
sqlite::table_sql_for_replay(sql)
}
fn format_table_sql(
&self,
input: &str,
_tbl_name: &turso_parser::ast::QualifiedName,
_body: &turso_parser::ast::CreateTableBody,
) -> crate::Result<String> {
Ok(format!("{}{input}", Self::PREFIX))
}
fn register_catalog(
&self,
schema: &mut crate::schema::Schema,
enable_custom_types: bool,
) -> crate::Result<()> {
sqlite::register_builtin_catalog(schema, enable_custom_types)
}
fn resolve_function(
&self,
name: &str,
arg_count: usize,
) -> crate::Result<Option<crate::function::Func>> {
sqlite::resolve_builtin_function(name, arg_count)
}
}
#[derive(Debug)]
struct TestCatalogTable;
impl crate::InternalVirtualTable for TestCatalogTable {
fn name(&self) -> String {
"test_catalog".to_string()
}
fn sql(&self) -> String {
"CREATE TABLE test_catalog (value INTEGER)".to_string()
}
fn open(
&self,
_conn: Arc<crate::Connection>,
) -> crate::Result<Arc<crate::sync::RwLock<dyn crate::InternalVirtualTableCursor>>>
{
Ok(Arc::new(crate::sync::RwLock::new(TestCatalogCursor {
row: 0,
})))
}
fn best_index(
&self,
constraints: &[turso_ext::ConstraintInfo],
_order_by: &[turso_ext::OrderByInfo],
) -> std::result::Result<turso_ext::IndexInfo, turso_ext::ResultCode> {
Ok(turso_ext::IndexInfo {
idx_num: 0,
idx_str: None,
order_by_consumed: false,
estimated_cost: 1.0,
estimated_rows: 1,
constraint_usages: constraints
.iter()
.map(|_| turso_ext::ConstraintUsage {
argv_index: None,
omit: false,
})
.collect(),
})
}
}
struct TestCatalogCursor {
row: usize,
}
impl crate::InternalVirtualTableCursor for TestCatalogCursor {
fn filter(
&mut self,
_args: &[crate::Value],
_idx_str: Option<String>,
_idx_num: i32,
) -> crate::Result<bool> {
self.row = 0;
Ok(true)
}
fn next(&mut self) -> crate::Result<bool> {
self.row += 1;
Ok(self.row < 1)
}
fn rowid(&self) -> i64 {
self.row as i64
}
fn column(&self, column: usize) -> crate::Result<crate::Value> {
match column {
0 => Ok(crate::Value::Numeric(crate::numeric::Numeric::Integer(42))),
_ => Ok(crate::Value::Null),
}
}
}
fn open_db(
io: &Arc<dyn IO>,
path: &str,
dialect: Arc<dyn Dialect>,
) -> crate::Result<Arc<Database>> {
let file = io.open_file(path, OpenFlags::Create, true)?;
let db_file = Arc::new(DatabaseFile::new(file));
Database::open(
io.clone(),
path,
crate::OpenOptions::new(dialect).storage(db_file),
)
}
#[test]
fn schema_load_routes_through_dialect() {
let io: Arc<dyn IO> = Arc::new(MemoryIO::new());
{
let db = open_db(&io, "dialect-load.db", Arc::new(SqliteDialect)).unwrap();
let conn = db.connect().unwrap();
conn.execute("CREATE TABLE t (x INTEGER)").unwrap();
conn.close().unwrap();
}
let dialect = Arc::new(TestDialect::default());
let db = open_db(&io, "dialect-load.db", dialect.clone()).unwrap();
assert!(dialect.parse_calls.load(Ordering::SeqCst) >= 1);
let conn = db.connect().unwrap();
let before = dialect.parse_calls.load(Ordering::SeqCst);
conn.execute("CREATE TABLE u (y INTEGER)").unwrap();
assert!(dialect.parse_calls.load(Ordering::SeqCst) > before);
conn.execute("INSERT INTO t VALUES (1)").unwrap();
conn.execute("INSERT INTO u VALUES (2)").unwrap();
conn.close().unwrap();
}
#[test]
fn dialect_parser_is_used_for_reprepare() {
let io: Arc<dyn IO> = Arc::new(MemoryIO::new());
let dialect = Arc::new(TestDialect::default());
let db = open_db(&io, "dialect-reprepare.db", dialect.clone()).unwrap();
let conn = db.connect().unwrap();
let mut stmt = conn.prepare("test: SELECT 42").unwrap();
conn.set_full_column_names(true);
let rows = stmt.run_collect_rows().unwrap();
assert_eq!(rows, vec![vec![crate::Value::from_i64(42)]]);
assert_eq!(
stmt.stmt_status(crate::StatementStatusCounter::Reprepare),
1
);
assert_eq!(dialect.statement_parse_calls.load(Ordering::SeqCst), 2);
conn.close().unwrap();
}
#[test]
fn query_runner_reports_invalid_utf8_once() {
let io: Arc<dyn IO> = Arc::new(MemoryIO::new());
let db = open_db(&io, "query-runner-invalid-utf8.db", Arc::new(SqliteDialect)).unwrap();
let conn = db.connect().unwrap();
let mut runner = conn.query_runner(b"SELECT 1;\xff");
let Some(Err(crate::LimboError::ParseError(message))) = runner.next() else {
panic!("invalid UTF-8 must produce a parse error");
};
assert!(message.contains("invalid UTF-8"));
assert!(runner.next().is_none());
conn.close().unwrap();
}
#[test]
fn query_runner_reports_parse_error_once() {
let io: Arc<dyn IO> = Arc::new(MemoryIO::new());
let db = open_db(&io, "query-runner-parse-error.db", Arc::new(SqliteDialect)).unwrap();
let conn = db.connect().unwrap();
let mut runner = conn.query_runner(b"SELECT * FROM");
assert!(runner.next().is_some_and(|result| result.is_err()));
assert!(runner.next().is_none());
conn.close().unwrap();
}
#[test]
fn dialect_catalog_available_on_every_schema_and_rebuild() {
let io: Arc<dyn IO> = Arc::new(MemoryIO::new());
let db = open_db(&io, "dialect-catalog.db", Arc::new(TestDialect::default())).unwrap();
let query_catalog = |conn: &Arc<crate::Connection>| -> Vec<Vec<crate::Value>> {
conn.prepare("SELECT value FROM test_catalog")
.unwrap()
.run_collect_rows()
.unwrap()
};
let conn1 = db.connect().unwrap();
let conn2 = db.connect().unwrap();
assert_eq!(
query_catalog(&conn1),
vec![vec![crate::Value::Numeric(
crate::numeric::Numeric::Integer(42)
)]]
);
assert_eq!(
query_catalog(&conn2),
vec![vec![crate::Value::Numeric(
crate::numeric::Numeric::Integer(42)
)]]
);
conn2.execute("CREATE TABLE t (x INTEGER)").unwrap();
assert_eq!(
query_catalog(&conn1),
vec![vec![crate::Value::Numeric(
crate::numeric::Numeric::Integer(42)
)]]
);
conn1.close().unwrap();
conn2.close().unwrap();
}
#[test]
fn dialect_catalog_cannot_be_dropped() {
let io: Arc<dyn IO> = Arc::new(MemoryIO::new());
let db = open_db(
&io,
"dialect-catalog-drop.db",
Arc::new(TestDialect::default()),
)
.unwrap();
let conn = db.connect().unwrap();
let error = conn.execute("DROP TABLE test_catalog").unwrap_err();
assert!(
error
.to_string()
.contains("table test_catalog may not be dropped"),
"unexpected error: {error}"
);
let new_conn = db.connect().unwrap();
for catalog_conn in [&conn, &new_conn] {
let rows = catalog_conn
.prepare("SELECT value FROM test_catalog")
.unwrap()
.run_collect_rows()
.unwrap();
assert_eq!(rows, vec![vec![crate::Value::from_i64(42)]]);
}
conn.close().unwrap();
new_conn.close().unwrap();
}
#[test]
fn dialect_catalog_survives_mvcc_recovery() {
let io: Arc<dyn IO> = Arc::new(MemoryIO::new());
let path = "dialect-catalog-mvcc-recovery.db";
{
let db = open_db(&io, path, Arc::new(TestDialect::default())).unwrap();
let conn = db.connect().unwrap();
conn.execute("PRAGMA journal_mode = mvcc").unwrap();
conn.execute("CREATE TABLE t (x INTEGER)").unwrap();
conn.close().unwrap();
}
let db = open_db(&io, path, Arc::new(TestDialect::default())).unwrap();
let conn = db.connect().unwrap();
let rows = conn
.prepare("SELECT value FROM test_catalog")
.unwrap()
.run_collect_rows()
.unwrap();
assert_eq!(rows, vec![vec![crate::Value::from_i64(42)]]);
conn.close().unwrap();
}
#[test]
fn dialect_catalog_available_in_initialized_temp_schema() {
let io: Arc<dyn IO> = Arc::new(MemoryIO::new());
let db = open_db(
&io,
"dialect-catalog-temp.db",
Arc::new(TestDialect::default()),
)
.unwrap();
for temp_store in ["MEMORY", "FILE"] {
let conn = db.connect().unwrap();
conn.execute(format!("PRAGMA temp_store = {temp_store}"))
.unwrap();
conn.execute("CREATE TEMP TABLE t (x INTEGER)").unwrap();
let rows = conn
.prepare("SELECT value FROM temp.test_catalog")
.unwrap()
.run_collect_rows()
.unwrap();
assert_eq!(rows, vec![vec![crate::Value::from_i64(42)]]);
conn.close().unwrap();
}
}
#[test]
fn create_table_stores_dialect_formatted_sql() {
let io: Arc<dyn IO> = Arc::new(MemoryIO::new());
{
let dialect = Arc::new(TestDialect::default());
let db = open_db(&io, "dialect-store.db", dialect).unwrap();
let conn = db.connect().unwrap();
let input = "CREATE TABLE t (x INTEGER)";
let stmt = match turso_parser::parser::Parser::new(input.as_bytes())
.next_cmd()
.unwrap()
.unwrap()
{
turso_parser::ast::Cmd::Stmt(stmt) => stmt,
other => panic!("unexpected command: {other:?}"),
};
conn.prepare_translated_stmt(stmt, input)
.unwrap()
.run_ignore_rows()
.unwrap();
let rows = conn
.prepare("SELECT sql FROM sqlite_schema WHERE name = 't'")
.unwrap()
.run_collect_rows()
.unwrap();
assert_eq!(rows.len(), 1);
let stored = rows[0][0].to_string();
assert_eq!(stored.trim_matches('\''), format!("/* test */ {input}"));
conn.close().unwrap();
}
let dialect = Arc::new(TestDialect::default());
let db = open_db(&io, "dialect-store.db", dialect.clone()).unwrap();
assert!(dialect.parse_calls.load(Ordering::SeqCst) >= 1);
let conn = db.connect().unwrap();
conn.execute("INSERT INTO t VALUES (1)").unwrap();
let rows = conn
.prepare("SELECT x FROM t")
.unwrap()
.run_collect_rows()
.unwrap();
assert_eq!(rows.len(), 1);
conn.close().unwrap();
}
struct NoFunctionsDialect;
impl Dialect for NoFunctionsDialect {
fn name(&self) -> &'static str {
"nofuncs"
}
fn parse(&self, sql: &str) -> crate::Result<(Option<turso_parser::ast::Cmd>, usize)> {
sqlite::parse(sql)
}
fn parse_table_sql(&self, sql: &str, root_page: i64) -> crate::Result<BTreeTable> {
BTreeTable::from_sql(sql, root_page)
}
fn parse_table_sql_ast(&self, sql: &str) -> crate::Result<turso_parser::ast::Stmt> {
sqlite::parse_table_sql_ast(sql)
}
fn table_sql_for_replay(&self, sql: &str) -> crate::Result<String> {
sqlite::table_sql_for_replay(sql)
}
fn format_table_sql(
&self,
_input: &str,
tbl_name: &turso_parser::ast::QualifiedName,
body: &turso_parser::ast::CreateTableBody,
) -> crate::Result<String> {
Ok(format!(
"CREATE TABLE {} {}",
tbl_name.name.as_ident(),
body
))
}
fn register_catalog(
&self,
schema: &mut crate::schema::Schema,
enable_custom_types: bool,
) -> crate::Result<()> {
sqlite::register_builtin_catalog(schema, enable_custom_types)
}
fn resolve_function(
&self,
_name: &str,
_arg_count: usize,
) -> crate::Result<Option<crate::function::Func>> {
Ok(None)
}
}
#[test]
fn dialect_scalar_function_resolves_and_executes() {
let io: Arc<dyn IO> = Arc::new(MemoryIO::new());
let db = open_db(&io, "dialect-funcs.db", Arc::new(TestDialect::default())).unwrap();
let conn = db.connect().unwrap();
let rows = conn
.prepare("SELECT test_add_one(41)")
.unwrap()
.run_collect_rows()
.unwrap();
assert_eq!(
rows,
vec![vec![crate::Value::Numeric(
crate::numeric::Numeric::Integer(42)
)]]
);
let rows = conn
.prepare("SELECT abs(-7)")
.unwrap()
.run_collect_rows()
.unwrap();
assert_eq!(
rows,
vec![vec![crate::Value::Numeric(
crate::numeric::Numeric::Integer(7)
)]]
);
let err = conn.prepare("SELECT no_such_function(1)").unwrap_err();
assert!(err.to_string().contains("no such function"));
conn.close().unwrap();
}
#[test]
fn dialect_function_alias_preserves_outer_join() {
let io: Arc<dyn IO> = Arc::new(MemoryIO::new());
let db = open_db(
&io,
"dialect-outer-join.db",
Arc::new(TestDialect::default()),
)
.unwrap();
let conn = db.connect().unwrap();
conn.execute("CREATE TABLE lhs (id INTEGER)").unwrap();
conn.execute("CREATE TABLE rhs (id INTEGER, value INTEGER)")
.unwrap();
conn.execute("INSERT INTO lhs VALUES (1), (2)").unwrap();
conn.execute("INSERT INTO rhs VALUES (1, 0)").unwrap();
let rows = conn
.prepare(
"SELECT lhs.id FROM lhs LEFT JOIN rhs ON rhs.id = lhs.id \
WHERE nvl(rhs.value, 1) = 1 ORDER BY lhs.id",
)
.unwrap()
.run_collect_rows()
.unwrap();
assert_eq!(rows, vec![vec![crate::Value::from_i64(2)]]);
conn.close().unwrap();
}
#[test]
fn dialect_owns_the_function_surface() {
let io: Arc<dyn IO> = Arc::new(MemoryIO::new());
let db = open_db(&io, "dialect-nofuncs.db", Arc::new(NoFunctionsDialect)).unwrap();
let conn = db.connect().unwrap();
conn.execute("CREATE TABLE t (x INTEGER)").unwrap();
conn.execute("INSERT INTO t VALUES (-7)").unwrap();
let err = conn.prepare("SELECT abs(x) FROM t").unwrap_err();
assert!(
err.to_string().contains("no such function"),
"unexpected error: {err}"
);
conn.close().unwrap();
}
#[test]
fn cdc_generated_functions_bypass_the_dialect() {
let io: Arc<dyn IO> = Arc::new(MemoryIO::new());
let db = open_db(&io, "dialect-cdc.db", Arc::new(NoFunctionsDialect)).unwrap();
let conn = db.connect().unwrap();
conn.execute("CREATE TABLE t (x INTEGER)").unwrap();
conn.execute("PRAGMA capture_data_changes_conn('full')")
.unwrap();
conn.execute("INSERT INTO t VALUES (7)").unwrap();
conn.execute("BEGIN").unwrap();
conn.execute("INSERT INTO t VALUES (8)").unwrap();
conn.execute("COMMIT").unwrap();
let rows = conn
.prepare(
"SELECT change_type, table_name, id, change_txn_id \
FROM turso_cdc ORDER BY change_id",
)
.unwrap()
.run_collect_rows()
.unwrap();
assert_eq!(rows.len(), 4);
assert_eq!(rows[0][0], crate::Value::from_i64(1));
assert_eq!(rows[0][1], crate::Value::build_text("t"));
assert_eq!(rows[0][2], crate::Value::from_i64(1));
assert_eq!(rows[1][0], crate::Value::from_i64(2));
assert_eq!(rows[1][1], crate::Value::Null);
assert_eq!(rows[1][2], crate::Value::Null);
assert_eq!(rows[0][3], rows[1][3]);
assert_eq!(rows[2][0], crate::Value::from_i64(1));
assert_eq!(rows[2][1], crate::Value::build_text("t"));
assert_eq!(rows[2][2], crate::Value::from_i64(2));
assert_eq!(rows[3][0], crate::Value::from_i64(2));
assert_eq!(rows[3][1], crate::Value::Null);
assert_eq!(rows[3][2], crate::Value::Null);
assert_eq!(rows[2][3], rows[3][3]);
conn.close().unwrap();
}
#[test]
fn alter_table_rewrites_dialect_formatted_sql() {
let io: Arc<dyn IO> = Arc::new(MemoryIO::new());
let db = open_db(&io, "dialect-alter-table.db", Arc::new(StrictTestDialect)).unwrap();
let conn = db.connect().unwrap();
conn.execute("CREATE TABLE t (x INTEGER)").unwrap();
conn.execute("ALTER TABLE t RENAME TO u").unwrap();
let rows = conn
.prepare("SELECT sql FROM sqlite_schema WHERE name = 'u'")
.unwrap()
.run_collect_rows()
.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(
rows[0][0].to_string().trim_matches('\''),
"strict: CREATE TABLE u (x INTEGER)"
);
conn.execute("INSERT INTO u VALUES (1)").unwrap();
conn.close().unwrap();
}
#[test]
fn alter_table_rename_column_decodes_dialect_formatted_sql() {
let io: Arc<dyn IO> = Arc::new(MemoryIO::new());
let db = open_db(&io, "dialect-alter-column.db", Arc::new(StrictTestDialect)).unwrap();
let conn = db.connect().unwrap();
conn.execute("CREATE TABLE t (x INTEGER)").unwrap();
conn.execute("ALTER TABLE t RENAME COLUMN x TO y").unwrap();
let rows = conn
.prepare("SELECT sql FROM sqlite_schema WHERE name = 't'")
.unwrap()
.run_collect_rows()
.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(
rows[0][0].to_string().trim_matches('\''),
"strict: CREATE TABLE t (y INTEGER)"
);
conn.execute("INSERT INTO t VALUES (1)").unwrap();
assert_eq!(
conn.prepare("SELECT y FROM t")
.unwrap()
.run_collect_rows()
.unwrap(),
vec![vec![crate::Value::from_i64(1)]]
);
conn.close().unwrap();
}
#[test]
fn registry_rejects_dialect_mismatch() {
let io: Arc<dyn IO> = Arc::new(MemoryIO::new());
let _db = open_db(&io, "dialect-mismatch.db", Arc::new(SqliteDialect)).unwrap();
let err =
open_db(&io, "dialect-mismatch.db", Arc::new(TestDialect::default())).unwrap_err();
assert!(
err.to_string().contains("already open with dialect"),
"unexpected error: {err}"
);
}
#[test]
fn registry_rejects_default_open_of_dialect_database() {
let io: Arc<dyn IO> = Arc::new(MemoryIO::new());
let _db = open_db(
&io,
"dialect-mismatch-reverse.db",
Arc::new(TestDialect::default()),
)
.unwrap();
let err = open_db(&io, "dialect-mismatch-reverse.db", Arc::new(SqliteDialect)).unwrap_err();
assert!(
err.to_string().contains("already open with dialect"),
"unexpected error: {err}"
);
}
#[cfg(feature = "fs")]
#[test]
fn shared_memory_registry_rejects_dialect_mismatch() {
let name = "dialect-shared-memory-mismatch";
let _db = Database::open_shared_memory(name, Arc::new(SqliteDialect)).unwrap();
let err = Database::open_shared_memory(name, Arc::new(TestDialect::default())).unwrap_err();
assert!(
err.to_string().contains("already open with dialect"),
"unexpected error: {err}"
);
}
#[cfg(all(feature = "fs", not(target_family = "wasm")))]
#[test]
fn vacuum_into_replays_schema_with_source_dialect() {
let dir = tempfile::tempdir().unwrap();
let source_path = dir.path().join("source.db");
let output_path = dir.path().join("output.db");
let io: Arc<dyn IO> = Arc::new(crate::io::PlatformIO::new().unwrap());
let db = Database::open_file_with_flags(
io.clone(),
source_path.to_str().unwrap(),
OpenFlags::Create,
DatabaseOpts::new(),
None,
Arc::new(StrictTestDialect),
)
.unwrap();
let conn = db.connect().unwrap();
conn.execute("CREATE TABLE t(x INTEGER)").unwrap();
conn.execute("INSERT INTO t VALUES (42)").unwrap();
conn.execute(format!("VACUUM INTO '{}'", output_path.display()))
.unwrap();
let output_db = Database::open_file(
io,
output_path.to_str().unwrap(),
Arc::new(StrictTestDialect),
)
.unwrap();
let output_conn = output_db.connect().unwrap();
let schema_rows = output_conn
.prepare("SELECT sql FROM sqlite_schema WHERE name = 't'")
.unwrap()
.run_collect_rows()
.unwrap();
assert_eq!(schema_rows.len(), 1);
assert_eq!(
schema_rows[0][0].to_string().trim_matches('\''),
"strict: CREATE TABLE t(x INTEGER)"
);
assert_eq!(
output_conn
.prepare("SELECT x FROM t")
.unwrap()
.run_collect_rows()
.unwrap(),
vec![vec![crate::Value::from_i64(42)]]
);
}
#[cfg(all(feature = "fs", not(target_family = "wasm")))]
#[test]
fn vacuum_attached_database_strips_source_schema_from_replay() {
let dir = tempfile::tempdir().unwrap();
let source_path = dir.path().join("source.db");
let attached_path = dir.path().join("attached.db");
let output_path = dir.path().join("output.db");
let io: Arc<dyn IO> = Arc::new(crate::io::PlatformIO::new().unwrap());
let db = Database::open_file_with_flags(
io.clone(),
source_path.to_str().unwrap(),
OpenFlags::Create,
DatabaseOpts::new().with_attach(true),
None,
Arc::new(StrictTestDialect),
)
.unwrap();
let conn = db.connect().unwrap();
conn.execute(format!(
"ATTACH DATABASE '{}' AS aux",
attached_path.display()
))
.unwrap();
conn.execute("CREATE TABLE aux.t (x INTEGER)").unwrap();
conn.execute("INSERT INTO aux.t VALUES (42)").unwrap();
conn.execute(format!("VACUUM aux INTO '{}'", output_path.display()))
.unwrap();
let output_db = Database::open_file(
io,
output_path.to_str().unwrap(),
Arc::new(StrictTestDialect),
)
.unwrap();
let output_conn = output_db.connect().unwrap();
assert_eq!(
output_conn
.prepare("SELECT x FROM t")
.unwrap()
.run_collect_rows()
.unwrap(),
vec![vec![crate::Value::from_i64(42)]]
);
}
#[cfg(all(feature = "fs", not(target_family = "wasm")))]
#[test]
fn in_place_vacuum_replays_schema_with_source_dialect() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("source.db");
let io: Arc<dyn IO> = Arc::new(crate::io::PlatformIO::new().unwrap());
let db = Database::open_file_with_flags(
io,
path.to_str().unwrap(),
OpenFlags::Create,
DatabaseOpts::new().with_vacuum(true),
None,
Arc::new(StrictTestDialect),
)
.unwrap();
let conn = db.connect().unwrap();
conn.execute("CREATE TABLE t(x INTEGER)").unwrap();
conn.execute("INSERT INTO t VALUES (42)").unwrap();
conn.execute("VACUUM").unwrap();
let schema_rows = conn
.prepare("SELECT sql FROM sqlite_schema WHERE name = 't'")
.unwrap()
.run_collect_rows()
.unwrap();
assert_eq!(schema_rows.len(), 1);
assert_eq!(
schema_rows[0][0].to_string().trim_matches('\''),
"strict: CREATE TABLE t(x INTEGER)"
);
assert_eq!(
conn.prepare("SELECT x FROM t")
.unwrap()
.run_collect_rows()
.unwrap(),
vec![vec![crate::Value::from_i64(42)]]
);
}
}