mirror of
https://github.com/aljazceru/turso.git
synced 2026-02-09 18:24:20 +01:00
Treat table-valued functions as tables
With this change, the following two queries are considered equivalent: ```sql SELECT value FROM generate_series(5, 50); SELECT value FROM generate_series WHERE start = 5 AND stop = 50; ``` Arguments passed in parentheses to the virtual table name are now matched to hidden columns. Column references are still not supported as table-valued function arguments. The only difference is that previously, a query like: ```sql SELECT one.value, series.value FROM (SELECT 1 AS value) one, generate_series(one.value, 3) series; ``` would cause a panic. Now, it returns a proper error message instead. Adding support for column references is more nuanced for two main reasons: - We need to ensure that in joins where a TVF depends on other tables, those other tables are processed first. For example, in: ```sql SELECT one.value, series.value FROM generate_series(one.value, 3) series, (SELECT 1 AS value) one; ``` the one table must be processed by the top-level loop, and series must be nested. - For outer joins involving TVFs, the arguments must be treated as ON predicates, not WHERE predicates.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
#[cfg(feature = "fs")]
|
||||
mod dynamic;
|
||||
mod vtab_xconnect;
|
||||
use crate::vtab::VirtualTable;
|
||||
#[cfg(all(target_os = "linux", feature = "io_uring"))]
|
||||
use crate::UringIO;
|
||||
use crate::{function::ExternalFunc, Connection, Database, LimboError, IO};
|
||||
@@ -148,6 +149,13 @@ impl Connection {
|
||||
.borrow_mut()
|
||||
.vtab_modules
|
||||
.insert(name.to_string(), vmodule.into());
|
||||
if kind == VTabKind::TableValuedFunction {
|
||||
if let Ok(vtab) = VirtualTable::function(name, &self.syms.borrow()) {
|
||||
self.schema.borrow_mut().add_virtual_table(vtab);
|
||||
} else {
|
||||
return ResultCode::Error;
|
||||
}
|
||||
}
|
||||
ResultCode::OK
|
||||
}
|
||||
|
||||
|
||||
101
core/pragma.rs
101
core/pragma.rs
@@ -1,8 +1,8 @@
|
||||
use crate::{Connection, LimboError, Statement, StepResult, Value};
|
||||
use bitflags::bitflags;
|
||||
use std::str::FromStr;
|
||||
use turso_ext::{ConstraintInfo, ConstraintOp, ConstraintUsage, IndexInfo};
|
||||
use std::sync::Arc;
|
||||
use strum::IntoEnumIterator;
|
||||
use turso_ext::{ConstraintInfo, ConstraintOp, ConstraintUsage, IndexInfo};
|
||||
use turso_sqlite3_parser::ast::PragmaName;
|
||||
|
||||
bitflags! {
|
||||
@@ -31,7 +31,7 @@ impl Pragma {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pragma_for(pragma: PragmaName) -> Pragma {
|
||||
pub fn pragma_for(pragma: &PragmaName) -> Pragma {
|
||||
use PragmaName::*;
|
||||
|
||||
match pragma {
|
||||
@@ -87,66 +87,65 @@ pub fn pragma_for(pragma: PragmaName) -> Pragma {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct PragmaVirtualTable {
|
||||
pragma_name: String,
|
||||
pub(crate) pragma_name: String,
|
||||
visible_column_count: usize,
|
||||
max_arg_count: usize,
|
||||
has_pragma_arg: bool,
|
||||
}
|
||||
|
||||
impl PragmaVirtualTable {
|
||||
pub(crate) fn create(pragma_name: &str) -> crate::Result<(Self, String)> {
|
||||
if let Ok(pragma) = PragmaName::from_str(pragma_name) {
|
||||
if pragma == PragmaName::LegacyFileFormat {
|
||||
return Err(Self::no_such_pragma(pragma_name));
|
||||
}
|
||||
let pragma = pragma_for(pragma);
|
||||
if pragma
|
||||
.flags
|
||||
.intersects(PragmaFlags::Result0 | PragmaFlags::Result1)
|
||||
{
|
||||
let mut max_arg_count = 0;
|
||||
let mut has_pragma_arg = false;
|
||||
|
||||
let mut sql = String::from("CREATE TABLE x(");
|
||||
let col_defs = pragma
|
||||
.columns
|
||||
.iter()
|
||||
.map(|col| format!("\"{col}\""))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
sql.push_str(&col_defs);
|
||||
if pragma.flags.contains(PragmaFlags::Result1) {
|
||||
sql.push_str(", arg HIDDEN");
|
||||
max_arg_count += 1;
|
||||
has_pragma_arg = true;
|
||||
}
|
||||
pub(crate) fn functions() -> Vec<(PragmaVirtualTable, String)> {
|
||||
PragmaName::iter()
|
||||
.filter(|name| *name != PragmaName::LegacyFileFormat)
|
||||
.filter_map(|name| {
|
||||
let pragma = pragma_for(&name);
|
||||
if pragma
|
||||
.flags
|
||||
.intersects(PragmaFlags::SchemaOpt | PragmaFlags::SchemaReq)
|
||||
.intersects(PragmaFlags::Result0 | PragmaFlags::Result1)
|
||||
{
|
||||
sql.push_str(", schema HIDDEN");
|
||||
max_arg_count += 1;
|
||||
Some(Self::create(name.to_string(), pragma))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
sql.push(')');
|
||||
|
||||
return Ok((
|
||||
PragmaVirtualTable {
|
||||
pragma_name: pragma_name.to_owned(),
|
||||
visible_column_count: pragma.columns.len(),
|
||||
max_arg_count,
|
||||
has_pragma_arg,
|
||||
},
|
||||
sql,
|
||||
));
|
||||
}
|
||||
}
|
||||
Err(Self::no_such_pragma(pragma_name))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn no_such_pragma(pragma_name: &str) -> LimboError {
|
||||
LimboError::ParseError(format!(
|
||||
"No such table-valued function: pragma_{pragma_name}"
|
||||
))
|
||||
fn create(pragma_name: String, pragma: Pragma) -> (Self, String) {
|
||||
let mut max_arg_count = 0;
|
||||
let mut has_pragma_arg = false;
|
||||
|
||||
let mut sql = String::from("CREATE TABLE x(");
|
||||
let col_defs = pragma
|
||||
.columns
|
||||
.iter()
|
||||
.map(|col| format!("\"{col}\""))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
sql.push_str(&col_defs);
|
||||
if pragma.flags.contains(PragmaFlags::Result1) {
|
||||
sql.push_str(", arg HIDDEN");
|
||||
max_arg_count += 1;
|
||||
has_pragma_arg = true;
|
||||
}
|
||||
if pragma
|
||||
.flags
|
||||
.intersects(PragmaFlags::SchemaOpt | PragmaFlags::SchemaReq)
|
||||
{
|
||||
sql.push_str(", schema HIDDEN");
|
||||
max_arg_count += 1;
|
||||
}
|
||||
sql.push(')');
|
||||
|
||||
(
|
||||
PragmaVirtualTable {
|
||||
pragma_name,
|
||||
visible_column_count: pragma.columns.len(),
|
||||
max_arg_count,
|
||||
has_pragma_arg,
|
||||
},
|
||||
sql,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn open(&self, conn: Arc<Connection>) -> crate::Result<PragmaVirtualTableCursor> {
|
||||
|
||||
@@ -43,6 +43,9 @@ impl Schema {
|
||||
SCHEMA_TABLE_NAME.to_string(),
|
||||
Arc::new(Table::BTree(sqlite_schema_table().into())),
|
||||
);
|
||||
for function in VirtualTable::builtin_functions() {
|
||||
tables.insert(function.name.to_owned(), Arc::new(Table::Virtual(function)));
|
||||
}
|
||||
Self {
|
||||
tables,
|
||||
indexes,
|
||||
|
||||
@@ -12,12 +12,12 @@ pub fn register_extension(ext_api: &mut ExtensionApi) {
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! try_option {
|
||||
($expr:expr, $err:expr) => {
|
||||
match $expr {
|
||||
Some(val) => val,
|
||||
None => return $err,
|
||||
}
|
||||
macro_rules! extract_arg_integer {
|
||||
($args:expr, $idx:expr, $unknown_type_default:expr) => {
|
||||
$args
|
||||
.get($idx)
|
||||
.map(|v| v.to_integer().unwrap_or($unknown_type_default))
|
||||
.unwrap_or(-1)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -153,20 +153,36 @@ impl GenerateSeriesCursor {
|
||||
impl VTabCursor for GenerateSeriesCursor {
|
||||
type Error = ResultCode;
|
||||
|
||||
fn filter(&mut self, args: &[Value], _: Option<(&str, i32)>) -> ResultCode {
|
||||
// args are the start, stop, and step
|
||||
if args.is_empty() || args.len() > 3 {
|
||||
fn filter(&mut self, args: &[Value], idx_info: Option<(&str, i32)>) -> ResultCode {
|
||||
let mut start = -1;
|
||||
let mut stop = -1;
|
||||
let mut step = 1;
|
||||
|
||||
if let Some((_, idx_num)) = idx_info {
|
||||
let mut arg_idx = 0;
|
||||
// For the semantics of `idx_num`, see the comment in the `best_index` method.
|
||||
if idx_num & 1 != 0 {
|
||||
start = extract_arg_integer!(args, arg_idx, -1);
|
||||
arg_idx += 1;
|
||||
}
|
||||
if idx_num & 2 != 0 {
|
||||
stop = extract_arg_integer!(args, arg_idx, i64::MAX);
|
||||
arg_idx += 1;
|
||||
}
|
||||
if idx_num & 4 != 0 {
|
||||
step = args
|
||||
.get(arg_idx)
|
||||
.map(|v| v.to_integer().unwrap_or(1))
|
||||
.unwrap_or(1);
|
||||
}
|
||||
}
|
||||
|
||||
if start == -1 {
|
||||
return ResultCode::InvalidArgs;
|
||||
}
|
||||
let start = try_option!(args[0].to_integer(), ResultCode::InvalidArgs);
|
||||
let stop = try_option!(
|
||||
args.get(1).map(|v| v.to_integer().unwrap_or(i64::MAX)),
|
||||
ResultCode::EOF // Sqlite returns an empty series for wacky args
|
||||
);
|
||||
let mut step = args
|
||||
.get(2)
|
||||
.map(|v| v.to_integer().unwrap_or(1))
|
||||
.unwrap_or(1);
|
||||
if stop == -1 {
|
||||
return ResultCode::EOF; // Sqlite returns an empty series for wacky args
|
||||
}
|
||||
|
||||
// Convert zero step to 1, matching SQLite behavior
|
||||
if step == 0 {
|
||||
@@ -300,7 +316,7 @@ mod tests {
|
||||
];
|
||||
|
||||
// Initialize cursor through filter
|
||||
match cursor.filter(&args, None) {
|
||||
match cursor.filter(&args, Some(("idx", 1 | 2 | 4))) {
|
||||
ResultCode::OK => (),
|
||||
ResultCode::EOF => return Ok(vec![]),
|
||||
err => return Err(err),
|
||||
@@ -586,7 +602,7 @@ mod tests {
|
||||
];
|
||||
|
||||
// Initialize cursor through filter
|
||||
cursor.filter(&args, None);
|
||||
cursor.filter(&args, Some(("idx", 1 | 2 | 4)));
|
||||
|
||||
let mut rowids = vec![];
|
||||
while !cursor.eof() {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use turso_ext::VTabKind;
|
||||
use turso_sqlite3_parser::ast::{self, SortOrder};
|
||||
|
||||
use std::sync::Arc;
|
||||
@@ -442,116 +441,94 @@ pub fn open_loop(
|
||||
program.preassign_label_to_next_insn(loop_start);
|
||||
}
|
||||
Table::Virtual(vtab) => {
|
||||
let (start_reg, count, maybe_idx_str, maybe_idx_int) =
|
||||
if vtab.kind.eq(&VTabKind::VirtualTable) {
|
||||
// Virtual‑table (non‑TVF) modules can receive constraints via xBestIndex.
|
||||
// They return information with which to pass to VFilter operation.
|
||||
// We forward every predicate that touches vtab columns.
|
||||
//
|
||||
// vtab.col = literal (always usable)
|
||||
// vtab.col = outer_table.col (usable, because outer_table is already positioned)
|
||||
// vtab.col = later_table.col (forwarded with usable = false)
|
||||
//
|
||||
// xBestIndex decides which ones it wants by setting argvIndex and whether the
|
||||
// core layer may omit them (omit = true).
|
||||
// We then materialise the RHS/LHS into registers before issuing VFilter.
|
||||
let converted_constraints = predicates
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, p)| p.should_eval_at_loop(join_index, join_order))
|
||||
.filter_map(|(i, p)| {
|
||||
// Build ConstraintInfo from the predicates
|
||||
convert_where_to_vtab_constraint(
|
||||
p,
|
||||
joined_table_index,
|
||||
i,
|
||||
join_order,
|
||||
)
|
||||
.unwrap_or(None)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
// TODO: get proper order_by information to pass to the vtab.
|
||||
// maybe encode more info on t_ctx? we need: [col_idx, is_descending]
|
||||
let index_info = vtab.best_index(&converted_constraints, &[]);
|
||||
let (start_reg, count, maybe_idx_str, maybe_idx_int) = {
|
||||
// Virtual‑table modules can receive constraints via xBestIndex.
|
||||
// They return information with which to pass to VFilter operation.
|
||||
// We forward every predicate that touches vtab columns.
|
||||
//
|
||||
// vtab.col = literal (always usable)
|
||||
// vtab.col = outer_table.col (usable, because outer_table is already positioned)
|
||||
// vtab.col = later_table.col (forwarded with usable = false)
|
||||
//
|
||||
// xBestIndex decides which ones it wants by setting argvIndex and whether the
|
||||
// core layer may omit them (omit = true).
|
||||
// We then materialise the RHS/LHS into registers before issuing VFilter.
|
||||
let converted_constraints = predicates
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, p)| p.should_eval_at_loop(join_index, join_order))
|
||||
.filter_map(|(i, p)| {
|
||||
// Build ConstraintInfo from the predicates
|
||||
convert_where_to_vtab_constraint(
|
||||
p,
|
||||
joined_table_index,
|
||||
i,
|
||||
join_order,
|
||||
)
|
||||
.unwrap_or(None)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
// TODO: get proper order_by information to pass to the vtab.
|
||||
// maybe encode more info on t_ctx? we need: [col_idx, is_descending]
|
||||
let index_info = vtab.best_index(&converted_constraints, &[]);
|
||||
|
||||
// Determine the number of VFilter arguments (constraints with an argv_index).
|
||||
let args_needed = index_info
|
||||
.constraint_usages
|
||||
.iter()
|
||||
.filter(|u| u.argv_index.is_some())
|
||||
.count();
|
||||
let start_reg = program.alloc_registers(args_needed);
|
||||
// Determine the number of VFilter arguments (constraints with an argv_index).
|
||||
let args_needed = index_info
|
||||
.constraint_usages
|
||||
.iter()
|
||||
.filter(|u| u.argv_index.is_some())
|
||||
.count();
|
||||
let start_reg = program.alloc_registers(args_needed);
|
||||
|
||||
// For each constraint used by best_index, translate the opposite side.
|
||||
for (i, usage) in index_info.constraint_usages.iter().enumerate() {
|
||||
if let Some(argv_index) = usage.argv_index {
|
||||
if let Some(cinfo) = converted_constraints.get(i) {
|
||||
let (pred_idx, is_rhs) = cinfo.unpack_plan_info();
|
||||
if let ast::Expr::Binary(lhs, _, rhs) =
|
||||
&predicates[pred_idx].expr
|
||||
{
|
||||
// translate the opposite side of the referenced vtab column
|
||||
let expr = if is_rhs { lhs } else { rhs };
|
||||
// argv_index is 1-based; adjust to get the proper register offset.
|
||||
if argv_index == 0 {
|
||||
// invalid since argv_index is 1-based
|
||||
continue;
|
||||
}
|
||||
let target_reg =
|
||||
start_reg + (argv_index - 1) as usize;
|
||||
translate_expr(
|
||||
program,
|
||||
Some(table_references),
|
||||
expr,
|
||||
target_reg,
|
||||
&t_ctx.resolver,
|
||||
)?;
|
||||
if cinfo.usable && usage.omit {
|
||||
predicates[pred_idx].consumed.set(true);
|
||||
}
|
||||
// For each constraint used by best_index, translate the opposite side.
|
||||
for (i, usage) in index_info.constraint_usages.iter().enumerate() {
|
||||
if let Some(argv_index) = usage.argv_index {
|
||||
if let Some(cinfo) = converted_constraints.get(i) {
|
||||
let (pred_idx, is_rhs) = cinfo.unpack_plan_info();
|
||||
if let ast::Expr::Binary(lhs, _, rhs) =
|
||||
&predicates[pred_idx].expr
|
||||
{
|
||||
// translate the opposite side of the referenced vtab column
|
||||
let expr = if is_rhs { lhs } else { rhs };
|
||||
// argv_index is 1-based; adjust to get the proper register offset.
|
||||
if argv_index == 0 {
|
||||
// invalid since argv_index is 1-based
|
||||
continue;
|
||||
}
|
||||
let target_reg = start_reg + (argv_index - 1) as usize;
|
||||
translate_expr(
|
||||
program,
|
||||
Some(table_references),
|
||||
expr,
|
||||
target_reg,
|
||||
&t_ctx.resolver,
|
||||
)?;
|
||||
if cinfo.usable && usage.omit {
|
||||
predicates[pred_idx].consumed.set(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If best_index provided an idx_str, translate it.
|
||||
let maybe_idx_str = if let Some(idx_str) = index_info.idx_str {
|
||||
let reg = program.alloc_register();
|
||||
program.emit_insn(Insn::String8 {
|
||||
dest: reg,
|
||||
value: idx_str,
|
||||
});
|
||||
Some(reg)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(
|
||||
start_reg,
|
||||
args_needed,
|
||||
maybe_idx_str,
|
||||
Some(index_info.idx_num),
|
||||
)
|
||||
// If best_index provided an idx_str, translate it.
|
||||
let maybe_idx_str = if let Some(idx_str) = index_info.idx_str {
|
||||
let reg = program.alloc_register();
|
||||
program.emit_insn(Insn::String8 {
|
||||
dest: reg,
|
||||
value: idx_str,
|
||||
});
|
||||
Some(reg)
|
||||
} else {
|
||||
// For table-valued functions: translate the table args.
|
||||
let args = match vtab.args.as_ref() {
|
||||
Some(args) => args,
|
||||
None => &vec![],
|
||||
};
|
||||
let start_reg = program.alloc_registers(args.len());
|
||||
let mut cur_reg = start_reg;
|
||||
for arg in args {
|
||||
let reg = cur_reg;
|
||||
cur_reg += 1;
|
||||
let _ = translate_expr(
|
||||
program,
|
||||
Some(table_references),
|
||||
arg,
|
||||
reg,
|
||||
&t_ctx.resolver,
|
||||
)?;
|
||||
}
|
||||
(start_reg, args.len(), None, None)
|
||||
None
|
||||
};
|
||||
(
|
||||
start_reg,
|
||||
args_needed,
|
||||
maybe_idx_str,
|
||||
Some(index_info.idx_num),
|
||||
)
|
||||
};
|
||||
|
||||
// Emit VFilter with the computed arguments.
|
||||
program.emit_insn(Insn::VFilter {
|
||||
|
||||
@@ -19,6 +19,7 @@ use crate::{
|
||||
vdbe::{builder::TableRefIdCounter, BranchOffset},
|
||||
Result,
|
||||
};
|
||||
use turso_sqlite3_parser::ast::Literal::Null;
|
||||
use turso_sqlite3_parser::ast::{
|
||||
self, As, Expr, FromClause, JoinType, Limit, Materialized, QualifiedName, TableInternalId,
|
||||
UnaryOperator, With,
|
||||
@@ -243,6 +244,7 @@ fn parse_from_clause_table(
|
||||
schema: &Schema,
|
||||
table: ast::SelectTable,
|
||||
table_references: &mut TableReferences,
|
||||
out_where_clause: &mut Vec<WhereTerm>,
|
||||
ctes: &mut Vec<JoinedTable>,
|
||||
syms: &SymbolTable,
|
||||
table_ref_counter: &mut TableRefIdCounter,
|
||||
@@ -253,8 +255,10 @@ fn parse_from_clause_table(
|
||||
table_references,
|
||||
ctes,
|
||||
table_ref_counter,
|
||||
out_where_clause,
|
||||
qualified_name,
|
||||
maybe_alias,
|
||||
None,
|
||||
),
|
||||
ast::SelectTable::Select(subselect, maybe_alias) => {
|
||||
let Plan::Select(subplan) = prepare_select_plan(
|
||||
@@ -286,42 +290,30 @@ fn parse_from_clause_table(
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
ast::SelectTable::TableCall(qualified_name, maybe_args, maybe_alias) => {
|
||||
let normalized_name = &normalize_ident(qualified_name.name.0.as_str());
|
||||
let vtab = crate::VirtualTable::function(normalized_name, maybe_args, syms)?;
|
||||
let alias = maybe_alias
|
||||
.as_ref()
|
||||
.map(|a| match a {
|
||||
ast::As::As(id) => id.0.clone(),
|
||||
ast::As::Elided(id) => id.0.clone(),
|
||||
})
|
||||
.unwrap_or(normalized_name.to_string());
|
||||
|
||||
table_references.add_joined_table(JoinedTable {
|
||||
op: Operation::Scan {
|
||||
iter_dir: IterationDirection::Forwards,
|
||||
index: None,
|
||||
},
|
||||
join_info: None,
|
||||
table: Table::Virtual(vtab),
|
||||
identifier: alias,
|
||||
internal_id: table_ref_counter.next(),
|
||||
col_used_mask: ColumnUsedMask::default(),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
ast::SelectTable::TableCall(qualified_name, maybe_args, maybe_alias) => parse_table(
|
||||
schema,
|
||||
table_references,
|
||||
ctes,
|
||||
table_ref_counter,
|
||||
out_where_clause,
|
||||
qualified_name,
|
||||
maybe_alias,
|
||||
maybe_args,
|
||||
),
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn parse_table(
|
||||
schema: &Schema,
|
||||
table_references: &mut TableReferences,
|
||||
ctes: &mut Vec<JoinedTable>,
|
||||
table_ref_counter: &mut TableRefIdCounter,
|
||||
out_where_clause: &mut Vec<WhereTerm>,
|
||||
qualified_name: QualifiedName,
|
||||
maybe_alias: Option<As>,
|
||||
maybe_args: Option<Vec<Expr>>,
|
||||
) -> Result<()> {
|
||||
let normalized_qualified_name = normalize_ident(qualified_name.name.0.as_str());
|
||||
// Check if the FROM clause table is referring to a CTE in the current scope.
|
||||
@@ -343,7 +335,16 @@ fn parse_table(
|
||||
ast::As::Elided(id) => id,
|
||||
})
|
||||
.map(|a| a.0);
|
||||
let internal_id = table_ref_counter.next();
|
||||
let tbl_ref = if let Table::Virtual(tbl) = table.as_ref() {
|
||||
if let Some(args) = maybe_args {
|
||||
transform_args_into_where_terms(
|
||||
args,
|
||||
internal_id,
|
||||
out_where_clause,
|
||||
table.as_ref(),
|
||||
)?;
|
||||
}
|
||||
Table::Virtual(tbl.clone())
|
||||
} else if let Table::BTree(table) = table.as_ref() {
|
||||
Table::BTree(table.clone())
|
||||
@@ -359,7 +360,7 @@ fn parse_table(
|
||||
},
|
||||
table: tbl_ref,
|
||||
identifier: alias.unwrap_or(normalized_qualified_name),
|
||||
internal_id: table_ref_counter.next(),
|
||||
internal_id,
|
||||
join_info: None,
|
||||
col_used_mask: ColumnUsedMask::default(),
|
||||
});
|
||||
@@ -395,6 +396,72 @@ fn parse_table(
|
||||
crate::bail_parse_error!("no such table: {}", normalized_qualified_name);
|
||||
}
|
||||
|
||||
fn transform_args_into_where_terms(
|
||||
args: Vec<Expr>,
|
||||
internal_id: TableInternalId,
|
||||
out_where_clause: &mut Vec<WhereTerm>,
|
||||
table: &Table,
|
||||
) -> Result<()> {
|
||||
let mut args_iter = args.into_iter();
|
||||
let mut hidden_count = 0;
|
||||
for (i, col) in table.columns().iter().enumerate() {
|
||||
if !col.hidden {
|
||||
continue;
|
||||
}
|
||||
hidden_count += 1;
|
||||
|
||||
if let Some(arg_expr) = args_iter.next() {
|
||||
if contains_column_reference(&arg_expr)? {
|
||||
crate::bail_parse_error!(
|
||||
"Column references are not supported as table-valued function arguments yet"
|
||||
);
|
||||
}
|
||||
let column_expr = Expr::Column {
|
||||
database: None,
|
||||
table: internal_id,
|
||||
column: i,
|
||||
is_rowid_alias: col.is_rowid_alias,
|
||||
};
|
||||
let expr = match arg_expr {
|
||||
Expr::Literal(Null) => Expr::IsNull(Box::new(column_expr)),
|
||||
other => Expr::Binary(
|
||||
Box::new(column_expr),
|
||||
ast::Operator::Equals,
|
||||
Box::new(other),
|
||||
),
|
||||
};
|
||||
out_where_clause.push(WhereTerm {
|
||||
expr,
|
||||
from_outer_join: None,
|
||||
consumed: Cell::new(false),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if args_iter.next().is_some() {
|
||||
return Err(crate::LimboError::ParseError(format!(
|
||||
"Too many arguments for {}: expected at most {}, got {}",
|
||||
table.get_name(),
|
||||
hidden_count,
|
||||
hidden_count + 1 + args_iter.count()
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn contains_column_reference(top_level_expr: &Expr) -> Result<bool> {
|
||||
let mut contains = false;
|
||||
walk_expr(top_level_expr, &mut |expr: &Expr| -> Result<WalkControl> {
|
||||
match expr {
|
||||
Expr::Id(_) | Expr::Qualified(_, _) | Expr::Column { .. } => contains = true,
|
||||
_ => {}
|
||||
};
|
||||
Ok(WalkControl::Continue)
|
||||
})?;
|
||||
Ok(contains)
|
||||
}
|
||||
|
||||
pub fn parse_from(
|
||||
schema: &Schema,
|
||||
mut from: Option<FromClause>,
|
||||
@@ -485,6 +552,7 @@ pub fn parse_from(
|
||||
schema,
|
||||
select_owned,
|
||||
table_references,
|
||||
out_where_clause,
|
||||
&mut ctes_as_subqueries,
|
||||
syms,
|
||||
table_ref_counter,
|
||||
@@ -723,6 +791,7 @@ fn parse_join(
|
||||
schema,
|
||||
table,
|
||||
table_references,
|
||||
out_where_clause,
|
||||
ctes,
|
||||
syms,
|
||||
table_ref_counter,
|
||||
|
||||
@@ -386,7 +386,7 @@ fn query_pragma(
|
||||
translate_integrity_check(schema, &mut program)?;
|
||||
}
|
||||
PragmaName::UnstableCaptureDataChangesConn => {
|
||||
let pragma = pragma_for(pragma);
|
||||
let pragma = pragma_for(&pragma);
|
||||
let second_column = program.alloc_register();
|
||||
let opts = connection.get_capture_data_changes();
|
||||
program.emit_string8(opts.mode_name().to_string(), register);
|
||||
|
||||
29
core/util.rs
29
core/util.rs
@@ -1074,35 +1074,6 @@ pub fn parse_pragma_bool(expr: &Expr) -> Result<bool> {
|
||||
))
|
||||
}
|
||||
|
||||
// for TVF's we need these at planning time so we cannot emit translate_expr
|
||||
pub fn vtable_args(args: &[ast::Expr]) -> Vec<turso_ext::Value> {
|
||||
let mut vtable_args = Vec::new();
|
||||
for arg in args {
|
||||
match arg {
|
||||
Expr::Literal(lit) => match lit {
|
||||
Literal::Numeric(i) => {
|
||||
if i.contains('.') {
|
||||
vtable_args.push(turso_ext::Value::from_float(i.parse().unwrap()));
|
||||
} else {
|
||||
vtable_args.push(turso_ext::Value::from_integer(i.parse().unwrap()));
|
||||
}
|
||||
}
|
||||
Literal::String(s) => {
|
||||
vtable_args.push(turso_ext::Value::from_text(s.clone()));
|
||||
}
|
||||
Literal::Blob(b) => {
|
||||
vtable_args.push(turso_ext::Value::from_blob(b.as_bytes().into()));
|
||||
}
|
||||
_ => {
|
||||
vtable_args.push(turso_ext::Value::null());
|
||||
}
|
||||
},
|
||||
_ => vtable_args.push(turso_ext::Value::null()),
|
||||
}
|
||||
}
|
||||
vtable_args
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
|
||||
36
core/vtab.rs
36
core/vtab.rs
@@ -1,6 +1,6 @@
|
||||
use crate::pragma::{PragmaVirtualTable, PragmaVirtualTableCursor};
|
||||
use crate::schema::Column;
|
||||
use crate::util::{columns_from_create_table_body, vtable_args};
|
||||
use crate::util::columns_from_create_table_body;
|
||||
use crate::{Connection, LimboError, SymbolTable, Value};
|
||||
use fallible_iterator::FallibleIterator;
|
||||
use std::cell::RefCell;
|
||||
@@ -19,29 +19,33 @@ enum VirtualTableType {
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct VirtualTable {
|
||||
pub(crate) name: String,
|
||||
pub(crate) args: Option<Vec<ast::Expr>>,
|
||||
pub(crate) columns: Vec<Column>,
|
||||
pub(crate) kind: VTabKind,
|
||||
vtab_type: VirtualTableType,
|
||||
}
|
||||
|
||||
impl VirtualTable {
|
||||
pub(crate) fn function(
|
||||
name: &str,
|
||||
args: Option<Vec<ast::Expr>>,
|
||||
syms: &SymbolTable,
|
||||
) -> crate::Result<Rc<VirtualTable>> {
|
||||
pub(crate) fn builtin_functions() -> Vec<Rc<VirtualTable>> {
|
||||
PragmaVirtualTable::functions()
|
||||
.into_iter()
|
||||
.map(|(tab, schema)| {
|
||||
let vtab = VirtualTable {
|
||||
name: format!("pragma_{}", tab.pragma_name),
|
||||
columns: Self::resolve_columns(schema)
|
||||
.expect("built-in function schema resolution should not fail"),
|
||||
kind: VTabKind::TableValuedFunction,
|
||||
vtab_type: VirtualTableType::Pragma(tab),
|
||||
};
|
||||
Rc::new(vtab)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn function(name: &str, syms: &SymbolTable) -> crate::Result<Rc<VirtualTable>> {
|
||||
let module = syms.vtab_modules.get(name);
|
||||
let (vtab_type, schema) = if module.is_some() {
|
||||
let ext_args = match args {
|
||||
Some(ref args) => vtable_args(args),
|
||||
None => vec![],
|
||||
};
|
||||
ExtVirtualTable::create(name, module, ext_args, VTabKind::TableValuedFunction)
|
||||
ExtVirtualTable::create(name, module, Vec::new(), VTabKind::TableValuedFunction)
|
||||
.map(|(vtab, columns)| (VirtualTableType::External(vtab), columns))?
|
||||
} else if let Some(pragma_name) = name.strip_prefix("pragma_") {
|
||||
PragmaVirtualTable::create(pragma_name)
|
||||
.map(|(vtab, columns)| (VirtualTableType::Pragma(vtab), columns))?
|
||||
} else {
|
||||
return Err(LimboError::ParseError(format!(
|
||||
"No such table-valued function: {name}"
|
||||
@@ -50,7 +54,6 @@ impl VirtualTable {
|
||||
|
||||
let vtab = VirtualTable {
|
||||
name: name.to_owned(),
|
||||
args,
|
||||
columns: Self::resolve_columns(schema)?,
|
||||
kind: VTabKind::TableValuedFunction,
|
||||
vtab_type,
|
||||
@@ -69,7 +72,6 @@ impl VirtualTable {
|
||||
ExtVirtualTable::create(module_name, module, args, VTabKind::VirtualTable)?;
|
||||
let vtab = VirtualTable {
|
||||
name: tbl_name.unwrap_or(module_name).to_owned(),
|
||||
args: None,
|
||||
columns: Self::resolve_columns(schema)?,
|
||||
kind: VTabKind::VirtualTable,
|
||||
vtab_type: VirtualTableType::External(table),
|
||||
|
||||
@@ -15,12 +15,13 @@ register_extension! {
|
||||
vtabs: { CompletionVTabModule }
|
||||
}
|
||||
|
||||
macro_rules! try_option {
|
||||
($expr:expr, $err:expr) => {
|
||||
match $expr {
|
||||
Some(val) => val,
|
||||
None => return $err,
|
||||
}
|
||||
macro_rules! extract_arg_text {
|
||||
($args:expr, $idx:expr) => {
|
||||
$args
|
||||
.get($idx)
|
||||
.map(|v| v.to_text().unwrap_or(""))
|
||||
.unwrap_or("")
|
||||
.to_string()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -170,19 +171,21 @@ impl CompletionCursor {
|
||||
impl VTabCursor for CompletionCursor {
|
||||
type Error = ResultCode;
|
||||
|
||||
fn filter(&mut self, args: &[Value], _: Option<(&str, i32)>) -> ResultCode {
|
||||
if args.is_empty() || args.len() > 2 {
|
||||
return ResultCode::InvalidArgs;
|
||||
}
|
||||
fn filter(&mut self, args: &[Value], idx_info: Option<(&str, i32)>) -> ResultCode {
|
||||
self.reset();
|
||||
let prefix = try_option!(args[0].to_text(), ResultCode::InvalidArgs);
|
||||
|
||||
let wholeline = args.get(1).map(|v| v.to_text().unwrap_or("")).unwrap_or("");
|
||||
if let Some((_, idx_num)) = idx_info {
|
||||
let mut arg_idx = 0;
|
||||
// For the semantics of `idx_num`, see the comment in the `best_index` method.
|
||||
if idx_num & 1 != 0 {
|
||||
self.prefix = extract_arg_text!(args, arg_idx);
|
||||
arg_idx += 1;
|
||||
}
|
||||
if idx_num & 2 != 0 {
|
||||
self.line = extract_arg_text!(args, arg_idx);
|
||||
}
|
||||
}
|
||||
|
||||
self.line = wholeline.to_string();
|
||||
self.prefix = prefix.to_string();
|
||||
|
||||
// Currently best index is not implemented so the correct arg parsing is not done here
|
||||
if !self.line.is_empty() && self.prefix.is_empty() {
|
||||
let mut i = self.line.len();
|
||||
while let Some(ch) = self.line.chars().next() {
|
||||
|
||||
@@ -316,13 +316,41 @@ def _test_series(limbo: TestTursoShell):
|
||||
"SELECT * FROM generate_series(1, 10);",
|
||||
lambda res: res == "1\n2\n3\n4\n5\n6\n7\n8\n9\n10",
|
||||
)
|
||||
limbo.run_test_fn(
|
||||
"SELECT * FROM generate_series WHERE start = 1 AND stop = 10;",
|
||||
lambda res: res == "1\n2\n3\n4\n5\n6\n7\n8\n9\n10",
|
||||
)
|
||||
limbo.run_test_fn(
|
||||
"SELECT * FROM generate_series(1, 10) WHERE value < 5;",
|
||||
lambda res: res == "1\n2\n3\n4",
|
||||
)
|
||||
limbo.run_test_fn(
|
||||
"SELECT * FROM generate_series WHERE start = 1 AND stop = 10 AND value < 5;",
|
||||
lambda res: res == "1\n2\n3\n4",
|
||||
)
|
||||
limbo.run_test_fn(
|
||||
"SELECT * FROM generate_series WHERE start = 1 AND stop = 10 AND start = 5;",
|
||||
lambda res: res == "",
|
||||
)
|
||||
limbo.run_test_fn(
|
||||
"SELECT * FROM generate_series WHERE start = 1 AND stop = 10 AND start > 5;",
|
||||
lambda res: res == "",
|
||||
)
|
||||
limbo.run_test_fn(
|
||||
"SELECT * FROM generate_series;",
|
||||
lambda res: "Invalid Argument" in res or 'first argument to "generate_series()" missing or unusable' in res,
|
||||
)
|
||||
limbo.run_test_fn(
|
||||
"SELECT * FROM generate_series(1, 10, 2);",
|
||||
lambda res: res == "1\n3\n5\n7\n9",
|
||||
)
|
||||
limbo.run_test_fn(
|
||||
"SELECT * FROM generate_series WHERE start = 1 AND stop = 10 AND step = 2;",
|
||||
lambda res: res == "1\n3\n5\n7\n9",
|
||||
)
|
||||
limbo.run_test_fn(
|
||||
"SELECT * FROM generate_series(1, 10, 2, 3);",
|
||||
lambda res: "Invalid Argument" in res or "too many arguments" in res,
|
||||
lambda res: "too many arguments" in res.lower(),
|
||||
)
|
||||
limbo.run_test_fn(
|
||||
"SELECT * FROM generate_series(10, 1, -2);",
|
||||
@@ -949,6 +977,22 @@ def _test_hidden_columns(exec_name, ext_path):
|
||||
"SELECT * FROM r NATURAL JOIN l NATURAL JOIN r;",
|
||||
lambda res: "comment0|2|3" == res,
|
||||
)
|
||||
limbo.run_test_fn(
|
||||
"SELECT * FROM (SELECT * FROM l JOIN r USING(key, value)) JOIN r USING(comment, key, value);",
|
||||
lambda res: "2|3|comment0" == res,
|
||||
)
|
||||
limbo.run_test_fn(
|
||||
"SELECT * FROM (SELECT * FROM l NATURAL JOIN r) JOIN r USING(comment, key, value);",
|
||||
lambda res: "2|3|comment0" == res,
|
||||
)
|
||||
limbo.run_test_fn(
|
||||
"SELECT * FROM l JOIN r USING(key, value) JOIN r USING(comment, key, value);",
|
||||
lambda res: "" == res,
|
||||
)
|
||||
limbo.run_test_fn(
|
||||
"SELECT * FROM l NATURAL JOIN r JOIN r USING(comment, key, value);",
|
||||
lambda res: "" == res,
|
||||
)
|
||||
|
||||
limbo.quit()
|
||||
|
||||
|
||||
@@ -83,6 +83,15 @@ do_execsql_test pragma-function-table-info {
|
||||
4|sql|TEXT|0||0
|
||||
}
|
||||
|
||||
do_execsql_test pragma-vtab-table-info {
|
||||
SELECT * FROM pragma_table_info WHERE arg = 'sqlite_schema'
|
||||
} {0|type|TEXT|0||0
|
||||
1|name|TEXT|0||0
|
||||
2|tbl_name|TEXT|0||0
|
||||
3|rootpage|INT|0||0
|
||||
4|sql|TEXT|0||0
|
||||
}
|
||||
|
||||
do_execsql_test pragma-table-info-invalid-table {
|
||||
PRAGMA table_info=pekka
|
||||
} {}
|
||||
@@ -91,6 +100,10 @@ do_execsql_test pragma-function-table-info-invalid-table {
|
||||
SELECT * FROM pragma_table_info('pekka')
|
||||
} {}
|
||||
|
||||
do_execsql_test pragma-vtab-table-info-invalid-table {
|
||||
SELECT * FROM pragma_table_info WHERE arg = 'pekka'
|
||||
} {}
|
||||
|
||||
# temporarily skip this test case. The issue is detailed in #1407
|
||||
#do_execsql_test_on_specific_db ":memory:" pragma-page-count-empty {
|
||||
# PRAGMA page_count
|
||||
@@ -128,17 +141,17 @@ do_execsql_test pragma-legacy-file-format {
|
||||
PRAGMA legacy_file_format
|
||||
} {}
|
||||
|
||||
do_execsql_test_error_content pragma-function-legacy-file-format {
|
||||
do_execsql_test_error pragma-function-legacy-file-format {
|
||||
SELECT * FROM pragma_legacy_file_format()
|
||||
} {"No such table"}
|
||||
} {(no such table|Table.*not found)}
|
||||
|
||||
do_execsql_test_error_content pragma-function-too-many-arguments {
|
||||
SELECT * FROM pragma_table_info('sqlite_schema', 'main', 'arg3')
|
||||
} {"Too many arguments"}
|
||||
|
||||
do_execsql_test_error_content pragma-function-update {
|
||||
do_execsql_test_error pragma-function-update {
|
||||
SELECT * FROM pragma_wal_checkpoint()
|
||||
} {"No such table"}
|
||||
} {(no such table|Table.*not found)}
|
||||
|
||||
do_execsql_test pragma-function-nontext-argument {
|
||||
SELECT * FROM pragma_table_info('sqlite_schema', NULL);
|
||||
@@ -149,10 +162,23 @@ do_execsql_test pragma-function-nontext-argument {
|
||||
4|sql|TEXT|0||0
|
||||
}
|
||||
|
||||
do_execsql_test pragma-vtab-nontext-argument {
|
||||
SELECT * FROM pragma_table_info WHERE arg ='sqlite_schema' AND schema IS NULL;
|
||||
} {0|type|TEXT|0||0
|
||||
1|name|TEXT|0||0
|
||||
2|tbl_name|TEXT|0||0
|
||||
3|rootpage|INT|0||0
|
||||
4|sql|TEXT|0||0
|
||||
}
|
||||
|
||||
do_execsql_test pragma-function-no-arguments {
|
||||
SELECT * FROM pragma_table_info();
|
||||
} {}
|
||||
|
||||
do_execsql_test pragma-vtab-no-arguments {
|
||||
SELECT * FROM pragma_table_info;
|
||||
} {}
|
||||
|
||||
do_execsql_test_on_specific_db ":memory:" pragma-function-argument-with-space {
|
||||
CREATE TABLE "foo bar"(c0);
|
||||
SELECT * FROM pragma_table_info('foo bar')
|
||||
|
||||
Reference in New Issue
Block a user