mirror of
https://github.com/aljazceru/turso.git
synced 2025-12-26 20:44:23 +01:00
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.
275 lines
8.0 KiB
Rust
275 lines
8.0 KiB
Rust
//! Reference for implementation
|
|
//! https://github.com/sqlite/sqlite/blob/a80089c5167856f0aadc9c878bd65843df724c06/ext/misc/completion.c
|
|
|
|
mod keywords;
|
|
|
|
use std::sync::Arc;
|
|
|
|
use keywords::KEYWORDS;
|
|
use turso_ext::{
|
|
register_extension, Connection, ConstraintInfo, ConstraintOp, ConstraintUsage, IndexInfo,
|
|
OrderByInfo, ResultCode, VTabCursor, VTabModule, VTabModuleDerive, VTable, Value,
|
|
};
|
|
|
|
register_extension! {
|
|
vtabs: { CompletionVTabModule }
|
|
}
|
|
|
|
macro_rules! extract_arg_text {
|
|
($args:expr, $idx:expr) => {
|
|
$args
|
|
.get($idx)
|
|
.map(|v| v.to_text().unwrap_or(""))
|
|
.unwrap_or("")
|
|
.to_string()
|
|
};
|
|
}
|
|
|
|
#[derive(Debug, Default, PartialEq, Clone)]
|
|
enum CompletionPhase {
|
|
#[default]
|
|
Keywords = 1,
|
|
// TODO other options now implemented for now
|
|
// Pragmas = 2,
|
|
// Functions = 3,
|
|
// Collations = 4,
|
|
// Indexes = 5,
|
|
// Triggers = 6,
|
|
// Databases = 7,
|
|
// Tables = 8, // Also VIEWs and TRIGGERs
|
|
// Columns = 9,
|
|
// Modules = 10,
|
|
Eof = 11,
|
|
}
|
|
|
|
impl From<CompletionPhase> for i64 {
|
|
fn from(val: CompletionPhase) -> Self {
|
|
use self::CompletionPhase::*;
|
|
match val {
|
|
Keywords => 1,
|
|
// Pragmas => 2,
|
|
// Functions => 3,
|
|
// Collations => 4,
|
|
// Indexes => 5,
|
|
// Triggers => 6,
|
|
// Databases => 7,
|
|
// Tables => 8,
|
|
// Columns => 9,
|
|
// Modules => 10,
|
|
Eof => 11,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A virtual table that generates candidate completions
|
|
#[derive(Debug, Default, VTabModuleDerive)]
|
|
struct CompletionVTabModule {}
|
|
|
|
impl VTabModule for CompletionVTabModule {
|
|
type Table = CompletionTable;
|
|
const NAME: &'static str = "completion";
|
|
const VTAB_KIND: turso_ext::VTabKind = turso_ext::VTabKind::TableValuedFunction;
|
|
|
|
fn create(_args: &[Value]) -> Result<(String, Self::Table), ResultCode> {
|
|
let schema = "CREATE TABLE completion(
|
|
candidate TEXT,
|
|
prefix TEXT HIDDEN,
|
|
wholeline TEXT HIDDEN,
|
|
phase INT HIDDEN
|
|
)"
|
|
.to_string();
|
|
Ok((schema, CompletionTable {}))
|
|
}
|
|
}
|
|
|
|
struct CompletionTable {}
|
|
|
|
impl VTable for CompletionTable {
|
|
type Cursor = CompletionCursor;
|
|
type Error = ResultCode;
|
|
|
|
fn open(&self, _conn: Option<Arc<Connection>>) -> Result<Self::Cursor, Self::Error> {
|
|
Ok(CompletionCursor::default())
|
|
}
|
|
|
|
fn best_index(constraints: &[ConstraintInfo], _order_by: &[OrderByInfo]) -> IndexInfo {
|
|
// The bits of `idx_num` are used to indicate which arguments are available to the filter method:
|
|
// - Bit 0 set -> 'prefix' is available
|
|
// - Bit 1 set -> 'wholeline' is available
|
|
let mut idx_num = 0;
|
|
let mut prefix_idx = None;
|
|
let mut wholeline_idx = None;
|
|
|
|
for (i, c) in constraints.iter().enumerate() {
|
|
if !c.usable || c.op != ConstraintOp::Eq {
|
|
continue;
|
|
}
|
|
match c.column_index {
|
|
1 => {
|
|
prefix_idx = Some(i);
|
|
idx_num |= 1;
|
|
}
|
|
2 => {
|
|
wholeline_idx = Some(i);
|
|
idx_num |= 2;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
let mut argv_idx = 1;
|
|
let constraint_usages = constraints
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, _)| {
|
|
if Some(i) == prefix_idx || Some(i) == wholeline_idx {
|
|
let usage = ConstraintUsage {
|
|
argv_index: Some(argv_idx),
|
|
omit: true,
|
|
};
|
|
argv_idx += 1;
|
|
usage
|
|
} else {
|
|
ConstraintUsage {
|
|
argv_index: Some(0),
|
|
omit: false,
|
|
}
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
IndexInfo {
|
|
idx_num,
|
|
idx_str: Some(idx_num.to_string()),
|
|
constraint_usages,
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The cursor for iterating over the completions
|
|
#[derive(Debug, Default)]
|
|
struct CompletionCursor {
|
|
line: String,
|
|
prefix: String,
|
|
curr_row: String,
|
|
rowid: i64,
|
|
phase: CompletionPhase,
|
|
inter_phase_counter: usize,
|
|
// stmt: Statement
|
|
// conn: Connection
|
|
}
|
|
|
|
impl CompletionCursor {
|
|
fn reset(&mut self) {
|
|
self.line.clear();
|
|
self.prefix.clear();
|
|
self.inter_phase_counter = 0;
|
|
}
|
|
}
|
|
|
|
impl VTabCursor for CompletionCursor {
|
|
type Error = ResultCode;
|
|
|
|
fn filter(&mut self, args: &[Value], idx_info: Option<(&str, i32)>) -> ResultCode {
|
|
self.reset();
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
if !self.line.is_empty() && self.prefix.is_empty() {
|
|
let mut i = self.line.len();
|
|
while let Some(ch) = self.line.chars().next() {
|
|
if i > 0 && (ch.is_alphanumeric() || ch == '_') {
|
|
i -= 1;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
if self.line.len() - i > 0 {
|
|
// TODO see if need to inclusive range
|
|
self.prefix = self.line[..i].to_string();
|
|
}
|
|
}
|
|
|
|
self.rowid = 0;
|
|
self.phase = CompletionPhase::Keywords;
|
|
|
|
self.next()
|
|
}
|
|
|
|
fn next(&mut self) -> ResultCode {
|
|
self.rowid += 1;
|
|
|
|
while self.phase != CompletionPhase::Eof {
|
|
// dbg!(&self.phase, &self.prefix, &self.curr_row);
|
|
match self.phase {
|
|
CompletionPhase::Keywords => {
|
|
if self.inter_phase_counter >= KEYWORDS.len() {
|
|
self.curr_row.clear();
|
|
self.phase = CompletionPhase::Eof;
|
|
} else {
|
|
self.curr_row.clear();
|
|
self.curr_row.push_str(KEYWORDS[self.inter_phase_counter]);
|
|
self.inter_phase_counter += 1;
|
|
}
|
|
}
|
|
// TODO implement this when db conn is available
|
|
// CompletionPhase::Databases => {
|
|
//
|
|
// // self.stmt = self.conn.prepare("PRAGMA database_list")
|
|
// curr_col = 1;
|
|
// next_phase = CompletionPhase::Tables;
|
|
// self.phase = CompletionPhase::Eof; // for now skip other phases
|
|
// }
|
|
_ => {
|
|
return ResultCode::EOF;
|
|
}
|
|
}
|
|
if self.prefix.is_empty() {
|
|
break;
|
|
}
|
|
if self.prefix.len() <= self.curr_row.len()
|
|
&& self.prefix.to_lowercase() == self.curr_row.to_lowercase()[..self.prefix.len()]
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
if self.phase == CompletionPhase::Eof {
|
|
return ResultCode::EOF;
|
|
}
|
|
ResultCode::OK
|
|
}
|
|
|
|
fn eof(&self) -> bool {
|
|
self.phase == CompletionPhase::Eof
|
|
}
|
|
|
|
fn column(&self, idx: u32) -> Result<Value, Self::Error> {
|
|
let val = match idx {
|
|
0 => Value::from_text(self.curr_row.clone()), // COMPLETION_COLUMN_CANDIDATE
|
|
1 => Value::from_text(self.prefix.clone()), // COMPLETION_COLUMN_PREFIX
|
|
2 => Value::from_text(self.line.clone()), // COMPLETION_COLUMN_WHOLELINE
|
|
3 => Value::from_integer(self.phase.clone().into()), // COMPLETION_COLUMN_PHASE
|
|
_ => Value::null(),
|
|
};
|
|
Ok(val)
|
|
}
|
|
|
|
fn rowid(&self) -> i64 {
|
|
self.rowid
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {}
|