Skip to content

Stage shadow tables with their virtual table through named add#1650

Merged
timsehn merged 1 commit into
masterfrom
bug/vtab-shadow-staging
Jul 16, 2026
Merged

Stage shadow tables with their virtual table through named add#1650
timsehn merged 1 commit into
masterfrom
bug/vtab-shadow-staging

Conversation

@timsehn

@timsehn timsehn commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Fixes #1647.

The bug ran deeper than filed: virtual tables have no catalog entry at all (schema row only), so the entry-based named-add flow silently ignored them entirely — dolt_add('ft') returned success while staging nothing, and staged drops of a vtab failed just as silently, leaving the vtab and shadows in the commit. Committed fts/rtree content was whatever the last -Am commit happened to capture.

The fix, in the named-add path (same functions as #1643's index staging):

  • Staging a virtual table stages every working entry sqlite3IsShadowTableOf identifies as its shadow (plus their indexes), and adopts the working master so the vtab's schema row travels.
  • Staging a dropped vtab resolves through the staged catalog's schema rows (no entry to find) and removes the shadow entries with it.
  • Add-time snapshot semantics hold: rows written after dolt_add stay out of the commit (verified).

The index×VC matrix grows five vtab scenarios — named add, -am, staged drop, old-branch checkout, and clone, over both fts5 and rtree. Fail-before/pass-after: pre-fix binary fails the three staging scenarios (35/38), fixed binary passes 38/38.

Validation: matrix 38/38, shell battery 88/88, C regression 309,830/309,830 (DOLTLITE_PROLLY_CHECK=1).

Remaining sibling in the class: #1648 (view/trigger ride-along on master adoption) stays filed with analysis — its fix is a staging-model design change, not a patch.

🤖 Generated with Claude Code

Virtual tables carry no catalog entry of their own — their commit
content IS the shadow tables — so the entry-based named-add flow
silently ignored them: dolt_add('ft') returned success while staging
nothing, and committed fts/rtree content was whatever the last -Am
commit happened to capture. Staged drops of a vtab errored the same
silent way, leaving the vtab and its shadows in the commit.

Named add of a virtual table now stages every working entry that
sqlite3IsShadowTableOf identifies as its shadow (with their indexes)
and adopts the working master so the vtab's schema row travels;
staging a dropped vtab resolves through the staged catalog's schema
rows and removes the shadow entries with it. Add-time snapshot
semantics hold: rows written after dolt_add stay out of the commit.

The index x VC matrix grows vtab scenarios: named add, -am, staged
drop, old-branch checkout, and clone, over fts5 and rtree.

Fixes #1647

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Version-Control Performance Ceilings

Runs: median of 3 executions per benchmark, excluding fixture setup. The
working-set fixtures use 800 committed tables with 125 rows each
(100000 rows total); data-dirty cases update 1 row in 40
tables, and schema-dirty cases update 1 row plus add a column in 20 tables.
Branch tests use 300 branches, checkout uses a clean 400-table
branch switch over 100000 rows, and merge
tests use 100000-row tables with 2000 changed or conflicting
rows per side.

Benchmark Median ms Ceiling ms Result
status_clean_many_tables 85 200 PASS
status_dirty_many_tables 85 200 PASS
diff_regular_working_one_table 82 150 PASS
diff_regular_working_many_tables 87 200 PASS
diff_stat_working_many_tables 84 200 PASS
diff_schema_working_many_tables 84 200 PASS
branch_list_many_branches 22 100 PASS
branch_create_delete 25 100 PASS
checkout_branch_clean 57 200 PASS
merge_data_no_conflicts 30 150 PASS
merge_schema_no_conflicts 22 100 PASS
merge_data_conflicts 126 250 PASS
merge_data_conflicts_with_resolve 125 250 PASS

@github-actions

Copy link
Copy Markdown

Sysbench-Style Benchmark: Doltlite vs SQLite

In-Memory

Reads

Test SQLite (us) Doltlite (us) Multiplier
oltp_point_select 20,833 23,610 1.13
oltp_range_select 9,062 11,370 1.25
oltp_sum_range 8,512 10,322 1.21
oltp_order_range 2,407 2,822 1.17
oltp_distinct_range 3,368 3,715 1.10
oltp_index_scan 3,547 4,241 1.20
select_random_points 9,880 10,148 1.03
select_random_ranges 2,630 3,353 1.27
covering_index_scan 3,503 3,537 1.01
groupby_scan 28,882 30,703 1.06
index_join 5,240 7,141 1.36
index_join_scan 2,850 4,167 1.46
types_table_scan 1,027,697 1,181,270 1.15
table_scan 1,167,629 1,289,836 1.10
oltp_read_only 91,346 107,476 1.18
Average 1.18

Writes

Test SQLite (us) Doltlite (us) Multiplier
oltp_bulk_insert 144,410 197,347 1.37
oltp_insert 12,813 23,107 1.80
oltp_update_index 43,741 75,732 1.73
oltp_update_non_index 28,488 48,934 1.72
oltp_delete_insert 40,269 59,784 1.48
oltp_write_only 18,940 36,764 1.94
types_delete_insert 20,763 33,160 1.60
oltp_read_write 55,646 88,942 1.60
Average 1.66

File-Backed

Reads

Test SQLite (us) Doltlite (us) Multiplier
oltp_point_select 49,613 33,428 0.67
oltp_range_select 12,278 12,566 1.02
oltp_sum_range 11,567 11,783 1.02
oltp_order_range 2,852 2,976 1.04
oltp_distinct_range 3,781 3,940 1.04
oltp_index_scan 6,611 5,608 0.85
select_random_points 13,127 11,619 0.89
select_random_ranges 5,683 4,462 0.79
covering_index_scan 6,545 4,617 0.71
groupby_scan 28,369 30,779 1.08
index_join 6,951 7,952 1.14
index_join_scan 3,203 4,395 1.37
types_table_scan 1,013,317 1,167,322 1.15
table_scan 1,154,236 1,278,886 1.11
oltp_read_only 132,391 121,872 0.92
Average 0.99

Writes

Test SQLite (us) Doltlite (us) Multiplier
oltp_bulk_insert 180,464 251,197 1.39
oltp_insert 19,201 37,451 1.95
oltp_update_index 237,613 151,986 0.64
oltp_update_non_index 100,923 99,156 0.98
oltp_delete_insert 110,456 109,358 0.99
oltp_write_only 81,329 70,075 0.86
types_delete_insert 60,210 70,288 1.17
oltp_read_write 110,838 129,804 1.17
Average 1.14

File-Backed (autocommit)

Each statement runs as its own transaction — exposes per-commit
fixed costs that the wrapped-in-BEGIN/COMMIT tests amortize away.
SQLite uses WAL mode with synchronous=FULL in this section so
the comparison uses SQLite's durable WAL autocommit path.

Reads

Reads have no commit cost; these are the same SQL files as the
File-Backed Reads section, included here for symmetry and to
catch any per-statement overhead doltlite pays on the read path.

Test SQLite (us) Doltlite (us) Multiplier
oltp_point_select 30,425 33,414 1.10
oltp_range_select 10,487 12,712 1.21
oltp_sum_range 9,936 11,782 1.19
oltp_order_range 2,705 2,991 1.11
oltp_distinct_range 3,584 3,963 1.11
oltp_index_scan 4,687 5,693 1.21
select_random_points 11,445 11,980 1.05
select_random_ranges 3,746 4,467 1.19
covering_index_scan 4,600 4,727 1.03
groupby_scan 28,993 31,484 1.09
index_join 6,065 8,239 1.36
index_join_scan 3,066 4,444 1.45
types_table_scan 1,020,842 1,171,188 1.15
table_scan 1,162,389 1,293,877 1.11
oltp_read_only 105,829 122,580 1.16
Average 1.17

Writes

Test SQLite (us) Doltlite (us) Multiplier
oltp_bulk_insert_ac 34,555 94,433 2.73
oltp_insert_ac 119,448 578,800 4.85
oltp_update_index_ac 343,516 401,864 1.17
oltp_update_non_index_ac 254,229 118,814 0.47
oltp_delete_insert_ac 40,380 127,499 3.16
oltp_write_only_ac 38,773 127,510 3.29
types_delete_insert_ac 43,229 105,775 2.45
oltp_read_write_ac 51,285 114,459 2.23
Average 2.54

100000 rows, median of 5 invocations per test; autocommit writes use 9, workload-only timing via host monotonic clock when available.

Performance Ceiling Check (2.5x individual, 2x average; autocommit writes: 6x / 5x)

All tests within ceilings.

@github-actions

Copy link
Copy Markdown

Sysbench-Style Benchmark (BLOB PK): Doltlite vs SQLite

Companion to the classic Sysbench-Style Benchmark. Every workload here
runs against tables with a 16-byte big-endian BLOB PRIMARY KEY.

Individual ratios gated at 2.5×; section averages gated at 2×. Autocommit writes use 6× / 5×.

In-Memory

Reads

Test SQLite (us) Doltlite (us) Multiplier
oltp_point_select 30,175 33,843 1.12
oltp_range_select 13,859 13,278 0.96
oltp_sum_range 11,942 12,595 1.05
oltp_order_range 3,100 3,122 1.01
oltp_distinct_range 4,140 4,149 1.00
oltp_index_scan 4,479 5,603 1.25
select_random_points 17,410 19,644 1.13
select_random_ranges 4,134 5,196 1.26
covering_index_scan 4,533 4,356 0.96
groupby_scan 33,675 34,602 1.03
index_join 6,708 8,706 1.30
index_join_scan 4,140 5,448 1.32
types_table_scan 1,175,067 1,241,850 1.06
table_scan 1,358,301 1,351,705 1.00
oltp_read_only 117,624 128,513 1.09
Average 1.10

Writes

Test SQLite (us) Doltlite (us) Multiplier
oltp_bulk_insert 239,070 332,915 1.39
oltp_insert 20,325 38,914 1.91
oltp_update_index 69,155 126,975 1.84
oltp_update_non_index 49,610 82,819 1.67
oltp_delete_insert 48,474 101,961 2.10
oltp_write_only 27,791 62,344 2.24
types_delete_insert 32,220 51,911 1.61
oltp_read_write 79,972 129,754 1.62
Average 1.80

File-Backed

Reads

Test SQLite (us) Doltlite (us) Multiplier
oltp_point_select 112,417 62,202 0.55
oltp_range_select 20,612 16,091 0.78
oltp_sum_range 19,305 15,500 0.80
oltp_order_range 3,705 3,487 0.94
oltp_distinct_range 4,722 4,508 0.95
oltp_index_scan 12,606 8,809 0.70
select_random_points 25,255 22,628 0.90
select_random_ranges 12,023 8,116 0.68
covering_index_scan 12,937 7,692 0.59
groupby_scan 34,049 35,069 1.03
index_join 11,194 10,981 0.98
index_join_scan 5,084 5,890 1.16
types_table_scan 1,178,006 1,231,010 1.04
table_scan 1,357,459 1,353,718 1.00
oltp_read_only 229,158 169,157 0.74
Average 0.86

Writes

Test SQLite (us) Doltlite (us) Multiplier
oltp_bulk_insert 255,331 357,854 1.40
oltp_insert 30,423 51,432 1.69
oltp_update_index 101,000 161,152 1.60
oltp_update_non_index 75,821 106,570 1.41
oltp_delete_insert 78,116 128,730 1.65
oltp_write_only 51,721 84,295 1.63
types_delete_insert 49,993 71,108 1.42
oltp_read_write 106,431 151,448 1.42
Average 1.53

File-Backed (autocommit)

Each statement runs as its own transaction — exposes per-commit
fixed costs that the wrapped-in-BEGIN/COMMIT tests amortize away.
SQLite uses WAL mode with synchronous=FULL in this section so
the comparison uses SQLite's durable WAL autocommit path.

Reads

Reads have no commit cost; these are the same SQL files as the
File-Backed Reads section, included here for symmetry and to
catch any per-statement overhead doltlite pays on the read path.

Test SQLite (us) Doltlite (us) Multiplier
oltp_point_select 52,938 62,915 1.19
oltp_range_select 16,489 16,243 0.99
oltp_sum_range 14,606 15,598 1.07
oltp_order_range 3,430 3,497 1.02
oltp_distinct_range 4,376 4,495 1.03
oltp_index_scan 7,425 8,874 1.20
select_random_points 20,144 22,638 1.12
select_random_ranges 6,545 8,116 1.24
covering_index_scan 7,507 7,571 1.01
groupby_scan 33,672 35,205 1.05
index_join 8,590 11,070 1.29
index_join_scan 4,683 5,876 1.25
types_table_scan 1,176,456 1,246,802 1.06
table_scan 1,360,395 1,350,227 0.99
oltp_read_only 156,310 168,414 1.08
Average 1.10

Writes

Test SQLite (us) Doltlite (us) Multiplier
oltp_bulk_insert_ac 17,048 61,539 3.61
oltp_insert_ac 17,494 82,739 4.73
oltp_update_index_ac 18,895 92,114 4.88
oltp_update_non_index_ac 16,523 74,833 4.53
oltp_delete_insert_ac 17,256 85,278 4.94
oltp_write_only_ac 18,759 88,909 4.74
types_delete_insert_ac 15,391 74,691 4.85
oltp_read_write_ac 23,529 89,592 3.81
Average 4.51

100000 rows, median of 5 invocations per test; autocommit writes use 9, workload-only timing via host monotonic clock when available.

Performance Ceiling Check (2.5x individual, 2x average; autocommit writes: 6x / 5x)

All tests within ceilings.

@github-actions

Copy link
Copy Markdown

Sysbench-Style Benchmark (TEXT PK): Doltlite vs SQLite

Companion to the classic Sysbench-Style Benchmark. Every workload here
runs against tables with a 32-char hex TEXT PRIMARY KEY (UUID-shaped).

Individual ratios gated at 2.5×; section averages gated at 2×. Autocommit writes use 6× / 5×.

In-Memory

Reads

Test SQLite (us) Doltlite (us) Multiplier
oltp_point_select 31,648 35,860 1.13
oltp_range_select 15,129 13,814 0.91
oltp_sum_range 12,758 13,365 1.05
oltp_order_range 3,276 3,208 0.98
oltp_distinct_range 4,190 4,215 1.01
oltp_index_scan 4,659 6,092 1.31
select_random_points 18,281 20,572 1.13
select_random_ranges 4,236 5,340 1.26
covering_index_scan 5,057 4,828 0.95
groupby_scan 34,832 35,362 1.02
index_join 7,208 9,657 1.34
index_join_scan 5,116 5,719 1.12
types_table_scan 1,277,036 1,284,806 1.01
table_scan 1,633,815 1,422,817 0.87
oltp_read_only 126,191 133,553 1.06
Average 1.08

Writes

Test SQLite (us) Doltlite (us) Multiplier
oltp_bulk_insert 229,602 342,018 1.49
oltp_insert 22,592 40,163 1.78
oltp_update_index 76,530 140,552 1.84
oltp_update_non_index 53,099 90,434 1.70
oltp_delete_insert 56,129 109,581 1.95
oltp_write_only 30,845 65,671 2.13
types_delete_insert 34,759 56,053 1.61
oltp_read_write 92,654 138,942 1.50
Average 1.75

File-Backed

Reads

Test SQLite (us) Doltlite (us) Multiplier
oltp_point_select 114,981 64,220 0.56
oltp_range_select 24,496 16,734 0.68
oltp_sum_range 22,210 16,400 0.74
oltp_order_range 4,151 3,608 0.87
oltp_distinct_range 5,123 4,561 0.89
oltp_index_scan 13,451 9,080 0.68
select_random_points 27,121 24,468 0.90
select_random_ranges 12,625 8,278 0.66
covering_index_scan 14,393 7,752 0.54
groupby_scan 35,942 35,728 0.99
index_join 12,002 11,441 0.95
index_join_scan 6,104 6,192 1.01
types_table_scan 1,255,467 1,276,540 1.02
table_scan 1,661,131 1,423,398 0.86
oltp_read_only 246,767 176,536 0.72
Average 0.80

Writes

Test SQLite (us) Doltlite (us) Multiplier
oltp_bulk_insert 249,329 372,442 1.49
oltp_insert 40,367 52,537 1.30
oltp_update_index 116,269 177,376 1.53
oltp_update_non_index 92,300 118,396 1.28
oltp_delete_insert 91,659 139,757 1.52
oltp_write_only 95,007 89,943 0.95
types_delete_insert 55,517 77,384 1.39
oltp_read_write 136,092 163,733 1.20
Average 1.33

File-Backed (autocommit)

Each statement runs as its own transaction — exposes per-commit
fixed costs that the wrapped-in-BEGIN/COMMIT tests amortize away.
SQLite uses WAL mode with synchronous=FULL in this section so
the comparison uses SQLite's durable WAL autocommit path.

Reads

Reads have no commit cost; these are the same SQL files as the
File-Backed Reads section, included here for symmetry and to
catch any per-statement overhead doltlite pays on the read path.

Test SQLite (us) Doltlite (us) Multiplier
oltp_point_select 58,476 64,738 1.11
oltp_range_select 18,666 16,792 0.90
oltp_sum_range 16,603 16,798 1.01
oltp_order_range 3,688 3,607 0.98
oltp_distinct_range 4,667 4,559 0.98
oltp_index_scan 7,999 9,096 1.14
select_random_points 21,734 24,199 1.11
select_random_ranges 7,237 8,249 1.14
covering_index_scan 9,102 7,787 0.86
groupby_scan 35,160 35,691 1.02
index_join 9,615 11,514 1.20
index_join_scan 5,245 6,266 1.19
types_table_scan 1,272,588 1,279,565 1.01
table_scan 1,734,790 1,453,460 0.84
oltp_read_only 168,055 177,970 1.06
Average 1.04

Writes

Test SQLite (us) Doltlite (us) Multiplier
oltp_bulk_insert_ac 16,215 67,891 4.19
oltp_insert_ac 18,824 82,151 4.36
oltp_update_index_ac 20,932 98,558 4.71
oltp_update_non_index_ac 16,470 77,287 4.69
oltp_delete_insert_ac 18,243 86,115 4.72
oltp_write_only_ac 18,636 86,203 4.63
types_delete_insert_ac 15,547 75,616 4.86
oltp_read_write_ac 26,363 94,328 3.58
Average 4.47

100000 rows, median of 5 invocations per test; autocommit writes use 9, workload-only timing via host monotonic clock when available.

Performance Ceiling Check (2.5x individual, 2x average; autocommit writes: 6x / 5x)

All tests within ceilings.

@github-actions

Copy link
Copy Markdown

Sysbench-Style Benchmark (composite PK): Doltlite vs SQLite

Companion to the classic Sysbench-Style Benchmark. Every workload here
runs against tables with a 2-column INTEGER PRIMARY KEY(a, b) WITHOUT ROWID.

Individual ratios gated at 2.5×; section averages gated at 2×. Autocommit writes use 6× / 5×.

In-Memory

Reads

Test SQLite (us) Doltlite (us) Multiplier
oltp_point_select 32,324 40,310 1.25
oltp_range_select 17,567 22,275 1.27
oltp_sum_range 18,035 20,580 1.14
oltp_order_range 3,338 3,799 1.14
oltp_distinct_range 4,459 4,863 1.09
oltp_index_scan 4,754 6,011 1.26
select_random_points 27,996 32,663 1.17
select_random_ranges 7,851 9,164 1.17
covering_index_scan 4,230 4,419 1.04
groupby_scan 36,178 38,918 1.08
index_join 8,185 10,506 1.28
index_join_scan 3,991 5,501 1.38
types_table_scan 1,058,336 1,264,825 1.20
table_scan 1,188,193 1,364,393 1.15
oltp_read_only 150,772 167,934 1.11
Average 1.18

Writes

Test SQLite (us) Doltlite (us) Multiplier
oltp_bulk_insert 245,249 354,685 1.45
oltp_insert 19,001 36,510 1.92
oltp_update_index 68,282 116,878 1.71
oltp_update_non_index 51,603 82,663 1.60
oltp_delete_insert 50,909 99,057 1.95
oltp_write_only 28,169 58,004 2.06
types_delete_insert 32,655 55,598 1.70
oltp_read_write 103,668 158,756 1.53
Average 1.74

File-Backed

Reads

Test SQLite (us) Doltlite (us) Multiplier
oltp_point_select 104,253 66,289 0.64
oltp_range_select 26,806 24,818 0.93
oltp_sum_range 24,899 23,750 0.95
oltp_order_range 4,436 4,290 0.97
oltp_distinct_range 5,602 5,393 0.96
oltp_index_scan 11,806 9,267 0.78
select_random_points 36,749 36,183 0.98
select_random_ranges 15,152 12,136 0.80
covering_index_scan 11,515 7,126 0.62
groupby_scan 36,986 39,978 1.08
index_join 12,691 13,078 1.03
index_join_scan 5,154 6,131 1.19
types_table_scan 1,085,362 1,284,462 1.18
table_scan 1,242,381 1,360,075 1.09
oltp_read_only 253,918 206,605 0.81
Average 0.94

Writes

Test SQLite (us) Doltlite (us) Multiplier
oltp_bulk_insert 262,334 378,242 1.44
oltp_insert 26,931 46,501 1.73
oltp_update_index 96,217 144,254 1.50
oltp_update_non_index 78,114 107,637 1.38
oltp_delete_insert 79,870 121,657 1.52
oltp_write_only 54,481 80,400 1.48
types_delete_insert 52,037 72,733 1.40
oltp_read_write 134,118 177,906 1.33
Average 1.47

File-Backed (autocommit)

Each statement runs as its own transaction — exposes per-commit
fixed costs that the wrapped-in-BEGIN/COMMIT tests amortize away.
SQLite uses WAL mode with synchronous=FULL in this section so
the comparison uses SQLite's durable WAL autocommit path.

Reads

Reads have no commit cost; these are the same SQL files as the
File-Backed Reads section, included here for symmetry and to
catch any per-statement overhead doltlite pays on the read path.

Test SQLite (us) Doltlite (us) Multiplier
oltp_point_select 56,615 65,776 1.16
oltp_range_select 22,243 24,798 1.11
oltp_sum_range 20,359 23,572 1.16
oltp_order_range 3,965 4,317 1.09
oltp_distinct_range 4,962 5,493 1.11
oltp_index_scan 6,990 8,948 1.28
select_random_points 31,345 35,986 1.15
select_random_ranges 10,270 11,928 1.16
covering_index_scan 6,838 7,041 1.03
groupby_scan 35,736 39,105 1.09
index_join 9,832 12,866 1.31
index_join_scan 4,582 5,890 1.29
types_table_scan 1,055,022 1,282,575 1.22
table_scan 1,218,299 1,393,559 1.14
oltp_read_only 190,552 212,337 1.11
Average 1.16

Writes

Test SQLite (us) Doltlite (us) Multiplier
oltp_bulk_insert_ac 22,900 77,516 3.38
oltp_insert_ac 23,154 105,671 4.56
oltp_update_index_ac 26,928 111,777 4.15
oltp_update_non_index_ac 24,245 91,544 3.78
oltp_delete_insert_ac 23,467 107,013 4.56
oltp_write_only_ac 24,650 104,927 4.26
types_delete_insert_ac 22,637 93,118 4.11
oltp_read_write_ac 31,509 111,057 3.52
Average 4.04

100000 rows, median of 5 invocations per test; autocommit writes use 9, workload-only timing via host monotonic clock when available.

Performance Ceiling Check (2.5x individual, 2x average; autocommit writes: 6x / 5x)

All tests within ceilings.

@timsehn
timsehn merged commit 6b62eda into master Jul 16, 2026
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Named dolt_add of a virtual table does not stage its shadow tables — fts content silently lost from commits

1 participant