use std::sync::Arc;
use crate::bail_parse_error;
use crate::schema::{
BTreeTable, Sequence, AUTOINCREMENT_SEQ_PREFIX, SEQ_BACKING_TABLE_PREFIX,
SQLITE_SEQUENCE_TABLE_NAME,
};
use crate::storage::pager::CreateBTreeFlags;
use crate::translate::emitter::Resolver;
use crate::translate::schema::{emit_schema_entry, SchemaEntryType, SQLITE_TABLEID};
use crate::util::{escape_sql_string_literal, normalize_ident};
use crate::vdbe::builder::{CursorType, ProgramBuilder};
use crate::vdbe::insn::{to_u16, CmpInsFlags, Cookie, InsertFlags, Insn, RegisterOrLiteral};
use crate::Result;
use turso_parser::ast;
pub fn sequence_backing_table_name(seq_name: &str) -> String {
String::from(SEQ_BACKING_TABLE_PREFIX) + seq_name
}
pub fn sequence_backing_table_sql(seq_name: &str) -> String {
let table_name = sequence_backing_table_name(seq_name);
format!(
"CREATE TABLE \"{table_name}\"(\
value INTEGER PRIMARY KEY,\
is_called INTEGER,\
start INTEGER,\
inc INTEGER,\
min INTEGER,\
max INTEGER,\
cycle INTEGER)"
)
}
#[allow(clippy::too_many_arguments)]
pub fn emit_sequence_backing_table(
program: &mut ProgramBuilder,
resolver: &Resolver,
database_id: usize,
sqlite_schema_cursor_id: usize,
seq_name: &str,
start: i64,
increment: i64,
min_value: i64,
max_value: i64,
cycle: bool,
) -> Result<()> {
let backing_table_name = sequence_backing_table_name(seq_name);
let sql = sequence_backing_table_sql(seq_name);
let table_root_reg = program.alloc_register();
program.emit_insn(Insn::CreateBtree {
db: database_id,
root: table_root_reg,
flags: CreateBTreeFlags::new_table(),
});
emit_schema_entry(
program,
resolver,
sqlite_schema_cursor_id,
None,
SchemaEntryType::Table,
&backing_table_name,
&backing_table_name,
table_root_reg,
Some(sql.clone()),
)?;
let seq_btree = Arc::new(BTreeTable::from_sql(&sql, 0)?);
let seq_cursor_id = program.alloc_cursor_id(CursorType::BTreeTable(seq_btree));
program.emit_insn(Insn::OpenWrite {
cursor_id: seq_cursor_id,
root_page: RegisterOrLiteral::Register(table_root_reg),
db: database_id,
});
let base_reg = program.alloc_registers(7);
program.emit_insn(Insn::Integer {
dest: base_reg,
value: start,
});
program.emit_insn(Insn::Integer {
dest: base_reg + 1,
value: 0, });
program.emit_insn(Insn::Integer {
dest: base_reg + 2,
value: start,
});
program.emit_insn(Insn::Integer {
dest: base_reg + 3,
value: increment,
});
program.emit_insn(Insn::Integer {
dest: base_reg + 4,
value: min_value,
});
program.emit_insn(Insn::Integer {
dest: base_reg + 5,
value: max_value,
});
program.emit_insn(Insn::Integer {
dest: base_reg + 6,
value: if cycle { 1 } else { 0 },
});
let record_reg = program.alloc_register();
program.emit_insn(Insn::MakeRecord {
start_reg: to_u16(base_reg),
count: 7,
dest_reg: to_u16(record_reg),
index_name: None,
affinity_str: None,
});
program.emit_insn(Insn::Insert {
cursor: seq_cursor_id,
key_reg: base_reg, record_reg,
flag: InsertFlags::new().require_seek().skip_all_change_counts(),
table_name: seq_name.to_string(),
});
program.emit_insn(Insn::Close {
cursor_id: seq_cursor_id,
});
let escaped = escape_sql_string_literal(&backing_table_name);
program.emit_insn(Insn::ParseSchema {
db: database_id,
where_clause: Some(format!("name = '{escaped}'")),
});
Ok(())
}
pub fn emit_disk_read_nextval(
program: &mut ProgramBuilder,
resolver: &Resolver,
database_id: usize,
seq_name: &str,
seq: &Sequence,
target_register: usize,
seq_name_reg: Option<usize>,
) -> Result<()> {
let backing_table_name = sequence_backing_table_name(seq_name);
let backing_table = resolver
.with_schema(database_id, |s| s.get_btree_table(&backing_table_name))
.ok_or_else(|| {
crate::LimboError::InternalError(format!(
"missing backing table for sequence \"{seq_name}\""
))
})?;
let root_page = backing_table.root_page;
let cursor_id = program.alloc_cursor_id(CursorType::BTreeTable(backing_table));
let seq_name_reg =
seq_name_reg.unwrap_or_else(|| program.emit_string8_new_reg(seq_name.to_string()));
let path_kind_reg = program.alloc_register();
let saved_outer_reg = program.alloc_register();
let status_reg = program.alloc_register();
let retry_top_label = program.allocate_label();
program.preassign_label_to_next_insn(retry_top_label);
program.emit_insn(Insn::SequenceBeginInnerTx {
db: database_id,
path_kind_reg,
saved_outer_reg,
});
program.emit_insn(Insn::OpenWrite {
cursor_id,
root_page: RegisterOrLiteral::Literal(root_page),
db: database_id,
});
let col_base = program.alloc_registers(7);
let was_empty_reg = program.alloc_register();
let have_row_label = program.allocate_label();
let empty_label = program.allocate_label();
program.emit_insn(Insn::Integer {
dest: was_empty_reg,
value: 0,
});
if seq.increment_by >= 0 {
program.emit_insn(Insn::Last {
cursor_id,
pc_if_empty: empty_label,
});
} else {
program.emit_insn(Insn::Rewind {
cursor_id,
pc_if_empty: empty_label,
});
}
program.emit_column_or_rowid(cursor_id, 0, col_base);
program.emit_column_or_rowid(cursor_id, 1, col_base + 1);
program.emit_insn(Insn::Goto {
target_pc: have_row_label,
});
program.preassign_label_to_next_insn(empty_label);
program.emit_insn(Insn::Integer {
dest: was_empty_reg,
value: 1,
});
program.preassign_label_to_next_insn(have_row_label);
program.emit_insn(Insn::SequenceComputeNext {
db: database_id,
seq_name_reg,
in_value_reg: col_base,
in_is_called_reg: col_base + 1,
was_empty_reg,
out_value_reg: target_register,
});
program.emit_insn(Insn::Copy {
src_reg: target_register,
dst_reg: col_base,
extra_amount: 0,
});
program.emit_insn(Insn::Integer {
dest: col_base + 1,
value: 1,
});
emit_sequence_descriptor_literals(program, seq, col_base + 2);
let record_reg = program.alloc_register();
program.emit_insn(Insn::MakeRecord {
start_reg: to_u16(col_base),
count: 7,
dest_reg: to_u16(record_reg),
index_name: None,
affinity_str: None,
});
program.emit_insn(Insn::Insert {
cursor: cursor_id,
key_reg: target_register,
record_reg,
flag: InsertFlags::new().require_seek().skip_all_change_counts(),
table_name: seq_name.to_string(),
});
if seq.cycle {
emit_backing_table_compaction(program, cursor_id, seq_name, target_register);
} else {
let skip_compact_label = program.allocate_label();
program.emit_insn(Insn::If {
reg: path_kind_reg,
target_pc: skip_compact_label,
jump_if_null: false,
});
emit_backing_table_compaction(program, cursor_id, seq_name, target_register);
program.preassign_label_to_next_insn(skip_compact_label);
}
program.emit_insn(Insn::Close { cursor_id });
emit_autoincrement_sqlite_sequence_sync(
program,
resolver,
database_id,
seq_name,
target_register,
)?;
program.emit_insn(Insn::SequenceRegisterAllocation {
db: database_id,
seq_name_reg,
value_reg: target_register,
saved_outer_reg,
});
program.emit_insn(Insn::SequenceCommitInnerTx {
db: database_id,
path_kind_reg,
saved_outer_reg,
status_reg,
});
let after_retry_label = program.allocate_label();
program.emit_insn(Insn::IfNot {
reg: status_reg,
target_pc: after_retry_label,
jump_if_null: false,
});
program.emit_insn(Insn::Goto {
target_pc: retry_top_label,
});
program.preassign_label_to_next_insn(after_retry_label);
program.emit_insn(Insn::SequenceTrackAllocation {
db: database_id,
seq_name_reg,
value_reg: target_register,
});
program.emit_insn(Insn::SetSequenceCurrval {
seq_name_reg,
value_reg: target_register,
});
Ok(())
}
pub fn emit_disk_advance_past(
program: &mut ProgramBuilder,
resolver: &Resolver,
database_id: usize,
seq_name: &str,
seq: &Sequence,
value_reg: usize,
) -> Result<()> {
let backing_table_name = sequence_backing_table_name(seq_name);
let backing_table = resolver
.with_schema(database_id, |s| s.get_btree_table(&backing_table_name))
.ok_or_else(|| {
crate::LimboError::InternalError(format!(
"missing backing table for sequence \"{seq_name}\""
))
})?;
let root_page = backing_table.root_page;
let cursor_id = program.alloc_cursor_id(CursorType::BTreeTable(backing_table));
let path_kind_reg = program.alloc_register();
let saved_outer_reg = program.alloc_register();
let status_reg = program.alloc_register();
let retry_top_label = program.allocate_label();
program.preassign_label_to_next_insn(retry_top_label);
program.emit_insn(Insn::SequenceBeginInnerTx {
db: database_id,
path_kind_reg,
saved_outer_reg,
});
program.emit_insn(Insn::OpenWrite {
cursor_id,
root_page: RegisterOrLiteral::Literal(root_page),
db: database_id,
});
let done_seek_label = program.allocate_label();
let do_advance_label = program.allocate_label();
let col_value_reg = program.alloc_register();
if seq.increment_by >= 0 {
program.emit_insn(Insn::Last {
cursor_id,
pc_if_empty: do_advance_label,
});
} else {
program.emit_insn(Insn::Rewind {
cursor_id,
pc_if_empty: do_advance_label,
});
}
program.emit_column_or_rowid(cursor_id, 0, col_value_reg);
if seq.increment_by >= 0 {
program.emit_insn(Insn::Le {
lhs: value_reg,
rhs: col_value_reg,
target_pc: done_seek_label,
flags: CmpInsFlags::default(),
collation: program.curr_collation(),
});
} else {
program.emit_insn(Insn::Ge {
lhs: value_reg,
rhs: col_value_reg,
target_pc: done_seek_label,
flags: CmpInsFlags::default(),
collation: program.curr_collation(),
});
}
program.preassign_label_to_next_insn(do_advance_label);
let col_base = program.alloc_registers(7);
program.emit_insn(Insn::Copy {
src_reg: value_reg,
dst_reg: col_base,
extra_amount: 0,
});
program.emit_insn(Insn::Integer {
dest: col_base + 1,
value: 1,
});
emit_sequence_descriptor_literals(program, seq, col_base + 2);
let record_reg = program.alloc_register();
program.emit_insn(Insn::MakeRecord {
start_reg: to_u16(col_base),
count: 7,
dest_reg: to_u16(record_reg),
index_name: None,
affinity_str: None,
});
program.emit_insn(Insn::Insert {
cursor: cursor_id,
key_reg: value_reg,
record_reg,
flag: InsertFlags::new().require_seek().skip_all_change_counts(),
table_name: seq_name.to_string(),
});
let seq_name_reg = program.emit_string8_new_reg(seq_name.to_string());
program.emit_insn(Insn::SetSequenceCurrval {
seq_name_reg,
value_reg,
});
if seq.cycle {
emit_backing_table_compaction(program, cursor_id, seq_name, value_reg);
} else {
let skip_compact_label = program.allocate_label();
program.emit_insn(Insn::If {
reg: path_kind_reg,
target_pc: skip_compact_label,
jump_if_null: false,
});
emit_backing_table_compaction(program, cursor_id, seq_name, value_reg);
program.preassign_label_to_next_insn(skip_compact_label);
}
emit_autoincrement_sqlite_sequence_sync(program, resolver, database_id, seq_name, value_reg)?;
program.preassign_label_to_next_insn(done_seek_label);
program.emit_insn(Insn::Close { cursor_id });
program.emit_insn(Insn::SequenceCommitInnerTx {
db: database_id,
path_kind_reg,
saved_outer_reg,
status_reg,
});
let after_retry_label = program.allocate_label();
program.emit_insn(Insn::IfNot {
reg: status_reg,
target_pc: after_retry_label,
jump_if_null: false,
});
program.emit_insn(Insn::Goto {
target_pc: retry_top_label,
});
program.preassign_label_to_next_insn(after_retry_label);
Ok(())
}
pub(crate) fn emit_backing_table_compaction(
program: &mut ProgramBuilder,
cursor_id: usize,
seq_name: &str,
keep_value_reg: usize,
) {
let skip_label = program.allocate_label();
let loop_top_label = program.allocate_label();
let skip_delete_label = program.allocate_label();
program.emit_insn(Insn::Rewind {
cursor_id,
pc_if_empty: skip_label,
});
program.preassign_label_to_next_insn(loop_top_label);
let row_value_reg = program.alloc_register();
program.emit_column_or_rowid(cursor_id, 0, row_value_reg);
program.emit_insn(Insn::Eq {
lhs: row_value_reg,
rhs: keep_value_reg,
target_pc: skip_delete_label,
flags: CmpInsFlags::default(),
collation: program.curr_collation(),
});
program.emit_insn(Insn::Delete {
cursor_id,
table_name: seq_name.to_string(),
is_part_of_update: true,
});
program.preassign_label_to_next_insn(skip_delete_label);
program.emit_insn(Insn::Next {
cursor_id,
pc_if_next: loop_top_label,
});
program.preassign_label_to_next_insn(skip_label);
}
pub(crate) fn emit_sqlite_sequence_sync(
program: &mut ProgramBuilder,
resolver: &Resolver,
database_id: usize,
autoinc_table_name: &str,
value_reg: usize,
) -> Result<bool> {
let Some(sseq_table) = resolver.with_schema(database_id, |s| {
s.get_btree_table(SQLITE_SEQUENCE_TABLE_NAME)
}) else {
return Ok(false);
};
let sseq_root = sseq_table.root_page;
let sseq_cursor = program.alloc_cursor_id(CursorType::BTreeTable(sseq_table));
program.emit_insn(Insn::OpenWrite {
cursor_id: sseq_cursor,
root_page: RegisterOrLiteral::Literal(sseq_root),
db: database_id,
});
let name_reg = program.emit_string8_new_reg(autoinc_table_name.to_string());
let insert_label = program.allocate_label();
let loop_top_label = program.allocate_label();
let skip_delete_label = program.allocate_label();
program.emit_insn(Insn::Rewind {
cursor_id: sseq_cursor,
pc_if_empty: insert_label,
});
program.preassign_label_to_next_insn(loop_top_label);
let col_name_reg = program.alloc_register();
program.emit_column_or_rowid(sseq_cursor, 0, col_name_reg);
program.emit_insn(Insn::Ne {
lhs: col_name_reg,
rhs: name_reg,
target_pc: skip_delete_label,
flags: CmpInsFlags::default(),
collation: program.curr_collation(),
});
program.emit_insn(Insn::Delete {
cursor_id: sseq_cursor,
table_name: SQLITE_SEQUENCE_TABLE_NAME.to_string(),
is_part_of_update: true,
});
program.preassign_label_to_next_insn(skip_delete_label);
program.emit_insn(Insn::Next {
cursor_id: sseq_cursor,
pc_if_next: loop_top_label,
});
program.preassign_label_to_next_insn(insert_label);
let col_base = program.alloc_registers(2);
program.emit_insn(Insn::Copy {
src_reg: name_reg,
dst_reg: col_base,
extra_amount: 0,
});
program.emit_insn(Insn::Copy {
src_reg: value_reg,
dst_reg: col_base + 1,
extra_amount: 0,
});
let record_reg = program.alloc_register();
program.emit_insn(Insn::MakeRecord {
start_reg: to_u16(col_base),
count: to_u16(2),
dest_reg: to_u16(record_reg),
index_name: None,
affinity_str: None,
});
let rowid_reg = program.alloc_register();
program.emit_insn(Insn::NewRowid {
cursor: sseq_cursor,
rowid_reg,
prev_largest_reg: 0,
});
program.emit_insn(Insn::Insert {
cursor: sseq_cursor,
key_reg: rowid_reg,
record_reg,
flag: InsertFlags::new().require_seek().skip_all_change_counts(),
table_name: SQLITE_SEQUENCE_TABLE_NAME.to_string(),
});
program.emit_insn(Insn::Close {
cursor_id: sseq_cursor,
});
Ok(true)
}
pub(crate) fn emit_autoincrement_sqlite_sequence_sync(
program: &mut ProgramBuilder,
resolver: &Resolver,
database_id: usize,
seq_name: &str,
value_reg: usize,
) -> Result<()> {
let Some(table_name) = seq_name.strip_prefix(AUTOINCREMENT_SEQ_PREFIX) else {
return Ok(());
};
emit_sqlite_sequence_sync(program, resolver, database_id, table_name, value_reg)?;
Ok(())
}
pub(crate) fn emit_sequence_descriptor_literals(
program: &mut ProgramBuilder,
seq: &Sequence,
dest_base: usize,
) {
program.emit_insn(Insn::Integer {
dest: dest_base,
value: seq.start_value,
});
program.emit_insn(Insn::Integer {
dest: dest_base + 1,
value: seq.increment_by,
});
program.emit_insn(Insn::Integer {
dest: dest_base + 2,
value: seq.min_value,
});
program.emit_insn(Insn::Integer {
dest: dest_base + 3,
value: seq.max_value,
});
program.emit_insn(Insn::Integer {
dest: dest_base + 4,
value: if seq.cycle { 1 } else { 0 },
});
}
#[allow(clippy::too_many_arguments)]
pub fn translate_create_sequence(
seq_name: &ast::QualifiedName,
if_not_exists: bool,
start: &Option<i64>,
increment: &Option<i64>,
min_value: &Option<i64>,
max_value: &Option<i64>,
cycle: bool,
resolver: &Resolver,
program: &mut ProgramBuilder,
) -> Result<()> {
let database_id = resolver.resolve_database_id(seq_name)?;
let schema_cookie = resolver.with_schema(database_id, |s| s.schema_version);
program.begin_write_on_database(database_id, schema_cookie)?;
let normalized_name = normalize_ident(seq_name.name.as_str());
if normalized_name.starts_with(AUTOINCREMENT_SEQ_PREFIX) {
bail_parse_error!(
"sequence name \"{}\" is reserved for internal AUTOINCREMENT use",
normalized_name
);
}
let exists = resolver.with_schema(database_id, |s| s.get_sequence(&normalized_name).is_some());
if exists {
if if_not_exists {
return Ok(());
}
bail_parse_error!("sequence \"{}\" already exists", normalized_name);
}
let seq = Sequence::new(
normalized_name.clone(),
*start,
*increment,
*min_value,
*max_value,
cycle,
)?;
let table = resolver.with_schema(database_id, |s| s.get_btree_table(SQLITE_TABLEID).unwrap());
let sqlite_schema_cursor_id = program.alloc_cursor_id(CursorType::BTreeTable(table));
program.emit_insn(Insn::OpenWrite {
cursor_id: sqlite_schema_cursor_id,
root_page: 1i64.into(),
db: database_id,
});
emit_sequence_backing_table(
program,
resolver,
database_id,
sqlite_schema_cursor_id,
&normalized_name,
seq.start_value,
seq.increment_by,
seq.min_value,
seq.max_value,
seq.cycle,
)?;
program.emit_insn(Insn::AddSequence {
db: database_id,
name: normalized_name,
start: seq.start_value,
increment: seq.increment_by,
min_value: seq.min_value,
max_value: seq.max_value,
cycle: seq.cycle,
});
program.emit_insn(Insn::SetCookie {
db: database_id,
cookie: Cookie::SchemaVersion,
value: schema_cookie as i32 + 1,
p5: 0,
});
Ok(())
}
pub(crate) fn emit_drop_sequence_cleanup(
program: &mut ProgramBuilder,
resolver: &Resolver,
database_id: usize,
seq_name: &str,
) -> Result<bool> {
let backing_table_name = sequence_backing_table_name(seq_name);
let root_page = resolver.with_schema(database_id, |s| {
s.get_sequence(seq_name)?;
Some(s.get_btree_table(&backing_table_name)?.root_page)
});
let Some(root_page) = root_page else {
return Ok(false);
};
let schema_table =
resolver.with_schema(database_id, |s| s.get_btree_table(SQLITE_TABLEID).unwrap());
let sqlite_schema_cursor_id = program.alloc_cursor_id(CursorType::BTreeTable(schema_table));
program.emit_insn(Insn::OpenWrite {
cursor_id: sqlite_schema_cursor_id,
root_page: 1i64.into(),
db: database_id,
});
let seq_name_reg = program.alloc_register();
let type_str_reg = program.alloc_register();
program.emit_insn(Insn::String8 {
dest: seq_name_reg,
value: backing_table_name,
});
program.emit_insn(Insn::String8 {
dest: type_str_reg,
value: "table".to_string(),
});
let end_loop_label = program.allocate_label();
let loop_start_label = program.allocate_label();
program.emit_insn(Insn::Rewind {
cursor_id: sqlite_schema_cursor_id,
pc_if_empty: end_loop_label,
});
program.preassign_label_to_next_insn(loop_start_label);
let col0_reg = program.alloc_register();
let col1_reg = program.alloc_register();
program.emit_column_or_rowid(sqlite_schema_cursor_id, 0, col0_reg);
program.emit_column_or_rowid(sqlite_schema_cursor_id, 1, col1_reg);
let skip_delete_label = program.allocate_label();
program.emit_insn(Insn::Ne {
lhs: col0_reg,
rhs: type_str_reg,
target_pc: skip_delete_label,
flags: CmpInsFlags::default(),
collation: program.curr_collation(),
});
program.emit_insn(Insn::Ne {
lhs: col1_reg,
rhs: seq_name_reg,
target_pc: skip_delete_label,
flags: CmpInsFlags::default(),
collation: program.curr_collation(),
});
program.emit_insn(Insn::Delete {
cursor_id: sqlite_schema_cursor_id,
table_name: "sqlite_schema".to_string(),
is_part_of_update: false,
});
program.preassign_label_to_next_insn(skip_delete_label);
program.emit_insn(Insn::Next {
cursor_id: sqlite_schema_cursor_id,
pc_if_next: loop_start_label,
});
program.preassign_label_to_next_insn(end_loop_label);
let former_root_reg = program.alloc_register();
program.emit_insn(Insn::Destroy {
db: database_id,
root: root_page,
former_root_reg,
is_temp: 0,
});
program.emit_insn(Insn::DropSequence {
db: database_id,
seq_name: seq_name.to_string(),
});
Ok(true)
}
pub fn translate_drop_sequence(
seq_name: &ast::QualifiedName,
if_exists: bool,
resolver: &Resolver,
program: &mut ProgramBuilder,
) -> Result<()> {
let database_id = resolver.resolve_database_id(seq_name)?;
let schema_cookie = resolver.with_schema(database_id, |s| s.schema_version);
program.begin_write_on_database(database_id, schema_cookie)?;
let normalized_name = normalize_ident(seq_name.name.as_str());
let dropped = emit_drop_sequence_cleanup(program, resolver, database_id, &normalized_name)?;
if !dropped {
if if_exists {
return Ok(());
}
bail_parse_error!("sequence \"{}\" does not exist", normalized_name);
}
program.emit_insn(Insn::SetCookie {
db: database_id,
cookie: Cookie::SchemaVersion,
value: schema_cookie as i32 + 1,
p5: 0,
});
Ok(())
}