Files
turso/core/translate/mod.rs
Alex Miller 370da9fa59 ANALYZE creates sqlite_stat1 if it doesn't exist
This change replaces a bail_parse_error!() when sqlite_stat1 doesn't
exist with the appropriate codegen to create the table, and handle both
cases of the table existing or not existing.

SQLite's codegen looks like:

sqlite> create table stat_test(a,b,c);
sqlite> explain analyze stat_test;
addr  opcode         p1    p2    p3    p4             p5  comment
----  -------------  ----  ----  ----  -------------  --  -------------
0     Init           0     40    0                    0   Start at 40
1     ReadCookie     0     3     2                    0
2     If             3     5     0                    0
3     SetCookie      0     2     4                    0
4     SetCookie      0     5     1                    0
5     CreateBtree    0     2     1                    0   r[2]=root iDb=0 flags=1
6     OpenWrite      0     1     0     5              0   root=1 iDb=0
7     NewRowid       0     1     0                    0   r[1]=rowid
8     Blob           6     3     0                   0   r[3]= (len=6)
9     Insert         0     3     1                    8   intkey=r[1] data=r[3]
10    Close          0     0     0                    0
11    Close          0     0     0                    0
12    Null           0     4     5                    0   r[4..5]=NULL
13    Noop           4     0     4                    0
14    OpenWrite      3     1     0     5              0   root=1 iDb=0; sqlite_master
15    SeekRowid      3     17    1                    0   intkey=r[1]
16    Rowid          3     5     0                    0   r[5]= rowid of 3
17    IsNull         5     26    0                    0   if r[5]==NULL goto 26
18    String8        0     6     0     table          0   r[6]='table'
19    String8        0     7     0     sqlite_stat1   0   r[7]='sqlite_stat1'
20    String8        0     8     0     sqlite_stat1   0   r[8]='sqlite_stat1'
21    Copy           2     9     0                    0   r[9]=r[2]
22    String8        0     10    0     CREATE TABLE sqlite_stat1(tbl,idx,stat) 0   r[10]='CREATE TABLE sqlite_stat1(tbl,idx,stat)'
23    MakeRecord     6     5     4     BBBDB          0   r[4]=mkrec(r[6..10])
24    Delete         3     68    5                    0
25    Insert         3     4     5                    0   intkey=r[5] data=r[4]
26    SetCookie      0     1     2                    0
27    ParseSchema    0     0     0     tbl_name='sqlite_stat1' AND type!='trigger' 0
28    OpenWrite      0     2     0     3              16  root=2 iDb=0; sqlite_stat1
29    OpenRead       5     2     0     3              0   root=2 iDb=0; stat_test
30    String8        0     18    0     stat_test      0   r[18]='stat_test'; stat_test
31    Count          5     20    0                    0   r[20]=count()
32    IfNot          20    37    0                    0
33    Null           0     19    0                    0   r[19]=NULL
34    MakeRecord     18    3     16    BBB            0   r[16]=mkrec(r[18..20])
35    NewRowid       0     12    0                    0   r[12]=rowid
36    Insert         0     16    12                   8   intkey=r[12] data=r[16]
37    LoadAnalysis   0     0     0                    0
38    Expire         0     0     0                    0
39    Halt           0     0     0                    0
40    Transaction    0     1     1     0              1   usesStmtJournal=1
41    Goto           0     1     0                    0

And now Turso's looks like:

turso> create table stat_test(a,b,c);
turso> explain analyze stat_test;
addr  opcode             p1    p2    p3    p4             p5  comment
----  -----------------  ----  ----  ----  -------------  --  -------
0     Init               0     23    0                    0   Start at 23
1     Null               0     1     0                    0   r[1]=NULL
2     CreateBtree        0     2     1                    0   r[2]=root iDb=0 flags=1
3     OpenWrite          0     1     0                    0   root=1; iDb=0
4     NewRowid           0     3     0                    0   r[3]=rowid
5     String8            0     4     0     table          0   r[4]='table'
6     String8            0     5     0     sqlite_stat1   0   r[5]='sqlite_stat1'
7     String8            0     6     0     sqlite_stat1   0   r[6]='sqlite_stat1'
8     Copy               2     7     0                    0   r[7]=r[2]
9     String8            0     8     0     CREATE TABLE sqlite_stat1(tbl,idx,stat)  0   r[8]='CREATE TABLE sqlite_stat1(tbl,idx,stat)'
10    MakeRecord         4     5     9                    0   r[9]=mkrec(r[4..8])
11    Insert             0     9     3     sqlite_stat1   0   intkey=r[3] data=r[9]
12    ParseSchema        0     0     0     tbl_name = 'sqlite_stat1' AND type != 'trigger'  0   tbl_name = 'sqlite_stat1' AND type != 'trigger'
13    OpenWrite          1     2     0                    0   root=2; iDb=0
14    OpenRead           2     2     0                    0   =stat_test, root=2, iDb=0
15    String8            0     12    0     stat_test      0   r[12]='stat_test'
16    Count              2     14    0                    0
17    IfNot              14    22    0                    0   if !r[14] goto 22
18    Null               0     13    0                    0   r[13]=NULL
19    MakeRecord         12    3     11                   0   r[11]=mkrec(r[12..14])
20    NewRowid           1     10    0                    0   r[10]=rowid
21    Insert             1     11    10    sqlite_stat1   0   intkey=r[10] data=r[11]
22    Halt               0     0     0                    0
23    Goto               0     1     0                    0

The notable difference in size is following the same codegen difference
in CREATE TABLE, where sqlite's odd dance of adding a placeholder entry
which is immediately replaced is instead done in tursodb as just
inserting the correct row in the first place. Aside from lines 6-13 of
sqlite's vdbe being missing, there's still the lack of LoadAnalysis,
Expire, and Cookie management.
2025-08-24 13:35:39 -07:00

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, syms, 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)
}