mirror of
https://github.com/aljazceru/turso.git
synced 2026-01-23 01:44:33 +01:00
Merge branch 'main' into ext-static-feature
This commit is contained in:
@@ -28,16 +28,17 @@ ipaddr = ["limbo_ipaddr/static"]
|
||||
completion = ["limbo_completion/static"]
|
||||
testvfs = ["limbo_ext_tests/static"]
|
||||
static = ["limbo_ext/static"]
|
||||
fuzz = []
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
io-uring = { version = "0.6.1", optional = true }
|
||||
io-uring = { version = "0.7.5", optional = true }
|
||||
|
||||
[target.'cfg(target_family = "unix")'.dependencies]
|
||||
polling = "3.7.2"
|
||||
rustix = "0.38.34"
|
||||
polling = "3.7.4"
|
||||
rustix = { version = "1.0.5", features = ["fs"]}
|
||||
|
||||
[target.'cfg(not(target_family = "wasm"))'.dependencies]
|
||||
mimalloc = { version = "0.1", default-features = false }
|
||||
mimalloc = { version = "0.1.46", default-features = false }
|
||||
libloading = "0.8.6"
|
||||
|
||||
[dependencies]
|
||||
@@ -45,7 +46,7 @@ limbo_ext = { workspace = true, features = ["core_only"] }
|
||||
cfg_block = "0.1.1"
|
||||
fallible-iterator = "0.3.0"
|
||||
hex = "0.4.3"
|
||||
libc = { version = "0.2.155", optional = true }
|
||||
libc = { version = "0.2.172", optional = true }
|
||||
limbo_sqlite3_parser = { workspace = true }
|
||||
thiserror = "1.0.61"
|
||||
getrandom = { version = "0.2.15" }
|
||||
@@ -54,7 +55,7 @@ regex-syntax = { version = "0.8.5", default-features = false, features = [
|
||||
"unicode",
|
||||
] }
|
||||
chrono = { version = "0.4.38", default-features = false, features = ["clock"] }
|
||||
julian_day_converter = "0.4.4"
|
||||
julian_day_converter = "0.4.5"
|
||||
rand = "0.8.5"
|
||||
libm = "0.2"
|
||||
limbo_macros = { workspace = true }
|
||||
@@ -67,12 +68,13 @@ limbo_series = { workspace = true, optional = true, features = ["static"] }
|
||||
limbo_ipaddr = { workspace = true, optional = true, features = ["static"] }
|
||||
limbo_completion = { workspace = true, optional = true, features = ["static"] }
|
||||
limbo_ext_tests = { workspace = true, optional = true, features = ["static"] }
|
||||
miette = "7.4.0"
|
||||
miette = "7.6.0"
|
||||
strum = "0.26"
|
||||
parking_lot = "0.12.3"
|
||||
crossbeam-skiplist = "0.1.3"
|
||||
tracing = "0.1.41"
|
||||
ryu = "1.0.19"
|
||||
bitflags = "2.9.0"
|
||||
|
||||
[build-dependencies]
|
||||
chrono = { version = "0.4.38", default-features = false }
|
||||
@@ -96,7 +98,7 @@ rand = "0.8.5" # Required for quickcheck
|
||||
rand_chacha = "0.9.0"
|
||||
env_logger = "0.11.6"
|
||||
test-log = { version = "0.2.17", features = ["trace"] }
|
||||
lru = "0.13.0"
|
||||
lru = "0.14.0"
|
||||
|
||||
[[bench]]
|
||||
name = "benchmark"
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::num::NonZero;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error, miette::Diagnostic)]
|
||||
@@ -49,12 +47,12 @@ pub enum LimboError {
|
||||
Constraint(String),
|
||||
#[error("Extension error: {0}")]
|
||||
ExtensionError(String),
|
||||
#[error("Unbound parameter at index {0}")]
|
||||
Unbound(NonZero<usize>),
|
||||
#[error("Runtime error: integer overflow")]
|
||||
IntegerOverflow,
|
||||
#[error("Schema is locked for write")]
|
||||
SchemaLocked,
|
||||
#[error("Database Connection is read-only")]
|
||||
ReadOnly,
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
|
||||
@@ -6,6 +6,7 @@ use libloading::{Library, Symbol};
|
||||
use limbo_ext::{ExtensionApi, ExtensionApiRef, ExtensionEntryPoint, ResultCode, VfsImpl};
|
||||
use std::{
|
||||
ffi::{c_char, CString},
|
||||
rc::Rc,
|
||||
sync::{Arc, Mutex, OnceLock},
|
||||
};
|
||||
|
||||
@@ -29,7 +30,10 @@ unsafe impl Send for VfsMod {}
|
||||
unsafe impl Sync for VfsMod {}
|
||||
|
||||
impl Connection {
|
||||
pub fn load_extension<P: AsRef<std::ffi::OsStr>>(&self, path: P) -> crate::Result<()> {
|
||||
pub fn load_extension<P: AsRef<std::ffi::OsStr>>(
|
||||
self: &Rc<Connection>,
|
||||
path: P,
|
||||
) -> crate::Result<()> {
|
||||
use limbo_ext::ExtensionApiRef;
|
||||
|
||||
let api = Box::new(self.build_limbo_ext());
|
||||
@@ -44,7 +48,15 @@ impl Connection {
|
||||
let result_code = unsafe { entry(api_ptr) };
|
||||
if result_code.is_ok() {
|
||||
let extensions = get_extension_libraries();
|
||||
extensions.lock().unwrap().push((Arc::new(lib), api_ref));
|
||||
extensions
|
||||
.lock()
|
||||
.map_err(|_| {
|
||||
LimboError::ExtensionError("Error locking extension libraries".to_string())
|
||||
})?
|
||||
.push((Arc::new(lib), api_ref));
|
||||
{
|
||||
self.parse_schema_rows()?;
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
if !api_ptr.is_null() {
|
||||
|
||||
@@ -89,12 +89,12 @@ impl Database {
|
||||
path: &str,
|
||||
vfs: &str,
|
||||
) -> crate::Result<(Arc<dyn IO>, Arc<Database>)> {
|
||||
use crate::{MemoryIO, PlatformIO};
|
||||
use crate::{MemoryIO, SyscallIO};
|
||||
use dynamic::get_vfs_modules;
|
||||
|
||||
let io: Arc<dyn IO> = match vfs {
|
||||
"memory" => Arc::new(MemoryIO::new()),
|
||||
"syscall" => Arc::new(PlatformIO::new()?),
|
||||
"syscall" => Arc::new(SyscallIO::new()?),
|
||||
#[cfg(all(target_os = "linux", feature = "io_uring"))]
|
||||
"io_uring" => Arc::new(UringIO::new()?),
|
||||
other => match get_vfs_modules().iter().find(|v| v.0 == vfs) {
|
||||
|
||||
110
core/function.rs
110
core/function.rs
@@ -10,6 +10,12 @@ pub struct ExternalFunc {
|
||||
pub func: ExtFunc,
|
||||
}
|
||||
|
||||
impl ExternalFunc {
|
||||
pub fn is_deterministic(&self) -> bool {
|
||||
false // external functions can be whatever so let's just default to false
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ExtFunc {
|
||||
Scalar(ScalarFunction),
|
||||
@@ -98,6 +104,13 @@ pub enum JsonFunc {
|
||||
JsonQuote,
|
||||
}
|
||||
|
||||
#[cfg(feature = "json")]
|
||||
impl JsonFunc {
|
||||
pub fn is_deterministic(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "json")]
|
||||
impl Display for JsonFunc {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
@@ -145,6 +158,12 @@ pub enum VectorFunc {
|
||||
VectorDistanceCos,
|
||||
}
|
||||
|
||||
impl VectorFunc {
|
||||
pub fn is_deterministic(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for VectorFunc {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let str = match self {
|
||||
@@ -198,6 +217,10 @@ impl PartialEq for AggFunc {
|
||||
}
|
||||
|
||||
impl AggFunc {
|
||||
pub fn is_deterministic(&self) -> bool {
|
||||
false // consider aggregate functions nondeterministic since they depend on the number of rows, not only the input arguments
|
||||
}
|
||||
|
||||
pub fn num_args(&self) -> usize {
|
||||
match self {
|
||||
Self::Avg => 1,
|
||||
@@ -292,6 +315,68 @@ pub enum ScalarFunc {
|
||||
LoadExtension,
|
||||
StrfTime,
|
||||
Printf,
|
||||
Likely,
|
||||
TimeDiff,
|
||||
Likelihood,
|
||||
}
|
||||
|
||||
impl ScalarFunc {
|
||||
pub fn is_deterministic(&self) -> bool {
|
||||
match self {
|
||||
ScalarFunc::Cast => true,
|
||||
ScalarFunc::Changes => false, // depends on DB state
|
||||
ScalarFunc::Char => true,
|
||||
ScalarFunc::Coalesce => true,
|
||||
ScalarFunc::Concat => true,
|
||||
ScalarFunc::ConcatWs => true,
|
||||
ScalarFunc::Glob => true,
|
||||
ScalarFunc::IfNull => true,
|
||||
ScalarFunc::Iif => true,
|
||||
ScalarFunc::Instr => true,
|
||||
ScalarFunc::Like => true,
|
||||
ScalarFunc::Abs => true,
|
||||
ScalarFunc::Upper => true,
|
||||
ScalarFunc::Lower => true,
|
||||
ScalarFunc::Random => false, // duh
|
||||
ScalarFunc::RandomBlob => false, // duh
|
||||
ScalarFunc::Trim => true,
|
||||
ScalarFunc::LTrim => true,
|
||||
ScalarFunc::RTrim => true,
|
||||
ScalarFunc::Round => true,
|
||||
ScalarFunc::Length => true,
|
||||
ScalarFunc::OctetLength => true,
|
||||
ScalarFunc::Min => true,
|
||||
ScalarFunc::Max => true,
|
||||
ScalarFunc::Nullif => true,
|
||||
ScalarFunc::Sign => true,
|
||||
ScalarFunc::Substr => true,
|
||||
ScalarFunc::Substring => true,
|
||||
ScalarFunc::Soundex => true,
|
||||
ScalarFunc::Date => false,
|
||||
ScalarFunc::Time => false,
|
||||
ScalarFunc::TotalChanges => false,
|
||||
ScalarFunc::DateTime => false,
|
||||
ScalarFunc::Typeof => true,
|
||||
ScalarFunc::Unicode => true,
|
||||
ScalarFunc::Quote => true,
|
||||
ScalarFunc::SqliteVersion => true,
|
||||
ScalarFunc::SqliteSourceId => true,
|
||||
ScalarFunc::UnixEpoch => false,
|
||||
ScalarFunc::JulianDay => false,
|
||||
ScalarFunc::Hex => true,
|
||||
ScalarFunc::Unhex => true,
|
||||
ScalarFunc::ZeroBlob => true,
|
||||
ScalarFunc::LastInsertRowid => false,
|
||||
ScalarFunc::Replace => true,
|
||||
#[cfg(feature = "fs")]
|
||||
ScalarFunc::LoadExtension => true,
|
||||
ScalarFunc::StrfTime => false,
|
||||
ScalarFunc::Printf => false,
|
||||
ScalarFunc::Likely => true,
|
||||
ScalarFunc::TimeDiff => false,
|
||||
ScalarFunc::Likelihood => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ScalarFunc {
|
||||
@@ -346,6 +431,9 @@ impl Display for ScalarFunc {
|
||||
Self::LoadExtension => "load_extension".to_string(),
|
||||
Self::StrfTime => "strftime".to_string(),
|
||||
Self::Printf => "printf".to_string(),
|
||||
Self::Likely => "likely".to_string(),
|
||||
Self::TimeDiff => "timediff".to_string(),
|
||||
Self::Likelihood => "likelihood".to_string(),
|
||||
};
|
||||
write!(f, "{}", str)
|
||||
}
|
||||
@@ -392,6 +480,9 @@ pub enum MathFuncArity {
|
||||
}
|
||||
|
||||
impl MathFunc {
|
||||
pub fn is_deterministic(&self) -> bool {
|
||||
true
|
||||
}
|
||||
pub fn arity(&self) -> MathFuncArity {
|
||||
match self {
|
||||
Self::Pi => MathFuncArity::Nullary,
|
||||
@@ -495,6 +586,17 @@ pub struct FuncCtx {
|
||||
}
|
||||
|
||||
impl Func {
|
||||
pub fn is_deterministic(&self) -> bool {
|
||||
match self {
|
||||
Self::Agg(agg_func) => agg_func.is_deterministic(),
|
||||
Self::Scalar(scalar_func) => scalar_func.is_deterministic(),
|
||||
Self::Math(math_func) => math_func.is_deterministic(),
|
||||
Self::Vector(vector_func) => vector_func.is_deterministic(),
|
||||
#[cfg(feature = "json")]
|
||||
Self::Json(json_func) => json_func.is_deterministic(),
|
||||
Self::External(external_func) => external_func.is_deterministic(),
|
||||
}
|
||||
}
|
||||
pub fn resolve_function(name: &str, arg_count: usize) -> Result<Self, LimboError> {
|
||||
match name {
|
||||
"avg" => {
|
||||
@@ -553,6 +655,12 @@ impl Func {
|
||||
}
|
||||
Ok(Self::Agg(AggFunc::Total))
|
||||
}
|
||||
"timediff" => {
|
||||
if arg_count != 2 {
|
||||
crate::bail_parse_error!("wrong number of arguments to function {}()", name)
|
||||
}
|
||||
Ok(Self::Scalar(ScalarFunc::TimeDiff))
|
||||
}
|
||||
#[cfg(feature = "json")]
|
||||
"jsonb_group_array" => Ok(Self::Agg(AggFunc::JsonbGroupArray)),
|
||||
#[cfg(feature = "json")]
|
||||
@@ -596,6 +704,8 @@ impl Func {
|
||||
"sqlite_version" => Ok(Self::Scalar(ScalarFunc::SqliteVersion)),
|
||||
"sqlite_source_id" => Ok(Self::Scalar(ScalarFunc::SqliteSourceId)),
|
||||
"replace" => Ok(Self::Scalar(ScalarFunc::Replace)),
|
||||
"likely" => Ok(Self::Scalar(ScalarFunc::Likely)),
|
||||
"likelihood" => Ok(Self::Scalar(ScalarFunc::Likelihood)),
|
||||
#[cfg(feature = "json")]
|
||||
"json" => Ok(Self::Json(JsonFunc::Json)),
|
||||
#[cfg(feature = "json")]
|
||||
|
||||
@@ -46,21 +46,13 @@ enum DateTimeOutput {
|
||||
DateTime,
|
||||
// Holds the format string
|
||||
StrfTime(String),
|
||||
JuliaDay,
|
||||
}
|
||||
|
||||
fn exec_datetime(values: &[Register], output_type: DateTimeOutput) -> OwnedValue {
|
||||
if values.is_empty() {
|
||||
let now = parse_naive_date_time(&OwnedValue::build_text("now")).unwrap();
|
||||
|
||||
let formatted_str = match output_type {
|
||||
DateTimeOutput::DateTime => now.format("%Y-%m-%d %H:%M:%S").to_string(),
|
||||
DateTimeOutput::Time => now.format("%H:%M:%S").to_string(),
|
||||
DateTimeOutput::Date => now.format("%Y-%m-%d").to_string(),
|
||||
DateTimeOutput::StrfTime(ref format_str) => strftime_format(&now, format_str),
|
||||
};
|
||||
|
||||
// Parse here
|
||||
return OwnedValue::build_text(&formatted_str);
|
||||
return format_dt(now, output_type, false);
|
||||
}
|
||||
if let Some(mut dt) = parse_naive_date_time(values[0].get_owned_value()) {
|
||||
// if successful, treat subsequent entries as modifiers
|
||||
@@ -91,28 +83,32 @@ fn modify_dt(dt: &mut NaiveDateTime, mods: &[Register], output_type: DateTimeOut
|
||||
if is_leap_second(dt) || *dt > get_max_datetime_exclusive() {
|
||||
return OwnedValue::build_text("");
|
||||
}
|
||||
let formatted = format_dt(*dt, output_type, subsec_requested);
|
||||
OwnedValue::build_text(&formatted)
|
||||
format_dt(*dt, output_type, subsec_requested)
|
||||
}
|
||||
|
||||
fn format_dt(dt: NaiveDateTime, output_type: DateTimeOutput, subsec: bool) -> String {
|
||||
fn format_dt(dt: NaiveDateTime, output_type: DateTimeOutput, subsec: bool) -> OwnedValue {
|
||||
match output_type {
|
||||
DateTimeOutput::Date => dt.format("%Y-%m-%d").to_string(),
|
||||
DateTimeOutput::Date => OwnedValue::from_text(dt.format("%Y-%m-%d").to_string().as_str()),
|
||||
DateTimeOutput::Time => {
|
||||
if subsec {
|
||||
let t = if subsec {
|
||||
dt.format("%H:%M:%S%.3f").to_string()
|
||||
} else {
|
||||
dt.format("%H:%M:%S").to_string()
|
||||
}
|
||||
};
|
||||
OwnedValue::from_text(t.as_str())
|
||||
}
|
||||
DateTimeOutput::DateTime => {
|
||||
if subsec {
|
||||
let t = if subsec {
|
||||
dt.format("%Y-%m-%d %H:%M:%S%.3f").to_string()
|
||||
} else {
|
||||
dt.format("%Y-%m-%d %H:%M:%S").to_string()
|
||||
}
|
||||
};
|
||||
OwnedValue::from_text(t.as_str())
|
||||
}
|
||||
DateTimeOutput::StrfTime(format_str) => strftime_format(&dt, &format_str),
|
||||
DateTimeOutput::StrfTime(format_str) => {
|
||||
OwnedValue::from_text(strftime_format(&dt, &format_str).as_str())
|
||||
}
|
||||
DateTimeOutput::JuliaDay => OwnedValue::Float(to_julian_day_exact(&dt)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,14 +321,8 @@ fn last_day_in_month(year: i32, month: u32) -> u32 {
|
||||
28
|
||||
}
|
||||
|
||||
pub fn exec_julianday(time_value: &OwnedValue) -> Result<String> {
|
||||
let dt = parse_naive_date_time(time_value);
|
||||
match dt {
|
||||
// if we did something heinous like: parse::<f64>().unwrap().to_string()
|
||||
// that would solve the precision issue, but dear lord...
|
||||
Some(dt) => Ok(format!("{:.1$}", to_julian_day_exact(&dt), 8)),
|
||||
None => Ok(String::new()),
|
||||
}
|
||||
pub fn exec_julianday(values: &[Register]) -> OwnedValue {
|
||||
exec_datetime(values, DateTimeOutput::JuliaDay)
|
||||
}
|
||||
|
||||
fn to_julian_day_exact(dt: &NaiveDateTime) -> f64 {
|
||||
@@ -656,6 +646,61 @@ fn parse_modifier(modifier: &str) -> Result<Modifier> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn exec_timediff(values: &[Register]) -> OwnedValue {
|
||||
if values.len() < 2 {
|
||||
return OwnedValue::Null;
|
||||
}
|
||||
|
||||
let start = parse_naive_date_time(values[0].get_owned_value());
|
||||
let end = parse_naive_date_time(values[1].get_owned_value());
|
||||
|
||||
match (start, end) {
|
||||
(Some(start), Some(end)) => {
|
||||
let duration = start.signed_duration_since(end);
|
||||
format_time_duration(&duration)
|
||||
}
|
||||
_ => OwnedValue::Null,
|
||||
}
|
||||
}
|
||||
|
||||
/// Format the time duration as +/-YYYY-MM-DD HH:MM:SS.SSS as per SQLite's timediff() function
|
||||
fn format_time_duration(duration: &chrono::Duration) -> OwnedValue {
|
||||
let is_negative = duration.num_seconds() < 0;
|
||||
|
||||
let abs_duration = if is_negative {
|
||||
-duration.clone()
|
||||
} else {
|
||||
duration.clone()
|
||||
};
|
||||
|
||||
let total_seconds = abs_duration.num_seconds();
|
||||
let hours = (total_seconds % 86400) / 3600;
|
||||
let minutes = (total_seconds % 3600) / 60;
|
||||
let seconds = total_seconds % 60;
|
||||
|
||||
let days = total_seconds / 86400;
|
||||
let years = days / 365;
|
||||
let remaining_days = days % 365;
|
||||
let months = 0;
|
||||
|
||||
let total_millis = abs_duration.num_milliseconds();
|
||||
let millis = total_millis % 1000;
|
||||
|
||||
let result = format!(
|
||||
"{}{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:03}",
|
||||
if is_negative { "-" } else { "+" },
|
||||
years,
|
||||
months,
|
||||
remaining_days,
|
||||
hours,
|
||||
minutes,
|
||||
seconds,
|
||||
millis
|
||||
);
|
||||
|
||||
OwnedValue::build_text(&result)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -1642,4 +1687,67 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_strftime() {}
|
||||
|
||||
#[test]
|
||||
fn test_exec_timediff() {
|
||||
let start = OwnedValue::build_text("12:00:00");
|
||||
let end = OwnedValue::build_text("14:30:45");
|
||||
let expected = OwnedValue::build_text("-0000-00-00 02:30:45.000");
|
||||
assert_eq!(
|
||||
exec_timediff(&[Register::OwnedValue(start), Register::OwnedValue(end)]),
|
||||
expected
|
||||
);
|
||||
|
||||
let start = OwnedValue::build_text("14:30:45");
|
||||
let end = OwnedValue::build_text("12:00:00");
|
||||
let expected = OwnedValue::build_text("+0000-00-00 02:30:45.000");
|
||||
assert_eq!(
|
||||
exec_timediff(&[Register::OwnedValue(start), Register::OwnedValue(end)]),
|
||||
expected
|
||||
);
|
||||
|
||||
let start = OwnedValue::build_text("12:00:01.300");
|
||||
let end = OwnedValue::build_text("12:00:00.500");
|
||||
let expected = OwnedValue::build_text("+0000-00-00 00:00:00.800");
|
||||
assert_eq!(
|
||||
exec_timediff(&[Register::OwnedValue(start), Register::OwnedValue(end)]),
|
||||
expected
|
||||
);
|
||||
|
||||
let start = OwnedValue::build_text("13:30:00");
|
||||
let end = OwnedValue::build_text("16:45:30");
|
||||
let expected = OwnedValue::build_text("-0000-00-00 03:15:30.000");
|
||||
assert_eq!(
|
||||
exec_timediff(&[Register::OwnedValue(start), Register::OwnedValue(end)]),
|
||||
expected
|
||||
);
|
||||
|
||||
let start = OwnedValue::build_text("2023-05-10 23:30:00");
|
||||
let end = OwnedValue::build_text("2023-05-11 01:15:00");
|
||||
let expected = OwnedValue::build_text("-0000-00-00 01:45:00.000");
|
||||
assert_eq!(
|
||||
exec_timediff(&[Register::OwnedValue(start), Register::OwnedValue(end)]),
|
||||
expected
|
||||
);
|
||||
|
||||
let start = OwnedValue::Null;
|
||||
let end = OwnedValue::build_text("12:00:00");
|
||||
let expected = OwnedValue::Null;
|
||||
assert_eq!(
|
||||
exec_timediff(&[Register::OwnedValue(start), Register::OwnedValue(end)]),
|
||||
expected
|
||||
);
|
||||
|
||||
let start = OwnedValue::build_text("not a time");
|
||||
let end = OwnedValue::build_text("12:00:00");
|
||||
let expected = OwnedValue::Null;
|
||||
assert_eq!(
|
||||
exec_timediff(&[Register::OwnedValue(start), Register::OwnedValue(end)]),
|
||||
expected
|
||||
);
|
||||
|
||||
let start = OwnedValue::build_text("12:00:00");
|
||||
let expected = OwnedValue::Null;
|
||||
assert_eq!(exec_timediff(&[Register::OwnedValue(start)]), expected);
|
||||
}
|
||||
}
|
||||
|
||||
9
core/io/clock.rs
Normal file
9
core/io/clock.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Instant {
|
||||
pub secs: i64,
|
||||
pub micros: u32,
|
||||
}
|
||||
|
||||
pub trait Clock {
|
||||
fn now(&self) -> Instant;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::{Completion, File, LimboError, OpenFlags, Result, IO};
|
||||
use super::MemoryIO;
|
||||
use crate::{Clock, Completion, File, Instant, LimboError, OpenFlags, Result, IO};
|
||||
use std::cell::RefCell;
|
||||
use std::io::{Read, Seek, Write};
|
||||
use std::sync::Arc;
|
||||
@@ -19,13 +20,18 @@ unsafe impl Sync for GenericIO {}
|
||||
impl IO for GenericIO {
|
||||
fn open_file(&self, path: &str, flags: OpenFlags, _direct: bool) -> Result<Arc<dyn File>> {
|
||||
trace!("open_file(path = {})", path);
|
||||
let file = std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(matches!(flags, OpenFlags::Create))
|
||||
.open(path)?;
|
||||
let mut file = std::fs::File::options();
|
||||
file.read(true);
|
||||
|
||||
if !flags.contains(OpenFlags::ReadOnly) {
|
||||
file.write(true);
|
||||
file.create(flags.contains(OpenFlags::Create));
|
||||
}
|
||||
|
||||
let file = file.open(path)?;
|
||||
Ok(Arc::new(GenericFile {
|
||||
file: RefCell::new(file),
|
||||
memory_io: Arc::new(MemoryIO::new()),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -39,13 +45,24 @@ impl IO for GenericIO {
|
||||
i64::from_ne_bytes(buf)
|
||||
}
|
||||
|
||||
fn get_current_time(&self) -> String {
|
||||
chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string()
|
||||
fn get_memory_io(&self) -> Arc<MemoryIO> {
|
||||
Arc::new(MemoryIO::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl Clock for GenericIO {
|
||||
fn now(&self) -> Instant {
|
||||
let now = chrono::Local::now();
|
||||
Instant {
|
||||
secs: now.timestamp(),
|
||||
micros: now.timestamp_subsec_micros(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GenericFile {
|
||||
file: RefCell<std::fs::File>,
|
||||
memory_io: Arc<MemoryIO>,
|
||||
}
|
||||
|
||||
unsafe impl Send for GenericFile {}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::{common, Completion, File, OpenFlags, WriteCompletion, IO};
|
||||
use crate::{LimboError, Result};
|
||||
use crate::io::clock::{Clock, Instant};
|
||||
use crate::{LimboError, MemoryIO, Result};
|
||||
use rustix::fs::{self, FlockOperation, OFlags};
|
||||
use rustix::io_uring::iovec;
|
||||
use std::cell::RefCell;
|
||||
@@ -138,11 +139,15 @@ impl WrappedIOUring {
|
||||
impl IO for UringIO {
|
||||
fn open_file(&self, path: &str, flags: OpenFlags, direct: bool) -> Result<Arc<dyn File>> {
|
||||
trace!("open_file(path = {})", path);
|
||||
let file = std::fs::File::options()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(matches!(flags, OpenFlags::Create))
|
||||
.open(path)?;
|
||||
let mut file = std::fs::File::options();
|
||||
file.read(true);
|
||||
|
||||
if !flags.contains(OpenFlags::ReadOnly) {
|
||||
file.write(true);
|
||||
file.create(flags.contains(OpenFlags::Create));
|
||||
}
|
||||
|
||||
let file = file.open(path)?;
|
||||
// Let's attempt to enable direct I/O. Not all filesystems support it
|
||||
// so ignore any errors.
|
||||
let fd = file.as_fd();
|
||||
@@ -157,7 +162,7 @@ impl IO for UringIO {
|
||||
file,
|
||||
});
|
||||
if std::env::var(common::ENV_DISABLE_FILE_LOCK).is_err() {
|
||||
uring_file.lock_file(true)?;
|
||||
uring_file.lock_file(!flags.contains(OpenFlags::ReadOnly))?;
|
||||
}
|
||||
Ok(uring_file)
|
||||
}
|
||||
@@ -197,8 +202,18 @@ impl IO for UringIO {
|
||||
i64::from_ne_bytes(buf)
|
||||
}
|
||||
|
||||
fn get_current_time(&self) -> String {
|
||||
chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string()
|
||||
fn get_memory_io(&self) -> Arc<MemoryIO> {
|
||||
Arc::new(MemoryIO::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl Clock for UringIO {
|
||||
fn now(&self) -> Instant {
|
||||
let now = chrono::Local::now();
|
||||
Instant {
|
||||
secs: now.timestamp(),
|
||||
micros: now.timestamp_subsec_micros(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::{Buffer, Completion, File, OpenFlags, IO};
|
||||
use super::{Buffer, Clock, Completion, File, OpenFlags, IO};
|
||||
use crate::Result;
|
||||
|
||||
use crate::io::clock::Instant;
|
||||
use std::{
|
||||
cell::{Cell, RefCell, UnsafeCell},
|
||||
collections::BTreeMap,
|
||||
@@ -29,6 +30,16 @@ impl Default for MemoryIO {
|
||||
}
|
||||
}
|
||||
|
||||
impl Clock for MemoryIO {
|
||||
fn now(&self) -> Instant {
|
||||
let now = chrono::Local::now();
|
||||
Instant {
|
||||
secs: now.timestamp(),
|
||||
micros: now.timestamp_subsec_micros(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IO for MemoryIO {
|
||||
fn open_file(&self, _path: &str, _flags: OpenFlags, _direct: bool) -> Result<Arc<dyn File>> {
|
||||
Ok(Arc::new(MemoryFile {
|
||||
@@ -48,8 +59,8 @@ impl IO for MemoryIO {
|
||||
i64::from_ne_bytes(buf)
|
||||
}
|
||||
|
||||
fn get_current_time(&self) -> String {
|
||||
chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string()
|
||||
fn get_memory_io(&self) -> Arc<MemoryIO> {
|
||||
Arc::new(MemoryIO::new())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::Result;
|
||||
use bitflags::bitflags;
|
||||
use cfg_block::cfg_block;
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
@@ -19,29 +20,31 @@ pub trait File: Send + Sync {
|
||||
fn size(&self) -> Result<u64>;
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum OpenFlags {
|
||||
None,
|
||||
Create,
|
||||
}
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub struct OpenFlags(i32);
|
||||
|
||||
impl OpenFlags {
|
||||
pub fn to_flags(&self) -> i32 {
|
||||
match self {
|
||||
Self::None => 0,
|
||||
Self::Create => 1,
|
||||
}
|
||||
bitflags! {
|
||||
impl OpenFlags: i32 {
|
||||
const None = 0b00000000;
|
||||
const Create = 0b0000001;
|
||||
const ReadOnly = 0b0000010;
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IO: Send + Sync {
|
||||
impl Default for OpenFlags {
|
||||
fn default() -> Self {
|
||||
Self::Create
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IO: Clock + Send + Sync {
|
||||
fn open_file(&self, path: &str, flags: OpenFlags, direct: bool) -> Result<Arc<dyn File>>;
|
||||
|
||||
fn run_once(&self) -> Result<()>;
|
||||
|
||||
fn generate_random_number(&self) -> i64;
|
||||
|
||||
fn get_current_time(&self) -> String;
|
||||
fn get_memory_io(&self) -> Arc<MemoryIO>;
|
||||
}
|
||||
|
||||
pub type Complete = dyn Fn(Arc<RefCell<Buffer>>);
|
||||
@@ -191,7 +194,8 @@ cfg_block! {
|
||||
mod unix;
|
||||
#[cfg(feature = "fs")]
|
||||
pub use unix::UnixIO;
|
||||
pub use io_uring::UringIO as PlatformIO;
|
||||
pub use unix::UnixIO as SyscallIO;
|
||||
pub use unix::UnixIO as PlatformIO;
|
||||
}
|
||||
|
||||
#[cfg(any(all(target_os = "linux",not(feature = "io_uring")), target_os = "macos"))] {
|
||||
@@ -199,16 +203,19 @@ cfg_block! {
|
||||
#[cfg(feature = "fs")]
|
||||
pub use unix::UnixIO;
|
||||
pub use unix::UnixIO as PlatformIO;
|
||||
pub use PlatformIO as SyscallIO;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")] {
|
||||
mod windows;
|
||||
pub use windows::WindowsIO as PlatformIO;
|
||||
pub use PlatformIO as SyscallIO;
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] {
|
||||
mod generic;
|
||||
pub use generic::GenericIO as PlatformIO;
|
||||
pub use PlatformIO as SyscallIO;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,4 +223,6 @@ mod memory;
|
||||
#[cfg(feature = "fs")]
|
||||
mod vfs;
|
||||
pub use memory::MemoryIO;
|
||||
pub mod clock;
|
||||
mod common;
|
||||
pub use clock::Clock;
|
||||
|
||||
@@ -2,7 +2,8 @@ use crate::error::LimboError;
|
||||
use crate::io::common;
|
||||
use crate::Result;
|
||||
|
||||
use super::{Completion, File, OpenFlags, IO};
|
||||
use super::{Completion, File, MemoryIO, OpenFlags, IO};
|
||||
use crate::io::clock::{Clock, Instant};
|
||||
use polling::{Event, Events, Poller};
|
||||
use rustix::{
|
||||
fd::{AsFd, AsRawFd},
|
||||
@@ -183,15 +184,28 @@ impl UnixIO {
|
||||
}
|
||||
}
|
||||
|
||||
impl Clock for UnixIO {
|
||||
fn now(&self) -> Instant {
|
||||
let now = chrono::Local::now();
|
||||
Instant {
|
||||
secs: now.timestamp(),
|
||||
micros: now.timestamp_subsec_micros(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IO for UnixIO {
|
||||
fn open_file(&self, path: &str, flags: OpenFlags, _direct: bool) -> Result<Arc<dyn File>> {
|
||||
trace!("open_file(path = {})", path);
|
||||
let file = std::fs::File::options()
|
||||
.read(true)
|
||||
.custom_flags(OFlags::NONBLOCK.bits() as i32)
|
||||
.write(true)
|
||||
.create(matches!(flags, OpenFlags::Create))
|
||||
.open(path)?;
|
||||
let mut file = std::fs::File::options();
|
||||
file.read(true).custom_flags(OFlags::NONBLOCK.bits() as i32);
|
||||
|
||||
if !flags.contains(OpenFlags::ReadOnly) {
|
||||
file.write(true);
|
||||
file.create(flags.contains(OpenFlags::Create));
|
||||
}
|
||||
|
||||
let file = file.open(path)?;
|
||||
|
||||
#[allow(clippy::arc_with_non_send_sync)]
|
||||
let unix_file = Arc::new(UnixFile {
|
||||
@@ -200,7 +214,7 @@ impl IO for UnixIO {
|
||||
callbacks: BorrowedCallbacks(self.callbacks.as_mut().into()),
|
||||
});
|
||||
if std::env::var(common::ENV_DISABLE_FILE_LOCK).is_err() {
|
||||
unix_file.lock_file(true)?;
|
||||
unix_file.lock_file(!flags.contains(OpenFlags::ReadOnly))?;
|
||||
}
|
||||
Ok(unix_file)
|
||||
}
|
||||
@@ -248,8 +262,8 @@ impl IO for UnixIO {
|
||||
i64::from_ne_bytes(buf)
|
||||
}
|
||||
|
||||
fn get_current_time(&self) -> String {
|
||||
chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string()
|
||||
fn get_memory_io(&self) -> Arc<MemoryIO> {
|
||||
Arc::new(MemoryIO::new())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
use super::{Buffer, Completion, File, MemoryIO, OpenFlags, IO};
|
||||
use crate::ext::VfsMod;
|
||||
use crate::io::clock::{Clock, Instant};
|
||||
use crate::{LimboError, Result};
|
||||
use limbo_ext::{VfsFileImpl, VfsImpl};
|
||||
use std::cell::RefCell;
|
||||
use std::ffi::{c_void, CString};
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::{Buffer, Completion, File, OpenFlags, IO};
|
||||
impl Clock for VfsMod {
|
||||
fn now(&self) -> Instant {
|
||||
let now = chrono::Local::now();
|
||||
Instant {
|
||||
secs: now.timestamp(),
|
||||
micros: now.timestamp_subsec_micros(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IO for VfsMod {
|
||||
fn open_file(&self, path: &str, flags: OpenFlags, direct: bool) -> Result<Arc<dyn File>> {
|
||||
@@ -14,7 +24,7 @@ impl IO for VfsMod {
|
||||
})?;
|
||||
let ctx = self.ctx as *mut c_void;
|
||||
let vfs = unsafe { &*self.ctx };
|
||||
let file = unsafe { (vfs.open)(ctx, c_path.as_ptr(), flags.to_flags(), direct) };
|
||||
let file = unsafe { (vfs.open)(ctx, c_path.as_ptr(), flags.0, direct) };
|
||||
if file.is_null() {
|
||||
return Err(LimboError::ExtensionError("File not found".to_string()));
|
||||
}
|
||||
@@ -41,6 +51,13 @@ impl IO for VfsMod {
|
||||
unsafe { (vfs.gen_random_number)() }
|
||||
}
|
||||
|
||||
fn get_memory_io(&self) -> Arc<MemoryIO> {
|
||||
Arc::new(MemoryIO::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl VfsMod {
|
||||
#[allow(dead_code)] // used in FFI call
|
||||
fn get_current_time(&self) -> String {
|
||||
if self.ctx.is_null() {
|
||||
return "".to_string();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::{Completion, File, LimboError, OpenFlags, Result, IO};
|
||||
use super::MemoryIO;
|
||||
use crate::{Clock, Completion, File, Instant, LimboError, OpenFlags, Result, IO};
|
||||
use std::cell::RefCell;
|
||||
use std::io::{Read, Seek, Write};
|
||||
use std::sync::Arc;
|
||||
use tracing::{debug, trace};
|
||||
|
||||
pub struct WindowsIO {}
|
||||
|
||||
impl WindowsIO {
|
||||
@@ -19,11 +19,15 @@ unsafe impl Sync for WindowsIO {}
|
||||
impl IO for WindowsIO {
|
||||
fn open_file(&self, path: &str, flags: OpenFlags, direct: bool) -> Result<Arc<dyn File>> {
|
||||
trace!("open_file(path = {})", path);
|
||||
let file = std::fs::File::options()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(matches!(flags, OpenFlags::Create))
|
||||
.open(path)?;
|
||||
let mut file = std::fs::File::options();
|
||||
file.read(true);
|
||||
|
||||
if !flags.contains(OpenFlags::ReadOnly) {
|
||||
file.write(true);
|
||||
file.create(flags.contains(OpenFlags::Create));
|
||||
}
|
||||
|
||||
let file = file.open(path)?;
|
||||
Ok(Arc::new(WindowsFile {
|
||||
file: RefCell::new(file),
|
||||
}))
|
||||
@@ -39,8 +43,18 @@ impl IO for WindowsIO {
|
||||
i64::from_ne_bytes(buf)
|
||||
}
|
||||
|
||||
fn get_current_time(&self) -> String {
|
||||
chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string()
|
||||
fn get_memory_io(&self) -> Arc<MemoryIO> {
|
||||
Arc::new(MemoryIO::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl Clock for WindowsIO {
|
||||
fn now(&self) -> Instant {
|
||||
let now = chrono::Local::now();
|
||||
Instant {
|
||||
secs: now.timestamp(),
|
||||
micros: now.timestamp_subsec_micros(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
180
core/lib.rs
180
core/lib.rs
@@ -20,6 +20,12 @@ mod util;
|
||||
mod vdbe;
|
||||
mod vector;
|
||||
|
||||
#[cfg(feature = "fuzz")]
|
||||
pub mod numeric;
|
||||
|
||||
#[cfg(not(feature = "fuzz"))]
|
||||
mod numeric;
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
#[global_allocator]
|
||||
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
@@ -27,12 +33,15 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
use crate::{fast_lock::SpinLock, translate::optimizer::optimize_plan};
|
||||
pub use error::LimboError;
|
||||
use fallible_iterator::FallibleIterator;
|
||||
pub use io::clock::{Clock, Instant};
|
||||
#[cfg(all(feature = "fs", target_family = "unix"))]
|
||||
pub use io::UnixIO;
|
||||
#[cfg(all(feature = "fs", target_os = "linux", feature = "io_uring"))]
|
||||
pub use io::UringIO;
|
||||
pub use io::{Buffer, Completion, File, MemoryIO, OpenFlags, PlatformIO, WriteCompletion, IO};
|
||||
use limbo_ext::{ResultCode, VTabKind, VTabModuleImpl};
|
||||
pub use io::{
|
||||
Buffer, Completion, File, MemoryIO, OpenFlags, PlatformIO, SyscallIO, WriteCompletion, IO,
|
||||
};
|
||||
use limbo_ext::{ConstraintInfo, IndexInfo, OrderByInfo, ResultCode, VTabKind, VTabModuleImpl};
|
||||
use limbo_sqlite3_parser::{ast, ast::Cmd, lexer::sql::Parser};
|
||||
use parking_lot::RwLock;
|
||||
use schema::{Column, Schema};
|
||||
@@ -66,20 +75,19 @@ pub use types::OwnedValue;
|
||||
pub use types::RefValue;
|
||||
use util::{columns_from_create_table_body, parse_schema_rows};
|
||||
use vdbe::{builder::QueryMode, VTabOpaqueCursor};
|
||||
|
||||
pub type Result<T, E = LimboError> = std::result::Result<T, E>;
|
||||
pub static DATABASE_VERSION: OnceLock<String> = OnceLock::new();
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum TransactionState {
|
||||
Write,
|
||||
Read,
|
||||
None,
|
||||
}
|
||||
|
||||
pub(crate) type MvStore = crate::mvcc::MvStore<crate::mvcc::LocalClock>;
|
||||
pub(crate) type MvStore = mvcc::MvStore<mvcc::LocalClock>;
|
||||
|
||||
pub(crate) type MvCursor = crate::mvcc::cursor::ScanCursor<crate::mvcc::LocalClock>;
|
||||
pub(crate) type MvCursor = mvcc::cursor::ScanCursor<mvcc::LocalClock>;
|
||||
|
||||
pub struct Database {
|
||||
mv_store: Option<Rc<MvStore>>,
|
||||
@@ -88,11 +96,12 @@ pub struct Database {
|
||||
header: Arc<SpinLock<DatabaseHeader>>,
|
||||
db_file: Arc<dyn DatabaseStorage>,
|
||||
io: Arc<dyn IO>,
|
||||
page_size: u16,
|
||||
page_size: u32,
|
||||
// Shared structures of a Database are the parts that are common to multiple threads that might
|
||||
// create DB connections.
|
||||
shared_page_cache: Arc<RwLock<DumbLruPageCache>>,
|
||||
shared_wal: Arc<UnsafeCell<WalFileShared>>,
|
||||
open_flags: OpenFlags,
|
||||
}
|
||||
|
||||
unsafe impl Send for Database {}
|
||||
@@ -101,53 +110,74 @@ unsafe impl Sync for Database {}
|
||||
impl Database {
|
||||
#[cfg(feature = "fs")]
|
||||
pub fn open_file(io: Arc<dyn IO>, path: &str, enable_mvcc: bool) -> Result<Arc<Database>> {
|
||||
use storage::wal::WalFileShared;
|
||||
Self::open_file_with_flags(io, path, OpenFlags::default(), enable_mvcc)
|
||||
}
|
||||
|
||||
let file = io.open_file(path, OpenFlags::Create, true)?;
|
||||
#[cfg(feature = "fs")]
|
||||
pub fn open_file_with_flags(
|
||||
io: Arc<dyn IO>,
|
||||
path: &str,
|
||||
flags: OpenFlags,
|
||||
enable_mvcc: bool,
|
||||
) -> Result<Arc<Database>> {
|
||||
let file = io.open_file(path, flags, true)?;
|
||||
maybe_init_database_file(&file, &io)?;
|
||||
let db_file = Arc::new(DatabaseFile::new(file));
|
||||
let wal_path = format!("{}-wal", path);
|
||||
let db_header = Pager::begin_open(db_file.clone())?;
|
||||
io.run_once()?;
|
||||
let page_size = db_header.lock().page_size;
|
||||
let wal_shared = WalFileShared::open_shared(&io, wal_path.as_str(), page_size)?;
|
||||
Self::open(io, db_file, wal_shared, enable_mvcc)
|
||||
Self::open_with_flags(io, path, db_file, flags, enable_mvcc)
|
||||
}
|
||||
|
||||
#[allow(clippy::arc_with_non_send_sync)]
|
||||
pub fn open(
|
||||
io: Arc<dyn IO>,
|
||||
path: &str,
|
||||
db_file: Arc<dyn DatabaseStorage>,
|
||||
shared_wal: Arc<UnsafeCell<WalFileShared>>,
|
||||
enable_mvcc: bool,
|
||||
) -> Result<Arc<Database>> {
|
||||
Self::open_with_flags(io, path, db_file, OpenFlags::default(), enable_mvcc)
|
||||
}
|
||||
|
||||
#[allow(clippy::arc_with_non_send_sync)]
|
||||
pub fn open_with_flags(
|
||||
io: Arc<dyn IO>,
|
||||
path: &str,
|
||||
db_file: Arc<dyn DatabaseStorage>,
|
||||
flags: OpenFlags,
|
||||
enable_mvcc: bool,
|
||||
) -> Result<Arc<Database>> {
|
||||
let db_header = Pager::begin_open(db_file.clone())?;
|
||||
// ensure db header is there
|
||||
io.run_once()?;
|
||||
|
||||
let page_size = db_header.lock().get_page_size();
|
||||
let wal_path = format!("{}-wal", path);
|
||||
let shared_wal = WalFileShared::open_shared(&io, wal_path.as_str(), page_size)?;
|
||||
|
||||
DATABASE_VERSION.get_or_init(|| {
|
||||
let version = db_header.lock().version_number;
|
||||
version.to_string()
|
||||
});
|
||||
|
||||
let mv_store = if enable_mvcc {
|
||||
Some(Rc::new(MvStore::new(
|
||||
crate::mvcc::LocalClock::new(),
|
||||
crate::mvcc::persistent_storage::Storage::new_noop(),
|
||||
mvcc::LocalClock::new(),
|
||||
mvcc::persistent_storage::Storage::new_noop(),
|
||||
)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let shared_page_cache = Arc::new(RwLock::new(DumbLruPageCache::new(10)));
|
||||
let page_size = db_header.lock().page_size;
|
||||
let header = db_header;
|
||||
let schema = Arc::new(RwLock::new(Schema::new()));
|
||||
let db = Database {
|
||||
mv_store,
|
||||
schema: schema.clone(),
|
||||
header: header.clone(),
|
||||
header: db_header.clone(),
|
||||
shared_page_cache: shared_page_cache.clone(),
|
||||
shared_wal: shared_wal.clone(),
|
||||
db_file,
|
||||
io: io.clone(),
|
||||
page_size,
|
||||
open_flags: flags,
|
||||
};
|
||||
let db = Arc::new(db);
|
||||
{
|
||||
@@ -158,7 +188,13 @@ impl Database {
|
||||
.try_write()
|
||||
.expect("lock on schema should succeed first try");
|
||||
let syms = conn.syms.borrow();
|
||||
parse_schema_rows(rows, &mut schema, io, syms.deref(), None)?;
|
||||
if let Err(LimboError::ExtensionError(e)) =
|
||||
parse_schema_rows(rows, &mut schema, io, &syms, None)
|
||||
{
|
||||
// this means that a vtab exists and we no longer have the module loaded. we print
|
||||
// a warning to the user to load the module
|
||||
eprintln!("Warning: {}", e);
|
||||
}
|
||||
}
|
||||
Ok(db)
|
||||
}
|
||||
@@ -168,14 +204,14 @@ impl Database {
|
||||
|
||||
let wal = Rc::new(RefCell::new(WalFile::new(
|
||||
self.io.clone(),
|
||||
self.page_size as usize,
|
||||
self.page_size,
|
||||
self.shared_wal.clone(),
|
||||
buffer_pool.clone(),
|
||||
)));
|
||||
let pager = Rc::new(Pager::finish_open(
|
||||
self.header.clone(),
|
||||
self.db_file.clone(),
|
||||
wal,
|
||||
Some(wal),
|
||||
self.io.clone(),
|
||||
self.shared_page_cache.clone(),
|
||||
buffer_pool,
|
||||
@@ -186,9 +222,9 @@ impl Database {
|
||||
schema: self.schema.clone(),
|
||||
header: self.header.clone(),
|
||||
last_insert_rowid: Cell::new(0),
|
||||
auto_commit: RefCell::new(true),
|
||||
auto_commit: Cell::new(true),
|
||||
mv_transactions: RefCell::new(Vec::new()),
|
||||
transaction_state: RefCell::new(TransactionState::None),
|
||||
transaction_state: Cell::new(TransactionState::None),
|
||||
last_change: Cell::new(0),
|
||||
syms: RefCell::new(SymbolTable::new()),
|
||||
total_changes: Cell::new(0),
|
||||
@@ -204,12 +240,12 @@ impl Database {
|
||||
#[cfg(feature = "fs")]
|
||||
#[allow(clippy::arc_with_non_send_sync)]
|
||||
pub fn open_new(path: &str, vfs: &str) -> Result<(Arc<dyn IO>, Arc<Database>)> {
|
||||
let vfsmods = crate::ext::add_builtin_vfs_extensions(None)?;
|
||||
let vfsmods = ext::add_builtin_vfs_extensions(None)?;
|
||||
let io: Arc<dyn IO> = match vfsmods.iter().find(|v| v.0 == vfs).map(|v| v.1.clone()) {
|
||||
Some(vfs) => vfs,
|
||||
None => match vfs.trim() {
|
||||
"memory" => Arc::new(MemoryIO::new()),
|
||||
"syscall" => Arc::new(PlatformIO::new()?),
|
||||
"syscall" => Arc::new(SyscallIO::new()?),
|
||||
#[cfg(all(target_os = "linux", feature = "io_uring"))]
|
||||
"io_uring" => Arc::new(UringIO::new()?),
|
||||
other => {
|
||||
@@ -231,7 +267,7 @@ pub fn maybe_init_database_file(file: &Arc<dyn File>, io: &Arc<dyn IO>) -> Resul
|
||||
let db_header = DatabaseHeader::default();
|
||||
let page1 = allocate_page(
|
||||
1,
|
||||
&Rc::new(BufferPool::new(db_header.page_size as usize)),
|
||||
&Rc::new(BufferPool::new(db_header.get_page_size() as usize)),
|
||||
DATABASE_HEADER_SIZE,
|
||||
);
|
||||
{
|
||||
@@ -243,7 +279,7 @@ pub fn maybe_init_database_file(file: &Arc<dyn File>, io: &Arc<dyn IO>) -> Resul
|
||||
&page1,
|
||||
storage::sqlite3_ondisk::PageType::TableLeaf,
|
||||
DATABASE_HEADER_SIZE,
|
||||
db_header.page_size - db_header.reserved_space as u16,
|
||||
(db_header.get_page_size() - db_header.reserved_space as u32) as u16,
|
||||
);
|
||||
|
||||
let contents = page1.get().contents.as_mut().unwrap();
|
||||
@@ -278,9 +314,9 @@ pub struct Connection {
|
||||
pager: Rc<Pager>,
|
||||
schema: Arc<RwLock<Schema>>,
|
||||
header: Arc<SpinLock<DatabaseHeader>>,
|
||||
auto_commit: RefCell<bool>,
|
||||
auto_commit: Cell<bool>,
|
||||
mv_transactions: RefCell<Vec<crate::mvcc::database::TxID>>,
|
||||
transaction_state: RefCell<TransactionState>,
|
||||
transaction_state: Cell<TransactionState>,
|
||||
last_insert_rowid: Cell<u64>,
|
||||
last_change: Cell<i64>,
|
||||
total_changes: Cell<i64>,
|
||||
@@ -517,7 +553,26 @@ impl Connection {
|
||||
}
|
||||
|
||||
pub fn get_auto_commit(&self) -> bool {
|
||||
*self.auto_commit.borrow()
|
||||
self.auto_commit.get()
|
||||
}
|
||||
|
||||
pub fn parse_schema_rows(self: &Rc<Connection>) -> Result<()> {
|
||||
let rows = self.query("SELECT * FROM sqlite_schema")?;
|
||||
let mut schema = self
|
||||
.schema
|
||||
.try_write()
|
||||
.expect("lock on schema should succeed first try");
|
||||
{
|
||||
let syms = self.syms.borrow();
|
||||
if let Err(LimboError::ExtensionError(e)) =
|
||||
parse_schema_rows(rows, &mut schema, self.pager.io.clone(), &syms, None)
|
||||
{
|
||||
// this means that a vtab exists and we no longer have the module loaded. we print
|
||||
// a warning to the user to load the module
|
||||
eprintln!("Warning: {}", e);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -564,7 +619,7 @@ impl Statement {
|
||||
self.program.result_columns.len()
|
||||
}
|
||||
|
||||
pub fn get_column_name(&self, idx: usize) -> Cow<String> {
|
||||
pub fn get_column_name(&self, idx: usize) -> Cow<str> {
|
||||
let column = &self.program.result_columns[idx];
|
||||
match column.name(&self.program.table_references) {
|
||||
Some(name) => Cow::Borrowed(name),
|
||||
@@ -607,12 +662,28 @@ pub struct VirtualTable {
|
||||
args: Option<Vec<ast::Expr>>,
|
||||
pub implementation: Rc<VTabModuleImpl>,
|
||||
columns: Vec<Column>,
|
||||
kind: VTabKind,
|
||||
}
|
||||
|
||||
impl VirtualTable {
|
||||
pub(crate) fn rowid(&self, cursor: &VTabOpaqueCursor) -> i64 {
|
||||
unsafe { (self.implementation.rowid)(cursor.as_ptr()) }
|
||||
}
|
||||
|
||||
pub(crate) fn best_index(
|
||||
&self,
|
||||
constraints: &[ConstraintInfo],
|
||||
order_by: &[OrderByInfo],
|
||||
) -> IndexInfo {
|
||||
unsafe {
|
||||
IndexInfo::from_ffi((self.implementation.best_idx)(
|
||||
constraints.as_ptr(),
|
||||
constraints.len() as i32,
|
||||
order_by.as_ptr(),
|
||||
order_by.len() as i32,
|
||||
))
|
||||
}
|
||||
}
|
||||
/// takes ownership of the provided Args
|
||||
pub(crate) fn from_args(
|
||||
tbl_name: Option<&str>,
|
||||
@@ -630,7 +701,7 @@ impl VirtualTable {
|
||||
module_name
|
||||
)))?;
|
||||
if let VTabKind::VirtualTable = kind {
|
||||
if module.module_kind != VTabKind::VirtualTable {
|
||||
if module.module_kind == VTabKind::TableValuedFunction {
|
||||
return Err(LimboError::ExtensionError(format!(
|
||||
"{} is not a virtual table module",
|
||||
module_name
|
||||
@@ -648,6 +719,7 @@ impl VirtualTable {
|
||||
implementation: module.implementation.clone(),
|
||||
columns,
|
||||
args: exprs,
|
||||
kind,
|
||||
});
|
||||
return Ok(vtab);
|
||||
}
|
||||
@@ -661,21 +733,30 @@ impl VirtualTable {
|
||||
VTabOpaqueCursor::new(cursor)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(cursor))]
|
||||
pub fn filter(
|
||||
&self,
|
||||
cursor: &VTabOpaqueCursor,
|
||||
idx_num: i32,
|
||||
idx_str: Option<String>,
|
||||
arg_count: usize,
|
||||
args: Vec<OwnedValue>,
|
||||
args: Vec<limbo_ext::Value>,
|
||||
) -> Result<bool> {
|
||||
let mut filter_args = Vec::with_capacity(arg_count);
|
||||
for i in 0..arg_count {
|
||||
let ownedvalue_arg = args.get(i).unwrap();
|
||||
filter_args.push(ownedvalue_arg.to_ffi());
|
||||
}
|
||||
tracing::trace!("xFilter");
|
||||
let c_idx_str = idx_str
|
||||
.map(|s| std::ffi::CString::new(s).unwrap())
|
||||
.map(|cstr| cstr.into_raw())
|
||||
.unwrap_or(std::ptr::null_mut());
|
||||
let rc = unsafe {
|
||||
(self.implementation.filter)(cursor.as_ptr(), arg_count as i32, filter_args.as_ptr())
|
||||
(self.implementation.filter)(
|
||||
cursor.as_ptr(),
|
||||
arg_count as i32,
|
||||
args.as_ptr(),
|
||||
c_idx_str,
|
||||
idx_num,
|
||||
)
|
||||
};
|
||||
for arg in filter_args {
|
||||
for arg in args {
|
||||
unsafe {
|
||||
arg.__free_internal_type();
|
||||
}
|
||||
@@ -725,6 +806,19 @@ impl VirtualTable {
|
||||
_ => Err(LimboError::ExtensionError(rc.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn destroy(&self) -> Result<()> {
|
||||
let implementation = self.implementation.as_ref();
|
||||
let rc = unsafe {
|
||||
(self.implementation.destroy)(
|
||||
implementation as *const VTabModuleImpl as *const std::ffi::c_void,
|
||||
)
|
||||
};
|
||||
match rc {
|
||||
ResultCode::OK => Ok(()),
|
||||
_ => Err(LimboError::ExtensionError(rc.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SymbolTable {
|
||||
|
||||
575
core/numeric.rs
Normal file
575
core/numeric.rs
Normal file
@@ -0,0 +1,575 @@
|
||||
use crate::OwnedValue;
|
||||
|
||||
mod nonnan;
|
||||
|
||||
use nonnan::NonNan;
|
||||
|
||||
// TODO: Remove when https://github.com/rust-lang/libs-team/issues/230 is available
|
||||
trait SaturatingShl {
|
||||
fn saturating_shl(self, rhs: u32) -> Self;
|
||||
}
|
||||
|
||||
impl SaturatingShl for i64 {
|
||||
fn saturating_shl(self, rhs: u32) -> Self {
|
||||
if rhs >= Self::BITS {
|
||||
0
|
||||
} else {
|
||||
self << rhs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Remove when https://github.com/rust-lang/libs-team/issues/230 is available
|
||||
trait SaturatingShr {
|
||||
fn saturating_shr(self, rhs: u32) -> Self;
|
||||
}
|
||||
|
||||
impl SaturatingShr for i64 {
|
||||
fn saturating_shr(self, rhs: u32) -> Self {
|
||||
if rhs >= Self::BITS {
|
||||
if self >= 0 {
|
||||
0
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
} else {
|
||||
self >> rhs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Numeric {
|
||||
Null,
|
||||
Integer(i64),
|
||||
Float(NonNan),
|
||||
}
|
||||
|
||||
impl Numeric {
|
||||
pub fn try_into_bool(&self) -> Option<bool> {
|
||||
match self {
|
||||
Numeric::Null => None,
|
||||
Numeric::Integer(0) => Some(false),
|
||||
Numeric::Float(non_nan) if *non_nan == 0.0 => Some(false),
|
||||
_ => Some(true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Numeric> for NullableInteger {
|
||||
fn from(value: Numeric) -> Self {
|
||||
match value {
|
||||
Numeric::Null => NullableInteger::Null,
|
||||
Numeric::Integer(v) => NullableInteger::Integer(v),
|
||||
Numeric::Float(v) => NullableInteger::Integer(f64::from(v) as i64),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Numeric> for OwnedValue {
|
||||
fn from(value: Numeric) -> Self {
|
||||
match value {
|
||||
Numeric::Null => OwnedValue::Null,
|
||||
Numeric::Integer(v) => OwnedValue::Integer(v),
|
||||
Numeric::Float(v) => OwnedValue::Float(v.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRef<str>> From<T> for Numeric {
|
||||
fn from(value: T) -> Self {
|
||||
let text = value.as_ref();
|
||||
|
||||
match str_to_f64(text) {
|
||||
None => Self::Integer(0),
|
||||
Some(StrToF64::Fractional(value)) => Self::Float(value),
|
||||
Some(StrToF64::Decimal(real)) => {
|
||||
let integer = str_to_i64(text).unwrap_or(0);
|
||||
|
||||
if real == integer as f64 {
|
||||
Self::Integer(integer)
|
||||
} else {
|
||||
Self::Float(real)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OwnedValue> for Numeric {
|
||||
fn from(value: OwnedValue) -> Self {
|
||||
Self::from(&value)
|
||||
}
|
||||
}
|
||||
impl From<&OwnedValue> for Numeric {
|
||||
fn from(value: &OwnedValue) -> Self {
|
||||
match value {
|
||||
OwnedValue::Null => Self::Null,
|
||||
OwnedValue::Integer(v) => Self::Integer(*v),
|
||||
OwnedValue::Float(v) => match NonNan::new(*v) {
|
||||
Some(v) => Self::Float(v),
|
||||
None => Self::Null,
|
||||
},
|
||||
OwnedValue::Text(text) => Numeric::from(text.as_str()),
|
||||
OwnedValue::Blob(blob) => {
|
||||
let text = String::from_utf8_lossy(blob.as_slice());
|
||||
Numeric::from(&text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Add for Numeric {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
match (self, rhs) {
|
||||
(Numeric::Null, _) | (_, Numeric::Null) => Numeric::Null,
|
||||
(Numeric::Integer(lhs), Numeric::Integer(rhs)) => match lhs.checked_add(rhs) {
|
||||
None => Numeric::Float(lhs.into()) + Numeric::Float(rhs.into()),
|
||||
Some(i) => Numeric::Integer(i),
|
||||
},
|
||||
(Numeric::Float(lhs), Numeric::Float(rhs)) => match lhs + rhs {
|
||||
Some(v) => Numeric::Float(v),
|
||||
None => Numeric::Null,
|
||||
},
|
||||
(f @ Numeric::Float(_), Numeric::Integer(i))
|
||||
| (Numeric::Integer(i), f @ Numeric::Float(_)) => f + Numeric::Float(i.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Sub for Numeric {
|
||||
type Output = Self;
|
||||
|
||||
fn sub(self, rhs: Self) -> Self::Output {
|
||||
match (self, rhs) {
|
||||
(Numeric::Null, _) | (_, Numeric::Null) => Numeric::Null,
|
||||
(Numeric::Float(lhs), Numeric::Float(rhs)) => match lhs - rhs {
|
||||
Some(v) => Numeric::Float(v),
|
||||
None => Numeric::Null,
|
||||
},
|
||||
(Numeric::Integer(lhs), Numeric::Integer(rhs)) => match lhs.checked_sub(rhs) {
|
||||
None => Numeric::Float(lhs.into()) - Numeric::Float(rhs.into()),
|
||||
Some(i) => Numeric::Integer(i),
|
||||
},
|
||||
(f @ Numeric::Float(_), Numeric::Integer(i)) => f - Numeric::Float(i.into()),
|
||||
(Numeric::Integer(i), f @ Numeric::Float(_)) => Numeric::Float(i.into()) - f,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul for Numeric {
|
||||
type Output = Self;
|
||||
|
||||
fn mul(self, rhs: Self) -> Self::Output {
|
||||
match (self, rhs) {
|
||||
(Numeric::Null, _) | (_, Numeric::Null) => Numeric::Null,
|
||||
(Numeric::Float(lhs), Numeric::Float(rhs)) => match lhs * rhs {
|
||||
Some(v) => Numeric::Float(v),
|
||||
None => Numeric::Null,
|
||||
},
|
||||
(Numeric::Integer(lhs), Numeric::Integer(rhs)) => match lhs.checked_mul(rhs) {
|
||||
None => Numeric::Float(lhs.into()) * Numeric::Float(rhs.into()),
|
||||
Some(i) => Numeric::Integer(i),
|
||||
},
|
||||
(f @ Numeric::Float(_), Numeric::Integer(i))
|
||||
| (Numeric::Integer(i), f @ Numeric::Float(_)) => f * Numeric::Float(i.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Div for Numeric {
|
||||
type Output = Self;
|
||||
|
||||
fn div(self, rhs: Self) -> Self::Output {
|
||||
match (self, rhs) {
|
||||
(Numeric::Null, _) | (_, Numeric::Null) => Numeric::Null,
|
||||
(Numeric::Float(lhs), Numeric::Float(rhs)) => match lhs / rhs {
|
||||
Some(v) if rhs != 0.0 => Numeric::Float(v),
|
||||
_ => Numeric::Null,
|
||||
},
|
||||
(Numeric::Integer(lhs), Numeric::Integer(rhs)) => match lhs.checked_div(rhs) {
|
||||
None => Numeric::Float(lhs.into()) / Numeric::Float(rhs.into()),
|
||||
Some(v) => Numeric::Integer(v),
|
||||
},
|
||||
(f @ Numeric::Float(_), Numeric::Integer(i)) => f / Numeric::Float(i.into()),
|
||||
(Numeric::Integer(i), f @ Numeric::Float(_)) => Numeric::Float(i.into()) / f,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Neg for Numeric {
|
||||
type Output = Self;
|
||||
|
||||
fn neg(self) -> Self::Output {
|
||||
match self {
|
||||
Numeric::Null => Numeric::Null,
|
||||
Numeric::Integer(v) => match v.checked_neg() {
|
||||
None => -Numeric::Float(v.into()),
|
||||
Some(i) => Numeric::Integer(i),
|
||||
},
|
||||
Numeric::Float(v) => Numeric::Float(-v),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum NullableInteger {
|
||||
Null,
|
||||
Integer(i64),
|
||||
}
|
||||
|
||||
impl From<NullableInteger> for OwnedValue {
|
||||
fn from(value: NullableInteger) -> Self {
|
||||
match value {
|
||||
NullableInteger::Null => OwnedValue::Null,
|
||||
NullableInteger::Integer(v) => OwnedValue::Integer(v),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRef<str>> From<T> for NullableInteger {
|
||||
fn from(value: T) -> Self {
|
||||
Self::Integer(str_to_i64(value.as_ref()).unwrap_or(0))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OwnedValue> for NullableInteger {
|
||||
fn from(value: OwnedValue) -> Self {
|
||||
Self::from(&value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&OwnedValue> for NullableInteger {
|
||||
fn from(value: &OwnedValue) -> Self {
|
||||
match value {
|
||||
OwnedValue::Null => Self::Null,
|
||||
OwnedValue::Integer(v) => Self::Integer(*v),
|
||||
OwnedValue::Float(v) => Self::Integer(*v as i64),
|
||||
OwnedValue::Text(text) => Self::from(text.as_str()),
|
||||
OwnedValue::Blob(blob) => {
|
||||
let text = String::from_utf8_lossy(blob.as_slice());
|
||||
Self::from(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Not for NullableInteger {
|
||||
type Output = Self;
|
||||
|
||||
fn not(self) -> Self::Output {
|
||||
match self {
|
||||
NullableInteger::Null => NullableInteger::Null,
|
||||
NullableInteger::Integer(lhs) => NullableInteger::Integer(!lhs),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::BitAnd for NullableInteger {
|
||||
type Output = Self;
|
||||
|
||||
fn bitand(self, rhs: Self) -> Self::Output {
|
||||
match (self, rhs) {
|
||||
(NullableInteger::Null, _) | (_, NullableInteger::Null) => NullableInteger::Null,
|
||||
(NullableInteger::Integer(lhs), NullableInteger::Integer(rhs)) => {
|
||||
NullableInteger::Integer(lhs & rhs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::BitOr for NullableInteger {
|
||||
type Output = Self;
|
||||
|
||||
fn bitor(self, rhs: Self) -> Self::Output {
|
||||
match (self, rhs) {
|
||||
(NullableInteger::Null, _) | (_, NullableInteger::Null) => NullableInteger::Null,
|
||||
(NullableInteger::Integer(lhs), NullableInteger::Integer(rhs)) => {
|
||||
NullableInteger::Integer(lhs | rhs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Shl for NullableInteger {
|
||||
type Output = Self;
|
||||
|
||||
fn shl(self, rhs: Self) -> Self::Output {
|
||||
match (self, rhs) {
|
||||
(NullableInteger::Null, _) | (_, NullableInteger::Null) => NullableInteger::Null,
|
||||
(NullableInteger::Integer(lhs), NullableInteger::Integer(rhs)) => {
|
||||
NullableInteger::Integer(if rhs.is_positive() {
|
||||
lhs.saturating_shl(rhs.try_into().unwrap_or(u32::MAX))
|
||||
} else {
|
||||
lhs.saturating_shr(rhs.saturating_abs().try_into().unwrap_or(u32::MAX))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Shr for NullableInteger {
|
||||
type Output = Self;
|
||||
|
||||
fn shr(self, rhs: Self) -> Self::Output {
|
||||
match (self, rhs) {
|
||||
(NullableInteger::Null, _) | (_, NullableInteger::Null) => NullableInteger::Null,
|
||||
(NullableInteger::Integer(lhs), NullableInteger::Integer(rhs)) => {
|
||||
NullableInteger::Integer(if rhs.is_positive() {
|
||||
lhs.saturating_shr(rhs.try_into().unwrap_or(u32::MAX))
|
||||
} else {
|
||||
lhs.saturating_shl(rhs.saturating_abs().try_into().unwrap_or(u32::MAX))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Rem for NullableInteger {
|
||||
type Output = Self;
|
||||
|
||||
fn rem(self, rhs: Self) -> Self::Output {
|
||||
match (self, rhs) {
|
||||
(NullableInteger::Null, _) | (_, NullableInteger::Null) => NullableInteger::Null,
|
||||
(_, NullableInteger::Integer(0)) => NullableInteger::Null,
|
||||
(lhs, NullableInteger::Integer(-1)) => lhs % NullableInteger::Integer(1),
|
||||
(NullableInteger::Integer(lhs), NullableInteger::Integer(rhs)) => {
|
||||
NullableInteger::Integer(lhs % rhs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Maximum u64 that can survive a f64 round trip
|
||||
const MAX_EXACT: u64 = u64::MAX << 11;
|
||||
|
||||
const VERTICAL_TAB: char = '\u{b}';
|
||||
|
||||
/// Encapsulates Dekker's arithmetic for higher precision. This is spiritually the same as using a
|
||||
/// f128 for arithmetic, but cross platform and compatible with sqlite.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct DoubleDouble(f64, f64);
|
||||
|
||||
impl From<u64> for DoubleDouble {
|
||||
fn from(value: u64) -> Self {
|
||||
let r = value as f64;
|
||||
|
||||
// If the value is smaller than MAX_EXACT, the error isn't significant
|
||||
let rr = if r <= MAX_EXACT as f64 {
|
||||
let round_tripped = value as f64 as u64;
|
||||
let sign = if value >= round_tripped { 1.0 } else { -1.0 };
|
||||
|
||||
// Error term is the signed distance of the round tripped value and itself
|
||||
sign * value.abs_diff(round_tripped) as f64
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
DoubleDouble(r, rr)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DoubleDouble> for f64 {
|
||||
fn from(DoubleDouble(a, aa): DoubleDouble) -> Self {
|
||||
a + aa
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul for DoubleDouble {
|
||||
type Output = Self;
|
||||
|
||||
/// Double-Double multiplication. (self.0, self.1) *= (rhs.0, rhs.1)
|
||||
///
|
||||
/// Reference:
|
||||
/// T. J. Dekker, "A Floating-Point Technique for Extending the Available Precision".
|
||||
/// 1971-07-26.
|
||||
///
|
||||
fn mul(self, rhs: Self) -> Self::Output {
|
||||
// TODO: Better variable naming
|
||||
|
||||
let mask = u64::MAX << 26;
|
||||
|
||||
let hx = f64::from_bits(self.0.to_bits() & mask);
|
||||
let tx = self.0 - hx;
|
||||
|
||||
let hy = f64::from_bits(rhs.0.to_bits() & mask);
|
||||
let ty = rhs.0 - hy;
|
||||
|
||||
let p = hx * hy;
|
||||
let q = hx * ty + tx * hy;
|
||||
|
||||
let c = p + q;
|
||||
let cc = p - c + q + tx * ty;
|
||||
let cc = self.0 * rhs.1 + self.1 * rhs.0 + cc;
|
||||
|
||||
let r = c + cc;
|
||||
let rr = (c - r) + cc;
|
||||
|
||||
DoubleDouble(r, rr)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::MulAssign for DoubleDouble {
|
||||
fn mul_assign(&mut self, rhs: Self) {
|
||||
*self = self.clone() * rhs;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn str_to_i64(input: impl AsRef<str>) -> Option<i64> {
|
||||
let input = input
|
||||
.as_ref()
|
||||
.trim_matches(|ch: char| ch.is_ascii_whitespace() || ch == VERTICAL_TAB);
|
||||
|
||||
let mut iter = input.chars().enumerate().peekable();
|
||||
|
||||
iter.next_if(|(_, ch)| matches!(ch, '+' | '-'));
|
||||
let Some((end, _)) = iter.take_while(|(_, ch)| ch.is_ascii_digit()).last() else {
|
||||
return Some(0);
|
||||
};
|
||||
|
||||
input[0..=end].parse::<i64>().map_or_else(
|
||||
|err| match err.kind() {
|
||||
std::num::IntErrorKind::PosOverflow => Some(i64::MAX),
|
||||
std::num::IntErrorKind::NegOverflow => Some(i64::MIN),
|
||||
std::num::IntErrorKind::Empty => unreachable!(),
|
||||
_ => Some(0),
|
||||
},
|
||||
Some,
|
||||
)
|
||||
}
|
||||
|
||||
pub enum StrToF64 {
|
||||
Fractional(NonNan),
|
||||
Decimal(NonNan),
|
||||
}
|
||||
|
||||
pub fn str_to_f64(input: impl AsRef<str>) -> Option<StrToF64> {
|
||||
let mut input = input
|
||||
.as_ref()
|
||||
.trim_matches(|ch: char| ch.is_ascii_whitespace() || ch == VERTICAL_TAB)
|
||||
.chars()
|
||||
.peekable();
|
||||
|
||||
let sign = match input.next_if(|ch| matches!(ch, '-' | '+')) {
|
||||
Some('-') => -1.0,
|
||||
_ => 1.0,
|
||||
};
|
||||
|
||||
let mut had_digits = false;
|
||||
let mut is_fractional = false;
|
||||
|
||||
if matches!(input.peek(), Some('e' | 'E')) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut significant: u64 = 0;
|
||||
|
||||
// Copy as many significant digits as we can
|
||||
while let Some(digit) = input.peek().and_then(|ch| ch.to_digit(10)) {
|
||||
had_digits = true;
|
||||
|
||||
match significant
|
||||
.checked_mul(10)
|
||||
.and_then(|v| v.checked_add(digit as u64))
|
||||
{
|
||||
Some(new) => significant = new,
|
||||
None => break,
|
||||
}
|
||||
|
||||
input.next();
|
||||
}
|
||||
|
||||
let mut exponent = 0;
|
||||
|
||||
// Increment the exponent for every non significant digit we skipped
|
||||
while input.next_if(char::is_ascii_digit).is_some() {
|
||||
exponent += 1
|
||||
}
|
||||
|
||||
if input.next_if(|ch| matches!(ch, '.')).is_some() {
|
||||
if had_digits || input.peek().is_some_and(char::is_ascii_digit) {
|
||||
is_fractional = true
|
||||
}
|
||||
|
||||
while let Some(digit) = input.peek().and_then(|ch| ch.to_digit(10)) {
|
||||
if significant < (u64::MAX - 9) / 10 {
|
||||
significant = significant * 10 + digit as u64;
|
||||
exponent -= 1;
|
||||
}
|
||||
|
||||
input.next();
|
||||
}
|
||||
};
|
||||
|
||||
if input.next_if(|ch| matches!(ch, 'e' | 'E')).is_some() {
|
||||
let sign = match input.next_if(|ch| matches!(ch, '-' | '+')) {
|
||||
Some('-') => -1,
|
||||
_ => 1,
|
||||
};
|
||||
|
||||
if input.peek().is_some_and(char::is_ascii_digit) {
|
||||
is_fractional = true
|
||||
}
|
||||
|
||||
let e = input.map_while(|ch| ch.to_digit(10)).fold(0, |acc, digit| {
|
||||
if acc < 1000 {
|
||||
acc * 10 + digit as i32
|
||||
} else {
|
||||
1000
|
||||
}
|
||||
});
|
||||
|
||||
exponent += sign * e;
|
||||
};
|
||||
|
||||
while exponent.is_positive() && significant < MAX_EXACT / 10 {
|
||||
significant *= 10;
|
||||
exponent -= 1;
|
||||
}
|
||||
|
||||
while exponent.is_negative() && significant % 10 == 0 {
|
||||
significant /= 10;
|
||||
exponent += 1;
|
||||
}
|
||||
|
||||
let mut result = DoubleDouble::from(significant);
|
||||
|
||||
if exponent > 0 {
|
||||
while exponent >= 100 {
|
||||
exponent -= 100;
|
||||
result *= DoubleDouble(1.0e+100, -1.5902891109759918046e+83);
|
||||
}
|
||||
while exponent >= 10 {
|
||||
exponent -= 10;
|
||||
result *= DoubleDouble(1.0e+10, 0.0);
|
||||
}
|
||||
while exponent >= 1 {
|
||||
exponent -= 1;
|
||||
result *= DoubleDouble(1.0e+01, 0.0);
|
||||
}
|
||||
} else {
|
||||
while exponent <= -100 {
|
||||
exponent += 100;
|
||||
result *= DoubleDouble(1.0e-100, -1.99918998026028836196e-117);
|
||||
}
|
||||
while exponent <= -10 {
|
||||
exponent += 10;
|
||||
result *= DoubleDouble(1.0e-10, -3.6432197315497741579e-27);
|
||||
}
|
||||
while exponent <= -1 {
|
||||
exponent += 1;
|
||||
result *= DoubleDouble(1.0e-01, -5.5511151231257827021e-18);
|
||||
}
|
||||
}
|
||||
|
||||
let result = NonNan::new(f64::from(result) * sign)
|
||||
.unwrap_or_else(|| NonNan::new(sign * f64::INFINITY).unwrap());
|
||||
|
||||
Some(if is_fractional {
|
||||
StrToF64::Fractional(result)
|
||||
} else {
|
||||
StrToF64::Decimal(result)
|
||||
})
|
||||
}
|
||||
105
core/numeric/nonnan.rs
Normal file
105
core/numeric/nonnan.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
#[repr(transparent)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct NonNan(f64);
|
||||
|
||||
impl NonNan {
|
||||
pub fn new(value: f64) -> Option<Self> {
|
||||
if value.is_nan() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(NonNan(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<NonNan> for f64 {
|
||||
fn eq(&self, other: &NonNan) -> bool {
|
||||
*self == other.0
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<f64> for NonNan {
|
||||
fn eq(&self, other: &f64) -> bool {
|
||||
self.0 == *other
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd<f64> for NonNan {
|
||||
fn partial_cmp(&self, other: &f64) -> Option<std::cmp::Ordering> {
|
||||
self.0.partial_cmp(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd<NonNan> for f64 {
|
||||
fn partial_cmp(&self, other: &NonNan) -> Option<std::cmp::Ordering> {
|
||||
self.partial_cmp(&other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i64> for NonNan {
|
||||
fn from(value: i64) -> Self {
|
||||
NonNan(value as f64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NonNan> for f64 {
|
||||
fn from(value: NonNan) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for NonNan {
|
||||
type Target = f64;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Add for NonNan {
|
||||
type Output = Option<NonNan>;
|
||||
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Self::new(self.0 + rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Sub for NonNan {
|
||||
type Output = Option<NonNan>;
|
||||
|
||||
fn sub(self, rhs: Self) -> Self::Output {
|
||||
Self::new(self.0 - rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul for NonNan {
|
||||
type Output = Option<NonNan>;
|
||||
|
||||
fn mul(self, rhs: Self) -> Self::Output {
|
||||
Self::new(self.0 * rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Div for NonNan {
|
||||
type Output = Option<NonNan>;
|
||||
|
||||
fn div(self, rhs: Self) -> Self::Output {
|
||||
Self::new(self.0 / rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Rem for NonNan {
|
||||
type Output = Option<NonNan>;
|
||||
|
||||
fn rem(self, rhs: Self) -> Self::Output {
|
||||
Self::new(self.0 % rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Neg for NonNan {
|
||||
type Output = Self;
|
||||
|
||||
fn neg(self) -> Self::Output {
|
||||
Self(-self.0)
|
||||
}
|
||||
}
|
||||
285
core/schema.rs
285
core/schema.rs
@@ -1,8 +1,8 @@
|
||||
use crate::VirtualTable;
|
||||
use crate::{util::normalize_ident, Result};
|
||||
use crate::{LimboError, VirtualTable};
|
||||
use core::fmt;
|
||||
use fallible_iterator::FallibleIterator;
|
||||
use limbo_sqlite3_parser::ast::{Expr, Literal, TableOptions};
|
||||
use limbo_sqlite3_parser::ast::{Expr, Literal, SortOrder, TableOptions};
|
||||
use limbo_sqlite3_parser::{
|
||||
ast::{Cmd, CreateTableBody, QualifiedName, ResultColumn, Stmt},
|
||||
lexer::sql::Parser,
|
||||
@@ -30,6 +30,13 @@ impl Schema {
|
||||
Self { tables, indexes }
|
||||
}
|
||||
|
||||
pub fn is_unique_idx_name(&self, name: &str) -> bool {
|
||||
!self
|
||||
.indexes
|
||||
.iter()
|
||||
.any(|idx| idx.1.iter().any(|i| i.name == name))
|
||||
}
|
||||
|
||||
pub fn add_btree_table(&mut self, table: Rc<BTreeTable>) {
|
||||
let name = normalize_ident(&table.name);
|
||||
self.tables.insert(name, Table::BTree(table).into());
|
||||
@@ -74,6 +81,14 @@ impl Schema {
|
||||
.map_or_else(|| &[] as &[Arc<Index>], |v| v.as_slice())
|
||||
}
|
||||
|
||||
pub fn get_index(&self, table_name: &str, index_name: &str) -> Option<&Arc<Index>> {
|
||||
let name = normalize_ident(table_name);
|
||||
self.indexes
|
||||
.get(&name)?
|
||||
.iter()
|
||||
.find(|index| index.name == index_name)
|
||||
}
|
||||
|
||||
pub fn remove_indices_for_table(&mut self, table_name: &str) {
|
||||
let name = normalize_ident(table_name);
|
||||
self.indexes.remove(&name);
|
||||
@@ -151,15 +166,16 @@ impl PartialEq for Table {
|
||||
pub struct BTreeTable {
|
||||
pub root_page: usize,
|
||||
pub name: String,
|
||||
pub primary_key_column_names: Vec<String>,
|
||||
pub primary_key_columns: Vec<(String, SortOrder)>,
|
||||
pub columns: Vec<Column>,
|
||||
pub has_rowid: bool,
|
||||
pub is_strict: bool,
|
||||
}
|
||||
|
||||
impl BTreeTable {
|
||||
pub fn get_rowid_alias_column(&self) -> Option<(usize, &Column)> {
|
||||
if self.primary_key_column_names.len() == 1 {
|
||||
let (idx, col) = self.get_column(&self.primary_key_column_names[0]).unwrap();
|
||||
if self.primary_key_columns.len() == 1 {
|
||||
let (idx, col) = self.get_column(&self.primary_key_columns[0].0).unwrap();
|
||||
if self.column_is_rowid_alias(col) {
|
||||
return Some((idx, col));
|
||||
}
|
||||
@@ -171,6 +187,10 @@ impl BTreeTable {
|
||||
col.is_rowid_alias
|
||||
}
|
||||
|
||||
/// Returns the column position and column for a given column name.
|
||||
/// Returns None if the column name is not found.
|
||||
/// E.g. if table is CREATE TABLE t(a, b, c)
|
||||
/// then get_column("b") returns (1, &Column { .. })
|
||||
pub fn get_column(&self, name: &str) -> Option<(usize, &Column)> {
|
||||
let name = normalize_ident(name);
|
||||
for (i, column) in self.columns.iter().enumerate() {
|
||||
@@ -209,7 +229,7 @@ impl BTreeTable {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct PseudoTable {
|
||||
pub columns: Vec<Column>,
|
||||
}
|
||||
@@ -245,12 +265,6 @@ impl PseudoTable {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PseudoTable {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn create_table(
|
||||
tbl_name: QualifiedName,
|
||||
body: CreateTableBody,
|
||||
@@ -259,14 +273,16 @@ fn create_table(
|
||||
let table_name = normalize_ident(&tbl_name.name.0);
|
||||
trace!("Creating table {}", table_name);
|
||||
let mut has_rowid = true;
|
||||
let mut primary_key_column_names = vec![];
|
||||
let mut primary_key_columns = vec![];
|
||||
let mut cols = vec![];
|
||||
let is_strict: bool;
|
||||
match body {
|
||||
CreateTableBody::ColumnsAndConstraints {
|
||||
columns,
|
||||
constraints,
|
||||
options,
|
||||
} => {
|
||||
is_strict = options.contains(TableOptions::STRICT);
|
||||
if let Some(constraints) = constraints {
|
||||
for c in constraints {
|
||||
if let limbo_sqlite3_parser::ast::TableConstraint::PrimaryKey {
|
||||
@@ -274,7 +290,7 @@ fn create_table(
|
||||
} = c.constraint
|
||||
{
|
||||
for column in columns {
|
||||
primary_key_column_names.push(match column.expr {
|
||||
let col_name = match column.expr {
|
||||
Expr::Id(id) => normalize_ident(&id.0),
|
||||
Expr::Literal(Literal::String(value)) => {
|
||||
value.trim_matches('\'').to_owned()
|
||||
@@ -282,7 +298,9 @@ fn create_table(
|
||||
_ => {
|
||||
todo!("Unsupported primary key expression");
|
||||
}
|
||||
});
|
||||
};
|
||||
primary_key_columns
|
||||
.push((col_name, column.order.unwrap_or(SortOrder::Asc)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -339,10 +357,17 @@ fn create_table(
|
||||
let mut default = None;
|
||||
let mut primary_key = false;
|
||||
let mut notnull = false;
|
||||
let mut order = SortOrder::Asc;
|
||||
for c_def in &col_def.constraints {
|
||||
match &c_def.constraint {
|
||||
limbo_sqlite3_parser::ast::ColumnConstraint::PrimaryKey { .. } => {
|
||||
limbo_sqlite3_parser::ast::ColumnConstraint::PrimaryKey {
|
||||
order: o,
|
||||
..
|
||||
} => {
|
||||
primary_key = true;
|
||||
if let Some(o) = o {
|
||||
order = o.clone();
|
||||
}
|
||||
}
|
||||
limbo_sqlite3_parser::ast::ColumnConstraint::NotNull { .. } => {
|
||||
notnull = true;
|
||||
@@ -355,8 +380,11 @@ fn create_table(
|
||||
}
|
||||
|
||||
if primary_key {
|
||||
primary_key_column_names.push(name.clone());
|
||||
} else if primary_key_column_names.contains(&name) {
|
||||
primary_key_columns.push((name.clone(), order));
|
||||
} else if primary_key_columns
|
||||
.iter()
|
||||
.any(|(col_name, _)| col_name == &name)
|
||||
{
|
||||
primary_key = true;
|
||||
}
|
||||
|
||||
@@ -378,7 +406,7 @@ fn create_table(
|
||||
};
|
||||
// flip is_rowid_alias back to false if the table has multiple primary keys
|
||||
// or if the table has no rowid
|
||||
if !has_rowid || primary_key_column_names.len() > 1 {
|
||||
if !has_rowid || primary_key_columns.len() > 1 {
|
||||
for col in cols.iter_mut() {
|
||||
col.is_rowid_alias = false;
|
||||
}
|
||||
@@ -387,8 +415,9 @@ fn create_table(
|
||||
root_page,
|
||||
name: table_name,
|
||||
has_rowid,
|
||||
primary_key_column_names,
|
||||
primary_key_columns,
|
||||
columns: cols,
|
||||
is_strict,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -455,7 +484,7 @@ pub fn affinity(datatype: &str) -> Affinity {
|
||||
}
|
||||
|
||||
// Rule 3: BLOB or empty -> BLOB affinity (historically called NONE)
|
||||
if datatype.contains("BLOB") || datatype.is_empty() {
|
||||
if datatype.contains("BLOB") || datatype.is_empty() || datatype.contains("ANY") {
|
||||
return Affinity::Blob;
|
||||
}
|
||||
|
||||
@@ -478,26 +507,72 @@ pub enum Type {
|
||||
Blob,
|
||||
}
|
||||
|
||||
/// # SQLite Column Type Affinities
|
||||
///
|
||||
/// Each column in an SQLite 3 database is assigned one of the following type affinities:
|
||||
///
|
||||
/// TEXT
|
||||
/// NUMERIC
|
||||
/// INTEGER
|
||||
/// REAL
|
||||
/// BLOB
|
||||
/// (Historical note: The "BLOB" type affinity used to be called "NONE". But that term was easy to confuse with "no affinity" and so it was renamed.)
|
||||
/// - **TEXT**
|
||||
/// - **NUMERIC**
|
||||
/// - **INTEGER**
|
||||
/// - **REAL**
|
||||
/// - **BLOB**
|
||||
///
|
||||
/// A column with TEXT affinity stores all data using storage classes NULL, TEXT or BLOB. If numerical data is inserted into a column with TEXT affinity it is converted into text form before being stored.
|
||||
/// > **Note:** Historically, the "BLOB" type affinity was called "NONE". However, this term was renamed to avoid confusion with "no affinity".
|
||||
///
|
||||
/// A column with NUMERIC affinity may contain values using all five storage classes. When text data is inserted into a NUMERIC column, the storage class of the text is converted to INTEGER or REAL (in order of preference) if the text is a well-formed integer or real literal, respectively. If the TEXT value is a well-formed integer literal that is too large to fit in a 64-bit signed integer, it is converted to REAL. For conversions between TEXT and REAL storage classes, only the first 15 significant decimal digits of the number are preserved. If the TEXT value is not a well-formed integer or real literal, then the value is stored as TEXT. For the purposes of this paragraph, hexadecimal integer literals are not considered well-formed and are stored as TEXT. (This is done for historical compatibility with versions of SQLite prior to version 3.8.6 2014-08-15 where hexadecimal integer literals were first introduced into SQLite.) If a floating point value that can be represented exactly as an integer is inserted into a column with NUMERIC affinity, the value is converted into an integer. No attempt is made to convert NULL or BLOB values.
|
||||
/// ## Affinity Descriptions
|
||||
///
|
||||
/// A string might look like a floating-point literal with a decimal point and/or exponent notation but as long as the value can be expressed as an integer, the NUMERIC affinity will convert it into an integer. Hence, the string '3.0e+5' is stored in a column with NUMERIC affinity as the integer 300000, not as the floating point value 300000.0.
|
||||
/// ### **TEXT**
|
||||
/// - Stores data using the NULL, TEXT, or BLOB storage classes.
|
||||
/// - Numerical data inserted into a column with TEXT affinity is converted into text form before being stored.
|
||||
/// - **Example:**
|
||||
/// ```sql
|
||||
/// CREATE TABLE example (col TEXT);
|
||||
/// INSERT INTO example (col) VALUES (123); -- Stored as '123' (text)
|
||||
/// SELECT typeof(col) FROM example; -- Returns 'text'
|
||||
/// ```
|
||||
///
|
||||
/// A column that uses INTEGER affinity behaves the same as a column with NUMERIC affinity. The difference between INTEGER and NUMERIC affinity is only evident in a CAST expression: The expression "CAST(4.0 AS INT)" returns an integer 4, whereas "CAST(4.0 AS NUMERIC)" leaves the value as a floating-point 4.0.
|
||||
/// ### **NUMERIC**
|
||||
/// - Can store values using all five storage classes.
|
||||
/// - Text data is converted to INTEGER or REAL (in that order of preference) if it is a well-formed integer or real literal.
|
||||
/// - If the text represents an integer too large for a 64-bit signed integer, it is converted to REAL.
|
||||
/// - If the text is not a well-formed literal, it is stored as TEXT.
|
||||
/// - Hexadecimal integer literals are stored as TEXT for historical compatibility.
|
||||
/// - Floating-point values that can be exactly represented as integers are converted to integers.
|
||||
/// - **Example:**
|
||||
/// ```sql
|
||||
/// CREATE TABLE example (col NUMERIC);
|
||||
/// INSERT INTO example (col) VALUES ('3.0e+5'); -- Stored as 300000 (integer)
|
||||
/// SELECT typeof(col) FROM example; -- Returns 'integer'
|
||||
/// ```
|
||||
///
|
||||
/// A column with REAL affinity behaves like a column with NUMERIC affinity except that it forces integer values into floating point representation. (As an internal optimization, small floating point values with no fractional component and stored in columns with REAL affinity are written to disk as integers in order to take up less space and are automatically converted back into floating point as the value is read out. This optimization is completely invisible at the SQL level and can only be detected by examining the raw bits of the database file.)
|
||||
/// ### **INTEGER**
|
||||
/// - Behaves like NUMERIC affinity but differs in `CAST` expressions.
|
||||
/// - **Example:**
|
||||
/// ```sql
|
||||
/// CREATE TABLE example (col INTEGER);
|
||||
/// INSERT INTO example (col) VALUES (4.0); -- Stored as 4 (integer)
|
||||
/// SELECT typeof(col) FROM example; -- Returns 'integer'
|
||||
/// ```
|
||||
///
|
||||
/// A column with affinity BLOB does not prefer one storage class over another and no attempt is made to coerce data from one storage class into another.
|
||||
/// ### **REAL**
|
||||
/// - Similar to NUMERIC affinity but forces integer values into floating-point representation.
|
||||
/// - **Optimization:** Small floating-point values with no fractional component may be stored as integers on disk to save space. This is invisible at the SQL level.
|
||||
/// - **Example:**
|
||||
/// ```sql
|
||||
/// CREATE TABLE example (col REAL);
|
||||
/// INSERT INTO example (col) VALUES (4); -- Stored as 4.0 (real)
|
||||
/// SELECT typeof(col) FROM example; -- Returns 'real'
|
||||
/// ```
|
||||
///
|
||||
/// ### **BLOB**
|
||||
/// - Does not prefer any storage class.
|
||||
/// - No coercion is performed between storage classes.
|
||||
/// - **Example:**
|
||||
/// ```sql
|
||||
/// CREATE TABLE example (col BLOB);
|
||||
/// INSERT INTO example (col) VALUES (x'1234'); -- Stored as a binary blob
|
||||
/// SELECT typeof(col) FROM example; -- Returns 'blob'
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Affinity {
|
||||
Integer,
|
||||
@@ -507,11 +582,11 @@ pub enum Affinity {
|
||||
Numeric,
|
||||
}
|
||||
|
||||
pub const SQLITE_AFF_TEXT: char = 'a';
|
||||
pub const SQLITE_AFF_NONE: char = 'b'; // Historically called NONE, but it's the same as BLOB
|
||||
pub const SQLITE_AFF_NUMERIC: char = 'c';
|
||||
pub const SQLITE_AFF_INTEGER: char = 'd';
|
||||
pub const SQLITE_AFF_REAL: char = 'e';
|
||||
pub const SQLITE_AFF_NONE: char = 'A'; // Historically called NONE, but it's the same as BLOB
|
||||
pub const SQLITE_AFF_TEXT: char = 'B';
|
||||
pub const SQLITE_AFF_NUMERIC: char = 'C';
|
||||
pub const SQLITE_AFF_INTEGER: char = 'D';
|
||||
pub const SQLITE_AFF_REAL: char = 'E';
|
||||
|
||||
impl Affinity {
|
||||
/// This is meant to be used in opcodes like Eq, which state:
|
||||
@@ -530,6 +605,20 @@ impl Affinity {
|
||||
Affinity::Numeric => SQLITE_AFF_NUMERIC,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_char(char: char) -> Result<Self> {
|
||||
match char {
|
||||
SQLITE_AFF_INTEGER => Ok(Affinity::Integer),
|
||||
SQLITE_AFF_TEXT => Ok(Affinity::Text),
|
||||
SQLITE_AFF_NONE => Ok(Affinity::Blob),
|
||||
SQLITE_AFF_REAL => Ok(Affinity::Real),
|
||||
SQLITE_AFF_NUMERIC => Ok(Affinity::Numeric),
|
||||
_ => Err(LimboError::InternalError(format!(
|
||||
"Invalid affinity character: {}",
|
||||
char
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Type {
|
||||
@@ -551,7 +640,8 @@ pub fn sqlite_schema_table() -> BTreeTable {
|
||||
root_page: 1,
|
||||
name: "sqlite_schema".to_string(),
|
||||
has_rowid: true,
|
||||
primary_key_column_names: vec![],
|
||||
is_strict: false,
|
||||
primary_key_columns: vec![],
|
||||
columns: vec![
|
||||
Column {
|
||||
name: Some("type".to_string()),
|
||||
@@ -610,23 +700,24 @@ pub struct Index {
|
||||
pub root_page: usize,
|
||||
pub columns: Vec<IndexColumn>,
|
||||
pub unique: bool,
|
||||
pub ephemeral: bool,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IndexColumn {
|
||||
pub name: String,
|
||||
pub order: Order,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Order {
|
||||
Ascending,
|
||||
Descending,
|
||||
pub order: SortOrder,
|
||||
/// the position of the column in the source table.
|
||||
/// for example:
|
||||
/// CREATE TABLE t(a,b,c)
|
||||
/// CREATE INDEX idx ON t(b)
|
||||
/// b.pos_in_table == 1
|
||||
pub pos_in_table: usize,
|
||||
}
|
||||
|
||||
impl Index {
|
||||
pub fn from_sql(sql: &str, root_page: usize) -> Result<Index> {
|
||||
pub fn from_sql(sql: &str, root_page: usize, table: &BTreeTable) -> Result<Index> {
|
||||
let mut parser = Parser::new(sql.as_bytes());
|
||||
let cmd = parser.next()?;
|
||||
match cmd {
|
||||
@@ -638,23 +729,28 @@ impl Index {
|
||||
..
|
||||
})) => {
|
||||
let index_name = normalize_ident(&idx_name.name.0);
|
||||
let index_columns = columns
|
||||
.into_iter()
|
||||
.map(|col| IndexColumn {
|
||||
name: normalize_ident(&col.expr.to_string()),
|
||||
order: match col.order {
|
||||
Some(limbo_sqlite3_parser::ast::SortOrder::Asc) => Order::Ascending,
|
||||
Some(limbo_sqlite3_parser::ast::SortOrder::Desc) => Order::Descending,
|
||||
None => Order::Ascending,
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
let mut index_columns = Vec::with_capacity(columns.len());
|
||||
for col in columns.into_iter() {
|
||||
let name = normalize_ident(&col.expr.to_string());
|
||||
let Some((pos_in_table, _)) = table.get_column(&name) else {
|
||||
return Err(crate::LimboError::InternalError(format!(
|
||||
"Column {} is in index {} but not found in table {}",
|
||||
name, index_name, table.name
|
||||
)));
|
||||
};
|
||||
index_columns.push(IndexColumn {
|
||||
name,
|
||||
order: col.order.unwrap_or(SortOrder::Asc),
|
||||
pos_in_table,
|
||||
});
|
||||
}
|
||||
Ok(Index {
|
||||
name: index_name,
|
||||
table_name: normalize_ident(&tbl_name.0),
|
||||
root_page,
|
||||
columns: index_columns,
|
||||
unique,
|
||||
ephemeral: false,
|
||||
})
|
||||
}
|
||||
_ => todo!("Expected create index statement"),
|
||||
@@ -666,26 +762,27 @@ impl Index {
|
||||
index_name: &str,
|
||||
root_page: usize,
|
||||
) -> Result<Index> {
|
||||
if table.primary_key_column_names.is_empty() {
|
||||
if table.primary_key_columns.is_empty() {
|
||||
return Err(crate::LimboError::InternalError(
|
||||
"Cannot create automatic index for table without primary key".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let index_columns = table
|
||||
.primary_key_column_names
|
||||
.primary_key_columns
|
||||
.iter()
|
||||
.map(|col_name| {
|
||||
.map(|(col_name, order)| {
|
||||
// Verify that each primary key column exists in the table
|
||||
if table.get_column(col_name).is_none() {
|
||||
let Some((pos_in_table, _)) = table.get_column(col_name) else {
|
||||
return Err(crate::LimboError::InternalError(format!(
|
||||
"Primary key column {} not found in table {}",
|
||||
col_name, table.name
|
||||
"Column {} is in index {} but not found in table {}",
|
||||
col_name, index_name, table.name
|
||||
)));
|
||||
}
|
||||
};
|
||||
Ok(IndexColumn {
|
||||
name: normalize_ident(col_name),
|
||||
order: Order::Ascending, // Primary key indexes are always ascending
|
||||
order: order.clone(),
|
||||
pos_in_table,
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
@@ -696,8 +793,21 @@ impl Index {
|
||||
root_page,
|
||||
columns: index_columns,
|
||||
unique: true, // Primary key indexes are always unique
|
||||
ephemeral: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Given a column position in the table, return the position in the index.
|
||||
/// Returns None if the column is not found in the index.
|
||||
/// For example, given:
|
||||
/// CREATE TABLE t(a, b, c)
|
||||
/// CREATE INDEX idx ON t(b)
|
||||
/// then column_table_pos_to_index_pos(1) returns Some(0)
|
||||
pub fn column_table_pos_to_index_pos(&self, table_pos: usize) -> Option<usize> {
|
||||
self.columns
|
||||
.iter()
|
||||
.position(|c| c.pos_in_table == table_pos)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -818,8 +928,8 @@ mod tests {
|
||||
let column = table.get_column("c").unwrap().1;
|
||||
assert!(!column.primary_key, "column 'c' shouldn't be a primary key");
|
||||
assert_eq!(
|
||||
vec!["a"],
|
||||
table.primary_key_column_names,
|
||||
vec![("a".to_string(), SortOrder::Asc)],
|
||||
table.primary_key_columns,
|
||||
"primary key column names should be ['a']"
|
||||
);
|
||||
Ok(())
|
||||
@@ -836,8 +946,11 @@ mod tests {
|
||||
let column = table.get_column("c").unwrap().1;
|
||||
assert!(!column.primary_key, "column 'c' shouldn't be a primary key");
|
||||
assert_eq!(
|
||||
vec!["a", "b"],
|
||||
table.primary_key_column_names,
|
||||
vec![
|
||||
("a".to_string(), SortOrder::Asc),
|
||||
("b".to_string(), SortOrder::Asc)
|
||||
],
|
||||
table.primary_key_columns,
|
||||
"primary key column names should be ['a', 'b']"
|
||||
);
|
||||
Ok(())
|
||||
@@ -845,7 +958,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
pub fn test_primary_key_separate_single() -> Result<()> {
|
||||
let sql = r#"CREATE TABLE t1 (a INTEGER, b TEXT, c REAL, PRIMARY KEY(a));"#;
|
||||
let sql = r#"CREATE TABLE t1 (a INTEGER, b TEXT, c REAL, PRIMARY KEY(a desc));"#;
|
||||
let table = BTreeTable::from_sql(sql, 0)?;
|
||||
let column = table.get_column("a").unwrap().1;
|
||||
assert!(column.primary_key, "column 'a' should be a primary key");
|
||||
@@ -854,8 +967,8 @@ mod tests {
|
||||
let column = table.get_column("c").unwrap().1;
|
||||
assert!(!column.primary_key, "column 'c' shouldn't be a primary key");
|
||||
assert_eq!(
|
||||
vec!["a"],
|
||||
table.primary_key_column_names,
|
||||
vec![("a".to_string(), SortOrder::Desc)],
|
||||
table.primary_key_columns,
|
||||
"primary key column names should be ['a']"
|
||||
);
|
||||
Ok(())
|
||||
@@ -863,7 +976,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
pub fn test_primary_key_separate_multiple() -> Result<()> {
|
||||
let sql = r#"CREATE TABLE t1 (a INTEGER, b TEXT, c REAL, PRIMARY KEY(a, b));"#;
|
||||
let sql = r#"CREATE TABLE t1 (a INTEGER, b TEXT, c REAL, PRIMARY KEY(a, b desc));"#;
|
||||
let table = BTreeTable::from_sql(sql, 0)?;
|
||||
let column = table.get_column("a").unwrap().1;
|
||||
assert!(column.primary_key, "column 'a' should be a primary key");
|
||||
@@ -872,8 +985,11 @@ mod tests {
|
||||
let column = table.get_column("c").unwrap().1;
|
||||
assert!(!column.primary_key, "column 'c' shouldn't be a primary key");
|
||||
assert_eq!(
|
||||
vec!["a", "b"],
|
||||
table.primary_key_column_names,
|
||||
vec![
|
||||
("a".to_string(), SortOrder::Asc),
|
||||
("b".to_string(), SortOrder::Desc)
|
||||
],
|
||||
table.primary_key_columns,
|
||||
"primary key column names should be ['a', 'b']"
|
||||
);
|
||||
Ok(())
|
||||
@@ -890,8 +1006,8 @@ mod tests {
|
||||
let column = table.get_column("c").unwrap().1;
|
||||
assert!(!column.primary_key, "column 'c' shouldn't be a primary key");
|
||||
assert_eq!(
|
||||
vec!["a"],
|
||||
table.primary_key_column_names,
|
||||
vec![("a".to_string(), SortOrder::Asc)],
|
||||
table.primary_key_columns,
|
||||
"primary key column names should be ['a']"
|
||||
);
|
||||
Ok(())
|
||||
@@ -907,8 +1023,8 @@ mod tests {
|
||||
let column = table.get_column("c").unwrap().1;
|
||||
assert!(!column.primary_key, "column 'c' shouldn't be a primary key");
|
||||
assert_eq!(
|
||||
vec!["a"],
|
||||
table.primary_key_column_names,
|
||||
vec![("a".to_string(), SortOrder::Asc)],
|
||||
table.primary_key_columns,
|
||||
"primary key column names should be ['a']"
|
||||
);
|
||||
Ok(())
|
||||
@@ -1012,7 +1128,7 @@ mod tests {
|
||||
assert!(index.unique);
|
||||
assert_eq!(index.columns.len(), 1);
|
||||
assert_eq!(index.columns[0].name, "a");
|
||||
assert!(matches!(index.columns[0].order, Order::Ascending));
|
||||
assert!(matches!(index.columns[0].order, SortOrder::Asc));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1029,8 +1145,8 @@ mod tests {
|
||||
assert_eq!(index.columns.len(), 2);
|
||||
assert_eq!(index.columns[0].name, "a");
|
||||
assert_eq!(index.columns[1].name, "b");
|
||||
assert!(matches!(index.columns[0].order, Order::Ascending));
|
||||
assert!(matches!(index.columns[1].order, Order::Ascending));
|
||||
assert!(matches!(index.columns[0].order, SortOrder::Asc));
|
||||
assert!(matches!(index.columns[1].order, SortOrder::Asc));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1055,7 +1171,8 @@ mod tests {
|
||||
root_page: 0,
|
||||
name: "t1".to_string(),
|
||||
has_rowid: true,
|
||||
primary_key_column_names: vec!["nonexistent".to_string()],
|
||||
is_strict: false,
|
||||
primary_key_columns: vec![("nonexistent".to_string(), SortOrder::Asc)],
|
||||
columns: vec![Column {
|
||||
name: Some("a".to_string()),
|
||||
ty: Type::Integer,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,3 @@
|
||||
#[cfg(feature = "fs")]
|
||||
use crate::error::LimboError;
|
||||
use crate::{io::Completion, Buffer, Result};
|
||||
use std::{cell::RefCell, sync::Arc};
|
||||
@@ -70,3 +69,52 @@ impl DatabaseFile {
|
||||
Self { file }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FileMemoryStorage {
|
||||
file: Arc<dyn crate::io::File>,
|
||||
}
|
||||
|
||||
unsafe impl Send for FileMemoryStorage {}
|
||||
unsafe impl Sync for FileMemoryStorage {}
|
||||
|
||||
impl DatabaseStorage for FileMemoryStorage {
|
||||
fn read_page(&self, page_idx: usize, c: Completion) -> Result<()> {
|
||||
let r = match c {
|
||||
Completion::Read(ref r) => r,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let size = r.buf().len();
|
||||
assert!(page_idx > 0);
|
||||
if !(512..=65536).contains(&size) || size & (size - 1) != 0 {
|
||||
return Err(LimboError::NotADB);
|
||||
}
|
||||
let pos = (page_idx - 1) * size;
|
||||
self.file.pread(pos, c)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_page(
|
||||
&self,
|
||||
page_idx: usize,
|
||||
buffer: Arc<RefCell<Buffer>>,
|
||||
c: Completion,
|
||||
) -> Result<()> {
|
||||
let buffer_size = buffer.borrow().len();
|
||||
assert!(buffer_size >= 512);
|
||||
assert!(buffer_size <= 65536);
|
||||
assert_eq!(buffer_size & (buffer_size - 1), 0);
|
||||
let pos = (page_idx - 1) * buffer_size;
|
||||
self.file.pwrite(pos, buffer, c)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sync(&self, c: Completion) -> Result<()> {
|
||||
self.file.sync(c)
|
||||
}
|
||||
}
|
||||
|
||||
impl FileMemoryStorage {
|
||||
pub fn new(file: Arc<dyn crate::io::File>) -> Self {
|
||||
Self { file }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +123,13 @@ impl Page {
|
||||
tracing::debug!("clear loaded {}", self.get().id);
|
||||
self.get().flags.fetch_and(!PAGE_LOADED, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
pub fn is_index(&self) -> bool {
|
||||
match self.get_contents().page_type() {
|
||||
PageType::IndexLeaf | PageType::IndexInterior => true,
|
||||
PageType::TableLeaf | PageType::TableInterior => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
@@ -157,7 +164,7 @@ pub struct Pager {
|
||||
/// Source of the database pages.
|
||||
pub db_file: Arc<dyn DatabaseStorage>,
|
||||
/// The write-ahead log (WAL) for the database.
|
||||
wal: Rc<RefCell<dyn Wal>>,
|
||||
wal: Option<Rc<RefCell<dyn Wal>>>,
|
||||
/// A page cache for the database.
|
||||
page_cache: Arc<RwLock<DumbLruPageCache>>,
|
||||
/// Buffer pool for temporary data storage.
|
||||
@@ -183,7 +190,7 @@ impl Pager {
|
||||
pub fn finish_open(
|
||||
db_header_ref: Arc<SpinLock<DatabaseHeader>>,
|
||||
db_file: Arc<dyn DatabaseStorage>,
|
||||
wal: Rc<RefCell<dyn Wal>>,
|
||||
wal: Option<Rc<RefCell<dyn Wal>>>,
|
||||
io: Arc<dyn crate::io::IO>,
|
||||
page_cache: Arc<RwLock<DumbLruPageCache>>,
|
||||
buffer_pool: Rc<BufferPool>,
|
||||
@@ -206,20 +213,31 @@ impl Pager {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn btree_create(&self, flags: usize) -> u32 {
|
||||
pub fn btree_create(&self, flags: &CreateBTreeFlags) -> u32 {
|
||||
let page_type = match flags {
|
||||
1 => PageType::TableLeaf,
|
||||
2 => PageType::IndexLeaf,
|
||||
_ => unreachable!(
|
||||
"wrong create table flags, should be 1 for table and 2 for index, got {}",
|
||||
flags,
|
||||
),
|
||||
_ if flags.is_table() => PageType::TableLeaf,
|
||||
_ if flags.is_index() => PageType::IndexLeaf,
|
||||
_ => unreachable!("Invalid flags state"),
|
||||
};
|
||||
let page = self.do_allocate_page(page_type, 0);
|
||||
let id = page.get().id;
|
||||
id as u32
|
||||
}
|
||||
|
||||
/// Allocate a new overflow page.
|
||||
/// This is done when a cell overflows and new space is needed.
|
||||
pub fn allocate_overflow_page(&self) -> PageRef {
|
||||
let page = self.allocate_page().unwrap();
|
||||
tracing::debug!("Pager::allocate_overflow_page(id={})", page.get().id);
|
||||
|
||||
// setup overflow page
|
||||
let contents = page.get().contents.as_mut().unwrap();
|
||||
let buf = contents.as_ptr();
|
||||
buf.fill(0);
|
||||
|
||||
page
|
||||
}
|
||||
|
||||
/// Allocate a new page to the btree via the pager.
|
||||
/// This marks the page as dirty and writes the page header.
|
||||
pub fn do_allocate_page(&self, page_type: PageType, offset: usize) -> PageRef {
|
||||
@@ -239,33 +257,47 @@ impl Pager {
|
||||
/// In other words, if the page size is 512, then the reserved space size cannot exceed 32.
|
||||
pub fn usable_space(&self) -> usize {
|
||||
let db_header = self.db_header.lock();
|
||||
(db_header.page_size - db_header.reserved_space as u16) as usize
|
||||
(db_header.get_page_size() - db_header.reserved_space as u32) as usize
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn begin_read_tx(&self) -> Result<LimboResult> {
|
||||
self.wal.borrow_mut().begin_read_tx()
|
||||
if let Some(wal) = &self.wal {
|
||||
return wal.borrow_mut().begin_read_tx();
|
||||
}
|
||||
|
||||
Ok(LimboResult::Ok)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn begin_write_tx(&self) -> Result<LimboResult> {
|
||||
self.wal.borrow_mut().begin_write_tx()
|
||||
if let Some(wal) = &self.wal {
|
||||
return wal.borrow_mut().begin_write_tx();
|
||||
}
|
||||
|
||||
Ok(LimboResult::Ok)
|
||||
}
|
||||
|
||||
pub fn end_tx(&self) -> Result<CheckpointStatus> {
|
||||
let checkpoint_status = self.cacheflush()?;
|
||||
match checkpoint_status {
|
||||
CheckpointStatus::IO => Ok(checkpoint_status),
|
||||
CheckpointStatus::Done(_) => {
|
||||
self.wal.borrow().end_write_tx()?;
|
||||
self.wal.borrow().end_read_tx()?;
|
||||
Ok(checkpoint_status)
|
||||
}
|
||||
if let Some(wal) = &self.wal {
|
||||
let checkpoint_status = self.cacheflush()?;
|
||||
return match checkpoint_status {
|
||||
CheckpointStatus::IO => Ok(checkpoint_status),
|
||||
CheckpointStatus::Done(_) => {
|
||||
wal.borrow().end_write_tx()?;
|
||||
wal.borrow().end_read_tx()?;
|
||||
Ok(checkpoint_status)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Ok(CheckpointStatus::Done(CheckpointResult::default()))
|
||||
}
|
||||
|
||||
pub fn end_read_tx(&self) -> Result<()> {
|
||||
self.wal.borrow().end_read_tx()?;
|
||||
if let Some(wal) = &self.wal {
|
||||
wal.borrow().end_read_tx()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -273,7 +305,11 @@ impl Pager {
|
||||
pub fn read_page(&self, page_idx: usize) -> Result<PageRef> {
|
||||
tracing::trace!("read_page(page_idx = {})", page_idx);
|
||||
let mut page_cache = self.page_cache.write();
|
||||
let page_key = PageCacheKey::new(page_idx, Some(self.wal.borrow().get_max_frame()));
|
||||
let max_frame = match &self.wal {
|
||||
Some(wal) => wal.borrow().get_max_frame(),
|
||||
None => 0,
|
||||
};
|
||||
let page_key = PageCacheKey::new(page_idx, Some(max_frame));
|
||||
if let Some(page) = page_cache.get(&page_key) {
|
||||
tracing::trace!("read_page(page_idx = {}) = cached", page_idx);
|
||||
return Ok(page.clone());
|
||||
@@ -281,17 +317,18 @@ impl Pager {
|
||||
let page = Arc::new(Page::new(page_idx));
|
||||
page.set_locked();
|
||||
|
||||
if let Some(frame_id) = self.wal.borrow().find_frame(page_idx as u64)? {
|
||||
self.wal
|
||||
.borrow()
|
||||
.read_frame(frame_id, page.clone(), self.buffer_pool.clone())?;
|
||||
{
|
||||
page.set_uptodate();
|
||||
if let Some(wal) = &self.wal {
|
||||
if let Some(frame_id) = wal.borrow().find_frame(page_idx as u64)? {
|
||||
wal.borrow()
|
||||
.read_frame(frame_id, page.clone(), self.buffer_pool.clone())?;
|
||||
{
|
||||
page.set_uptodate();
|
||||
}
|
||||
// TODO(pere) ensure page is inserted, we should probably first insert to page cache
|
||||
// and if successful, read frame or page
|
||||
page_cache.insert(page_key, page.clone());
|
||||
return Ok(page);
|
||||
}
|
||||
// TODO(pere) ensure page is inserted, we should probably first insert to page cache
|
||||
// and if successful, read frame or page
|
||||
page_cache.insert(page_key, page.clone());
|
||||
return Ok(page);
|
||||
}
|
||||
sqlite3_ondisk::begin_read_page(
|
||||
self.db_file.clone(),
|
||||
@@ -310,19 +347,29 @@ impl Pager {
|
||||
trace!("load_page(page_idx = {})", id);
|
||||
let mut page_cache = self.page_cache.write();
|
||||
page.set_locked();
|
||||
let page_key = PageCacheKey::new(id, Some(self.wal.borrow().get_max_frame()));
|
||||
if let Some(frame_id) = self.wal.borrow().find_frame(id as u64)? {
|
||||
self.wal
|
||||
.borrow()
|
||||
.read_frame(frame_id, page.clone(), self.buffer_pool.clone())?;
|
||||
{
|
||||
page.set_uptodate();
|
||||
let max_frame = match &self.wal {
|
||||
Some(wal) => wal.borrow().get_max_frame(),
|
||||
None => 0,
|
||||
};
|
||||
let page_key = PageCacheKey::new(id, Some(max_frame));
|
||||
if let Some(wal) = &self.wal {
|
||||
if let Some(frame_id) = wal.borrow().find_frame(id as u64)? {
|
||||
wal.borrow()
|
||||
.read_frame(frame_id, page.clone(), self.buffer_pool.clone())?;
|
||||
{
|
||||
page.set_uptodate();
|
||||
}
|
||||
// TODO(pere) ensure page is inserted
|
||||
if !page_cache.contains_key(&page_key) {
|
||||
page_cache.insert(page_key, page.clone());
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
// TODO(pere) ensure page is inserted
|
||||
if !page_cache.contains_key(&page_key) {
|
||||
page_cache.insert(page_key, page.clone());
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// TODO(pere) ensure page is inserted
|
||||
if !page_cache.contains_key(&page_key) {
|
||||
page_cache.insert(page_key, page.clone());
|
||||
}
|
||||
sqlite3_ondisk::begin_read_page(
|
||||
self.db_file.clone(),
|
||||
@@ -330,10 +377,7 @@ impl Pager {
|
||||
page.clone(),
|
||||
id,
|
||||
)?;
|
||||
// TODO(pere) ensure page is inserted
|
||||
if !page_cache.contains_key(&page_key) {
|
||||
page_cache.insert(page_key, page.clone());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -362,18 +406,23 @@ impl Pager {
|
||||
match state {
|
||||
FlushState::Start => {
|
||||
let db_size = self.db_header.lock().database_size;
|
||||
let max_frame = match &self.wal {
|
||||
Some(wal) => wal.borrow().get_max_frame(),
|
||||
None => 0,
|
||||
};
|
||||
for page_id in self.dirty_pages.borrow().iter() {
|
||||
let mut cache = self.page_cache.write();
|
||||
let page_key =
|
||||
PageCacheKey::new(*page_id, Some(self.wal.borrow().get_max_frame()));
|
||||
let page = cache.get(&page_key).expect("we somehow added a page to dirty list but we didn't mark it as dirty, causing cache to drop it.");
|
||||
let page_type = page.get().contents.as_ref().unwrap().maybe_page_type();
|
||||
trace!("cacheflush(page={}, page_type={:?}", page_id, page_type);
|
||||
self.wal.borrow_mut().append_frame(
|
||||
page.clone(),
|
||||
db_size,
|
||||
self.flush_info.borrow().in_flight_writes.clone(),
|
||||
)?;
|
||||
let page_key = PageCacheKey::new(*page_id, Some(max_frame));
|
||||
if let Some(wal) = &self.wal {
|
||||
let page = cache.get(&page_key).expect("we somehow added a page to dirty list but we didn't mark it as dirty, causing cache to drop it.");
|
||||
let page_type = page.get().contents.as_ref().unwrap().maybe_page_type();
|
||||
trace!("cacheflush(page={}, page_type={:?}", page_id, page_type);
|
||||
wal.borrow_mut().append_frame(
|
||||
page.clone(),
|
||||
db_size,
|
||||
self.flush_info.borrow().in_flight_writes.clone(),
|
||||
)?;
|
||||
}
|
||||
// This page is no longer valid.
|
||||
// For example:
|
||||
// We took page with key (page_num, max_frame) -- this page is no longer valid for that max_frame so it must be invalidated.
|
||||
@@ -392,13 +441,16 @@ impl Pager {
|
||||
}
|
||||
}
|
||||
FlushState::SyncWal => {
|
||||
match self.wal.borrow_mut().sync() {
|
||||
let wal = self.wal.clone().ok_or(LimboError::InternalError(
|
||||
"SyncWal was called without a existing wal".to_string(),
|
||||
))?;
|
||||
match wal.borrow_mut().sync() {
|
||||
Ok(CheckpointStatus::IO) => return Ok(CheckpointStatus::IO),
|
||||
Ok(CheckpointStatus::Done(res)) => checkpoint_result = res,
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
|
||||
let should_checkpoint = self.wal.borrow().should_checkpoint();
|
||||
let should_checkpoint = wal.borrow().should_checkpoint();
|
||||
if should_checkpoint {
|
||||
self.flush_info.borrow_mut().state = FlushState::Checkpoint;
|
||||
} else {
|
||||
@@ -440,11 +492,13 @@ impl Pager {
|
||||
match state {
|
||||
CheckpointState::Checkpoint => {
|
||||
let in_flight = self.checkpoint_inflight.clone();
|
||||
match self.wal.borrow_mut().checkpoint(
|
||||
self,
|
||||
in_flight,
|
||||
CheckpointMode::Passive,
|
||||
)? {
|
||||
let wal = self.wal.clone().ok_or(LimboError::InternalError(
|
||||
"Checkpoint was called without a existing wal".to_string(),
|
||||
))?;
|
||||
match wal
|
||||
.borrow_mut()
|
||||
.checkpoint(self, in_flight, CheckpointMode::Passive)?
|
||||
{
|
||||
CheckpointStatus::IO => return Ok(CheckpointStatus::IO),
|
||||
CheckpointStatus::Done(res) => {
|
||||
checkpoint_result = res;
|
||||
@@ -481,7 +535,7 @@ impl Pager {
|
||||
pub fn clear_page_cache(&self) -> CheckpointResult {
|
||||
let checkpoint_result: CheckpointResult;
|
||||
loop {
|
||||
match self.wal.borrow_mut().checkpoint(
|
||||
match self.wal.clone().unwrap().borrow_mut().checkpoint(
|
||||
self,
|
||||
Rc::new(RefCell::new(0)),
|
||||
CheckpointMode::Passive,
|
||||
@@ -606,8 +660,12 @@ impl Pager {
|
||||
page.set_dirty();
|
||||
self.add_dirty(page.get().id);
|
||||
let mut cache = self.page_cache.write();
|
||||
let page_key =
|
||||
PageCacheKey::new(page.get().id, Some(self.wal.borrow().get_max_frame()));
|
||||
let max_frame = match &self.wal {
|
||||
Some(wal) => wal.borrow().get_max_frame(),
|
||||
None => 0,
|
||||
};
|
||||
|
||||
let page_key = PageCacheKey::new(page.get().id, Some(max_frame));
|
||||
cache.insert(page_key, page.clone());
|
||||
}
|
||||
Ok(page)
|
||||
@@ -616,14 +674,18 @@ impl Pager {
|
||||
pub fn put_loaded_page(&self, id: usize, page: PageRef) {
|
||||
let mut cache = self.page_cache.write();
|
||||
// cache insert invalidates previous page
|
||||
let page_key = PageCacheKey::new(id, Some(self.wal.borrow().get_max_frame()));
|
||||
let max_frame = match &self.wal {
|
||||
Some(wal) => wal.borrow().get_max_frame(),
|
||||
None => 0,
|
||||
};
|
||||
let page_key = PageCacheKey::new(id, Some(max_frame));
|
||||
cache.insert(page_key, page.clone());
|
||||
page.set_loaded();
|
||||
}
|
||||
|
||||
pub fn usable_size(&self) -> usize {
|
||||
let db_header = self.db_header.lock();
|
||||
(db_header.page_size - db_header.reserved_space as u16) as usize
|
||||
(db_header.get_page_size() - db_header.reserved_space as u32) as usize
|
||||
}
|
||||
}
|
||||
|
||||
@@ -637,15 +699,38 @@ pub fn allocate_page(page_id: usize, buffer_pool: &Rc<BufferPool>, offset: usize
|
||||
});
|
||||
let buffer = Arc::new(RefCell::new(Buffer::new(buffer, drop_fn)));
|
||||
page.set_loaded();
|
||||
page.get().contents = Some(PageContent {
|
||||
offset,
|
||||
buffer,
|
||||
overflow_cells: Vec::new(),
|
||||
});
|
||||
page.get().contents = Some(PageContent::new(offset, buffer));
|
||||
}
|
||||
page
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CreateBTreeFlags(pub u8);
|
||||
impl CreateBTreeFlags {
|
||||
pub const TABLE: u8 = 0b0001;
|
||||
pub const INDEX: u8 = 0b0010;
|
||||
|
||||
pub fn new_table() -> Self {
|
||||
Self(CreateBTreeFlags::TABLE)
|
||||
}
|
||||
|
||||
pub fn new_index() -> Self {
|
||||
Self(CreateBTreeFlags::INDEX)
|
||||
}
|
||||
|
||||
pub fn is_table(&self) -> bool {
|
||||
(self.0 & CreateBTreeFlags::TABLE) != 0
|
||||
}
|
||||
|
||||
pub fn is_index(&self) -> bool {
|
||||
(self.0 & CreateBTreeFlags::INDEX) != 0
|
||||
}
|
||||
|
||||
pub fn get_flags(&self) -> u8 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -47,7 +47,9 @@ use crate::io::{Buffer, Completion, ReadCompletion, SyncCompletion, WriteComplet
|
||||
use crate::storage::buffer_pool::BufferPool;
|
||||
use crate::storage::database::DatabaseStorage;
|
||||
use crate::storage::pager::Pager;
|
||||
use crate::types::{ImmutableRecord, RawSlice, RefValue, TextRef, TextSubtype};
|
||||
use crate::types::{
|
||||
ImmutableRecord, RawSlice, RefValue, SerialType, SerialTypeKind, TextRef, TextSubtype,
|
||||
};
|
||||
use crate::{File, Result};
|
||||
use std::cell::RefCell;
|
||||
use std::mem::MaybeUninit;
|
||||
@@ -63,9 +65,19 @@ pub const DATABASE_HEADER_SIZE: usize = 100;
|
||||
// DEFAULT_CACHE_SIZE negative values mean that we store the amount of pages a XKiB of memory can hold.
|
||||
// We can calculate "real" cache size by diving by page size.
|
||||
const DEFAULT_CACHE_SIZE: i32 = -2000;
|
||||
|
||||
// Minimum number of pages that cache can hold.
|
||||
pub const MIN_PAGE_CACHE_SIZE: usize = 10;
|
||||
|
||||
/// The minimum page size in bytes.
|
||||
const MIN_PAGE_SIZE: u32 = 512;
|
||||
|
||||
/// The maximum page size in bytes.
|
||||
const MAX_PAGE_SIZE: u32 = 65536;
|
||||
|
||||
/// The default page size in bytes.
|
||||
const DEFAULT_PAGE_SIZE: u16 = 4096;
|
||||
|
||||
/// The database header.
|
||||
/// The first 100 bytes of the database file comprise the database file header.
|
||||
/// The database file header is divided into fields as shown by the table below.
|
||||
@@ -77,7 +89,7 @@ pub struct DatabaseHeader {
|
||||
|
||||
/// The database page size in bytes. Must be a power of two between 512 and 32768 inclusive,
|
||||
/// or the value 1 representing a page size of 65536.
|
||||
pub page_size: u16,
|
||||
page_size: u16,
|
||||
|
||||
/// File format write version. 1 for legacy; 2 for WAL.
|
||||
write_version: u8,
|
||||
@@ -113,7 +125,7 @@ pub struct DatabaseHeader {
|
||||
pub freelist_pages: u32,
|
||||
|
||||
/// The schema cookie. Incremented when the database schema changes.
|
||||
schema_cookie: u32,
|
||||
pub schema_cookie: u32,
|
||||
|
||||
/// The schema format number. Supported formats are 1, 2, 3, and 4.
|
||||
schema_format: u32,
|
||||
@@ -168,7 +180,7 @@ pub struct WalHeader {
|
||||
/// WAL format version. Currently 3007000
|
||||
pub file_format: u32,
|
||||
|
||||
/// Database page size in bytes. Power of two between 512 and 32768 inclusive
|
||||
/// Database page size in bytes. Power of two between 512 and 65536 inclusive
|
||||
pub page_size: u32,
|
||||
|
||||
/// Checkpoint sequence number. Increases with each checkpoint
|
||||
@@ -217,7 +229,7 @@ impl Default for DatabaseHeader {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
magic: *b"SQLite format 3\0",
|
||||
page_size: 4096,
|
||||
page_size: DEFAULT_PAGE_SIZE,
|
||||
write_version: 2,
|
||||
read_version: 2,
|
||||
reserved_space: 0,
|
||||
@@ -243,6 +255,28 @@ impl Default for DatabaseHeader {
|
||||
}
|
||||
}
|
||||
|
||||
impl DatabaseHeader {
|
||||
pub fn update_page_size(&mut self, size: u32) {
|
||||
if !(MIN_PAGE_SIZE..=MAX_PAGE_SIZE).contains(&size) || (size & (size - 1) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.page_size = if size == MAX_PAGE_SIZE {
|
||||
1u16
|
||||
} else {
|
||||
size as u16
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_page_size(&self) -> u32 {
|
||||
if self.page_size == 1 {
|
||||
MAX_PAGE_SIZE
|
||||
} else {
|
||||
self.page_size as u32
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn begin_read_database_header(
|
||||
db_file: Arc<dyn DatabaseStorage>,
|
||||
) -> Result<Arc<SpinLock<DatabaseHeader>>> {
|
||||
@@ -413,6 +447,14 @@ impl Clone for PageContent {
|
||||
}
|
||||
|
||||
impl PageContent {
|
||||
pub fn new(offset: usize, buffer: Arc<RefCell<Buffer>>) -> Self {
|
||||
Self {
|
||||
offset,
|
||||
buffer,
|
||||
overflow_cells: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn page_type(&self) -> PageType {
|
||||
self.read_u8(0).try_into().unwrap()
|
||||
}
|
||||
@@ -590,6 +632,54 @@ impl PageContent {
|
||||
usable_size,
|
||||
)
|
||||
}
|
||||
|
||||
/// Read the rowid of a table interior cell.
|
||||
#[inline(always)]
|
||||
pub fn cell_table_interior_read_rowid(&self, idx: usize) -> Result<u64> {
|
||||
debug_assert!(self.page_type() == PageType::TableInterior);
|
||||
let buf = self.as_ptr();
|
||||
const INTERIOR_PAGE_HEADER_SIZE_BYTES: usize = 12;
|
||||
let cell_pointer_array_start = INTERIOR_PAGE_HEADER_SIZE_BYTES;
|
||||
let cell_pointer = cell_pointer_array_start + (idx * 2);
|
||||
let cell_pointer = self.read_u16(cell_pointer) as usize;
|
||||
const LEFT_CHILD_PAGE_SIZE_BYTES: usize = 4;
|
||||
let (rowid, _) = read_varint(&buf[cell_pointer + LEFT_CHILD_PAGE_SIZE_BYTES..])?;
|
||||
Ok(rowid)
|
||||
}
|
||||
|
||||
/// Read the left child page of a table interior cell.
|
||||
#[inline(always)]
|
||||
pub fn cell_table_interior_read_left_child_page(&self, idx: usize) -> Result<u32> {
|
||||
debug_assert!(self.page_type() == PageType::TableInterior);
|
||||
let buf = self.as_ptr();
|
||||
const INTERIOR_PAGE_HEADER_SIZE_BYTES: usize = 12;
|
||||
let cell_pointer_array_start = INTERIOR_PAGE_HEADER_SIZE_BYTES;
|
||||
let cell_pointer = cell_pointer_array_start + (idx * 2);
|
||||
let cell_pointer = self.read_u16(cell_pointer) as usize;
|
||||
Ok(u32::from_be_bytes([
|
||||
buf[cell_pointer],
|
||||
buf[cell_pointer + 1],
|
||||
buf[cell_pointer + 2],
|
||||
buf[cell_pointer + 3],
|
||||
]))
|
||||
}
|
||||
|
||||
/// Read the rowid of a table leaf cell.
|
||||
#[inline(always)]
|
||||
pub fn cell_table_leaf_read_rowid(&self, idx: usize) -> Result<u64> {
|
||||
debug_assert!(self.page_type() == PageType::TableLeaf);
|
||||
let buf = self.as_ptr();
|
||||
const LEAF_PAGE_HEADER_SIZE_BYTES: usize = 8;
|
||||
let cell_pointer_array_start = LEAF_PAGE_HEADER_SIZE_BYTES;
|
||||
let cell_pointer = cell_pointer_array_start + (idx * 2);
|
||||
let cell_pointer = self.read_u16(cell_pointer) as usize;
|
||||
let mut pos = cell_pointer;
|
||||
let (_, nr) = read_varint(&buf[pos..])?;
|
||||
pos += nr;
|
||||
let (rowid, _) = read_varint(&buf[pos..])?;
|
||||
Ok(rowid)
|
||||
}
|
||||
|
||||
/// The cell pointer array of a b-tree page immediately follows the b-tree page header.
|
||||
/// Let K be the number of cells on the btree.
|
||||
/// The cell pointer array consists of K 2-byte integer offsets to the cell contents.
|
||||
@@ -626,9 +716,9 @@ impl PageContent {
|
||||
usable_size,
|
||||
);
|
||||
if overflows {
|
||||
4 + to_read + n_payload + 4
|
||||
4 + to_read + n_payload
|
||||
} else {
|
||||
4 + len_payload as usize + n_payload + 4
|
||||
4 + len_payload as usize + n_payload
|
||||
}
|
||||
}
|
||||
PageType::TableInterior => {
|
||||
@@ -644,9 +734,9 @@ impl PageContent {
|
||||
usable_size,
|
||||
);
|
||||
if overflows {
|
||||
to_read + n_payload + 4
|
||||
to_read + n_payload
|
||||
} else {
|
||||
len_payload as usize + n_payload + 4
|
||||
len_payload as usize + n_payload
|
||||
}
|
||||
}
|
||||
PageType::TableLeaf => {
|
||||
@@ -741,11 +831,7 @@ fn finish_read_page(
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let inner = PageContent {
|
||||
offset: pos,
|
||||
buffer: buffer_ref.clone(),
|
||||
overflow_cells: Vec::new(),
|
||||
};
|
||||
let inner = PageContent::new(pos, buffer_ref.clone());
|
||||
{
|
||||
page.get().contents.replace(inner);
|
||||
page.set_uptodate();
|
||||
@@ -950,116 +1036,24 @@ fn read_payload(unread: &'static [u8], payload_size: usize) -> (&'static [u8], O
|
||||
}
|
||||
}
|
||||
|
||||
pub type SerialType = u64;
|
||||
|
||||
pub const SERIAL_TYPE_NULL: SerialType = 0;
|
||||
pub const SERIAL_TYPE_INT8: SerialType = 1;
|
||||
pub const SERIAL_TYPE_BEINT16: SerialType = 2;
|
||||
pub const SERIAL_TYPE_BEINT24: SerialType = 3;
|
||||
pub const SERIAL_TYPE_BEINT32: SerialType = 4;
|
||||
pub const SERIAL_TYPE_BEINT48: SerialType = 5;
|
||||
pub const SERIAL_TYPE_BEINT64: SerialType = 6;
|
||||
pub const SERIAL_TYPE_BEFLOAT64: SerialType = 7;
|
||||
pub const SERIAL_TYPE_CONSTINT0: SerialType = 8;
|
||||
pub const SERIAL_TYPE_CONSTINT1: SerialType = 9;
|
||||
|
||||
pub trait SerialTypeExt {
|
||||
fn is_null(self) -> bool;
|
||||
fn is_int8(self) -> bool;
|
||||
fn is_beint16(self) -> bool;
|
||||
fn is_beint24(self) -> bool;
|
||||
fn is_beint32(self) -> bool;
|
||||
fn is_beint48(self) -> bool;
|
||||
fn is_beint64(self) -> bool;
|
||||
fn is_befloat64(self) -> bool;
|
||||
fn is_constint0(self) -> bool;
|
||||
fn is_constint1(self) -> bool;
|
||||
fn is_blob(self) -> bool;
|
||||
fn is_string(self) -> bool;
|
||||
fn blob_size(self) -> usize;
|
||||
fn string_size(self) -> usize;
|
||||
fn is_valid(self) -> bool;
|
||||
#[inline(always)]
|
||||
pub fn validate_serial_type(value: u64) -> Result<()> {
|
||||
if !SerialType::u64_is_valid_serial_type(value) {
|
||||
crate::bail_corrupt_error!("Invalid serial type: {}", value);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl SerialTypeExt for u64 {
|
||||
fn is_null(self) -> bool {
|
||||
self == SERIAL_TYPE_NULL
|
||||
}
|
||||
|
||||
fn is_int8(self) -> bool {
|
||||
self == SERIAL_TYPE_INT8
|
||||
}
|
||||
|
||||
fn is_beint16(self) -> bool {
|
||||
self == SERIAL_TYPE_BEINT16
|
||||
}
|
||||
|
||||
fn is_beint24(self) -> bool {
|
||||
self == SERIAL_TYPE_BEINT24
|
||||
}
|
||||
|
||||
fn is_beint32(self) -> bool {
|
||||
self == SERIAL_TYPE_BEINT32
|
||||
}
|
||||
|
||||
fn is_beint48(self) -> bool {
|
||||
self == SERIAL_TYPE_BEINT48
|
||||
}
|
||||
|
||||
fn is_beint64(self) -> bool {
|
||||
self == SERIAL_TYPE_BEINT64
|
||||
}
|
||||
|
||||
fn is_befloat64(self) -> bool {
|
||||
self == SERIAL_TYPE_BEFLOAT64
|
||||
}
|
||||
|
||||
fn is_constint0(self) -> bool {
|
||||
self == SERIAL_TYPE_CONSTINT0
|
||||
}
|
||||
|
||||
fn is_constint1(self) -> bool {
|
||||
self == SERIAL_TYPE_CONSTINT1
|
||||
}
|
||||
|
||||
fn is_blob(self) -> bool {
|
||||
self >= 12 && self % 2 == 0
|
||||
}
|
||||
|
||||
fn is_string(self) -> bool {
|
||||
self >= 13 && self % 2 == 1
|
||||
}
|
||||
|
||||
fn blob_size(self) -> usize {
|
||||
debug_assert!(self.is_blob());
|
||||
((self - 12) / 2) as usize
|
||||
}
|
||||
|
||||
fn string_size(self) -> usize {
|
||||
debug_assert!(self.is_string());
|
||||
((self - 13) / 2) as usize
|
||||
}
|
||||
|
||||
fn is_valid(self) -> bool {
|
||||
self <= 9 || self.is_blob() || self.is_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validate_serial_type(value: u64) -> Result<SerialType> {
|
||||
if value.is_valid() {
|
||||
Ok(value)
|
||||
} else {
|
||||
crate::bail_corrupt_error!("Invalid serial type: {}", value)
|
||||
}
|
||||
}
|
||||
|
||||
struct SmallVec<T> {
|
||||
pub data: [std::mem::MaybeUninit<T>; 64],
|
||||
pub struct SmallVec<T, const N: usize = 64> {
|
||||
/// Stack allocated data
|
||||
pub data: [std::mem::MaybeUninit<T>; N],
|
||||
/// Length of the vector, accounting for both stack and heap allocated data
|
||||
pub len: usize,
|
||||
/// Extra data on heap
|
||||
pub extra_data: Option<Vec<T>>,
|
||||
}
|
||||
|
||||
impl<T: Default + Copy> SmallVec<T> {
|
||||
impl<T: Default + Copy, const N: usize> SmallVec<T, N> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
data: unsafe { std::mem::MaybeUninit::uninit().assume_init() },
|
||||
@@ -1080,6 +1074,50 @@ impl<T: Default + Copy> SmallVec<T> {
|
||||
self.len += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn get_from_heap(&self, index: usize) -> T {
|
||||
assert!(self.extra_data.is_some());
|
||||
assert!(index >= self.data.len());
|
||||
let extra_data_index = index - self.data.len();
|
||||
let extra_data = self.extra_data.as_ref().unwrap();
|
||||
assert!(extra_data_index < extra_data.len());
|
||||
extra_data[extra_data_index]
|
||||
}
|
||||
|
||||
pub fn get(&self, index: usize) -> Option<T> {
|
||||
if index >= self.len {
|
||||
return None;
|
||||
}
|
||||
let data_is_on_stack = index < self.data.len();
|
||||
if data_is_on_stack {
|
||||
// SAFETY: We know this index is initialized we checked for index < self.len earlier above.
|
||||
unsafe { Some(self.data[index].assume_init()) }
|
||||
} else {
|
||||
Some(self.get_from_heap(index))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Default + Copy, const N: usize> SmallVec<T, N> {
|
||||
pub fn iter(&self) -> SmallVecIter<'_, T, N> {
|
||||
SmallVecIter { vec: self, pos: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SmallVecIter<'a, T, const N: usize> {
|
||||
vec: &'a SmallVec<T, N>,
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl<'a, T: Default + Copy, const N: usize> Iterator for SmallVecIter<'a, T, N> {
|
||||
type Item = T;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.vec.get(self.pos).map(|item| {
|
||||
self.pos += 1;
|
||||
item
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_record(payload: &[u8], reuse_immutable: &mut ImmutableRecord) -> Result<()> {
|
||||
@@ -1095,10 +1133,10 @@ pub fn read_record(payload: &[u8], reuse_immutable: &mut ImmutableRecord) -> Res
|
||||
let mut header_size = (header_size as usize) - nr;
|
||||
pos += nr;
|
||||
|
||||
let mut serial_types = SmallVec::new();
|
||||
let mut serial_types = SmallVec::<u64, 64>::new();
|
||||
while header_size > 0 {
|
||||
let (serial_type, nr) = read_varint(&reuse_immutable.get_payload()[pos..])?;
|
||||
let serial_type = validate_serial_type(serial_type)?;
|
||||
validate_serial_type(serial_type)?;
|
||||
serial_types.push(serial_type);
|
||||
pos += nr;
|
||||
assert!(header_size >= nr);
|
||||
@@ -1107,14 +1145,17 @@ pub fn read_record(payload: &[u8], reuse_immutable: &mut ImmutableRecord) -> Res
|
||||
|
||||
for &serial_type in &serial_types.data[..serial_types.len.min(serial_types.data.len())] {
|
||||
let (value, n) = read_value(&reuse_immutable.get_payload()[pos..], unsafe {
|
||||
*serial_type.as_ptr()
|
||||
serial_type.assume_init().try_into()?
|
||||
})?;
|
||||
pos += n;
|
||||
reuse_immutable.add_value(value);
|
||||
}
|
||||
if let Some(extra) = serial_types.extra_data.as_ref() {
|
||||
for serial_type in extra {
|
||||
let (value, n) = read_value(&reuse_immutable.get_payload()[pos..], *serial_type)?;
|
||||
let (value, n) = read_value(
|
||||
&reuse_immutable.get_payload()[pos..],
|
||||
(*serial_type).try_into()?,
|
||||
)?;
|
||||
pos += n;
|
||||
reuse_immutable.add_value(value);
|
||||
}
|
||||
@@ -1127,140 +1168,125 @@ pub fn read_record(payload: &[u8], reuse_immutable: &mut ImmutableRecord) -> Res
|
||||
/// always.
|
||||
#[inline(always)]
|
||||
pub fn read_value(buf: &[u8], serial_type: SerialType) -> Result<(RefValue, usize)> {
|
||||
if serial_type.is_null() {
|
||||
return Ok((RefValue::Null, 0));
|
||||
}
|
||||
|
||||
if serial_type.is_int8() {
|
||||
if buf.is_empty() {
|
||||
crate::bail_corrupt_error!("Invalid UInt8 value");
|
||||
match serial_type.kind() {
|
||||
SerialTypeKind::Null => Ok((RefValue::Null, 0)),
|
||||
SerialTypeKind::I8 => {
|
||||
if buf.is_empty() {
|
||||
crate::bail_corrupt_error!("Invalid UInt8 value");
|
||||
}
|
||||
let val = buf[0] as i8;
|
||||
Ok((RefValue::Integer(val as i64), 1))
|
||||
}
|
||||
let val = buf[0] as i8;
|
||||
return Ok((RefValue::Integer(val as i64), 1));
|
||||
}
|
||||
|
||||
if serial_type.is_beint16() {
|
||||
if buf.len() < 2 {
|
||||
crate::bail_corrupt_error!("Invalid BEInt16 value");
|
||||
SerialTypeKind::I16 => {
|
||||
if buf.len() < 2 {
|
||||
crate::bail_corrupt_error!("Invalid BEInt16 value");
|
||||
}
|
||||
Ok((
|
||||
RefValue::Integer(i16::from_be_bytes([buf[0], buf[1]]) as i64),
|
||||
2,
|
||||
))
|
||||
}
|
||||
return Ok((
|
||||
RefValue::Integer(i16::from_be_bytes([buf[0], buf[1]]) as i64),
|
||||
2,
|
||||
));
|
||||
}
|
||||
|
||||
if serial_type.is_beint24() {
|
||||
if buf.len() < 3 {
|
||||
crate::bail_corrupt_error!("Invalid BEInt24 value");
|
||||
SerialTypeKind::I24 => {
|
||||
if buf.len() < 3 {
|
||||
crate::bail_corrupt_error!("Invalid BEInt24 value");
|
||||
}
|
||||
let sign_extension = if buf[0] <= 127 { 0 } else { 255 };
|
||||
Ok((
|
||||
RefValue::Integer(
|
||||
i32::from_be_bytes([sign_extension, buf[0], buf[1], buf[2]]) as i64
|
||||
),
|
||||
3,
|
||||
))
|
||||
}
|
||||
let sign_extension = if buf[0] <= 127 { 0 } else { 255 };
|
||||
return Ok((
|
||||
RefValue::Integer(i32::from_be_bytes([sign_extension, buf[0], buf[1], buf[2]]) as i64),
|
||||
3,
|
||||
));
|
||||
}
|
||||
|
||||
if serial_type.is_beint32() {
|
||||
if buf.len() < 4 {
|
||||
crate::bail_corrupt_error!("Invalid BEInt32 value");
|
||||
SerialTypeKind::I32 => {
|
||||
if buf.len() < 4 {
|
||||
crate::bail_corrupt_error!("Invalid BEInt32 value");
|
||||
}
|
||||
Ok((
|
||||
RefValue::Integer(i32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]) as i64),
|
||||
4,
|
||||
))
|
||||
}
|
||||
return Ok((
|
||||
RefValue::Integer(i32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]) as i64),
|
||||
4,
|
||||
));
|
||||
}
|
||||
|
||||
if serial_type.is_beint48() {
|
||||
if buf.len() < 6 {
|
||||
crate::bail_corrupt_error!("Invalid BEInt48 value");
|
||||
SerialTypeKind::I48 => {
|
||||
if buf.len() < 6 {
|
||||
crate::bail_corrupt_error!("Invalid BEInt48 value");
|
||||
}
|
||||
let sign_extension = if buf[0] <= 127 { 0 } else { 255 };
|
||||
Ok((
|
||||
RefValue::Integer(i64::from_be_bytes([
|
||||
sign_extension,
|
||||
sign_extension,
|
||||
buf[0],
|
||||
buf[1],
|
||||
buf[2],
|
||||
buf[3],
|
||||
buf[4],
|
||||
buf[5],
|
||||
])),
|
||||
6,
|
||||
))
|
||||
}
|
||||
let sign_extension = if buf[0] <= 127 { 0 } else { 255 };
|
||||
return Ok((
|
||||
RefValue::Integer(i64::from_be_bytes([
|
||||
sign_extension,
|
||||
sign_extension,
|
||||
buf[0],
|
||||
buf[1],
|
||||
buf[2],
|
||||
buf[3],
|
||||
buf[4],
|
||||
buf[5],
|
||||
])),
|
||||
6,
|
||||
));
|
||||
}
|
||||
|
||||
if serial_type.is_beint64() {
|
||||
if buf.len() < 8 {
|
||||
crate::bail_corrupt_error!("Invalid BEInt64 value");
|
||||
SerialTypeKind::I64 => {
|
||||
if buf.len() < 8 {
|
||||
crate::bail_corrupt_error!("Invalid BEInt64 value");
|
||||
}
|
||||
Ok((
|
||||
RefValue::Integer(i64::from_be_bytes([
|
||||
buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7],
|
||||
])),
|
||||
8,
|
||||
))
|
||||
}
|
||||
return Ok((
|
||||
RefValue::Integer(i64::from_be_bytes([
|
||||
buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7],
|
||||
])),
|
||||
8,
|
||||
));
|
||||
}
|
||||
|
||||
if serial_type.is_befloat64() {
|
||||
if buf.len() < 8 {
|
||||
crate::bail_corrupt_error!("Invalid BEFloat64 value");
|
||||
SerialTypeKind::F64 => {
|
||||
if buf.len() < 8 {
|
||||
crate::bail_corrupt_error!("Invalid BEFloat64 value");
|
||||
}
|
||||
Ok((
|
||||
RefValue::Float(f64::from_be_bytes([
|
||||
buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7],
|
||||
])),
|
||||
8,
|
||||
))
|
||||
}
|
||||
return Ok((
|
||||
RefValue::Float(f64::from_be_bytes([
|
||||
buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7],
|
||||
])),
|
||||
8,
|
||||
));
|
||||
}
|
||||
|
||||
if serial_type.is_constint0() {
|
||||
return Ok((RefValue::Integer(0), 0));
|
||||
}
|
||||
|
||||
if serial_type.is_constint1() {
|
||||
return Ok((RefValue::Integer(1), 0));
|
||||
}
|
||||
|
||||
if serial_type.is_blob() {
|
||||
let n = serial_type.blob_size();
|
||||
if buf.len() < n {
|
||||
crate::bail_corrupt_error!("Invalid Blob value");
|
||||
SerialTypeKind::ConstInt0 => Ok((RefValue::Integer(0), 0)),
|
||||
SerialTypeKind::ConstInt1 => Ok((RefValue::Integer(1), 0)),
|
||||
SerialTypeKind::Blob => {
|
||||
let content_size = serial_type.size();
|
||||
if buf.len() < content_size {
|
||||
crate::bail_corrupt_error!("Invalid Blob value");
|
||||
}
|
||||
if content_size == 0 {
|
||||
Ok((RefValue::Blob(RawSlice::new(std::ptr::null(), 0)), 0))
|
||||
} else {
|
||||
let ptr = &buf[0] as *const u8;
|
||||
let slice = RawSlice::new(ptr, content_size);
|
||||
Ok((RefValue::Blob(slice), content_size))
|
||||
}
|
||||
}
|
||||
if n == 0 {
|
||||
return Ok((RefValue::Blob(RawSlice::new(std::ptr::null(), 0)), 0));
|
||||
SerialTypeKind::Text => {
|
||||
let content_size = serial_type.size();
|
||||
if buf.len() < content_size {
|
||||
crate::bail_corrupt_error!(
|
||||
"Invalid String value, length {} < expected length {}",
|
||||
buf.len(),
|
||||
content_size
|
||||
);
|
||||
}
|
||||
let slice = if content_size == 0 {
|
||||
RawSlice::new(std::ptr::null(), 0)
|
||||
} else {
|
||||
let ptr = &buf[0] as *const u8;
|
||||
RawSlice::new(ptr, content_size)
|
||||
};
|
||||
Ok((
|
||||
RefValue::Text(TextRef {
|
||||
value: slice,
|
||||
subtype: TextSubtype::Text,
|
||||
}),
|
||||
content_size,
|
||||
))
|
||||
}
|
||||
let ptr = &buf[0] as *const u8;
|
||||
let slice = RawSlice::new(ptr, n);
|
||||
return Ok((RefValue::Blob(slice), n));
|
||||
}
|
||||
|
||||
if serial_type.is_string() {
|
||||
let n = serial_type.string_size();
|
||||
if buf.len() < n {
|
||||
crate::bail_corrupt_error!(
|
||||
"Invalid String value, length {} < expected length {}",
|
||||
buf.len(),
|
||||
n
|
||||
);
|
||||
}
|
||||
let slice = if n == 0 {
|
||||
RawSlice::new(std::ptr::null(), 0)
|
||||
} else {
|
||||
let ptr = &buf[0] as *const u8;
|
||||
RawSlice::new(ptr, n)
|
||||
};
|
||||
return Ok((
|
||||
RefValue::Text(TextRef {
|
||||
value: slice,
|
||||
subtype: TextSubtype::Text,
|
||||
}),
|
||||
n,
|
||||
));
|
||||
}
|
||||
|
||||
// This should never happen if validate_serial_type is used correctly
|
||||
crate::bail_corrupt_error!("Invalid serial type: {}", serial_type)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
@@ -1393,6 +1419,7 @@ pub fn begin_write_wal_frame(
|
||||
io: &Arc<dyn File>,
|
||||
offset: usize,
|
||||
page: &PageRef,
|
||||
page_size: u16,
|
||||
db_size: u32,
|
||||
write_counter: Rc<RefCell<usize>>,
|
||||
wal_header: &WalHeader,
|
||||
@@ -1429,15 +1456,16 @@ pub fn begin_write_wal_frame(
|
||||
let content_len = contents_buf.len();
|
||||
buf[WAL_FRAME_HEADER_SIZE..WAL_FRAME_HEADER_SIZE + content_len]
|
||||
.copy_from_slice(contents_buf);
|
||||
if content_len < 4096 {
|
||||
buf[WAL_FRAME_HEADER_SIZE + content_len..WAL_FRAME_HEADER_SIZE + 4096].fill(0);
|
||||
if content_len < page_size as usize {
|
||||
buf[WAL_FRAME_HEADER_SIZE + content_len..WAL_FRAME_HEADER_SIZE + page_size as usize]
|
||||
.fill(0);
|
||||
}
|
||||
|
||||
let expects_be = wal_header.magic & 1;
|
||||
let use_native_endian = cfg!(target_endian = "big") as u32 == expects_be;
|
||||
let header_checksum = checksum_wal(&buf[0..8], wal_header, checksums, use_native_endian); // Only 8 bytes
|
||||
let final_checksum = checksum_wal(
|
||||
&buf[WAL_FRAME_HEADER_SIZE..WAL_FRAME_HEADER_SIZE + 4096],
|
||||
&buf[WAL_FRAME_HEADER_SIZE..WAL_FRAME_HEADER_SIZE + page_size as usize],
|
||||
wal_header,
|
||||
header_checksum,
|
||||
use_native_endian,
|
||||
@@ -1594,32 +1622,32 @@ mod tests {
|
||||
use rstest::rstest;
|
||||
|
||||
#[rstest]
|
||||
#[case(&[], SERIAL_TYPE_NULL, OwnedValue::Null)]
|
||||
#[case(&[255], SERIAL_TYPE_INT8, OwnedValue::Integer(-1))]
|
||||
#[case(&[0x12, 0x34], SERIAL_TYPE_BEINT16, OwnedValue::Integer(0x1234))]
|
||||
#[case(&[0xFE], SERIAL_TYPE_INT8, OwnedValue::Integer(-2))]
|
||||
#[case(&[0x12, 0x34, 0x56], SERIAL_TYPE_BEINT24, OwnedValue::Integer(0x123456))]
|
||||
#[case(&[0x12, 0x34, 0x56, 0x78], SERIAL_TYPE_BEINT32, OwnedValue::Integer(0x12345678))]
|
||||
#[case(&[0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC], SERIAL_TYPE_BEINT48, OwnedValue::Integer(0x123456789ABC))]
|
||||
#[case(&[0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xFF], SERIAL_TYPE_BEINT64, OwnedValue::Integer(0x123456789ABCDEFF))]
|
||||
#[case(&[0x40, 0x09, 0x21, 0xFB, 0x54, 0x44, 0x2D, 0x18], SERIAL_TYPE_BEFLOAT64, OwnedValue::Float(std::f64::consts::PI))]
|
||||
#[case(&[1, 2], SERIAL_TYPE_CONSTINT0, OwnedValue::Integer(0))]
|
||||
#[case(&[65, 66], SERIAL_TYPE_CONSTINT1, OwnedValue::Integer(1))]
|
||||
#[case(&[1, 2, 3], 18, OwnedValue::Blob(vec![1, 2, 3].into()))]
|
||||
#[case(&[], 12, OwnedValue::Blob(vec![].into()))] // empty blob
|
||||
#[case(&[65, 66, 67], 19, OwnedValue::build_text("ABC"))]
|
||||
#[case(&[0x80], SERIAL_TYPE_INT8, OwnedValue::Integer(-128))]
|
||||
#[case(&[0x80, 0], SERIAL_TYPE_BEINT16, OwnedValue::Integer(-32768))]
|
||||
#[case(&[0x80, 0, 0], SERIAL_TYPE_BEINT24, OwnedValue::Integer(-8388608))]
|
||||
#[case(&[0x80, 0, 0, 0], SERIAL_TYPE_BEINT32, OwnedValue::Integer(-2147483648))]
|
||||
#[case(&[0x80, 0, 0, 0, 0, 0], SERIAL_TYPE_BEINT48, OwnedValue::Integer(-140737488355328))]
|
||||
#[case(&[0x80, 0, 0, 0, 0, 0, 0, 0], SERIAL_TYPE_BEINT64, OwnedValue::Integer(-9223372036854775808))]
|
||||
#[case(&[0x7f], SERIAL_TYPE_INT8, OwnedValue::Integer(127))]
|
||||
#[case(&[0x7f, 0xff], SERIAL_TYPE_BEINT16, OwnedValue::Integer(32767))]
|
||||
#[case(&[0x7f, 0xff, 0xff], SERIAL_TYPE_BEINT24, OwnedValue::Integer(8388607))]
|
||||
#[case(&[0x7f, 0xff, 0xff, 0xff], SERIAL_TYPE_BEINT32, OwnedValue::Integer(2147483647))]
|
||||
#[case(&[0x7f, 0xff, 0xff, 0xff, 0xff, 0xff], SERIAL_TYPE_BEINT48, OwnedValue::Integer(140737488355327))]
|
||||
#[case(&[0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], SERIAL_TYPE_BEINT64, OwnedValue::Integer(9223372036854775807))]
|
||||
#[case(&[], SerialType::null(), OwnedValue::Null)]
|
||||
#[case(&[255], SerialType::i8(), OwnedValue::Integer(-1))]
|
||||
#[case(&[0x12, 0x34], SerialType::i16(), OwnedValue::Integer(0x1234))]
|
||||
#[case(&[0xFE], SerialType::i8(), OwnedValue::Integer(-2))]
|
||||
#[case(&[0x12, 0x34, 0x56], SerialType::i24(), OwnedValue::Integer(0x123456))]
|
||||
#[case(&[0x12, 0x34, 0x56, 0x78], SerialType::i32(), OwnedValue::Integer(0x12345678))]
|
||||
#[case(&[0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC], SerialType::i48(), OwnedValue::Integer(0x123456789ABC))]
|
||||
#[case(&[0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xFF], SerialType::i64(), OwnedValue::Integer(0x123456789ABCDEFF))]
|
||||
#[case(&[0x40, 0x09, 0x21, 0xFB, 0x54, 0x44, 0x2D, 0x18], SerialType::f64(), OwnedValue::Float(std::f64::consts::PI))]
|
||||
#[case(&[1, 2], SerialType::const_int0(), OwnedValue::Integer(0))]
|
||||
#[case(&[65, 66], SerialType::const_int1(), OwnedValue::Integer(1))]
|
||||
#[case(&[1, 2, 3], SerialType::blob(3), OwnedValue::Blob(vec![1, 2, 3].into()))]
|
||||
#[case(&[], SerialType::blob(0), OwnedValue::Blob(vec![].into()))] // empty blob
|
||||
#[case(&[65, 66, 67], SerialType::text(3), OwnedValue::build_text("ABC"))]
|
||||
#[case(&[0x80], SerialType::i8(), OwnedValue::Integer(-128))]
|
||||
#[case(&[0x80, 0], SerialType::i16(), OwnedValue::Integer(-32768))]
|
||||
#[case(&[0x80, 0, 0], SerialType::i24(), OwnedValue::Integer(-8388608))]
|
||||
#[case(&[0x80, 0, 0, 0], SerialType::i32(), OwnedValue::Integer(-2147483648))]
|
||||
#[case(&[0x80, 0, 0, 0, 0, 0], SerialType::i48(), OwnedValue::Integer(-140737488355328))]
|
||||
#[case(&[0x80, 0, 0, 0, 0, 0, 0, 0], SerialType::i64(), OwnedValue::Integer(-9223372036854775808))]
|
||||
#[case(&[0x7f], SerialType::i8(), OwnedValue::Integer(127))]
|
||||
#[case(&[0x7f, 0xff], SerialType::i16(), OwnedValue::Integer(32767))]
|
||||
#[case(&[0x7f, 0xff, 0xff], SerialType::i24(), OwnedValue::Integer(8388607))]
|
||||
#[case(&[0x7f, 0xff, 0xff, 0xff], SerialType::i32(), OwnedValue::Integer(2147483647))]
|
||||
#[case(&[0x7f, 0xff, 0xff, 0xff, 0xff, 0xff], SerialType::i48(), OwnedValue::Integer(140737488355327))]
|
||||
#[case(&[0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], SerialType::i64(), OwnedValue::Integer(9223372036854775807))]
|
||||
fn test_read_value(
|
||||
#[case] buf: &[u8],
|
||||
#[case] serial_type: SerialType,
|
||||
@@ -1631,54 +1659,94 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_serial_type_helpers() {
|
||||
assert!(SERIAL_TYPE_NULL.is_null());
|
||||
assert!(SERIAL_TYPE_INT8.is_int8());
|
||||
assert!(SERIAL_TYPE_BEINT16.is_beint16());
|
||||
assert!(SERIAL_TYPE_BEINT24.is_beint24());
|
||||
assert!(SERIAL_TYPE_BEINT32.is_beint32());
|
||||
assert!(SERIAL_TYPE_BEINT48.is_beint48());
|
||||
assert!(SERIAL_TYPE_BEINT64.is_beint64());
|
||||
assert!(SERIAL_TYPE_BEFLOAT64.is_befloat64());
|
||||
assert!(SERIAL_TYPE_CONSTINT0.is_constint0());
|
||||
assert!(SERIAL_TYPE_CONSTINT1.is_constint1());
|
||||
|
||||
assert!(12u64.is_blob());
|
||||
assert!(14u64.is_blob());
|
||||
assert!(13u64.is_string());
|
||||
assert!(15u64.is_string());
|
||||
|
||||
assert_eq!(12u64.blob_size(), 0);
|
||||
assert_eq!(14u64.blob_size(), 1);
|
||||
assert_eq!(16u64.blob_size(), 2);
|
||||
|
||||
assert_eq!(13u64.string_size(), 0);
|
||||
assert_eq!(15u64.string_size(), 1);
|
||||
assert_eq!(17u64.string_size(), 2);
|
||||
assert_eq!(
|
||||
TryInto::<SerialType>::try_into(12u64).unwrap(),
|
||||
SerialType::blob(0)
|
||||
);
|
||||
assert_eq!(
|
||||
TryInto::<SerialType>::try_into(14u64).unwrap(),
|
||||
SerialType::blob(1)
|
||||
);
|
||||
assert_eq!(
|
||||
TryInto::<SerialType>::try_into(13u64).unwrap(),
|
||||
SerialType::text(0)
|
||||
);
|
||||
assert_eq!(
|
||||
TryInto::<SerialType>::try_into(15u64).unwrap(),
|
||||
SerialType::text(1)
|
||||
);
|
||||
assert_eq!(
|
||||
TryInto::<SerialType>::try_into(16u64).unwrap(),
|
||||
SerialType::blob(2)
|
||||
);
|
||||
assert_eq!(
|
||||
TryInto::<SerialType>::try_into(17u64).unwrap(),
|
||||
SerialType::text(2)
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(0, SERIAL_TYPE_NULL)]
|
||||
#[case(1, SERIAL_TYPE_INT8)]
|
||||
#[case(2, SERIAL_TYPE_BEINT16)]
|
||||
#[case(3, SERIAL_TYPE_BEINT24)]
|
||||
#[case(4, SERIAL_TYPE_BEINT32)]
|
||||
#[case(5, SERIAL_TYPE_BEINT48)]
|
||||
#[case(6, SERIAL_TYPE_BEINT64)]
|
||||
#[case(7, SERIAL_TYPE_BEFLOAT64)]
|
||||
#[case(8, SERIAL_TYPE_CONSTINT0)]
|
||||
#[case(9, SERIAL_TYPE_CONSTINT1)]
|
||||
#[case(12, 12)] // Blob(0)
|
||||
#[case(13, 13)] // String(0)
|
||||
#[case(14, 14)] // Blob(1)
|
||||
#[case(15, 15)] // String(1)
|
||||
fn test_validate_serial_type(#[case] input: u64, #[case] expected: SerialType) {
|
||||
let result = validate_serial_type(input).unwrap();
|
||||
#[case(0, SerialType::null())]
|
||||
#[case(1, SerialType::i8())]
|
||||
#[case(2, SerialType::i16())]
|
||||
#[case(3, SerialType::i24())]
|
||||
#[case(4, SerialType::i32())]
|
||||
#[case(5, SerialType::i48())]
|
||||
#[case(6, SerialType::i64())]
|
||||
#[case(7, SerialType::f64())]
|
||||
#[case(8, SerialType::const_int0())]
|
||||
#[case(9, SerialType::const_int1())]
|
||||
#[case(12, SerialType::blob(0))]
|
||||
#[case(13, SerialType::text(0))]
|
||||
#[case(14, SerialType::blob(1))]
|
||||
#[case(15, SerialType::text(1))]
|
||||
fn test_parse_serial_type(#[case] input: u64, #[case] expected: SerialType) {
|
||||
let result = SerialType::try_from(input).unwrap();
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_serial_type() {
|
||||
let result = validate_serial_type(10);
|
||||
assert!(result.is_err());
|
||||
fn test_validate_serial_type() {
|
||||
for i in 0..=9 {
|
||||
let result = validate_serial_type(i);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
for i in 10..=11 {
|
||||
let result = validate_serial_type(i);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
for i in 12..=1000 {
|
||||
let result = validate_serial_type(i);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_smallvec_iter() {
|
||||
let mut small_vec = SmallVec::<i32, 4>::new();
|
||||
(0..8).for_each(|i| small_vec.push(i));
|
||||
|
||||
let mut iter = small_vec.iter();
|
||||
assert_eq!(iter.next(), Some(0));
|
||||
assert_eq!(iter.next(), Some(1));
|
||||
assert_eq!(iter.next(), Some(2));
|
||||
assert_eq!(iter.next(), Some(3));
|
||||
assert_eq!(iter.next(), Some(4));
|
||||
assert_eq!(iter.next(), Some(5));
|
||||
assert_eq!(iter.next(), Some(6));
|
||||
assert_eq!(iter.next(), Some(7));
|
||||
assert_eq!(iter.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_smallvec_get() {
|
||||
let mut small_vec = SmallVec::<i32, 4>::new();
|
||||
(0..8).for_each(|i| small_vec.push(i));
|
||||
|
||||
(0..8).for_each(|i| {
|
||||
assert_eq!(small_vec.get(i), Some(i as i32));
|
||||
});
|
||||
|
||||
assert_eq!(small_vec.get(8), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,7 +246,7 @@ pub struct WalFile {
|
||||
|
||||
sync_state: RefCell<SyncState>,
|
||||
syncing: Rc<RefCell<bool>>,
|
||||
page_size: usize,
|
||||
page_size: u32,
|
||||
|
||||
shared: Arc<UnsafeCell<WalFileShared>>,
|
||||
ongoing_checkpoint: OngoingCheckpoint,
|
||||
@@ -462,6 +462,7 @@ impl Wal for WalFile {
|
||||
&shared.file,
|
||||
offset,
|
||||
&page,
|
||||
self.page_size as u16,
|
||||
db_size,
|
||||
write_counter,
|
||||
&header,
|
||||
@@ -687,7 +688,7 @@ impl Wal for WalFile {
|
||||
impl WalFile {
|
||||
pub fn new(
|
||||
io: Arc<dyn IO>,
|
||||
page_size: usize,
|
||||
page_size: u32,
|
||||
shared: Arc<UnsafeCell<WalFileShared>>,
|
||||
buffer_pool: Rc<BufferPool>,
|
||||
) -> Self {
|
||||
@@ -698,11 +699,10 @@ impl WalFile {
|
||||
let drop_fn = Rc::new(move |buf| {
|
||||
buffer_pool.put(buf);
|
||||
});
|
||||
checkpoint_page.get().contents = Some(PageContent {
|
||||
offset: 0,
|
||||
buffer: Arc::new(RefCell::new(Buffer::new(buffer, drop_fn))),
|
||||
overflow_cells: Vec::new(),
|
||||
});
|
||||
checkpoint_page.get().contents = Some(PageContent::new(
|
||||
0,
|
||||
Arc::new(RefCell::new(Buffer::new(buffer, drop_fn))),
|
||||
));
|
||||
}
|
||||
Self {
|
||||
io,
|
||||
@@ -728,7 +728,7 @@ impl WalFile {
|
||||
fn frame_offset(&self, frame_id: u64) -> usize {
|
||||
assert!(frame_id > 0, "Frame ID must be 1-based");
|
||||
let page_size = self.page_size;
|
||||
let page_offset = (frame_id - 1) * (page_size + WAL_FRAME_HEADER_SIZE) as u64;
|
||||
let page_offset = (frame_id - 1) * (page_size + WAL_FRAME_HEADER_SIZE as u32) as u64;
|
||||
let offset = WAL_HEADER_SIZE as u64 + page_offset;
|
||||
offset as usize
|
||||
}
|
||||
@@ -743,7 +743,7 @@ impl WalFileShared {
|
||||
pub fn open_shared(
|
||||
io: &Arc<dyn IO>,
|
||||
path: &str,
|
||||
page_size: u16,
|
||||
page_size: u32,
|
||||
) -> Result<Arc<UnsafeCell<WalFileShared>>> {
|
||||
let file = io.open_file(path, crate::io::OpenFlags::Create, false)?;
|
||||
let header = if file.size()? > 0 {
|
||||
@@ -764,7 +764,7 @@ impl WalFileShared {
|
||||
let mut wal_header = WalHeader {
|
||||
magic,
|
||||
file_format: 3007000,
|
||||
page_size: page_size as u32,
|
||||
page_size,
|
||||
checkpoint_seq: 0, // TODO implement sequence number
|
||||
salt_1: io.generate_random_number() as u32,
|
||||
salt_2: io.generate_random_number() as u32,
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::vdbe::builder::{ProgramBuilder, ProgramBuilderOpts, QueryMode};
|
||||
use crate::{schema::Schema, Result, SymbolTable};
|
||||
use limbo_sqlite3_parser::ast::{Expr, Limit, QualifiedName};
|
||||
|
||||
use super::plan::TableReference;
|
||||
use super::plan::{ColumnUsedMask, IterationDirection, TableReference};
|
||||
|
||||
pub fn translate_delete(
|
||||
query_mode: QueryMode,
|
||||
@@ -50,11 +50,20 @@ pub fn prepare_delete_plan(
|
||||
crate::bail_corrupt_error!("Table is neither a virtual table nor a btree table");
|
||||
};
|
||||
let name = tbl_name.name.0.as_str().to_string();
|
||||
let table_references = vec![TableReference {
|
||||
let indexes = schema
|
||||
.get_indices(table.get_name())
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect();
|
||||
let mut table_references = vec![TableReference {
|
||||
table,
|
||||
identifier: name,
|
||||
op: Operation::Scan { iter_dir: None },
|
||||
op: Operation::Scan {
|
||||
iter_dir: IterationDirection::Forwards,
|
||||
index: None,
|
||||
},
|
||||
join_info: None,
|
||||
col_used_mask: ColumnUsedMask::new(),
|
||||
}];
|
||||
|
||||
let mut where_predicates = vec![];
|
||||
@@ -62,13 +71,13 @@ pub fn prepare_delete_plan(
|
||||
// Parse the WHERE clause
|
||||
parse_where(
|
||||
where_clause.map(|e| *e),
|
||||
&table_references,
|
||||
&mut table_references,
|
||||
None,
|
||||
&mut where_predicates,
|
||||
)?;
|
||||
|
||||
// Parse the LIMIT/OFFSET clause
|
||||
let (resolved_limit, resolved_offset) = limit.map_or(Ok((None, None)), |l| parse_limit(*l))?;
|
||||
let (resolved_limit, resolved_offset) = limit.map_or(Ok((None, None)), |l| parse_limit(&l))?;
|
||||
|
||||
let plan = DeletePlan {
|
||||
table_references,
|
||||
@@ -78,6 +87,7 @@ pub fn prepare_delete_plan(
|
||||
limit: resolved_limit,
|
||||
offset: resolved_offset,
|
||||
contains_constant_false_condition: false,
|
||||
indexes,
|
||||
};
|
||||
|
||||
Ok(Plan::Delete(plan))
|
||||
@@ -86,7 +96,5 @@ pub fn prepare_delete_plan(
|
||||
fn estimate_num_instructions(plan: &DeletePlan) -> usize {
|
||||
let base = 20;
|
||||
|
||||
let num_instructions = base + plan.table_references.len() * 10;
|
||||
|
||||
num_instructions
|
||||
base + plan.table_references.len() * 10
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
// This module contains code for emitting bytecode instructions for SQL query execution.
|
||||
// It handles translating high-level SQL operations into low-level bytecode that can be executed by the virtual machine.
|
||||
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use limbo_sqlite3_parser::ast::{self};
|
||||
|
||||
use crate::function::Func;
|
||||
use crate::schema::Index;
|
||||
use crate::translate::plan::{DeletePlan, Plan, Search};
|
||||
use crate::util::exprs_are_equivalent;
|
||||
use crate::vdbe::builder::ProgramBuilder;
|
||||
use crate::vdbe::builder::{CursorType, ProgramBuilder};
|
||||
use crate::vdbe::insn::{IdxInsertFlags, RegisterOrLiteral};
|
||||
use crate::vdbe::{insn::Insn, BranchOffset};
|
||||
use crate::{Result, SymbolTable};
|
||||
|
||||
@@ -62,6 +67,10 @@ pub struct TranslateCtx<'a> {
|
||||
pub label_main_loop_end: Option<BranchOffset>,
|
||||
// First register of the aggregation results
|
||||
pub reg_agg_start: Option<usize>,
|
||||
// In non-group-by statements with aggregations (e.g. SELECT foo, bar, sum(baz) FROM t),
|
||||
// we want to emit the non-aggregate columns (foo and bar) only once.
|
||||
// This register is a flag that tracks whether we have already done that.
|
||||
pub reg_nonagg_emit_once_flag: Option<usize>,
|
||||
// First register of the result columns of the query
|
||||
pub reg_result_cols_start: Option<usize>,
|
||||
// The register holding the limit value, if any.
|
||||
@@ -84,11 +93,12 @@ pub struct TranslateCtx<'a> {
|
||||
// This vector holds the indexes of the result columns that we need to skip.
|
||||
pub result_columns_to_skip_in_orderby_sorter: Option<Vec<usize>>,
|
||||
pub resolver: Resolver<'a>,
|
||||
pub omit_predicates: Vec<usize>,
|
||||
}
|
||||
|
||||
/// Used to distinguish database operations
|
||||
#[allow(clippy::upper_case_acronyms, dead_code)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum OperationMode {
|
||||
SELECT,
|
||||
INSERT,
|
||||
@@ -115,6 +125,7 @@ fn prologue<'a>(
|
||||
labels_main_loop: (0..table_count).map(|_| LoopLabels::new(program)).collect(),
|
||||
label_main_loop_end: None,
|
||||
reg_agg_start: None,
|
||||
reg_nonagg_emit_once_flag: None,
|
||||
reg_limit: None,
|
||||
reg_offset: None,
|
||||
reg_limit_offset_sum: None,
|
||||
@@ -125,11 +136,13 @@ fn prologue<'a>(
|
||||
result_column_indexes_in_orderby_sorter: (0..result_column_count).collect(),
|
||||
result_columns_to_skip_in_orderby_sorter: None,
|
||||
resolver: Resolver::new(syms),
|
||||
omit_predicates: Vec::new(),
|
||||
};
|
||||
|
||||
Ok((t_ctx, init_label, start_offset))
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum TransactionMode {
|
||||
None,
|
||||
Read,
|
||||
@@ -149,8 +162,7 @@ fn epilogue(
|
||||
err_code: 0,
|
||||
description: String::new(),
|
||||
});
|
||||
|
||||
program.resolve_label(init_label, program.offset());
|
||||
program.preassign_label_to_next_insn(init_label);
|
||||
|
||||
match txn_mode {
|
||||
TransactionMode::Read => program.emit_insn(Insn::Transaction { write: false }),
|
||||
@@ -243,6 +255,18 @@ pub fn emit_query<'a>(
|
||||
});
|
||||
}
|
||||
|
||||
// For non-grouped aggregation queries that also have non-aggregate columns,
|
||||
// we need to ensure non-aggregate columns are only emitted once.
|
||||
// This flag helps track whether we've already emitted these columns.
|
||||
if !plan.aggregates.is_empty()
|
||||
&& plan.group_by.is_none()
|
||||
&& plan.result_columns.iter().any(|c| !c.contains_aggregates)
|
||||
{
|
||||
let flag = program.alloc_register();
|
||||
program.emit_int(0, flag); // Initialize flag to 0 (not yet emitted)
|
||||
t_ctx.reg_nonagg_emit_once_flag = Some(flag);
|
||||
}
|
||||
|
||||
// Allocate registers for result columns
|
||||
t_ctx.reg_result_cols_start = Some(program.alloc_registers(plan.result_columns.len()));
|
||||
|
||||
@@ -251,8 +275,8 @@ pub fn emit_query<'a>(
|
||||
init_order_by(program, t_ctx, order_by)?;
|
||||
}
|
||||
|
||||
if let Some(ref mut group_by) = plan.group_by {
|
||||
init_group_by(program, t_ctx, group_by, &plan.aggregates)?;
|
||||
if let Some(ref group_by) = plan.group_by {
|
||||
init_group_by(program, t_ctx, group_by, &plan)?;
|
||||
}
|
||||
init_loop(
|
||||
program,
|
||||
@@ -275,7 +299,7 @@ pub fn emit_query<'a>(
|
||||
condition_metadata,
|
||||
&t_ctx.resolver,
|
||||
)?;
|
||||
program.resolve_label(jump_target_when_true, program.offset());
|
||||
program.preassign_label_to_next_insn(jump_target_when_true);
|
||||
}
|
||||
|
||||
// Set up main query execution loop
|
||||
@@ -286,8 +310,7 @@ pub fn emit_query<'a>(
|
||||
|
||||
// Clean up and close the main execution loop
|
||||
close_loop(program, t_ctx, &plan.table_references)?;
|
||||
|
||||
program.resolve_label(after_main_loop_label, program.offset());
|
||||
program.preassign_label_to_next_insn(after_main_loop_label);
|
||||
|
||||
let mut order_by_necessary = plan.order_by.is_some() && !plan.contains_constant_false_condition;
|
||||
let order_by = plan.order_by.as_ref();
|
||||
@@ -353,12 +376,17 @@ fn emit_program_for_delete(
|
||||
&plan.table_references,
|
||||
&plan.where_clause,
|
||||
)?;
|
||||
emit_delete_insns(program, &mut t_ctx, &plan.table_references, &plan.limit)?;
|
||||
emit_delete_insns(
|
||||
program,
|
||||
&mut t_ctx,
|
||||
&plan.table_references,
|
||||
&plan.indexes,
|
||||
&plan.limit,
|
||||
)?;
|
||||
|
||||
// Clean up and close the main execution loop
|
||||
close_loop(program, &mut t_ctx, &plan.table_references)?;
|
||||
|
||||
program.resolve_label(after_main_loop_label, program.offset());
|
||||
program.preassign_label_to_next_insn(after_main_loop_label);
|
||||
|
||||
// Finalize program
|
||||
epilogue(program, init_label, start_offset, TransactionMode::Write)?;
|
||||
@@ -371,24 +399,28 @@ fn emit_delete_insns(
|
||||
program: &mut ProgramBuilder,
|
||||
t_ctx: &mut TranslateCtx,
|
||||
table_references: &[TableReference],
|
||||
index_references: &[Arc<Index>],
|
||||
limit: &Option<isize>,
|
||||
) -> Result<()> {
|
||||
let table_reference = table_references.first().unwrap();
|
||||
let cursor_id = match &table_reference.op {
|
||||
Operation::Scan { .. } => program.resolve_cursor_id(&table_reference.identifier),
|
||||
Operation::Search(search) => match search {
|
||||
Search::RowidEq { .. } | Search::RowidSearch { .. } => {
|
||||
Search::RowidEq { .. } | Search::Seek { index: None, .. } => {
|
||||
program.resolve_cursor_id(&table_reference.identifier)
|
||||
}
|
||||
Search::IndexSearch { index, .. } => program.resolve_cursor_id(&index.name),
|
||||
Search::Seek {
|
||||
index: Some(index), ..
|
||||
} => program.resolve_cursor_id(&index.name),
|
||||
},
|
||||
_ => return Ok(()),
|
||||
};
|
||||
let main_table_cursor_id = program.resolve_cursor_id(table_reference.table.get_name());
|
||||
|
||||
// Emit the instructions to delete the row
|
||||
let key_reg = program.alloc_register();
|
||||
program.emit_insn(Insn::RowId {
|
||||
cursor_id,
|
||||
cursor_id: main_table_cursor_id,
|
||||
dest: key_reg,
|
||||
});
|
||||
|
||||
@@ -409,8 +441,43 @@ fn emit_delete_insns(
|
||||
conflict_action,
|
||||
});
|
||||
} else {
|
||||
program.emit_insn(Insn::DeleteAsync { cursor_id });
|
||||
program.emit_insn(Insn::DeleteAwait { cursor_id });
|
||||
for index in index_references {
|
||||
let index_cursor_id = program.alloc_cursor_id(
|
||||
Some(index.name.clone()),
|
||||
crate::vdbe::builder::CursorType::BTreeIndex(index.clone()),
|
||||
);
|
||||
|
||||
program.emit_insn(Insn::OpenWrite {
|
||||
cursor_id: index_cursor_id,
|
||||
root_page: RegisterOrLiteral::Literal(index.root_page),
|
||||
});
|
||||
let num_regs = index.columns.len() + 1;
|
||||
let start_reg = program.alloc_registers(num_regs);
|
||||
// Emit columns that are part of the index
|
||||
index
|
||||
.columns
|
||||
.iter()
|
||||
.enumerate()
|
||||
.for_each(|(reg_offset, column_index)| {
|
||||
program.emit_insn(Insn::Column {
|
||||
cursor_id: main_table_cursor_id,
|
||||
column: column_index.pos_in_table,
|
||||
dest: start_reg + reg_offset,
|
||||
});
|
||||
});
|
||||
program.emit_insn(Insn::RowId {
|
||||
cursor_id: main_table_cursor_id,
|
||||
dest: start_reg + num_regs - 1,
|
||||
});
|
||||
program.emit_insn(Insn::IdxDelete {
|
||||
start_reg,
|
||||
num_regs,
|
||||
cursor_id: index_cursor_id,
|
||||
});
|
||||
}
|
||||
program.emit_insn(Insn::Delete {
|
||||
cursor_id: main_table_cursor_id,
|
||||
});
|
||||
}
|
||||
if let Some(limit) = limit {
|
||||
let limit_reg = program.alloc_register();
|
||||
@@ -442,25 +509,11 @@ fn emit_program_for_update(
|
||||
|
||||
// Exit on LIMIT 0
|
||||
if let Some(0) = plan.limit {
|
||||
epilogue(program, init_label, start_offset, TransactionMode::Read)?;
|
||||
epilogue(program, init_label, start_offset, TransactionMode::None)?;
|
||||
program.result_columns = plan.returning.unwrap_or_default();
|
||||
program.table_references = plan.table_references;
|
||||
return Ok(());
|
||||
}
|
||||
let after_main_loop_label = program.allocate_label();
|
||||
t_ctx.label_main_loop_end = Some(after_main_loop_label);
|
||||
if plan.contains_constant_false_condition {
|
||||
program.emit_insn(Insn::Goto {
|
||||
target_pc: after_main_loop_label,
|
||||
});
|
||||
}
|
||||
let skip_label = program.allocate_label();
|
||||
init_loop(
|
||||
program,
|
||||
&mut t_ctx,
|
||||
&plan.table_references,
|
||||
OperationMode::UPDATE,
|
||||
)?;
|
||||
if t_ctx.reg_limit.is_none() && plan.limit.is_some() {
|
||||
let reg = program.alloc_register();
|
||||
t_ctx.reg_limit = Some(reg);
|
||||
@@ -469,6 +522,50 @@ fn emit_program_for_update(
|
||||
dest: reg,
|
||||
});
|
||||
program.mark_last_insn_constant();
|
||||
if t_ctx.reg_offset.is_none() && plan.offset.is_some_and(|n| n.ne(&0)) {
|
||||
let reg = program.alloc_register();
|
||||
t_ctx.reg_offset = Some(reg);
|
||||
program.emit_insn(Insn::Integer {
|
||||
value: plan.offset.unwrap() as i64,
|
||||
dest: reg,
|
||||
});
|
||||
program.mark_last_insn_constant();
|
||||
let combined_reg = program.alloc_register();
|
||||
t_ctx.reg_limit_offset_sum = Some(combined_reg);
|
||||
program.emit_insn(Insn::OffsetLimit {
|
||||
limit_reg: t_ctx.reg_limit.unwrap(),
|
||||
offset_reg: reg,
|
||||
combined_reg,
|
||||
});
|
||||
}
|
||||
}
|
||||
let after_main_loop_label = program.allocate_label();
|
||||
t_ctx.label_main_loop_end = Some(after_main_loop_label);
|
||||
if plan.contains_constant_false_condition {
|
||||
program.emit_insn(Insn::Goto {
|
||||
target_pc: after_main_loop_label,
|
||||
});
|
||||
}
|
||||
|
||||
init_loop(
|
||||
program,
|
||||
&mut t_ctx,
|
||||
&plan.table_references,
|
||||
OperationMode::UPDATE,
|
||||
)?;
|
||||
// Open indexes for update.
|
||||
let mut index_cursors = Vec::with_capacity(plan.indexes_to_update.len());
|
||||
// TODO: do not reopen if there is table reference using it.
|
||||
for index in &plan.indexes_to_update {
|
||||
let index_cursor = program.alloc_cursor_id(
|
||||
Some(index.table_name.clone()),
|
||||
CursorType::BTreeIndex(index.clone()),
|
||||
);
|
||||
program.emit_insn(Insn::OpenWrite {
|
||||
cursor_id: index_cursor,
|
||||
root_page: RegisterOrLiteral::Literal(index.root_page),
|
||||
});
|
||||
index_cursors.push(index_cursor);
|
||||
}
|
||||
open_loop(
|
||||
program,
|
||||
@@ -476,11 +573,9 @@ fn emit_program_for_update(
|
||||
&plan.table_references,
|
||||
&plan.where_clause,
|
||||
)?;
|
||||
emit_update_insns(&plan, &t_ctx, program)?;
|
||||
program.resolve_label(skip_label, program.offset());
|
||||
emit_update_insns(&plan, &t_ctx, program, index_cursors)?;
|
||||
close_loop(program, &mut t_ctx, &plan.table_references)?;
|
||||
|
||||
program.resolve_label(after_main_loop_label, program.offset());
|
||||
program.preassign_label_to_next_insn(after_main_loop_label);
|
||||
|
||||
// Finalize program
|
||||
epilogue(program, init_label, start_offset, TransactionMode::Write)?;
|
||||
@@ -493,17 +588,28 @@ fn emit_update_insns(
|
||||
plan: &UpdatePlan,
|
||||
t_ctx: &TranslateCtx,
|
||||
program: &mut ProgramBuilder,
|
||||
index_cursors: Vec<usize>,
|
||||
) -> crate::Result<()> {
|
||||
let table_ref = &plan.table_references.first().unwrap();
|
||||
let (cursor_id, index) = match &table_ref.op {
|
||||
Operation::Scan { .. } => (program.resolve_cursor_id(&table_ref.identifier), None),
|
||||
let loop_labels = t_ctx.labels_main_loop.first().unwrap();
|
||||
let (cursor_id, index, is_virtual) = match &table_ref.op {
|
||||
Operation::Scan { .. } => (
|
||||
program.resolve_cursor_id(&table_ref.identifier),
|
||||
None,
|
||||
table_ref.virtual_table().is_some(),
|
||||
),
|
||||
Operation::Search(search) => match search {
|
||||
&Search::RowidEq { .. } | Search::RowidSearch { .. } => {
|
||||
(program.resolve_cursor_id(&table_ref.identifier), None)
|
||||
}
|
||||
Search::IndexSearch { index, .. } => (
|
||||
&Search::RowidEq { .. } | Search::Seek { index: None, .. } => (
|
||||
program.resolve_cursor_id(&table_ref.identifier),
|
||||
None,
|
||||
false,
|
||||
),
|
||||
Search::Seek {
|
||||
index: Some(index), ..
|
||||
} => (
|
||||
program.resolve_cursor_id(&table_ref.identifier),
|
||||
Some((index.clone(), program.resolve_cursor_id(&index.name))),
|
||||
false,
|
||||
),
|
||||
},
|
||||
_ => return Ok(()),
|
||||
@@ -523,25 +629,102 @@ fn emit_update_insns(
|
||||
meta,
|
||||
&t_ctx.resolver,
|
||||
)?;
|
||||
program.resolve_label(jump_target, program.offset());
|
||||
program.preassign_label_to_next_insn(jump_target);
|
||||
}
|
||||
let first_col_reg = program.alloc_registers(table_ref.table.columns().len());
|
||||
let rowid_reg = program.alloc_register();
|
||||
let beg = program.alloc_registers(
|
||||
table_ref.table.columns().len()
|
||||
+ if is_virtual {
|
||||
2 // two args before the relevant columns for VUpdate
|
||||
} else {
|
||||
1 // rowid reg
|
||||
},
|
||||
);
|
||||
program.emit_insn(Insn::RowId {
|
||||
cursor_id,
|
||||
dest: rowid_reg,
|
||||
dest: beg,
|
||||
});
|
||||
// if no rowid, we're done
|
||||
program.emit_insn(Insn::IsNull {
|
||||
reg: rowid_reg,
|
||||
reg: beg,
|
||||
target_pc: t_ctx.label_main_loop_end.unwrap(),
|
||||
});
|
||||
if is_virtual {
|
||||
program.emit_insn(Insn::Copy {
|
||||
src_reg: beg,
|
||||
dst_reg: beg + 1,
|
||||
amount: 0,
|
||||
})
|
||||
}
|
||||
|
||||
if let Some(offset) = t_ctx.reg_offset {
|
||||
program.emit_insn(Insn::IfPos {
|
||||
reg: offset,
|
||||
target_pc: loop_labels.next,
|
||||
decrement_by: 1,
|
||||
});
|
||||
}
|
||||
|
||||
for cond in plan.where_clause.iter().filter(|c| c.is_constant()) {
|
||||
let meta = ConditionMetadata {
|
||||
jump_if_condition_is_true: false,
|
||||
jump_target_when_true: BranchOffset::Placeholder,
|
||||
jump_target_when_false: loop_labels.next,
|
||||
};
|
||||
translate_condition_expr(
|
||||
program,
|
||||
&plan.table_references,
|
||||
&cond.expr,
|
||||
meta,
|
||||
&t_ctx.resolver,
|
||||
)?;
|
||||
}
|
||||
|
||||
// Update indexes first. Columns that are updated will be translated from an expression and those who aren't modified will be
|
||||
// read from table. Mutiple value index key could be updated partially.
|
||||
for (index, index_cursor) in plan.indexes_to_update.iter().zip(index_cursors) {
|
||||
let index_record_reg_count = index.columns.len() + 1;
|
||||
let index_record_reg_start = program.alloc_registers(index_record_reg_count);
|
||||
for (idx, column) in index.columns.iter().enumerate() {
|
||||
if let Some((_, expr)) = plan.set_clauses.iter().find(|(i, _)| *i == idx) {
|
||||
translate_expr(
|
||||
program,
|
||||
Some(&plan.table_references),
|
||||
expr,
|
||||
index_record_reg_start + idx,
|
||||
&t_ctx.resolver,
|
||||
)?;
|
||||
} else {
|
||||
program.emit_insn(Insn::Column {
|
||||
cursor_id: cursor_id,
|
||||
column: column.pos_in_table,
|
||||
dest: index_record_reg_start + idx,
|
||||
});
|
||||
}
|
||||
}
|
||||
program.emit_insn(Insn::RowId {
|
||||
cursor_id: cursor_id,
|
||||
dest: index_record_reg_start + index.columns.len(),
|
||||
});
|
||||
let index_record_reg = program.alloc_register();
|
||||
program.emit_insn(Insn::MakeRecord {
|
||||
start_reg: index_record_reg_start,
|
||||
count: index_record_reg_count,
|
||||
dest_reg: index_record_reg,
|
||||
});
|
||||
program.emit_insn(Insn::IdxInsert {
|
||||
cursor_id: index_cursor,
|
||||
record_reg: index_record_reg,
|
||||
unpacked_start: Some(index_record_reg_start),
|
||||
unpacked_count: Some(index_record_reg_count as u16),
|
||||
flags: IdxInsertFlags::new(),
|
||||
});
|
||||
}
|
||||
// we scan a column at a time, loading either the column's values, or the new value
|
||||
// from the Set expression, into registers so we can emit a MakeRecord and update the row.
|
||||
let start = if is_virtual { beg + 2 } else { beg + 1 };
|
||||
for idx in 0..table_ref.columns().len() {
|
||||
if let Some((idx, expr)) = plan.set_clauses.iter().find(|(i, _)| *i == idx) {
|
||||
let target_reg = first_col_reg + idx;
|
||||
let target_reg = start + idx;
|
||||
if let Some((_, expr)) = plan.set_clauses.iter().find(|(i, _)| *i == idx) {
|
||||
translate_expr(
|
||||
program,
|
||||
Some(&plan.table_references),
|
||||
@@ -556,9 +739,17 @@ fn emit_update_insns(
|
||||
.iter()
|
||||
.position(|c| Some(&c.name) == table_column.name.as_ref())
|
||||
});
|
||||
let dest = first_col_reg + idx;
|
||||
if table_column.primary_key {
|
||||
program.emit_null(dest, None);
|
||||
|
||||
// don't emit null for pkey of virtual tables. they require first two args
|
||||
// before the 'record' to be explicitly non-null
|
||||
if table_column.is_rowid_alias && !is_virtual {
|
||||
program.emit_null(target_reg, None);
|
||||
} else if is_virtual {
|
||||
program.emit_insn(Insn::VColumn {
|
||||
cursor_id,
|
||||
column: idx,
|
||||
dest: target_reg,
|
||||
});
|
||||
} else {
|
||||
program.emit_insn(Insn::Column {
|
||||
cursor_id: *index
|
||||
@@ -572,24 +763,42 @@ fn emit_update_insns(
|
||||
})
|
||||
.unwrap_or(&cursor_id),
|
||||
column: column_idx_in_index.unwrap_or(idx),
|
||||
dest,
|
||||
dest: target_reg,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
let record_reg = program.alloc_register();
|
||||
program.emit_insn(Insn::MakeRecord {
|
||||
start_reg: first_col_reg,
|
||||
count: table_ref.columns().len(),
|
||||
dest_reg: record_reg,
|
||||
});
|
||||
program.emit_insn(Insn::InsertAsync {
|
||||
cursor: cursor_id,
|
||||
key_reg: rowid_reg,
|
||||
record_reg,
|
||||
flag: 0,
|
||||
});
|
||||
program.emit_insn(Insn::InsertAwait { cursor_id });
|
||||
if let Some(btree_table) = table_ref.btree() {
|
||||
if btree_table.is_strict {
|
||||
program.emit_insn(Insn::TypeCheck {
|
||||
start_reg: start,
|
||||
count: table_ref.columns().len(),
|
||||
check_generated: true,
|
||||
table_reference: Rc::clone(&btree_table),
|
||||
});
|
||||
}
|
||||
let record_reg = program.alloc_register();
|
||||
program.emit_insn(Insn::MakeRecord {
|
||||
start_reg: start,
|
||||
count: table_ref.columns().len(),
|
||||
dest_reg: record_reg,
|
||||
});
|
||||
program.emit_insn(Insn::Insert {
|
||||
cursor: cursor_id,
|
||||
key_reg: beg,
|
||||
record_reg,
|
||||
flag: 0,
|
||||
});
|
||||
} else if let Some(vtab) = table_ref.virtual_table() {
|
||||
let arg_count = table_ref.columns().len() + 2;
|
||||
program.emit_insn(Insn::VUpdate {
|
||||
cursor_id,
|
||||
arg_count,
|
||||
start_reg: beg,
|
||||
vtab_ptr: vtab.implementation.as_ref().ctx as usize,
|
||||
conflict_action: 0u16,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(limit_reg) = t_ctx.reg_limit {
|
||||
program.emit_insn(Insn::DecrJumpZero {
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
use limbo_sqlite3_parser::ast::{self, UnaryOperator};
|
||||
|
||||
use super::emitter::Resolver;
|
||||
use super::optimizer::Optimizable;
|
||||
use super::plan::{Operation, TableReference};
|
||||
#[cfg(feature = "json")]
|
||||
use crate::function::JsonFunc;
|
||||
use crate::function::{Func, FuncCtx, MathFuncArity, ScalarFunc, VectorFunc};
|
||||
use crate::functions::datetime;
|
||||
use crate::schema::{Table, Type};
|
||||
use crate::util::normalize_ident;
|
||||
use crate::util::{exprs_are_equivalent, normalize_ident};
|
||||
use crate::vdbe::{
|
||||
builder::ProgramBuilder,
|
||||
insn::{CmpInsFlags, Insn},
|
||||
@@ -12,9 +16,6 @@ use crate::vdbe::{
|
||||
};
|
||||
use crate::Result;
|
||||
|
||||
use super::emitter::Resolver;
|
||||
use super::plan::{Operation, TableReference};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ConditionMetadata {
|
||||
pub jump_if_condition_is_true: bool,
|
||||
@@ -186,7 +187,9 @@ pub fn translate_condition_expr(
|
||||
resolver: &Resolver,
|
||||
) -> Result<()> {
|
||||
match expr {
|
||||
ast::Expr::Between { .. } => todo!(),
|
||||
ast::Expr::Between { .. } => {
|
||||
unreachable!("expression should have been rewritten in optmizer")
|
||||
}
|
||||
ast::Expr::Binary(lhs, ast::Operator::And, rhs) => {
|
||||
// In a binary AND, never jump to the parent 'jump_target_when_true' label on the first condition, because
|
||||
// the second condition MUST also be true. Instead we instruct the child expression to jump to a local
|
||||
@@ -203,7 +206,7 @@ pub fn translate_condition_expr(
|
||||
},
|
||||
resolver,
|
||||
)?;
|
||||
program.resolve_label(jump_target_when_true, program.offset());
|
||||
program.preassign_label_to_next_insn(jump_target_when_true);
|
||||
translate_condition_expr(
|
||||
program,
|
||||
referenced_tables,
|
||||
@@ -228,7 +231,7 @@ pub fn translate_condition_expr(
|
||||
},
|
||||
resolver,
|
||||
)?;
|
||||
program.resolve_label(jump_target_when_false, program.offset());
|
||||
program.preassign_label_to_next_insn(jump_target_when_false);
|
||||
translate_condition_expr(
|
||||
program,
|
||||
referenced_tables,
|
||||
@@ -252,8 +255,8 @@ pub fn translate_condition_expr(
|
||||
{
|
||||
let lhs_reg = program.alloc_register();
|
||||
let rhs_reg = program.alloc_register();
|
||||
translate_and_mark(program, Some(referenced_tables), lhs, lhs_reg, resolver)?;
|
||||
translate_and_mark(program, Some(referenced_tables), rhs, rhs_reg, resolver)?;
|
||||
translate_expr(program, Some(referenced_tables), lhs, lhs_reg, resolver)?;
|
||||
translate_expr(program, Some(referenced_tables), rhs, rhs_reg, resolver)?;
|
||||
match op {
|
||||
ast::Operator::Greater => {
|
||||
emit_cmp_insn!(program, condition_metadata, Gt, Le, lhs_reg, rhs_reg)
|
||||
@@ -408,7 +411,7 @@ pub fn translate_condition_expr(
|
||||
}
|
||||
|
||||
if !condition_metadata.jump_if_condition_is_true {
|
||||
program.resolve_label(jump_target_when_true, program.offset());
|
||||
program.preassign_label_to_next_insn(jump_target_when_true);
|
||||
}
|
||||
}
|
||||
ast::Expr::Like { not, .. } => {
|
||||
@@ -476,6 +479,38 @@ pub fn translate_condition_expr(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reason why [translate_expr_no_constant_opt()] was called.
|
||||
#[derive(Debug)]
|
||||
pub enum NoConstantOptReason {
|
||||
/// The expression translation involves reusing register(s),
|
||||
/// so hoisting those register assignments is not safe.
|
||||
/// e.g. SELECT COALESCE(1, t.x, NULL) would overwrite 1 with NULL, which is invalid.
|
||||
RegisterReuse,
|
||||
}
|
||||
|
||||
/// Translate an expression into bytecode via [translate_expr()], and forbid any constant values from being hoisted
|
||||
/// into the beginning of the program. This is a good idea in most cases where
|
||||
/// a register will end up being reused e.g. in a coroutine.
|
||||
pub fn translate_expr_no_constant_opt(
|
||||
program: &mut ProgramBuilder,
|
||||
referenced_tables: Option<&[TableReference]>,
|
||||
expr: &ast::Expr,
|
||||
target_register: usize,
|
||||
resolver: &Resolver,
|
||||
deopt_reason: NoConstantOptReason,
|
||||
) -> Result<usize> {
|
||||
tracing::debug!(
|
||||
"translate_expr_no_constant_opt: expr={:?}, deopt_reason={:?}",
|
||||
expr,
|
||||
deopt_reason
|
||||
);
|
||||
let next_span_idx = program.constant_spans_next_idx();
|
||||
let translated = translate_expr(program, referenced_tables, expr, target_register, resolver)?;
|
||||
program.constant_spans_invalidate_after(next_span_idx);
|
||||
Ok(translated)
|
||||
}
|
||||
|
||||
/// Translate an expression into bytecode.
|
||||
pub fn translate_expr(
|
||||
program: &mut ProgramBuilder,
|
||||
referenced_tables: Option<&[TableReference]>,
|
||||
@@ -483,34 +518,51 @@ pub fn translate_expr(
|
||||
target_register: usize,
|
||||
resolver: &Resolver,
|
||||
) -> Result<usize> {
|
||||
let constant_span = if expr.is_constant(resolver) {
|
||||
if !program.constant_span_is_open() {
|
||||
Some(program.constant_span_start())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
program.constant_span_end_all();
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(reg) = resolver.resolve_cached_expr_reg(expr) {
|
||||
program.emit_insn(Insn::Copy {
|
||||
src_reg: reg,
|
||||
dst_reg: target_register,
|
||||
amount: 0,
|
||||
});
|
||||
if let Some(span) = constant_span {
|
||||
program.constant_span_end(span);
|
||||
}
|
||||
return Ok(target_register);
|
||||
}
|
||||
|
||||
match expr {
|
||||
ast::Expr::Between { .. } => todo!(),
|
||||
ast::Expr::Between { .. } => {
|
||||
unreachable!("expression should have been rewritten in optmizer")
|
||||
}
|
||||
ast::Expr::Binary(e1, op, e2) => {
|
||||
// Check if both sides of the expression are identical and reuse the same register if so
|
||||
if e1 == e2 {
|
||||
// Check if both sides of the expression are equivalent and reuse the same register if so
|
||||
if exprs_are_equivalent(e1, e2) {
|
||||
let shared_reg = program.alloc_register();
|
||||
translate_expr(program, referenced_tables, e1, shared_reg, resolver)?;
|
||||
|
||||
emit_binary_insn(program, op, shared_reg, shared_reg, target_register)?;
|
||||
return Ok(target_register);
|
||||
Ok(target_register)
|
||||
} else {
|
||||
let e1_reg = program.alloc_registers(2);
|
||||
let e2_reg = e1_reg + 1;
|
||||
|
||||
translate_expr(program, referenced_tables, e1, e1_reg, resolver)?;
|
||||
translate_expr(program, referenced_tables, e2, e2_reg, resolver)?;
|
||||
|
||||
emit_binary_insn(program, op, e1_reg, e2_reg, target_register)?;
|
||||
Ok(target_register)
|
||||
}
|
||||
|
||||
let e1_reg = program.alloc_registers(2);
|
||||
let e2_reg = e1_reg + 1;
|
||||
|
||||
translate_expr(program, referenced_tables, e1, e1_reg, resolver)?;
|
||||
translate_expr(program, referenced_tables, e2, e2_reg, resolver)?;
|
||||
|
||||
emit_binary_insn(program, op, e1_reg, e2_reg, target_register)?;
|
||||
Ok(target_register)
|
||||
}
|
||||
ast::Expr::Case {
|
||||
base,
|
||||
@@ -541,7 +593,14 @@ pub fn translate_expr(
|
||||
)?;
|
||||
};
|
||||
for (when_expr, then_expr) in when_then_pairs {
|
||||
translate_expr(program, referenced_tables, when_expr, expr_reg, resolver)?;
|
||||
translate_expr_no_constant_opt(
|
||||
program,
|
||||
referenced_tables,
|
||||
when_expr,
|
||||
expr_reg,
|
||||
resolver,
|
||||
NoConstantOptReason::RegisterReuse,
|
||||
)?;
|
||||
match base_reg {
|
||||
// CASE 1 WHEN 0 THEN 0 ELSE 1 becomes 1==0, Ne branch to next clause
|
||||
Some(base_reg) => program.emit_insn(Insn::Ne {
|
||||
@@ -559,12 +618,13 @@ pub fn translate_expr(
|
||||
}),
|
||||
};
|
||||
// THEN...
|
||||
translate_expr(
|
||||
translate_expr_no_constant_opt(
|
||||
program,
|
||||
referenced_tables,
|
||||
then_expr,
|
||||
target_register,
|
||||
resolver,
|
||||
NoConstantOptReason::RegisterReuse,
|
||||
)?;
|
||||
program.emit_insn(Insn::Goto {
|
||||
target_pc: return_label,
|
||||
@@ -576,7 +636,14 @@ pub fn translate_expr(
|
||||
}
|
||||
match else_expr {
|
||||
Some(expr) => {
|
||||
translate_expr(program, referenced_tables, expr, target_register, resolver)?;
|
||||
translate_expr_no_constant_opt(
|
||||
program,
|
||||
referenced_tables,
|
||||
expr,
|
||||
target_register,
|
||||
resolver,
|
||||
NoConstantOptReason::RegisterReuse,
|
||||
)?;
|
||||
}
|
||||
// If ELSE isn't specified, it means ELSE null.
|
||||
None => {
|
||||
@@ -586,7 +653,7 @@ pub fn translate_expr(
|
||||
});
|
||||
}
|
||||
};
|
||||
program.resolve_label(return_label, program.offset());
|
||||
program.preassign_label_to_next_insn(return_label);
|
||||
Ok(target_register)
|
||||
}
|
||||
ast::Expr::Cast { expr, type_name } => {
|
||||
@@ -772,7 +839,7 @@ pub fn translate_expr(
|
||||
if let Some(args) = args {
|
||||
for (i, arg) in args.iter().enumerate() {
|
||||
// register containing result of each argument expression
|
||||
translate_and_mark(
|
||||
translate_expr(
|
||||
program,
|
||||
referenced_tables,
|
||||
arg,
|
||||
@@ -900,12 +967,13 @@ pub fn translate_expr(
|
||||
// whenever a not null check succeeds, we jump to the end of the series
|
||||
let label_coalesce_end = program.allocate_label();
|
||||
for (index, arg) in args.iter().enumerate() {
|
||||
let reg = translate_expr(
|
||||
let reg = translate_expr_no_constant_opt(
|
||||
program,
|
||||
referenced_tables,
|
||||
arg,
|
||||
target_register,
|
||||
resolver,
|
||||
NoConstantOptReason::RegisterReuse,
|
||||
)?;
|
||||
if index < args.len() - 1 {
|
||||
program.emit_insn(Insn::NotNull {
|
||||
@@ -987,12 +1055,13 @@ pub fn translate_expr(
|
||||
};
|
||||
|
||||
let temp_reg = program.alloc_register();
|
||||
translate_expr(
|
||||
translate_expr_no_constant_opt(
|
||||
program,
|
||||
referenced_tables,
|
||||
&args[0],
|
||||
temp_reg,
|
||||
resolver,
|
||||
NoConstantOptReason::RegisterReuse,
|
||||
)?;
|
||||
let before_copy_label = program.allocate_label();
|
||||
program.emit_insn(Insn::NotNull {
|
||||
@@ -1000,12 +1069,13 @@ pub fn translate_expr(
|
||||
target_pc: before_copy_label,
|
||||
});
|
||||
|
||||
translate_expr(
|
||||
translate_expr_no_constant_opt(
|
||||
program,
|
||||
referenced_tables,
|
||||
&args[1],
|
||||
temp_reg,
|
||||
resolver,
|
||||
NoConstantOptReason::RegisterReuse,
|
||||
)?;
|
||||
program.resolve_label(before_copy_label, program.offset());
|
||||
program.emit_insn(Insn::Copy {
|
||||
@@ -1025,12 +1095,13 @@ pub fn translate_expr(
|
||||
),
|
||||
};
|
||||
let temp_reg = program.alloc_register();
|
||||
translate_expr(
|
||||
translate_expr_no_constant_opt(
|
||||
program,
|
||||
referenced_tables,
|
||||
&args[0],
|
||||
temp_reg,
|
||||
resolver,
|
||||
NoConstantOptReason::RegisterReuse,
|
||||
)?;
|
||||
let jump_target_when_false = program.allocate_label();
|
||||
program.emit_insn(Insn::IfNot {
|
||||
@@ -1038,26 +1109,28 @@ pub fn translate_expr(
|
||||
target_pc: jump_target_when_false,
|
||||
jump_if_null: true,
|
||||
});
|
||||
translate_expr(
|
||||
translate_expr_no_constant_opt(
|
||||
program,
|
||||
referenced_tables,
|
||||
&args[1],
|
||||
target_register,
|
||||
resolver,
|
||||
NoConstantOptReason::RegisterReuse,
|
||||
)?;
|
||||
let jump_target_result = program.allocate_label();
|
||||
program.emit_insn(Insn::Goto {
|
||||
target_pc: jump_target_result,
|
||||
});
|
||||
program.resolve_label(jump_target_when_false, program.offset());
|
||||
translate_expr(
|
||||
program.preassign_label_to_next_insn(jump_target_when_false);
|
||||
translate_expr_no_constant_opt(
|
||||
program,
|
||||
referenced_tables,
|
||||
&args[2],
|
||||
target_register,
|
||||
resolver,
|
||||
NoConstantOptReason::RegisterReuse,
|
||||
)?;
|
||||
program.resolve_label(jump_target_result, program.offset());
|
||||
program.preassign_label_to_next_insn(jump_target_result);
|
||||
Ok(target_register)
|
||||
}
|
||||
ScalarFunc::Glob | ScalarFunc::Like => {
|
||||
@@ -1109,7 +1182,7 @@ pub fn translate_expr(
|
||||
| ScalarFunc::ZeroBlob => {
|
||||
let args = expect_arguments_exact!(args, 1, srf);
|
||||
let start_reg = program.alloc_register();
|
||||
translate_and_mark(
|
||||
translate_expr(
|
||||
program,
|
||||
referenced_tables,
|
||||
&args[0],
|
||||
@@ -1128,7 +1201,7 @@ pub fn translate_expr(
|
||||
ScalarFunc::LoadExtension => {
|
||||
let args = expect_arguments_exact!(args, 1, srf);
|
||||
let start_reg = program.alloc_register();
|
||||
translate_and_mark(
|
||||
translate_expr(
|
||||
program,
|
||||
referenced_tables,
|
||||
&args[0],
|
||||
@@ -1159,13 +1232,13 @@ pub fn translate_expr(
|
||||
});
|
||||
Ok(target_register)
|
||||
}
|
||||
ScalarFunc::Date | ScalarFunc::DateTime => {
|
||||
ScalarFunc::Date | ScalarFunc::DateTime | ScalarFunc::JulianDay => {
|
||||
let start_reg = program
|
||||
.alloc_registers(args.as_ref().map(|x| x.len()).unwrap_or(1));
|
||||
if let Some(args) = args {
|
||||
for (i, arg) in args.iter().enumerate() {
|
||||
// register containing result of each argument expression
|
||||
translate_and_mark(
|
||||
translate_expr(
|
||||
program,
|
||||
referenced_tables,
|
||||
arg,
|
||||
@@ -1244,7 +1317,7 @@ pub fn translate_expr(
|
||||
crate::bail_parse_error!("hex function with no arguments",);
|
||||
};
|
||||
let start_reg = program.alloc_register();
|
||||
translate_and_mark(
|
||||
translate_expr(
|
||||
program,
|
||||
referenced_tables,
|
||||
&args[0],
|
||||
@@ -1259,11 +1332,11 @@ pub fn translate_expr(
|
||||
});
|
||||
Ok(target_register)
|
||||
}
|
||||
ScalarFunc::UnixEpoch | ScalarFunc::JulianDay => {
|
||||
ScalarFunc::UnixEpoch => {
|
||||
let mut start_reg = 0;
|
||||
match args {
|
||||
Some(args) if args.len() > 1 => {
|
||||
crate::bail_parse_error!("epoch or julianday function with > 1 arguments. Modifiers are not yet supported.");
|
||||
crate::bail_parse_error!("epoch function with > 1 arguments. Modifiers are not yet supported.");
|
||||
}
|
||||
Some(args) if args.len() == 1 => {
|
||||
let arg_reg = program.alloc_register();
|
||||
@@ -1292,7 +1365,7 @@ pub fn translate_expr(
|
||||
if let Some(args) = args {
|
||||
for (i, arg) in args.iter().enumerate() {
|
||||
// register containing result of each argument expression
|
||||
translate_and_mark(
|
||||
translate_expr(
|
||||
program,
|
||||
referenced_tables,
|
||||
arg,
|
||||
@@ -1309,6 +1382,33 @@ pub fn translate_expr(
|
||||
});
|
||||
Ok(target_register)
|
||||
}
|
||||
ScalarFunc::TimeDiff => {
|
||||
let args = expect_arguments_exact!(args, 2, srf);
|
||||
|
||||
let start_reg = program.alloc_registers(2);
|
||||
translate_expr(
|
||||
program,
|
||||
referenced_tables,
|
||||
&args[0],
|
||||
start_reg,
|
||||
resolver,
|
||||
)?;
|
||||
translate_expr(
|
||||
program,
|
||||
referenced_tables,
|
||||
&args[1],
|
||||
start_reg + 1,
|
||||
resolver,
|
||||
)?;
|
||||
|
||||
program.emit_insn(Insn::Function {
|
||||
constant_mask: 0,
|
||||
start_reg,
|
||||
dest: target_register,
|
||||
func: func_ctx,
|
||||
});
|
||||
Ok(target_register)
|
||||
}
|
||||
ScalarFunc::TotalChanges => {
|
||||
if args.is_some() {
|
||||
crate::bail_parse_error!(
|
||||
@@ -1334,7 +1434,7 @@ pub fn translate_expr(
|
||||
|
||||
let start_reg = program.alloc_registers(args.len());
|
||||
for (i, arg) in args.iter().enumerate() {
|
||||
translate_and_mark(
|
||||
translate_expr(
|
||||
program,
|
||||
referenced_tables,
|
||||
arg,
|
||||
@@ -1363,7 +1463,7 @@ pub fn translate_expr(
|
||||
};
|
||||
let start_reg = program.alloc_registers(args.len());
|
||||
for (i, arg) in args.iter().enumerate() {
|
||||
translate_and_mark(
|
||||
translate_expr(
|
||||
program,
|
||||
referenced_tables,
|
||||
arg,
|
||||
@@ -1393,7 +1493,7 @@ pub fn translate_expr(
|
||||
};
|
||||
let start_reg = program.alloc_registers(args.len());
|
||||
for (i, arg) in args.iter().enumerate() {
|
||||
translate_and_mark(
|
||||
translate_expr(
|
||||
program,
|
||||
referenced_tables,
|
||||
arg,
|
||||
@@ -1546,7 +1646,7 @@ pub fn translate_expr(
|
||||
if let Some(args) = args {
|
||||
for (i, arg) in args.iter().enumerate() {
|
||||
// register containing result of each argument expression
|
||||
translate_and_mark(
|
||||
translate_expr(
|
||||
program,
|
||||
referenced_tables,
|
||||
arg,
|
||||
@@ -1571,6 +1671,85 @@ pub fn translate_expr(
|
||||
target_register,
|
||||
func_ctx,
|
||||
),
|
||||
ScalarFunc::Likely => {
|
||||
let args = if let Some(args) = args {
|
||||
if args.len() != 1 {
|
||||
crate::bail_parse_error!(
|
||||
"likely function must have exactly 1 argument",
|
||||
);
|
||||
}
|
||||
args
|
||||
} else {
|
||||
crate::bail_parse_error!("likely function with no arguments",);
|
||||
};
|
||||
let start_reg = program.alloc_register();
|
||||
translate_expr(
|
||||
program,
|
||||
referenced_tables,
|
||||
&args[0],
|
||||
start_reg,
|
||||
resolver,
|
||||
)?;
|
||||
program.emit_insn(Insn::Function {
|
||||
constant_mask: 0,
|
||||
start_reg,
|
||||
dest: target_register,
|
||||
func: func_ctx,
|
||||
});
|
||||
Ok(target_register)
|
||||
}
|
||||
ScalarFunc::Likelihood => {
|
||||
let args = if let Some(args) = args {
|
||||
if args.len() != 2 {
|
||||
crate::bail_parse_error!(
|
||||
"likelihood() function must have exactly 2 arguments",
|
||||
);
|
||||
}
|
||||
args
|
||||
} else {
|
||||
crate::bail_parse_error!("likelihood() function with no arguments",);
|
||||
};
|
||||
|
||||
if let ast::Expr::Literal(ast::Literal::Numeric(ref value)) = args[1] {
|
||||
if let Ok(probability) = value.parse::<f64>() {
|
||||
if !(0.0..=1.0).contains(&probability) {
|
||||
crate::bail_parse_error!(
|
||||
"second argument of likelihood() must be between 0.0 and 1.0",
|
||||
);
|
||||
}
|
||||
if !value.contains('.') {
|
||||
crate::bail_parse_error!(
|
||||
"second argument of likelihood() must be a floating point number with decimal point",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
crate::bail_parse_error!(
|
||||
"second argument of likelihood() must be a floating point constant",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
crate::bail_parse_error!(
|
||||
"second argument of likelihood() must be a numeric literal",
|
||||
);
|
||||
}
|
||||
|
||||
let start_reg = program.alloc_register();
|
||||
translate_expr(
|
||||
program,
|
||||
referenced_tables,
|
||||
&args[0],
|
||||
start_reg,
|
||||
resolver,
|
||||
)?;
|
||||
|
||||
program.emit_insn(Insn::Copy {
|
||||
src_reg: start_reg,
|
||||
dst_reg: target_register,
|
||||
amount: 0,
|
||||
});
|
||||
|
||||
Ok(target_register)
|
||||
}
|
||||
}
|
||||
}
|
||||
Func::Math(math_func) => match math_func.arity() {
|
||||
@@ -1591,13 +1770,7 @@ pub fn translate_expr(
|
||||
MathFuncArity::Unary => {
|
||||
let args = expect_arguments_exact!(args, 1, math_func);
|
||||
let start_reg = program.alloc_register();
|
||||
translate_and_mark(
|
||||
program,
|
||||
referenced_tables,
|
||||
&args[0],
|
||||
start_reg,
|
||||
resolver,
|
||||
)?;
|
||||
translate_expr(program, referenced_tables, &args[0], start_reg, resolver)?;
|
||||
program.emit_insn(Insn::Function {
|
||||
constant_mask: 0,
|
||||
start_reg,
|
||||
@@ -1664,41 +1837,79 @@ pub fn translate_expr(
|
||||
is_rowid_alias,
|
||||
} => {
|
||||
let table_reference = referenced_tables.as_ref().unwrap().get(*table).unwrap();
|
||||
let index = table_reference.op.index();
|
||||
let use_covering_index = table_reference.utilizes_covering_index();
|
||||
match table_reference.op {
|
||||
// If we are reading a column from a table, we find the cursor that corresponds to
|
||||
// the table and read the column from the cursor.
|
||||
Operation::Scan { .. } | Operation::Search(_) => match &table_reference.table {
|
||||
Table::BTree(_) => {
|
||||
let cursor_id = program.resolve_cursor_id(&table_reference.identifier);
|
||||
if *is_rowid_alias {
|
||||
program.emit_insn(Insn::RowId {
|
||||
cursor_id,
|
||||
dest: target_register,
|
||||
});
|
||||
} else {
|
||||
program.emit_insn(Insn::Column {
|
||||
// If we have a covering index, we don't have an open table cursor so we read from the index cursor.
|
||||
Operation::Scan { .. } | Operation::Search(_) => {
|
||||
match &table_reference.table {
|
||||
Table::BTree(_) => {
|
||||
let table_cursor_id = if use_covering_index {
|
||||
None
|
||||
} else {
|
||||
Some(program.resolve_cursor_id(&table_reference.identifier))
|
||||
};
|
||||
let index_cursor_id = if let Some(index) = index {
|
||||
Some(program.resolve_cursor_id(&index.name))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if *is_rowid_alias {
|
||||
if let Some(index_cursor_id) = index_cursor_id {
|
||||
program.emit_insn(Insn::IdxRowId {
|
||||
cursor_id: index_cursor_id,
|
||||
dest: target_register,
|
||||
});
|
||||
} else if let Some(table_cursor_id) = table_cursor_id {
|
||||
program.emit_insn(Insn::RowId {
|
||||
cursor_id: table_cursor_id,
|
||||
dest: target_register,
|
||||
});
|
||||
} else {
|
||||
unreachable!("Either index or table cursor must be opened");
|
||||
}
|
||||
} else {
|
||||
let read_cursor = if use_covering_index {
|
||||
index_cursor_id
|
||||
.expect("index cursor should be opened when use_covering_index=true")
|
||||
} else {
|
||||
table_cursor_id
|
||||
.expect("table cursor should be opened when use_covering_index=false")
|
||||
};
|
||||
let column = if use_covering_index {
|
||||
let index = index.expect("index cursor should be opened when use_covering_index=true");
|
||||
index.column_table_pos_to_index_pos(*column).unwrap_or_else(|| {
|
||||
panic!("covering index {} does not contain column number {} of table {}", index.name, column, table_reference.identifier)
|
||||
})
|
||||
} else {
|
||||
*column
|
||||
};
|
||||
program.emit_insn(Insn::Column {
|
||||
cursor_id: read_cursor,
|
||||
column,
|
||||
dest: target_register,
|
||||
});
|
||||
}
|
||||
let Some(column) = table_reference.table.get_column_at(*column) else {
|
||||
crate::bail_parse_error!("column index out of bounds");
|
||||
};
|
||||
maybe_apply_affinity(column.ty, target_register, program);
|
||||
Ok(target_register)
|
||||
}
|
||||
Table::Virtual(_) => {
|
||||
let cursor_id = program.resolve_cursor_id(&table_reference.identifier);
|
||||
program.emit_insn(Insn::VColumn {
|
||||
cursor_id,
|
||||
column: *column,
|
||||
dest: target_register,
|
||||
});
|
||||
Ok(target_register)
|
||||
}
|
||||
let Some(column) = table_reference.table.get_column_at(*column) else {
|
||||
crate::bail_parse_error!("column index out of bounds");
|
||||
};
|
||||
maybe_apply_affinity(column.ty, target_register, program);
|
||||
Ok(target_register)
|
||||
_ => unreachable!(),
|
||||
}
|
||||
Table::Virtual(_) => {
|
||||
let cursor_id = program.resolve_cursor_id(&table_reference.identifier);
|
||||
program.emit_insn(Insn::VColumn {
|
||||
cursor_id,
|
||||
column: *column,
|
||||
dest: target_register,
|
||||
});
|
||||
Ok(target_register)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
},
|
||||
}
|
||||
// If we are reading a column from a subquery, we instead copy the column from the
|
||||
// subquery's result registers.
|
||||
Operation::Subquery {
|
||||
@@ -1716,11 +1927,23 @@ pub fn translate_expr(
|
||||
}
|
||||
ast::Expr::RowId { database: _, table } => {
|
||||
let table_reference = referenced_tables.as_ref().unwrap().get(*table).unwrap();
|
||||
let cursor_id = program.resolve_cursor_id(&table_reference.identifier);
|
||||
program.emit_insn(Insn::RowId {
|
||||
cursor_id,
|
||||
dest: target_register,
|
||||
});
|
||||
let index = table_reference.op.index();
|
||||
let use_covering_index = table_reference.utilizes_covering_index();
|
||||
if use_covering_index {
|
||||
let index =
|
||||
index.expect("index cursor should be opened when use_covering_index=true");
|
||||
let cursor_id = program.resolve_cursor_id(&index.name);
|
||||
program.emit_insn(Insn::IdxRowId {
|
||||
cursor_id,
|
||||
dest: target_register,
|
||||
});
|
||||
} else {
|
||||
let cursor_id = program.resolve_cursor_id(&table_reference.identifier);
|
||||
program.emit_insn(Insn::RowId {
|
||||
cursor_id,
|
||||
dest: target_register,
|
||||
});
|
||||
}
|
||||
Ok(target_register)
|
||||
}
|
||||
ast::Expr::InList { .. } => todo!(),
|
||||
@@ -1744,8 +1967,14 @@ pub fn translate_expr(
|
||||
}
|
||||
ast::Expr::Literal(lit) => match lit {
|
||||
ast::Literal::Numeric(val) => {
|
||||
let maybe_int = val.parse::<i64>();
|
||||
if let Ok(int_value) = maybe_int {
|
||||
if val.starts_with("0x") || val.starts_with("0X") {
|
||||
// must be a hex decimal
|
||||
let int_value = i64::from_str_radix(&val[2..], 16)?;
|
||||
program.emit_insn(Insn::Integer {
|
||||
value: int_value,
|
||||
dest: target_register,
|
||||
});
|
||||
} else if let Ok(int_value) = val.parse::<i64>() {
|
||||
program.emit_insn(Insn::Integer {
|
||||
value: int_value,
|
||||
dest: target_register,
|
||||
@@ -1791,9 +2020,27 @@ pub fn translate_expr(
|
||||
});
|
||||
Ok(target_register)
|
||||
}
|
||||
ast::Literal::CurrentDate => todo!(),
|
||||
ast::Literal::CurrentTime => todo!(),
|
||||
ast::Literal::CurrentTimestamp => todo!(),
|
||||
ast::Literal::CurrentDate => {
|
||||
program.emit_insn(Insn::String8 {
|
||||
value: datetime::exec_date(&[]).to_string(),
|
||||
dest: target_register,
|
||||
});
|
||||
Ok(target_register)
|
||||
}
|
||||
ast::Literal::CurrentTime => {
|
||||
program.emit_insn(Insn::String8 {
|
||||
value: datetime::exec_time(&[]).to_string(),
|
||||
dest: target_register,
|
||||
});
|
||||
Ok(target_register)
|
||||
}
|
||||
ast::Literal::CurrentTimestamp => {
|
||||
program.emit_insn(Insn::String8 {
|
||||
value: datetime::exec_datetime_full(&[]).to_string(),
|
||||
dest: target_register,
|
||||
});
|
||||
Ok(target_register)
|
||||
}
|
||||
},
|
||||
ast::Expr::Name(_) => todo!(),
|
||||
ast::Expr::NotNull(_) => todo!(),
|
||||
@@ -1829,14 +2076,22 @@ pub fn translate_expr(
|
||||
// Special case: if we're negating "9223372036854775808", this is exactly MIN_INT64
|
||||
// If we don't do this -1 * 9223372036854775808 will overflow and parse will fail
|
||||
// and trigger conversion to Real.
|
||||
if numeric_value == "9223372036854775808" {
|
||||
if numeric_value == "9223372036854775808"
|
||||
|| numeric_value.eq_ignore_ascii_case("0x7fffffffffffffff")
|
||||
{
|
||||
program.emit_insn(Insn::Integer {
|
||||
value: i64::MIN,
|
||||
dest: target_register,
|
||||
});
|
||||
} else {
|
||||
let maybe_int = numeric_value.parse::<i64>();
|
||||
if let Ok(value) = maybe_int {
|
||||
if numeric_value.starts_with("0x") || numeric_value.starts_with("0X") {
|
||||
// must be a hex decimal
|
||||
let int_value = i64::from_str_radix(&numeric_value[2..], 16)?;
|
||||
program.emit_insn(Insn::Integer {
|
||||
value: -int_value,
|
||||
dest: target_register,
|
||||
});
|
||||
} else if let Ok(value) = numeric_value.parse::<i64>() {
|
||||
program.emit_insn(Insn::Integer {
|
||||
value: value * -1,
|
||||
dest: target_register,
|
||||
@@ -1852,7 +2107,7 @@ pub fn translate_expr(
|
||||
Ok(target_register)
|
||||
}
|
||||
(UnaryOperator::Negative, _) => {
|
||||
let value = -1;
|
||||
let value = 0;
|
||||
|
||||
let reg = program.alloc_register();
|
||||
translate_expr(program, referenced_tables, expr, reg, resolver)?;
|
||||
@@ -1862,7 +2117,7 @@ pub fn translate_expr(
|
||||
dest: zero_reg,
|
||||
});
|
||||
program.mark_last_insn_constant();
|
||||
program.emit_insn(Insn::Multiply {
|
||||
program.emit_insn(Insn::Subtract {
|
||||
lhs: zero_reg,
|
||||
rhs: reg,
|
||||
dest: target_register,
|
||||
@@ -1870,8 +2125,13 @@ pub fn translate_expr(
|
||||
Ok(target_register)
|
||||
}
|
||||
(UnaryOperator::BitwiseNot, ast::Expr::Literal(ast::Literal::Numeric(num_val))) => {
|
||||
let maybe_int = num_val.parse::<i64>();
|
||||
if let Ok(val) = maybe_int {
|
||||
if num_val.starts_with("0x") || num_val.starts_with("0X") {
|
||||
let int_value = i64::from_str_radix(&num_val[2..], 16)?;
|
||||
program.emit_insn(Insn::Integer {
|
||||
value: !int_value,
|
||||
dest: target_register,
|
||||
});
|
||||
} else if let Ok(val) = num_val.parse::<i64>() {
|
||||
program.emit_insn(Insn::Integer {
|
||||
value: !val,
|
||||
dest: target_register,
|
||||
@@ -1919,7 +2179,13 @@ pub fn translate_expr(
|
||||
});
|
||||
Ok(target_register)
|
||||
}
|
||||
}?;
|
||||
|
||||
if let Some(span) = constant_span {
|
||||
program.constant_span_end(span);
|
||||
}
|
||||
|
||||
Ok(target_register)
|
||||
}
|
||||
|
||||
fn emit_binary_insn(
|
||||
@@ -2188,17 +2454,11 @@ fn translate_like_base(
|
||||
let arg_count = if matches!(escape, Some(_)) { 3 } else { 2 };
|
||||
let start_reg = program.alloc_registers(arg_count);
|
||||
let mut constant_mask = 0;
|
||||
translate_and_mark(program, referenced_tables, lhs, start_reg + 1, resolver)?;
|
||||
translate_expr(program, referenced_tables, lhs, start_reg + 1, resolver)?;
|
||||
let _ = translate_expr(program, referenced_tables, rhs, start_reg, resolver)?;
|
||||
if arg_count == 3 {
|
||||
if let Some(escape) = escape {
|
||||
translate_and_mark(
|
||||
program,
|
||||
referenced_tables,
|
||||
escape,
|
||||
start_reg + 2,
|
||||
resolver,
|
||||
)?;
|
||||
translate_expr(program, referenced_tables, escape, start_reg + 2, resolver)?;
|
||||
}
|
||||
}
|
||||
if matches!(rhs.as_ref(), ast::Expr::Literal(_)) {
|
||||
@@ -2303,20 +2563,6 @@ pub fn maybe_apply_affinity(col_type: Type, target_register: usize, program: &mu
|
||||
}
|
||||
}
|
||||
|
||||
pub fn translate_and_mark(
|
||||
program: &mut ProgramBuilder,
|
||||
referenced_tables: Option<&[TableReference]>,
|
||||
expr: &ast::Expr,
|
||||
target_register: usize,
|
||||
resolver: &Resolver,
|
||||
) -> Result<()> {
|
||||
translate_expr(program, referenced_tables, expr, target_register, resolver)?;
|
||||
if matches!(expr, ast::Expr::Literal(_)) {
|
||||
program.mark_last_insn_constant();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sanitaizes a string literal by removing single quote at front and back
|
||||
/// and escaping double single quotes
|
||||
pub fn sanitize_string(input: &str) -> String {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use limbo_sqlite3_parser::ast;
|
||||
use limbo_sqlite3_parser::ast::{self, SortOrder};
|
||||
|
||||
use crate::{
|
||||
function::AggFunc,
|
||||
schema::{Column, PseudoTable},
|
||||
types::{OwnedValue, Record},
|
||||
util::exprs_are_equivalent,
|
||||
vdbe::{
|
||||
builder::{CursorType, ProgramBuilder},
|
||||
insn::Insn,
|
||||
@@ -37,12 +37,15 @@ pub struct GroupByMetadata {
|
||||
pub reg_sorter_key: usize,
|
||||
// Register holding a flag to abort the grouping process if necessary
|
||||
pub reg_abort_flag: usize,
|
||||
// Register holding the start of the accumulator group registers (i.e. the groups, not the aggregates)
|
||||
pub reg_group_exprs_acc: usize,
|
||||
// Register holding the start of the non aggregate query members (all columns except aggregate arguments)
|
||||
pub reg_non_aggregate_exprs_acc: usize,
|
||||
// Starting index of the register(s) that hold the comparison result between the current row and the previous row
|
||||
// The comparison result is used to determine if the current row belongs to the same group as the previous row
|
||||
// Each group by expression has a corresponding register
|
||||
pub reg_group_exprs_cmp: usize,
|
||||
// Columns that not part of GROUP BY clause and not arguments of Aggregation function.
|
||||
// Heavy calculation and needed in different functions, so it is reasonable to do it once and save.
|
||||
pub non_group_by_non_agg_column_count: Option<usize>,
|
||||
}
|
||||
|
||||
/// Initialize resources needed for GROUP BY processing
|
||||
@@ -50,29 +53,30 @@ pub fn init_group_by(
|
||||
program: &mut ProgramBuilder,
|
||||
t_ctx: &mut TranslateCtx,
|
||||
group_by: &GroupBy,
|
||||
aggregates: &[Aggregate],
|
||||
plan: &SelectPlan,
|
||||
) -> Result<()> {
|
||||
let num_aggs = aggregates.len();
|
||||
let num_aggs = plan.aggregates.len();
|
||||
|
||||
let non_aggregate_count = plan
|
||||
.result_columns
|
||||
.iter()
|
||||
.filter(|rc| !rc.contains_aggregates)
|
||||
.count();
|
||||
|
||||
let sort_cursor = program.alloc_cursor_id(None, CursorType::Sorter);
|
||||
|
||||
let reg_abort_flag = program.alloc_register();
|
||||
let reg_group_exprs_cmp = program.alloc_registers(group_by.exprs.len());
|
||||
let reg_group_exprs_acc = program.alloc_registers(group_by.exprs.len());
|
||||
let reg_non_aggregate_exprs_acc = program.alloc_registers(non_aggregate_count);
|
||||
let reg_agg_exprs_start = program.alloc_registers(num_aggs);
|
||||
let reg_sorter_key = program.alloc_register();
|
||||
|
||||
let label_subrtn_acc_clear = program.allocate_label();
|
||||
|
||||
let mut order = Vec::new();
|
||||
const ASCENDING: i64 = 0;
|
||||
for _ in group_by.exprs.iter() {
|
||||
order.push(OwnedValue::Integer(ASCENDING));
|
||||
}
|
||||
program.emit_insn(Insn::SorterOpen {
|
||||
cursor_id: sort_cursor,
|
||||
columns: aggregates.len() + group_by.exprs.len(),
|
||||
order: Record::new(order),
|
||||
columns: non_aggregate_count + plan.aggregates.len(),
|
||||
order: (0..group_by.exprs.len()).map(|_| SortOrder::Asc).collect(),
|
||||
});
|
||||
|
||||
program.add_comment(program.offset(), "clear group by abort flag");
|
||||
@@ -110,9 +114,10 @@ pub fn init_group_by(
|
||||
label_acc_indicator_set_flag_true: program.allocate_label(),
|
||||
reg_subrtn_acc_clear_return_offset,
|
||||
reg_abort_flag,
|
||||
reg_group_exprs_acc,
|
||||
reg_non_aggregate_exprs_acc,
|
||||
reg_group_exprs_cmp,
|
||||
reg_sorter_key,
|
||||
non_group_by_non_agg_column_count: None,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
@@ -146,25 +151,57 @@ pub fn emit_group_by<'a>(
|
||||
sort_cursor,
|
||||
reg_group_exprs_cmp,
|
||||
reg_subrtn_acc_clear_return_offset,
|
||||
reg_group_exprs_acc,
|
||||
reg_non_aggregate_exprs_acc,
|
||||
reg_abort_flag,
|
||||
reg_sorter_key,
|
||||
label_subrtn_acc_clear,
|
||||
label_acc_indicator_set_flag_true,
|
||||
non_group_by_non_agg_column_count,
|
||||
..
|
||||
} = *t_ctx.meta_group_by.as_mut().unwrap();
|
||||
|
||||
let group_by = plan.group_by.as_ref().unwrap();
|
||||
|
||||
// all group by columns and all arguments of agg functions are in the sorter.
|
||||
// the sort keys are the group by columns (the aggregation within groups is done based on how long the sort keys remain the same)
|
||||
let sorter_column_count = group_by.exprs.len()
|
||||
+ plan
|
||||
.aggregates
|
||||
let agg_args_count = plan
|
||||
.aggregates
|
||||
.iter()
|
||||
.map(|agg| agg.args.len())
|
||||
.sum::<usize>();
|
||||
let group_by_count = group_by.exprs.len();
|
||||
let non_group_by_non_agg_column_count = non_group_by_non_agg_column_count.unwrap();
|
||||
|
||||
// We have to know which group by expr present in resulting set
|
||||
let group_by_expr_in_res_cols = group_by.exprs.iter().map(|expr| {
|
||||
plan.result_columns
|
||||
.iter()
|
||||
.map(|agg| agg.args.len())
|
||||
.sum::<usize>();
|
||||
// sorter column names do not matter
|
||||
.any(|e| exprs_are_equivalent(&e.expr, expr))
|
||||
});
|
||||
|
||||
// Create a map from sorter column index to result register
|
||||
// This helps track where each column from the sorter should be stored
|
||||
let mut column_register_mapping =
|
||||
vec![None; group_by_count + non_group_by_non_agg_column_count];
|
||||
let mut next_reg = reg_non_aggregate_exprs_acc;
|
||||
|
||||
// Map GROUP BY columns that are in the result set to registers
|
||||
for (i, is_in_result) in group_by_expr_in_res_cols.clone().enumerate() {
|
||||
if is_in_result {
|
||||
column_register_mapping[i] = Some(next_reg);
|
||||
next_reg += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle other non-aggregate columns that aren't part of GROUP BY and not part of Aggregation function
|
||||
for i in group_by_count..group_by_count + non_group_by_non_agg_column_count {
|
||||
column_register_mapping[i] = Some(next_reg);
|
||||
next_reg += 1;
|
||||
}
|
||||
|
||||
// Calculate total number of columns in the sorter
|
||||
// The sorter contains all GROUP BY columns, aggregate arguments, and other columns
|
||||
let sorter_column_count = agg_args_count + group_by_count + non_group_by_non_agg_column_count;
|
||||
|
||||
// Create pseudo-columns for the pseudo-table
|
||||
// (these are placeholders as we only care about structure, not semantics)
|
||||
let ty = crate::schema::Type::Null;
|
||||
let pseudo_columns = (0..sorter_column_count)
|
||||
.map(|_| Column {
|
||||
@@ -178,7 +215,8 @@ pub fn emit_group_by<'a>(
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// A pseudo table is a "fake" table to which we read one row at a time from the sorter
|
||||
// Create a pseudo-table to read one row at a time from the sorter
|
||||
// This allows us to use standard table access operations on the sorted data
|
||||
let pseudo_table = Rc::new(PseudoTable {
|
||||
columns: pseudo_columns,
|
||||
});
|
||||
@@ -231,10 +269,21 @@ pub fn emit_group_by<'a>(
|
||||
"start new group if comparison is not equal",
|
||||
);
|
||||
// If we are at a new group, continue. If we are at the same group, jump to the aggregation step (i.e. accumulate more values into the aggregations)
|
||||
let label_jump_after_comparison = program.allocate_label();
|
||||
program.emit_insn(Insn::Jump {
|
||||
target_pc_lt: program.offset().add(1u32),
|
||||
target_pc_lt: label_jump_after_comparison,
|
||||
target_pc_eq: agg_step_label,
|
||||
target_pc_gt: program.offset().add(1u32),
|
||||
target_pc_gt: label_jump_after_comparison,
|
||||
});
|
||||
|
||||
program.add_comment(
|
||||
program.offset(),
|
||||
"check if ended group had data, and output if so",
|
||||
);
|
||||
program.resolve_label(label_jump_after_comparison, program.offset());
|
||||
program.emit_insn(Insn::Gosub {
|
||||
target_pc: label_subrtn_acc_output,
|
||||
return_reg: reg_subrtn_acc_output_return_offset,
|
||||
});
|
||||
|
||||
// New group, move current group by columns into the comparison register
|
||||
@@ -244,15 +293,6 @@ pub fn emit_group_by<'a>(
|
||||
count: group_by.exprs.len(),
|
||||
});
|
||||
|
||||
program.add_comment(
|
||||
program.offset(),
|
||||
"check if ended group had data, and output if so",
|
||||
);
|
||||
program.emit_insn(Insn::Gosub {
|
||||
target_pc: label_subrtn_acc_output,
|
||||
return_reg: reg_subrtn_acc_output_return_offset,
|
||||
});
|
||||
|
||||
program.add_comment(program.offset(), "check abort flag");
|
||||
program.emit_insn(Insn::IfPos {
|
||||
reg: reg_abort_flag,
|
||||
@@ -266,10 +306,10 @@ pub fn emit_group_by<'a>(
|
||||
return_reg: reg_subrtn_acc_clear_return_offset,
|
||||
});
|
||||
|
||||
// Accumulate the values into the aggregations
|
||||
// Process each aggregate function for the current row
|
||||
program.resolve_label(agg_step_label, program.offset());
|
||||
let start_reg = t_ctx.reg_agg_start.unwrap();
|
||||
let mut cursor_index = group_by.exprs.len();
|
||||
let mut cursor_index = group_by_count + non_group_by_non_agg_column_count; // Skipping all columns in sorter that not an aggregation arguments
|
||||
for (i, agg) in plan.aggregates.iter().enumerate() {
|
||||
let agg_result_reg = start_reg + i;
|
||||
translate_aggregation_step_groupby(
|
||||
@@ -284,7 +324,8 @@ pub fn emit_group_by<'a>(
|
||||
cursor_index += agg.args.len();
|
||||
}
|
||||
|
||||
// We only emit the group by columns if we are going to start a new group (i.e. the prev group will not accumulate any more values into the aggregations)
|
||||
// We only need to store non-aggregate columns once per group
|
||||
// Skip if we've already stored them for this group
|
||||
program.add_comment(
|
||||
program.offset(),
|
||||
"don't emit group columns if continuing existing group",
|
||||
@@ -295,17 +336,18 @@ pub fn emit_group_by<'a>(
|
||||
jump_if_null: false,
|
||||
});
|
||||
|
||||
// Read the group by columns for a finished group
|
||||
for i in 0..group_by.exprs.len() {
|
||||
let key_reg = reg_group_exprs_acc + i;
|
||||
let sorter_column_index = i;
|
||||
program.emit_insn(Insn::Column {
|
||||
cursor_id: pseudo_cursor,
|
||||
column: sorter_column_index,
|
||||
dest: key_reg,
|
||||
});
|
||||
// Read non-aggregate columns from the current row
|
||||
for (sorter_column_index, dest_reg) in column_register_mapping.iter().enumerate() {
|
||||
if let Some(dest_reg) = dest_reg {
|
||||
program.emit_insn(Insn::Column {
|
||||
cursor_id: pseudo_cursor,
|
||||
column: sorter_column_index,
|
||||
dest: *dest_reg,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Mark that we've stored data for this group
|
||||
program.resolve_label(label_acc_indicator_set_flag_true, program.offset());
|
||||
program.add_comment(program.offset(), "indicate data in accumulator");
|
||||
program.emit_insn(Insn::Integer {
|
||||
@@ -313,12 +355,12 @@ pub fn emit_group_by<'a>(
|
||||
dest: reg_data_in_acc_flag,
|
||||
});
|
||||
|
||||
// Continue to the next row in the sorter
|
||||
program.emit_insn(Insn::SorterNext {
|
||||
cursor_id: sort_cursor,
|
||||
pc_if_next: label_grouping_loop_start,
|
||||
});
|
||||
|
||||
program.resolve_label(label_grouping_loop_end, program.offset());
|
||||
program.preassign_label_to_next_insn(label_grouping_loop_end);
|
||||
|
||||
program.add_comment(program.offset(), "emit row for final group");
|
||||
program.emit_insn(Insn::Gosub {
|
||||
@@ -340,18 +382,22 @@ pub fn emit_group_by<'a>(
|
||||
|
||||
program.resolve_label(label_subrtn_acc_output, program.offset());
|
||||
|
||||
// Only output a row if there's data in the accumulator
|
||||
program.add_comment(program.offset(), "output group by row subroutine start");
|
||||
program.emit_insn(Insn::IfPos {
|
||||
reg: reg_data_in_acc_flag,
|
||||
target_pc: label_agg_final,
|
||||
decrement_by: 0,
|
||||
});
|
||||
|
||||
// If no data, return without outputting a row
|
||||
let group_by_end_without_emitting_row_label = program.allocate_label();
|
||||
program.resolve_label(group_by_end_without_emitting_row_label, program.offset());
|
||||
program.emit_insn(Insn::Return {
|
||||
return_reg: reg_subrtn_acc_output_return_offset,
|
||||
});
|
||||
|
||||
// Finalize aggregate values for output
|
||||
let agg_start_reg = t_ctx.reg_agg_start.unwrap();
|
||||
// Resolve the label for the start of the group by output row subroutine
|
||||
program.resolve_label(label_agg_final, program.offset());
|
||||
@@ -363,16 +409,34 @@ pub fn emit_group_by<'a>(
|
||||
});
|
||||
}
|
||||
|
||||
// we now have the group by columns in registers (group_exprs_start_register..group_exprs_start_register + group_by.len() - 1)
|
||||
// and the agg results in (agg_start_reg..agg_start_reg + aggregates.len() - 1)
|
||||
// we need to call translate_expr on each result column, but replace the expr with a register copy in case any part of the
|
||||
// result column expression matches a) a group by column or b) an aggregation result.
|
||||
for (i, expr) in group_by.exprs.iter().enumerate() {
|
||||
t_ctx
|
||||
.resolver
|
||||
.expr_to_reg_cache
|
||||
.push((expr, reg_group_exprs_acc + i));
|
||||
// Map GROUP BY expressions to their registers in the result set
|
||||
for (i, (expr, is_in_result)) in group_by
|
||||
.exprs
|
||||
.iter()
|
||||
.zip(group_by_expr_in_res_cols)
|
||||
.enumerate()
|
||||
{
|
||||
if is_in_result {
|
||||
if let Some(reg) = &column_register_mapping.get(i).and_then(|opt| *opt) {
|
||||
t_ctx.resolver.expr_to_reg_cache.push((expr, *reg));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Map non-aggregate, non-GROUP BY columns to their registers
|
||||
let non_agg_cols = plan
|
||||
.result_columns
|
||||
.iter()
|
||||
.filter(|rc| !rc.contains_aggregates && !is_column_in_group_by(&rc.expr, &group_by.exprs));
|
||||
|
||||
for (idx, rc) in non_agg_cols.enumerate() {
|
||||
let sorter_idx = group_by_count + idx;
|
||||
if let Some(reg) = column_register_mapping.get(sorter_idx).and_then(|opt| *opt) {
|
||||
t_ctx.resolver.expr_to_reg_cache.push((&rc.expr, reg));
|
||||
}
|
||||
}
|
||||
|
||||
// Map aggregate expressions to their result registers
|
||||
for (i, agg) in plan.aggregates.iter().enumerate() {
|
||||
t_ctx
|
||||
.resolver
|
||||
@@ -415,12 +479,18 @@ pub fn emit_group_by<'a>(
|
||||
return_reg: reg_subrtn_acc_output_return_offset,
|
||||
});
|
||||
|
||||
// Subroutine to clear accumulators for a new group
|
||||
program.add_comment(program.offset(), "clear accumulator subroutine start");
|
||||
program.resolve_label(label_subrtn_acc_clear, program.offset());
|
||||
let start_reg = reg_group_exprs_acc;
|
||||
let start_reg = reg_non_aggregate_exprs_acc;
|
||||
|
||||
// Reset all accumulator registers to NULL
|
||||
program.emit_insn(Insn::Null {
|
||||
dest: start_reg,
|
||||
dest_end: Some(start_reg + group_by.exprs.len() + plan.aggregates.len() - 1),
|
||||
dest_end: Some(
|
||||
start_reg + non_group_by_non_agg_column_count + group_by_count + plan.aggregates.len()
|
||||
- 1,
|
||||
),
|
||||
});
|
||||
|
||||
program.emit_insn(Insn::Integer {
|
||||
@@ -430,8 +500,7 @@ pub fn emit_group_by<'a>(
|
||||
program.emit_insn(Insn::Return {
|
||||
return_reg: reg_subrtn_acc_clear_return_offset,
|
||||
});
|
||||
|
||||
program.resolve_label(label_group_by_end, program.offset());
|
||||
program.preassign_label_to_next_insn(label_group_by_end);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -668,3 +737,9 @@ pub fn translate_aggregation_step_groupby(
|
||||
};
|
||||
Ok(dest)
|
||||
}
|
||||
|
||||
pub fn is_column_in_group_by(expr: &ast::Expr, group_by_exprs: &[ast::Expr]) -> bool {
|
||||
group_by_exprs
|
||||
.iter()
|
||||
.any(|expr2| exprs_are_equivalent(expr, expr2))
|
||||
}
|
||||
|
||||
298
core/translate/index.rs
Normal file
298
core/translate/index.rs
Normal file
@@ -0,0 +1,298 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
schema::{BTreeTable, Column, Index, IndexColumn, PseudoTable, Schema},
|
||||
storage::pager::CreateBTreeFlags,
|
||||
util::normalize_ident,
|
||||
vdbe::{
|
||||
builder::{CursorType, ProgramBuilder, QueryMode},
|
||||
insn::{IdxInsertFlags, Insn, RegisterOrLiteral},
|
||||
},
|
||||
};
|
||||
use limbo_sqlite3_parser::ast::{self, Expr, Id, SortOrder, SortedColumn};
|
||||
|
||||
use super::schema::{emit_schema_entry, SchemaEntryType, SQLITE_TABLEID};
|
||||
|
||||
pub fn translate_create_index(
|
||||
mode: QueryMode,
|
||||
unique_if_not_exists: (bool, bool),
|
||||
idx_name: &str,
|
||||
tbl_name: &str,
|
||||
columns: &[SortedColumn],
|
||||
schema: &Schema,
|
||||
) -> crate::Result<ProgramBuilder> {
|
||||
let idx_name = normalize_ident(idx_name);
|
||||
let tbl_name = normalize_ident(tbl_name);
|
||||
let mut program = ProgramBuilder::new(crate::vdbe::builder::ProgramBuilderOpts {
|
||||
query_mode: mode,
|
||||
num_cursors: 5,
|
||||
approx_num_insns: 40,
|
||||
approx_num_labels: 5,
|
||||
});
|
||||
|
||||
// Check if the index is being created on a valid btree table and
|
||||
// the name is globally unique in the schema.
|
||||
if !schema.is_unique_idx_name(&idx_name) {
|
||||
crate::bail_parse_error!("Error: index with name '{idx_name}' already exists.");
|
||||
}
|
||||
let Some(tbl) = schema.tables.get(&tbl_name) else {
|
||||
crate::bail_parse_error!("Error: table '{tbl_name}' does not exist.");
|
||||
};
|
||||
let Some(tbl) = tbl.btree() else {
|
||||
crate::bail_parse_error!("Error: table '{tbl_name}' is not a b-tree table.");
|
||||
};
|
||||
let columns = resolve_sorted_columns(&tbl, columns)?;
|
||||
|
||||
// Prologue:
|
||||
let init_label = program.emit_init();
|
||||
let start_offset = program.offset();
|
||||
|
||||
let idx = Arc::new(Index {
|
||||
name: idx_name.clone(),
|
||||
table_name: tbl.name.clone(),
|
||||
root_page: 0, // we dont have access till its created, after we parse the schema table
|
||||
columns: columns
|
||||
.iter()
|
||||
.map(|((pos_in_table, col), order)| IndexColumn {
|
||||
name: col.name.as_ref().unwrap().clone(),
|
||||
order: *order,
|
||||
pos_in_table: *pos_in_table,
|
||||
})
|
||||
.collect(),
|
||||
unique: unique_if_not_exists.0,
|
||||
ephemeral: false,
|
||||
});
|
||||
|
||||
// Allocate the necessary cursors:
|
||||
//
|
||||
// 1. sqlite_schema_cursor_id - sqlite_schema table
|
||||
// 2. btree_cursor_id - new index btree
|
||||
// 3. table_cursor_id - table we are creating the index on
|
||||
// 4. sorter_cursor_id - sorter
|
||||
// 5. pseudo_cursor_id - pseudo table to store the sorted index values
|
||||
let sqlite_table = schema.get_btree_table(SQLITE_TABLEID).unwrap();
|
||||
let sqlite_schema_cursor_id = program.alloc_cursor_id(
|
||||
Some(SQLITE_TABLEID.to_owned()),
|
||||
CursorType::BTreeTable(sqlite_table.clone()),
|
||||
);
|
||||
let btree_cursor_id = program.alloc_cursor_id(
|
||||
Some(idx_name.to_owned()),
|
||||
CursorType::BTreeIndex(idx.clone()),
|
||||
);
|
||||
let table_cursor_id = program.alloc_cursor_id(
|
||||
Some(tbl_name.to_owned()),
|
||||
CursorType::BTreeTable(tbl.clone()),
|
||||
);
|
||||
let sorter_cursor_id = program.alloc_cursor_id(None, CursorType::Sorter);
|
||||
let pseudo_table = PseudoTable::new_with_columns(tbl.columns.clone());
|
||||
let pseudo_cursor_id = program.alloc_cursor_id(None, CursorType::Pseudo(pseudo_table.into()));
|
||||
|
||||
// Create a new B-Tree and store the root page index in a register
|
||||
let root_page_reg = program.alloc_register();
|
||||
program.emit_insn(Insn::CreateBtree {
|
||||
db: 0,
|
||||
root: root_page_reg,
|
||||
flags: CreateBTreeFlags::new_index(),
|
||||
});
|
||||
|
||||
// open the sqlite schema table for writing and create a new entry for the index
|
||||
program.emit_insn(Insn::OpenWrite {
|
||||
cursor_id: sqlite_schema_cursor_id,
|
||||
root_page: RegisterOrLiteral::Literal(sqlite_table.root_page),
|
||||
});
|
||||
let sql = create_idx_stmt_to_sql(&tbl_name, &idx_name, unique_if_not_exists, &columns);
|
||||
emit_schema_entry(
|
||||
&mut program,
|
||||
sqlite_schema_cursor_id,
|
||||
SchemaEntryType::Index,
|
||||
&idx_name,
|
||||
&tbl_name,
|
||||
root_page_reg,
|
||||
Some(sql),
|
||||
);
|
||||
|
||||
// determine the order of the columns in the index for the sorter
|
||||
let order = idx.columns.iter().map(|c| c.order.clone()).collect();
|
||||
// open the sorter and the pseudo table
|
||||
program.emit_insn(Insn::SorterOpen {
|
||||
cursor_id: sorter_cursor_id,
|
||||
columns: columns.len(),
|
||||
order,
|
||||
});
|
||||
let content_reg = program.alloc_register();
|
||||
program.emit_insn(Insn::OpenPseudo {
|
||||
cursor_id: pseudo_cursor_id,
|
||||
content_reg,
|
||||
num_fields: columns.len() + 1,
|
||||
});
|
||||
|
||||
// open the table we are creating the index on for reading
|
||||
program.emit_insn(Insn::OpenRead {
|
||||
cursor_id: table_cursor_id,
|
||||
root_page: tbl.root_page,
|
||||
});
|
||||
|
||||
let loop_start_label = program.allocate_label();
|
||||
let loop_end_label = program.allocate_label();
|
||||
program.emit_insn(Insn::Rewind {
|
||||
cursor_id: table_cursor_id,
|
||||
pc_if_empty: loop_end_label,
|
||||
});
|
||||
program.preassign_label_to_next_insn(loop_start_label);
|
||||
|
||||
// Loop start:
|
||||
// Collect index values into start_reg..rowid_reg
|
||||
// emit MakeRecord (index key + rowid) into record_reg.
|
||||
//
|
||||
// Then insert the record into the sorter
|
||||
let start_reg = program.alloc_registers(columns.len() + 1);
|
||||
for (i, (col, _)) in columns.iter().enumerate() {
|
||||
program.emit_insn(Insn::Column {
|
||||
cursor_id: table_cursor_id,
|
||||
column: col.0,
|
||||
dest: start_reg + i,
|
||||
});
|
||||
}
|
||||
let rowid_reg = start_reg + columns.len();
|
||||
program.emit_insn(Insn::RowId {
|
||||
cursor_id: table_cursor_id,
|
||||
dest: rowid_reg,
|
||||
});
|
||||
let record_reg = program.alloc_register();
|
||||
program.emit_insn(Insn::MakeRecord {
|
||||
start_reg,
|
||||
count: columns.len() + 1,
|
||||
dest_reg: record_reg,
|
||||
});
|
||||
program.emit_insn(Insn::SorterInsert {
|
||||
cursor_id: sorter_cursor_id,
|
||||
record_reg,
|
||||
});
|
||||
|
||||
program.emit_insn(Insn::Next {
|
||||
cursor_id: table_cursor_id,
|
||||
pc_if_next: loop_start_label,
|
||||
});
|
||||
program.preassign_label_to_next_insn(loop_end_label);
|
||||
|
||||
// Open the index btree we created for writing to insert the
|
||||
// newly sorted index records.
|
||||
program.emit_insn(Insn::OpenWrite {
|
||||
cursor_id: btree_cursor_id,
|
||||
root_page: RegisterOrLiteral::Register(root_page_reg),
|
||||
});
|
||||
|
||||
let sorted_loop_start = program.allocate_label();
|
||||
let sorted_loop_end = program.allocate_label();
|
||||
|
||||
// Sort the index records in the sorter
|
||||
program.emit_insn(Insn::SorterSort {
|
||||
cursor_id: sorter_cursor_id,
|
||||
pc_if_empty: sorted_loop_end,
|
||||
});
|
||||
program.preassign_label_to_next_insn(sorted_loop_start);
|
||||
let sorted_record_reg = program.alloc_register();
|
||||
program.emit_insn(Insn::SorterData {
|
||||
pseudo_cursor: pseudo_cursor_id,
|
||||
cursor_id: sorter_cursor_id,
|
||||
dest_reg: sorted_record_reg,
|
||||
});
|
||||
|
||||
// seek to the end of the index btree to position the cursor for appending
|
||||
program.emit_insn(Insn::SeekEnd {
|
||||
cursor_id: btree_cursor_id,
|
||||
});
|
||||
// insert new index record
|
||||
program.emit_insn(Insn::IdxInsert {
|
||||
cursor_id: btree_cursor_id,
|
||||
record_reg: sorted_record_reg,
|
||||
unpacked_start: None, // TODO: optimize with these to avoid decoding record twice
|
||||
unpacked_count: None,
|
||||
flags: IdxInsertFlags::new().use_seek(false),
|
||||
});
|
||||
program.emit_insn(Insn::SorterNext {
|
||||
cursor_id: sorter_cursor_id,
|
||||
pc_if_next: sorted_loop_start,
|
||||
});
|
||||
program.preassign_label_to_next_insn(sorted_loop_end);
|
||||
|
||||
// End of the outer loop
|
||||
//
|
||||
// Keep schema table open to emit ParseSchema, close the other cursors.
|
||||
program.close_cursors(&[sorter_cursor_id, table_cursor_id, btree_cursor_id]);
|
||||
|
||||
// TODO: SetCookie for schema change
|
||||
//
|
||||
// Parse the schema table to get the index root page and add new index to Schema
|
||||
let parse_schema_where_clause = format!("name = '{}' AND type = 'index'", idx_name);
|
||||
program.emit_insn(Insn::ParseSchema {
|
||||
db: sqlite_schema_cursor_id,
|
||||
where_clause: parse_schema_where_clause,
|
||||
});
|
||||
// Close the final sqlite_schema cursor
|
||||
program.emit_insn(Insn::Close {
|
||||
cursor_id: sqlite_schema_cursor_id,
|
||||
});
|
||||
|
||||
// Epilogue:
|
||||
program.emit_halt();
|
||||
program.preassign_label_to_next_insn(init_label);
|
||||
program.emit_transaction(true);
|
||||
program.emit_constant_insns();
|
||||
program.emit_goto(start_offset);
|
||||
|
||||
Ok(program)
|
||||
}
|
||||
|
||||
fn resolve_sorted_columns<'a>(
|
||||
table: &'a BTreeTable,
|
||||
cols: &[SortedColumn],
|
||||
) -> crate::Result<Vec<((usize, &'a Column), SortOrder)>> {
|
||||
let mut resolved = Vec::with_capacity(cols.len());
|
||||
for sc in cols {
|
||||
let ident = normalize_ident(match &sc.expr {
|
||||
Expr::Id(Id(col_name)) | Expr::Name(ast::Name(col_name)) => col_name,
|
||||
_ => crate::bail_parse_error!("Error: cannot use expressions in CREATE INDEX"),
|
||||
});
|
||||
let Some(col) = table.get_column(&ident) else {
|
||||
crate::bail_parse_error!(
|
||||
"Error: column '{ident}' does not exist in table '{}'",
|
||||
table.name
|
||||
);
|
||||
};
|
||||
resolved.push((col, sc.order.unwrap_or(SortOrder::Asc)));
|
||||
}
|
||||
Ok(resolved)
|
||||
}
|
||||
|
||||
fn create_idx_stmt_to_sql(
|
||||
tbl_name: &str,
|
||||
idx_name: &str,
|
||||
unique_if_not_exists: (bool, bool),
|
||||
cols: &[((usize, &Column), SortOrder)],
|
||||
) -> String {
|
||||
let mut sql = String::with_capacity(128);
|
||||
sql.push_str("CREATE ");
|
||||
if unique_if_not_exists.0 {
|
||||
sql.push_str("UNIQUE ");
|
||||
}
|
||||
sql.push_str("INDEX ");
|
||||
if unique_if_not_exists.1 {
|
||||
sql.push_str("IF NOT EXISTS ");
|
||||
}
|
||||
sql.push_str(idx_name);
|
||||
sql.push_str(" ON ");
|
||||
sql.push_str(tbl_name);
|
||||
sql.push_str(" (");
|
||||
for (i, (col, order)) in cols.iter().enumerate() {
|
||||
if i > 0 {
|
||||
sql.push_str(", ");
|
||||
}
|
||||
sql.push_str(col.1.name.as_ref().unwrap());
|
||||
if *order == SortOrder::Desc {
|
||||
sql.push_str(" DESC");
|
||||
}
|
||||
}
|
||||
sql.push(')');
|
||||
sql
|
||||
}
|
||||
@@ -6,22 +6,22 @@ use limbo_sqlite3_parser::ast::{
|
||||
};
|
||||
|
||||
use crate::error::SQLITE_CONSTRAINT_PRIMARYKEY;
|
||||
use crate::schema::Table;
|
||||
use crate::schema::{IndexColumn, Table};
|
||||
use crate::util::normalize_ident;
|
||||
use crate::vdbe::builder::{ProgramBuilderOpts, QueryMode};
|
||||
use crate::vdbe::insn::{IdxInsertFlags, RegisterOrLiteral};
|
||||
use crate::vdbe::BranchOffset;
|
||||
use crate::{
|
||||
schema::{Column, Schema},
|
||||
translate::expr::translate_expr,
|
||||
vdbe::{
|
||||
builder::{CursorType, ProgramBuilder},
|
||||
insn::Insn,
|
||||
},
|
||||
SymbolTable,
|
||||
};
|
||||
use crate::{Result, VirtualTable};
|
||||
use crate::{Result, SymbolTable, VirtualTable};
|
||||
|
||||
use super::emitter::Resolver;
|
||||
use super::expr::{translate_expr_no_constant_opt, NoConstantOptReason};
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn translate_insert(
|
||||
@@ -82,16 +82,33 @@ pub fn translate_insert(
|
||||
Some(table_name.0.clone()),
|
||||
CursorType::BTreeTable(btree_table.clone()),
|
||||
);
|
||||
// allocate cursor id's for each btree index cursor we'll need to populate the indexes
|
||||
// (idx name, root_page, idx cursor id)
|
||||
let idx_cursors = schema
|
||||
.get_indices(&table_name.0)
|
||||
.iter()
|
||||
.map(|idx| {
|
||||
(
|
||||
&idx.name,
|
||||
idx.root_page,
|
||||
program.alloc_cursor_id(
|
||||
Some(table_name.0.clone()),
|
||||
CursorType::BTreeIndex(idx.clone()),
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<(&String, usize, usize)>>();
|
||||
let root_page = btree_table.root_page;
|
||||
let values = match body {
|
||||
InsertBody::Select(select, None) => match &select.body.select.deref() {
|
||||
InsertBody::Select(select, _) => match &select.body.select.deref() {
|
||||
OneSelect::Values(values) => values,
|
||||
_ => todo!(),
|
||||
},
|
||||
_ => todo!(),
|
||||
InsertBody::DefaultValues => &vec![vec![]],
|
||||
};
|
||||
|
||||
let column_mappings = resolve_columns_for_insert(&table, columns, values)?;
|
||||
let index_col_mappings = resolve_indicies_for_insert(schema, table.as_ref(), &column_mappings)?;
|
||||
// Check if rowid was provided (through INTEGER PRIMARY KEY as a rowid alias)
|
||||
let rowid_alias_index = btree_table.columns.iter().position(|c| c.is_rowid_alias);
|
||||
let has_user_provided_rowid = {
|
||||
@@ -126,12 +143,15 @@ pub fn translate_insert(
|
||||
if inserting_multiple_rows {
|
||||
let yield_reg = program.alloc_register();
|
||||
let jump_on_definition_label = program.allocate_label();
|
||||
let start_offset_label = program.allocate_label();
|
||||
program.emit_insn(Insn::InitCoroutine {
|
||||
yield_reg,
|
||||
jump_on_definition: jump_on_definition_label,
|
||||
start_offset: program.offset().add(1u32),
|
||||
start_offset: start_offset_label,
|
||||
});
|
||||
|
||||
program.resolve_label(start_offset_label, program.offset());
|
||||
|
||||
for value in values {
|
||||
populate_column_registers(
|
||||
&mut program,
|
||||
@@ -148,13 +168,12 @@ pub fn translate_insert(
|
||||
});
|
||||
}
|
||||
program.emit_insn(Insn::EndCoroutine { yield_reg });
|
||||
program.resolve_label(jump_on_definition_label, program.offset());
|
||||
program.preassign_label_to_next_insn(jump_on_definition_label);
|
||||
|
||||
program.emit_insn(Insn::OpenWriteAsync {
|
||||
program.emit_insn(Insn::OpenWrite {
|
||||
cursor_id,
|
||||
root_page,
|
||||
root_page: RegisterOrLiteral::Literal(root_page),
|
||||
});
|
||||
program.emit_insn(Insn::OpenWriteAwait {});
|
||||
|
||||
// Main loop
|
||||
// FIXME: rollback is not implemented. E.g. if you insert 2 rows and one fails to unique constraint violation,
|
||||
@@ -166,11 +185,10 @@ pub fn translate_insert(
|
||||
});
|
||||
} else {
|
||||
// Single row - populate registers directly
|
||||
program.emit_insn(Insn::OpenWriteAsync {
|
||||
program.emit_insn(Insn::OpenWrite {
|
||||
cursor_id,
|
||||
root_page,
|
||||
root_page: RegisterOrLiteral::Literal(root_page),
|
||||
});
|
||||
program.emit_insn(Insn::OpenWriteAwait {});
|
||||
|
||||
populate_column_registers(
|
||||
&mut program,
|
||||
@@ -182,7 +200,13 @@ pub fn translate_insert(
|
||||
&resolver,
|
||||
)?;
|
||||
}
|
||||
|
||||
// Open all the index btrees for writing
|
||||
for idx_cursor in idx_cursors.iter() {
|
||||
program.emit_insn(Insn::OpenWrite {
|
||||
cursor_id: idx_cursor.2,
|
||||
root_page: idx_cursor.1.into(),
|
||||
});
|
||||
}
|
||||
// Common record insertion logic for both single and multiple rows
|
||||
let check_rowid_is_integer_label = rowid_alias_reg.and(Some(program.allocate_label()));
|
||||
if let Some(reg) = rowid_alias_reg {
|
||||
@@ -246,8 +270,109 @@ pub fn translate_insert(
|
||||
err_code: SQLITE_CONSTRAINT_PRIMARYKEY,
|
||||
description: format!("{}.{}", table_name.0, rowid_column_name),
|
||||
});
|
||||
program.preassign_label_to_next_insn(make_record_label);
|
||||
}
|
||||
|
||||
program.resolve_label(make_record_label, program.offset());
|
||||
match table.btree() {
|
||||
Some(t) if t.is_strict => {
|
||||
program.emit_insn(Insn::TypeCheck {
|
||||
start_reg: column_registers_start,
|
||||
count: num_cols,
|
||||
check_generated: true,
|
||||
table_reference: Rc::clone(&t),
|
||||
});
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
for index_col_mapping in index_col_mappings.iter() {
|
||||
// find which cursor we opened earlier for this index
|
||||
let idx_cursor_id = idx_cursors
|
||||
.iter()
|
||||
.find(|(name, _, _)| *name == &index_col_mapping.idx_name)
|
||||
.map(|(_, _, c_id)| *c_id)
|
||||
.expect("no cursor found for index");
|
||||
|
||||
let num_cols = index_col_mapping.columns.len();
|
||||
// allocate scratch registers for the index columns plus rowid
|
||||
let idx_start_reg = program.alloc_registers(num_cols + 1);
|
||||
|
||||
// copy each index column from the table's column registers into these scratch regs
|
||||
for (i, col) in index_col_mapping.columns.iter().enumerate() {
|
||||
// copy from the table's column register over to the index's scratch register
|
||||
|
||||
program.emit_insn(Insn::Copy {
|
||||
src_reg: column_registers_start + col.0,
|
||||
dst_reg: idx_start_reg + i,
|
||||
amount: 0,
|
||||
});
|
||||
}
|
||||
// last register is the rowid
|
||||
program.emit_insn(Insn::Copy {
|
||||
src_reg: rowid_reg,
|
||||
dst_reg: idx_start_reg + num_cols,
|
||||
amount: 0,
|
||||
});
|
||||
|
||||
let record_reg = program.alloc_register();
|
||||
program.emit_insn(Insn::MakeRecord {
|
||||
start_reg: idx_start_reg,
|
||||
count: num_cols + 1,
|
||||
dest_reg: record_reg,
|
||||
});
|
||||
|
||||
let index = schema
|
||||
.get_index(&table_name.0, &index_col_mapping.idx_name)
|
||||
.expect("index should be present");
|
||||
|
||||
if index.unique {
|
||||
let label_idx_insert = program.allocate_label();
|
||||
program.emit_insn(Insn::NoConflict {
|
||||
cursor_id: idx_cursor_id,
|
||||
target_pc: label_idx_insert,
|
||||
record_reg: idx_start_reg,
|
||||
num_regs: num_cols,
|
||||
});
|
||||
let column_names = index_col_mapping.columns.iter().enumerate().fold(
|
||||
String::with_capacity(50),
|
||||
|mut accum, (idx, (index, _))| {
|
||||
if idx > 0 {
|
||||
accum.push_str(", ");
|
||||
}
|
||||
|
||||
accum.push_str(&btree_table.name);
|
||||
accum.push('.');
|
||||
|
||||
let name = btree_table
|
||||
.columns
|
||||
.get(*index)
|
||||
.unwrap()
|
||||
.name
|
||||
.as_ref()
|
||||
.expect("column name is None");
|
||||
accum.push_str(name);
|
||||
|
||||
accum
|
||||
},
|
||||
);
|
||||
|
||||
program.emit_insn(Insn::Halt {
|
||||
err_code: SQLITE_CONSTRAINT_PRIMARYKEY,
|
||||
description: column_names,
|
||||
});
|
||||
|
||||
program.resolve_label(label_idx_insert, program.offset());
|
||||
}
|
||||
|
||||
// now do the actual index insertion using the unpacked registers
|
||||
program.emit_insn(Insn::IdxInsert {
|
||||
cursor_id: idx_cursor_id,
|
||||
record_reg,
|
||||
unpacked_start: Some(idx_start_reg), // TODO: enable optimization
|
||||
unpacked_count: Some((num_cols + 1) as u16),
|
||||
// TODO: figure out how to determine whether or not we need to seek prior to insert.
|
||||
flags: IdxInsertFlags::new(),
|
||||
});
|
||||
}
|
||||
|
||||
// Create and insert the record
|
||||
@@ -257,13 +382,12 @@ pub fn translate_insert(
|
||||
dest_reg: record_register,
|
||||
});
|
||||
|
||||
program.emit_insn(Insn::InsertAsync {
|
||||
program.emit_insn(Insn::Insert {
|
||||
cursor: cursor_id,
|
||||
key_reg: rowid_reg,
|
||||
record_reg: record_register,
|
||||
flag: 0,
|
||||
});
|
||||
program.emit_insn(Insn::InsertAwait { cursor_id });
|
||||
|
||||
if inserting_multiple_rows {
|
||||
// For multiple rows, loop back
|
||||
@@ -277,8 +401,8 @@ pub fn translate_insert(
|
||||
err_code: 0,
|
||||
description: String::new(),
|
||||
});
|
||||
program.preassign_label_to_next_insn(init_label);
|
||||
|
||||
program.resolve_label(init_label, program.offset());
|
||||
program.emit_insn(Insn::Transaction { write: true });
|
||||
program.emit_constant_insns();
|
||||
program.emit_insn(Insn::Goto {
|
||||
@@ -297,6 +421,8 @@ struct ColumnMapping<'a> {
|
||||
/// If Some(i), use the i-th value from the VALUES tuple
|
||||
/// If None, use NULL (column was not specified in INSERT statement)
|
||||
value_index: Option<usize>,
|
||||
/// The default value for the column, if defined
|
||||
default_value: Option<&'a Expr>,
|
||||
}
|
||||
|
||||
/// Resolves how each column in a table should be populated during an INSERT.
|
||||
@@ -352,6 +478,7 @@ fn resolve_columns_for_insert<'a>(
|
||||
.map(|(i, col)| ColumnMapping {
|
||||
column: col,
|
||||
value_index: if i < num_values { Some(i) } else { None },
|
||||
default_value: col.default.as_ref(),
|
||||
})
|
||||
.collect());
|
||||
}
|
||||
@@ -362,6 +489,7 @@ fn resolve_columns_for_insert<'a>(
|
||||
.map(|col| ColumnMapping {
|
||||
column: col,
|
||||
value_index: None,
|
||||
default_value: col.default.as_ref(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -388,6 +516,69 @@ fn resolve_columns_for_insert<'a>(
|
||||
Ok(mappings)
|
||||
}
|
||||
|
||||
/// Represents how a column in an index should be populated during an INSERT.
|
||||
/// Similar to ColumnMapping above but includes the index name, as well as multiple
|
||||
/// possible value indices for each.
|
||||
#[derive(Debug, Default)]
|
||||
struct IndexColMapping {
|
||||
idx_name: String,
|
||||
columns: Vec<(usize, IndexColumn)>,
|
||||
value_indicies: Vec<Option<usize>>,
|
||||
}
|
||||
|
||||
impl IndexColMapping {
|
||||
fn new(name: String) -> Self {
|
||||
IndexColMapping {
|
||||
idx_name: name,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Example:
|
||||
/// Table 'test': (a, b, c);
|
||||
/// Index 'idx': test(a, b);
|
||||
///________________________________
|
||||
/// Insert (a, c): (2, 3)
|
||||
/// Record: (2, NULL, 3)
|
||||
/// IndexColMapping: (a, b) = (2, NULL)
|
||||
fn resolve_indicies_for_insert(
|
||||
schema: &Schema,
|
||||
table: &Table,
|
||||
columns: &[ColumnMapping<'_>],
|
||||
) -> Result<Vec<IndexColMapping>> {
|
||||
let mut index_col_mappings = Vec::new();
|
||||
// Iterate over all indices for this table
|
||||
for index in schema.get_indices(table.get_name()) {
|
||||
let mut idx_map = IndexColMapping::new(index.name.clone());
|
||||
// For each column in the index (in the order defined by the index),
|
||||
// try to find the corresponding column in the insert’s column mapping.
|
||||
for idx_col in &index.columns {
|
||||
let target_name = normalize_ident(idx_col.name.as_str());
|
||||
if let Some((i, col_mapping)) = columns.iter().enumerate().find(|(_, mapping)| {
|
||||
mapping
|
||||
.column
|
||||
.name
|
||||
.as_ref()
|
||||
.map_or(false, |name| name.eq_ignore_ascii_case(&target_name))
|
||||
}) {
|
||||
idx_map.columns.push((i, idx_col.clone()));
|
||||
idx_map.value_indicies.push(col_mapping.value_index);
|
||||
} else {
|
||||
return Err(crate::LimboError::ParseError(format!(
|
||||
"Column {} not found in index {}",
|
||||
target_name, index.name
|
||||
)));
|
||||
}
|
||||
}
|
||||
// Add the mapping if at least one column was found.
|
||||
if !idx_map.columns.is_empty() {
|
||||
index_col_mappings.push(idx_map);
|
||||
}
|
||||
}
|
||||
Ok(index_col_mappings)
|
||||
}
|
||||
|
||||
/// Populates the column registers with values for a single row
|
||||
fn populate_column_registers(
|
||||
program: &mut ProgramBuilder,
|
||||
@@ -413,18 +604,28 @@ fn populate_column_registers(
|
||||
} else {
|
||||
target_reg
|
||||
};
|
||||
translate_expr(
|
||||
translate_expr_no_constant_opt(
|
||||
program,
|
||||
None,
|
||||
value.get(value_index).expect("value index out of bounds"),
|
||||
reg,
|
||||
resolver,
|
||||
NoConstantOptReason::RegisterReuse,
|
||||
)?;
|
||||
if write_directly_to_rowid_reg {
|
||||
program.emit_insn(Insn::SoftNull { reg: target_reg });
|
||||
}
|
||||
} else if let Some(default_expr) = mapping.default_value {
|
||||
translate_expr_no_constant_opt(
|
||||
program,
|
||||
None,
|
||||
default_expr,
|
||||
target_reg,
|
||||
resolver,
|
||||
NoConstantOptReason::RegisterReuse,
|
||||
)?;
|
||||
} else {
|
||||
// Column was not specified - use NULL if it is nullable, otherwise error
|
||||
// Column was not specified as has no DEFAULT - use NULL if it is nullable, otherwise error
|
||||
// Rowid alias columns can be NULL because we will autogenerate a rowid in that case.
|
||||
let is_nullable = !mapping.column.primary_key || mapping.column.is_rowid_alias;
|
||||
if is_nullable {
|
||||
@@ -472,7 +673,14 @@ fn translate_virtual_table_insert(
|
||||
|
||||
let value_registers_start = program.alloc_registers(values[0].len());
|
||||
for (i, expr) in values[0].iter().enumerate() {
|
||||
translate_expr(program, None, expr, value_registers_start + i, resolver)?;
|
||||
translate_expr_no_constant_opt(
|
||||
program,
|
||||
None,
|
||||
expr,
|
||||
value_registers_start + i,
|
||||
resolver,
|
||||
NoConstantOptReason::RegisterReuse,
|
||||
)?;
|
||||
}
|
||||
/* *
|
||||
* Inserts for virtual tables are done in a single step.
|
||||
@@ -526,12 +734,12 @@ fn translate_virtual_table_insert(
|
||||
});
|
||||
|
||||
let halt_label = program.allocate_label();
|
||||
program.resolve_label(halt_label, program.offset());
|
||||
program.emit_insn(Insn::Halt {
|
||||
err_code: 0,
|
||||
description: String::new(),
|
||||
});
|
||||
|
||||
program.resolve_label(halt_label, program.offset());
|
||||
program.resolve_label(init_label, program.offset());
|
||||
|
||||
program.emit_insn(Insn::Goto {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@ pub(crate) mod delete;
|
||||
pub(crate) mod emitter;
|
||||
pub(crate) mod expr;
|
||||
pub(crate) mod group_by;
|
||||
pub(crate) mod index;
|
||||
pub(crate) mod insert;
|
||||
pub(crate) mod main_loop;
|
||||
pub(crate) mod optimizer;
|
||||
@@ -34,6 +35,7 @@ use crate::translate::delete::translate_delete;
|
||||
use crate::vdbe::builder::{ProgramBuilder, ProgramBuilderOpts, QueryMode};
|
||||
use crate::vdbe::Program;
|
||||
use crate::{bail_parse_error, Connection, Result, SymbolTable};
|
||||
use index::translate_create_index;
|
||||
use insert::translate_insert;
|
||||
use limbo_sqlite3_parser::ast::{self, Delete, Insert};
|
||||
use schema::{translate_create_table, translate_create_virtual_table, translate_drop_table};
|
||||
@@ -61,7 +63,24 @@ pub fn translate(
|
||||
ast::Stmt::Attach { .. } => bail_parse_error!("ATTACH not supported yet"),
|
||||
ast::Stmt::Begin(tx_type, tx_name) => translate_tx_begin(tx_type, tx_name)?,
|
||||
ast::Stmt::Commit(tx_name) => translate_tx_commit(tx_name)?,
|
||||
ast::Stmt::CreateIndex { .. } => bail_parse_error!("CREATE INDEX not supported yet"),
|
||||
ast::Stmt::CreateIndex {
|
||||
unique,
|
||||
if_not_exists,
|
||||
idx_name,
|
||||
tbl_name,
|
||||
columns,
|
||||
..
|
||||
} => {
|
||||
change_cnt_on = true;
|
||||
translate_create_index(
|
||||
query_mode,
|
||||
(unique, if_not_exists),
|
||||
&idx_name.name.0,
|
||||
&tbl_name.0,
|
||||
&columns,
|
||||
schema,
|
||||
)?
|
||||
}
|
||||
ast::Stmt::CreateTable {
|
||||
temporary,
|
||||
if_not_exists,
|
||||
@@ -78,7 +97,7 @@ pub fn translate(
|
||||
ast::Stmt::CreateTrigger { .. } => bail_parse_error!("CREATE TRIGGER not supported yet"),
|
||||
ast::Stmt::CreateView { .. } => bail_parse_error!("CREATE VIEW not supported yet"),
|
||||
ast::Stmt::CreateVirtualTable(vtab) => {
|
||||
translate_create_virtual_table(*vtab, schema, query_mode)?
|
||||
translate_create_virtual_table(*vtab, schema, query_mode, &syms)?
|
||||
}
|
||||
ast::Stmt::Delete(delete) => {
|
||||
let Delete {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,9 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use limbo_sqlite3_parser::ast;
|
||||
use limbo_sqlite3_parser::ast::{self, SortOrder};
|
||||
|
||||
use crate::{
|
||||
schema::{Column, PseudoTable},
|
||||
types::{OwnedValue, Record},
|
||||
util::exprs_are_equivalent,
|
||||
vdbe::{
|
||||
builder::{CursorType, ProgramBuilder},
|
||||
@@ -16,7 +15,7 @@ use crate::{
|
||||
use super::{
|
||||
emitter::TranslateCtx,
|
||||
expr::translate_expr,
|
||||
plan::{Direction, ResultSetColumn, SelectPlan},
|
||||
plan::{ResultSetColumn, SelectPlan},
|
||||
result_row::{emit_offset, emit_result_row_and_limit},
|
||||
};
|
||||
|
||||
@@ -33,21 +32,17 @@ pub struct SortMetadata {
|
||||
pub fn init_order_by(
|
||||
program: &mut ProgramBuilder,
|
||||
t_ctx: &mut TranslateCtx,
|
||||
order_by: &[(ast::Expr, Direction)],
|
||||
order_by: &[(ast::Expr, SortOrder)],
|
||||
) -> Result<()> {
|
||||
let sort_cursor = program.alloc_cursor_id(None, CursorType::Sorter);
|
||||
t_ctx.meta_sort = Some(SortMetadata {
|
||||
sort_cursor,
|
||||
reg_sorter_data: program.alloc_register(),
|
||||
});
|
||||
let mut order = Vec::new();
|
||||
for (_, direction) in order_by.iter() {
|
||||
order.push(OwnedValue::Integer(*direction as i64));
|
||||
}
|
||||
program.emit_insn(Insn::SorterOpen {
|
||||
cursor_id: sort_cursor,
|
||||
columns: order_by.len(),
|
||||
order: Record::new(order),
|
||||
order: order_by.iter().map(|(_, direction)| *direction).collect(),
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
@@ -124,8 +119,8 @@ pub fn emit_order_by(
|
||||
cursor_id: sort_cursor,
|
||||
pc_if_empty: sort_loop_end_label,
|
||||
});
|
||||
program.preassign_label_to_next_insn(sort_loop_start_label);
|
||||
|
||||
program.resolve_label(sort_loop_start_label, program.offset());
|
||||
emit_offset(program, t_ctx, plan, sort_loop_next_label)?;
|
||||
|
||||
program.emit_insn(Insn::SorterData {
|
||||
@@ -154,8 +149,7 @@ pub fn emit_order_by(
|
||||
cursor_id: sort_cursor,
|
||||
pc_if_next: sort_loop_start_label,
|
||||
});
|
||||
|
||||
program.resolve_label(sort_loop_end_label, program.offset());
|
||||
program.preassign_label_to_next_insn(sort_loop_end_label);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -258,7 +252,7 @@ pub fn sorter_insert(
|
||||
///
|
||||
/// If any result columns can be skipped, this returns list of 2-tuples of (SkippedResultColumnIndex: usize, ResultColumnIndexInOrderBySorter: usize)
|
||||
pub fn order_by_deduplicate_result_columns(
|
||||
order_by: &[(ast::Expr, Direction)],
|
||||
order_by: &[(ast::Expr, SortOrder)],
|
||||
result_columns: &[ResultSetColumn],
|
||||
) -> Option<Vec<(usize, usize)>> {
|
||||
let mut result_column_remapping: Option<Vec<(usize, usize)>> = None;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use core::fmt;
|
||||
use limbo_sqlite3_parser::ast;
|
||||
use limbo_ext::{ConstraintInfo, ConstraintOp};
|
||||
use limbo_sqlite3_parser::ast::{self, SortOrder};
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
fmt::{Display, Formatter},
|
||||
@@ -7,13 +8,22 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use crate::schema::{PseudoTable, Type};
|
||||
use crate::{
|
||||
function::AggFunc,
|
||||
schema::{BTreeTable, Column, Index, Table},
|
||||
vdbe::BranchOffset,
|
||||
VirtualTable,
|
||||
vdbe::{
|
||||
builder::{CursorType, ProgramBuilder},
|
||||
BranchOffset, CursorID,
|
||||
},
|
||||
Result, VirtualTable,
|
||||
};
|
||||
use crate::{
|
||||
schema::{PseudoTable, Type},
|
||||
types::SeekOp,
|
||||
util::can_pushdown_predicate,
|
||||
};
|
||||
|
||||
use super::emitter::OperationMode;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ResultSetColumn {
|
||||
@@ -24,13 +34,26 @@ pub struct ResultSetColumn {
|
||||
}
|
||||
|
||||
impl ResultSetColumn {
|
||||
pub fn name<'a>(&'a self, tables: &'a [TableReference]) -> Option<&'a String> {
|
||||
pub fn name<'a>(&'a self, tables: &'a [TableReference]) -> Option<&'a str> {
|
||||
if let Some(alias) = &self.alias {
|
||||
return Some(alias);
|
||||
}
|
||||
match &self.expr {
|
||||
ast::Expr::Column { table, column, .. } => {
|
||||
tables[*table].columns()[*column].name.as_ref()
|
||||
tables[*table].columns()[*column].name.as_deref()
|
||||
}
|
||||
ast::Expr::RowId { table, .. } => {
|
||||
// If there is a rowid alias column, use its name
|
||||
if let Table::BTree(table) = &tables[*table].table {
|
||||
if let Some(rowid_alias_column) = table.get_rowid_alias_column() {
|
||||
if let Some(name) = &rowid_alias_column.1.name {
|
||||
return Some(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there is no rowid alias, use "rowid".
|
||||
Some("rowid")
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
@@ -72,6 +95,114 @@ impl WhereTerm {
|
||||
}
|
||||
}
|
||||
|
||||
use crate::ast::{Expr, Operator};
|
||||
|
||||
// This function takes an operator and returns the operator you would obtain if the operands were swapped.
|
||||
// e.g. "literal < column"
|
||||
// which is not the canonical order for constraint pushdown.
|
||||
// This function will return > so that the expression can be treated as if it were written "column > literal"
|
||||
fn reverse_operator(op: &Operator) -> Option<Operator> {
|
||||
match op {
|
||||
Operator::Equals => Some(Operator::Equals),
|
||||
Operator::Less => Some(Operator::Greater),
|
||||
Operator::LessEquals => Some(Operator::GreaterEquals),
|
||||
Operator::Greater => Some(Operator::Less),
|
||||
Operator::GreaterEquals => Some(Operator::LessEquals),
|
||||
Operator::NotEquals => Some(Operator::NotEquals),
|
||||
Operator::Is => Some(Operator::Is),
|
||||
Operator::IsNot => Some(Operator::IsNot),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_ext_constraint_op(op: &Operator) -> Option<ConstraintOp> {
|
||||
match op {
|
||||
Operator::Equals => Some(ConstraintOp::Eq),
|
||||
Operator::Less => Some(ConstraintOp::Lt),
|
||||
Operator::LessEquals => Some(ConstraintOp::Le),
|
||||
Operator::Greater => Some(ConstraintOp::Gt),
|
||||
Operator::GreaterEquals => Some(ConstraintOp::Ge),
|
||||
Operator::NotEquals => Some(ConstraintOp::Ne),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// This function takes a WhereTerm for a select involving a VTab at index 'table_index'.
|
||||
/// It determines whether or not it involves the given table and whether or not it can
|
||||
/// be converted into a ConstraintInfo which can be passed to the vtab module's xBestIndex
|
||||
/// method, which will possibly calculate some information to improve the query plan, that we can send
|
||||
/// back to it as arguments for the VFilter operation.
|
||||
/// is going to be filtered against: e.g:
|
||||
/// 'SELECT key, value FROM vtab WHERE key = 'some_key';
|
||||
/// we need to send the OwnedValue('some_key') as an argument to VFilter, and possibly omit it from
|
||||
/// the filtration in the vdbe layer.
|
||||
pub fn convert_where_to_vtab_constraint(
|
||||
term: &WhereTerm,
|
||||
table_index: usize,
|
||||
pred_idx: usize,
|
||||
) -> Option<ConstraintInfo> {
|
||||
if term.from_outer_join {
|
||||
return None;
|
||||
}
|
||||
let Expr::Binary(lhs, op, rhs) = &term.expr else {
|
||||
return None;
|
||||
};
|
||||
let expr_is_ready = |e: &Expr| -> bool { can_pushdown_predicate(e, table_index) };
|
||||
let (vcol_idx, op_for_vtab, usable, is_rhs) = match (&**lhs, &**rhs) {
|
||||
(
|
||||
Expr::Column {
|
||||
table: tbl_l,
|
||||
column: col_l,
|
||||
..
|
||||
},
|
||||
Expr::Column {
|
||||
table: tbl_r,
|
||||
column: col_r,
|
||||
..
|
||||
},
|
||||
) => {
|
||||
// one side must be the virtual table
|
||||
let vtab_on_l = *tbl_l == table_index;
|
||||
let vtab_on_r = *tbl_r == table_index;
|
||||
if vtab_on_l == vtab_on_r {
|
||||
return None; // either both or none -> not convertible
|
||||
}
|
||||
|
||||
if vtab_on_l {
|
||||
// vtab on left side: operator unchanged
|
||||
let usable = *tbl_r < table_index; // usable if the other table is already positioned
|
||||
(col_l, op, usable, false)
|
||||
} else {
|
||||
// vtab on right side of the expr: reverse operator
|
||||
let usable = *tbl_l < table_index;
|
||||
(col_r, &reverse_operator(op).unwrap_or(*op), usable, true)
|
||||
}
|
||||
}
|
||||
(Expr::Column { table, column, .. }, other) if *table == table_index => {
|
||||
(
|
||||
column,
|
||||
op,
|
||||
expr_is_ready(other), // literal / earlier‑table / deterministic func ?
|
||||
false,
|
||||
)
|
||||
}
|
||||
(other, Expr::Column { table, column, .. }) if *table == table_index => (
|
||||
column,
|
||||
&reverse_operator(op).unwrap_or(*op),
|
||||
expr_is_ready(other),
|
||||
true,
|
||||
),
|
||||
|
||||
_ => return None, // does not involve the virtual table at all
|
||||
};
|
||||
|
||||
Some(ConstraintInfo {
|
||||
column_index: *vcol_idx as u32,
|
||||
op: to_ext_constraint_op(op_for_vtab)?,
|
||||
usable,
|
||||
plan_info: ConstraintInfo::pack_plan_info(pred_idx as u32, is_rhs),
|
||||
})
|
||||
}
|
||||
/// The loop index where to evaluate the condition.
|
||||
/// For example, in `SELECT * FROM u JOIN p WHERE u.id = 5`, the condition can already be evaluated at the first loop (idx 0),
|
||||
/// because that is the rightmost table that it references.
|
||||
@@ -136,7 +267,7 @@ pub struct SelectPlan {
|
||||
/// group by clause
|
||||
pub group_by: Option<GroupBy>,
|
||||
/// order by clause
|
||||
pub order_by: Option<Vec<(ast::Expr, Direction)>>,
|
||||
pub order_by: Option<Vec<(ast::Expr, SortOrder)>>,
|
||||
/// all the aggregates collected from the result columns, order by, and (TODO) having clauses
|
||||
pub aggregates: Vec<Aggregate>,
|
||||
/// limit clause
|
||||
@@ -159,13 +290,15 @@ pub struct DeletePlan {
|
||||
/// where clause split into a vec at 'AND' boundaries.
|
||||
pub where_clause: Vec<WhereTerm>,
|
||||
/// order by clause
|
||||
pub order_by: Option<Vec<(ast::Expr, Direction)>>,
|
||||
pub order_by: Option<Vec<(ast::Expr, SortOrder)>>,
|
||||
/// limit clause
|
||||
pub limit: Option<isize>,
|
||||
/// offset clause
|
||||
pub offset: Option<isize>,
|
||||
/// query contains a constant condition that is always false
|
||||
pub contains_constant_false_condition: bool,
|
||||
/// Indexes that must be updated by the delete operation.
|
||||
pub indexes: Vec<Arc<Index>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -175,13 +308,14 @@ pub struct UpdatePlan {
|
||||
// (colum index, new value) pairs
|
||||
pub set_clauses: Vec<(usize, ast::Expr)>,
|
||||
pub where_clause: Vec<WhereTerm>,
|
||||
pub order_by: Option<Vec<(ast::Expr, Direction)>>,
|
||||
// TODO: support OFFSET
|
||||
pub order_by: Option<Vec<(ast::Expr, SortOrder)>>,
|
||||
pub limit: Option<isize>,
|
||||
pub offset: Option<isize>,
|
||||
// TODO: optional RETURNING clause
|
||||
pub returning: Option<Vec<ResultSetColumn>>,
|
||||
// whether the WHERE clause is always false
|
||||
pub contains_constant_false_condition: bool,
|
||||
pub indexes_to_update: Vec<Arc<Index>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -253,18 +387,54 @@ pub struct TableReference {
|
||||
pub identifier: String,
|
||||
/// The join info for this table reference, if it is the right side of a join (which all except the first table reference have)
|
||||
pub join_info: Option<JoinInfo>,
|
||||
/// Bitmask of columns that are referenced in the query.
|
||||
/// Used to decide whether a covering index can be used.
|
||||
pub col_used_mask: ColumnUsedMask,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[repr(transparent)]
|
||||
pub struct ColumnUsedMask(u128);
|
||||
|
||||
impl ColumnUsedMask {
|
||||
pub fn new() -> Self {
|
||||
Self(0)
|
||||
}
|
||||
|
||||
pub fn set(&mut self, index: usize) {
|
||||
assert!(
|
||||
index < 128,
|
||||
"ColumnUsedMask only supports up to 128 columns"
|
||||
);
|
||||
self.0 |= 1 << index;
|
||||
}
|
||||
|
||||
pub fn get(&self, index: usize) -> bool {
|
||||
assert!(
|
||||
index < 128,
|
||||
"ColumnUsedMask only supports up to 128 columns"
|
||||
);
|
||||
self.0 & (1 << index) != 0
|
||||
}
|
||||
|
||||
pub fn contains_all_set_bits_of(&self, other: &Self) -> bool {
|
||||
self.0 & other.0 == other.0
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0 == 0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Operation {
|
||||
// Scan operation
|
||||
// This operation is used to scan a table.
|
||||
// The iter_dir are uset to indicate the direction of the iterator.
|
||||
// The use of Option for iter_dir is aimed at implementing a conservative optimization strategy: it only pushes
|
||||
// iter_dir down to Scan when iter_dir is None, to prevent potential result set errors caused by multiple
|
||||
// assignments. for more detailed discussions, please refer to https://github.com/tursodatabase/limbo/pull/376
|
||||
// The iter_dir is used to indicate the direction of the iterator.
|
||||
Scan {
|
||||
iter_dir: Option<IterationDirection>,
|
||||
iter_dir: IterationDirection,
|
||||
/// The index that we are using to scan the table, if any.
|
||||
index: Option<Arc<Index>>,
|
||||
},
|
||||
// Search operation
|
||||
// This operation is used to search for a row in a table using an index
|
||||
@@ -279,6 +449,17 @@ pub enum Operation {
|
||||
},
|
||||
}
|
||||
|
||||
impl Operation {
|
||||
pub fn index(&self) -> Option<&Arc<Index>> {
|
||||
match self {
|
||||
Operation::Scan { index, .. } => index.as_ref(),
|
||||
Operation::Search(Search::RowidEq { .. }) => None,
|
||||
Operation::Search(Search::Seek { index, .. }) => index.as_ref(),
|
||||
Operation::Subquery { .. } => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TableReference {
|
||||
/// Returns the btree table for this table reference, if it is a BTreeTable.
|
||||
pub fn btree(&self) -> Option<Rc<BTreeTable>> {
|
||||
@@ -300,7 +481,7 @@ impl TableReference {
|
||||
plan.result_columns
|
||||
.iter()
|
||||
.map(|rc| Column {
|
||||
name: rc.name(&plan.table_references).map(String::clone),
|
||||
name: rc.name(&plan.table_references).map(String::from),
|
||||
ty: Type::Text, // FIXME: infer proper type
|
||||
ty_str: "TEXT".to_string(),
|
||||
is_rowid_alias: false,
|
||||
@@ -318,12 +499,172 @@ impl TableReference {
|
||||
table,
|
||||
identifier: identifier.clone(),
|
||||
join_info,
|
||||
col_used_mask: ColumnUsedMask::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn columns(&self) -> &[Column] {
|
||||
self.table.columns()
|
||||
}
|
||||
|
||||
/// Mark a column as used in the query.
|
||||
/// This is used to determine whether a covering index can be used.
|
||||
pub fn mark_column_used(&mut self, index: usize) {
|
||||
self.col_used_mask.set(index);
|
||||
}
|
||||
|
||||
/// Open the necessary cursors for this table reference.
|
||||
/// Generally a table cursor is always opened unless a SELECT query can use a covering index.
|
||||
/// An index cursor is opened if an index is used in any way for reading data from the table.
|
||||
pub fn open_cursors(
|
||||
&self,
|
||||
program: &mut ProgramBuilder,
|
||||
mode: OperationMode,
|
||||
) -> Result<(Option<CursorID>, Option<CursorID>)> {
|
||||
let index = self.op.index();
|
||||
match &self.table {
|
||||
Table::BTree(btree) => {
|
||||
let use_covering_index = self.utilizes_covering_index();
|
||||
let index_is_ephemeral = index.map_or(false, |index| index.ephemeral);
|
||||
let table_not_required =
|
||||
OperationMode::SELECT == mode && use_covering_index && !index_is_ephemeral;
|
||||
let table_cursor_id = if table_not_required {
|
||||
None
|
||||
} else {
|
||||
Some(program.alloc_cursor_id(
|
||||
Some(self.identifier.clone()),
|
||||
CursorType::BTreeTable(btree.clone()),
|
||||
))
|
||||
};
|
||||
let index_cursor_id = if let Some(index) = index {
|
||||
Some(program.alloc_cursor_id(
|
||||
Some(index.name.clone()),
|
||||
CursorType::BTreeIndex(index.clone()),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok((table_cursor_id, index_cursor_id))
|
||||
}
|
||||
Table::Virtual(virtual_table) => {
|
||||
let table_cursor_id = Some(program.alloc_cursor_id(
|
||||
Some(self.identifier.clone()),
|
||||
CursorType::VirtualTable(virtual_table.clone()),
|
||||
));
|
||||
let index_cursor_id = None;
|
||||
Ok((table_cursor_id, index_cursor_id))
|
||||
}
|
||||
Table::Pseudo(_) => Ok((None, None)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve the already opened cursors for this table reference.
|
||||
pub fn resolve_cursors(
|
||||
&self,
|
||||
program: &mut ProgramBuilder,
|
||||
) -> Result<(Option<CursorID>, Option<CursorID>)> {
|
||||
let index = self.op.index();
|
||||
let table_cursor_id = program.resolve_cursor_id_safe(&self.identifier);
|
||||
let index_cursor_id = index.map(|index| program.resolve_cursor_id(&index.name));
|
||||
Ok((table_cursor_id, index_cursor_id))
|
||||
}
|
||||
|
||||
/// Returns true if a given index is a covering index for this [TableReference].
|
||||
pub fn index_is_covering(&self, index: &Index) -> bool {
|
||||
let Table::BTree(btree) = &self.table else {
|
||||
return false;
|
||||
};
|
||||
if self.col_used_mask.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let mut index_cols_mask = ColumnUsedMask::new();
|
||||
for col in index.columns.iter() {
|
||||
index_cols_mask.set(col.pos_in_table);
|
||||
}
|
||||
|
||||
// If a table has a rowid (i.e. is not a WITHOUT ROWID table), the index is guaranteed to contain the rowid as well.
|
||||
if btree.has_rowid {
|
||||
if let Some(pos_of_rowid_alias_col) = btree.get_rowid_alias_column().map(|(pos, _)| pos)
|
||||
{
|
||||
let mut empty_mask = ColumnUsedMask::new();
|
||||
empty_mask.set(pos_of_rowid_alias_col);
|
||||
if self.col_used_mask == empty_mask {
|
||||
// However if the index would be ONLY used for the rowid, then let's not bother using it to cover the query.
|
||||
// Example: if the query is SELECT id FROM t, and id is a rowid alias, then let's rather just scan the table
|
||||
// instead of an index.
|
||||
return false;
|
||||
}
|
||||
index_cols_mask.set(pos_of_rowid_alias_col);
|
||||
}
|
||||
}
|
||||
|
||||
index_cols_mask.contains_all_set_bits_of(&self.col_used_mask)
|
||||
}
|
||||
|
||||
/// Returns true if the index selected for use with this [TableReference] is a covering index,
|
||||
/// meaning that it contains all the columns that are referenced in the query.
|
||||
pub fn utilizes_covering_index(&self) -> bool {
|
||||
let Some(index) = self.op.index() else {
|
||||
return false;
|
||||
};
|
||||
self.index_is_covering(index.as_ref())
|
||||
}
|
||||
|
||||
pub fn column_is_used(&self, index: usize) -> bool {
|
||||
self.col_used_mask.get(index)
|
||||
}
|
||||
}
|
||||
|
||||
/// A definition of a rowid/index search.
|
||||
///
|
||||
/// [SeekKey] is the condition that is used to seek to a specific row in a table/index.
|
||||
/// [TerminationKey] is the condition that is used to terminate the search after a seek.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SeekDef {
|
||||
/// The key to use when seeking and when terminating the scan that follows the seek.
|
||||
/// For example, given:
|
||||
/// - CREATE INDEX i ON t (x, y desc)
|
||||
/// - SELECT * FROM t WHERE x = 1 AND y >= 30
|
||||
/// The key is [(1, ASC), (30, DESC)]
|
||||
pub key: Vec<(ast::Expr, SortOrder)>,
|
||||
/// The condition to use when seeking. See [SeekKey] for more details.
|
||||
pub seek: Option<SeekKey>,
|
||||
/// The condition to use when terminating the scan that follows the seek. See [TerminationKey] for more details.
|
||||
pub termination: Option<TerminationKey>,
|
||||
/// The direction of the scan that follows the seek.
|
||||
pub iter_dir: IterationDirection,
|
||||
}
|
||||
|
||||
/// A condition to use when seeking.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SeekKey {
|
||||
/// How many columns from [SeekDef::key] are used in seeking.
|
||||
pub len: usize,
|
||||
/// Whether to NULL pad the last column of the seek key to match the length of [SeekDef::key].
|
||||
/// The reason it is done is that sometimes our full index key is not used in seeking,
|
||||
/// but we want to find the lowest value that matches the non-null prefix of the key.
|
||||
/// For example, given:
|
||||
/// - CREATE INDEX i ON t (x, y)
|
||||
/// - SELECT * FROM t WHERE x = 1 AND y < 30
|
||||
/// We want to seek to the first row where x = 1, and then iterate forwards.
|
||||
/// In this case, the seek key is GT(1, NULL) since NULL is always LT in index key comparisons.
|
||||
/// We can't use just GT(1) because in index key comparisons, only the given number of columns are compared,
|
||||
/// so this means any index keys with (x=1) will compare equal, e.g. (x=1, y=usize::MAX) will compare equal to the seek key (x:1)
|
||||
pub null_pad: bool,
|
||||
/// The comparison operator to use when seeking.
|
||||
pub op: SeekOp,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// A condition to use when terminating the scan that follows a seek.
|
||||
pub struct TerminationKey {
|
||||
/// How many columns from [SeekDef::key] are used in terminating the scan that follows the seek.
|
||||
pub len: usize,
|
||||
/// Whether to NULL pad the last column of the termination key to match the length of [SeekDef::key].
|
||||
/// See [SeekKey::null_pad].
|
||||
pub null_pad: bool,
|
||||
/// The comparison operator to use when terminating the scan that follows the seek.
|
||||
pub op: SeekOp,
|
||||
}
|
||||
|
||||
/// An enum that represents a search operation that can be used to search for a row in a table using an index
|
||||
@@ -333,32 +674,11 @@ impl TableReference {
|
||||
pub enum Search {
|
||||
/// A rowid equality point lookup. This is a special case that uses the SeekRowid bytecode instruction and does not loop.
|
||||
RowidEq { cmp_expr: WhereTerm },
|
||||
/// A rowid search. Uses bytecode instructions like SeekGT, SeekGE etc.
|
||||
RowidSearch {
|
||||
cmp_op: ast::Operator,
|
||||
cmp_expr: WhereTerm,
|
||||
/// A search on a table btree (via `rowid`) or a secondary index search. Uses bytecode instructions like SeekGE, SeekGT etc.
|
||||
Seek {
|
||||
index: Option<Arc<Index>>,
|
||||
seek_def: SeekDef,
|
||||
},
|
||||
/// A secondary index search. Uses bytecode instructions like SeekGE, SeekGT etc.
|
||||
IndexSearch {
|
||||
index: Arc<Index>,
|
||||
cmp_op: ast::Operator,
|
||||
cmp_expr: WhereTerm,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Direction {
|
||||
Ascending,
|
||||
Descending,
|
||||
}
|
||||
|
||||
impl Display for Direction {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Direction::Ascending => write!(f, "ASC"),
|
||||
Direction::Descending => write!(f, "DESC"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -419,14 +739,16 @@ impl Display for SelectPlan {
|
||||
writeln!(f, "{}SCAN {}", indent, table_name)?;
|
||||
}
|
||||
Operation::Search(search) => match search {
|
||||
Search::RowidEq { .. } | Search::RowidSearch { .. } => {
|
||||
Search::RowidEq { .. } | Search::Seek { index: None, .. } => {
|
||||
writeln!(
|
||||
f,
|
||||
"{}SEARCH {} USING INTEGER PRIMARY KEY (rowid=?)",
|
||||
indent, reference.identifier
|
||||
)?;
|
||||
}
|
||||
Search::IndexSearch { index, .. } => {
|
||||
Search::Seek {
|
||||
index: Some(index), ..
|
||||
} => {
|
||||
writeln!(
|
||||
f,
|
||||
"{}SEARCH {} USING INDEX {}",
|
||||
@@ -508,14 +830,16 @@ impl fmt::Display for UpdatePlan {
|
||||
}
|
||||
}
|
||||
Operation::Search(search) => match search {
|
||||
Search::RowidEq { .. } | Search::RowidSearch { .. } => {
|
||||
Search::RowidEq { .. } | Search::Seek { index: None, .. } => {
|
||||
writeln!(
|
||||
f,
|
||||
"{}SEARCH {} USING INTEGER PRIMARY KEY (rowid=?)",
|
||||
indent, reference.identifier
|
||||
)?;
|
||||
}
|
||||
Search::IndexSearch { index, .. } => {
|
||||
Search::Seek {
|
||||
index: Some(index), ..
|
||||
} => {
|
||||
writeln!(
|
||||
f,
|
||||
"{}SEARCH {} USING INDEX {}",
|
||||
@@ -534,7 +858,16 @@ impl fmt::Display for UpdatePlan {
|
||||
if let Some(order_by) = &self.order_by {
|
||||
writeln!(f, "ORDER BY:")?;
|
||||
for (expr, dir) in order_by {
|
||||
writeln!(f, " - {} {}", expr, dir)?;
|
||||
writeln!(
|
||||
f,
|
||||
" - {} {}",
|
||||
expr,
|
||||
if *dir == SortOrder::Asc {
|
||||
"ASC"
|
||||
} else {
|
||||
"DESC"
|
||||
}
|
||||
)?;
|
||||
}
|
||||
}
|
||||
if let Some(limit) = self.limit {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::{
|
||||
plan::{
|
||||
Aggregate, EvalAt, JoinInfo, Operation, Plan, ResultSetColumn, SelectPlan, SelectQueryType,
|
||||
TableReference, WhereTerm,
|
||||
Aggregate, ColumnUsedMask, EvalAt, IterationDirection, JoinInfo, Operation, Plan,
|
||||
ResultSetColumn, SelectPlan, SelectQueryType, TableReference, WhereTerm,
|
||||
},
|
||||
select::prepare_select_plan,
|
||||
SymbolTable,
|
||||
@@ -85,7 +85,7 @@ pub fn resolve_aggregates(expr: &Expr, aggs: &mut Vec<Aggregate>) -> bool {
|
||||
|
||||
pub fn bind_column_references(
|
||||
expr: &mut Expr,
|
||||
referenced_tables: &[TableReference],
|
||||
referenced_tables: &mut [TableReference],
|
||||
result_columns: Option<&[ResultSetColumn]>,
|
||||
) -> Result<()> {
|
||||
match expr {
|
||||
@@ -128,6 +128,7 @@ pub fn bind_column_references(
|
||||
column: col_idx,
|
||||
is_rowid_alias,
|
||||
};
|
||||
referenced_tables[tbl_idx].mark_column_used(col_idx);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -178,6 +179,7 @@ pub fn bind_column_references(
|
||||
column: col_idx.unwrap(),
|
||||
is_rowid_alias: col.is_rowid_alias,
|
||||
};
|
||||
referenced_tables[tbl_idx].mark_column_used(col_idx.unwrap());
|
||||
Ok(())
|
||||
}
|
||||
Expr::Between {
|
||||
@@ -320,10 +322,14 @@ fn parse_from_clause_table<'a>(
|
||||
));
|
||||
};
|
||||
scope.tables.push(TableReference {
|
||||
op: Operation::Scan { iter_dir: None },
|
||||
op: Operation::Scan {
|
||||
iter_dir: IterationDirection::Forwards,
|
||||
index: None,
|
||||
},
|
||||
table: tbl_ref,
|
||||
identifier: alias.unwrap_or(normalized_qualified_name),
|
||||
join_info: None,
|
||||
col_used_mask: ColumnUsedMask::new(),
|
||||
});
|
||||
return Ok(());
|
||||
};
|
||||
@@ -399,10 +405,14 @@ fn parse_from_clause_table<'a>(
|
||||
.unwrap_or(normalized_name.to_string());
|
||||
|
||||
scope.tables.push(TableReference {
|
||||
op: Operation::Scan { iter_dir: None },
|
||||
op: Operation::Scan {
|
||||
iter_dir: IterationDirection::Forwards,
|
||||
index: None,
|
||||
},
|
||||
join_info: None,
|
||||
table: Table::Virtual(vtab),
|
||||
identifier: alias,
|
||||
col_used_mask: ColumnUsedMask::new(),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
@@ -533,7 +543,7 @@ pub fn parse_from<'a>(
|
||||
|
||||
pub fn parse_where(
|
||||
where_clause: Option<Expr>,
|
||||
table_references: &[TableReference],
|
||||
table_references: &mut [TableReference],
|
||||
result_columns: Option<&[ResultSetColumn]>,
|
||||
out_where_clause: &mut Vec<WhereTerm>,
|
||||
) -> Result<()> {
|
||||
@@ -564,7 +574,7 @@ pub fn parse_where(
|
||||
For expressions not referencing any tables (e.g. constants), this is before the main loop is
|
||||
opened, because they do not need any table data.
|
||||
*/
|
||||
fn determine_where_to_eval_expr<'a>(predicate: &'a ast::Expr) -> Result<EvalAt> {
|
||||
pub fn determine_where_to_eval_expr<'a>(predicate: &'a ast::Expr) -> Result<EvalAt> {
|
||||
let mut eval_at: EvalAt = EvalAt::BeforeLoop;
|
||||
match predicate {
|
||||
ast::Expr::Binary(e1, _, e2) => {
|
||||
@@ -752,7 +762,7 @@ fn parse_join<'a>(
|
||||
let mut preds = vec![];
|
||||
break_predicate_at_and_boundaries(expr, &mut preds);
|
||||
for predicate in preds.iter_mut() {
|
||||
bind_column_references(predicate, &scope.tables, None)?;
|
||||
bind_column_references(predicate, &mut scope.tables, None)?;
|
||||
}
|
||||
for pred in preds {
|
||||
let cur_table_idx = scope.tables.len() - 1;
|
||||
@@ -826,6 +836,11 @@ fn parse_join<'a>(
|
||||
is_rowid_alias: right_col.is_rowid_alias,
|
||||
}),
|
||||
);
|
||||
|
||||
let left_table = scope.tables.get_mut(left_table_idx).unwrap();
|
||||
left_table.mark_column_used(left_col_idx);
|
||||
let right_table = scope.tables.get_mut(cur_table_idx).unwrap();
|
||||
right_table.mark_column_used(right_col_idx);
|
||||
let eval_at = if outer {
|
||||
EvalAt::Loop(cur_table_idx)
|
||||
} else {
|
||||
@@ -850,30 +865,33 @@ fn parse_join<'a>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn parse_limit(limit: Limit) -> Result<(Option<isize>, Option<isize>)> {
|
||||
let offset_val = match limit.offset {
|
||||
pub fn parse_limit(limit: &Limit) -> Result<(Option<isize>, Option<isize>)> {
|
||||
let offset_val = match &limit.offset {
|
||||
Some(offset_expr) => match offset_expr {
|
||||
Expr::Literal(ast::Literal::Numeric(n)) => n.parse().ok(),
|
||||
// If OFFSET is negative, the result is as if OFFSET is zero
|
||||
Expr::Unary(UnaryOperator::Negative, expr) => match *expr {
|
||||
Expr::Literal(ast::Literal::Numeric(n)) => n.parse::<isize>().ok().map(|num| -num),
|
||||
_ => crate::bail_parse_error!("Invalid OFFSET clause"),
|
||||
},
|
||||
Expr::Unary(UnaryOperator::Negative, expr) => {
|
||||
if let Expr::Literal(ast::Literal::Numeric(ref n)) = &**expr {
|
||||
n.parse::<isize>().ok().map(|num| -num)
|
||||
} else {
|
||||
crate::bail_parse_error!("Invalid OFFSET clause");
|
||||
}
|
||||
}
|
||||
_ => crate::bail_parse_error!("Invalid OFFSET clause"),
|
||||
},
|
||||
None => Some(0),
|
||||
};
|
||||
|
||||
if let Expr::Literal(ast::Literal::Numeric(n)) = limit.expr {
|
||||
if let Expr::Literal(ast::Literal::Numeric(n)) = &limit.expr {
|
||||
Ok((n.parse().ok(), offset_val))
|
||||
} else if let Expr::Unary(UnaryOperator::Negative, expr) = limit.expr {
|
||||
if let Expr::Literal(ast::Literal::Numeric(n)) = *expr {
|
||||
} else if let Expr::Unary(UnaryOperator::Negative, expr) = &limit.expr {
|
||||
if let Expr::Literal(ast::Literal::Numeric(n)) = &**expr {
|
||||
let limit_val = n.parse::<isize>().ok().map(|num| -num);
|
||||
Ok((limit_val, offset_val))
|
||||
} else {
|
||||
crate::bail_parse_error!("Invalid LIMIT clause");
|
||||
}
|
||||
} else if let Expr::Id(id) = limit.expr {
|
||||
} else if let Expr::Id(id) = &limit.expr {
|
||||
if id.0.eq_ignore_ascii_case("true") {
|
||||
Ok((Some(1), offset_val))
|
||||
} else if id.0.eq_ignore_ascii_case("false") {
|
||||
|
||||
@@ -29,7 +29,7 @@ fn list_pragmas(
|
||||
}
|
||||
|
||||
program.emit_halt();
|
||||
program.resolve_label(init_label, program.offset());
|
||||
program.preassign_label_to_next_insn(init_label);
|
||||
program.emit_constant_insns();
|
||||
program.emit_goto(start_offset);
|
||||
}
|
||||
@@ -104,7 +104,7 @@ pub fn translate_pragma(
|
||||
},
|
||||
};
|
||||
program.emit_halt();
|
||||
program.resolve_label(init_label, program.offset());
|
||||
program.preassign_label_to_next_insn(init_label);
|
||||
program.emit_transaction(write);
|
||||
program.emit_constant_insns();
|
||||
program.emit_goto(start_offset);
|
||||
@@ -154,12 +154,19 @@ fn update_pragma(
|
||||
// TODO: Implement updating user_version
|
||||
todo!("updating user_version not yet implemented")
|
||||
}
|
||||
PragmaName::SchemaVersion => {
|
||||
// TODO: Implement updating schema_version
|
||||
todo!("updating schema_version not yet implemented")
|
||||
}
|
||||
PragmaName::TableInfo => {
|
||||
// because we need control over the write parameter for the transaction,
|
||||
// this should be unreachable. We have to force-call query_pragma before
|
||||
// getting here
|
||||
unreachable!();
|
||||
}
|
||||
PragmaName::PageSize => {
|
||||
todo!("updating page_size is not yet implemented")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,7 +256,6 @@ fn query_pragma(
|
||||
}
|
||||
}
|
||||
PragmaName::UserVersion => {
|
||||
program.emit_transaction(false);
|
||||
program.emit_insn(Insn::ReadCookie {
|
||||
db: 0,
|
||||
dest: register,
|
||||
@@ -257,6 +263,18 @@ fn query_pragma(
|
||||
});
|
||||
program.emit_result_row(register, 1);
|
||||
}
|
||||
PragmaName::SchemaVersion => {
|
||||
program.emit_insn(Insn::ReadCookie {
|
||||
db: 0,
|
||||
dest: register,
|
||||
cookie: Cookie::SchemaVersion,
|
||||
});
|
||||
program.emit_result_row(register, 1);
|
||||
}
|
||||
PragmaName::PageSize => {
|
||||
program.emit_int(database_header.lock().get_page_size().into(), register);
|
||||
program.emit_result_row(register, 1);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -25,7 +25,16 @@ pub fn emit_select_result(
|
||||
}
|
||||
|
||||
let start_reg = t_ctx.reg_result_cols_start.unwrap();
|
||||
for (i, rc) in plan.result_columns.iter().enumerate() {
|
||||
for (i, rc) in plan.result_columns.iter().enumerate().filter(|(_, rc)| {
|
||||
// For aggregate queries, we handle columns differently; example: select id, first_name, sum(age) from users limit 1;
|
||||
// 1. Columns with aggregates (e.g., sum(age)) are computed in each iteration of aggregation
|
||||
// 2. Non-aggregate columns (e.g., id, first_name) are only computed once in the first iteration
|
||||
// This filter ensures we only emit expressions for non aggregate columns once,
|
||||
// preserving previously calculated values while updating aggregate results
|
||||
// For all other queries where reg_nonagg_emit_once_flag is none we do nothing.
|
||||
t_ctx.reg_nonagg_emit_once_flag.is_some() && rc.contains_aggregates
|
||||
|| t_ctx.reg_nonagg_emit_once_flag.is_none()
|
||||
}) {
|
||||
let reg = start_reg + i;
|
||||
translate_expr(
|
||||
program,
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
use std::fmt::Display;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::ast;
|
||||
use crate::ext::VTabImpl;
|
||||
use crate::schema::Schema;
|
||||
use crate::schema::Table;
|
||||
use crate::storage::pager::CreateBTreeFlags;
|
||||
use crate::translate::ProgramBuilder;
|
||||
use crate::translate::ProgramBuilderOpts;
|
||||
use crate::translate::QueryMode;
|
||||
@@ -9,8 +13,10 @@ use crate::util::PRIMARY_KEY_AUTOMATIC_INDEX_NAME_PREFIX;
|
||||
use crate::vdbe::builder::CursorType;
|
||||
use crate::vdbe::insn::{CmpInsFlags, Insn};
|
||||
use crate::LimboError;
|
||||
use crate::SymbolTable;
|
||||
use crate::{bail_parse_error, Result};
|
||||
|
||||
use limbo_ext::VTabKind;
|
||||
use limbo_sqlite3_parser::ast::{fmt::ToTokens, CreateVirtualTable};
|
||||
|
||||
pub fn translate_create_table(
|
||||
@@ -35,7 +41,7 @@ pub fn translate_create_table(
|
||||
let init_label = program.emit_init();
|
||||
let start_offset = program.offset();
|
||||
program.emit_halt();
|
||||
program.resolve_label(init_label, program.offset());
|
||||
program.preassign_label_to_next_insn(init_label);
|
||||
program.emit_transaction(true);
|
||||
program.emit_constant_insns();
|
||||
program.emit_goto(start_offset);
|
||||
@@ -60,7 +66,7 @@ pub fn translate_create_table(
|
||||
program.emit_insn(Insn::CreateBtree {
|
||||
db: 0,
|
||||
root: table_root_reg,
|
||||
flags: 1, // Table leaf page
|
||||
flags: CreateBTreeFlags::new_table(),
|
||||
});
|
||||
|
||||
// Create an automatic index B-tree if needed
|
||||
@@ -92,7 +98,7 @@ pub fn translate_create_table(
|
||||
program.emit_insn(Insn::CreateBtree {
|
||||
db: 0,
|
||||
root: index_root_reg,
|
||||
flags: 2, // Index leaf page
|
||||
flags: CreateBTreeFlags::new_index(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -101,11 +107,10 @@ pub fn translate_create_table(
|
||||
Some(SQLITE_TABLEID.to_owned()),
|
||||
CursorType::BTreeTable(table.clone()),
|
||||
);
|
||||
program.emit_insn(Insn::OpenWriteAsync {
|
||||
program.emit_insn(Insn::OpenWrite {
|
||||
cursor_id: sqlite_schema_cursor_id,
|
||||
root_page: 1,
|
||||
root_page: 1usize.into(),
|
||||
});
|
||||
program.emit_insn(Insn::OpenWriteAwait {});
|
||||
|
||||
// Add the table entry to sqlite_schema
|
||||
emit_schema_entry(
|
||||
@@ -147,7 +152,7 @@ pub fn translate_create_table(
|
||||
|
||||
// TODO: SqlExec
|
||||
program.emit_halt();
|
||||
program.resolve_label(init_label, program.offset());
|
||||
program.preassign_label_to_next_insn(init_label);
|
||||
program.emit_transaction(true);
|
||||
program.emit_constant_insns();
|
||||
program.emit_goto(start_offset);
|
||||
@@ -155,8 +160,8 @@ pub fn translate_create_table(
|
||||
Ok(program)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum SchemaEntryType {
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum SchemaEntryType {
|
||||
Table,
|
||||
Index,
|
||||
}
|
||||
@@ -169,9 +174,9 @@ impl SchemaEntryType {
|
||||
}
|
||||
}
|
||||
}
|
||||
const SQLITE_TABLEID: &str = "sqlite_schema";
|
||||
pub const SQLITE_TABLEID: &str = "sqlite_schema";
|
||||
|
||||
fn emit_schema_entry(
|
||||
pub fn emit_schema_entry(
|
||||
program: &mut ProgramBuilder,
|
||||
sqlite_schema_cursor_id: usize,
|
||||
entry_type: SchemaEntryType,
|
||||
@@ -219,15 +224,12 @@ fn emit_schema_entry(
|
||||
dest_reg: record_reg,
|
||||
});
|
||||
|
||||
program.emit_insn(Insn::InsertAsync {
|
||||
program.emit_insn(Insn::Insert {
|
||||
cursor: sqlite_schema_cursor_id,
|
||||
key_reg: rowid_reg,
|
||||
record_reg,
|
||||
flag: 0,
|
||||
});
|
||||
program.emit_insn(Insn::InsertAwait {
|
||||
cursor_id: sqlite_schema_cursor_id,
|
||||
});
|
||||
}
|
||||
|
||||
struct PrimaryKeyColumnInfo<'a> {
|
||||
@@ -398,7 +400,7 @@ fn create_table_body_to_str(tbl_name: &ast::QualifiedName, body: &ast::CreateTab
|
||||
sql
|
||||
}
|
||||
|
||||
fn create_vtable_body_to_str(vtab: &CreateVirtualTable) -> String {
|
||||
fn create_vtable_body_to_str(vtab: &CreateVirtualTable, module: Rc<VTabImpl>) -> String {
|
||||
let args = if let Some(args) = &vtab.args {
|
||||
args.iter()
|
||||
.map(|arg| arg.to_string())
|
||||
@@ -412,8 +414,25 @@ fn create_vtable_body_to_str(vtab: &CreateVirtualTable) -> String {
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let ext_args = vtab
|
||||
.args
|
||||
.as_ref()
|
||||
.unwrap_or(&vec![])
|
||||
.iter()
|
||||
.map(|a| limbo_ext::Value::from_text(a.to_string()))
|
||||
.collect::<Vec<_>>();
|
||||
let schema = module
|
||||
.implementation
|
||||
.init_schema(ext_args)
|
||||
.unwrap_or_default();
|
||||
let vtab_args = if let Some(first_paren) = schema.find('(') {
|
||||
let closing_paren = schema.rfind(')').unwrap_or_default();
|
||||
&schema[first_paren..=closing_paren]
|
||||
} else {
|
||||
"()"
|
||||
};
|
||||
format!(
|
||||
"CREATE VIRTUAL TABLE {} {} USING {}{}",
|
||||
"CREATE VIRTUAL TABLE {} {} USING {}{}\n /*{}{}*/",
|
||||
vtab.tbl_name.name.0,
|
||||
if_not_exists,
|
||||
vtab.module_name.0,
|
||||
@@ -421,7 +440,9 @@ fn create_vtable_body_to_str(vtab: &CreateVirtualTable) -> String {
|
||||
String::new()
|
||||
} else {
|
||||
format!("({})", args)
|
||||
}
|
||||
},
|
||||
vtab.tbl_name.name.0,
|
||||
vtab_args
|
||||
)
|
||||
}
|
||||
|
||||
@@ -429,6 +450,7 @@ pub fn translate_create_virtual_table(
|
||||
vtab: CreateVirtualTable,
|
||||
schema: &Schema,
|
||||
query_mode: QueryMode,
|
||||
syms: &SymbolTable,
|
||||
) -> Result<ProgramBuilder> {
|
||||
let ast::CreateVirtualTable {
|
||||
if_not_exists,
|
||||
@@ -440,7 +462,12 @@ pub fn translate_create_virtual_table(
|
||||
let table_name = tbl_name.name.0.clone();
|
||||
let module_name_str = module_name.0.clone();
|
||||
let args_vec = args.clone().unwrap_or_default();
|
||||
|
||||
let Some(vtab_module) = syms.vtab_modules.get(&module_name_str) else {
|
||||
bail_parse_error!("no such module: {}", module_name_str);
|
||||
};
|
||||
if !vtab_module.module_kind.eq(&VTabKind::VirtualTable) {
|
||||
bail_parse_error!("module {} is not a virtual table", module_name_str);
|
||||
};
|
||||
if schema.get_table(&table_name).is_some() && *if_not_exists {
|
||||
let mut program = ProgramBuilder::new(ProgramBuilderOpts {
|
||||
query_mode,
|
||||
@@ -450,7 +477,7 @@ pub fn translate_create_virtual_table(
|
||||
});
|
||||
let init_label = program.emit_init();
|
||||
program.emit_halt();
|
||||
program.resolve_label(init_label, program.offset());
|
||||
program.preassign_label_to_next_insn(init_label);
|
||||
program.emit_transaction(true);
|
||||
program.emit_constant_insns();
|
||||
return Ok(program);
|
||||
@@ -462,10 +489,10 @@ pub fn translate_create_virtual_table(
|
||||
approx_num_insns: 40,
|
||||
approx_num_labels: 2,
|
||||
});
|
||||
|
||||
let init_label = program.emit_init();
|
||||
let start_offset = program.offset();
|
||||
let module_name_reg = program.emit_string8_new_reg(module_name_str.clone());
|
||||
let table_name_reg = program.emit_string8_new_reg(table_name.clone());
|
||||
|
||||
let args_reg = if !args_vec.is_empty() {
|
||||
let args_start = program.alloc_register();
|
||||
|
||||
@@ -491,19 +518,17 @@ pub fn translate_create_virtual_table(
|
||||
table_name: table_name_reg,
|
||||
args_reg,
|
||||
});
|
||||
|
||||
let table = schema.get_btree_table(SQLITE_TABLEID).unwrap();
|
||||
let sqlite_schema_cursor_id = program.alloc_cursor_id(
|
||||
Some(SQLITE_TABLEID.to_owned()),
|
||||
CursorType::BTreeTable(table.clone()),
|
||||
);
|
||||
program.emit_insn(Insn::OpenWriteAsync {
|
||||
program.emit_insn(Insn::OpenWrite {
|
||||
cursor_id: sqlite_schema_cursor_id,
|
||||
root_page: 1,
|
||||
root_page: 1usize.into(),
|
||||
});
|
||||
program.emit_insn(Insn::OpenWriteAwait {});
|
||||
|
||||
let sql = create_vtable_body_to_str(&vtab);
|
||||
let sql = create_vtable_body_to_str(&vtab, vtab_module.clone());
|
||||
emit_schema_entry(
|
||||
&mut program,
|
||||
sqlite_schema_cursor_id,
|
||||
@@ -520,10 +545,8 @@ pub fn translate_create_virtual_table(
|
||||
where_clause: parse_schema_where_clause,
|
||||
});
|
||||
|
||||
let init_label = program.emit_init();
|
||||
let start_offset = program.offset();
|
||||
program.emit_halt();
|
||||
program.resolve_label(init_label, program.offset());
|
||||
program.preassign_label_to_next_insn(init_label);
|
||||
program.emit_transaction(true);
|
||||
program.emit_constant_insns();
|
||||
program.emit_goto(start_offset);
|
||||
@@ -543,13 +566,13 @@ pub fn translate_drop_table(
|
||||
approx_num_insns: 30,
|
||||
approx_num_labels: 1,
|
||||
});
|
||||
let table = schema.get_btree_table(tbl_name.name.0.as_str());
|
||||
let table = schema.get_table(tbl_name.name.0.as_str());
|
||||
if table.is_none() {
|
||||
if if_exists {
|
||||
let init_label = program.emit_init();
|
||||
let start_offset = program.offset();
|
||||
program.emit_halt();
|
||||
program.resolve_label(init_label, program.offset());
|
||||
program.preassign_label_to_next_insn(init_label);
|
||||
program.emit_transaction(true);
|
||||
program.emit_constant_insns();
|
||||
program.emit_goto(start_offset);
|
||||
@@ -558,6 +581,7 @@ pub fn translate_drop_table(
|
||||
}
|
||||
bail_parse_error!("No such table: {}", tbl_name.name.0.as_str());
|
||||
}
|
||||
|
||||
let table = table.unwrap(); // safe since we just checked for None
|
||||
|
||||
let init_label = program.emit_init();
|
||||
@@ -573,31 +597,27 @@ pub fn translate_drop_table(
|
||||
let row_id_reg = program.alloc_register(); // r5
|
||||
|
||||
let table_name = "sqlite_schema";
|
||||
let schema_table = schema.get_btree_table(&table_name).unwrap();
|
||||
let schema_table = schema.get_btree_table(table_name).unwrap();
|
||||
let sqlite_schema_cursor_id = program.alloc_cursor_id(
|
||||
Some(table_name.to_string()),
|
||||
CursorType::BTreeTable(schema_table.clone()),
|
||||
);
|
||||
program.emit_insn(Insn::OpenWriteAsync {
|
||||
program.emit_insn(Insn::OpenWrite {
|
||||
cursor_id: sqlite_schema_cursor_id,
|
||||
root_page: 1,
|
||||
root_page: 1usize.into(),
|
||||
});
|
||||
program.emit_insn(Insn::OpenWriteAwait {});
|
||||
|
||||
// 1. Remove all entries from the schema table related to the table we are dropping, except for triggers
|
||||
// loop to beginning of schema table
|
||||
program.emit_insn(Insn::RewindAsync {
|
||||
cursor_id: sqlite_schema_cursor_id,
|
||||
});
|
||||
let end_metadata_label = program.allocate_label();
|
||||
program.emit_insn(Insn::RewindAwait {
|
||||
let metadata_loop = program.allocate_label();
|
||||
program.emit_insn(Insn::Rewind {
|
||||
cursor_id: sqlite_schema_cursor_id,
|
||||
pc_if_empty: end_metadata_label,
|
||||
});
|
||||
program.preassign_label_to_next_insn(metadata_loop);
|
||||
|
||||
// start loop on schema table
|
||||
let metadata_loop = program.allocate_label();
|
||||
program.resolve_label(metadata_loop, program.offset());
|
||||
program.emit_insn(Insn::Column {
|
||||
cursor_id: sqlite_schema_cursor_id,
|
||||
column: 2,
|
||||
@@ -625,22 +645,16 @@ pub fn translate_drop_table(
|
||||
cursor_id: sqlite_schema_cursor_id,
|
||||
dest: row_id_reg,
|
||||
});
|
||||
program.emit_insn(Insn::DeleteAsync {
|
||||
cursor_id: sqlite_schema_cursor_id,
|
||||
});
|
||||
program.emit_insn(Insn::DeleteAwait {
|
||||
program.emit_insn(Insn::Delete {
|
||||
cursor_id: sqlite_schema_cursor_id,
|
||||
});
|
||||
|
||||
program.resolve_label(next_label, program.offset());
|
||||
program.emit_insn(Insn::NextAsync {
|
||||
cursor_id: sqlite_schema_cursor_id,
|
||||
});
|
||||
program.emit_insn(Insn::NextAwait {
|
||||
program.emit_insn(Insn::Next {
|
||||
cursor_id: sqlite_schema_cursor_id,
|
||||
pc_if_next: metadata_loop,
|
||||
});
|
||||
program.resolve_label(end_metadata_label, program.offset());
|
||||
program.preassign_label_to_next_insn(end_metadata_label);
|
||||
// end of loop on schema table
|
||||
|
||||
// 2. Destroy the indices within a loop
|
||||
@@ -663,11 +677,31 @@ pub fn translate_drop_table(
|
||||
}
|
||||
|
||||
// 3. Destroy the table structure
|
||||
program.emit_insn(Insn::Destroy {
|
||||
root: table.root_page,
|
||||
former_root_reg: 0, // no autovacuum (https://www.sqlite.org/opcode.html#Destroy)
|
||||
is_temp: 0,
|
||||
});
|
||||
match table.as_ref() {
|
||||
Table::BTree(table) => {
|
||||
program.emit_insn(Insn::Destroy {
|
||||
root: table.root_page,
|
||||
former_root_reg: 0, // no autovacuum (https://www.sqlite.org/opcode.html#Destroy)
|
||||
is_temp: 0,
|
||||
});
|
||||
}
|
||||
Table::Virtual(vtab) => {
|
||||
// From what I see, TableValuedFunction is not stored in the schema as a table.
|
||||
// But this line here below is a safeguard in case this behavior changes in the future
|
||||
// And mirrors what SQLite does.
|
||||
if matches!(vtab.kind, limbo_ext::VTabKind::TableValuedFunction) {
|
||||
return Err(crate::LimboError::ParseError(format!(
|
||||
"table {} may not be dropped",
|
||||
vtab.name
|
||||
)));
|
||||
}
|
||||
program.emit_insn(Insn::VDestroy {
|
||||
table_name: vtab.name.clone(),
|
||||
db: 0, // TODO change this for multiple databases
|
||||
});
|
||||
}
|
||||
Table::Pseudo(..) => unimplemented!(),
|
||||
};
|
||||
|
||||
let r6 = program.alloc_register();
|
||||
let r7 = program.alloc_register();
|
||||
@@ -689,7 +723,7 @@ pub fn translate_drop_table(
|
||||
|
||||
// end of the program
|
||||
program.emit_halt();
|
||||
program.resolve_label(init_label, program.offset());
|
||||
program.preassign_label_to_next_insn(init_label);
|
||||
program.emit_transaction(true);
|
||||
program.emit_constant_insns();
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use super::plan::{select_star, Operation, Search, SelectQueryType};
|
||||
use super::planner::Scope;
|
||||
use crate::function::{AggFunc, ExtFunc, Func};
|
||||
use crate::translate::optimizer::optimize_plan;
|
||||
use crate::translate::plan::{Aggregate, Direction, GroupBy, Plan, ResultSetColumn, SelectPlan};
|
||||
use crate::translate::plan::{Aggregate, GroupBy, Plan, ResultSetColumn, SelectPlan};
|
||||
use crate::translate::planner::{
|
||||
bind_column_references, break_predicate_at_and_boundaries, parse_from, parse_limit,
|
||||
parse_where, resolve_aggregates,
|
||||
@@ -104,12 +104,17 @@ pub fn prepare_select_plan<'a>(
|
||||
match column {
|
||||
ResultColumn::Star => {
|
||||
select_star(&plan.table_references, &mut plan.result_columns);
|
||||
for table in plan.table_references.iter_mut() {
|
||||
for idx in 0..table.columns().len() {
|
||||
table.mark_column_used(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
ResultColumn::TableStar(name) => {
|
||||
let name_normalized = normalize_ident(name.0.as_str());
|
||||
let referenced_table = plan
|
||||
.table_references
|
||||
.iter()
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.find(|(_, t)| t.identifier == name_normalized);
|
||||
|
||||
@@ -117,23 +122,29 @@ pub fn prepare_select_plan<'a>(
|
||||
crate::bail_parse_error!("Table {} not found", name.0);
|
||||
}
|
||||
let (table_index, table) = referenced_table.unwrap();
|
||||
for (idx, col) in table.columns().iter().enumerate() {
|
||||
let num_columns = table.columns().len();
|
||||
for idx in 0..num_columns {
|
||||
let is_rowid_alias = {
|
||||
let columns = table.columns();
|
||||
columns[idx].is_rowid_alias
|
||||
};
|
||||
plan.result_columns.push(ResultSetColumn {
|
||||
expr: ast::Expr::Column {
|
||||
database: None, // TODO: support different databases
|
||||
table: table_index,
|
||||
column: idx,
|
||||
is_rowid_alias: col.is_rowid_alias,
|
||||
is_rowid_alias,
|
||||
},
|
||||
alias: None,
|
||||
contains_aggregates: false,
|
||||
});
|
||||
table.mark_column_used(idx);
|
||||
}
|
||||
}
|
||||
ResultColumn::Expr(ref mut expr, maybe_alias) => {
|
||||
bind_column_references(
|
||||
expr,
|
||||
&plan.table_references,
|
||||
&mut plan.table_references,
|
||||
Some(&plan.result_columns),
|
||||
)?;
|
||||
match expr {
|
||||
@@ -293,7 +304,7 @@ pub fn prepare_select_plan<'a>(
|
||||
// Parse the actual WHERE clause and add its conditions to the plan WHERE clause that already contains the join conditions.
|
||||
parse_where(
|
||||
where_clause,
|
||||
&plan.table_references,
|
||||
&mut plan.table_references,
|
||||
Some(&plan.result_columns),
|
||||
&mut plan.where_clause,
|
||||
)?;
|
||||
@@ -303,7 +314,7 @@ pub fn prepare_select_plan<'a>(
|
||||
replace_column_number_with_copy_of_column_expr(expr, &plan.result_columns)?;
|
||||
bind_column_references(
|
||||
expr,
|
||||
&plan.table_references,
|
||||
&mut plan.table_references,
|
||||
Some(&plan.result_columns),
|
||||
)?;
|
||||
}
|
||||
@@ -316,7 +327,7 @@ pub fn prepare_select_plan<'a>(
|
||||
for expr in predicates.iter_mut() {
|
||||
bind_column_references(
|
||||
expr,
|
||||
&plan.table_references,
|
||||
&mut plan.table_references,
|
||||
Some(&plan.result_columns),
|
||||
)?;
|
||||
let contains_aggregates =
|
||||
@@ -352,25 +363,19 @@ pub fn prepare_select_plan<'a>(
|
||||
|
||||
bind_column_references(
|
||||
&mut o.expr,
|
||||
&plan.table_references,
|
||||
&mut plan.table_references,
|
||||
Some(&plan.result_columns),
|
||||
)?;
|
||||
resolve_aggregates(&o.expr, &mut plan.aggregates);
|
||||
|
||||
key.push((
|
||||
o.expr,
|
||||
o.order.map_or(Direction::Ascending, |o| match o {
|
||||
ast::SortOrder::Asc => Direction::Ascending,
|
||||
ast::SortOrder::Desc => Direction::Descending,
|
||||
}),
|
||||
));
|
||||
key.push((o.expr, o.order.unwrap_or(ast::SortOrder::Asc)));
|
||||
}
|
||||
plan.order_by = Some(key);
|
||||
}
|
||||
|
||||
// Parse the LIMIT/OFFSET clause
|
||||
(plan.limit, plan.offset) =
|
||||
select.limit.map_or(Ok((None, None)), |l| parse_limit(*l))?;
|
||||
select.limit.map_or(Ok((None, None)), |l| parse_limit(&l))?;
|
||||
|
||||
// Return the unoptimized query plan
|
||||
Ok(Plan::Select(plan))
|
||||
@@ -411,8 +416,8 @@ fn count_plan_required_cursors(plan: &SelectPlan) -> usize {
|
||||
.map(|t| match &t.op {
|
||||
Operation::Scan { .. } => 1,
|
||||
Operation::Search(search) => match search {
|
||||
Search::RowidEq { .. } | Search::RowidSearch { .. } => 1,
|
||||
Search::IndexSearch { .. } => 2, // btree cursor and index cursor
|
||||
Search::RowidEq { .. } => 1,
|
||||
Search::Seek { index, .. } => 1 + index.is_some() as usize,
|
||||
},
|
||||
Operation::Subquery { plan, .. } => count_plan_required_cursors(plan),
|
||||
})
|
||||
|
||||
@@ -52,7 +52,7 @@ pub fn emit_subquery<'a>(
|
||||
t_ctx: &mut TranslateCtx<'a>,
|
||||
) -> Result<usize> {
|
||||
let yield_reg = program.alloc_register();
|
||||
let coroutine_implementation_start_offset = program.offset().add(1u32);
|
||||
let coroutine_implementation_start_offset = program.allocate_label();
|
||||
match &mut plan.query_type {
|
||||
SelectQueryType::Subquery {
|
||||
yield_reg: y,
|
||||
@@ -75,6 +75,7 @@ pub fn emit_subquery<'a>(
|
||||
meta_left_joins: (0..plan.table_references.len()).map(|_| None).collect(),
|
||||
meta_sort: None,
|
||||
reg_agg_start: None,
|
||||
reg_nonagg_emit_once_flag: None,
|
||||
reg_result_cols_start: None,
|
||||
result_column_indexes_in_orderby_sorter: (0..plan.result_columns.len()).collect(),
|
||||
result_columns_to_skip_in_orderby_sorter: None,
|
||||
@@ -82,6 +83,7 @@ pub fn emit_subquery<'a>(
|
||||
reg_offset: plan.offset.map(|_| program.alloc_register()),
|
||||
reg_limit_offset_sum: plan.offset.map(|_| program.alloc_register()),
|
||||
resolver: Resolver::new(t_ctx.resolver.symbol_table),
|
||||
omit_predicates: Vec::new(),
|
||||
};
|
||||
let subquery_body_end_label = program.allocate_label();
|
||||
program.emit_insn(Insn::InitCoroutine {
|
||||
@@ -89,6 +91,7 @@ pub fn emit_subquery<'a>(
|
||||
jump_on_definition: subquery_body_end_label,
|
||||
start_offset: coroutine_implementation_start_offset,
|
||||
});
|
||||
program.preassign_label_to_next_insn(coroutine_implementation_start_offset);
|
||||
// Normally we mark each LIMIT value as a constant insn that is emitted only once, but in the case of a subquery,
|
||||
// we need to initialize it every time the subquery is run; otherwise subsequent runs of the subquery will already
|
||||
// have the LIMIT counter at 0, and will never return rows.
|
||||
@@ -101,6 +104,6 @@ pub fn emit_subquery<'a>(
|
||||
let result_column_start_reg = emit_query(program, plan, &mut metadata)?;
|
||||
program.resolve_label(end_coroutine_label, program.offset());
|
||||
program.emit_insn(Insn::EndCoroutine { yield_reg });
|
||||
program.resolve_label(subquery_body_end_label, program.offset());
|
||||
program.preassign_label_to_next_insn(subquery_body_end_label);
|
||||
Ok(result_column_start_reg)
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ pub fn translate_tx_begin(
|
||||
}
|
||||
}
|
||||
program.emit_halt();
|
||||
program.resolve_label(init_label, program.offset());
|
||||
program.preassign_label_to_next_insn(init_label);
|
||||
program.emit_goto(start_offset);
|
||||
Ok(program)
|
||||
}
|
||||
@@ -52,7 +52,7 @@ pub fn translate_tx_commit(_tx_name: Option<Name>) -> Result<ProgramBuilder> {
|
||||
rollback: false,
|
||||
});
|
||||
program.emit_halt();
|
||||
program.resolve_label(init_label, program.offset());
|
||||
program.preassign_label_to_next_insn(init_label);
|
||||
program.emit_goto(start_offset);
|
||||
Ok(program)
|
||||
}
|
||||
|
||||
@@ -11,9 +11,10 @@ use limbo_sqlite3_parser::ast::{self, Expr, ResultColumn, SortOrder, Update};
|
||||
use super::emitter::emit_program;
|
||||
use super::optimizer::optimize_plan;
|
||||
use super::plan::{
|
||||
Direction, IterationDirection, Plan, ResultSetColumn, TableReference, UpdatePlan,
|
||||
ColumnUsedMask, IterationDirection, Plan, ResultSetColumn, TableReference, UpdatePlan,
|
||||
};
|
||||
use super::planner::{bind_column_references, parse_limit, parse_where};
|
||||
use super::planner::bind_column_references;
|
||||
use super::planner::{parse_limit, parse_where};
|
||||
|
||||
/*
|
||||
* Update is simple. By default we scan the table, and for each row, we check the WHERE
|
||||
@@ -64,35 +65,50 @@ pub fn translate_update(
|
||||
}
|
||||
|
||||
pub fn prepare_update_plan(schema: &Schema, body: &mut Update) -> crate::Result<Plan> {
|
||||
if body.with.is_some() {
|
||||
bail_parse_error!("WITH clause is not supported");
|
||||
}
|
||||
if body.or_conflict.is_some() {
|
||||
bail_parse_error!("ON CONFLICT clause is not supported");
|
||||
}
|
||||
let table_name = &body.tbl_name.name;
|
||||
let table = match schema.get_table(table_name.0.as_str()) {
|
||||
Some(table) => table,
|
||||
None => bail_parse_error!("Parse error: no such table: {}", table_name),
|
||||
};
|
||||
let Some(btree_table) = table.btree() else {
|
||||
bail_parse_error!("Error: {} is not a btree table", table_name);
|
||||
};
|
||||
let iter_dir: Option<IterationDirection> = body.order_by.as_ref().and_then(|order_by| {
|
||||
order_by.first().and_then(|ob| {
|
||||
ob.order.map(|o| match o {
|
||||
SortOrder::Asc => IterationDirection::Forwards,
|
||||
SortOrder::Desc => IterationDirection::Backwards,
|
||||
let iter_dir = body
|
||||
.order_by
|
||||
.as_ref()
|
||||
.and_then(|order_by| {
|
||||
order_by.first().and_then(|ob| {
|
||||
ob.order.map(|o| match o {
|
||||
SortOrder::Asc => IterationDirection::Forwards,
|
||||
SortOrder::Desc => IterationDirection::Backwards,
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
let table_references = vec![TableReference {
|
||||
table: Table::BTree(btree_table.clone()),
|
||||
.unwrap_or(IterationDirection::Forwards);
|
||||
let mut table_references = vec![TableReference {
|
||||
table: match table.as_ref() {
|
||||
Table::Virtual(vtab) => Table::Virtual(vtab.clone()),
|
||||
Table::BTree(btree_table) => Table::BTree(btree_table.clone()),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
identifier: table_name.0.clone(),
|
||||
op: Operation::Scan { iter_dir },
|
||||
op: Operation::Scan {
|
||||
iter_dir,
|
||||
index: None,
|
||||
},
|
||||
join_info: None,
|
||||
col_used_mask: ColumnUsedMask::new(),
|
||||
}];
|
||||
let set_clauses = body
|
||||
.sets
|
||||
.iter_mut()
|
||||
.map(|set| {
|
||||
let ident = normalize_ident(set.col_names[0].0.as_str());
|
||||
let col_index = btree_table
|
||||
.columns
|
||||
let col_index = table
|
||||
.columns()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(i, col)| {
|
||||
@@ -108,7 +124,7 @@ pub fn prepare_update_plan(schema: &Schema, body: &mut Update) -> crate::Result<
|
||||
))
|
||||
})?;
|
||||
|
||||
let _ = bind_column_references(&mut set.expr, &table_references, None);
|
||||
let _ = bind_column_references(&mut set.expr, &mut table_references, None);
|
||||
Ok((col_index, set.expr.clone()))
|
||||
})
|
||||
.collect::<Result<Vec<(usize, Expr)>, crate::LimboError>>()?;
|
||||
@@ -118,7 +134,7 @@ pub fn prepare_update_plan(schema: &Schema, body: &mut Update) -> crate::Result<
|
||||
if let Some(returning) = &mut body.returning {
|
||||
for rc in returning.iter_mut() {
|
||||
if let ResultColumn::Expr(expr, alias) = rc {
|
||||
bind_column_references(expr, &table_references, None)?;
|
||||
bind_column_references(expr, &mut table_references, None)?;
|
||||
result_columns.push(ResultSetColumn {
|
||||
expr: expr.clone(),
|
||||
alias: alias.as_ref().and_then(|a| {
|
||||
@@ -138,30 +154,39 @@ pub fn prepare_update_plan(schema: &Schema, body: &mut Update) -> crate::Result<
|
||||
let order_by = body.order_by.as_ref().map(|order| {
|
||||
order
|
||||
.iter()
|
||||
.map(|o| {
|
||||
(
|
||||
o.expr.clone(),
|
||||
o.order
|
||||
.map(|s| match s {
|
||||
SortOrder::Asc => Direction::Ascending,
|
||||
SortOrder::Desc => Direction::Descending,
|
||||
})
|
||||
.unwrap_or(Direction::Ascending),
|
||||
)
|
||||
})
|
||||
.map(|o| (o.expr.clone(), o.order.unwrap_or(SortOrder::Asc)))
|
||||
.collect()
|
||||
});
|
||||
// Parse the WHERE clause
|
||||
parse_where(
|
||||
body.where_clause.as_ref().map(|w| *w.clone()),
|
||||
&table_references,
|
||||
&mut table_references,
|
||||
Some(&result_columns),
|
||||
&mut where_clause,
|
||||
)?;
|
||||
let limit = if let Some(Ok((limit, _))) = body.limit.as_ref().map(|l| parse_limit(*l.clone())) {
|
||||
limit
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Parse the LIMIT/OFFSET clause
|
||||
let (limit, offset) = body
|
||||
.limit
|
||||
.as_ref()
|
||||
.map(|l| parse_limit(l))
|
||||
.unwrap_or(Ok((None, None)))?;
|
||||
|
||||
// Check what indexes will need to be updated by checking set_clauses and see
|
||||
// if a column is contained in an index.
|
||||
let indexes = schema.get_indices(&table_name.0);
|
||||
let indexes_to_update = indexes
|
||||
.iter()
|
||||
.filter(|index| {
|
||||
index.columns.iter().any(|index_column| {
|
||||
set_clauses
|
||||
.iter()
|
||||
.any(|(set_index_column, _)| index_column.pos_in_table == *set_index_column)
|
||||
})
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
Ok(Plan::Update(UpdatePlan {
|
||||
table_references,
|
||||
set_clauses,
|
||||
@@ -169,6 +194,8 @@ pub fn prepare_update_plan(schema: &Schema, body: &mut Update) -> crate::Result<
|
||||
returning: Some(result_columns),
|
||||
order_by,
|
||||
limit,
|
||||
offset,
|
||||
contains_constant_false_condition: false,
|
||||
indexes_to_update,
|
||||
}))
|
||||
}
|
||||
|
||||
401
core/types.rs
401
core/types.rs
@@ -1,10 +1,13 @@
|
||||
use limbo_ext::{AggCtx, FinalizeFunction, StepFunction};
|
||||
use limbo_sqlite3_parser::ast::SortOrder;
|
||||
|
||||
use crate::error::LimboError;
|
||||
use crate::ext::{ExtValue, ExtValueType};
|
||||
use crate::pseudo::PseudoCursor;
|
||||
use crate::schema::Index;
|
||||
use crate::storage::btree::BTreeCursor;
|
||||
use crate::storage::sqlite3_ondisk::write_varint;
|
||||
use crate::translate::plan::IterationDirection;
|
||||
use crate::vdbe::sorter::Sorter;
|
||||
use crate::vdbe::{Register, VTabOpaqueCursor};
|
||||
use crate::Result;
|
||||
@@ -22,6 +25,20 @@ pub enum OwnedValueType {
|
||||
Error,
|
||||
}
|
||||
|
||||
impl Display for OwnedValueType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let value = match self {
|
||||
Self::Null => "NULL",
|
||||
Self::Integer => "INT",
|
||||
Self::Float => "REAL",
|
||||
Self::Blob => "BLOB",
|
||||
Self::Text => "TEXT",
|
||||
Self::Error => "ERROR",
|
||||
};
|
||||
write!(f, "{}", value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum TextSubtype {
|
||||
Text,
|
||||
@@ -69,6 +86,15 @@ impl Text {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Text {
|
||||
fn from(value: String) -> Self {
|
||||
Text {
|
||||
value: value.into_bytes(),
|
||||
subtype: TextSubtype::Text,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TextRef {
|
||||
pub fn as_str(&self) -> &str {
|
||||
unsafe { std::str::from_utf8_unchecked(self.value.to_slice()) }
|
||||
@@ -145,13 +171,13 @@ impl OwnedValue {
|
||||
OwnedValue::Null => {}
|
||||
OwnedValue::Integer(i) => {
|
||||
let serial_type = SerialType::from(self);
|
||||
match serial_type {
|
||||
SerialType::I8 => out.extend_from_slice(&(*i as i8).to_be_bytes()),
|
||||
SerialType::I16 => out.extend_from_slice(&(*i as i16).to_be_bytes()),
|
||||
SerialType::I24 => out.extend_from_slice(&(*i as i32).to_be_bytes()[1..]), // remove most significant byte
|
||||
SerialType::I32 => out.extend_from_slice(&(*i as i32).to_be_bytes()),
|
||||
SerialType::I48 => out.extend_from_slice(&i.to_be_bytes()[2..]), // remove 2 most significant bytes
|
||||
SerialType::I64 => out.extend_from_slice(&i.to_be_bytes()),
|
||||
match serial_type.kind() {
|
||||
SerialTypeKind::I8 => out.extend_from_slice(&(*i as i8).to_be_bytes()),
|
||||
SerialTypeKind::I16 => out.extend_from_slice(&(*i as i16).to_be_bytes()),
|
||||
SerialTypeKind::I24 => out.extend_from_slice(&(*i as i32).to_be_bytes()[1..]), // remove most significant byte
|
||||
SerialTypeKind::I32 => out.extend_from_slice(&(*i as i32).to_be_bytes()),
|
||||
SerialTypeKind::I48 => out.extend_from_slice(&i.to_be_bytes()[2..]), // remove 2 most significant bytes
|
||||
SerialTypeKind::I64 => out.extend_from_slice(&i.to_be_bytes()),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
@@ -197,6 +223,12 @@ impl Display for OwnedValue {
|
||||
}
|
||||
Self::Float(fl) => {
|
||||
let fl = *fl;
|
||||
if fl == f64::INFINITY {
|
||||
return write!(f, "Inf");
|
||||
}
|
||||
if fl == f64::NEG_INFINITY {
|
||||
return write!(f, "-Inf");
|
||||
}
|
||||
if fl.is_nan() {
|
||||
return write!(f, "");
|
||||
}
|
||||
@@ -732,6 +764,10 @@ impl ImmutableRecord {
|
||||
&self.values[idx]
|
||||
}
|
||||
|
||||
pub fn get_value_opt(&self, idx: usize) -> Option<&RefValue> {
|
||||
self.values.get(idx)
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.values.len()
|
||||
}
|
||||
@@ -750,18 +786,7 @@ impl ImmutableRecord {
|
||||
let n = write_varint(&mut serial_type_buf[0..], serial_type.into());
|
||||
serials.push((serial_type_buf, n));
|
||||
|
||||
let value_size = match serial_type {
|
||||
SerialType::Null => 0,
|
||||
SerialType::I8 => 1,
|
||||
SerialType::I16 => 2,
|
||||
SerialType::I24 => 3,
|
||||
SerialType::I32 => 4,
|
||||
SerialType::I48 => 6,
|
||||
SerialType::I64 => 8,
|
||||
SerialType::F64 => 8,
|
||||
SerialType::Text { content_size } => content_size,
|
||||
SerialType::Blob { content_size } => content_size,
|
||||
};
|
||||
let value_size = serial_type.size();
|
||||
|
||||
size_header += n;
|
||||
size_values += value_size;
|
||||
@@ -808,16 +833,17 @@ impl ImmutableRecord {
|
||||
OwnedValue::Integer(i) => {
|
||||
values.push(RefValue::Integer(*i));
|
||||
let serial_type = SerialType::from(value);
|
||||
match serial_type {
|
||||
SerialType::I8 => writer.extend_from_slice(&(*i as i8).to_be_bytes()),
|
||||
SerialType::I16 => writer.extend_from_slice(&(*i as i16).to_be_bytes()),
|
||||
SerialType::I24 => {
|
||||
match serial_type.kind() {
|
||||
SerialTypeKind::ConstInt0 | SerialTypeKind::ConstInt1 => {}
|
||||
SerialTypeKind::I8 => writer.extend_from_slice(&(*i as i8).to_be_bytes()),
|
||||
SerialTypeKind::I16 => writer.extend_from_slice(&(*i as i16).to_be_bytes()),
|
||||
SerialTypeKind::I24 => {
|
||||
writer.extend_from_slice(&(*i as i32).to_be_bytes()[1..])
|
||||
} // remove most significant byte
|
||||
SerialType::I32 => writer.extend_from_slice(&(*i as i32).to_be_bytes()),
|
||||
SerialType::I48 => writer.extend_from_slice(&i.to_be_bytes()[2..]), // remove 2 most significant bytes
|
||||
SerialType::I64 => writer.extend_from_slice(&i.to_be_bytes()),
|
||||
_ => unreachable!(),
|
||||
SerialTypeKind::I32 => writer.extend_from_slice(&(*i as i32).to_be_bytes()),
|
||||
SerialTypeKind::I48 => writer.extend_from_slice(&i.to_be_bytes()[2..]), // remove 2 most significant bytes
|
||||
SerialTypeKind::I64 => writer.extend_from_slice(&i.to_be_bytes()),
|
||||
other => panic!("Serial type is not an integer: {:?}", other),
|
||||
}
|
||||
}
|
||||
OwnedValue::Float(f) => {
|
||||
@@ -877,6 +903,26 @@ impl ImmutableRecord {
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ImmutableRecord {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
for value in &self.values {
|
||||
match value {
|
||||
RefValue::Null => write!(f, "NULL")?,
|
||||
RefValue::Integer(i) => write!(f, "Integer({})", *i)?,
|
||||
RefValue::Float(flo) => write!(f, "Float({})", *flo)?,
|
||||
RefValue::Text(text_ref) => write!(f, "Text({})", text_ref.as_str())?,
|
||||
RefValue::Blob(raw_slice) => {
|
||||
write!(f, "Blob({})", String::from_utf8_lossy(raw_slice.to_slice()))?
|
||||
}
|
||||
}
|
||||
if value != self.values.last().unwrap() {
|
||||
write!(f, ", ")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for ImmutableRecord {
|
||||
fn clone(&self) -> Self {
|
||||
let mut new_values = Vec::new();
|
||||
@@ -1009,8 +1055,66 @@ impl PartialOrd<RefValue> for RefValue {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compare_immutable(l: &[RefValue], r: &[RefValue]) -> std::cmp::Ordering {
|
||||
l.partial_cmp(r).unwrap()
|
||||
/// A bitfield that represents the comparison spec for index keys.
|
||||
/// Since indexed columns can individually specify ASC/DESC, each key must
|
||||
/// be compared differently.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[repr(transparent)]
|
||||
pub struct IndexKeySortOrder(u64);
|
||||
|
||||
impl IndexKeySortOrder {
|
||||
pub fn get_sort_order_for_col(&self, column_idx: usize) -> SortOrder {
|
||||
assert!(column_idx < 64, "column index out of range: {}", column_idx);
|
||||
match self.0 & (1 << column_idx) {
|
||||
0 => SortOrder::Asc,
|
||||
_ => SortOrder::Desc,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_index(index: &Index) -> Self {
|
||||
let mut spec = 0;
|
||||
for (i, column) in index.columns.iter().enumerate() {
|
||||
spec |= ((column.order == SortOrder::Desc) as u64) << i;
|
||||
}
|
||||
IndexKeySortOrder(spec)
|
||||
}
|
||||
|
||||
pub fn from_list(order: &[SortOrder]) -> Self {
|
||||
let mut spec = 0;
|
||||
for (i, order) in order.iter().enumerate() {
|
||||
spec |= ((*order == SortOrder::Desc) as u64) << i;
|
||||
}
|
||||
IndexKeySortOrder(spec)
|
||||
}
|
||||
|
||||
pub fn default() -> Self {
|
||||
Self(0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for IndexKeySortOrder {
|
||||
fn default() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compare_immutable(
|
||||
l: &[RefValue],
|
||||
r: &[RefValue],
|
||||
index_key_sort_order: IndexKeySortOrder,
|
||||
) -> std::cmp::Ordering {
|
||||
assert_eq!(l.len(), r.len());
|
||||
for (i, (l, r)) in l.iter().zip(r).enumerate() {
|
||||
let column_order = index_key_sort_order.get_sort_order_for_col(i);
|
||||
let cmp = l.partial_cmp(r).unwrap();
|
||||
if !cmp.is_eq() {
|
||||
return match column_order {
|
||||
SortOrder::Asc => cmp,
|
||||
SortOrder::Desc => cmp.reverse(),
|
||||
};
|
||||
}
|
||||
}
|
||||
std::cmp::Ordering::Equal
|
||||
}
|
||||
|
||||
const I8_LOW: i64 = -128;
|
||||
@@ -1027,7 +1131,11 @@ const I48_HIGH: i64 = 140737488355327;
|
||||
/// Sqlite Serial Types
|
||||
/// https://www.sqlite.org/fileformat.html#record_format
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum SerialType {
|
||||
#[repr(transparent)]
|
||||
pub struct SerialType(u64);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum SerialTypeKind {
|
||||
Null,
|
||||
I8,
|
||||
I16,
|
||||
@@ -1036,47 +1144,154 @@ enum SerialType {
|
||||
I48,
|
||||
I64,
|
||||
F64,
|
||||
Text { content_size: usize },
|
||||
Blob { content_size: usize },
|
||||
ConstInt0,
|
||||
ConstInt1,
|
||||
Text,
|
||||
Blob,
|
||||
}
|
||||
|
||||
impl SerialType {
|
||||
#[inline(always)]
|
||||
pub fn u64_is_valid_serial_type(n: u64) -> bool {
|
||||
n != 10 && n != 11
|
||||
}
|
||||
|
||||
const NULL: Self = Self(0);
|
||||
const I8: Self = Self(1);
|
||||
const I16: Self = Self(2);
|
||||
const I24: Self = Self(3);
|
||||
const I32: Self = Self(4);
|
||||
const I48: Self = Self(5);
|
||||
const I64: Self = Self(6);
|
||||
const F64: Self = Self(7);
|
||||
const CONST_INT0: Self = Self(8);
|
||||
const CONST_INT1: Self = Self(9);
|
||||
|
||||
pub fn null() -> Self {
|
||||
Self::NULL
|
||||
}
|
||||
|
||||
pub fn i8() -> Self {
|
||||
Self::I8
|
||||
}
|
||||
|
||||
pub fn i16() -> Self {
|
||||
Self::I16
|
||||
}
|
||||
|
||||
pub fn i24() -> Self {
|
||||
Self::I24
|
||||
}
|
||||
|
||||
pub fn i32() -> Self {
|
||||
Self::I32
|
||||
}
|
||||
|
||||
pub fn i48() -> Self {
|
||||
Self::I48
|
||||
}
|
||||
|
||||
pub fn i64() -> Self {
|
||||
Self::I64
|
||||
}
|
||||
|
||||
pub fn f64() -> Self {
|
||||
Self::F64
|
||||
}
|
||||
|
||||
pub fn const_int0() -> Self {
|
||||
Self::CONST_INT0
|
||||
}
|
||||
|
||||
pub fn const_int1() -> Self {
|
||||
Self::CONST_INT1
|
||||
}
|
||||
|
||||
pub fn blob(size: u64) -> Self {
|
||||
Self(12 + size * 2)
|
||||
}
|
||||
|
||||
pub fn text(size: u64) -> Self {
|
||||
Self(13 + size * 2)
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> SerialTypeKind {
|
||||
match self.0 {
|
||||
0 => SerialTypeKind::Null,
|
||||
1 => SerialTypeKind::I8,
|
||||
2 => SerialTypeKind::I16,
|
||||
3 => SerialTypeKind::I24,
|
||||
4 => SerialTypeKind::I32,
|
||||
5 => SerialTypeKind::I48,
|
||||
6 => SerialTypeKind::I64,
|
||||
7 => SerialTypeKind::F64,
|
||||
8 => SerialTypeKind::ConstInt0,
|
||||
9 => SerialTypeKind::ConstInt1,
|
||||
n if n >= 12 => match n % 2 {
|
||||
0 => SerialTypeKind::Blob,
|
||||
1 => SerialTypeKind::Text,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(&self) -> usize {
|
||||
match self.kind() {
|
||||
SerialTypeKind::Null => 0,
|
||||
SerialTypeKind::I8 => 1,
|
||||
SerialTypeKind::I16 => 2,
|
||||
SerialTypeKind::I24 => 3,
|
||||
SerialTypeKind::I32 => 4,
|
||||
SerialTypeKind::I48 => 6,
|
||||
SerialTypeKind::I64 => 8,
|
||||
SerialTypeKind::F64 => 8,
|
||||
SerialTypeKind::ConstInt0 => 0,
|
||||
SerialTypeKind::ConstInt1 => 0,
|
||||
SerialTypeKind::Text => (self.0 as usize - 13) / 2,
|
||||
SerialTypeKind::Blob => (self.0 as usize - 12) / 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&OwnedValue> for SerialType {
|
||||
fn from(value: &OwnedValue) -> Self {
|
||||
match value {
|
||||
OwnedValue::Null => SerialType::Null,
|
||||
OwnedValue::Null => SerialType::null(),
|
||||
OwnedValue::Integer(i) => match i {
|
||||
i if *i >= I8_LOW && *i <= I8_HIGH => SerialType::I8,
|
||||
i if *i >= I16_LOW && *i <= I16_HIGH => SerialType::I16,
|
||||
i if *i >= I24_LOW && *i <= I24_HIGH => SerialType::I24,
|
||||
i if *i >= I32_LOW && *i <= I32_HIGH => SerialType::I32,
|
||||
i if *i >= I48_LOW && *i <= I48_HIGH => SerialType::I48,
|
||||
_ => SerialType::I64,
|
||||
},
|
||||
OwnedValue::Float(_) => SerialType::F64,
|
||||
OwnedValue::Text(t) => SerialType::Text {
|
||||
content_size: t.value.len(),
|
||||
},
|
||||
OwnedValue::Blob(b) => SerialType::Blob {
|
||||
content_size: b.len(),
|
||||
0 => SerialType::const_int0(),
|
||||
1 => SerialType::const_int1(),
|
||||
i if *i >= I8_LOW && *i <= I8_HIGH => SerialType::i8(),
|
||||
i if *i >= I16_LOW && *i <= I16_HIGH => SerialType::i16(),
|
||||
i if *i >= I24_LOW && *i <= I24_HIGH => SerialType::i24(),
|
||||
i if *i >= I32_LOW && *i <= I32_HIGH => SerialType::i32(),
|
||||
i if *i >= I48_LOW && *i <= I48_HIGH => SerialType::i48(),
|
||||
_ => SerialType::i64(),
|
||||
},
|
||||
OwnedValue::Float(_) => SerialType::f64(),
|
||||
OwnedValue::Text(t) => SerialType::text(t.value.len() as u64),
|
||||
OwnedValue::Blob(b) => SerialType::blob(b.len() as u64),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SerialType> for u64 {
|
||||
fn from(serial_type: SerialType) -> Self {
|
||||
match serial_type {
|
||||
SerialType::Null => 0,
|
||||
SerialType::I8 => 1,
|
||||
SerialType::I16 => 2,
|
||||
SerialType::I24 => 3,
|
||||
SerialType::I32 => 4,
|
||||
SerialType::I48 => 5,
|
||||
SerialType::I64 => 6,
|
||||
SerialType::F64 => 7,
|
||||
SerialType::Text { content_size } => (content_size * 2 + 13) as u64,
|
||||
SerialType::Blob { content_size } => (content_size * 2 + 12) as u64,
|
||||
serial_type.0
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<u64> for SerialType {
|
||||
type Error = LimboError;
|
||||
|
||||
fn try_from(uint: u64) -> Result<Self> {
|
||||
if uint == 10 || uint == 11 {
|
||||
return Err(LimboError::Corrupt(format!(
|
||||
"Invalid serial type: {}",
|
||||
uint
|
||||
)));
|
||||
}
|
||||
Ok(SerialType(uint))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1104,13 +1319,15 @@ impl Record {
|
||||
OwnedValue::Null => {}
|
||||
OwnedValue::Integer(i) => {
|
||||
let serial_type = SerialType::from(value);
|
||||
match serial_type {
|
||||
SerialType::I8 => buf.extend_from_slice(&(*i as i8).to_be_bytes()),
|
||||
SerialType::I16 => buf.extend_from_slice(&(*i as i16).to_be_bytes()),
|
||||
SerialType::I24 => buf.extend_from_slice(&(*i as i32).to_be_bytes()[1..]), // remove most significant byte
|
||||
SerialType::I32 => buf.extend_from_slice(&(*i as i32).to_be_bytes()),
|
||||
SerialType::I48 => buf.extend_from_slice(&i.to_be_bytes()[2..]), // remove 2 most significant bytes
|
||||
SerialType::I64 => buf.extend_from_slice(&i.to_be_bytes()),
|
||||
match serial_type.kind() {
|
||||
SerialTypeKind::I8 => buf.extend_from_slice(&(*i as i8).to_be_bytes()),
|
||||
SerialTypeKind::I16 => buf.extend_from_slice(&(*i as i16).to_be_bytes()),
|
||||
SerialTypeKind::I24 => {
|
||||
buf.extend_from_slice(&(*i as i32).to_be_bytes()[1..])
|
||||
} // remove most significant byte
|
||||
SerialTypeKind::I32 => buf.extend_from_slice(&(*i as i32).to_be_bytes()),
|
||||
SerialTypeKind::I48 => buf.extend_from_slice(&i.to_be_bytes()[2..]), // remove 2 most significant bytes
|
||||
SerialTypeKind::I64 => buf.extend_from_slice(&i.to_be_bytes()),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
@@ -1193,11 +1410,43 @@ pub enum CursorResult<T> {
|
||||
IO,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
/// The match condition of a table/index seek.
|
||||
pub enum SeekOp {
|
||||
EQ,
|
||||
GE,
|
||||
GT,
|
||||
LE,
|
||||
LT,
|
||||
}
|
||||
|
||||
impl SeekOp {
|
||||
/// A given seek op implies an iteration direction.
|
||||
///
|
||||
/// For example, a seek with SeekOp::GT implies:
|
||||
/// Find the first table/index key that compares greater than the seek key
|
||||
/// -> used in forwards iteration.
|
||||
///
|
||||
/// A seek with SeekOp::LE implies:
|
||||
/// Find the last table/index key that compares less than or equal to the seek key
|
||||
/// -> used in backwards iteration.
|
||||
#[inline(always)]
|
||||
pub fn iteration_direction(&self) -> IterationDirection {
|
||||
match self {
|
||||
SeekOp::EQ | SeekOp::GE | SeekOp::GT => IterationDirection::Forwards,
|
||||
SeekOp::LE | SeekOp::LT => IterationDirection::Backwards,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reverse(&self) -> Self {
|
||||
match self {
|
||||
SeekOp::EQ => SeekOp::EQ,
|
||||
SeekOp::GE => SeekOp::LE,
|
||||
SeekOp::GT => SeekOp::LT,
|
||||
SeekOp::LE => SeekOp::GE,
|
||||
SeekOp::LT => SeekOp::GT,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
@@ -1234,7 +1483,7 @@ mod tests {
|
||||
// First byte should be header size
|
||||
assert_eq!(header[0], header_length as u8);
|
||||
// Second byte should be serial type for NULL
|
||||
assert_eq!(header[1] as u64, u64::from(SerialType::Null));
|
||||
assert_eq!(header[1] as u64, u64::from(SerialType::null()));
|
||||
// Check that the buffer is empty after the header
|
||||
assert_eq!(buf.len(), header_length);
|
||||
}
|
||||
@@ -1258,12 +1507,12 @@ mod tests {
|
||||
assert_eq!(header[0], header_length as u8); // Header should be larger than number of values
|
||||
|
||||
// Check that correct serial types were chosen
|
||||
assert_eq!(header[1] as u64, u64::from(SerialType::I8));
|
||||
assert_eq!(header[2] as u64, u64::from(SerialType::I16));
|
||||
assert_eq!(header[3] as u64, u64::from(SerialType::I24));
|
||||
assert_eq!(header[4] as u64, u64::from(SerialType::I32));
|
||||
assert_eq!(header[5] as u64, u64::from(SerialType::I48));
|
||||
assert_eq!(header[6] as u64, u64::from(SerialType::I64));
|
||||
assert_eq!(header[1] as u64, u64::from(SerialType::i8()));
|
||||
assert_eq!(header[2] as u64, u64::from(SerialType::i16()));
|
||||
assert_eq!(header[3] as u64, u64::from(SerialType::i24()));
|
||||
assert_eq!(header[4] as u64, u64::from(SerialType::i32()));
|
||||
assert_eq!(header[5] as u64, u64::from(SerialType::i48()));
|
||||
assert_eq!(header[6] as u64, u64::from(SerialType::i64()));
|
||||
|
||||
// test that the bytes after the header can be interpreted as the correct values
|
||||
let mut cur_offset = header_length;
|
||||
@@ -1326,7 +1575,7 @@ mod tests {
|
||||
// First byte should be header size
|
||||
assert_eq!(header[0], header_length as u8);
|
||||
// Second byte should be serial type for FLOAT
|
||||
assert_eq!(header[1] as u64, u64::from(SerialType::F64));
|
||||
assert_eq!(header[1] as u64, u64::from(SerialType::f64()));
|
||||
// Check that the bytes after the header can be interpreted as the float
|
||||
let float_bytes = &buf[header_length..header_length + size_of::<f64>()];
|
||||
let float = f64::from_be_bytes(float_bytes.try_into().unwrap());
|
||||
@@ -1390,11 +1639,11 @@ mod tests {
|
||||
// First byte should be header size
|
||||
assert_eq!(header[0], header_length as u8);
|
||||
// Second byte should be serial type for NULL
|
||||
assert_eq!(header[1] as u64, u64::from(SerialType::Null));
|
||||
assert_eq!(header[1] as u64, u64::from(SerialType::null()));
|
||||
// Third byte should be serial type for I8
|
||||
assert_eq!(header[2] as u64, u64::from(SerialType::I8));
|
||||
assert_eq!(header[2] as u64, u64::from(SerialType::i8()));
|
||||
// Fourth byte should be serial type for F64
|
||||
assert_eq!(header[3] as u64, u64::from(SerialType::F64));
|
||||
assert_eq!(header[3] as u64, u64::from(SerialType::f64()));
|
||||
// Fifth byte should be serial type for TEXT, which is (len * 2 + 13)
|
||||
assert_eq!(header[4] as u64, (4 * 2 + 13) as u64);
|
||||
|
||||
|
||||
345
core/util.rs
345
core/util.rs
@@ -2,6 +2,7 @@ use limbo_sqlite3_parser::ast::{self, CreateTableBody, Expr, FunctionTail, Liter
|
||||
use std::{rc::Rc, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
function::Func,
|
||||
schema::{self, Column, Schema, Type},
|
||||
types::{OwnedValue, OwnedValueType},
|
||||
LimboError, OpenFlags, Result, Statement, StepResult, SymbolTable, IO,
|
||||
@@ -36,6 +37,21 @@ pub fn normalize_ident(identifier: &str) -> String {
|
||||
|
||||
pub const PRIMARY_KEY_AUTOMATIC_INDEX_NAME_PREFIX: &str = "sqlite_autoindex_";
|
||||
|
||||
enum UnparsedIndex {
|
||||
/// CREATE INDEX idx ON table_name(sql)
|
||||
FromSql {
|
||||
table_name: String,
|
||||
root_page: usize,
|
||||
sql: String,
|
||||
},
|
||||
/// Implicitly created index due to primary key constraints (or UNIQUE, but not implemented)
|
||||
FromConstraint {
|
||||
name: String,
|
||||
table_name: String,
|
||||
root_page: usize,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn parse_schema_rows(
|
||||
rows: Option<Statement>,
|
||||
schema: &mut Schema,
|
||||
@@ -45,7 +61,7 @@ pub fn parse_schema_rows(
|
||||
) -> Result<()> {
|
||||
if let Some(mut rows) = rows {
|
||||
rows.set_mv_tx_id(mv_tx_id);
|
||||
let mut automatic_indexes = Vec::new();
|
||||
let mut unparsed_indexes = Vec::with_capacity(10);
|
||||
loop {
|
||||
match rows.step()? {
|
||||
StepResult::Row => {
|
||||
@@ -58,9 +74,37 @@ pub fn parse_schema_rows(
|
||||
"table" => {
|
||||
let root_page: i64 = row.get::<i64>(3)?;
|
||||
let sql: &str = row.get::<&str>(4)?;
|
||||
if root_page == 0 && sql.to_lowercase().contains("virtual") {
|
||||
if root_page == 0 && sql.to_lowercase().contains("create virtual") {
|
||||
let name: &str = row.get::<&str>(1)?;
|
||||
let vtab = syms.vtabs.get(name).unwrap().clone();
|
||||
// a virtual table is found in the sqlite_schema, but it's no
|
||||
// longer in the in-memory schema. We need to recreate it if
|
||||
// the module is loaded in the symbol table.
|
||||
let vtab = if let Some(vtab) = syms.vtabs.get(name) {
|
||||
vtab.clone()
|
||||
} else {
|
||||
let mod_name = module_name_from_sql(sql)?;
|
||||
if let Some(vmod) = syms.vtab_modules.get(mod_name) {
|
||||
if let limbo_ext::VTabKind::VirtualTable = vmod.module_kind
|
||||
{
|
||||
crate::VirtualTable::from_args(
|
||||
Some(name),
|
||||
mod_name,
|
||||
module_args_from_sql(sql)?,
|
||||
syms,
|
||||
vmod.module_kind,
|
||||
None,
|
||||
)?
|
||||
} else {
|
||||
return Err(LimboError::Corrupt("Table valued function: {name} registered as virtual table in schema".to_string()));
|
||||
}
|
||||
} else {
|
||||
// the extension isn't loaded, so we emit a warning.
|
||||
return Err(LimboError::ExtensionError(format!(
|
||||
"Virtual table module '{}' not found\nPlease load extension",
|
||||
&mod_name
|
||||
)));
|
||||
}
|
||||
};
|
||||
schema.add_virtual_table(vtab);
|
||||
} else {
|
||||
let table = schema::BTreeTable::from_sql(sql, root_page as usize)?;
|
||||
@@ -71,21 +115,24 @@ pub fn parse_schema_rows(
|
||||
let root_page: i64 = row.get::<i64>(3)?;
|
||||
match row.get::<&str>(4) {
|
||||
Ok(sql) => {
|
||||
let index = schema::Index::from_sql(sql, root_page as usize)?;
|
||||
schema.add_index(Arc::new(index));
|
||||
unparsed_indexes.push(UnparsedIndex::FromSql {
|
||||
table_name: row.get::<&str>(2)?.to_string(),
|
||||
root_page: root_page as usize,
|
||||
sql: sql.to_string(),
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
// Automatic index on primary key, e.g.
|
||||
// table|foo|foo|2|CREATE TABLE foo (a text PRIMARY KEY, b)
|
||||
// index|sqlite_autoindex_foo_1|foo|3|
|
||||
let index_name = row.get::<&str>(1)?;
|
||||
let table_name = row.get::<&str>(2)?;
|
||||
let index_name = row.get::<&str>(1)?.to_string();
|
||||
let table_name = row.get::<&str>(2)?.to_string();
|
||||
let root_page = row.get::<i64>(3)?;
|
||||
automatic_indexes.push((
|
||||
index_name.to_string(),
|
||||
table_name.to_string(),
|
||||
root_page,
|
||||
));
|
||||
unparsed_indexes.push(UnparsedIndex::FromConstraint {
|
||||
name: index_name,
|
||||
table_name,
|
||||
root_page: root_page as usize,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,12 +149,31 @@ pub fn parse_schema_rows(
|
||||
StepResult::Busy => break,
|
||||
}
|
||||
}
|
||||
for (index_name, table_name, root_page) in automatic_indexes {
|
||||
// We need to process these after all tables are loaded into memory due to the schema.get_table() call
|
||||
let table = schema.get_btree_table(&table_name).unwrap();
|
||||
let index =
|
||||
schema::Index::automatic_from_primary_key(&table, &index_name, root_page as usize)?;
|
||||
schema.add_index(Arc::new(index));
|
||||
for unparsed_index in unparsed_indexes {
|
||||
match unparsed_index {
|
||||
UnparsedIndex::FromSql {
|
||||
table_name,
|
||||
root_page,
|
||||
sql,
|
||||
} => {
|
||||
let table = schema.get_btree_table(&table_name).unwrap();
|
||||
let index = schema::Index::from_sql(&sql, root_page as usize, table.as_ref())?;
|
||||
schema.add_index(Arc::new(index));
|
||||
}
|
||||
UnparsedIndex::FromConstraint {
|
||||
name,
|
||||
table_name,
|
||||
root_page,
|
||||
} => {
|
||||
let table = schema.get_btree_table(&table_name).unwrap();
|
||||
let index = schema::Index::automatic_from_primary_key(
|
||||
table.as_ref(),
|
||||
&name,
|
||||
root_page as usize,
|
||||
)?;
|
||||
schema.add_index(Arc::new(index));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -132,6 +198,99 @@ pub fn check_ident_equivalency(ident1: &str, ident2: &str) -> bool {
|
||||
strip_quotes(ident1).eq_ignore_ascii_case(strip_quotes(ident2))
|
||||
}
|
||||
|
||||
fn module_name_from_sql(sql: &str) -> Result<&str> {
|
||||
if let Some(start) = sql.find("USING") {
|
||||
let start = start + 6;
|
||||
// stop at the first space, semicolon, or parenthesis
|
||||
let end = sql[start..]
|
||||
.find(|c: char| c.is_whitespace() || c == ';' || c == '(')
|
||||
.unwrap_or(sql.len() - start)
|
||||
+ start;
|
||||
Ok(sql[start..end].trim())
|
||||
} else {
|
||||
Err(LimboError::InvalidArgument(
|
||||
"Expected 'USING' in module name".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// CREATE VIRTUAL TABLE table_name USING module_name(arg1, arg2, ...);
|
||||
// CREATE VIRTUAL TABLE table_name USING module_name;
|
||||
fn module_args_from_sql(sql: &str) -> Result<Vec<limbo_ext::Value>> {
|
||||
if !sql.contains('(') {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
let start = sql.find('(').ok_or_else(|| {
|
||||
LimboError::InvalidArgument("Expected '(' in module argument list".to_string())
|
||||
})? + 1;
|
||||
let end = sql.rfind(')').ok_or_else(|| {
|
||||
LimboError::InvalidArgument("Expected ')' in module argument list".to_string())
|
||||
})?;
|
||||
|
||||
let mut args = Vec::new();
|
||||
let mut current_arg = String::new();
|
||||
let mut chars = sql[start..end].chars().peekable();
|
||||
let mut in_quotes = false;
|
||||
|
||||
while let Some(c) = chars.next() {
|
||||
match c {
|
||||
'\'' => {
|
||||
if in_quotes {
|
||||
if chars.peek() == Some(&'\'') {
|
||||
// Escaped quote
|
||||
current_arg.push('\'');
|
||||
chars.next();
|
||||
} else {
|
||||
in_quotes = false;
|
||||
args.push(limbo_ext::Value::from_text(current_arg.trim().to_string()));
|
||||
current_arg.clear();
|
||||
// Skip until comma or end
|
||||
while let Some(&nc) = chars.peek() {
|
||||
if nc == ',' {
|
||||
chars.next(); // Consume comma
|
||||
break;
|
||||
} else if nc.is_whitespace() {
|
||||
chars.next();
|
||||
} else {
|
||||
return Err(LimboError::InvalidArgument(
|
||||
"Unexpected characters after quoted argument".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
in_quotes = true;
|
||||
}
|
||||
}
|
||||
',' => {
|
||||
if !in_quotes {
|
||||
if !current_arg.trim().is_empty() {
|
||||
args.push(limbo_ext::Value::from_text(current_arg.trim().to_string()));
|
||||
current_arg.clear();
|
||||
}
|
||||
} else {
|
||||
current_arg.push(c);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
current_arg.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !current_arg.trim().is_empty() && !in_quotes {
|
||||
args.push(limbo_ext::Value::from_text(current_arg.trim().to_string()));
|
||||
}
|
||||
|
||||
if in_quotes {
|
||||
return Err(LimboError::InvalidArgument(
|
||||
"Unterminated string literal in module arguments".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(args)
|
||||
}
|
||||
|
||||
pub fn check_literal_equivalency(lhs: &Literal, rhs: &Literal) -> bool {
|
||||
match (lhs, rhs) {
|
||||
(Literal::Numeric(n1), Literal::Numeric(n2)) => cmp_numeric_strings(n1, n2),
|
||||
@@ -278,7 +437,11 @@ pub fn exprs_are_equivalent(expr1: &Expr, expr2: &Expr) -> bool {
|
||||
(Expr::Unary(op1, expr1), Expr::Unary(op2, expr2)) => {
|
||||
op1 == op2 && exprs_are_equivalent(expr1, expr2)
|
||||
}
|
||||
(Expr::Variable(var1), Expr::Variable(var2)) => var1 == var2,
|
||||
// Variables that are not bound to a specific value, are treated as NULL
|
||||
// https://sqlite.org/lang_expr.html#varparam
|
||||
(Expr::Variable(var), Expr::Variable(var2)) if var == "" && var2 == "" => false,
|
||||
// Named variables can be compared by their name
|
||||
(Expr::Variable(val), Expr::Variable(val2)) => val == val2,
|
||||
(Expr::Parenthesized(exprs1), Expr::Parenthesized(exprs2)) => {
|
||||
exprs1.len() == exprs2.len()
|
||||
&& exprs1
|
||||
@@ -403,6 +566,39 @@ pub fn columns_from_create_table_body(body: &ast::CreateTableBody) -> crate::Res
|
||||
.collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
/// This function checks if a given expression is a constant value that can be pushed down to the database engine.
|
||||
/// It is expected to be called with the other half of a binary expression with an Expr::Column
|
||||
pub fn can_pushdown_predicate(expr: &Expr, table_idx: usize) -> bool {
|
||||
match expr {
|
||||
Expr::Literal(_) => true,
|
||||
Expr::Column { table, .. } => *table <= table_idx,
|
||||
Expr::Binary(lhs, _, rhs) => {
|
||||
can_pushdown_predicate(lhs, table_idx) && can_pushdown_predicate(rhs, table_idx)
|
||||
}
|
||||
Expr::Parenthesized(exprs) => can_pushdown_predicate(exprs.first().unwrap(), table_idx),
|
||||
Expr::Unary(_, expr) => can_pushdown_predicate(expr, table_idx),
|
||||
Expr::FunctionCall { args, name, .. } => {
|
||||
let function = crate::function::Func::resolve_function(
|
||||
&name.0,
|
||||
args.as_ref().map_or(0, |a| a.len()),
|
||||
);
|
||||
// is deterministic
|
||||
matches!(function, Ok(Func::Scalar(_)))
|
||||
}
|
||||
Expr::Like { lhs, rhs, .. } => {
|
||||
can_pushdown_predicate(lhs, table_idx) && can_pushdown_predicate(rhs, table_idx)
|
||||
}
|
||||
Expr::Between {
|
||||
lhs, start, end, ..
|
||||
} => {
|
||||
can_pushdown_predicate(lhs, table_idx)
|
||||
&& can_pushdown_predicate(start, table_idx)
|
||||
&& can_pushdown_predicate(end, table_idx)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct OpenOptions<'a> {
|
||||
/// The authority component of the URI. may be 'localhost' or empty
|
||||
@@ -716,11 +912,10 @@ fn parse_numeric_str(text: &str) -> Result<(OwnedValueType, &str), ()> {
|
||||
let text = text.trim();
|
||||
let bytes = text.as_bytes();
|
||||
|
||||
if bytes.is_empty()
|
||||
|| bytes[0] == b'e'
|
||||
|| bytes[0] == b'E'
|
||||
|| (bytes[0] == b'.' && (bytes[1] == b'e' || bytes[1] == b'E'))
|
||||
{
|
||||
if matches!(
|
||||
bytes,
|
||||
[] | [b'e', ..] | [b'E', ..] | [b'.', b'e' | b'E', ..]
|
||||
) {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
@@ -824,6 +1019,24 @@ pub mod tests {
|
||||
assert_eq!(normalize_ident("\"foo\""), "foo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_anonymous_variable_comparison() {
|
||||
let expr1 = Expr::Variable("".to_string());
|
||||
let expr2 = Expr::Variable("".to_string());
|
||||
assert!(!exprs_are_equivalent(&expr1, &expr2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_named_variable_comparison() {
|
||||
let expr1 = Expr::Variable("1".to_string());
|
||||
let expr2 = Expr::Variable("1".to_string());
|
||||
assert!(exprs_are_equivalent(&expr1, &expr2));
|
||||
|
||||
let expr1 = Expr::Variable("1".to_string());
|
||||
let expr2 = Expr::Variable("2".to_string());
|
||||
assert!(!exprs_are_equivalent(&expr1, &expr2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_addition_exprs_are_equivalent() {
|
||||
let expr1 = Expr::Binary(
|
||||
@@ -1632,4 +1845,88 @@ pub mod tests {
|
||||
Ok((OwnedValueType::Float, "1.23e4"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_module_name_basic() {
|
||||
let sql = "CREATE VIRTUAL TABLE x USING y;";
|
||||
assert_eq!(module_name_from_sql(sql).unwrap(), "y");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_module_name_with_args() {
|
||||
let sql = "CREATE VIRTUAL TABLE x USING modname('a', 'b');";
|
||||
assert_eq!(module_name_from_sql(sql).unwrap(), "modname");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_module_name_missing_using() {
|
||||
let sql = "CREATE VIRTUAL TABLE x (a, b);";
|
||||
assert!(module_name_from_sql(sql).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_module_name_no_semicolon() {
|
||||
let sql = "CREATE VIRTUAL TABLE x USING limbo(a, b)";
|
||||
assert_eq!(module_name_from_sql(sql).unwrap(), "limbo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_module_name_no_semicolon_or_args() {
|
||||
let sql = "CREATE VIRTUAL TABLE x USING limbo";
|
||||
assert_eq!(module_name_from_sql(sql).unwrap(), "limbo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_module_args_none() {
|
||||
let sql = "CREATE VIRTUAL TABLE x USING modname;";
|
||||
let args = module_args_from_sql(sql).unwrap();
|
||||
assert_eq!(args.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_module_args_basic() {
|
||||
let sql = "CREATE VIRTUAL TABLE x USING modname('arg1', 'arg2');";
|
||||
let args = module_args_from_sql(sql).unwrap();
|
||||
assert_eq!(args.len(), 2);
|
||||
assert_eq!("arg1", args[0].to_text().unwrap());
|
||||
assert_eq!("arg2", args[1].to_text().unwrap());
|
||||
for arg in args {
|
||||
unsafe { arg.__free_internal_type() }
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_module_args_with_escaped_quote() {
|
||||
let sql = "CREATE VIRTUAL TABLE x USING modname('a''b', 'c');";
|
||||
let args = module_args_from_sql(sql).unwrap();
|
||||
assert_eq!(args.len(), 2);
|
||||
assert_eq!(args[0].to_text().unwrap(), "a'b");
|
||||
assert_eq!(args[1].to_text().unwrap(), "c");
|
||||
for arg in args {
|
||||
unsafe { arg.__free_internal_type() }
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_module_args_unterminated_string() {
|
||||
let sql = "CREATE VIRTUAL TABLE x USING modname('arg1, 'arg2');";
|
||||
assert!(module_args_from_sql(sql).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_module_args_extra_garbage_after_quote() {
|
||||
let sql = "CREATE VIRTUAL TABLE x USING modname('arg1'x);";
|
||||
assert!(module_args_from_sql(sql).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_module_args_trailing_comma() {
|
||||
let sql = "CREATE VIRTUAL TABLE x USING modname('arg1',);";
|
||||
let args = module_args_from_sql(sql).unwrap();
|
||||
assert_eq!(args.len(), 1);
|
||||
assert_eq!("arg1", args[0].to_text().unwrap());
|
||||
for arg in args {
|
||||
unsafe { arg.__free_internal_type() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{
|
||||
cell::Cell,
|
||||
collections::HashMap,
|
||||
cmp::Ordering,
|
||||
rc::{Rc, Weak},
|
||||
sync::Arc,
|
||||
};
|
||||
@@ -16,24 +16,25 @@ use crate::{
|
||||
Connection, VirtualTable,
|
||||
};
|
||||
|
||||
use super::{BranchOffset, CursorID, Insn, InsnFunction, InsnReference, Program};
|
||||
use super::{BranchOffset, CursorID, Insn, InsnFunction, InsnReference, JumpTarget, Program};
|
||||
#[allow(dead_code)]
|
||||
pub struct ProgramBuilder {
|
||||
next_free_register: usize,
|
||||
next_free_cursor_id: usize,
|
||||
insns: Vec<(Insn, InsnFunction)>,
|
||||
// for temporarily storing instructions that will be put after Transaction opcode
|
||||
constant_insns: Vec<(Insn, InsnFunction)>,
|
||||
// Vector of labels which must be assigned to next emitted instruction
|
||||
next_insn_labels: Vec<BranchOffset>,
|
||||
/// Instruction, the function to execute it with, and its original index in the vector.
|
||||
insns: Vec<(Insn, InsnFunction, usize)>,
|
||||
/// A span of instructions from (offset_start_inclusive, offset_end_exclusive),
|
||||
/// that are deemed to be compile-time constant and can be hoisted out of loops
|
||||
/// so that they get evaluated only once at the start of the program.
|
||||
pub constant_spans: Vec<(usize, usize)>,
|
||||
// Cursors that are referenced by the program. Indexed by CursorID.
|
||||
pub cursor_ref: Vec<(Option<String>, CursorType)>,
|
||||
/// A vector where index=label number, value=resolved offset. Resolved in build().
|
||||
label_to_resolved_offset: Vec<Option<InsnReference>>,
|
||||
label_to_resolved_offset: Vec<Option<(InsnReference, JumpTarget)>>,
|
||||
// Bitmask of cursors that have emitted a SeekRowid instruction.
|
||||
seekrowid_emitted_bitmask: u64,
|
||||
// map of instruction index to manual comment (used in EXPLAIN only)
|
||||
comments: Option<HashMap<InsnReference, &'static str>>,
|
||||
comments: Option<Vec<(InsnReference, &'static str)>>,
|
||||
pub parameters: Parameters,
|
||||
pub result_columns: Vec<ResultSetColumn>,
|
||||
pub table_references: Vec<TableReference>,
|
||||
@@ -82,13 +83,12 @@ impl ProgramBuilder {
|
||||
next_free_register: 1,
|
||||
next_free_cursor_id: 0,
|
||||
insns: Vec::with_capacity(opts.approx_num_insns),
|
||||
next_insn_labels: Vec::with_capacity(2),
|
||||
cursor_ref: Vec::with_capacity(opts.num_cursors),
|
||||
constant_insns: Vec::new(),
|
||||
constant_spans: Vec::new(),
|
||||
label_to_resolved_offset: Vec::with_capacity(opts.approx_num_labels),
|
||||
seekrowid_emitted_bitmask: 0,
|
||||
comments: if opts.query_mode == QueryMode::Explain {
|
||||
Some(HashMap::new())
|
||||
Some(Vec::new())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
@@ -98,6 +98,56 @@ impl ProgramBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Start a new constant span. The next instruction to be emitted will be the first
|
||||
/// instruction in the span.
|
||||
pub fn constant_span_start(&mut self) -> usize {
|
||||
let span = self.constant_spans.len();
|
||||
let start = self.insns.len();
|
||||
self.constant_spans.push((start, usize::MAX));
|
||||
span
|
||||
}
|
||||
|
||||
/// End the current constant span. The last instruction that was emitted is the last
|
||||
/// instruction in the span.
|
||||
pub fn constant_span_end(&mut self, span_idx: usize) {
|
||||
let span = &mut self.constant_spans[span_idx];
|
||||
if span.1 == usize::MAX {
|
||||
span.1 = self.insns.len().saturating_sub(1);
|
||||
}
|
||||
}
|
||||
|
||||
/// End all constant spans that are currently open. This is used to handle edge cases
|
||||
/// where we think a parent expression is constant, but we decide during the evaluation
|
||||
/// of one of its children that it is not.
|
||||
pub fn constant_span_end_all(&mut self) {
|
||||
for span in self.constant_spans.iter_mut() {
|
||||
if span.1 == usize::MAX {
|
||||
span.1 = self.insns.len().saturating_sub(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if there is a constant span that is currently open.
|
||||
pub fn constant_span_is_open(&self) -> bool {
|
||||
self.constant_spans
|
||||
.last()
|
||||
.map_or(false, |(_, end)| *end == usize::MAX)
|
||||
}
|
||||
|
||||
/// Get the index of the next constant span.
|
||||
/// Used in [crate::translate::expr::translate_expr_no_constant_opt()] to invalidate
|
||||
/// all constant spans after the given index.
|
||||
pub fn constant_spans_next_idx(&self) -> usize {
|
||||
self.constant_spans.len()
|
||||
}
|
||||
|
||||
/// Invalidate all constant spans after the given index. This is used when we want to
|
||||
/// be sure that constant optimization is never used for translating a given expression.
|
||||
/// See [crate::translate::expr::translate_expr_no_constant_opt()] for more details.
|
||||
pub fn constant_spans_invalidate_after(&mut self, idx: usize) {
|
||||
self.constant_spans.truncate(idx);
|
||||
}
|
||||
|
||||
pub fn alloc_register(&mut self) -> usize {
|
||||
let reg = self.next_free_register;
|
||||
self.next_free_register += 1;
|
||||
@@ -123,12 +173,14 @@ impl ProgramBuilder {
|
||||
}
|
||||
|
||||
pub fn emit_insn(&mut self, insn: Insn) {
|
||||
for label in self.next_insn_labels.drain(..) {
|
||||
self.label_to_resolved_offset[label.to_label_value() as usize] =
|
||||
Some(self.insns.len() as InsnReference);
|
||||
}
|
||||
let function = insn.to_function();
|
||||
self.insns.push((insn, function));
|
||||
self.insns.push((insn, function, self.insns.len()));
|
||||
}
|
||||
|
||||
pub fn close_cursors(&mut self, cursors: &[CursorID]) {
|
||||
for cursor in cursors {
|
||||
self.emit_insn(Insn::Close { cursor_id: *cursor });
|
||||
}
|
||||
}
|
||||
|
||||
pub fn emit_string8(&mut self, value: String, dest: usize) {
|
||||
@@ -194,20 +246,69 @@ impl ProgramBuilder {
|
||||
|
||||
pub fn add_comment(&mut self, insn_index: BranchOffset, comment: &'static str) {
|
||||
if let Some(comments) = &mut self.comments {
|
||||
comments.insert(insn_index.to_offset_int(), comment);
|
||||
comments.push((insn_index.to_offset_int(), comment));
|
||||
}
|
||||
}
|
||||
|
||||
// Emit an instruction that will be put at the end of the program (after Transaction statement).
|
||||
// This is useful for instructions that otherwise will be unnecessarily repeated in a loop.
|
||||
// Example: In `SELECT * from users where name='John'`, it is unnecessary to set r[1]='John' as we SCAN users table.
|
||||
// We could simply set it once before the SCAN started.
|
||||
pub fn mark_last_insn_constant(&mut self) {
|
||||
self.constant_insns.push(self.insns.pop().unwrap());
|
||||
if self.constant_span_is_open() {
|
||||
// no need to mark this insn as constant as the surrounding parent expression is already constant
|
||||
return;
|
||||
}
|
||||
|
||||
let prev = self.insns.len().saturating_sub(1);
|
||||
self.constant_spans.push((prev, prev));
|
||||
}
|
||||
|
||||
pub fn emit_constant_insns(&mut self) {
|
||||
self.insns.append(&mut self.constant_insns);
|
||||
// move compile-time constant instructions to the end of the program, where they are executed once after Init jumps to it.
|
||||
// any label_to_resolved_offset that points to an instruction within any moved constant span should be updated to point to the new location.
|
||||
|
||||
// the instruction reordering can be done by sorting the insns, so that the ordering is:
|
||||
// 1. if insn not in any constant span, it stays where it is
|
||||
// 2. if insn is in a constant span, it is after other insns, except those that are in a later constant span
|
||||
// 3. within a single constant span the order is preserver
|
||||
self.insns.sort_by(|(_, _, index_a), (_, _, index_b)| {
|
||||
let a_span = self
|
||||
.constant_spans
|
||||
.iter()
|
||||
.find(|span| span.0 <= *index_a && span.1 >= *index_a);
|
||||
let b_span = self
|
||||
.constant_spans
|
||||
.iter()
|
||||
.find(|span| span.0 <= *index_b && span.1 >= *index_b);
|
||||
if a_span.is_some() && b_span.is_some() {
|
||||
a_span.unwrap().0.cmp(&b_span.unwrap().0)
|
||||
} else if a_span.is_some() {
|
||||
Ordering::Greater
|
||||
} else if b_span.is_some() {
|
||||
Ordering::Less
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
});
|
||||
for resolved_offset in self.label_to_resolved_offset.iter_mut() {
|
||||
if let Some((old_offset, target)) = resolved_offset {
|
||||
let new_offset = self
|
||||
.insns
|
||||
.iter()
|
||||
.position(|(_, _, index)| *old_offset == *index as u32)
|
||||
.unwrap() as u32;
|
||||
*resolved_offset = Some((new_offset, *target));
|
||||
}
|
||||
}
|
||||
|
||||
// Fix comments to refer to new locations
|
||||
if let Some(comments) = &mut self.comments {
|
||||
for (old_offset, _) in comments.iter_mut() {
|
||||
let new_offset = self
|
||||
.insns
|
||||
.iter()
|
||||
.position(|(_, _, index)| *old_offset == *index as u32)
|
||||
.expect("comment must exist") as u32;
|
||||
*old_offset = new_offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn offset(&self) -> BranchOffset {
|
||||
@@ -220,18 +321,42 @@ impl ProgramBuilder {
|
||||
BranchOffset::Label(label_n as u32)
|
||||
}
|
||||
|
||||
// Effectively a GOTO <next insn> without the need to emit an explicit GOTO instruction.
|
||||
// Useful when you know you need to jump to "the next part", but the exact offset is unknowable
|
||||
// at the time of emitting the instruction.
|
||||
/// Resolve a label to whatever instruction follows the one that was
|
||||
/// last emitted.
|
||||
///
|
||||
/// Use this when your use case is: "the program should jump to whatever instruction
|
||||
/// follows the one that was previously emitted", and you don't care exactly
|
||||
/// which instruction that is. Examples include "the start of a loop", or
|
||||
/// "after the loop ends".
|
||||
///
|
||||
/// It is important to handle those cases this way, because the precise
|
||||
/// instruction that follows any given instruction might change due to
|
||||
/// reordering the emitted instructions.
|
||||
#[inline]
|
||||
pub fn preassign_label_to_next_insn(&mut self, label: BranchOffset) {
|
||||
self.next_insn_labels.push(label);
|
||||
assert!(label.is_label(), "BranchOffset {:?} is not a label", label);
|
||||
self._resolve_label(label, self.offset().sub(1u32), JumpTarget::AfterThisInsn);
|
||||
}
|
||||
|
||||
/// Resolve a label to exactly the instruction that was last emitted.
|
||||
///
|
||||
/// Use this when your use case is: "the program should jump to the exact instruction
|
||||
/// that was last emitted", and you don't care WHERE exactly that ends up being
|
||||
/// once the order of the bytecode of the program is finalized. Examples include
|
||||
/// "jump to the Halt instruction", or "jump to the Next instruction of a loop".
|
||||
#[inline]
|
||||
pub fn resolve_label(&mut self, label: BranchOffset, to_offset: BranchOffset) {
|
||||
self._resolve_label(label, to_offset, JumpTarget::ExactlyThisInsn);
|
||||
}
|
||||
|
||||
fn _resolve_label(&mut self, label: BranchOffset, to_offset: BranchOffset, target: JumpTarget) {
|
||||
assert!(matches!(label, BranchOffset::Label(_)));
|
||||
assert!(matches!(to_offset, BranchOffset::Offset(_)));
|
||||
self.label_to_resolved_offset[label.to_label_value() as usize] =
|
||||
Some(to_offset.to_offset_int());
|
||||
let BranchOffset::Label(label_number) = label else {
|
||||
unreachable!("Label is not a label");
|
||||
};
|
||||
self.label_to_resolved_offset[label_number as usize] =
|
||||
Some((to_offset.to_offset_int(), target));
|
||||
}
|
||||
|
||||
/// Resolve unresolved labels to a specific offset in the instruction list.
|
||||
@@ -242,19 +367,25 @@ impl ProgramBuilder {
|
||||
pub fn resolve_labels(&mut self) {
|
||||
let resolve = |pc: &mut BranchOffset, insn_name: &str| {
|
||||
if let BranchOffset::Label(label) = pc {
|
||||
let to_offset = self
|
||||
.label_to_resolved_offset
|
||||
.get(*label as usize)
|
||||
.unwrap_or_else(|| {
|
||||
panic!("Reference to undefined label in {}: {}", insn_name, label)
|
||||
});
|
||||
let Some(Some((to_offset, target))) =
|
||||
self.label_to_resolved_offset.get(*label as usize)
|
||||
else {
|
||||
panic!(
|
||||
"Reference to undefined or unresolved label in {}: {}",
|
||||
insn_name, label
|
||||
);
|
||||
};
|
||||
*pc = BranchOffset::Offset(
|
||||
to_offset
|
||||
.unwrap_or_else(|| panic!("Unresolved label in {}: {}", insn_name, label)),
|
||||
+ if *target == JumpTarget::ExactlyThisInsn {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
for (insn, _) in self.insns.iter_mut() {
|
||||
for (insn, _, _) in self.insns.iter_mut() {
|
||||
match insn {
|
||||
Insn::Init { target_pc } => {
|
||||
resolve(target_pc, "Init");
|
||||
@@ -321,17 +452,11 @@ impl ProgramBuilder {
|
||||
} => {
|
||||
resolve(target_pc, "IfNot");
|
||||
}
|
||||
Insn::RewindAwait {
|
||||
cursor_id: _cursor_id,
|
||||
pc_if_empty,
|
||||
} => {
|
||||
resolve(pc_if_empty, "RewindAwait");
|
||||
Insn::Rewind { pc_if_empty, .. } => {
|
||||
resolve(pc_if_empty, "Rewind");
|
||||
}
|
||||
Insn::LastAwait {
|
||||
cursor_id: _cursor_id,
|
||||
pc_if_empty,
|
||||
} => {
|
||||
resolve(pc_if_empty, "LastAwait");
|
||||
Insn::Last { pc_if_empty, .. } => {
|
||||
resolve(pc_if_empty, "Last");
|
||||
}
|
||||
Insn::Goto { target_pc } => {
|
||||
resolve(target_pc, "Goto");
|
||||
@@ -360,18 +485,25 @@ impl ProgramBuilder {
|
||||
Insn::IfPos { target_pc, .. } => {
|
||||
resolve(target_pc, "IfPos");
|
||||
}
|
||||
Insn::NextAwait { pc_if_next, .. } => {
|
||||
resolve(pc_if_next, "NextAwait");
|
||||
Insn::Next { pc_if_next, .. } => {
|
||||
resolve(pc_if_next, "Next");
|
||||
}
|
||||
Insn::PrevAwait { pc_if_next, .. } => {
|
||||
resolve(pc_if_next, "PrevAwait");
|
||||
Insn::Once {
|
||||
target_pc_when_reentered,
|
||||
..
|
||||
} => {
|
||||
resolve(target_pc_when_reentered, "Once");
|
||||
}
|
||||
Insn::Prev { pc_if_prev, .. } => {
|
||||
resolve(pc_if_prev, "Prev");
|
||||
}
|
||||
Insn::InitCoroutine {
|
||||
yield_reg: _,
|
||||
jump_on_definition,
|
||||
start_offset: _,
|
||||
start_offset,
|
||||
} => {
|
||||
resolve(jump_on_definition, "InitCoroutine");
|
||||
resolve(start_offset, "InitCoroutine");
|
||||
}
|
||||
Insn::NotExists {
|
||||
cursor: _,
|
||||
@@ -407,6 +539,12 @@ impl ProgramBuilder {
|
||||
Insn::SeekGT { target_pc, .. } => {
|
||||
resolve(target_pc, "SeekGT");
|
||||
}
|
||||
Insn::SeekLE { target_pc, .. } => {
|
||||
resolve(target_pc, "SeekLE");
|
||||
}
|
||||
Insn::SeekLT { target_pc, .. } => {
|
||||
resolve(target_pc, "SeekLT");
|
||||
}
|
||||
Insn::IdxGE { target_pc, .. } => {
|
||||
resolve(target_pc, "IdxGE");
|
||||
}
|
||||
@@ -428,6 +566,9 @@ impl ProgramBuilder {
|
||||
Insn::VFilter { pc_if_empty, .. } => {
|
||||
resolve(pc_if_empty, "VFilter");
|
||||
}
|
||||
Insn::NoConflict { target_pc, .. } => {
|
||||
resolve(target_pc, "NoConflict");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -435,15 +576,17 @@ impl ProgramBuilder {
|
||||
}
|
||||
|
||||
// translate table to cursor id
|
||||
pub fn resolve_cursor_id_safe(&self, table_identifier: &str) -> Option<CursorID> {
|
||||
self.cursor_ref.iter().position(|(t_ident, _)| {
|
||||
t_ident
|
||||
.as_ref()
|
||||
.is_some_and(|ident| ident == table_identifier)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn resolve_cursor_id(&self, table_identifier: &str) -> CursorID {
|
||||
self.cursor_ref
|
||||
.iter()
|
||||
.position(|(t_ident, _)| {
|
||||
t_ident
|
||||
.as_ref()
|
||||
.is_some_and(|ident| ident == table_identifier)
|
||||
})
|
||||
.unwrap()
|
||||
self.resolve_cursor_id_safe(table_identifier)
|
||||
.unwrap_or_else(|| panic!("Cursor not found: {}", table_identifier))
|
||||
}
|
||||
|
||||
pub fn build(
|
||||
@@ -453,15 +596,15 @@ impl ProgramBuilder {
|
||||
change_cnt_on: bool,
|
||||
) -> Program {
|
||||
self.resolve_labels();
|
||||
assert!(
|
||||
self.constant_insns.is_empty(),
|
||||
"constant_insns is not empty when build() is called, did you forget to call emit_constant_insns()?"
|
||||
);
|
||||
|
||||
self.parameters.list.dedup();
|
||||
Program {
|
||||
max_registers: self.next_free_register,
|
||||
insns: self.insns,
|
||||
insns: self
|
||||
.insns
|
||||
.into_iter()
|
||||
.map(|(insn, function, _)| (insn, function))
|
||||
.collect(),
|
||||
cursor_ref: self.cursor_ref,
|
||||
database_header,
|
||||
comments: self.comments,
|
||||
|
||||
1907
core/vdbe/execute.rs
1907
core/vdbe/execute.rs
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,6 @@
|
||||
use crate::vdbe::builder::CursorType;
|
||||
use limbo_sqlite3_parser::ast::SortOrder;
|
||||
|
||||
use crate::vdbe::{builder::CursorType, insn::RegisterOrLiteral};
|
||||
|
||||
use super::{Insn, InsnReference, OwnedValue, Program};
|
||||
use crate::function::{Func, ScalarFunc};
|
||||
@@ -336,11 +338,11 @@ pub fn insn_to_str(
|
||||
0,
|
||||
format!("if !r[{}] goto {}", reg, target_pc.to_debug_int()),
|
||||
),
|
||||
Insn::OpenReadAsync {
|
||||
Insn::OpenRead {
|
||||
cursor_id,
|
||||
root_page,
|
||||
} => (
|
||||
"OpenReadAsync",
|
||||
"OpenRead",
|
||||
*cursor_id as i32,
|
||||
*root_page as i32,
|
||||
0,
|
||||
@@ -355,17 +357,8 @@ pub fn insn_to_str(
|
||||
root_page
|
||||
),
|
||||
),
|
||||
Insn::OpenReadAwait => (
|
||||
"OpenReadAwait",
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
OwnedValue::build_text(""),
|
||||
0,
|
||||
"".to_string(),
|
||||
),
|
||||
Insn::VOpenAsync { cursor_id } => (
|
||||
"VOpenAsync",
|
||||
Insn::VOpen { cursor_id } => (
|
||||
"VOpen",
|
||||
*cursor_id as i32,
|
||||
0,
|
||||
0,
|
||||
@@ -373,15 +366,6 @@ pub fn insn_to_str(
|
||||
0,
|
||||
"".to_string(),
|
||||
),
|
||||
Insn::VOpenAwait => (
|
||||
"VOpenAwait",
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
OwnedValue::build_text(""),
|
||||
0,
|
||||
"".to_string(),
|
||||
),
|
||||
Insn::VCreate {
|
||||
table_name,
|
||||
module_name,
|
||||
@@ -449,6 +433,15 @@ pub fn insn_to_str(
|
||||
0,
|
||||
"".to_string(),
|
||||
),
|
||||
Insn::VDestroy { db, table_name } => (
|
||||
"VDestroy",
|
||||
*db as i32,
|
||||
0,
|
||||
0,
|
||||
OwnedValue::build_text(table_name),
|
||||
0,
|
||||
"".to_string(),
|
||||
),
|
||||
Insn::OpenPseudo {
|
||||
cursor_id,
|
||||
content_reg,
|
||||
@@ -462,27 +455,18 @@ pub fn insn_to_str(
|
||||
0,
|
||||
format!("{} columns in r[{}]", num_fields, content_reg),
|
||||
),
|
||||
Insn::RewindAsync { cursor_id } => (
|
||||
"RewindAsync",
|
||||
*cursor_id as i32,
|
||||
0,
|
||||
0,
|
||||
OwnedValue::build_text(""),
|
||||
0,
|
||||
"".to_string(),
|
||||
),
|
||||
Insn::RewindAwait {
|
||||
Insn::Rewind {
|
||||
cursor_id,
|
||||
pc_if_empty,
|
||||
} => (
|
||||
"RewindAwait",
|
||||
"Rewind",
|
||||
*cursor_id as i32,
|
||||
pc_if_empty.to_debug_int(),
|
||||
0,
|
||||
OwnedValue::build_text(""),
|
||||
0,
|
||||
format!(
|
||||
"Rewind table {}",
|
||||
"Rewind {}",
|
||||
program.cursor_ref[*cursor_id]
|
||||
.0
|
||||
.as_ref()
|
||||
@@ -528,6 +512,20 @@ pub fn insn_to_str(
|
||||
),
|
||||
)
|
||||
}
|
||||
Insn::TypeCheck {
|
||||
start_reg,
|
||||
count,
|
||||
check_generated,
|
||||
..
|
||||
} => (
|
||||
"TypeCheck",
|
||||
*start_reg as i32,
|
||||
*count as i32,
|
||||
*check_generated as i32,
|
||||
OwnedValue::build_text(""),
|
||||
0,
|
||||
String::from(""),
|
||||
),
|
||||
Insn::MakeRecord {
|
||||
start_reg,
|
||||
count,
|
||||
@@ -559,20 +557,11 @@ pub fn insn_to_str(
|
||||
format!("output=r[{}..{}]", start_reg, start_reg + count - 1)
|
||||
},
|
||||
),
|
||||
Insn::NextAsync { cursor_id } => (
|
||||
"NextAsync",
|
||||
*cursor_id as i32,
|
||||
0,
|
||||
0,
|
||||
OwnedValue::build_text(""),
|
||||
0,
|
||||
"".to_string(),
|
||||
),
|
||||
Insn::NextAwait {
|
||||
Insn::Next {
|
||||
cursor_id,
|
||||
pc_if_next,
|
||||
} => (
|
||||
"NextAwait",
|
||||
"Next",
|
||||
*cursor_id as i32,
|
||||
pc_if_next.to_debug_int(),
|
||||
0,
|
||||
@@ -582,13 +571,13 @@ pub fn insn_to_str(
|
||||
),
|
||||
Insn::Halt {
|
||||
err_code,
|
||||
description: _,
|
||||
description,
|
||||
} => (
|
||||
"Halt",
|
||||
*err_code as i32,
|
||||
0,
|
||||
0,
|
||||
OwnedValue::build_text(""),
|
||||
OwnedValue::build_text(&description),
|
||||
0,
|
||||
"".to_string(),
|
||||
),
|
||||
@@ -697,6 +686,22 @@ pub fn insn_to_str(
|
||||
.unwrap_or(&format!("cursor {}", cursor_id))
|
||||
),
|
||||
),
|
||||
Insn::IdxRowId { cursor_id, dest } => (
|
||||
"IdxRowId",
|
||||
*cursor_id as i32,
|
||||
*dest as i32,
|
||||
0,
|
||||
OwnedValue::build_text(""),
|
||||
0,
|
||||
format!(
|
||||
"r[{}]={}.rowid",
|
||||
dest,
|
||||
&program.cursor_ref[*cursor_id]
|
||||
.0
|
||||
.as_ref()
|
||||
.unwrap_or(&format!("cursor {}", cursor_id))
|
||||
),
|
||||
),
|
||||
Insn::SeekRowid {
|
||||
cursor_id,
|
||||
src_reg,
|
||||
@@ -734,87 +739,105 @@ pub fn insn_to_str(
|
||||
is_index: _,
|
||||
cursor_id,
|
||||
start_reg,
|
||||
num_regs: _,
|
||||
num_regs,
|
||||
target_pc,
|
||||
}
|
||||
| Insn::SeekGE {
|
||||
is_index: _,
|
||||
cursor_id,
|
||||
start_reg,
|
||||
num_regs,
|
||||
target_pc,
|
||||
}
|
||||
| Insn::SeekLE {
|
||||
is_index: _,
|
||||
cursor_id,
|
||||
start_reg,
|
||||
num_regs,
|
||||
target_pc,
|
||||
}
|
||||
| Insn::SeekLT {
|
||||
is_index: _,
|
||||
cursor_id,
|
||||
start_reg,
|
||||
num_regs,
|
||||
target_pc,
|
||||
} => (
|
||||
"SeekGT",
|
||||
match insn {
|
||||
Insn::SeekGT { .. } => "SeekGT",
|
||||
Insn::SeekGE { .. } => "SeekGE",
|
||||
Insn::SeekLE { .. } => "SeekLE",
|
||||
Insn::SeekLT { .. } => "SeekLT",
|
||||
_ => unreachable!(),
|
||||
},
|
||||
*cursor_id as i32,
|
||||
target_pc.to_debug_int(),
|
||||
*start_reg as i32,
|
||||
OwnedValue::build_text(""),
|
||||
0,
|
||||
format!("key=[{}..{}]", start_reg, start_reg + num_regs - 1),
|
||||
),
|
||||
Insn::SeekEnd { cursor_id } => (
|
||||
"SeekEnd",
|
||||
*cursor_id as i32,
|
||||
0,
|
||||
0,
|
||||
OwnedValue::build_text(""),
|
||||
0,
|
||||
"".to_string(),
|
||||
),
|
||||
Insn::SeekGE {
|
||||
is_index: _,
|
||||
Insn::IdxInsert {
|
||||
cursor_id,
|
||||
start_reg,
|
||||
num_regs: _,
|
||||
target_pc,
|
||||
record_reg,
|
||||
unpacked_start,
|
||||
flags,
|
||||
..
|
||||
} => (
|
||||
"SeekGE",
|
||||
"IdxInsert",
|
||||
*cursor_id as i32,
|
||||
target_pc.to_debug_int(),
|
||||
*start_reg as i32,
|
||||
*record_reg as i32,
|
||||
unpacked_start.unwrap_or(0) as i32,
|
||||
OwnedValue::build_text(""),
|
||||
0,
|
||||
"".to_string(),
|
||||
flags.0 as u16,
|
||||
format!("key=r[{}]", record_reg),
|
||||
),
|
||||
Insn::IdxGT {
|
||||
cursor_id,
|
||||
start_reg,
|
||||
num_regs: _,
|
||||
num_regs,
|
||||
target_pc,
|
||||
} => (
|
||||
"IdxGT",
|
||||
*cursor_id as i32,
|
||||
target_pc.to_debug_int(),
|
||||
*start_reg as i32,
|
||||
OwnedValue::build_text(""),
|
||||
0,
|
||||
"".to_string(),
|
||||
),
|
||||
Insn::IdxGE {
|
||||
}
|
||||
| Insn::IdxGE {
|
||||
cursor_id,
|
||||
start_reg,
|
||||
num_regs: _,
|
||||
num_regs,
|
||||
target_pc,
|
||||
} => (
|
||||
"IdxGE",
|
||||
*cursor_id as i32,
|
||||
target_pc.to_debug_int(),
|
||||
*start_reg as i32,
|
||||
OwnedValue::build_text(""),
|
||||
0,
|
||||
"".to_string(),
|
||||
),
|
||||
Insn::IdxLT {
|
||||
}
|
||||
| Insn::IdxLE {
|
||||
cursor_id,
|
||||
start_reg,
|
||||
num_regs: _,
|
||||
num_regs,
|
||||
target_pc,
|
||||
} => (
|
||||
"IdxLT",
|
||||
*cursor_id as i32,
|
||||
target_pc.to_debug_int(),
|
||||
*start_reg as i32,
|
||||
OwnedValue::build_text(""),
|
||||
0,
|
||||
"".to_string(),
|
||||
),
|
||||
Insn::IdxLE {
|
||||
}
|
||||
| Insn::IdxLT {
|
||||
cursor_id,
|
||||
start_reg,
|
||||
num_regs: _,
|
||||
num_regs,
|
||||
target_pc,
|
||||
} => (
|
||||
"IdxLE",
|
||||
match insn {
|
||||
Insn::IdxGT { .. } => "IdxGT",
|
||||
Insn::IdxGE { .. } => "IdxGE",
|
||||
Insn::IdxLE { .. } => "IdxLE",
|
||||
Insn::IdxLT { .. } => "IdxLT",
|
||||
_ => unreachable!(),
|
||||
},
|
||||
*cursor_id as i32,
|
||||
target_pc.to_debug_int(),
|
||||
*start_reg as i32,
|
||||
OwnedValue::build_text(""),
|
||||
0,
|
||||
"".to_string(),
|
||||
format!("key=[{}..{}]", start_reg, start_reg + num_regs - 1),
|
||||
),
|
||||
Insn::DecrJumpZero { reg, target_pc } => (
|
||||
"DecrJumpZero",
|
||||
@@ -855,17 +878,10 @@ pub fn insn_to_str(
|
||||
} => {
|
||||
let _p4 = String::new();
|
||||
let to_print: Vec<String> = order
|
||||
.get_values()
|
||||
.iter()
|
||||
.map(|v| match v {
|
||||
OwnedValue::Integer(i) => {
|
||||
if *i == 0 {
|
||||
"B".to_string()
|
||||
} else {
|
||||
"-B".to_string()
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
SortOrder::Asc => "B".to_string(),
|
||||
SortOrder::Desc => "-B".to_string(),
|
||||
})
|
||||
.collect();
|
||||
(
|
||||
@@ -993,13 +1009,13 @@ pub fn insn_to_str(
|
||||
0,
|
||||
"".to_string(),
|
||||
),
|
||||
Insn::InsertAsync {
|
||||
Insn::Insert {
|
||||
cursor,
|
||||
key_reg,
|
||||
record_reg,
|
||||
flag,
|
||||
} => (
|
||||
"InsertAsync",
|
||||
"Insert",
|
||||
*cursor as i32,
|
||||
*record_reg as i32,
|
||||
*key_reg as i32,
|
||||
@@ -1007,8 +1023,8 @@ pub fn insn_to_str(
|
||||
*flag as u16,
|
||||
"".to_string(),
|
||||
),
|
||||
Insn::InsertAwait { cursor_id } => (
|
||||
"InsertAwait",
|
||||
Insn::Delete { cursor_id } => (
|
||||
"Delete",
|
||||
*cursor_id as i32,
|
||||
0,
|
||||
0,
|
||||
@@ -1016,20 +1032,15 @@ pub fn insn_to_str(
|
||||
0,
|
||||
"".to_string(),
|
||||
),
|
||||
Insn::DeleteAsync { cursor_id } => (
|
||||
"DeleteAsync",
|
||||
Insn::IdxDelete {
|
||||
cursor_id,
|
||||
start_reg,
|
||||
num_regs,
|
||||
} => (
|
||||
"IdxDelete",
|
||||
*cursor_id as i32,
|
||||
0,
|
||||
0,
|
||||
OwnedValue::build_text(""),
|
||||
0,
|
||||
"".to_string(),
|
||||
),
|
||||
Insn::DeleteAwait { cursor_id } => (
|
||||
"DeleteAwait",
|
||||
*cursor_id as i32,
|
||||
0,
|
||||
0,
|
||||
*start_reg as i32,
|
||||
*num_regs as i32,
|
||||
OwnedValue::build_text(""),
|
||||
0,
|
||||
"".to_string(),
|
||||
@@ -1065,6 +1076,20 @@ pub fn insn_to_str(
|
||||
0,
|
||||
"".to_string(),
|
||||
),
|
||||
Insn::NoConflict {
|
||||
cursor_id,
|
||||
target_pc,
|
||||
record_reg,
|
||||
num_regs,
|
||||
} => (
|
||||
"NoConflict",
|
||||
*cursor_id as i32,
|
||||
target_pc.to_debug_int(),
|
||||
*record_reg as i32,
|
||||
OwnedValue::build_text(&format!("{num_regs}")),
|
||||
0,
|
||||
format!("key=r[{}]", record_reg),
|
||||
),
|
||||
Insn::NotExists {
|
||||
cursor,
|
||||
rowid_reg,
|
||||
@@ -1094,22 +1119,17 @@ pub fn insn_to_str(
|
||||
limit_reg, combined_reg, limit_reg, offset_reg, combined_reg
|
||||
),
|
||||
),
|
||||
Insn::OpenWriteAsync {
|
||||
Insn::OpenWrite {
|
||||
cursor_id,
|
||||
root_page,
|
||||
..
|
||||
} => (
|
||||
"OpenWriteAsync",
|
||||
"OpenWrite",
|
||||
*cursor_id as i32,
|
||||
*root_page as i32,
|
||||
0,
|
||||
OwnedValue::build_text(""),
|
||||
0,
|
||||
"".to_string(),
|
||||
),
|
||||
Insn::OpenWriteAwait {} => (
|
||||
"OpenWriteAwait",
|
||||
0,
|
||||
0,
|
||||
match root_page {
|
||||
RegisterOrLiteral::Literal(i) => *i as _,
|
||||
RegisterOrLiteral::Register(i) => *i as _,
|
||||
},
|
||||
0,
|
||||
OwnedValue::build_text(""),
|
||||
0,
|
||||
@@ -1132,10 +1152,10 @@ pub fn insn_to_str(
|
||||
"CreateBtree",
|
||||
*db as i32,
|
||||
*root as i32,
|
||||
*flags as i32,
|
||||
flags.get_flags() as i32,
|
||||
OwnedValue::build_text(""),
|
||||
0,
|
||||
format!("r[{}]=root iDb={} flags={}", root, db, flags),
|
||||
format!("r[{}]=root iDb={} flags={}", root, db, flags.get_flags()),
|
||||
),
|
||||
Insn::Destroy {
|
||||
root,
|
||||
@@ -1176,10 +1196,13 @@ pub fn insn_to_str(
|
||||
0,
|
||||
"".to_string(),
|
||||
),
|
||||
Insn::LastAsync { .. } => (
|
||||
"LastAsync",
|
||||
0,
|
||||
0,
|
||||
Insn::Last {
|
||||
cursor_id,
|
||||
pc_if_empty,
|
||||
} => (
|
||||
"Last",
|
||||
*cursor_id as i32,
|
||||
pc_if_empty.to_debug_int(),
|
||||
0,
|
||||
OwnedValue::build_text(""),
|
||||
0,
|
||||
@@ -1203,28 +1226,13 @@ pub fn insn_to_str(
|
||||
0,
|
||||
where_clause.clone(),
|
||||
),
|
||||
Insn::LastAwait { .. } => (
|
||||
"LastAwait",
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
OwnedValue::build_text(""),
|
||||
0,
|
||||
"".to_string(),
|
||||
),
|
||||
Insn::PrevAsync { .. } => (
|
||||
"PrevAsync",
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
OwnedValue::build_text(""),
|
||||
0,
|
||||
"".to_string(),
|
||||
),
|
||||
Insn::PrevAwait { .. } => (
|
||||
"PrevAwait",
|
||||
0,
|
||||
0,
|
||||
Insn::Prev {
|
||||
cursor_id,
|
||||
pc_if_prev,
|
||||
} => (
|
||||
"Prev",
|
||||
*cursor_id as i32,
|
||||
pc_if_prev.to_debug_int(),
|
||||
0,
|
||||
OwnedValue::build_text(""),
|
||||
0,
|
||||
@@ -1344,6 +1352,93 @@ pub fn insn_to_str(
|
||||
0,
|
||||
format!("auto_commit={}, rollback={}", auto_commit, rollback),
|
||||
),
|
||||
Insn::OpenEphemeral {
|
||||
cursor_id,
|
||||
is_table,
|
||||
} => (
|
||||
"OpenEphemeral",
|
||||
*cursor_id as i32,
|
||||
*is_table as i32,
|
||||
0,
|
||||
OwnedValue::build_text(""),
|
||||
0,
|
||||
format!(
|
||||
"cursor={} is_table={}",
|
||||
cursor_id,
|
||||
if *is_table { "true" } else { "false" }
|
||||
),
|
||||
),
|
||||
Insn::OpenAutoindex { cursor_id } => (
|
||||
"OpenAutoindex",
|
||||
*cursor_id as i32,
|
||||
0,
|
||||
0,
|
||||
OwnedValue::build_text(""),
|
||||
0,
|
||||
format!("cursor={}", cursor_id),
|
||||
),
|
||||
Insn::Once {
|
||||
target_pc_when_reentered,
|
||||
} => (
|
||||
"Once",
|
||||
target_pc_when_reentered.to_debug_int(),
|
||||
0,
|
||||
0,
|
||||
OwnedValue::build_text(""),
|
||||
0,
|
||||
format!("goto {}", target_pc_when_reentered.to_debug_int()),
|
||||
),
|
||||
Insn::BeginSubrtn { dest, dest_end } => (
|
||||
"BeginSubrtn",
|
||||
*dest as i32,
|
||||
dest_end.map_or(0, |end| end as i32),
|
||||
0,
|
||||
OwnedValue::build_text(""),
|
||||
0,
|
||||
dest_end.map_or(format!("r[{}]=NULL", dest), |end| {
|
||||
format!("r[{}..{}]=NULL", dest, end)
|
||||
}),
|
||||
),
|
||||
Insn::NotFound {
|
||||
cursor_id,
|
||||
target_pc,
|
||||
record_reg,
|
||||
..
|
||||
} => (
|
||||
"NotFound",
|
||||
*cursor_id as i32,
|
||||
target_pc.to_debug_int(),
|
||||
*record_reg as i32,
|
||||
OwnedValue::build_text(""),
|
||||
0,
|
||||
format!(
|
||||
"if (r[{}] != NULL) goto {}",
|
||||
record_reg,
|
||||
target_pc.to_debug_int()
|
||||
),
|
||||
),
|
||||
Insn::Affinity {
|
||||
start_reg,
|
||||
count,
|
||||
affinities,
|
||||
} => (
|
||||
"Affinity",
|
||||
*start_reg as i32,
|
||||
count.get() as i32,
|
||||
0,
|
||||
OwnedValue::build_text(""),
|
||||
0,
|
||||
format!(
|
||||
"r[{}..{}] = {}",
|
||||
start_reg,
|
||||
start_reg + count.get(),
|
||||
affinities
|
||||
.chars()
|
||||
.map(|a| a.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
),
|
||||
),
|
||||
};
|
||||
format!(
|
||||
"{:<4} {:<17} {:<4} {:<4} {:<4} {:<13} {:<2} {}",
|
||||
|
||||
1527
core/vdbe/insn.rs
1527
core/vdbe/insn.rs
File diff suppressed because it is too large
Load Diff
190
core/vdbe/mod.rs
190
core/vdbe/mod.rs
@@ -24,37 +24,61 @@ pub mod insn;
|
||||
pub mod likeop;
|
||||
pub mod sorter;
|
||||
|
||||
use crate::error::LimboError;
|
||||
use crate::fast_lock::SpinLock;
|
||||
use crate::function::{AggFunc, FuncCtx};
|
||||
|
||||
use crate::storage::sqlite3_ondisk::DatabaseHeader;
|
||||
use crate::storage::{btree::BTreeCursor, pager::Pager};
|
||||
use crate::translate::plan::{ResultSetColumn, TableReference};
|
||||
use crate::types::{
|
||||
AggContext, Cursor, CursorResult, ImmutableRecord, OwnedValue, SeekKey, SeekOp,
|
||||
use crate::{
|
||||
error::LimboError,
|
||||
fast_lock::SpinLock,
|
||||
function::{AggFunc, FuncCtx},
|
||||
storage::sqlite3_ondisk::SmallVec,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
storage::{btree::BTreeCursor, pager::Pager, sqlite3_ondisk::DatabaseHeader},
|
||||
translate::plan::{ResultSetColumn, TableReference},
|
||||
types::{AggContext, Cursor, CursorResult, ImmutableRecord, OwnedValue, SeekKey, SeekOp},
|
||||
vdbe::{builder::CursorType, insn::Insn},
|
||||
};
|
||||
use crate::util::cast_text_to_numeric;
|
||||
use crate::vdbe::builder::CursorType;
|
||||
use crate::vdbe::insn::Insn;
|
||||
|
||||
use crate::CheckpointStatus;
|
||||
|
||||
#[cfg(feature = "json")]
|
||||
use crate::json::JsonCacheCell;
|
||||
use crate::{Connection, MvStore, Result, TransactionState};
|
||||
use execute::{InsnFunction, InsnFunctionStepResult};
|
||||
use execute::{InsnFunction, InsnFunctionStepResult, OpIdxDeleteState};
|
||||
|
||||
use rand::distributions::{Distribution, Uniform};
|
||||
use rand::Rng;
|
||||
use rand::{
|
||||
distributions::{Distribution, Uniform},
|
||||
Rng,
|
||||
};
|
||||
use regex::Regex;
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::c_void;
|
||||
use std::num::NonZero;
|
||||
use std::ops::Deref;
|
||||
use std::rc::{Rc, Weak};
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
collections::HashMap,
|
||||
ffi::c_void,
|
||||
num::NonZero,
|
||||
ops::Deref,
|
||||
rc::{Rc, Weak},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
/// We use labels to indicate that we want to jump to whatever the instruction offset
|
||||
/// will be at runtime, because the offset cannot always be determined when the jump
|
||||
/// instruction is created.
|
||||
///
|
||||
/// In some cases, we want to jump to EXACTLY a specific instruction.
|
||||
/// - Example: a condition is not met, so we want to jump to wherever Halt is.
|
||||
/// In other cases, we don't care what the exact instruction is, but we know that we
|
||||
/// want to jump to whatever comes AFTER a certain instruction.
|
||||
/// - Example: a Next instruction will want to jump to "whatever the start of the loop is",
|
||||
/// but it doesn't care what instruction that is.
|
||||
///
|
||||
/// The reason this distinction is important is that we might reorder instructions that are
|
||||
/// constant at compile time, and when we do that, we need to change the offsets of any impacted
|
||||
/// jump instructions, so the instruction that comes immediately after "next Insn" might have changed during the reordering.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum JumpTarget {
|
||||
ExactlyThisInsn,
|
||||
AfterThisInsn,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
/// Represents a target for a jump instruction.
|
||||
@@ -91,15 +115,6 @@ impl BranchOffset {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the label value. Panics if the branch offset is an offset or placeholder.
|
||||
pub fn to_label_value(&self) -> u32 {
|
||||
match self {
|
||||
BranchOffset::Label(v) => *v,
|
||||
BranchOffset::Offset(_) => unreachable!("Offset cannot be converted to label value"),
|
||||
BranchOffset::Placeholder => unreachable!("Unresolved placeholder"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the branch offset as a signed integer.
|
||||
/// Used in explain output, where we don't want to panic in case we have an unresolved
|
||||
/// label or placeholder.
|
||||
@@ -117,6 +132,10 @@ impl BranchOffset {
|
||||
pub fn add<N: Into<u32>>(self, n: N) -> BranchOffset {
|
||||
BranchOffset::Offset(self.to_offset_int() + n.into())
|
||||
}
|
||||
|
||||
pub fn sub<N: Into<u32>>(self, n: N) -> BranchOffset {
|
||||
BranchOffset::Offset(self.to_offset_int() - n.into())
|
||||
}
|
||||
}
|
||||
|
||||
pub type CursorID = usize;
|
||||
@@ -229,6 +248,8 @@ pub struct ProgramState {
|
||||
last_compare: Option<std::cmp::Ordering>,
|
||||
deferred_seek: Option<(CursorID, CursorID)>,
|
||||
ended_coroutine: Bitfield<4>, // flag to indicate that a coroutine has ended (key is the yield register. currently we assume that the yield register is always between 0-255, YOLO)
|
||||
/// Indicate whether an [Insn::Once] instruction at a given program counter position has already been executed, well, once.
|
||||
once: SmallVec<u32, 4>,
|
||||
regex_cache: RegexCache,
|
||||
pub(crate) mv_tx_id: Option<crate::mvcc::database::TxID>,
|
||||
interrupted: bool,
|
||||
@@ -236,6 +257,7 @@ pub struct ProgramState {
|
||||
halt_state: Option<HaltState>,
|
||||
#[cfg(feature = "json")]
|
||||
json_cache: JsonCacheCell,
|
||||
op_idx_delete_state: Option<OpIdxDeleteState>,
|
||||
}
|
||||
|
||||
impl ProgramState {
|
||||
@@ -251,6 +273,7 @@ impl ProgramState {
|
||||
last_compare: None,
|
||||
deferred_seek: None,
|
||||
ended_coroutine: Bitfield::new(),
|
||||
once: SmallVec::<u32, 4>::new(),
|
||||
regex_cache: RegexCache::new(),
|
||||
mv_tx_id: None,
|
||||
interrupted: false,
|
||||
@@ -258,6 +281,7 @@ impl ProgramState {
|
||||
halt_state: None,
|
||||
#[cfg(feature = "json")]
|
||||
json_cache: JsonCacheCell::new(),
|
||||
op_idx_delete_state: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,8 +305,11 @@ impl ProgramState {
|
||||
self.parameters.insert(index, value);
|
||||
}
|
||||
|
||||
pub fn get_parameter(&self, index: NonZero<usize>) -> Option<&OwnedValue> {
|
||||
self.parameters.get(&index)
|
||||
pub fn get_parameter(&self, index: NonZero<usize>) -> OwnedValue {
|
||||
self.parameters
|
||||
.get(&index)
|
||||
.cloned()
|
||||
.unwrap_or(OwnedValue::Null)
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
@@ -342,7 +369,7 @@ pub struct Program {
|
||||
pub insns: Vec<(Insn, InsnFunction)>,
|
||||
pub cursor_ref: Vec<(Option<String>, CursorType)>,
|
||||
pub database_header: Arc<SpinLock<DatabaseHeader>>,
|
||||
pub comments: Option<HashMap<InsnReference, &'static str>>,
|
||||
pub comments: Option<Vec<(InsnReference, &'static str)>>,
|
||||
pub parameters: crate::parameters::Parameters,
|
||||
pub connection: Weak<Connection>,
|
||||
pub n_change: Cell<i64>,
|
||||
@@ -386,7 +413,7 @@ impl Program {
|
||||
) -> Result<StepResult> {
|
||||
if let Some(mv_store) = mv_store {
|
||||
let conn = self.connection.upgrade().unwrap();
|
||||
let auto_commit = *conn.auto_commit.borrow();
|
||||
let auto_commit = conn.auto_commit.get();
|
||||
if auto_commit {
|
||||
let mut mv_transactions = conn.mv_transactions.borrow_mut();
|
||||
for tx_id in mv_transactions.iter() {
|
||||
@@ -394,13 +421,13 @@ impl Program {
|
||||
}
|
||||
mv_transactions.clear();
|
||||
}
|
||||
return Ok(StepResult::Done);
|
||||
Ok(StepResult::Done)
|
||||
} else {
|
||||
let connection = self
|
||||
.connection
|
||||
.upgrade()
|
||||
.expect("only weak ref to connection?");
|
||||
let auto_commit = *connection.auto_commit.borrow();
|
||||
let auto_commit = connection.auto_commit.get();
|
||||
tracing::trace!("Halt auto_commit {}", auto_commit);
|
||||
assert!(
|
||||
program_state.halt_state.is_none()
|
||||
@@ -408,30 +435,28 @@ impl Program {
|
||||
);
|
||||
if program_state.halt_state.is_some() {
|
||||
self.step_end_write_txn(&pager, &mut program_state.halt_state, connection.deref())
|
||||
} else {
|
||||
if auto_commit {
|
||||
let current_state = connection.transaction_state.borrow().clone();
|
||||
match current_state {
|
||||
TransactionState::Write => self.step_end_write_txn(
|
||||
&pager,
|
||||
&mut program_state.halt_state,
|
||||
connection.deref(),
|
||||
),
|
||||
TransactionState::Read => {
|
||||
connection.transaction_state.replace(TransactionState::None);
|
||||
pager.end_read_tx()?;
|
||||
Ok(StepResult::Done)
|
||||
}
|
||||
TransactionState::None => Ok(StepResult::Done),
|
||||
} else if auto_commit {
|
||||
let current_state = connection.transaction_state.get();
|
||||
match current_state {
|
||||
TransactionState::Write => self.step_end_write_txn(
|
||||
&pager,
|
||||
&mut program_state.halt_state,
|
||||
connection.deref(),
|
||||
),
|
||||
TransactionState::Read => {
|
||||
connection.transaction_state.replace(TransactionState::None);
|
||||
pager.end_read_tx()?;
|
||||
Ok(StepResult::Done)
|
||||
}
|
||||
} else {
|
||||
if self.change_cnt_on {
|
||||
if let Some(conn) = self.connection.upgrade() {
|
||||
conn.set_changes(self.n_change.get());
|
||||
}
|
||||
}
|
||||
Ok(StepResult::Done)
|
||||
TransactionState::None => Ok(StepResult::Done),
|
||||
}
|
||||
} else {
|
||||
if self.change_cnt_on {
|
||||
if let Some(conn) = self.connection.upgrade() {
|
||||
conn.set_changes(self.n_change.get());
|
||||
}
|
||||
}
|
||||
Ok(StepResult::Done)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -534,10 +559,11 @@ fn trace_insn(program: &Program, addr: InsnReference, insn: &Insn) {
|
||||
addr,
|
||||
insn,
|
||||
String::new(),
|
||||
program
|
||||
.comments
|
||||
.as_ref()
|
||||
.and_then(|comments| comments.get(&{ addr }).copied())
|
||||
program.comments.as_ref().and_then(|comments| comments
|
||||
.iter()
|
||||
.find(|(offset, _)| *offset == addr)
|
||||
.map(|(_, comment)| comment)
|
||||
.copied())
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -548,10 +574,13 @@ fn print_insn(program: &Program, addr: InsnReference, insn: &Insn, indent: Strin
|
||||
addr,
|
||||
insn,
|
||||
indent,
|
||||
program
|
||||
.comments
|
||||
.as_ref()
|
||||
.and_then(|comments| comments.get(&{ addr }).copied()),
|
||||
program.comments.as_ref().and_then(|comments| {
|
||||
comments
|
||||
.iter()
|
||||
.find(|(offset, _)| *offset == addr)
|
||||
.map(|(_, comment)| comment)
|
||||
.copied()
|
||||
}),
|
||||
);
|
||||
w.push_str(&s);
|
||||
}
|
||||
@@ -559,11 +588,14 @@ fn print_insn(program: &Program, addr: InsnReference, insn: &Insn, indent: Strin
|
||||
fn get_indent_count(indent_count: usize, curr_insn: &Insn, prev_insn: Option<&Insn>) -> usize {
|
||||
let indent_count = if let Some(insn) = prev_insn {
|
||||
match insn {
|
||||
Insn::RewindAwait { .. }
|
||||
| Insn::LastAwait { .. }
|
||||
Insn::Rewind { .. }
|
||||
| Insn::Last { .. }
|
||||
| Insn::SorterSort { .. }
|
||||
| Insn::SeekGE { .. }
|
||||
| Insn::SeekGT { .. } => indent_count + 1,
|
||||
| Insn::SeekGT { .. }
|
||||
| Insn::SeekLE { .. }
|
||||
| Insn::SeekLT { .. } => indent_count + 1,
|
||||
|
||||
_ => indent_count,
|
||||
}
|
||||
} else {
|
||||
@@ -571,9 +603,7 @@ fn get_indent_count(indent_count: usize, curr_insn: &Insn, prev_insn: Option<&In
|
||||
};
|
||||
|
||||
match curr_insn {
|
||||
Insn::NextAsync { .. } | Insn::SorterNext { .. } | Insn::PrevAsync { .. } => {
|
||||
indent_count - 1
|
||||
}
|
||||
Insn::Next { .. } | Insn::SorterNext { .. } | Insn::Prev { .. } => indent_count - 1,
|
||||
_ => indent_count,
|
||||
}
|
||||
}
|
||||
@@ -593,6 +623,15 @@ impl<'a> FromValueRow<'a> for i64 {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FromValueRow<'a> for f64 {
|
||||
fn from_value(value: &'a OwnedValue) -> Result<Self> {
|
||||
match value {
|
||||
OwnedValue::Float(f) => Ok(*f),
|
||||
_ => Err(LimboError::ConversionError("Expected integer value".into())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FromValueRow<'a> for String {
|
||||
fn from_value(value: &'a OwnedValue) -> Result<Self> {
|
||||
match value {
|
||||
@@ -629,11 +668,10 @@ impl Row {
|
||||
|
||||
pub fn get_value<'a>(&'a self, idx: usize) -> &'a OwnedValue {
|
||||
let value = unsafe { self.values.add(idx).as_ref().unwrap() };
|
||||
let value = match value {
|
||||
match value {
|
||||
Register::OwnedValue(owned_value) => owned_value,
|
||||
_ => unreachable!("a row should be formed of values only"),
|
||||
};
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_values(&self) -> impl Iterator<Item = &OwnedValue> {
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
use crate::types::ImmutableRecord;
|
||||
use std::cmp::Ordering;
|
||||
use limbo_sqlite3_parser::ast::SortOrder;
|
||||
|
||||
use crate::types::{compare_immutable, ImmutableRecord, IndexKeySortOrder};
|
||||
|
||||
pub struct Sorter {
|
||||
records: Vec<ImmutableRecord>,
|
||||
current: Option<ImmutableRecord>,
|
||||
order: Vec<bool>,
|
||||
order: IndexKeySortOrder,
|
||||
key_len: usize,
|
||||
}
|
||||
|
||||
impl Sorter {
|
||||
pub fn new(order: Vec<bool>) -> Self {
|
||||
pub fn new(order: &[SortOrder]) -> Self {
|
||||
Self {
|
||||
records: Vec::new(),
|
||||
current: None,
|
||||
order,
|
||||
key_len: order.len(),
|
||||
order: IndexKeySortOrder::from_list(order),
|
||||
}
|
||||
}
|
||||
pub fn is_empty(&self) -> bool {
|
||||
@@ -26,24 +29,11 @@ impl Sorter {
|
||||
// We do the sorting here since this is what is called by the SorterSort instruction
|
||||
pub fn sort(&mut self) {
|
||||
self.records.sort_by(|a, b| {
|
||||
let cmp_by_idx = |idx: usize, ascending: bool| {
|
||||
let a = &a.get_value(idx);
|
||||
let b = &b.get_value(idx);
|
||||
if ascending {
|
||||
a.cmp(b)
|
||||
} else {
|
||||
b.cmp(a)
|
||||
}
|
||||
};
|
||||
|
||||
let mut cmp_ret = Ordering::Equal;
|
||||
for (idx, &is_asc) in self.order.iter().enumerate() {
|
||||
cmp_ret = cmp_by_idx(idx, is_asc);
|
||||
if cmp_ret != Ordering::Equal {
|
||||
break;
|
||||
}
|
||||
}
|
||||
cmp_ret
|
||||
compare_immutable(
|
||||
&a.values[..self.key_len],
|
||||
&b.values[..self.key_len],
|
||||
self.order,
|
||||
)
|
||||
});
|
||||
self.records.reverse();
|
||||
self.next()
|
||||
|
||||
Reference in New Issue
Block a user