Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions COMPAT.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,8 +250,8 @@ Feature support of [sqlite expr syntax](https://www.sqlite.org/lang_expr.html).
| rtrim(X,Y) | Yes | |
| sign(X) | Yes | |
| soundex(X) | Yes | |
| sqlite_compileoption_get(N) | No | |
| sqlite_compileoption_used(X) | No | |
| sqlite_compileoption_get(N) | Yes | |
| sqlite_compileoption_used(X) | Yes | |
| sqlite_offset(X) | No | |
| sqlite_source_id() | Yes | |
| sqlite_version() | Yes | |
Expand Down
8 changes: 8 additions & 0 deletions core/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,8 @@ pub enum ScalarFunc {
SqliteVersion,
TursoVersion,
SqliteSourceId,
SqliteCompileOptionGet,
SqliteCompileOptionUsed,
UnixEpoch,
JulianDay,
Hex,
Expand Down Expand Up @@ -384,6 +386,8 @@ impl ScalarFunc {
ScalarFunc::SqliteVersion => true,
ScalarFunc::TursoVersion => true,
ScalarFunc::SqliteSourceId => true,
ScalarFunc::SqliteCompileOptionGet => true,
ScalarFunc::SqliteCompileOptionUsed => true,
ScalarFunc::UnixEpoch => false,
ScalarFunc::JulianDay => false,
ScalarFunc::Hex => true,
Expand Down Expand Up @@ -452,6 +456,8 @@ impl Display for ScalarFunc {
Self::SqliteVersion => "sqlite_version".to_string(),
Self::TursoVersion => "turso_version".to_string(),
Self::SqliteSourceId => "sqlite_source_id".to_string(),
Self::SqliteCompileOptionGet => "sqlite_compileoption_get".to_string(),
Self::SqliteCompileOptionUsed => "sqlite_compileoption_used".to_string(),
Self::JulianDay => "julianday".to_string(),
Self::UnixEpoch => "unixepoch".to_string(),
Self::Hex => "hex".to_string(),
Expand Down Expand Up @@ -790,6 +796,8 @@ impl Func {
"sqlite_version" => Ok(Self::Scalar(ScalarFunc::SqliteVersion)),
"turso_version" => Ok(Self::Scalar(ScalarFunc::TursoVersion)),
"sqlite_source_id" => Ok(Self::Scalar(ScalarFunc::SqliteSourceId)),
"sqlite_compileoption_get" => Ok(Self::Scalar(ScalarFunc::SqliteCompileOptionGet)),
"sqlite_compileoption_used" => Ok(Self::Scalar(ScalarFunc::SqliteCompileOptionUsed)),
"replace" => Ok(Self::Scalar(ScalarFunc::Replace)),
"likely" => Ok(Self::Scalar(ScalarFunc::Likely)),
"likelihood" => Ok(Self::Scalar(ScalarFunc::Likelihood)),
Expand Down
16 changes: 16 additions & 0 deletions core/translate/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1815,6 +1815,22 @@ pub fn translate_expr(
});
Ok(target_register)
}
ScalarFunc::SqliteCompileOptionGet | ScalarFunc::SqliteCompileOptionUsed => {
if args.len() != 1 {
crate::bail_parse_error!(
"{} function must have exactly 1 argument",
srf
);
}
translate_function(
program,
args,
referenced_tables,
resolver,
target_register,
func_ctx,
)
}
ScalarFunc::Replace => {
if !args.len() == 3 {
crate::bail_parse_error!(
Expand Down
55 changes: 55 additions & 0 deletions core/vdbe/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5248,6 +5248,21 @@ pub fn op_function(
);
state.registers[*dest] = Register::Value(Value::build_text(src_id));
}
ScalarFunc::SqliteCompileOptionGet => {
assert_eq!(arg_count, 1);
let n = extract_int_value(state.registers[*start_reg].get_value());
let option = execute_sqlite_compileoption_get(n as i32);
state.registers[*dest] = match option {
Some(opt) => Register::Value(Value::build_text(opt)),
None => Register::Value(Value::Null),
};
}
ScalarFunc::SqliteCompileOptionUsed => {
assert_eq!(arg_count, 1);
let name = state.registers[*start_reg].get_value().to_string();
let used = execute_sqlite_compileoption_used(&name);
state.registers[*dest] = Register::Value(Value::Integer(if used { 1 } else { 0 }));
}
ScalarFunc::Replace => {
assert_eq!(arg_count, 3);
let source = &state.registers[*start_reg];
Expand Down Expand Up @@ -9819,6 +9834,25 @@ fn try_float_to_integer_affinity(value: &mut Value, fl: f64) -> bool {
false
}

const COMPILE_OPTIONS: &[&str] = &[
#[cfg(feature = "json")]
"ENABLE_JSON1",
"SYSTEM_MALLOC",
"THREADSAFE=1",
];

fn execute_sqlite_compileoption_get(n: i32) -> Option<String> {
if n >= 0 && (n as usize) < COMPILE_OPTIONS.len() {
Some(COMPILE_OPTIONS[n as usize].to_string())
} else {
None
}
}

fn execute_sqlite_compileoption_used(name: &str) -> bool {
COMPILE_OPTIONS.iter().any(|opt| opt.eq_ignore_ascii_case(name))
}

// Compat for applications that test for SQLite.
fn execute_sqlite_version() -> String {
"3.50.4".to_string()
Expand Down Expand Up @@ -10279,6 +10313,27 @@ mod tests {
assert_eq!(execute_turso_version(version_integer), expected);
}

#[test]
fn test_execute_sqlite_compileoption() {
// Test used
assert!(execute_sqlite_compileoption_used("SYSTEM_MALLOC"));
assert!(execute_sqlite_compileoption_used("THREADSAFE=1"));
assert!(execute_sqlite_compileoption_used("system_malloc")); // Case insensitive
assert!(!execute_sqlite_compileoption_used("NON_EXISTENT_OPTION"));

// Test get
let opt0 = execute_sqlite_compileoption_get(0);
assert!(opt0.is_some());
let opt0 = opt0.unwrap();
assert!(execute_sqlite_compileoption_used(&opt0));

let opt_last = execute_sqlite_compileoption_get((COMPILE_OPTIONS.len() - 1) as i32);
assert!(opt_last.is_some());

let opt_out_of_bounds = execute_sqlite_compileoption_get(COMPILE_OPTIONS.len() as i32);
assert!(opt_out_of_bounds.is_none());
}

#[test]
fn test_ascii_whitespace_is_trimmed() {
// Regular ASCII whitespace SHOULD be trimmed
Expand Down
18 changes: 18 additions & 0 deletions core/vdbe/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1474,6 +1474,24 @@ impl<'a> FromValueRow<'a> for &'a Value {
}
}

impl<'a> FromValueRow<'a> for Value {
fn from_value(value: &'a Value) -> Result<Self> {
Ok(value.clone())
}
}

impl<'a, T> FromValueRow<'a> for Option<T>
where
T: FromValueRow<'a> + 'a,
{
fn from_value(value: &'a Value) -> Result<Self> {
match value {
Value::Null => Ok(None),
_ => T::from_value(value).map(Some),
}
}
}

impl Row {
pub fn get<'a, T: FromValueRow<'a> + 'a>(&'a self, idx: usize) -> Result<T> {
let value = unsafe {
Expand Down
1 change: 1 addition & 0 deletions tests/integration/functions/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod test_cdc;
mod test_compile_options;
mod test_function_rowid;
mod test_sum;
mod test_wal_api;
38 changes: 38 additions & 0 deletions tests/integration/functions/test_compile_options.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use crate::common::{ExecRows, TempDatabase};

#[turso_macros::test]
fn test_sqlite_compileoption_used(tmp_db: TempDatabase) {
let conn = tmp_db.connect_limbo();

// Test known option
let rows: Vec<(i64,)> = conn.exec_rows("SELECT sqlite_compileoption_used('THREADSAFE=1')");
assert_eq!(rows, vec![(1,)]);

// Test case insensitivity
let rows: Vec<(i64,)> = conn.exec_rows("SELECT sqlite_compileoption_used('threadsafe=1')");
assert_eq!(rows, vec![(1,)]);

// Test unknown option
let rows: Vec<(i64,)> = conn.exec_rows("SELECT sqlite_compileoption_used('NON_EXISTENT_OPTION')");
assert_eq!(rows, vec![(0,)]);
}

#[turso_macros::test]
fn test_sqlite_compileoption_get(tmp_db: TempDatabase) {
let conn = tmp_db.connect_limbo();

// Test getting the first option
let rows: Vec<(String,)> = conn.exec_rows("SELECT sqlite_compileoption_get(0)");
assert!(!rows.is_empty());
let first_option = &rows[0].0;
assert!(!first_option.is_empty());

// Verify the retrieved option is considered "used"
let sql = format!("SELECT sqlite_compileoption_used('{}')", first_option);
let rows: Vec<(i64,)> = conn.exec_rows(&sql);
assert_eq!(rows, vec![(1,)]);

// Test out of bounds (should return NULL)
let rows: Vec<(Option<String>,)> = conn.exec_rows("SELECT sqlite_compileoption_get(1000000)");
assert_eq!(rows, vec![(None,)]);
}
Loading