mirror of
https://github.com/aljazceru/turso.git
synced 2026-01-07 10:14:21 +01:00
This permits only `ANALYZE <table_name>` to work, and all other forms fail with a parse error (as documented in the tests). On SQLite, ANALYZE generates: sqlite> CREATE TABLE sqlite_stat1(tbl,idx,stat); sqlite> CREATE TABLE iiftest(a int, b int, c int); sqlite> EXPLAIN ANALYZE iiftest; addr opcode p1 p2 p3 p4 p5 comment ---- ------------- ---- ---- ---- ------------- -- ------------- 0 Init 0 21 0 0 Start at 21 1 Null 0 1 0 0 r[1]=NULL 2 OpenWrite 3 4 0 3 0 root=4 iDb=0; sqlite_stat1 3 Rewind 3 9 0 0 4 Column 3 0 2 0 r[2]= cursor 3 column 0 5 Ne 3 8 2 BINARY-8 81 if r[2]!=r[3] goto 8 6 Rowid 3 4 0 0 r[4]=sqlite_stat1.rowid 7 Delete 3 0 0 sqlite_stat1 2 8 Next 3 4 0 1 9 OpenWrite 0 4 0 3 0 root=4 iDb=0; sqlite_stat1 10 OpenRead 4 2 0 3 0 root=2 iDb=0; iiftest 11 String8 0 11 0 iiftest 0 r[11]='iiftest'; iiftest 12 Count 4 13 0 0 r[13]=count() 13 IfNot 13 18 0 0 14 Null 0 12 0 0 r[12]=NULL 15 MakeRecord 11 3 9 BBB 0 r[9]=mkrec(r[11..13]) 16 NewRowid 0 5 0 0 r[5]=rowid 17 Insert 0 9 5 8 intkey=r[5] data=r[9] 18 LoadAnalysis 0 0 0 0 19 Expire 0 0 0 0 20 Halt 0 0 0 0 21 Transaction 0 1 9 0 1 usesStmtJournal=0 22 String8 0 3 0 iiftest 0 r[3]='iiftest' 23 Goto 0 1 0 0 Turso can now generate: turso> create table sqlite_stat1(tbl,idx,stat); turso> create table iiftest(a int, b int, c int); turso> explain analyze iiftest; addr opcode p1 p2 p3 p4 p5 comment ---- ----------------- ---- ---- ---- ------------- -- ------- 0 Init 0 19 0 0 Start at 19 1 Null 0 1 0 0 r[1]=NULL 2 OpenWrite 0 2 0 0 root=2; iDb=0 3 Rewind 0 9 0 0 Rewind sqlite_stat1 4 Column 0 0 2 0 r[2]=sqlite_stat1.tbl 5 Ne 2 3 9 0 if r[2]!=r[3] goto 9 6 RowId 0 4 0 0 r[4]=sqlite_stat1.rowid 7 Delete 0 0 0 sqlite_stat1 0 8 Next 0 4 0 0 9 OpenWrite 1 2 0 0 root=2; iDb=0 10 OpenRead 2 3 0 0 =iiftest, root=3, iDb=0 11 String8 0 7 0 iiftest 0 r[7]='iiftest' 12 Count 2 9 0 0 13 IfNot 9 18 0 0 if !r[9] goto 18 14 Null 0 8 0 0 r[8]=NULL 15 MakeRecord 7 3 6 0 r[6]=mkrec(r[7..9]) 16 NewRowid 1 5 0 0 r[5]=rowid 17 Insert 1 6 5 sqlite_stat1 0 intkey=r[5] data=r[6] 18 Halt 0 0 0 0 19 String8 0 3 0 iiftest 0 r[3]='iiftest' 20 Goto 0 1 0 0 Note the missing support for LoadAnalysis and Expire, but there's no optimizer work done yet to leverage any gathered statistics yet anyway.
321 lines
10 KiB
Rust
321 lines
10 KiB
Rust
//! The VDBE bytecode code generator.
|
|
//!
|
|
//! This module is responsible for translating the SQL AST into a sequence of
|
|
//! instructions for the VDBE. The VDBE is a register-based virtual machine that
|
|
//! executes bytecode instructions. This code generator is responsible for taking
|
|
//! the SQL AST and generating the corresponding VDBE instructions. For example,
|
|
//! a SELECT statement will be translated into a sequence of instructions that
|
|
//! will read rows from the database and filter them according to a WHERE clause.
|
|
|
|
pub(crate) mod aggregation;
|
|
pub(crate) mod alter;
|
|
pub(crate) mod analyze;
|
|
pub(crate) mod attach;
|
|
pub(crate) mod collate;
|
|
mod compound_select;
|
|
pub(crate) mod delete;
|
|
pub(crate) mod display;
|
|
pub(crate) mod emitter;
|
|
pub(crate) mod expr;
|
|
pub(crate) mod group_by;
|
|
pub(crate) mod index;
|
|
pub(crate) mod insert;
|
|
pub(crate) mod integrity_check;
|
|
pub(crate) mod main_loop;
|
|
pub(crate) mod optimizer;
|
|
pub(crate) mod order_by;
|
|
pub(crate) mod plan;
|
|
pub(crate) mod planner;
|
|
pub(crate) mod pragma;
|
|
pub(crate) mod result_row;
|
|
pub(crate) mod rollback;
|
|
pub(crate) mod schema;
|
|
pub(crate) mod select;
|
|
pub(crate) mod subquery;
|
|
pub(crate) mod transaction;
|
|
pub(crate) mod update;
|
|
mod values;
|
|
pub(crate) mod view;
|
|
|
|
use crate::schema::Schema;
|
|
use crate::storage::pager::Pager;
|
|
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 alter::translate_alter_table;
|
|
use analyze::translate_analyze;
|
|
use index::{translate_create_index, translate_drop_index};
|
|
use insert::translate_insert;
|
|
use rollback::translate_rollback;
|
|
use schema::{translate_create_table, translate_create_virtual_table, translate_drop_table};
|
|
use select::translate_select;
|
|
use std::rc::Rc;
|
|
use std::sync::Arc;
|
|
use tracing::{instrument, Level};
|
|
use transaction::{translate_tx_begin, translate_tx_commit};
|
|
use turso_parser::ast::{self, Indexed};
|
|
use update::translate_update;
|
|
|
|
#[instrument(skip_all, level = Level::DEBUG)]
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn translate(
|
|
schema: &Schema,
|
|
stmt: ast::Stmt,
|
|
pager: Rc<Pager>,
|
|
connection: Arc<Connection>,
|
|
syms: &SymbolTable,
|
|
query_mode: QueryMode,
|
|
input: &str,
|
|
) -> Result<Program> {
|
|
tracing::trace!("querying {}", input);
|
|
let change_cnt_on = matches!(
|
|
stmt,
|
|
ast::Stmt::CreateIndex { .. }
|
|
| ast::Stmt::Delete { .. }
|
|
| ast::Stmt::Insert { .. }
|
|
| ast::Stmt::Update { .. }
|
|
);
|
|
|
|
let mut program = ProgramBuilder::new(
|
|
query_mode,
|
|
connection.get_capture_data_changes().clone(),
|
|
// These options will be extended whithin each translate program
|
|
ProgramBuilderOpts {
|
|
num_cursors: 1,
|
|
approx_num_insns: 2,
|
|
approx_num_labels: 2,
|
|
},
|
|
);
|
|
|
|
program.prologue();
|
|
|
|
program = match stmt {
|
|
// There can be no nesting with pragma, so lift it up here
|
|
ast::Stmt::Pragma { name, body } => pragma::translate_pragma(
|
|
schema,
|
|
syms,
|
|
&name,
|
|
body,
|
|
pager,
|
|
connection.clone(),
|
|
program,
|
|
)?,
|
|
stmt => translate_inner(schema, stmt, syms, program, &connection, input)?,
|
|
};
|
|
|
|
program.epilogue(schema);
|
|
|
|
Ok(program.build(connection, change_cnt_on, input))
|
|
}
|
|
|
|
// TODO: for now leaving the return value as a Program. But ideally to support nested parsing of arbitraty
|
|
// statements, we would have to return a program builder instead
|
|
/// Translate SQL statement into bytecode program.
|
|
pub fn translate_inner(
|
|
schema: &Schema,
|
|
stmt: ast::Stmt,
|
|
syms: &SymbolTable,
|
|
program: ProgramBuilder,
|
|
connection: &Arc<Connection>,
|
|
input: &str,
|
|
) -> Result<ProgramBuilder> {
|
|
let is_write = matches!(
|
|
stmt,
|
|
ast::Stmt::AlterTable { .. }
|
|
| ast::Stmt::CreateIndex { .. }
|
|
| ast::Stmt::CreateTable { .. }
|
|
| ast::Stmt::CreateTrigger { .. }
|
|
| ast::Stmt::CreateView { .. }
|
|
| ast::Stmt::CreateMaterializedView { .. }
|
|
| ast::Stmt::CreateVirtualTable(..)
|
|
| ast::Stmt::Delete { .. }
|
|
| ast::Stmt::DropIndex { .. }
|
|
| ast::Stmt::DropTable { .. }
|
|
| ast::Stmt::DropView { .. }
|
|
| ast::Stmt::Reindex { .. }
|
|
| ast::Stmt::Update { .. }
|
|
| ast::Stmt::Insert { .. }
|
|
);
|
|
|
|
if is_write && connection.get_query_only() {
|
|
bail_parse_error!("Cannot execute write statement in query_only mode")
|
|
}
|
|
|
|
let is_select = matches!(stmt, ast::Stmt::Select { .. });
|
|
|
|
let mut program = match stmt {
|
|
ast::Stmt::AlterTable(alter) => {
|
|
translate_alter_table(alter, syms, schema, program, connection, input)?
|
|
}
|
|
ast::Stmt::Analyze { name } => translate_analyze(name, schema, program)?,
|
|
ast::Stmt::Attach { expr, db_name, key } => {
|
|
attach::translate_attach(&expr, &db_name, &key, schema, syms, program)?
|
|
}
|
|
ast::Stmt::Begin { typ, name } => translate_tx_begin(typ, name, schema, program)?,
|
|
ast::Stmt::Commit { name } => translate_tx_commit(name, program)?,
|
|
ast::Stmt::CreateIndex {
|
|
unique,
|
|
if_not_exists,
|
|
idx_name,
|
|
tbl_name,
|
|
columns,
|
|
where_clause,
|
|
} => {
|
|
if where_clause.is_some() {
|
|
bail_parse_error!("Partial indexes are not supported");
|
|
}
|
|
translate_create_index(
|
|
(unique, if_not_exists),
|
|
idx_name.name.as_str(),
|
|
tbl_name.as_str(),
|
|
&columns,
|
|
schema,
|
|
syms,
|
|
program,
|
|
)?
|
|
}
|
|
ast::Stmt::CreateTable {
|
|
temporary,
|
|
if_not_exists,
|
|
tbl_name,
|
|
body,
|
|
} => translate_create_table(
|
|
tbl_name,
|
|
temporary,
|
|
body,
|
|
if_not_exists,
|
|
schema,
|
|
syms,
|
|
program,
|
|
)?,
|
|
ast::Stmt::CreateTrigger { .. } => bail_parse_error!("CREATE TRIGGER not supported yet"),
|
|
ast::Stmt::CreateView {
|
|
view_name,
|
|
select,
|
|
columns,
|
|
..
|
|
} => view::translate_create_view(
|
|
schema,
|
|
view_name.name.as_str(),
|
|
&select,
|
|
&columns,
|
|
connection.clone(),
|
|
syms,
|
|
program,
|
|
)?,
|
|
ast::Stmt::CreateMaterializedView {
|
|
view_name, select, ..
|
|
} => view::translate_create_materialized_view(
|
|
schema,
|
|
view_name.name.as_str(),
|
|
&select,
|
|
connection.clone(),
|
|
syms,
|
|
program,
|
|
)?,
|
|
ast::Stmt::CreateVirtualTable(vtab) => {
|
|
translate_create_virtual_table(vtab, schema, syms, program)?
|
|
}
|
|
ast::Stmt::Delete {
|
|
tbl_name,
|
|
where_clause,
|
|
limit,
|
|
returning,
|
|
indexed,
|
|
order_by,
|
|
with,
|
|
} => {
|
|
if with.is_some() {
|
|
bail_parse_error!("WITH clause is not supported in DELETE");
|
|
}
|
|
if indexed.is_some_and(|i| matches!(i, Indexed::IndexedBy(_))) {
|
|
bail_parse_error!("INDEXED BY clause is not supported in DELETE");
|
|
}
|
|
if !order_by.is_empty() {
|
|
bail_parse_error!("ORDER BY clause is not supported in DELETE");
|
|
}
|
|
translate_delete(
|
|
schema,
|
|
&tbl_name,
|
|
where_clause,
|
|
limit,
|
|
returning,
|
|
syms,
|
|
program,
|
|
connection,
|
|
)?
|
|
}
|
|
ast::Stmt::Detach { name } => attach::translate_detach(&name, schema, syms, program)?,
|
|
ast::Stmt::DropIndex {
|
|
if_exists,
|
|
idx_name,
|
|
} => translate_drop_index(idx_name.name.as_str(), if_exists, schema, syms, program)?,
|
|
ast::Stmt::DropTable {
|
|
if_exists,
|
|
tbl_name,
|
|
} => translate_drop_table(tbl_name, if_exists, schema, syms, program)?,
|
|
ast::Stmt::DropTrigger { .. } => bail_parse_error!("DROP TRIGGER not supported yet"),
|
|
ast::Stmt::DropView {
|
|
if_exists,
|
|
view_name,
|
|
} => view::translate_drop_view(schema, view_name.name.as_str(), if_exists, program)?,
|
|
ast::Stmt::Pragma { .. } => {
|
|
bail_parse_error!("PRAGMA statement cannot be evaluated in a nested context")
|
|
}
|
|
ast::Stmt::Reindex { .. } => bail_parse_error!("REINDEX not supported yet"),
|
|
ast::Stmt::Release { .. } => bail_parse_error!("RELEASE not supported yet"),
|
|
ast::Stmt::Rollback {
|
|
tx_name,
|
|
savepoint_name,
|
|
} => translate_rollback(schema, syms, program, tx_name, savepoint_name)?,
|
|
ast::Stmt::Savepoint { .. } => bail_parse_error!("SAVEPOINT not supported yet"),
|
|
ast::Stmt::Select(select) => {
|
|
translate_select(
|
|
schema,
|
|
select,
|
|
syms,
|
|
program,
|
|
plan::QueryDestination::ResultRows,
|
|
connection,
|
|
)?
|
|
.program
|
|
}
|
|
ast::Stmt::Update(mut update) => {
|
|
translate_update(schema, &mut update, syms, program, connection)?
|
|
}
|
|
ast::Stmt::Vacuum { .. } => bail_parse_error!("VACUUM not supported yet"),
|
|
ast::Stmt::Insert {
|
|
with,
|
|
or_conflict,
|
|
tbl_name,
|
|
columns,
|
|
body,
|
|
returning,
|
|
} => translate_insert(
|
|
schema,
|
|
with,
|
|
or_conflict,
|
|
tbl_name,
|
|
columns,
|
|
body,
|
|
returning,
|
|
syms,
|
|
program,
|
|
connection,
|
|
)?,
|
|
};
|
|
|
|
// Indicate write operations so that in the epilogue we can emit the correct type of transaction
|
|
if is_write {
|
|
program.begin_write_operation();
|
|
}
|
|
|
|
// Indicate read operations so that in the epilogue we can emit the correct type of transaction
|
|
if is_select && !program.table_references.is_empty() {
|
|
program.begin_read_operation();
|
|
}
|
|
|
|
Ok(program)
|
|
}
|