Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
17 changes: 17 additions & 0 deletions core/translate/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1815,6 +1815,23 @@ 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
57 changes: 57 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,27 @@ 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 +10315,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;
39 changes: 39 additions & 0 deletions tests/integration/functions/test_compile_options.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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