Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ All notable changes to sqlite-rs. Format follows [Keep a Changelog](https://keep

**Versioning policy:** one minor version per completed plan phase — the version number tells the plan's story, sub-steps stay inside a phase. V1 (READ CORE) = 0.1.0 through 0.4.0. *(History note: internal iterations briefly numbered 0.4.0–0.6.0 were renumbered into the phase scheme on 14 Aug 2026, before any tag or publication of those versions existed.)*

## [0.18.10] - 2026-08-30

### Fixed

- INSERT and UPDATE codegen re-`SeekRowid`'d back onto the row they had
just written, then re-read every index column via `Opcode::Column`/
`Opcode::Rowid` to build `IdxInsert` keys — even though those same
values were still sitting in `col_regs`/`rowid_reg` from just before
the write. `emit_index_key_ops_from_regs` builds the key via
`Opcode::Copy` from those registers instead, dropping the seek and
re-read on every indexed INSERT/UPDATE. `IdxDelete` paths (removing a
*different*, already-on-disk row's stale entries) are unaffected —
they have no such register run to reuse (#663).

## [0.18.9] - 2026-08-30

### Fixed
Expand All @@ -17,6 +31,15 @@ All notable changes to sqlite-rs. Format follows [Keep a Changelog](https://keep
to the index-cursor side. Measured ~13.8% faster on an indexed range
scan and ~7% faster on an indexed join (#661).

- `SorterInsert` always re-decoded its sort-key columns out of the
record blob it had just been handed, even though those same values
were still sitting in registers moments earlier, before `MakeRecord`
encoded them. It now gains an optional source-register run (honored
when `p5` is nonzero), which `compile_grouped_scan`'s `GROUP BY` path
opts into; every other `SorterInsert` emitter keeps the original
decode-from-blob behavior. Measured ~3.5-4% faster on `group_by_agg`
(#660).

## [0.18.8] - 2026-08-29

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "sqlite-rs"
version = "0.18.9"
version = "0.18.10"
edition = "2021"
publish = false
license = "Apache-2.0"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ See [.openspec/plan.md](.openspec/plan.md) for the full breakdown and [.openspec

## Status

**Version 0.18.9** — see [CHANGELOG.md](CHANGELOG.md). One minor version per completed plan phase.
**Version 0.18.10** — see [CHANGELOG.md](CHANGELOG.md). One minor version per completed plan phase.

| Phase | Version | Status |
|-------|---------|--------|
Expand Down
99 changes: 89 additions & 10 deletions src/codegen/index_maintenance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@
//! cursor, and emit the `IdxInsert`/`IdxDelete` pair for a row's index
//! entries.
//!
//! Index keys are read back from the table cursor's *current* row via
//! ordinary `Opcode::Column`/`Opcode::Rowid` (rowid last, matching the
//! on-disk index key convention `btree::index::insert`/`index::delete`
//! use) rather than reusing already-computed value registers — there is
//! no register-copy opcode in the frozen V2 set
//! (`tools/opcodes-v2.json`), so a fresh contiguous register run is
//! rebuilt by reading from a cursor every time one is needed. For a
//! freshly-inserted/updated row, the caller must position the table
//! cursor on it first (`SeekRowid`) since `Opcode::Insert` does not
//! reposition the cursor itself.
//! For a row whose values are only available from disk (the *old* row
//! of an `UPDATE`'s rebuild, or a row displaced by an `INSERT OR
//! REPLACE` conflict), index keys are read back from the table cursor's
//! *current* row via ordinary `Opcode::Column`/`Opcode::Rowid` (rowid
//! last, matching the on-disk index key convention
//! `btree::index::insert`/`index::delete` use) — see
//! [`emit_index_key_ops`]. For a row whose values are already sitting in
//! registers (a freshly-inserted/updated row, before it's written),
//! [`emit_index_key_ops_from_regs`] builds the same key layout via
//! `Opcode::Copy` from those registers instead, with no cursor re-seek
//! or re-read.
//!
//! `DESC` index columns are rejected (`CodegenError::Unsupported`)
//! rather than silently mis-keyed: no index b-tree comparator in this
Expand Down Expand Up @@ -146,3 +147,81 @@ pub(crate) fn emit_index_key_ops(
}
Ok(())
}

/// Like [`emit_index_key_ops`], but for a row whose column values are
/// already sitting in `col_regs` (one register per `schema.columns`
/// entry, in order — the same layout `INSERT`/`UPDATE` codegen builds
/// for `MakeRecord`) and whose rowid is already in `rowid_reg`. Builds
/// each index's key via `Opcode::Copy` from those registers into a
/// fresh contiguous run instead of `Opcode::Column`/`Opcode::Rowid`
/// against a cursor — so callers don't need to `SeekRowid` back onto
/// the row first. Always emits `IdxInsert`: the only caller that needs
/// `IdxDelete` (removing a *different*, already-on-disk row's stale
/// entries) has no such register run to reuse and stays on
/// [`emit_index_key_ops`].
pub(crate) fn emit_index_key_ops_from_regs(
em: &mut Emitter,
reg: &mut RegAlloc,
schema: &TableSchema,
col_regs: &[i32],
rowid_reg: i32,
first_index_cursor: i32,
) -> Result<(), CodegenError> {
for (i, index) in schema.indexes.iter().enumerate() {
let index_cursor = first_index_cursor.saturating_add(i32::try_from(i).unwrap_or(0));
let mut start = None;
for col in &index.columns {
if col.desc {
return Err(CodegenError::Unsupported {
reason: format!(
"index {} has a DESC column ({}); descending index keys aren't supported yet",
index.name, col.name
),
});
}
let col_idx =
column_index(schema, &col.name).ok_or_else(|| CodegenError::Unsupported {
reason: format!(
"index {} references a column or expression this codegen can't resolve: {}",
index.name, col.name
),
})?;
// The rowid-alias column's own register holds NULL (readers
// substitute the cursor's actual rowid instead — see
// `emit_column_read`), so its live value is `rowid_reg`, not
// `col_regs[col_idx]`.
let src = if Some(col_idx) == schema.rowid_alias {
rowid_reg
} else {
*col_regs
.get(col_idx)
.ok_or_else(|| CodegenError::Unsupported {
reason: format!(
"index {} references column {} outside the row's register run",
index.name, col.name
),
})?
};
let r = reg.alloc();
if start.is_none() {
start = Some(r);
}
em.emit(Instruction::new(Opcode::Copy, src, r, 0));
}
let key_rowid_reg = reg.alloc();
if start.is_none() {
start = Some(key_rowid_reg);
}
em.emit(Instruction::new(Opcode::Copy, rowid_reg, key_rowid_reg, 0));

let count = i32::try_from(index.columns.len().saturating_add(1)).unwrap_or(0);
em.emit(Instruction::with_p4(
Opcode::IdxInsert,
index_cursor,
start.unwrap_or(key_rowid_reg),
0,
P4::Int(i64::from(count)),
));
}
Ok(())
}
29 changes: 5 additions & 24 deletions src/codegen/stmt/insert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ use std::sync::{Arc, Mutex, OnceLock};

use crate::codegen::expr::{column_index, compile_cond, compile_value};
use crate::codegen::index_maintenance::{
emit_index_key_ops, open_index_cursors, valid_table_root_page,
emit_index_key_ops, emit_index_key_ops_from_regs, open_index_cursors, valid_table_root_page,
};
use crate::codegen::select::{
compile_select_joined_scan, compile_select_scan, select_result_column_count,
Expand Down Expand Up @@ -794,29 +794,10 @@ fn compile_row(
));

if !schema.indexes.is_empty() {
// `Insert` doesn't reposition `TABLE_CURSOR` onto the row it
// just wrote, but the index-key registers are read back via
// `Opcode::Column`/`Opcode::Rowid` against the cursor's current
// row (see `index_maintenance`), so seek onto it first. A
// not-found jump target is required by `SeekRowid`'s shape but
// should be unreachable — the row was just inserted.
let seek_ok = em.new_label();
let seek_addr = em.emit(Instruction::new(
Opcode::SeekRowid,
TABLE_CURSOR,
0,
rowid_reg,
));
em.patch_p2(seek_addr, seek_ok);
emit_index_key_ops(
em,
reg,
schema,
TABLE_CURSOR,
FIRST_INDEX_CURSOR,
Opcode::IdxInsert,
)?;
em.place(seek_ok);
// The new row's values are already sitting in `col_regs`/
// `rowid_reg` — build index keys from those directly instead of
// seeking `TABLE_CURSOR` back onto the just-written row.
emit_index_key_ops_from_regs(em, reg, schema, &col_regs, rowid_reg, FIRST_INDEX_CURSOR)?;
}

em.place(row_skip);
Expand Down
23 changes: 7 additions & 16 deletions src/codegen/stmt/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@

use crate::codegen::expr::{column_index, compile_cond, compile_value, emit_column_read};
use crate::codegen::index_maintenance::{
emit_index_key_ops, open_index_cursors, valid_table_root_page,
emit_index_key_ops, emit_index_key_ops_from_regs, open_index_cursors, valid_table_root_page,
};
use crate::codegen::select::{is_rowid_reference, top_level_equality_operands, CodegenError};
use crate::codegen::stmt::insert::{
Expand Down Expand Up @@ -344,26 +344,17 @@ pub fn compile_update_with_catalog(
));

if !schema.indexes.is_empty() {
// Same "`Insert` doesn't reposition the cursor" caveat as
// `insert.rs` — seek back onto the row just written before
// reading its new index-key values.
let seek_ok = em.new_label();
let seek_addr = em.emit(Instruction::new(
Opcode::SeekRowid,
TABLE_CURSOR,
0,
rowid_reg,
));
em.patch_p2(seek_addr, seek_ok);
emit_index_key_ops(
// The new row's values are already sitting in `col_regs`/
// `rowid_reg` — build index keys from those directly instead of
// seeking `TABLE_CURSOR` back onto the just-written row.
emit_index_key_ops_from_regs(
&mut em,
&mut reg,
schema,
TABLE_CURSOR,
&col_regs,
rowid_reg,
FIRST_INDEX_CURSOR,
Opcode::IdxInsert,
)?;
em.place(seek_ok);
}

if let Some(loop_start) = loop_start {
Expand Down
Loading