modify loop functions to accomodate for ephemeral tables

This commit is contained in:
pedrocarlo
2025-06-14 14:55:05 -03:00
parent eda9d20a0b
commit 9048ad398b
6 changed files with 380 additions and 153 deletions

View File

@@ -22,14 +22,14 @@ use super::select::emit_simple_count;
use super::subquery::emit_subqueries;
use crate::error::SQLITE_CONSTRAINT_PRIMARYKEY;
use crate::function::Func;
use crate::schema::{BTreeTable, Column, Index, IndexColumn, Schema, Table, Type};
use crate::schema::{Index, IndexColumn, Schema};
use crate::translate::main_loop::EphemeralCtx;
use crate::translate::compound_select::emit_program_for_compound_select;
use crate::translate::plan::{DeletePlan, Plan, Search};
use crate::translate::values::emit_values;
use crate::util::exprs_are_equivalent;
use crate::vdbe::builder::{CursorKey, CursorType, ProgramBuilder};
use crate::vdbe::insn::{CmpInsFlags, IdxInsertFlags, InsertFlags, RegisterOrLiteral};
use crate::vdbe::CursorID;
use crate::vdbe::{insn::Insn, BranchOffset};
use crate::{Result, SymbolTable};
@@ -335,6 +335,7 @@ pub fn emit_query<'a>(
plan.group_by.as_ref(),
OperationMode::SELECT,
&plan.where_clause,
None,
)?;
if plan.is_simple_count() {
@@ -438,6 +439,7 @@ fn emit_program_for_delete(
None,
OperationMode::DELETE,
&plan.where_clause,
None,
)?;
// Set up main query execution loop
@@ -613,28 +615,56 @@ fn emit_program_for_update(
});
}
let temp_cursor_id = {
// Sqlite determines we should create an ephemeral table if we do not have a FROM clause
// Difficult to say what items from the plan can be checked for this so currently just checking the where clause
// https://github.com/sqlite/sqlite/blob/master/src/update.c#L395
// https://github.com/sqlite/sqlite/blob/master/src/update.c#L670
if !plan.where_clause.is_empty() {
None
} else {
let table_ref = plan
.table_references
.joined_tables()
.first()
.expect("at least one table needs to be referenced for UPDATE");
let columns = table_ref.columns();
let table_ref = plan
.table_references
.joined_tables()
.first()
.expect("at least one table needs to be referenced for UPDATE");
let rowid_alias_used = plan.set_clauses.iter().fold(false, |accum, (idx, _)| {
accum || columns[*idx].is_rowid_alias
});
let mut ephemeral_ctx = (plan.rowid_alias_used
&& !matches!(table_ref.op, Operation::Search(Search::RowidEq { .. })))
.then(|| EphemeralCtx::from_table(program, &table_ref));
rowid_alias_used.then(|| emit_ephemeral(program, &table_ref.table))
}
};
if let Some(ephemeral_ctx) = &mut ephemeral_ctx {
let mut t_ctx = TranslateCtx::new(
program,
schema,
syms,
plan.table_references.joined_tables().len(),
plan.returning.as_ref().map_or(0, |r| r.len()),
);
init_loop(
program,
&mut t_ctx,
&plan.table_references,
&mut [],
None,
OperationMode::UPDATE,
Some(ephemeral_ctx),
)?;
open_loop(
program,
&mut t_ctx,
&plan.table_references,
&[JoinOrderMember::default()],
&mut plan.where_clause,
Some(ephemeral_ctx),
)?;
emit_ephemeral_insert(program, ephemeral_ctx)?;
close_loop(
program,
&mut t_ctx,
&plan.table_references,
&[JoinOrderMember::default()],
Some(ephemeral_ctx),
)?;
ephemeral_ctx.finished_insert_loop = true;
}
init_loop(
program,
@@ -644,6 +674,7 @@ fn emit_program_for_update(
None,
OperationMode::UPDATE,
&plan.where_clause,
ephemeral_ctx.as_ref(),
)?;
// Open indexes for update.
let mut index_cursors = Vec::with_capacity(plan.indexes_to_update.len());
@@ -670,24 +701,72 @@ fn emit_program_for_update(
let record_reg = program.alloc_register();
index_cursors.push((index_cursor, record_reg));
}
open_loop(
program,
&mut t_ctx,
&plan.table_references,
&[JoinOrderMember::default()],
&mut plan.where_clause,
temp_cursor_id,
)?;
emit_update_insns(&plan, &t_ctx, program, index_cursors, temp_cursor_id)?;
close_loop(
if ephemeral_ctx.is_none() {
open_loop(
program,
&mut t_ctx,
&plan.table_references,
&[JoinOrderMember::default()],
&mut plan.where_clause,
None,
)?;
} else {
let ctx = ephemeral_ctx.as_ref().unwrap();
let LoopLabels {
loop_start,
loop_end,
..
} = t_ctx
.labels_main_loop
.first()
.expect("table has no loop labels");
if !matches!(ctx.table.op, Operation::Search(Search::RowidEq { .. })) {
program.emit_insn(Insn::Rewind {
cursor_id: ctx.temp_cursor_id,
pc_if_empty: *loop_end,
});
}
program.preassign_label_to_next_insn(*loop_start);
}
emit_update_insns(
&plan,
&t_ctx,
program,
&mut t_ctx,
&plan.table_references,
&[JoinOrderMember::default()],
temp_cursor_id,
index_cursors,
ephemeral_ctx.as_ref(),
)?;
if ephemeral_ctx.is_none() {
close_loop(
program,
&mut t_ctx,
&plan.table_references,
&[JoinOrderMember::default()],
None,
)?;
} else {
let ctx = ephemeral_ctx.as_ref().unwrap();
let LoopLabels {
loop_start,
loop_end,
next,
..
} = t_ctx
.labels_main_loop
.first()
.expect("table has no loop labels");
program.preassign_label_to_next_insn(*next);
if !matches!(ctx.table.op, Operation::Search(Search::RowidEq { .. })) {
program.emit_insn(Insn::Next {
cursor_id: ephemeral_ctx.as_ref().unwrap().temp_cursor_id,
pc_if_next: *loop_start,
});
}
program.preassign_label_to_next_insn(*loop_end);
}
program.preassign_label_to_next_insn(after_main_loop_label);
after(program);
@@ -705,7 +784,7 @@ fn emit_update_insns(
t_ctx: &TranslateCtx,
program: &mut ProgramBuilder,
index_cursors: Vec<(usize, usize)>,
temp_cursor_id: Option<CursorID>,
ephemeral_ctx: Option<&EphemeralCtx>,
) -> crate::Result<()> {
let table_ref = plan.table_references.joined_tables().first().unwrap();
let loop_labels = t_ctx.labels_main_loop.first().unwrap();
@@ -736,6 +815,28 @@ fn emit_update_insns(
},
};
if ephemeral_ctx.is_none() {
for cond in plan
.where_clause
.iter()
.filter(|c| c.should_eval_before_loop(&[JoinOrderMember::default()]))
{
let jump_target = program.allocate_label();
let meta = ConditionMetadata {
jump_if_condition_is_true: false,
jump_target_when_true: jump_target,
jump_target_when_false: t_ctx.label_main_loop_end.unwrap(),
};
translate_condition_expr(
program,
&plan.table_references,
&cond.expr,
meta,
&t_ctx.resolver,
)?;
program.preassign_label_to_next_insn(jump_target);
}
}
let beg = program.alloc_registers(
table_ref.table.columns().len()
+ if is_virtual {
@@ -745,7 +846,7 @@ fn emit_update_insns(
},
);
program.emit_insn(Insn::RowId {
cursor_id: temp_cursor_id.unwrap_or(cursor_id),
cursor_id: ephemeral_ctx.map_or(cursor_id, |ctx| ctx.temp_cursor_id),
dest: beg,
});
@@ -802,6 +903,29 @@ fn emit_update_insns(
});
}
if ephemeral_ctx.is_none() {
for cond in plan
.where_clause
.iter()
.filter(|c| c.should_eval_before_loop(&[JoinOrderMember::default()]))
{
let jump_target = program.allocate_label();
let meta = ConditionMetadata {
jump_if_condition_is_true: false,
jump_target_when_true: jump_target,
jump_target_when_false: loop_labels.next,
};
translate_condition_expr(
program,
&plan.table_references,
&cond.expr,
meta,
&t_ctx.resolver,
)?;
program.preassign_label_to_next_insn(jump_target);
}
}
// we scan a column at a time, loading either the column's values, or the new value
// from the Set expression, into registers so we can emit a MakeRecord and update the row.
let start = if is_virtual { beg + 2 } else { beg + 1 };
@@ -1140,75 +1264,17 @@ fn init_limit(
}
/// Emits an ephemeral table that reads the rowids from `table`
fn emit_ephemeral(program: &mut ProgramBuilder, table: &Table) -> CursorID {
let cursor_type = CursorType::BTreeTable(table.btree().unwrap());
let cursor_id = program.alloc_cursor_id(cursor_type);
let simple_table_rc = Rc::new(BTreeTable {
root_page: 0, // Not relevant for ephemeral table definition
name: "ephemeral_scratch".to_string(),
has_rowid: true,
primary_key_columns: vec![],
columns: vec![Column {
name: Some("rowid".to_string()),
ty: Type::Integer,
ty_str: "INTEGER".to_string(),
primary_key: false,
is_rowid_alias: false,
notnull: false,
default: None,
unique: false,
collation: None,
}],
is_strict: false,
unique_sets: None,
});
let temp_cursor_id = program.alloc_cursor_id(CursorType::BTreeTable(simple_table_rc));
let null_data_reg = program.alloc_register();
let rowid_reg = program.alloc_register();
program.emit_insn(Insn::Null {
dest: null_data_reg,
dest_end: Some(rowid_reg),
});
program.emit_insn(Insn::OpenEphemeral {
cursor_id: temp_cursor_id,
is_table: true,
});
program.emit_insn(Insn::OpenRead {
cursor_id,
root_page: table.get_root_page(),
});
let loop_labels = LoopLabels::new(program);
program.emit_insn(Insn::Rewind {
cursor_id,
pc_if_empty: loop_labels.loop_end,
});
program.preassign_label_to_next_insn(loop_labels.loop_start);
fn emit_ephemeral_insert(program: &mut ProgramBuilder, ctx: &EphemeralCtx) -> Result<()> {
program.emit_insn(Insn::RowId {
cursor_id,
dest: rowid_reg,
cursor_id: ctx.table_cursor_id,
dest: ctx.rowid_reg,
});
program.emit_insn(Insn::Insert {
cursor: temp_cursor_id,
key_reg: rowid_reg,
record_reg: null_data_reg,
cursor: ctx.temp_cursor_id,
key_reg: ctx.rowid_reg,
record_reg: ctx.null_data_reg,
flag: InsertFlags(0), // TODO: when we use the flags see if this needs to change
table_name: table.get_name().to_string(),
table_name: ctx.table.table.get_name().to_string(),
});
program.preassign_label_to_next_insn(loop_labels.next);
program.emit_insn(Insn::Next {
cursor_id,
pc_if_next: loop_labels.loop_start,
});
program.preassign_label_to_next_insn(loop_labels.loop_end);
temp_cursor_id
Ok(())
}

View File

@@ -1,12 +1,12 @@
use limbo_ext::VTabKind;
use limbo_sqlite3_parser::ast::{self, SortOrder};
use std::sync::Arc;
use std::{rc::Rc, sync::Arc};
use crate::{
schema::{Affinity, Index, IndexColumn, Table},
schema::{Affinity, BTreeTable, Column, Index, IndexColumn, Table, Type},
translate::{
plan::{DistinctCtx, Distinctness},
plan::{DistinctCtx, Distinctness, JoinedTable},
result_row::emit_select_result,
},
types::SeekOp,
@@ -103,6 +103,56 @@ pub fn init_distinct(program: &mut ProgramBuilder, plan: &SelectPlan) -> Distinc
return ctx;
}
pub struct EphemeralCtx<'a> {
pub temp_cursor_id: CursorID,
pub table_cursor_id: CursorID,
pub table: &'a JoinedTable,
pub null_data_reg: usize,
pub rowid_reg: usize,
/// Indicates whether we closed the insert loop
pub finished_insert_loop: bool,
}
impl<'a> EphemeralCtx<'a> {
/// Creates an [EphemeralCtx] a Btree `table`
pub fn from_table(program: &mut ProgramBuilder, table: &'a JoinedTable) -> Self {
let cursor_type = CursorType::BTreeTable(table.table.btree().unwrap());
let cursor_id =
program.alloc_cursor_id_keyed(CursorKey::table(table.internal_id), cursor_type);
let simple_table_rc = Rc::new(BTreeTable {
root_page: 0, // Not relevant for ephemeral table definition
name: "ephemeral_scratch".to_string(),
has_rowid: true,
primary_key_columns: vec![],
columns: vec![Column {
name: Some("rowid".to_string()),
ty: Type::Integer,
ty_str: "INTEGER".to_string(),
primary_key: false,
is_rowid_alias: false,
notnull: false,
default: None,
unique: false,
collation: None,
}],
is_strict: false,
unique_sets: None,
});
let temp_cursor_id = program.alloc_cursor_id(CursorType::BTreeTable(simple_table_rc));
Self {
temp_cursor_id,
table_cursor_id: cursor_id,
table,
null_data_reg: program.alloc_register(),
rowid_reg: program.alloc_register(),
finished_insert_loop: false,
}
}
}
/// Initialize resources needed for the source operators (tables, joins, etc)
pub fn init_loop(
program: &mut ProgramBuilder,
@@ -111,7 +161,12 @@ pub fn init_loop(
aggregates: &mut [Aggregate],
group_by: Option<&GroupBy>,
mode: OperationMode,
<<<<<<< HEAD
where_clause: &[WhereTerm],
||||||| parent of 400ce819 (modify loop functions to accomodate for ephemeral tables)
=======
ephemeral_ctx: Option<&EphemeralCtx>,
>>>>>>> 400ce819 (modify loop functions to accomodate for ephemeral tables)
) -> Result<()> {
assert!(
t_ctx.meta_left_joins.len() == tables.joined_tables().len(),
@@ -172,7 +227,11 @@ pub fn init_loop(
t_ctx.meta_left_joins[table_index] = Some(lj_metadata);
}
}
let (table_cursor_id, index_cursor_id) = table.open_cursors(program, mode)?;
let (table_cursor_id, index_cursor_id) = if ephemeral_ctx.is_some() {
(None, None)
} else {
table.open_cursors(program, mode)?
};
match &table.op {
Operation::Scan { index, .. } => match (mode, &table.table) {
(OperationMode::SELECT, Table::BTree(btree)) => {
@@ -229,18 +288,41 @@ pub fn init_loop(
}
(OperationMode::UPDATE, Table::BTree(btree)) => {
let root_page = btree.root_page;
program.emit_insn(Insn::OpenWrite {
cursor_id: table_cursor_id
.expect("table cursor is always opened in OperationMode::UPDATE"),
root_page: root_page.into(),
name: btree.name.clone(),
});
if let Some(index_cursor_id) = index_cursor_id {
program.emit_insn(Insn::OpenWrite {
cursor_id: index_cursor_id,
root_page: index.as_ref().unwrap().root_page.into(),
name: index.as_ref().unwrap().name.clone(),
if let Some(ctx) = &ephemeral_ctx.filter(|ctx| !ctx.finished_insert_loop) {
program.emit_insn(Insn::Null {
dest: ctx.null_data_reg,
dest_end: Some(ctx.rowid_reg),
});
program.emit_insn(Insn::OpenEphemeral {
cursor_id: ctx.temp_cursor_id,
is_table: true,
});
program.emit_insn(Insn::OpenRead {
cursor_id: ctx.table_cursor_id,
root_page: ctx.table.table.get_root_page(),
});
} else {
program.emit_insn(Insn::OpenWrite {
cursor_id: ephemeral_ctx.map_or_else(
|| {
table_cursor_id.expect(
"table cursor is always opened in OperationMode::UPDATE",
)
},
|ctx| ctx.table_cursor_id,
),
root_page: root_page.into(),
name: btree.name.clone(),
});
if let Some(index_cursor_id) = index_cursor_id {
program.emit_insn(Insn::OpenWrite {
cursor_id: index_cursor_id,
root_page: index.as_ref().unwrap().root_page.into(),
name: index.as_ref().unwrap().name.clone(),
});
}
}
}
(_, Table::Virtual(_)) => {
@@ -261,12 +343,38 @@ pub fn init_loop(
}
}
OperationMode::DELETE | OperationMode::UPDATE => {
let table_cursor_id = table_cursor_id.expect("table cursor is always opened in OperationMode::DELETE or OperationMode::UPDATE");
program.emit_insn(Insn::OpenWrite {
cursor_id: table_cursor_id,
root_page: table.table.get_root_page().into(),
name: table.table.get_name().to_string(),
});
let table_cursor_id = ephemeral_ctx.map_or_else(
|| {
table_cursor_id.expect(
"table cursor is always opened in OperationMode::DELETE or OperationMode::UPDATE",
)
},
|ctx| ctx.table_cursor_id,
);
if let Some(ctx) = &ephemeral_ctx.filter(|ctx| !ctx.finished_insert_loop) {
program.emit_insn(Insn::Null {
dest: ctx.null_data_reg,
dest_end: Some(ctx.rowid_reg),
});
program.emit_insn(Insn::OpenEphemeral {
cursor_id: ctx.temp_cursor_id,
is_table: true,
});
program.emit_insn(Insn::OpenRead {
cursor_id: ctx.table_cursor_id,
root_page: ctx.table.table.get_root_page(),
});
} else {
program.emit_insn(Insn::OpenWrite {
cursor_id: table_cursor_id,
root_page: table.table.get_root_page().into(),
name: table.table.get_name().to_string(),
});
}
// For DELETE, we need to open all the indexes for writing
// UPDATE opens these in emit_program_for_update() separately
if mode == OperationMode::DELETE {
@@ -357,7 +465,7 @@ pub fn open_loop(
table_references: &TableReferences,
join_order: &[JoinOrderMember],
predicates: &[WhereTerm],
temp_cursor_id: Option<CursorID>,
ephemeral_ctx: Option<&EphemeralCtx>,
) -> Result<()> {
for (join_index, join) in join_order.iter().enumerate() {
let joined_table_index = join.original_idx;
@@ -390,12 +498,21 @@ pub fn open_loop(
Operation::Scan { iter_dir, .. } => {
match &table.table {
Table::BTree(_) => {
let iteration_cursor_id = temp_cursor_id.unwrap_or_else(|| {
index_cursor_id.unwrap_or_else(|| {
table_cursor_id
.expect("Either index or table cursor must be opened")
let iteration_cursor_id = ephemeral_ctx
.map(|ctx| {
if ctx.finished_insert_loop {
ctx.temp_cursor_id
} else {
ctx.table_cursor_id
}
})
});
.unwrap_or_else(|| {
index_cursor_id.unwrap_or_else(|| {
table_cursor_id.expect(
"Either ephemeral or index or table cursor must be opened",
)
})
});
if *iter_dir == IterationDirection::Backwards {
program.emit_insn(Insn::Last {
cursor_id: iteration_cursor_id,
@@ -641,9 +758,21 @@ pub fn open_loop(
};
let is_index = index_cursor_id.is_some();
let seek_cursor_id = index_cursor_id.unwrap_or_else(|| {
table_cursor_id.expect("Either index or table cursor must be opened")
});
let seek_cursor_id = ephemeral_ctx
.map(|ctx| {
if ctx.finished_insert_loop {
ctx.temp_cursor_id
} else {
ctx.table_cursor_id
}
})
.unwrap_or_else(|| {
index_cursor_id.unwrap_or_else(|| {
table_cursor_id.expect(
"Either ephemeral or index or table cursor must be opened",
)
})
});
let Search::Seek { seek_def, .. } = search else {
unreachable!("Rowid equality point lookup should have been handled above");
};
@@ -975,7 +1104,7 @@ pub fn close_loop(
t_ctx: &mut TranslateCtx,
tables: &TableReferences,
join_order: &[JoinOrderMember],
temp_cursor_id: Option<CursorID>,
ephemeral_ctx: Option<&EphemeralCtx>,
) -> Result<()> {
// We close the loops for all tables in reverse order, i.e. innermost first.
// OPEN t1
@@ -1000,12 +1129,21 @@ pub fn close_loop(
program.resolve_label(loop_labels.next, program.offset());
match &table.table {
Table::BTree(_) => {
let iteration_cursor_id = temp_cursor_id.unwrap_or_else(|| {
index_cursor_id.unwrap_or_else(|| {
table_cursor_id
.expect("Either index or table cursor must be opened")
let iteration_cursor_id = ephemeral_ctx
.map(|ctx| {
if ctx.finished_insert_loop {
ctx.temp_cursor_id
} else {
ctx.table_cursor_id
}
})
});
.unwrap_or_else(|| {
index_cursor_id.unwrap_or_else(|| {
table_cursor_id.expect(
"Either ephemeral or index or table cursor must be opened",
)
})
});
if *iter_dir == IterationDirection::Backwards {
program.emit_insn(Insn::Prev {
cursor_id: iteration_cursor_id,
@@ -1043,9 +1181,20 @@ pub fn close_loop(
"Subqueries do not support index seeks"
);
program.resolve_label(loop_labels.next, program.offset());
let iteration_cursor_id = index_cursor_id.unwrap_or_else(|| {
table_cursor_id.expect("Either index or table cursor must be opened")
});
let iteration_cursor_id = ephemeral_ctx
.map(|ctx| {
if ctx.finished_insert_loop {
ctx.temp_cursor_id
} else {
ctx.table_cursor_id
}
})
.unwrap_or_else(|| {
index_cursor_id.unwrap_or_else(|| {
table_cursor_id
.expect("Either ephemeral or index or table cursor must be opened")
})
});
// Rowid equality point lookups are handled with a SeekRowid instruction which does not loop, so there is no need to emit a Next instruction.
if !matches!(search, Search::RowidEq { .. }) {
let iter_dir = match search {

View File

@@ -107,7 +107,7 @@ fn optimize_delete_plan(plan: &mut DeletePlan, _schema: &Schema) -> Result<()> {
Ok(())
}
fn optimize_update_plan(plan: &mut UpdatePlan, _schema: &Schema) -> Result<()> {
fn optimize_update_plan(plan: &mut UpdatePlan, schema: &Schema) -> Result<()> {
rewrite_exprs_update(plan)?;
if let ConstantConditionEliminationResult::ImpossibleCondition =
eliminate_constant_conditions(&mut plan.where_clause)?
@@ -120,13 +120,13 @@ fn optimize_update_plan(plan: &mut UpdatePlan, _schema: &Schema) -> Result<()> {
// e.g. in 'explain update t set x=x+5 where x > 10;' where x is an indexed column,
// sqlite first creates an ephemeral index to store the current values so the tree traversal
// doesn't get messed up while updating.
// let _ = optimize_table_access(
// &mut plan.table_references,
// &schema.indexes,
// &mut plan.where_clause,
// &mut plan.order_by,
// &mut None,
// )?;
let _ = optimize_table_access(
&mut plan.table_references,
&schema.indexes,
&mut plan.where_clause,
&mut plan.order_by,
&mut None,
)?;
Ok(())
}

View File

@@ -534,6 +534,7 @@ pub struct UpdatePlan {
// whether the WHERE clause is always false
pub contains_constant_false_condition: bool,
pub indexes_to_update: Vec<Arc<Index>>,
pub rowid_alias_used: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]

View File

@@ -228,6 +228,16 @@ pub fn prepare_update_plan(
.cloned()
.collect();
// Sqlite determines we should create an ephemeral table if we do not have a FROM clause
// Difficult to say what items from the plan can be checked for this so currently just checking if a RowId Alias is referenced
// https://github.com/sqlite/sqlite/blob/master/src/update.c#L395
// https://github.com/sqlite/sqlite/blob/master/src/update.c#L670
let columns = table.columns();
let rowid_alias_used = set_clauses.iter().fold(false, |accum, (idx, _)| {
accum || columns[*idx].is_rowid_alias
});
Ok(Plan::Update(UpdatePlan {
table_references,
set_clauses,
@@ -238,5 +248,6 @@ pub fn prepare_update_plan(
offset,
contains_constant_false_condition: false,
indexes_to_update,
rowid_alias_used,
}))
}

View File

@@ -860,7 +860,7 @@ impl ImmutableRecord {
}
pub fn from_registers<'a>(
registers: impl IntoIterator<Item = &'a Register> + Clone,
registers: impl IntoIterator<Item = &'a Register> + Copy,
len: usize,
) -> Self {
let mut values = Vec::with_capacity(len);
@@ -870,7 +870,7 @@ impl ImmutableRecord {
let mut serial_type_buf = [0; 9];
// write serial types
for value in registers.clone() {
for value in registers {
let value = value.get_owned_value();
let serial_type = SerialType::from(value);
let n = write_varint(&mut serial_type_buf[0..], serial_type.into());