Merge 'introduce eq/contains/starts_with/ends_with_ignore_ascii_case macros' from Lâm Hoàng Phúc

depend on #2865
```sh
`ALTER TABLE _ RENAME TO _`/limbo_rename_table/
                        time:   [10.100 ms 10.191 ms 10.283 ms]
                        change: [-16.770% -15.559% -14.417%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 2 outliers among 100 measurements (2.00%)
  1 (1.00%) low mild
  1 (1.00%) high mild

`ALTER TABLE _ RENAME COLUMN _ TO _`/limbo_rename_column/
                        time:   [7.4829 ms 7.5492 ms 7.6128 ms]
                        change: [-19.397% -18.093% -16.789%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 4 outliers among 100 measurements (4.00%)
  3 (3.00%) low mild
  1 (1.00%) high mild

`ALTER TABLE _ ADD COLUMN _`/limbo_add_column/
                        time:   [5.3255 ms 5.3713 ms 5.4183 ms]
                        change: [-24.002% -22.612% -21.195%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 39 outliers among 100 measurements (39.00%)
  17 (17.00%) low severe
  1 (1.00%) low mild
  1 (1.00%) high mild
  20 (20.00%) high severe

`ALTER TABLE _ DROP COLUMN _`/limbo_drop_column/
                        time:   [5.8858 ms 5.9183 ms 5.9510 ms]
                        change: [-16.233% -14.679% -13.083%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 25 outliers among 100 measurements (25.00%)
  8 (8.00%) low severe
  11 (11.00%) low mild
  2 (2.00%) high mild
  4 (4.00%) high severe

Prepare `SELECT 1`/limbo_parse_query/SELECT 1
                        time:   [590.28 ns 591.31 ns 592.35 ns]
                        change: [-3.7810% -3.5059% -3.2444%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 7 outliers among 100 measurements (7.00%)
  1 (1.00%) low severe
  6 (6.00%) high mild

Prepare `SELECT * FROM users LIMIT 1`/limbo_parse_query/SELECT * FROM users LIMIT 1
                        time:   [1.2569 µs 1.2582 µs 1.2596 µs]
                        change: [-5.0125% -4.7516% -4.4933%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 7 outliers among 100 measurements (7.00%)
  3 (3.00%) low severe
  2 (2.00%) low mild
  1 (1.00%) high mild
  1 (1.00%) high severe

Prepare `SELECT first_name, count(1) FROM users GROUP BY first_name HAVING count(1) > 1 ORDER BY cou...
                        time:   [3.7180 µs 3.7227 µs 3.7274 µs]
                        change: [-3.0557% -2.7642% -2.4761%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 6 outliers among 100 measurements (6.00%)
  2 (2.00%) low mild
  4 (4.00%) high mild

Execute `SELECT 1`/limbo_execute_select_1
                        time:   [27.455 ns 27.477 ns 27.499 ns]
                        change: [-2.9461% -2.7493% -2.5589%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 3 outliers among 100 measurements (3.00%)
  1 (1.00%) low mild
  1 (1.00%) high mild
  1 (1.00%) high severe

Execute `SELECT * FROM users LIMIT ?`/limbo_execute_select_rows/1
                        time:   [410.53 ns 411.05 ns 411.54 ns]
                        change: [-15.364% -15.133% -14.912%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 5 outliers among 100 measurements (5.00%)
  4 (4.00%) low mild
  1 (1.00%) high mild

Execute `SELECT * FROM users LIMIT ?`/limbo_execute_select_rows/10
                        time:   [2.1100 µs 2.1122 µs 2.1145 µs]
                        change: [-11.517% -11.065% -10.662%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 4 outliers among 100 measurements (4.00%)
  2 (2.00%) low severe
  2 (2.00%) low mild

Execute `SELECT * FROM users LIMIT ?`/limbo_execute_select_rows/50
                        time:   [9.5156 µs 9.5268 µs 9.5383 µs]
                        change: [-10.284% -10.086% -9.8833%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 3 outliers among 100 measurements (3.00%)
  1 (1.00%) low severe
  2 (2.00%) low mild

Execute `SELECT * FROM users LIMIT ?`/limbo_execute_select_rows/100
                        time:   [18.669 µs 18.698 µs 18.731 µs]
                        change: [-9.5949% -9.3407% -9.1140%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 2 outliers among 100 measurements (2.00%)
  1 (1.00%) low severe
  1 (1.00%) high mild

Execute `SELECT count() FROM users`/limbo_execute_select_count
                        time:   [7.1027 µs 7.1098 µs 7.1170 µs]
                        change: [-43.739% -43.596% -43.469%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 9 outliers among 100 measurements (9.00%)
  2 (2.00%) low mild
  5 (5.00%) high mild
  2 (2.00%) high severe

```

Closes #2866
This commit is contained in:
Pekka Enberg
2025-09-02 18:35:14 +03:00
committed by GitHub
9 changed files with 261 additions and 183 deletions

View File

@@ -1,9 +1,11 @@
use crate::LimboError::InvalidModifier;
use crate::Result;
use crate::{ends_with_ignore_ascii_case, eq_ignore_ascii_case, starts_with_ignore_ascii_case};
use crate::{types::Value, vdbe::Register};
use chrono::{
DateTime, Datelike, NaiveDate, NaiveDateTime, NaiveTime, TimeDelta, TimeZone, Timelike, Utc,
};
use turso_macros::match_ignore_ascii_case;
/// Execution of date/time/datetime functions
#[inline(always)]
@@ -544,102 +546,123 @@ fn parse_modifier_time(s: &str) -> Result<NaiveTime> {
}
fn parse_modifier(modifier: &str) -> Result<Modifier> {
let modifier = modifier.trim().to_lowercase();
let modifier = modifier.trim().as_bytes();
match modifier.as_str() {
#[inline(always)]
fn from_bytes(bytes: &[u8]) -> &str {
unsafe { str::from_utf8_unchecked(bytes) }
} // safe because input is from &str
match_ignore_ascii_case!(match modifier {
// exact matches first
"ceiling" => Ok(Modifier::Ceiling),
"floor" => Ok(Modifier::Floor),
"start of month" => Ok(Modifier::StartOfMonth),
"start of year" => Ok(Modifier::StartOfYear),
"start of day" => Ok(Modifier::StartOfDay),
s if s.starts_with("weekday ") => {
let day = parse_modifier_number(&s[8..])?;
if !(0..=6).contains(&day) {
Err(InvalidModifier(
"Weekday must be between 0 and 6".to_string(),
))
} else {
Ok(Modifier::Weekday(day as u32))
}
}
"unixepoch" => Ok(Modifier::UnixEpoch),
"julianday" => Ok(Modifier::JulianDay),
"auto" => Ok(Modifier::Auto),
"localtime" => Ok(Modifier::Localtime),
"utc" => Ok(Modifier::Utc),
"subsec" | "subsecond" => Ok(Modifier::Subsec),
s if s.ends_with(" day") => Ok(Modifier::Days(parse_modifier_number(&s[..s.len() - 4])?)),
s if s.ends_with(" days") => Ok(Modifier::Days(parse_modifier_number(&s[..s.len() - 5])?)),
s if s.ends_with(" hour") => Ok(Modifier::Hours(parse_modifier_number(&s[..s.len() - 5])?)),
s if s.ends_with(" hours") => {
Ok(Modifier::Hours(parse_modifier_number(&s[..s.len() - 6])?))
}
s if s.ends_with(" minute") => {
Ok(Modifier::Minutes(parse_modifier_number(&s[..s.len() - 7])?))
}
s if s.ends_with(" minutes") => {
Ok(Modifier::Minutes(parse_modifier_number(&s[..s.len() - 8])?))
}
s if s.ends_with(" second") => {
Ok(Modifier::Seconds(parse_modifier_number(&s[..s.len() - 7])?))
}
s if s.ends_with(" seconds") => {
Ok(Modifier::Seconds(parse_modifier_number(&s[..s.len() - 8])?))
}
s if s.ends_with(" month") => Ok(Modifier::Months(
parse_modifier_number(&s[..s.len() - 6])? as i32,
)),
s if s.ends_with(" months") => Ok(Modifier::Months(
parse_modifier_number(&s[..s.len() - 7])? as i32,
)),
s if s.ends_with(" year") => Ok(Modifier::Years(
parse_modifier_number(&s[..s.len() - 5])? as i32
)),
s if s.ends_with(" years") => Ok(Modifier::Years(
parse_modifier_number(&s[..s.len() - 6])? as i32,
)),
s if s.starts_with('+') || s.starts_with('-') => {
let sign = if s.starts_with('-') { -1 } else { 1 };
let parts: Vec<&str> = s[1..].split(' ').collect();
let digits_in_date = 10;
match parts.len() {
1 => {
if parts[0].len() == digits_in_date {
let date = parse_modifier_date(parts[0])?;
Ok(Modifier::DateOffset {
years: sign * date.year(),
months: sign * date.month() as i32,
days: sign * date.day() as i32,
})
b"ceiling" => Ok(Modifier::Ceiling),
b"floor" => Ok(Modifier::Floor),
b"start of month" => Ok(Modifier::StartOfMonth),
b"start of year" => Ok(Modifier::StartOfYear),
b"start of day" => Ok(Modifier::StartOfDay),
b"unixepoch" => Ok(Modifier::UnixEpoch),
b"julianday" => Ok(Modifier::JulianDay),
b"auto" => Ok(Modifier::Auto),
b"localtime" => Ok(Modifier::Localtime),
b"utc" => Ok(Modifier::Utc),
b"subsec" | b"subsecond" => Ok(Modifier::Subsec),
_ => {
match modifier {
s if starts_with_ignore_ascii_case!(s, b"weekday ") => {
let day = parse_modifier_number(from_bytes(&s[8..]))?;
if !(0..=6).contains(&day) {
Err(InvalidModifier(
"Weekday must be between 0 and 6".to_string(),
))
} else {
// time values are either 12, 8 or 5 digits
let time = parse_modifier_time(parts[0])?;
let time_delta = sign * (time.num_seconds_from_midnight() as i32);
Ok(Modifier::TimeOffset(TimeDelta::seconds(time_delta.into())))
Ok(Modifier::Weekday(day as u32))
}
}
2 => {
let date = parse_modifier_date(parts[0])?;
let time = parse_modifier_time(parts[1])?;
// Convert time to total seconds (with sign)
let time_delta = sign * (time.num_seconds_from_midnight() as i32);
Ok(Modifier::DateTimeOffset {
years: sign * (date.year()),
months: sign * (date.month() as i32),
days: sign * date.day() as i32,
seconds: time_delta,
})
s if ends_with_ignore_ascii_case!(s, b" day") => Ok(Modifier::Days(
parse_modifier_number(from_bytes(&s[..s.len() - 4]))?,
)),
s if ends_with_ignore_ascii_case!(s, b" days") => Ok(Modifier::Days(
parse_modifier_number(from_bytes(&s[..s.len() - 5]))?,
)),
s if ends_with_ignore_ascii_case!(s, b" hour") => Ok(Modifier::Hours(
parse_modifier_number(from_bytes(&s[..s.len() - 5]))?,
)),
s if ends_with_ignore_ascii_case!(s, b" hours") => Ok(Modifier::Hours(
parse_modifier_number(from_bytes(&s[..s.len() - 6]))?,
)),
s if ends_with_ignore_ascii_case!(s, b" minute") => Ok(Modifier::Minutes(
parse_modifier_number(from_bytes(&s[..s.len() - 7]))?,
)),
s if ends_with_ignore_ascii_case!(s, b" minutes") => Ok(Modifier::Minutes(
parse_modifier_number(from_bytes(&s[..s.len() - 8]))?,
)),
s if ends_with_ignore_ascii_case!(s, b" second") => Ok(Modifier::Seconds(
parse_modifier_number(from_bytes(&s[..s.len() - 7]))?,
)),
s if ends_with_ignore_ascii_case!(s, b" seconds") => Ok(Modifier::Seconds(
parse_modifier_number(from_bytes(&s[..s.len() - 8]))?,
)),
s if ends_with_ignore_ascii_case!(s, b" month") => Ok(Modifier::Months(
parse_modifier_number(from_bytes(&s[..s.len() - 6]))? as i32,
)),
s if ends_with_ignore_ascii_case!(s, b" months") => Ok(Modifier::Months(
parse_modifier_number(from_bytes(&s[..s.len() - 7]))? as i32,
)),
s if ends_with_ignore_ascii_case!(s, b" year") => Ok(Modifier::Years(
parse_modifier_number(from_bytes(&s[..s.len() - 5]))? as i32,
)),
s if ends_with_ignore_ascii_case!(s, b" years") => Ok(Modifier::Years(
parse_modifier_number(from_bytes(&s[..s.len() - 6]))? as i32,
)),
s if starts_with_ignore_ascii_case!(s, b"+")
|| starts_with_ignore_ascii_case!(s, b"-") =>
{
let sign = if starts_with_ignore_ascii_case!(s, b"-") {
-1
} else {
1
};
let parts: Vec<&str> = from_bytes(&s[1..]).split(' ').collect();
let digits_in_date = 10;
match parts.len() {
1 => {
if parts[0].len() == digits_in_date {
let date = parse_modifier_date(parts[0])?;
Ok(Modifier::DateOffset {
years: sign * date.year(),
months: sign * date.month() as i32,
days: sign * date.day() as i32,
})
} else {
// time values are either 12, 8 or 5 digits
let time = parse_modifier_time(parts[0])?;
let time_delta = sign * (time.num_seconds_from_midnight() as i32);
Ok(Modifier::TimeOffset(TimeDelta::seconds(time_delta.into())))
}
}
2 => {
let date = parse_modifier_date(parts[0])?;
let time = parse_modifier_time(parts[1])?;
// Convert time to total seconds (with sign)
let time_delta = sign * (time.num_seconds_from_midnight() as i32);
Ok(Modifier::DateTimeOffset {
years: sign * (date.year()),
months: sign * (date.month() as i32),
days: sign * date.day() as i32,
seconds: time_delta,
})
}
_ => Err(InvalidModifier(
"Invalid date/time offset format".to_string(),
)),
}
}
_ => Err(InvalidModifier(
"Invalid date/time offset format".to_string(),
)),
}
}
_ => Err(InvalidModifier(
"Invalid date/time offset format".to_string(),
)),
}
})
}
pub fn exec_timediff(values: &[Register]) -> Value {

View File

@@ -10,6 +10,7 @@ use std::collections::{HashMap, HashSet};
use std::fmt::{self, Debug, Display};
use std::sync::Arc;
use std::sync::Mutex;
use turso_macros::match_ignore_ascii_case;
/// Tracks computation counts to verify incremental behavior (for tests now), and in the future
/// should be used to provide statistics.
@@ -936,8 +937,9 @@ impl ProjectOperator {
}
}
Expr::FunctionCall { name, args, .. } => {
match name.as_str().to_lowercase().as_str() {
"hex" => {
let name_bytes = name.as_str().as_bytes();
match_ignore_ascii_case!(match name_bytes {
b"hex" => {
if args.len() == 1 {
let arg_val = self.evaluate_expression(&args[0], values);
match arg_val {
@@ -949,7 +951,7 @@ impl ProjectOperator {
}
}
_ => Value::Null, // Other functions not supported yet
}
})
}
Expr::Parenthesized(inner) => {
assert!(

View File

@@ -20,7 +20,9 @@ use crate::result::LimboResult;
use crate::storage::btree::BTreeCursor;
use crate::translate::collate::CollationSeq;
use crate::translate::plan::SelectPlan;
use crate::util::{module_args_from_sql, module_name_from_sql, IOExt, UnparsedFromSqlIndex};
use crate::util::{
module_args_from_sql, module_name_from_sql, type_from_name, IOExt, UnparsedFromSqlIndex,
};
use crate::{return_if_io, LimboError, MvCursor, Pager, RefValue, SymbolTable, VirtualTable};
use crate::{util::normalize_ident, Result};
use core::fmt;
@@ -904,37 +906,10 @@ fn create_table(
let mut typename_exactly_integer = false;
let ty = match col_type {
Some(data_type) => 'ty: {
// https://www.sqlite.org/datatype3.html
let mut type_name = data_type.name.clone();
type_name.make_ascii_uppercase();
if type_name.is_empty() {
break 'ty Type::Blob;
}
if type_name == "INTEGER" {
typename_exactly_integer = true;
break 'ty Type::Integer;
}
if let Some(ty) = type_name.as_bytes().windows(3).find_map(|s| match s {
b"INT" => Some(Type::Integer),
_ => None,
}) {
break 'ty ty;
}
if let Some(ty) = type_name.as_bytes().windows(4).find_map(|s| match s {
b"CHAR" | b"CLOB" | b"TEXT" => Some(Type::Text),
b"BLOB" => Some(Type::Blob),
b"REAL" | b"FLOA" | b"DOUB" => Some(Type::Real),
_ => None,
}) {
break 'ty ty;
}
Type::Numeric
Some(data_type) => {
let (ty, ei) = type_from_name(&data_type.name);
typename_exactly_integer = ei;
ty
}
None => Type::Null,
};
@@ -1101,28 +1076,7 @@ impl From<&ColumnDefinition> for Column {
}
let ty = match value.col_type {
Some(ref data_type) => {
// https://www.sqlite.org/datatype3.html
let type_name = data_type.name.clone().to_uppercase();
if type_name.contains("INT") {
Type::Integer
} else if type_name.contains("CHAR")
|| type_name.contains("CLOB")
|| type_name.contains("TEXT")
{
Type::Text
} else if type_name.contains("BLOB") || type_name.is_empty() {
Type::Blob
} else if type_name.contains("REAL")
|| type_name.contains("FLOA")
|| type_name.contains("DOUB")
{
Type::Real
} else {
Type::Numeric
}
}
Some(ref data_type) => type_from_name(&data_type.name).0,
None => Type::Null,
};

View File

@@ -3,6 +3,7 @@ use crate::{LimboError, Result};
use aegis::aegis256::Aegis256;
use aes_gcm::aead::{AeadCore, OsRng};
use std::ops::Deref;
use turso_macros::match_ignore_ascii_case;
// AEGIS-256 supports both 16 and 32 byte tags, we use the 16 byte variant, it is faster
// and provides sufficient security for our use case.
const AEGIS_TAG_SIZE: usize = 16;
@@ -267,13 +268,14 @@ impl TryFrom<&str> for CipherMode {
type Error = LimboError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
match s.to_lowercase().as_str() {
"aes256gcm" | "aes-256-gcm" | "aes_256_gcm" => Ok(CipherMode::Aes256Gcm),
"aegis256" | "aegis-256" | "aegis_256" => Ok(CipherMode::Aegis256),
let s_bytes = s.as_bytes();
match_ignore_ascii_case!(match s_bytes {
b"aes256gcm" | b"aes-256-gcm" | b"aes_256_gcm" => Ok(CipherMode::Aes256Gcm),
b"aegis256" | b"aegis-256" | b"aegis_256" => Ok(CipherMode::Aegis256),
_ => Err(LimboError::InvalidArgument(format!(
"Unknown cipher name: {s}"
))),
}
})
}
}

View File

@@ -14,6 +14,7 @@ use crate::{LimboError, Result};
use std::collections::HashMap;
use std::fmt::{self, Display, Formatter};
use std::sync::Arc;
use turso_macros::match_ignore_ascii_case;
use turso_parser::ast;
/// Result type for preprocessing aggregate expressions
@@ -1400,19 +1401,20 @@ impl<'a> LogicalPlanBuilder<'a> {
/// Parse aggregate function name (considering argument count for min/max)
fn parse_aggregate_function(name: &str, arg_count: usize) -> Option<AggregateFunction> {
match name.to_uppercase().as_str() {
"COUNT" => Some(AggFunc::Count),
"SUM" => Some(AggFunc::Sum),
"AVG" => Some(AggFunc::Avg),
let name_bytes = name.as_bytes();
match_ignore_ascii_case!(match name_bytes {
b"COUNT" => Some(AggFunc::Count),
b"SUM" => Some(AggFunc::Sum),
b"AVG" => Some(AggFunc::Avg),
// MIN and MAX are only aggregates with 1 argument
// With 2+ arguments, they're scalar functions
"MIN" if arg_count == 1 => Some(AggFunc::Min),
"MAX" if arg_count == 1 => Some(AggFunc::Max),
"GROUP_CONCAT" => Some(AggFunc::GroupConcat),
"STRING_AGG" => Some(AggFunc::StringAgg),
"TOTAL" => Some(AggFunc::Total),
b"MIN" if arg_count == 1 => Some(AggFunc::Min),
b"MAX" if arg_count == 1 => Some(AggFunc::Max),
b"GROUP_CONCAT" => Some(AggFunc::GroupConcat),
b"STRING_AGG" => Some(AggFunc::StringAgg),
b"TOTAL" => Some(AggFunc::Total),
_ => None,
}
})
}
// Check if expression contains aggregates

View File

@@ -4,6 +4,7 @@
use chrono::Datelike;
use std::rc::Rc;
use std::sync::Arc;
use turso_macros::match_ignore_ascii_case;
use turso_parser::ast::{self, ColumnDefinition, Expr, Literal, Name};
use turso_parser::ast::{PragmaName, QualifiedName};
@@ -213,17 +214,17 @@ fn update_pragma(
PragmaName::AutoVacuum => {
let auto_vacuum_mode = match value {
Expr::Name(name) => {
let name = name.as_str().to_lowercase();
match name.as_str() {
"none" => 0,
"full" => 1,
"incremental" => 2,
let name = name.as_str().as_bytes();
match_ignore_ascii_case!(match name {
b"none" => 0,
b"full" => 1,
b"incremental" => 2,
_ => {
return Err(LimboError::InvalidArgument(
"invalid auto vacuum mode".to_string(),
));
}
}
})
}
_ => {
return Err(LimboError::InvalidArgument(
@@ -330,11 +331,11 @@ fn update_pragma(
let mode = match value {
Expr::Name(name) => {
let name_upper = name.as_str().to_uppercase();
match name_upper.as_str() {
"OFF" | "FALSE" | "NO" | "0" => SyncMode::Off,
let name_bytes = name.as_str().as_bytes();
match_ignore_ascii_case!(match name_bytes {
b"OFF" | b"FALSE" | b"NO" | b"0" => SyncMode::Off,
_ => SyncMode::Full,
}
})
}
Expr::Literal(Literal::Numeric(n)) => match n.as_str() {
"0" => SyncMode::Off,
@@ -574,8 +575,11 @@ fn query_pragma(
ast::Expr::Literal(Literal::Numeric(i)) => i.parse::<i64>().unwrap() != 0,
ast::Expr::Literal(Literal::String(ref s))
| ast::Expr::Name(Name::Ident(ref s)) => {
let s = s.to_lowercase();
s == "1" || s == "on" || s == "true"
let s = s.as_bytes();
match_ignore_ascii_case!(match s {
b"1" | b"on" | b"true" => true,
_ => false,
})
}
_ => {
return Err(LimboError::ParseError(format!(

View File

@@ -13,6 +13,7 @@ use std::{
sync::{Arc, Mutex},
};
use tracing::{instrument, Level};
use turso_macros::match_ignore_ascii_case;
use turso_parser::ast::{
self, fmt::ToTokens, Cmd, CreateTableBody, Expr, FunctionTail, Literal, Stmt, UnaryOperator,
};
@@ -31,6 +32,58 @@ macro_rules! io_yield_many {
};
}
#[macro_export]
macro_rules! eq_ignore_ascii_case {
( $var:expr, $value:literal ) => {{
match_ignore_ascii_case!(match $var {
$value => true,
_ => false,
})
}};
}
#[macro_export]
macro_rules! contains_ignore_ascii_case {
( $var:expr, $value:literal ) => {{
let compare_to_idx = $var.len().saturating_sub($value.len());
if $var.len() < $value.len() {
false
} else {
let mut result = false;
for i in 0..=compare_to_idx {
if eq_ignore_ascii_case!(&$var[i..i + $value.len()], $value) {
result = true;
break;
}
}
result
}
}};
}
#[macro_export]
macro_rules! starts_with_ignore_ascii_case {
( $var:expr, $value:literal ) => {{
if $var.len() < $value.len() {
false
} else {
eq_ignore_ascii_case!(&$var[..$value.len()], $value)
}
}};
}
#[macro_export]
macro_rules! ends_with_ignore_ascii_case {
( $var:expr, $value:literal ) => {{
if $var.len() < $value.len() {
false
} else {
eq_ignore_ascii_case!(&$var[$var.len() - $value.len()..], $value)
}
}};
}
pub trait IOExt {
fn block<T>(&self, f: impl FnMut() -> Result<IOResult<T>>) -> Result<T>;
}
@@ -110,14 +163,14 @@ pub fn parse_schema_rows(
StepResult::Row => {
let row = rows.row().unwrap();
let ty = row.get::<&str>(0)?;
if !["table", "index", "view"].contains(&ty) {
continue;
}
match ty {
"table" => {
let root_page: i64 = row.get::<i64>(3)?;
let sql: &str = row.get::<&str>(4)?;
if root_page == 0 && sql.to_lowercase().contains("create virtual") {
let sql_bytes = sql.as_bytes();
if root_page == 0
&& contains_ignore_ascii_case!(sql_bytes, b"create virtual")
{
let name: &str = row.get::<&str>(1)?;
// 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
@@ -614,6 +667,36 @@ pub fn exprs_are_equivalent(expr1: &Expr, expr2: &Expr) -> bool {
}
}
// this function returns the affinity type and whether the type name was exactly "INTEGER"
// https://www.sqlite.org/datatype3.html
pub(crate) fn type_from_name(type_name: &str) -> (Type, bool) {
let type_name = type_name.as_bytes();
if type_name.is_empty() {
return (Type::Blob, false);
}
if eq_ignore_ascii_case!(type_name, b"INTEGER") {
return (Type::Integer, true);
}
if let Some(ty) = type_name.windows(4).find_map(|s| {
if contains_ignore_ascii_case!(s, b"INT") {
return Some(Type::Integer);
}
match_ignore_ascii_case!(match s {
b"CHAR" | b"CLOB" | b"TEXT" => Some(Type::Text),
b"BLOB" => Some(Type::Blob),
b"REAL" | b"FLOA" | b"DOUB" => Some(Type::Real),
_ => None,
})
}) {
return (ty, false);
}
(Type::Numeric, false)
}
pub fn columns_from_create_table_body(
body: &turso_parser::ast::CreateTableBody,
) -> crate::Result<Vec<Column>> {
@@ -710,15 +793,16 @@ impl From<&str> for CacheMode {
impl OpenMode {
pub fn from_str(s: &str) -> Result<Self> {
match s.trim().to_lowercase().as_str() {
"ro" => Ok(OpenMode::ReadOnly),
"rw" => Ok(OpenMode::ReadWrite),
"memory" => Ok(OpenMode::Memory),
"rwc" => Ok(OpenMode::ReadWriteCreate),
let s_bytes = s.trim().as_bytes();
match_ignore_ascii_case!(match s_bytes {
b"ro" => Ok(OpenMode::ReadOnly),
b"rw" => Ok(OpenMode::ReadWrite),
b"memory" => Ok(OpenMode::Memory),
b"rwc" => Ok(OpenMode::ReadWriteCreate),
_ => Err(LimboError::InvalidArgument(format!(
"Invalid mode: '{s}'. Expected one of 'ro', 'rw', 'memory', 'rwc'"
))),
}
})
}
}

View File

@@ -39,6 +39,7 @@ use std::{
rc::Rc,
sync::{Arc, Mutex},
};
use turso_macros::match_ignore_ascii_case;
use crate::{pseudo::PseudoCursor, result::LimboResult};
@@ -8831,11 +8832,11 @@ pub fn op_journal_mode(
// Currently, Turso only supports WAL mode
// If a new mode is specified, we validate it but always return "wal"
if let Some(mode) = new_mode {
let mode_lower = mode.to_lowercase();
let mode_bytes = mode.as_bytes();
// Valid journal modes in SQLite are: delete, truncate, persist, memory, wal, off
// We accept any valid mode but always use WAL
match mode_lower.as_str() {
"delete" | "truncate" | "persist" | "memory" | "wal" | "off" => {
match_ignore_ascii_case!(match mode_bytes {
b"delete" | b"truncate" | b"persist" | b"memory" | b"wal" | b"off" => {
// Mode is valid, but we stay in WAL mode
}
_ => {
@@ -8844,7 +8845,7 @@ pub fn op_journal_mode(
"Unknown journal mode: {mode}"
)));
}
}
})
}
// Always return "wal" as the current journal mode

View File

@@ -112,8 +112,14 @@ pub fn match_ignore_ascci_case(input: TokenStream) -> TokenStream {
entry: &PathEntry,
) -> proc_macro2::TokenStream {
let eof_handle = if let Some(ref result) = entry.result {
let guard = if let Some(ref b) = result.guard {
let expr = &b.1;
quote! { if #expr }
} else {
quote! {}
};
let body = &result.body;
quote! { None => { #body } }
quote! { None #guard => { #body } }
} else {
quote! {}
};