Commit Graph

3572 Commits

Author SHA1 Message Date
Jussi Saurio
0b627ed331 Merge 'btree/balance: support case where immediate parent page of unbalanced child page also overflows' from Jussi Saurio
Closes #2241
## What
When an index interior cell is deleted, it steals the leaf cell with the
largest key in its left subtree, deletes the old interior cell and then
replaces it with the stolen cell. This ensures the binary-search-tree
aspect of the btree remains correct. However, this can cause a situation
where both are true:
1. The leaf page is now UNDERFULL and must be rebalanced
2. The leaf's IMMEDIATE parent page is now OVERFULL and must be
rebalanced
## Why is this a problem
We simply didn't support the case where:
- Leaf page P is unbalanced and rebalancing starts on it
- Its immediate parent is ALSO unbalanced and _overflows_.
We had an assertion against this happening (see #2241)
## The fix
Allow exactly 1 overflow cell in the parent under very particular
conditions:
1. The parent page must be an index interior page
2. The parent must be positioned exactly at the divider cell whose left
child page underflows
This is the _only_ case where the immediate parent of a page about to
undergo rebalancing can have overflow cells.
## Implementation details
The parent overflow cell is folded into `cell_array` fairly early on and
`parent.overflow_cells` is cleared. However we need to be careful with
`cell_idx` for dividers other than the overflow cell because they get
shifted left on the page in `drop_cell()`. I've added a long comment
about this.
## Testing
Adds fuzz test that does inserts and deletes on an index btree and
asserts that all the expected keys are found at the end in the right
order. This test runs into this case quite frequently so I was able to
verify it.

Reviewed-by: Pere Diaz Bou <pere-altea@homail.com>

Closes #2243
2025-07-24 18:48:36 +03:00
Pere Diaz Bou
46f5609fce Merge 'Append WAL frames one by one' from Pere Diaz Bou
Let's make sure we don't end up in a weird situation by appending frames
one by one and we can later think of optimizations.

Reviewed-by: Jussi Saurio <jussi.saurio@gmail.com>

Closes #2034
2025-07-24 16:44:51 +02:00
Pere Diaz Bou
ce598b772e clippy i hate you so much 2025-07-24 15:29:21 +02:00
Pere Diaz Bou
b07e57d9d1 review fixes 2025-07-24 15:29:21 +02:00
Pere Diaz Bou
75f9c23ed3 end txn on vdbe failures 2025-07-24 15:29:21 +02:00
Pere Diaz Bou
674d88e140 do not clear dirty pages on cacheflush::start 2025-07-24 15:29:21 +02:00
Pere Diaz Bou
d77c899fa6 clippy 2025-07-24 15:29:21 +02:00
Pere Diaz Bou
5a1773edf1 clippy 2025-07-24 15:29:21 +02:00
Pere Diaz Bou
14de7c55af set connection state to None in vdbe rollback 2025-07-24 15:29:21 +02:00
Pere Diaz Bou
c397588ad6 change connection state after finding error on I/O 2025-07-24 15:29:06 +02:00
Pere Diaz Bou
5f8e386b48 reset internal states on rollback 2025-07-24 15:29:06 +02:00
Jussi Saurio
37955e9a04 Pager/WAL: fix not clearing stale page cache
SQLite behavior is: if another connection has modified the DB when a
read tx starts, it must clear its page cache due to the potentiality
of there being stale versions of pages in it.

In the future, we may want to do either:
1. a more granular invalidation logic for per-conn cache, or
2. a shared versioned page cache

But right now we must follow SQLite to make our current behavior not
corrupt data
2025-07-24 16:23:12 +03:00
Pere Diaz Bou
066ffcc940 append frame one by one
Let's make sure we don't end up in a weird situation by appending frames
one by one and we can later think of optimizations.
2025-07-24 15:12:13 +02:00
Pekka Enberg
62f5a42008 Merge 'WAL insert API: force schema re-parse if necessary after WAL sync session end' from Nikita Sivukhin
This PR partially fixes issue when schema changes were invisible after
WAL sync calls. Now, `wal_insert_end` always read fresh schema cookie
and re-parse schema from scratch if cookie changed.
Generally, the problem of "silent" schema update can be more generic
if(when?) `turso-db` will support multi-process setup. But for now only
single-process can work with `turso-db`, so I decided to inject re-parse
logic explicitly in WAL raw API in order to not introduce any
unnecessary overhead in the ordinary execution path.
This fix is not complete, as if we will have already prepared statements
- they should be re-prepared too in case of schema changes. But this
problem already tracked in the PR
https://github.com/tursodatabase/turso/pull/2214

Reviewed-by: Pedro Muniz (@pedrocarlo)

Closes #2246
2025-07-24 14:39:46 +03:00
Jussi Saurio
d1b1617231 btree: add index insert-delete fuzz test 2025-07-24 13:18:33 +03:00
Jussi Saurio
d773a7924d fix/btree/balance: allow exactly 1 parent overflow cell for index balancing 2025-07-24 13:18:33 +03:00
Jussi Saurio
025ea8808a Merge 'WAL insert: mark pages as dirty' from Nikita Sivukhin
WAL insert API introduced in the #2231 works incorrectly as it never
mark inserted pages as dirty.
This PR fixes this issue and also add simple fuzz test which fails
without fixes.

Reviewed-by: Jussi Saurio <jussi.saurio@gmail.com>

Closes #2245
2025-07-24 12:58:01 +03:00
Jussi Saurio
9a08c57b58 Merge 'make add dirty to change flag and also add page to the dirty list' from Nikita Sivukhin
Make `add_dirty` helper to set flag and add page to the dirt list. This
makes API safer as now its harder to do one thing and forget about
another (which can lead to DB corruption).

Reviewed-by: Pere Diaz Bou <pere-altea@homail.com>
Reviewed-by: Jussi Saurio <jussi.saurio@gmail.com>

Closes #2244
2025-07-24 12:22:33 +03:00
Nikita Sivukhin
6daa6d07f1 re-parse schema if necessary after WAL sync end 2025-07-24 11:52:07 +04:00
Nikita Sivukhin
edd6ef2d21 fix after rebase 2025-07-24 11:51:33 +04:00
Nikita Sivukhin
3d2a38eb88 add simple helper 2025-07-24 11:49:39 +04:00
Nikita Sivukhin
4a80306705 fix wal insert frame raw API
- we need to properly mark pages as dirty after insertion
2025-07-24 11:49:39 +04:00
Nikita Sivukhin
d618463906 simplify add_dirty API 2025-07-24 11:29:01 +04:00
Jussi Saurio
2d3c9001ee Merge 'emit SetCookie after DropTable' from Glauber Costa
The SetCookie opcode is used, among other things, to notify the
transaction of schema changes. We are not issuing it on DropTable.
Without it, the transaction thinks the schema hasn't changed, and does
not update the schema of the connection back to the database.
SQLite will, of course, issue it:
35    DropTable      0     0     0     foo            0
36    SetCookie      0     1     2                    0
Unfortunately I don't have a unit test that breaks with this, because
the one that is supposed to break is having, let's put it this way,
bigger problems.

Reviewed-by: Jussi Saurio <jussi.saurio@gmail.com>

Closes #2249
2025-07-24 10:12:16 +03:00
Jussi Saurio
92a10f94d8 Merge 'Bail early for read-only virtual tables' from Preston Thorpe
This PR adds a const associated value on the VTabModule trait,
`READONLY` defaulted to `true`, so we can bail early when a write
operation is done on an invalid vtable.
This prevents extensions from having to implement `insert`,`update`,
`delete` just to return `Error::ReadOnly`, and prevents us from having
to step through `VUpdate` just to error out.

Reviewed-by: Jussi Saurio <jussi.saurio@gmail.com>

Closes #2247
2025-07-24 10:12:07 +03:00
Jussi Saurio
dd30729f5e Merge 'Explicit rowid insert' from Nikita Sivukhin
This PR adds support for `INSERT` queries with explicit value for
`rowid` column (not thought rowid alias):
```
turso> create table t(x, y, z);
turso> insert into t(rowid, x, y, z) values (10, 1, 2, 3);
turso> select rowid, * from t;
┌───────┬───┬───┬───┐
│ rowid │ x │ y │ z │
├───────┼───┼───┼───┤
│    10 │ 1 │ 2 │ 3 │
└───────┴───┴───┴───┘
```

Reviewed-by: Jussi Saurio <jussi.saurio@gmail.com>

Closes #2239
2025-07-24 10:08:42 +03:00
Jussi Saurio
49b2bf4fdb Merge 'Deserialize keys only once when sorting immutable records' from Iaroslav Zeigerman
Before this update, the entire immutable record was **fully**
deserialized **every** time it was compared in the sorter.
This PR extends the sorter with incremental deserialization of record
keys, only when needed and only if they weren’t already deserialized in
a previous iteration.
I hate that we panic on failed deserialization in `cmp`, but
unfortunately, I can’t return `Result` as part of this interface.
Looking for feedback around a better way to handle this.
Alternatively, I could store the deserialization error as part of
`SortableImmutableRecord` and check it before returning the record in
`next`, thereby deferring the error handling. The downside of this
approach is that it complicates debugging, since the error will be
completely decoupled from the place where it occurs.

Reviewed-by: Jussi Saurio <jussi.saurio@gmail.com>

Closes #2207
2025-07-24 10:08:16 +03:00
Jussi Saurio
52b4c22be9 Merge 'fix: SUM returns correct float for mixed numeric/non-numeric types & return value on empty set' from Axel Tobieson Rova
# Fix SUM aggregate function for mixed types
Fixes #2133
The SUM aggregate function was returning incorrect results when
processing tables with mixed numeric and non-numeric values. According
to SQLite documentation:
> "If any input to sum() is neither an integer nor a NULL, then sum()
returns a floating point value"
[*](https://sqlite.org/lang_aggfunc.html)
Now both SQLite and Turso yield the same output of 44.0.
--
I modified `Sum` to increment only for numeric values, skipping non-
numeric values. However, if we have mixed numeric values or non-numeric
values, we return a float output. Added a flag to keep track of it.
as pointed out by @FHaggs , If there are no non-NULL input rows then
sum() returns NULL but total() returns 0.0. I decided to include it in
this PR as well. Empty was such a natural test case.

Reviewed-by: Jussi Saurio <jussi.saurio@gmail.com>

Closes #2182
2025-07-24 10:08:01 +03:00
Glauber Costa
2a2468026c emit SetCookie after DropTable
The SetCookie opcode is used, among other things, to notify the
transaction of schema changes. We are not issuing it on DropTable.

Without it, the transaction thinks the schema hasn't changed, and does
not update the schema of the connection back to the database.

SQLite will, of course, issue it:

35    DropTable      0     0     0     foo            0
36    SetCookie      0     1     2                    0

Unfortunately I don't have a unit test that breaks with this, because
the one that is supposed to break is having, let's put it this way,
bigger problems.
2025-07-23 19:34:41 -05:00
PThorpe92
b68539fc45 Make the ReadOnly error more generic 2025-07-23 16:58:22 -04:00
PThorpe92
0871a8c7f3 Bail early when we detect a readonly virtual table 2025-07-23 16:57:30 -04:00
PThorpe92
9c3f9426c3 Add readonly method for VirtualTable to bail early 2025-07-23 16:49:42 -04:00
Iaroslav Zeigerman
1e51d23bd6 store the key deserialization error instead of panicking 2025-07-23 11:22:01 -07:00
Nikita Sivukhin
f4a40c43cd fix clippy 2025-07-23 20:19:00 +04:00
Nikita Sivukhin
30c7bef27b make add dirty to change flag and also add page to the dirty list 2025-07-23 20:06:49 +04:00
Nikita Sivukhin
001670c069 fix clippy 2025-07-23 16:00:24 +04:00
Nikita Sivukhin
a017baced7 small refactoring 2025-07-23 15:56:38 +04:00
Nikita Sivukhin
d3f3807ede fix cdc emit 2025-07-23 15:50:34 +04:00
Nikita Sivukhin
fd63128227 adjust behaviour in case when both rowid alias and rowid provided 2025-07-23 15:42:05 +04:00
Nikita Sivukhin
a4d114460a adjust vtable insert 2025-07-23 15:22:56 +04:00
Jussi Saurio
1e38202084 Merge 'WAL insert API' from Nikita Sivukhin
This PR implements missing raw WAL API from LibSQL for future use for
offline-sync feature:
1. `wal_insert_begin` - begin WAL session by opening WAL read/write
transaction
2. `wal_insert_end` - finish WAL session by closing WAL transaction
opened by `wal_insert_begin` call
3. `wal_insert_frame` - insert frame `frame_no` with raw content `frame`
(WAL frame included)
For now any schema changes will not be reflected after
`wal_insert_frame` because `turso-db` do not re-parse schema without
need. I will fix this in follow up PR.

Reviewed-by: Pekka Enberg <penberg@iki.fi>

Closes #2231
2025-07-23 14:08:15 +03:00
Nikita Sivukhin
0178b41b28 accept explicit "rowid" column name in the INSERT statement 2025-07-23 15:03:38 +04:00
Jussi Saurio
63f488a1cc Merge 'Pager: clear overflow cells when freeing page' from Jussi Saurio
## Background
The `balance_non_root` procedure can end up freeing a page if the pages
to be balanced can fit the required combined number of cells in less
pages, even if the page that triggered balancing is overfull. This can
then free the originally overfull pages, leaving a non-zero
`overflow_cells` on the in-mem representation of the page.
```rust
balance_non_root: page=305, overflow_cells=0
balance_non_root: page=304, overflow_cells=0
balance_non_root: page=302, overflow_cells=1
pre_edit_page(page=304, page_idx=0, new_cells=4, old_cells=1, cells_per_page_old=[1, 3, 9, 0, 0], cells_per_page_new=[4, 9, 9, 0, 0], cell_array_count=9)
edit_page start_old_cells=0 start_new_cells=0 number_new_cells=4 cell_array=9 end_old_cells=1 end_new_cells=4
pre_edit_page(page=305, page_idx=1, new_cells=4, old_cells=1, cells_per_page_old=[1, 3, 9, 0, 0], cells_per_page_new=[4, 9, 9, 0, 0], cell_array_count=9)
edit_page start_old_cells=2 start_new_cells=5 number_new_cells=4 cell_array=9 end_old_cells=3 end_new_cells=9
balance_non_root: sibling_count_new=2, sibling_count=3

// Custom assertion to demonstrate this:
thread 'main' panicked at core/storage/pager.rs:1127:29:
Pager::free_page: In memory page with id 302 has overflow cells
```
## Why is this a problem
Right now this is not an immediate problem, because we always allocate
brand new pages. However, in #2233 we begin to reuse pages from the
freelist for page allocation to improve performance and reduce database
size bloat. In that PR, the `balance_non_root` procedure will calculate
cell counts incorrectly in `edit_page()` and panic if: 1. a new
allocated page is taken from the freelist, 2. the page is still in
memory, and 3. and it still contains `overflow_cells`.
## Solution
Clear `page_contents.overflow_cells` when an in-memory page is freed.

Reviewed-by: Pere Diaz Bou <pere-altea@homail.com>

Closes #2238
2025-07-23 13:38:31 +03:00
Jussi Saurio
f98a9e8939 Pager: don't assume page is necessarily in memory anymore 2025-07-23 11:08:34 +03:00
Jussi Saurio
ecb5fce1bd Pager: clear overflow cells when freeing page 2025-07-23 10:58:10 +03:00
Jussi Saurio
ffd2299aa1 types: less noisy Debug implementation for ImmutableRecord 2025-07-23 10:56:41 +03:00
Nikita Sivukhin
60eaa11add hide new methods behind fs feature 2025-07-23 11:51:39 +04:00
Nikita Sivukhin
a85283a84f add trailing comma 2025-07-23 11:31:00 +04:00
Nikita Sivukhin
3c0af3e389 small adjustments 2025-07-23 11:31:00 +04:00
Nikita Sivukhin
73761a8983 rollback non-commited changes 2025-07-23 11:31:00 +04:00