mirror of
https://github.com/aljazceru/turso.git
synced 2025-12-17 00:24:21 +01:00
508 lines
17 KiB
Rust
508 lines
17 KiB
Rust
use crate::{Connection, LimboError, Statement, StepResult, Value};
|
|
use bitflags::bitflags;
|
|
use std::sync::Arc;
|
|
use strum::IntoEnumIterator;
|
|
use turso_ext::{ConstraintInfo, ConstraintOp, ConstraintUsage, IndexInfo, ResultCode};
|
|
use turso_parser::ast::PragmaName;
|
|
|
|
bitflags! {
|
|
// Flag names match those used in SQLite:
|
|
// https://github.com/sqlite/sqlite/blob/b3c1884b65400da85636458298bd77cbbfdfb401/tool/mkpragmatab.tcl#L22-L29
|
|
pub struct PragmaFlags: u8 {
|
|
const NeedSchema = 0x01; /* Force schema load before running */
|
|
const NoColumns = 0x02; /* OP_ResultRow called with zero columns */
|
|
const NoColumns1 = 0x04; /* zero columns if RHS argument is present */
|
|
const ReadOnly = 0x08; /* Read-only HEADER_VALUE */
|
|
const Result0 = 0x10; /* Acts as query when no argument */
|
|
const Result1 = 0x20; /* Acts as query when has one argument */
|
|
const SchemaOpt = 0x40; /* Schema restricts name search if present */
|
|
const SchemaReq = 0x80; /* Schema required - "main" is default */
|
|
}
|
|
}
|
|
|
|
pub struct Pragma {
|
|
pub flags: PragmaFlags,
|
|
pub columns: &'static [&'static str],
|
|
}
|
|
|
|
impl Pragma {
|
|
const fn new(flags: PragmaFlags, columns: &'static [&'static str]) -> Self {
|
|
Self { flags, columns }
|
|
}
|
|
}
|
|
|
|
pub fn pragma_for(pragma: &PragmaName) -> Pragma {
|
|
use PragmaName::*;
|
|
|
|
match pragma {
|
|
ApplicationId => Pragma::new(
|
|
PragmaFlags::NoColumns1 | PragmaFlags::Result0,
|
|
&["application_id"],
|
|
),
|
|
CacheSize => Pragma::new(
|
|
PragmaFlags::NeedSchema
|
|
| PragmaFlags::Result0
|
|
| PragmaFlags::SchemaReq
|
|
| PragmaFlags::NoColumns1,
|
|
&["cache_size"],
|
|
),
|
|
DataSyncRetry => Pragma::new(
|
|
PragmaFlags::Result0 | PragmaFlags::NoColumns1,
|
|
&["data_sync_retry"],
|
|
),
|
|
DatabaseList => Pragma::new(PragmaFlags::Result0, &["seq", "name", "file"]),
|
|
Encoding => Pragma::new(
|
|
PragmaFlags::Result0 | PragmaFlags::NoColumns1,
|
|
&["encoding"],
|
|
),
|
|
JournalMode => Pragma::new(
|
|
PragmaFlags::NeedSchema | PragmaFlags::Result0 | PragmaFlags::SchemaReq,
|
|
&["journal_mode"],
|
|
),
|
|
LegacyFileFormat => {
|
|
unreachable!("pragma_for() called with LegacyFileFormat, which is unsupported")
|
|
}
|
|
ModuleList => Pragma::new(
|
|
PragmaFlags::NeedSchema | PragmaFlags::Result0 | PragmaFlags::SchemaReq,
|
|
&["module_list"],
|
|
),
|
|
PageCount => Pragma::new(
|
|
PragmaFlags::NeedSchema | PragmaFlags::Result0 | PragmaFlags::SchemaReq,
|
|
&["page_count"],
|
|
),
|
|
PageSize => Pragma::new(
|
|
PragmaFlags::Result0 | PragmaFlags::SchemaReq | PragmaFlags::NoColumns1,
|
|
&["page_size"],
|
|
),
|
|
MaxPageCount => Pragma::new(
|
|
PragmaFlags::NeedSchema
|
|
| PragmaFlags::Result0
|
|
| PragmaFlags::SchemaReq
|
|
| PragmaFlags::NoColumns1,
|
|
&["max_page_count"],
|
|
),
|
|
SchemaVersion => Pragma::new(
|
|
PragmaFlags::NoColumns1 | PragmaFlags::Result0,
|
|
&["schema_version"],
|
|
),
|
|
Synchronous => Pragma::new(
|
|
PragmaFlags::NoColumns1 | PragmaFlags::Result0,
|
|
&["synchronous"],
|
|
),
|
|
TableInfo => Pragma::new(
|
|
PragmaFlags::NeedSchema | PragmaFlags::Result1 | PragmaFlags::SchemaOpt,
|
|
&["cid", "name", "type", "notnull", "dflt_value", "pk"],
|
|
),
|
|
UserVersion => Pragma::new(
|
|
PragmaFlags::NoColumns1 | PragmaFlags::Result0,
|
|
&["user_version"],
|
|
),
|
|
WalCheckpoint => Pragma::new(PragmaFlags::NeedSchema, &["busy", "log", "checkpointed"]),
|
|
AutoVacuum => Pragma::new(
|
|
PragmaFlags::NoColumns1 | PragmaFlags::Result0,
|
|
&["auto_vacuum"],
|
|
),
|
|
BusyTimeout => Pragma::new(
|
|
PragmaFlags::NoColumns1 | PragmaFlags::Result0,
|
|
&["busy_timeout"],
|
|
),
|
|
IntegrityCheck => Pragma::new(
|
|
PragmaFlags::NeedSchema | PragmaFlags::ReadOnly | PragmaFlags::Result0,
|
|
&["message"],
|
|
),
|
|
UnstableCaptureDataChangesConn => Pragma::new(
|
|
PragmaFlags::NeedSchema | PragmaFlags::Result0 | PragmaFlags::SchemaReq,
|
|
&["mode", "table"],
|
|
),
|
|
QueryOnly => Pragma::new(
|
|
PragmaFlags::Result0 | PragmaFlags::NoColumns1,
|
|
&["query_only"],
|
|
),
|
|
FreelistCount => Pragma::new(PragmaFlags::Result0, &["freelist_count"]),
|
|
EncryptionKey => Pragma::new(
|
|
PragmaFlags::Result0 | PragmaFlags::SchemaReq | PragmaFlags::NoColumns1,
|
|
&["hexkey"],
|
|
),
|
|
EncryptionCipher => Pragma::new(
|
|
PragmaFlags::Result0 | PragmaFlags::SchemaReq | PragmaFlags::NoColumns1,
|
|
&["cipher"],
|
|
),
|
|
PragmaName::MvccCheckpointThreshold => Pragma::new(
|
|
PragmaFlags::NoColumns1 | PragmaFlags::Result0,
|
|
&["mvcc_checkpoint_threshold"],
|
|
),
|
|
ForeignKeys => Pragma::new(
|
|
PragmaFlags::NoColumns1 | PragmaFlags::Result0,
|
|
&["foreign_keys"],
|
|
),
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub(crate) struct PragmaVirtualTable {
|
|
pub(crate) pragma_name: String,
|
|
visible_column_count: usize,
|
|
max_arg_count: usize,
|
|
has_pragma_arg: bool,
|
|
}
|
|
|
|
impl PragmaVirtualTable {
|
|
pub(crate) fn functions() -> Vec<(PragmaVirtualTable, String)> {
|
|
PragmaName::iter()
|
|
.filter(|name| *name != PragmaName::LegacyFileFormat)
|
|
.filter_map(|name| {
|
|
let pragma = pragma_for(&name);
|
|
if pragma
|
|
.flags
|
|
.intersects(PragmaFlags::Result0 | PragmaFlags::Result1)
|
|
{
|
|
Some(Self::create(name.to_string(), pragma))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn create(pragma_name: String, pragma: Pragma) -> (Self, String) {
|
|
let mut max_arg_count = 0;
|
|
let mut has_pragma_arg = false;
|
|
|
|
let mut sql = String::from("CREATE TABLE x (");
|
|
let col_defs = pragma
|
|
.columns
|
|
.iter()
|
|
.map(|col| format!("\"{col}\""))
|
|
.collect::<Vec<_>>()
|
|
.join(", ");
|
|
sql.push_str(&col_defs);
|
|
if pragma.flags.contains(PragmaFlags::Result1) {
|
|
sql.push_str(", arg HIDDEN");
|
|
max_arg_count += 1;
|
|
has_pragma_arg = true;
|
|
}
|
|
if pragma
|
|
.flags
|
|
.intersects(PragmaFlags::SchemaOpt | PragmaFlags::SchemaReq)
|
|
{
|
|
sql.push_str(", schema HIDDEN");
|
|
max_arg_count += 1;
|
|
}
|
|
sql.push(')');
|
|
|
|
(
|
|
PragmaVirtualTable {
|
|
pragma_name,
|
|
visible_column_count: pragma.columns.len(),
|
|
max_arg_count,
|
|
has_pragma_arg,
|
|
},
|
|
sql,
|
|
)
|
|
}
|
|
|
|
pub(crate) fn open(&self, conn: Arc<Connection>) -> crate::Result<PragmaVirtualTableCursor> {
|
|
Ok(PragmaVirtualTableCursor {
|
|
pragma_name: self.pragma_name.clone(),
|
|
pos: 0,
|
|
conn,
|
|
stmt: None,
|
|
arg: None,
|
|
visible_column_count: self.visible_column_count,
|
|
max_arg_count: self.max_arg_count,
|
|
has_pragma_arg: self.has_pragma_arg,
|
|
})
|
|
}
|
|
|
|
pub(crate) fn best_index(
|
|
&self,
|
|
constraints: &[ConstraintInfo],
|
|
) -> Result<IndexInfo, ResultCode> {
|
|
let mut arg0_idx = None;
|
|
let mut arg1_idx = None;
|
|
|
|
for (i, c) in constraints.iter().enumerate() {
|
|
if c.op != ConstraintOp::Eq {
|
|
continue;
|
|
}
|
|
let visible_count = self.visible_column_count as u32;
|
|
if c.column_index < visible_count {
|
|
continue;
|
|
}
|
|
if !c.usable {
|
|
return Err(ResultCode::ConstraintViolation);
|
|
}
|
|
let hidden_idx = c.column_index - visible_count;
|
|
match hidden_idx {
|
|
0 => arg0_idx = Some(i),
|
|
1 => arg1_idx = Some(i),
|
|
_ => unreachable!("Unexpected hidden column index: {}", hidden_idx),
|
|
}
|
|
}
|
|
|
|
let argv_arg0_idx = arg0_idx.map_or(0, |_| 1);
|
|
let argv_arg1_idx = arg1_idx.map_or(argv_arg0_idx, |_| argv_arg0_idx + 1);
|
|
|
|
let constraint_usages = constraints
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, _)| {
|
|
let argv_index = if Some(i) == arg0_idx {
|
|
Some(argv_arg0_idx)
|
|
} else if Some(i) == arg1_idx {
|
|
Some(argv_arg1_idx)
|
|
} else {
|
|
None
|
|
};
|
|
ConstraintUsage {
|
|
argv_index,
|
|
omit: argv_index.is_some(),
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
Ok(IndexInfo {
|
|
constraint_usages,
|
|
..Default::default()
|
|
})
|
|
}
|
|
}
|
|
|
|
pub struct PragmaVirtualTableCursor {
|
|
pragma_name: String,
|
|
pos: usize,
|
|
conn: Arc<Connection>,
|
|
stmt: Option<Statement>,
|
|
arg: Option<String>,
|
|
visible_column_count: usize,
|
|
max_arg_count: usize,
|
|
has_pragma_arg: bool,
|
|
}
|
|
|
|
impl PragmaVirtualTableCursor {
|
|
pub(crate) fn rowid(&self) -> i64 {
|
|
self.pos as i64
|
|
}
|
|
|
|
pub(crate) fn next(&mut self) -> crate::Result<bool> {
|
|
let stmt = self
|
|
.stmt
|
|
.as_mut()
|
|
.ok_or_else(|| LimboError::InternalError("Statement is missing".into()))?;
|
|
let result = stmt.step()?;
|
|
match result {
|
|
StepResult::Done => Ok(false),
|
|
_ => {
|
|
self.pos += 1;
|
|
Ok(true)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn column(&self, idx: usize) -> crate::Result<Value> {
|
|
if idx < self.visible_column_count {
|
|
let value = self
|
|
.stmt
|
|
.as_ref()
|
|
.ok_or_else(|| LimboError::InternalError("Statement is missing".into()))?
|
|
.row()
|
|
.ok_or_else(|| LimboError::InternalError("No row available".into()))?
|
|
.get_value(idx)
|
|
.clone();
|
|
return Ok(value);
|
|
}
|
|
|
|
let value = match idx - self.visible_column_count {
|
|
0 => self
|
|
.arg
|
|
.as_ref()
|
|
.map_or(Value::Null, |arg| Value::from_text(arg.to_string())),
|
|
_ => Value::Null,
|
|
};
|
|
Ok(value)
|
|
}
|
|
|
|
pub(crate) fn filter(&mut self, args: Vec<Value>) -> crate::Result<bool> {
|
|
if args.len() > self.max_arg_count {
|
|
return Err(LimboError::ParseError(format!(
|
|
"Too many arguments for pragma {}: expected at most {}, got {}",
|
|
self.pragma_name,
|
|
self.max_arg_count,
|
|
args.len()
|
|
)));
|
|
}
|
|
|
|
let to_text = |v: &Value| v.to_text().map(str::to_owned);
|
|
let (arg, schema) = match args.as_slice() {
|
|
[arg0] if self.has_pragma_arg => (to_text(arg0), None),
|
|
[arg0] => (None, to_text(arg0)),
|
|
[arg0, arg1] => (to_text(arg0), to_text(arg1)),
|
|
_ => (None, None),
|
|
};
|
|
|
|
self.arg = arg;
|
|
|
|
if let Some(schema) = schema {
|
|
// Schema-qualified PRAGMA statements are not supported yet
|
|
return Err(LimboError::ParseError(format!(
|
|
"Schema argument is not supported yet (got schema: '{schema}')"
|
|
)));
|
|
}
|
|
|
|
let mut sql = format!("PRAGMA {}", self.pragma_name);
|
|
if let Some(arg) = &self.arg {
|
|
sql.push_str(&format!("=\"{arg}\""));
|
|
}
|
|
|
|
self.stmt = Some(self.conn.prepare(sql)?);
|
|
|
|
self.next()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_best_index_argv_order_both_hidden_constraints() {
|
|
// Test when both hidden constraints are present (arg and schema)
|
|
let pragma_vtab = PragmaVirtualTable {
|
|
pragma_name: "table_info".to_string(),
|
|
visible_column_count: 6,
|
|
max_arg_count: 2,
|
|
has_pragma_arg: true,
|
|
};
|
|
|
|
let constraints = vec![
|
|
usable_constraint(6), // arg (first hidden column)
|
|
usable_constraint(7), // schema (second hidden column)
|
|
];
|
|
|
|
let index_info = pragma_vtab.best_index(&constraints).unwrap();
|
|
|
|
// Verify arg gets argv_index 1, schema gets argv_index 2
|
|
assert_eq!(index_info.constraint_usages[0].argv_index, Some(1)); // arg
|
|
assert_eq!(index_info.constraint_usages[1].argv_index, Some(2)); // schema
|
|
assert!(index_info.constraint_usages[0].omit);
|
|
assert!(index_info.constraint_usages[1].omit);
|
|
}
|
|
|
|
#[test]
|
|
fn test_best_index_argv_order_only_arg() {
|
|
let pragma_vtab = PragmaVirtualTable {
|
|
pragma_name: "table_info".to_string(),
|
|
visible_column_count: 6,
|
|
max_arg_count: 2,
|
|
has_pragma_arg: true,
|
|
};
|
|
|
|
let constraints = vec![
|
|
usable_constraint(6), // arg (first hidden column)
|
|
];
|
|
|
|
let index_info = pragma_vtab.best_index(&constraints).unwrap();
|
|
|
|
// Verify arg gets argv_index 1
|
|
assert_eq!(index_info.constraint_usages[0].argv_index, Some(1)); // arg
|
|
assert!(index_info.constraint_usages[0].omit);
|
|
}
|
|
|
|
#[test]
|
|
fn test_best_index_argv_order_only_schema() {
|
|
let pragma_vtab = PragmaVirtualTable {
|
|
pragma_name: "table_info".to_string(),
|
|
visible_column_count: 1,
|
|
max_arg_count: 1,
|
|
has_pragma_arg: false,
|
|
};
|
|
|
|
let constraints = vec![
|
|
usable_constraint(1), // schema (first hidden column after visible columns)
|
|
];
|
|
|
|
let index_info = pragma_vtab.best_index(&constraints).unwrap();
|
|
|
|
// Verify schema gets argv_index 1
|
|
assert_eq!(index_info.constraint_usages[0].argv_index, Some(1)); // schema
|
|
assert!(index_info.constraint_usages[0].omit);
|
|
}
|
|
|
|
#[test]
|
|
fn test_best_index_argv_order_reverse_constraint_order() {
|
|
// Test when constraints are provided in reverse order (schema first, then arg)
|
|
let pragma_vtab = PragmaVirtualTable {
|
|
pragma_name: "table_info".to_string(),
|
|
visible_column_count: 6,
|
|
max_arg_count: 2,
|
|
has_pragma_arg: true,
|
|
};
|
|
|
|
let constraints = vec![
|
|
usable_constraint(7), // schema (second hidden column)
|
|
usable_constraint(6), // arg (first hidden column)
|
|
];
|
|
|
|
let index_info = pragma_vtab.best_index(&constraints).unwrap();
|
|
|
|
// Verify arg still gets argv_index 1, schema gets argv_index 2 regardless of constraint order
|
|
assert_eq!(index_info.constraint_usages[0].argv_index, Some(2)); // schema
|
|
assert_eq!(index_info.constraint_usages[1].argv_index, Some(1)); // arg
|
|
assert!(index_info.constraint_usages[0].omit);
|
|
assert!(index_info.constraint_usages[1].omit);
|
|
}
|
|
|
|
#[test]
|
|
fn test_best_index_visible_columns_ignored() {
|
|
let pragma_vtab = PragmaVirtualTable {
|
|
pragma_name: "table_info".to_string(),
|
|
visible_column_count: 6,
|
|
max_arg_count: 2,
|
|
has_pragma_arg: true,
|
|
};
|
|
|
|
let constraints = vec![
|
|
usable_constraint(0), // visible column (cid)
|
|
usable_constraint(6), // arg (hidden)
|
|
];
|
|
|
|
let index_info = pragma_vtab.best_index(&constraints).unwrap();
|
|
|
|
// Verify visible column constraint is ignored, arg gets argv_index 1
|
|
assert_eq!(index_info.constraint_usages[0].argv_index, None); // visible column
|
|
assert_eq!(index_info.constraint_usages[1].argv_index, Some(1)); // arg
|
|
assert!(!index_info.constraint_usages[0].omit); // visible column not omitted
|
|
assert!(index_info.constraint_usages[1].omit); // arg omitted
|
|
}
|
|
|
|
#[test]
|
|
fn test_best_index_no_usable_constraints() {
|
|
let pragma_vtab = PragmaVirtualTable {
|
|
pragma_name: "table_info".to_string(),
|
|
visible_column_count: 6,
|
|
max_arg_count: 2,
|
|
has_pragma_arg: true,
|
|
};
|
|
|
|
let constraints = vec![ConstraintInfo {
|
|
column_index: 6,
|
|
op: ConstraintOp::Eq,
|
|
usable: false,
|
|
index: 0,
|
|
}];
|
|
|
|
let result = pragma_vtab.best_index(&constraints);
|
|
|
|
assert!(matches!(result, Err(ResultCode::ConstraintViolation)));
|
|
}
|
|
|
|
fn usable_constraint(column_index: u32) -> ConstraintInfo {
|
|
ConstraintInfo {
|
|
column_index,
|
|
op: ConstraintOp::Eq,
|
|
usable: true,
|
|
index: 0,
|
|
}
|
|
}
|
|
}
|