Merge branch 'main' into feature/fix-clippy

This commit is contained in:
김선우
2024-09-16 21:58:07 +09:00
10 changed files with 271 additions and 61 deletions

View File

@@ -97,7 +97,7 @@ This document describes the SQLite compatibility status of Limbo:
| round(X,Y) | Yes | |
| rtrim(X) | Yes | |
| rtrim(X,Y) | Yes | |
| sign(X) | No | |
| sign(X) | Yes | |
| soundex(X) | No | |
| sqlite_compileoption_get(N) | No | |
| sqlite_compileoption_used(X) | No | |

View File

@@ -65,6 +65,7 @@ pub enum ScalarFunc {
Min,
Max,
Nullif,
Sign,
Substr,
Substring,
Date,
@@ -95,6 +96,7 @@ impl Display for ScalarFunc {
ScalarFunc::Min => "min".to_string(),
ScalarFunc::Max => "max".to_string(),
ScalarFunc::Nullif => "nullif".to_string(),
ScalarFunc::Sign => "sign".to_string(),
ScalarFunc::Substr => "substr".to_string(),
ScalarFunc::Substring => "substring".to_string(),
ScalarFunc::Date => "date".to_string(),
@@ -159,6 +161,7 @@ impl Func {
"rtrim" => Ok(Func::Scalar(ScalarFunc::RTrim)),
"round" => Ok(Func::Scalar(ScalarFunc::Round)),
"length" => Ok(Func::Scalar(ScalarFunc::Length)),
"sign" => Ok(Func::Scalar(ScalarFunc::Sign)),
"substr" => Ok(Func::Scalar(ScalarFunc::Substr)),
"substring" => Ok(Func::Scalar(ScalarFunc::Substring)),
"date" => Ok(Func::Scalar(ScalarFunc::Date)),

View File

@@ -2,12 +2,10 @@ use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use sqlite3_parser::ast;
use crate::schema::{BTreeTable, Column, PseudoTable, Table};
use crate::storage::sqlite3_ondisk::DatabaseHeader;
use crate::translate::expr::resolve_ident_pseudo_table;
use crate::types::{OwnedRecord, OwnedValue};
use crate::util::normalize_ident;
use crate::vdbe::builder::ProgramBuilder;
use crate::vdbe::{BranchOffset, Insn, Program};
use crate::Result;
@@ -648,21 +646,11 @@ impl Emitter for Operator {
.chain(aggregates.iter().map(|agg| &agg.args[0]))
// FIXME: just blindly taking the first arg is a hack
{
// FIXME: reading from pseudo tables made during sort operations
// now relies on them having the same column names as the original
// table. This is not very robust IMO and we should refactor how these
// are handled.
column_names.push(match expr {
ast::Expr::Id(ident) => normalize_ident(&ident.0),
ast::Expr::Qualified(tbl, ident) => {
format!(
"{}.{}",
normalize_ident(&tbl.0),
normalize_ident(&ident.0)
)
}
_ => "expr".to_string(),
});
// Sorter column names for group by are now just determined by stringifying the expression, since the group by
// columns and aggregations can be practically anything.
// FIXME: either come up with something more robust, or make this something like expr.to_canonical_string() so that we can handle
// things like `count(1)` and `COUNT(1)` the same way
column_names.push(expr.to_string());
}
let pseudo_columns = column_names
.iter()
@@ -673,12 +661,12 @@ impl Emitter for Operator {
})
.collect::<Vec<_>>();
let pseudo_cursor = program.alloc_cursor_id(
None,
Some(Table::Pseudo(Rc::new(PseudoTable {
columns: pseudo_columns,
}))),
);
let pseudo_table = Rc::new(PseudoTable {
columns: pseudo_columns,
});
let pseudo_cursor = program
.alloc_cursor_id(None, Some(Table::Pseudo(pseudo_table.clone())));
program.emit_insn(Insn::OpenPseudo {
cursor_id: pseudo_cursor,
@@ -707,15 +695,14 @@ impl Emitter for Operator {
let groups_start_reg = program.alloc_registers(group_by.len());
for (i, expr) in group_by.iter().enumerate() {
let sorter_column_index =
resolve_ident_pseudo_table(&expr.to_string(), &pseudo_table)?;
let group_reg = groups_start_reg + i;
translate_expr(
program,
Some(referenced_tables),
expr,
group_reg,
Some(pseudo_cursor),
None,
)?;
program.emit_insn(Insn::Column {
cursor_id: pseudo_cursor,
column: sorter_column_index,
dest: group_reg,
});
}
program.emit_insn(Insn::Compare {
@@ -806,14 +793,13 @@ impl Emitter for Operator {
for (i, expr) in group_by.iter().enumerate() {
let key_reg = group_exprs_start_register + i;
translate_expr(
program,
Some(referenced_tables),
expr,
key_reg,
Some(pseudo_cursor),
None,
)?;
let sorter_column_index =
resolve_ident_pseudo_table(&expr.to_string(), &pseudo_table)?;
program.emit_insn(Insn::Column {
cursor_id: pseudo_cursor,
column: sorter_column_index,
dest: key_reg,
});
}
program.resolve_label(

View File

@@ -4,7 +4,7 @@ use std::rc::Rc;
use super::optimizer::CachedResult;
use crate::function::{AggFunc, Func, FuncCtx, ScalarFunc};
use crate::schema::{Table, Type};
use crate::schema::{PseudoTable, Table, Type};
use crate::util::normalize_ident;
use crate::{
schema::BTreeTable,
@@ -451,21 +451,10 @@ pub fn translate_condition_expr(
escape: _,
} => {
let cur_reg = program.alloc_register();
assert!(matches!(rhs.as_ref(), ast::Expr::Literal(_)));
match op {
ast::LikeOperator::Like => {
let pattern_reg = program.alloc_register();
let column_reg = program.alloc_register();
// LIKE(pattern, column). We should translate the pattern first before the column
let _ = translate_expr(
program,
Some(referenced_tables),
rhs,
pattern_reg,
cursor_hint,
None,
)?;
program.mark_last_insn_constant();
let _ = translate_expr(
program,
Some(referenced_tables),
@@ -474,6 +463,20 @@ pub fn translate_condition_expr(
cursor_hint,
None,
)?;
if let ast::Expr::Literal(_) = lhs.as_ref() {
program.mark_last_insn_constant();
}
let _ = translate_expr(
program,
Some(referenced_tables),
rhs,
pattern_reg,
cursor_hint,
None,
)?;
if let ast::Expr::Literal(_) = rhs.as_ref() {
program.mark_last_insn_constant();
}
program.emit_insn(Insn::Function {
// Only constant patterns for LIKE are supported currently, so this
// is always 1
@@ -838,8 +841,11 @@ pub fn translate_expr(
srf.to_string()
);
};
let mut start_reg = None;
for arg in args.iter() {
let reg = program.alloc_register();
start_reg = Some(start_reg.unwrap_or(reg));
translate_expr(
program,
referenced_tables,
@@ -851,7 +857,7 @@ pub fn translate_expr(
}
program.emit_insn(Insn::Function {
constant_mask: 0,
start_reg: target_register + 1,
start_reg: start_reg.unwrap(),
dest: target_register,
func: func_ctx,
});
@@ -983,7 +989,8 @@ pub fn translate_expr(
| ScalarFunc::Upper
| ScalarFunc::Length
| ScalarFunc::Unicode
| ScalarFunc::Quote => {
| ScalarFunc::Quote
| ScalarFunc::Sign => {
let args = if let Some(args) = args {
if args.len() != 1 {
crate::bail_parse_error!(
@@ -1539,6 +1546,19 @@ pub fn resolve_ident_table(
crate::bail_parse_error!("ambiguous column name {}", ident.as_str());
}
pub fn resolve_ident_pseudo_table(ident: &String, pseudo_table: &PseudoTable) -> Result<usize> {
let res = pseudo_table
.columns
.iter()
.enumerate()
.find(|(_, col)| col.name == *ident);
if res.is_some() {
let (idx, _) = res.unwrap();
return Ok(idx);
}
crate::bail_parse_error!("column with name {} not found", ident.as_str());
}
pub fn maybe_apply_affinity(col_type: Type, target_register: usize, program: &mut ProgramBuilder) {
if col_type == crate::schema::Type::Real {
program.emit_insn(Insn::RealAffinity {

View File

@@ -1467,9 +1467,11 @@ impl Program {
| ScalarFunc::Upper
| ScalarFunc::Length
| ScalarFunc::Unicode
| ScalarFunc::Quote => {
| ScalarFunc::Quote
| ScalarFunc::Sign => {
let reg_value = state.registers[*start_reg].borrow_mut();
let result = match scalar_func {
ScalarFunc::Sign => exec_sign(reg_value),
ScalarFunc::Abs => exec_abs(reg_value),
ScalarFunc::Lower => exec_lower(reg_value),
ScalarFunc::Upper => exec_upper(reg_value),
@@ -1865,6 +1867,45 @@ fn exec_concat_ws(registers: &[OwnedValue]) -> OwnedValue {
OwnedValue::Text(Rc::new(result))
}
fn exec_sign(reg: &OwnedValue) -> Option<OwnedValue> {
let num = match reg {
OwnedValue::Integer(i) => *i as f64,
OwnedValue::Float(f) => *f,
OwnedValue::Text(s) => {
if let Ok(i) = s.parse::<i64>() {
i as f64
} else if let Ok(f) = s.parse::<f64>() {
f
} else {
return Some(OwnedValue::Null);
}
}
OwnedValue::Blob(b) => match std::str::from_utf8(b) {
Ok(s) => {
if let Ok(i) = s.parse::<i64>() {
i as f64
} else if let Ok(f) = s.parse::<f64>() {
f
} else {
return Some(OwnedValue::Null);
}
}
Err(_) => return Some(OwnedValue::Null),
},
_ => return Some(OwnedValue::Null),
};
let sign = if num > 0.0 {
1
} else if num < 0.0 {
-1
} else {
0
};
Some(OwnedValue::Integer(sign))
}
fn exec_abs(reg: &OwnedValue) -> Option<OwnedValue> {
match reg {
OwnedValue::Integer(x) => {
@@ -1930,7 +1971,10 @@ fn exec_char(values: Vec<OwnedValue>) -> OwnedValue {
}
fn construct_like_regex(pattern: &str) -> Regex {
Regex::new(&pattern.replace('%', ".*").replace('_', ".").to_string()).unwrap()
let mut regex_pattern = String::from("^");
regex_pattern.push_str(&pattern.replace('%', ".*").replace('_', "."));
regex_pattern.push('$');
Regex::new(&regex_pattern).unwrap()
}
// Implements LIKE pattern matching. Caches the constructed regex if a cache is provided
@@ -2116,9 +2160,9 @@ fn exec_if(reg: &OwnedValue, null_reg: &OwnedValue, not: bool) -> bool {
mod tests {
use super::{
exec_abs, exec_char, exec_if, exec_length, exec_like, exec_lower, exec_ltrim, exec_minmax,
exec_nullif, exec_quote, exec_random, exec_round, exec_rtrim, exec_substring, exec_trim,
exec_unicode, exec_upper, get_new_rowid, Cursor, CursorResult, LimboError, OwnedRecord,
OwnedValue, Result,
exec_nullif, exec_quote, exec_random, exec_round, exec_rtrim, exec_sign, exec_substring,
exec_trim, exec_unicode, exec_upper, get_new_rowid, Cursor, CursorResult, LimboError,
OwnedRecord, OwnedValue, Result,
};
use mockall::{mock, predicate};
use rand::{rngs::mock::StepRng, thread_rng};
@@ -2646,4 +2690,71 @@ mod tests {
expected_val
);
}
#[test]
fn test_exec_sign() {
let input = OwnedValue::Integer(42);
let expected = Some(OwnedValue::Integer(1));
assert_eq!(exec_sign(&input), expected);
let input = OwnedValue::Integer(-42);
let expected = Some(OwnedValue::Integer(-1));
assert_eq!(exec_sign(&input), expected);
let input = OwnedValue::Integer(0);
let expected = Some(OwnedValue::Integer(0));
assert_eq!(exec_sign(&input), expected);
let input = OwnedValue::Float(0.0);
let expected = Some(OwnedValue::Integer(0));
assert_eq!(exec_sign(&input), expected);
let input = OwnedValue::Float(0.1);
let expected = Some(OwnedValue::Integer(1));
assert_eq!(exec_sign(&input), expected);
let input = OwnedValue::Float(42.0);
let expected = Some(OwnedValue::Integer(1));
assert_eq!(exec_sign(&input), expected);
let input = OwnedValue::Float(-42.0);
let expected = Some(OwnedValue::Integer(-1));
assert_eq!(exec_sign(&input), expected);
let input = OwnedValue::Text(Rc::new("abc".to_string()));
let expected = Some(OwnedValue::Null);
assert_eq!(exec_sign(&input), expected);
let input = OwnedValue::Text(Rc::new("42".to_string()));
let expected = Some(OwnedValue::Integer(1));
assert_eq!(exec_sign(&input), expected);
let input = OwnedValue::Text(Rc::new("-42".to_string()));
let expected = Some(OwnedValue::Integer(-1));
assert_eq!(exec_sign(&input), expected);
let input = OwnedValue::Text(Rc::new("0".to_string()));
let expected = Some(OwnedValue::Integer(0));
assert_eq!(exec_sign(&input), expected);
let input = OwnedValue::Blob(Rc::new(b"abc".to_vec()));
let expected = Some(OwnedValue::Null);
assert_eq!(exec_sign(&input), expected);
let input = OwnedValue::Blob(Rc::new(b"42".to_vec()));
let expected = Some(OwnedValue::Integer(1));
assert_eq!(exec_sign(&input), expected);
let input = OwnedValue::Blob(Rc::new(b"-42".to_vec()));
let expected = Some(OwnedValue::Integer(-1));
assert_eq!(exec_sign(&input), expected);
let input = OwnedValue::Blob(Rc::new(b"0".to_vec()));
let expected = Some(OwnedValue::Integer(0));
assert_eq!(exec_sign(&input), expected);
let input = OwnedValue::Null;
let expected = Some(OwnedValue::Null);
assert_eq!(exec_sign(&input), expected);
}
}

View File

@@ -19,7 +19,7 @@ dirs = "5.0.1"
env_logger = "0.10.1"
limbo_core = { path = "../core" }
rustyline = "12.0.0"
rusqlite = "0.29.0"
rusqlite = { version = "0.29", features = ["bundled"] }
tempfile = "3.0.7"
log = "0.4.22"

View File

@@ -38,6 +38,7 @@ mod tests {
use super::*;
use limbo_core::{RowResult, Value};
#[ignore]
#[test]
fn test_sequential_write() -> anyhow::Result<()> {
let _ = env_logger::try_init();
@@ -99,6 +100,7 @@ mod tests {
Ok(())
}
#[ignore]
#[test]
fn test_btree_splitting() -> anyhow::Result<()> {
static INIT: Once = Once::new();
@@ -219,6 +221,7 @@ mod tests {
Ok(())
}
#[ignore]
#[test]
fn test_sequential_overflow_page() -> anyhow::Result<()> {
let _ = env_logger::try_init();

View File

@@ -105,3 +105,25 @@ Abigail|906
Adam|1675
Adrian|447
Adriana|84}
do_execsql_test group_by_function_expression {
select length(phone_number), count(1) from users group by length(phone_number) order by count(1);
} {15|392
22|416
13|762
20|791
10|793
19|816
21|821
17|1184
18|1211
16|1231
12|1583}
do_execsql_test group_by_function_expression_ridiculous {
select upper(substr(phone_number, 1,3)), count(1) from users group by upper(substr(phone_number, 1,3)) order by -1 * count(1) limit 5;
} {001|1677
+1-|1606
(97|36
(20|35
(31|35}

View File

@@ -43,3 +43,24 @@ do_execsql_test where-like-or {
5|sweatshirt|74.0
8|sneakers|82.0
11|accessories|81.0}
do_execsql_test where-like-another-column {
select first_name, last_name from users where last_name like first_name;
} {James|James
Daniel|Daniel
Taylor|Taylor}
do_execsql_test where-like-another-column-prefix {
select first_name, last_name from users where last_name like concat(first_name, '%');
} {James|James
Daniel|Daniel
William|Williams
John|Johnson
Taylor|Taylor
John|Johnson
Stephen|Stephens
Robert|Roberts}
do_execsql_test where-like-impossible {
select * from products where 'foobar' like 'fooba';
} {}

View File

@@ -386,3 +386,47 @@ do_execsql_test quote-null {
do_execsql_test quote-integer {
SELECT quote(123)
} {123}
do_execsql_test sign-positive-integer {
SELECT sign(42);
} {1}
do_execsql_test sign-negative-integer {
SELECT sign(-42);
} {-1}
do_execsql_test sign-zero {
SELECT sign(0);
} {0}
do_execsql_test sign-positive-float {
SELECT sign(42.0);
} {1}
do_execsql_test sign-negative-float {
SELECT sign(-42.0);
} {-1}
do_execsql_test sign-zero-float {
SELECT sign(0.0);
} {0}
do_execsql_test sign-text-positive-integer {
SELECT sign('42');
} {1}
do_execsql_test sign-text-negative-integer {
SELECT sign('-42');
} {-1}
do_execsql_test sign-text-zero {
SELECT sign('0');
} {0}
do_execsql_test sign-text-non-numeric {
SELECT sign('abc');
} {}
do_execsql_test sign-null {
SELECT sign(NULL);
} {}