From c8922e6f6b90eda21bf81073496efbccd44a2fe9 Mon Sep 17 00:00:00 2001 From: dan Date: Tue, 13 Oct 2026 14:59:41 +0000 Subject: [PATCH] Merge fixes to vec1 made on trunk into this branch. FossilOrigin-Name: 2ca1a93f01b777276494cf2bd6f7aaa86deeb2d8a43dbcd5dccc3a31318482a3 --- ext/vec1/test/vec1bucket.test | 194 ++++++++++++++ ext/vec1/test/vec1corrupt.test | 476 ++++++++++++++++++++++++++++++++- ext/vec1/tool/vec1_algo_test.c | 2 +- ext/vec1/vec1.c | 347 ++++++++++++++++++------ manifest | 22 +- manifest.uuid | 2 +- 6 files changed, 947 insertions(+), 96 deletions(-) create mode 100644 ext/vec1/test/vec1bucket.test diff --git a/ext/vec1/test/vec1bucket.test b/ext/vec1/test/vec1bucket.test new file mode 100644 index 0000000000..f2c3907d3b --- /dev/null +++ b/ext/vec1/test/vec1bucket.test @@ -0,0 +1,194 @@ +# 2026-10-10 +# +# The author disclaims copyright to this source code. In place of +# a legal notice, here is a blessing: +# +# May you do good and not evil. +# May you find forgiveness for yourself and forgive others. +# May you share freely, never taking more than you give. +# +#*********************************************************************** +# +# Tests for vectors that lie (almost) exactly on the boundary between two +# IVF buckets. The bucket each such vector is assigned to depends only on +# rounding in the L2 distance calculation. The 'rebuild' command, integrity +# check and DELETE must all agree on which bucket each vector is in. +# +# A real PQ model is trained, then its centroids are overwritten with known +# values. Centroids 0 and 1 are random, the others are far away from +# everything. All vectors are inserted near the midpoint of centroids +# 0 and 1. Using 8 centroids matters - with fewer than 8 the AVX2 rebuild +# code falls back to the same routine used by integrity-check and DELETE. +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1bucket +vec1_load db + +set nElem 128 +set nVec 2000 +set noise 1e-6 + +proc rlist {n} { + set v {} + for {set i 0} {$i<$n} {incr i} { lappend v [expr {rand()*2.0-1.0}] } + set v +} + +# Train a model with 8 buckets, plain PQ, no rotation and no residuals. +do_test 1.0 { + db eval { CREATE TEMP TABLE tr(v) } + for {set i 0} {$i<600} {incr i} { + db eval { INSERT INTO tr VALUES( random_vector($nElem) ) } + } + set T [db one { + SELECT vec1_train(v, '{codesize:8, nbucket:8, quantizer:"pq", residual:0}') + FROM tr + }] + binary scan $T I6 hdr + lassign $hdr iVersion flags nE nCodebook nBucket eDist + list [expr {$flags & 0x16}] $nBucket +} {0 8} + +# Replace the centroids in the model blob. +set nCodeElem [expr {($nElem + $nCodebook - 1) / $nCodebook}] +set iCentOff [expr {24 + $nCodebook*$nCodeElem*256*4}] +set c0 [rlist $nElem] +set c1 [rlist $nElem] +set aCent [concat $c0 $c1] +for {set i 2} {$i<$nBucket} {incr i} { + foreach x [rlist $nElem] { lappend aCent [expr {$x + 100.0}] } +} +set T [string replace $T $iCentOff [expr {$iCentOff+$nBucket*$nElem*4-1}] \ + [binary format f* $aCent]] + +# Insert vectors near the midpoint of c0 and c1, plus a little noise. +set mid {} +foreach a $c0 b $c1 { lappend mid [expr {($a+$b)/2.0}] } +do_test 1.1 { + db eval { CREATE VIRTUAL TABLE v1 USING vec1 } + db transaction { + for {set i 1} {$i<=$nVec} {incr i} { + set v {} + foreach m $mid { lappend v [expr {$m + (rand()-0.5)*$noise}] } + set blob [binary format f* $v] + db eval { INSERT INTO v1(rowid, vector) VALUES($i, $blob) } + } + } + db one { SELECT count(*) FROM v1 } +} $nVec + +# Rebuild (batched N-vector path), then integrity-check (per-vector path). +do_execsql_test 1.2 { INSERT INTO v1(cmd, vector) VALUES('rebuild', $T) } +do_execsql_test 1.3 { PRAGMA integrity_check } ok + +# Delete rows with rowids from iFirst to nVec, stepping by 2. Each DELETE +# recomputes the bucket using the per-vector path, so may compute a +# different bucket to the one used by 'rebuild'. Return the number of rows +# that could not be deleted. +proc delete_rows {tbl iFirst} { + global nVec + set nFail 0 + for {set i $iFirst} {$i<=$nVec} {incr i 2} { + if {[catch { db eval "DELETE FROM $tbl WHERE rowid=$i" }]} { + incr nFail + } + } + set nFail +} + +# Delete every row, checking the index is still consistent half way. +# Deleting rows causes lists to be rewritten once they contain enough +# tombstones, which must be done using the bucket the row was actually +# found in, not the one calculated for it. +do_test 1.4 { delete_rows v1 1 } 0 +do_execsql_test 1.5 { PRAGMA integrity_check } ok +do_test 1.6 { delete_rows v1 2 } 0 +do_execsql_test 1.7 { + SELECT count(*) FROM v1; + PRAGMA integrity_check; +} {0 ok} + +#------------------------------------------------------------------------- +# Same again, but this time move all of bucket 1's lists into bucket 7, +# which is nowhere near any of the vectors. Integrity-check reports this. +# But DELETE should still be able to find and delete the rows. +# +do_test 2.0 { + db eval { CREATE VIRTUAL TABLE v2 USING vec1 } + db transaction { + for {set i 1} {$i<=$nVec} {incr i} { + set v {} + foreach m $mid { lappend v [expr {$m + (rand()-0.5)*$noise}] } + set blob [binary format f* $v] + db eval { INSERT INTO v2(rowid, vector) VALUES($i, $blob) } + } + } + db eval { INSERT INTO v2(cmd, vector) VALUES('rebuild', $T) } + db one { SELECT count(*)>0 FROM v2_idx WHERE bucket=1 } +} 1 + +do_execsql_test 2.1 { + UPDATE v2_idx SET bucket=7 WHERE bucket=1; +} +do_test 2.2 { + string match "*is in wrong bucket*" [db one { PRAGMA integrity_check }] +} 1 +do_test 2.3 { delete_rows v2 1 } 0 +do_test 2.4 { delete_rows v2 2 } 0 +do_execsql_test 2.5 { SELECT count(*) FROM v2 } 0 + +#------------------------------------------------------------------------- +# Create a list in bucket 1 containing rows that belong in bucket 0 as +# well as rows that belong in bucket 1. Then delete all the bucket 0 rows. +# DELETE finds them by searching all buckets. When a list is rewritten to +# remove tombstones, it must stay in bucket 1, the bucket it was found in. +# If it were moved to bucket 0, the bucket 1 rows left in it would be in +# the wrong bucket. +# +# The list is created by rebuilding using model $TA, in which centroid 0 +# is far away from everything, so all rows go to bucket 1. The model is +# then replaced by $T, in which centroid 0 is c0 again. +# +set TA [string replace $T $iCentOff [expr {$iCentOff+$nElem*4-1}] \ + [binary format f* [lmap x $c0 {expr {$x + 200.0}}]]] + +do_test 3.0 { + db eval { CREATE VIRTUAL TABLE v3 USING vec1 } + db transaction { + for {set i 1} {$i<=$nVec} {incr i} { + set c [expr {$i<=$nVec/2 ? $c0 : $c1}] + set v [lmap x $c {expr {$x + (rand()-0.5)*0.001}}] + set blob [binary format f* $v] + db eval { INSERT INTO v3(rowid, vector) VALUES($i, $blob) } + } + } + db eval { INSERT INTO v3(cmd, vector) VALUES('rebuild', $TA) } + db eval { SELECT DISTINCT bucket FROM v3_idx } +} 1 + +do_test 3.1 { + db eval { UPDATE v3_model SET val = $T WHERE id=1 } + db close + sqlite3 db test.db + vec1_load db + string match "*is in wrong bucket*" [db one { PRAGMA integrity_check }] +} 1 + +proc delete_range {tbl iFirst iLast} { + set nFail 0 + for {set i $iFirst} {$i<=$iLast} {incr i} { + if {[catch { db eval "DELETE FROM $tbl WHERE rowid=$i" }]} { + incr nFail + } + } + set nFail +} + +do_test 3.2 { delete_range v3 1 [expr {$nVec/2}] } 0 +do_execsql_test 3.3 { + SELECT count(*) FROM v3; + PRAGMA integrity_check; +} [list [expr {$nVec/2}] ok] + +finish_test diff --git a/ext/vec1/test/vec1corrupt.test b/ext/vec1/test/vec1corrupt.test index 8cd0b3883a..99721374fa 100644 --- a/ext/vec1/test/vec1corrupt.test +++ b/ext/vec1/test/vec1corrupt.test @@ -45,13 +45,487 @@ do_execsql_test 1.3 { #------------------------------------------------------------------------- +reset_db + +do_test 2.0 { + sqlite3 db {} + db deserialize [decode_hexdb { +| size 28672 pagesize 4096 filename poc_oob_read.db +| page 1 offset 0 +| 0: 53 51 4c 69 74 65 20 66 6f 72 6d 61 74 20 33 00 SQLite format 3. +| 16: 10 00 01 01 00 40 20 20 00 00 00 17 00 00 00 07 .....@ ........ +| 32: 00 00 00 00 00 00 00 00 00 00 00 07 00 00 00 04 ................ +| 48: 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 ................ +| 80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 17 ................ +| 96: 00 2e 99 b0 0d 00 00 00 07 0d a6 00 0f ca 0f 74 ...............t +| 112: 0f 1b 0e 9f 0e 4b 0d f7 0d a6 00 00 00 00 00 00 .....K.......... +| 3488: 00 00 00 00 00 00 4f 07 06 17 19 19 01 7b 74 61 ......O.......ta +| 3504: 62 6c 65 74 5f 6d 65 74 61 74 5f 6d 65 74 61 07 blet_metat_meta. +| 3520: 43 52 45 41 54 45 20 54 41 42 4c 45 20 27 74 5f CREATE TABLE 't_ +| 3536: 6d 65 74 61 27 28 69 64 20 49 4e 54 45 47 45 52 meta'(id INTEGER +| 3552: 20 50 52 49 4d 41 52 59 20 4b 45 59 2c 20 76 61 PRIMARY KEY, va +| 3568: 6c 20 42 4c 4f 42 29 52 06 06 17 1b 1b 01 7d 74 l BLOB)R.......t +| 3584: 61 62 6c 65 74 5f 6d 6f 64 65 6c 74 5f 6d 6f 64 ablet_modelt_mod +| 3600: 65 6c 06 43 52 45 41 54 45 20 54 41 42 4c 45 20 el.CREATE TABLE +| 3616: 27 74 5f 6d 6f 64 65 6c 27 28 69 64 20 49 4e 54 't_model'(id INT +| 3632: 45 47 45 52 20 50 52 49 4d 41 52 59 20 4b 45 59 EGER PRIMARY KEY +| 3648: 2c 20 76 61 6c 20 42 4c 4f 42 29 52 05 06 17 1f , val BLOB)R.... +| 3664: 17 01 7d 69 6e 64 65 78 74 5f 69 64 78 5f 69 64 ...indext_idx_id +| 3680: 78 74 5f 69 64 78 05 43 52 45 41 54 45 20 49 4e xt_idx.CREATE IN +| 3696: 44 45 58 20 27 74 5f 69 64 78 5f 69 64 78 27 20 DEX 't_idx_idx' +| 3712: 4f 4e 20 27 74 5f 69 64 78 27 28 62 75 63 6b 65 ON 't_idx'(bucke +| 3728: 74 2c 20 66 69 72 73 74 2c 20 6c 61 73 74 29 7a t, first, last)z +| 3744: 04 07 17 17 17 01 81 53 74 61 62 6c 65 74 5f 69 .......Stablet_i +| 3760: 64 78 74 5f 69 64 78 04 43 52 45 41 54 45 20 54 dxt_idx.CREATE T +| 3776: 41 42 4c 45 20 27 74 5f 69 64 78 27 28 69 64 20 ABLE 't_idx'(id +| 3792: 49 4e 54 45 47 45 52 20 50 52 49 4d 41 52 59 20 INTEGER PRIMARY +| 3808: 4b 45 59 2c 20 62 75 63 6b 65 74 20 49 4e 54 45 KEY, bucket INTE +| 3824: 47 45 52 2c 20 66 69 72 73 74 20 49 4e 54 45 47 GER, first INTEG +| 3840: 45 52 2c 20 6c 61 73 74 20 49 4e 54 45 47 45 52 ER, last INTEGER +| 3856: 2c 20 76 61 6c 20 42 4c 4f 42 29 57 03 07 17 19 , val BLOB)W.... +| 3872: 19 01 81 09 74 61 62 6c 65 74 5f 62 61 73 65 74 ....tablet_baset +| 3888: 5f 62 61 73 65 03 43 52 45 41 54 45 20 54 41 42 _base.CREATE TAB +| 3904: 4c 45 20 27 74 5f 62 61 73 65 27 28 69 64 20 49 LE 't_base'(id I +| 3920: 4e 54 45 47 45 52 20 50 52 49 4d 41 52 59 20 4b NTEGER PRIMARY K +| 3936: 45 59 2c 20 76 65 63 74 6f 72 20 42 4c 4f 42 2c EY, vector BLOB, +| 3952: 20 63 30 29 54 02 06 17 1d 1d 01 7d 74 61 62 6c c0)T.......tabl +| 3968: 65 74 5f 63 6f 6e 66 69 67 74 5f 63 6f 6e 66 69 et_configt_confi +| 3984: 67 02 43 52 45 41 54 45 20 54 41 42 4c 45 20 27 g.CREATE TABLE ' +| 4000: 74 5f 63 6f 6e 66 69 67 27 28 69 64 20 49 4e 54 t_config'(id INT +| 4016: 45 47 45 52 20 50 52 49 4d 41 52 59 20 4b 45 59 EGER PRIMARY KEY +| 4032: 2c 20 76 61 6c 20 41 4e 59 29 34 01 06 17 0f 0f , val ANY)4..... +| 4048: 08 5b 74 61 62 6c 65 74 74 43 52 45 41 54 45 20 .[tablettCREATE +| 4064: 56 49 52 54 55 41 4c 20 54 41 42 4c 45 20 74 20 VIRTUAL TABLE t +| 4080: 55 53 49 4e 47 20 76 65 63 31 28 76 2c 20 6d 29 USING vec1(v, m) +| page 2 offset 4096 +| 0: 0d 00 00 00 03 0f ef 00 0f f5 0f ef 0f fa 00 00 ................ +| 4064: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 04 ................ +| 4080: 02 03 00 01 04 03 00 03 00 09 04 05 03 00 01 04 ................ +| page 3 offset 8192 +| 0: 0d 00 00 00 14 0e e8 00 0f f2 0f e4 0f d6 0f c8 ................ +| 16: 0f ba 0f ac 0f 9e 0f 90 0f 82 0f 74 0f 66 0f 58 ...........t.f.X +| 32: 0f 4a 0f 3c 0f 2e 0f 20 0f 12 0f 04 0e f6 0e e8 .J.<... ........ +| 3808: 00 00 00 00 00 00 00 00 0c 14 04 00 08 07 40 69 ..............@i +| 3824: 00 00 00 00 00 00 0c 13 04 00 08 07 40 67 c0 00 ............@g.. +| 3840: 00 00 00 00 0c 12 04 00 08 07 40 66 80 00 00 00 ..........@f.... +| 3856: 00 00 0c 11 04 00 08 07 40 65 40 00 00 00 00 00 ........@e@..... +| 3872: 0c 10 04 00 08 07 40 64 00 00 00 00 00 00 0c 0f ......@d........ +| 3888: 04 00 08 07 40 62 c0 00 00 00 00 00 0c 0e 04 00 ....@b.......... +| 3904: 08 07 40 61 80 00 00 00 00 00 0c 0d 04 00 08 07 ..@a............ +| 3920: 40 60 40 00 00 00 00 00 0c 0c 04 00 08 07 40 5e @`@...........@^ +| 3936: 00 00 00 00 00 00 0c 0b 04 00 08 07 40 5b 80 00 ............@[.. +| 3952: 00 00 00 00 0c 0a 04 00 08 07 40 59 00 00 00 00 ..........@Y.... +| 3968: 00 00 0c 09 04 00 08 07 40 56 80 00 00 00 00 00 ........@V...... +| 3984: 0c 08 04 00 08 07 40 54 00 00 00 00 00 00 0c 07 ......@T........ +| 4000: 04 00 08 07 40 51 80 00 00 00 00 00 0c 06 04 00 ....@Q.......... +| 4016: 08 07 40 4e 00 00 00 00 00 00 0c 05 04 00 08 07 ..@N............ +| 4032: 40 49 00 00 00 00 00 00 0c 04 04 00 08 07 40 44 @I............@D +| 4048: 00 00 00 00 00 00 0c 03 04 00 08 07 40 3e 00 00 ............@>.. +| 4064: 00 00 00 00 0c 02 04 00 08 07 40 34 00 00 00 00 ..........@4.... +| 4080: 00 00 0c 01 04 00 08 07 40 24 00 00 00 00 00 00 ........@$...... +| page 4 offset 12288 +| 0: 0d 00 00 00 01 0e 59 00 0e 59 00 00 00 00 00 00 ......Y..Y...... +| 3664: 00 00 00 00 00 00 00 00 00 83 24 01 07 00 08 09 ..........$..... +| 3680: 01 86 44 14 00 00 00 02 00 00 00 14 00 00 00 00 ..D............. +| 3696: 00 00 00 01 00 00 00 02 00 00 00 03 00 00 00 04 ................ +| 3712: 00 00 00 05 00 00 00 06 00 00 00 07 00 00 00 08 ................ +| 3728: 00 00 00 09 00 00 00 0a 00 00 00 0b 00 00 00 0c ................ +| 3744: 00 00 00 0d 00 00 00 0e 00 00 00 0f 00 00 00 10 ................ +| 3760: 00 00 00 11 00 00 00 12 00 00 00 13 00 00 00 14 ................ +| 3776: 00 00 80 3f 00 00 00 40 00 00 40 40 00 00 80 40 ...?...@..@@...@ +| 3792: 00 00 00 40 00 00 40 40 00 00 80 40 00 00 a0 40 ...@..@@...@...@ +| 3808: 00 00 40 40 00 00 80 40 00 00 a0 40 00 00 c0 40 ..@@...@...@...@ +| 3824: 00 00 80 40 00 00 a0 40 00 00 c0 40 00 00 e0 40 ...@...@...@...@ +| 3840: 00 00 a0 40 00 00 c0 40 00 00 e0 40 00 00 00 41 ...@...@...@...A +| 3856: 00 00 c0 40 00 00 e0 40 00 00 00 41 00 00 10 41 ...@...@...A...A +| 3872: 00 00 e0 40 00 00 00 41 00 00 10 41 00 00 20 41 ...@...A...A.. A +| 3888: 00 00 00 41 00 00 10 41 00 00 20 41 00 00 30 41 ...A...A.. A..0A +| 3904: 00 00 10 41 00 00 20 41 00 00 30 41 00 00 40 41 ...A.. A..0A..@A +| 3920: 00 00 20 41 00 00 30 41 00 00 40 41 00 00 50 41 .. A..0A..@A..PA +| 3936: 00 00 30 41 00 00 40 41 00 00 50 41 00 00 60 41 ..0A..@A..PA..`A +| 3952: 00 00 40 41 00 00 50 41 00 00 60 41 00 00 70 41 ..@A..PA..`A..pA +| 3968: 00 00 50 41 00 00 60 41 00 00 70 41 00 00 80 41 ..PA..`A..pA...A +| 3984: 00 00 60 41 00 00 70 41 00 00 80 41 00 00 88 41 ..`A..pA...A...A +| 4000: 00 00 70 41 00 00 80 41 00 00 88 41 00 00 90 41 ..pA...A...A...A +| 4016: 00 00 80 41 00 00 88 41 00 00 90 41 00 00 98 41 ...A...A...A...A +| 4032: 00 00 88 41 00 00 90 41 00 00 98 41 00 00 a0 41 ...A...A...A...A +| 4048: 00 00 90 41 00 00 98 41 00 00 a0 41 00 00 a8 41 ...A...A...A...A +| 4064: 00 00 98 41 00 00 a0 41 00 00 a8 41 00 00 b0 41 ...A...A...A...A +| 4080: 00 00 a0 41 00 00 a8 41 00 00 b0 41 00 00 b8 41 ...A...A...A...A +| page 5 offset 16384 +| 0: 0a 00 00 00 01 0f f9 00 0f f9 00 00 00 00 00 00 ................ +| 4080: 00 00 00 00 00 00 00 00 00 06 05 08 09 01 09 14 ................ +| page 6 offset 20480 +| 0: 0d 00 00 00 01 0f e3 00 0f e3 00 00 00 00 00 00 ................ +| 4064: 00 00 00 1b 01 03 00 3c 00 00 00 04 00 00 00 01 .......<........ +| 4080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 ................ +| page 7 offset 24576 +| 0: 0d 00 00 00 01 0f f2 00 0f f2 00 00 00 00 00 00 ................ +| 3920: 81 2c 82 00 04 00 82 5c 00 00 00 08 00 00 00 14 .,.............. +| 3936: 40 24 00 00 00 00 00 00 40 34 00 00 00 00 00 00 @$......@4...... +| 3952: 40 3e 00 00 00 00 00 00 40 44 00 00 00 00 00 00 @>......@D...... +| 3968: 40 49 00 00 00 00 00 00 40 4e 00 00 00 00 00 00 @I......@N...... +| 3984: 40 51 80 00 00 00 00 00 40 54 00 00 00 00 00 00 @Q......@T...... +| 4000: 40 56 80 00 00 00 00 00 40 59 00 00 00 00 00 00 @V......@Y...... +| 4016: 40 5b 80 00 00 00 00 00 40 5e 00 00 00 00 00 00 @[......@^...... +| 4032: 40 60 40 00 00 00 00 00 40 61 80 00 00 00 00 00 @`@.....@a...... +| 4048: 40 62 c0 00 00 00 00 00 40 64 00 00 00 00 00 00 @b......@d...... +| 4064: 40 65 40 00 00 00 00 00 40 66 80 00 00 00 00 00 @e@.....@f...... +| 4080: 40 67 0b 82 00 03 00 1c 00 00 00 08 00 00 00 00 @g.............. +| end poc_oob_read.db +}]} {} + +vec1_load db + +do_catchsql_test 2.1 { + SELECT rowid FROM t(vec1_from_json('[1,2,3,4]'), 10) WHERE m > 5.0; +} {1 {database disk image is malformed}} + +reset_db +do_test 3.0 { + sqlite3 db {} + db deserialize [decode_hexdb { +| size 28672 pagesize 4096 filename poc_oob_write.db +| page 1 offset 0 +| 0: 53 51 4c 69 74 65 20 66 6f 72 6d 61 74 20 33 00 SQLite format 3. +| 16: 10 00 01 01 00 40 20 20 00 00 00 04 00 00 00 07 .....@ ........ +| 32: 00 00 00 00 00 00 00 00 00 00 00 07 00 00 00 04 ................ +| 48: 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 ................ +| 80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 04 ................ +| 96: 00 2e 99 b0 0d 00 00 00 07 0d a6 00 0f ca 0f 74 ...............t +| 112: 0f 1b 0e 9f 0e 4b 0d f7 0d a6 00 00 00 00 00 00 .....K.......... +| 3488: 00 00 00 00 00 00 4f 07 06 17 19 19 01 7b 74 61 ......O.......ta +| 3504: 62 6c 65 74 5f 6d 65 74 61 74 5f 6d 65 74 61 07 blet_metat_meta. +| 3520: 43 52 45 41 54 45 20 54 41 42 4c 45 20 27 74 5f CREATE TABLE 't_ +| 3536: 6d 65 74 61 27 28 69 64 20 49 4e 54 45 47 45 52 meta'(id INTEGER +| 3552: 20 50 52 49 4d 41 52 59 20 4b 45 59 2c 20 76 61 PRIMARY KEY, va +| 3568: 6c 20 42 4c 4f 42 29 52 06 06 17 1b 1b 01 7d 74 l BLOB)R.......t +| 3584: 61 62 6c 65 74 5f 6d 6f 64 65 6c 74 5f 6d 6f 64 ablet_modelt_mod +| 3600: 65 6c 06 43 52 45 41 54 45 20 54 41 42 4c 45 20 el.CREATE TABLE +| 3616: 27 74 5f 6d 6f 64 65 6c 27 28 69 64 20 49 4e 54 't_model'(id INT +| 3632: 45 47 45 52 20 50 52 49 4d 41 52 59 20 4b 45 59 EGER PRIMARY KEY +| 3648: 2c 20 76 61 6c 20 42 4c 4f 42 29 52 05 06 17 1f , val BLOB)R.... +| 3664: 17 01 7d 69 6e 64 65 78 74 5f 69 64 78 5f 69 64 ...indext_idx_id +| 3680: 78 74 5f 69 64 78 05 43 52 45 41 54 45 20 49 4e xt_idx.CREATE IN +| 3696: 44 45 58 20 27 74 5f 69 64 78 5f 69 64 78 27 20 DEX 't_idx_idx' +| 3712: 4f 4e 20 27 74 5f 69 64 78 27 28 62 75 63 6b 65 ON 't_idx'(bucke +| 3728: 74 2c 20 66 69 72 73 74 2c 20 6c 61 73 74 29 7a t, first, last)z +| 3744: 04 07 17 17 17 01 81 53 74 61 62 6c 65 74 5f 69 .......Stablet_i +| 3760: 64 78 74 5f 69 64 78 04 43 52 45 41 54 45 20 54 dxt_idx.CREATE T +| 3776: 41 42 4c 45 20 27 74 5f 69 64 78 27 28 69 64 20 ABLE 't_idx'(id +| 3792: 49 4e 54 45 47 45 52 20 50 52 49 4d 41 52 59 20 INTEGER PRIMARY +| 3808: 4b 45 59 2c 20 62 75 63 6b 65 74 20 49 4e 54 45 KEY, bucket INTE +| 3824: 47 45 52 2c 20 66 69 72 73 74 20 49 4e 54 45 47 GER, first INTEG +| 3840: 45 52 2c 20 6c 61 73 74 20 49 4e 54 45 47 45 52 ER, last INTEGER +| 3856: 2c 20 76 61 6c 20 42 4c 4f 42 29 57 03 07 17 19 , val BLOB)W.... +| 3872: 19 01 81 09 74 61 62 6c 65 74 5f 62 61 73 65 74 ....tablet_baset +| 3888: 5f 62 61 73 65 03 43 52 45 41 54 45 20 54 41 42 _base.CREATE TAB +| 3904: 4c 45 20 27 74 5f 62 61 73 65 27 28 69 64 20 49 LE 't_base'(id I +| 3920: 4e 54 45 47 45 52 20 50 52 49 4d 41 52 59 20 4b NTEGER PRIMARY K +| 3936: 45 59 2c 20 76 65 63 74 6f 72 20 42 4c 4f 42 2c EY, vector BLOB, +| 3952: 20 63 30 29 54 02 06 17 1d 1d 01 7d 74 61 62 6c c0)T.......tabl +| 3968: 65 74 5f 63 6f 6e 66 69 67 74 5f 63 6f 6e 66 69 et_configt_confi +| 3984: 67 02 43 52 45 41 54 45 20 54 41 42 4c 45 20 27 g.CREATE TABLE ' +| 4000: 74 5f 63 6f 6e 66 69 67 27 28 69 64 20 49 4e 54 t_config'(id INT +| 4016: 45 47 45 52 20 50 52 49 4d 41 52 59 20 4b 45 59 EGER PRIMARY KEY +| 4032: 2c 20 76 61 6c 20 41 4e 59 29 34 01 06 17 0f 0f , val ANY)4..... +| 4048: 08 5b 74 61 62 6c 65 74 74 43 52 45 41 54 45 20 .[tablettCREATE +| 4064: 56 49 52 54 55 41 4c 20 54 41 42 4c 45 20 74 20 VIRTUAL TABLE t +| 4080: 55 53 49 4e 47 20 76 65 63 31 28 76 2c 20 6d 29 USING vec1(v, m) +| page 2 offset 4096 +| 0: 0d 00 00 00 03 0f ef 00 0f f5 0f ef 0f fa 00 00 ................ +| 4064: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 04 ................ +| 4080: 02 03 00 01 04 03 00 03 00 09 04 05 03 00 01 04 ................ +| page 3 offset 8192 +| 0: 0d 00 00 00 01 0f f2 00 0f f2 00 00 00 00 00 00 ................ +| 4080: 00 00 0c 01 04 00 08 07 40 45 00 00 00 00 00 00 ........@E...... +| page 4 offset 12288 +| 0: 0d 00 00 00 01 0f d8 00 0f d8 00 00 00 00 00 00 ................ +| 4048: 00 00 00 00 00 00 00 00 26 01 06 00 08 09 09 4c ........&......L +| 4064: 00 00 00 02 00 00 00 01 00 00 00 00 00 00 00 01 ................ +| 4080: 00 00 80 3f 00 00 00 40 00 00 40 40 00 00 80 40 ...?...@..@@...@ +| page 5 offset 16384 +| 0: 0a 00 00 00 01 0f fa 00 0f fa 00 00 00 00 00 00 ................ +| 4080: 00 00 00 00 00 00 00 00 00 00 05 05 08 09 09 09 ................ +| page 6 offset 20480 +| 0: 0d 00 00 00 01 0f e3 00 0f e3 00 00 00 00 00 00 ................ +| 4064: 00 00 00 1b 01 03 00 3c 00 00 00 04 00 00 00 01 .......<........ +| 4080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 ................ +| page 7 offset 24576 +| 0: 0d 00 00 00 01 0e 2e 00 0e 2e 00 00 00 00 00 00 ................ +| 3616: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 83 4e ...............N +| 3632: 82 00 04 00 87 20 00 00 00 09 00 00 00 00 04 41 ..... .........A +| 3648: 41 41 41 41 41 41 41 04 41 41 41 41 41 41 41 41 AAAAAAA.AAAAAAAA +| 3664: 04 41 41 41 41 41 41 41 41 04 41 41 41 41 41 41 .AAAAAAAA.AAAAAA +| 3680: 41 41 04 41 41 41 41 41 41 41 41 04 41 41 41 41 AA.AAAAAAAA.AAAA +| 3696: 41 41 41 41 04 41 41 41 41 41 41 41 41 04 41 41 AAAA.AAAAAAAA.AA +| 3712: 41 41 41 41 41 41 04 41 41 41 41 41 41 41 41 04 AAAAAA.AAAAAAAA. +| 3728: 41 41 41 41 41 41 41 41 04 41 41 41 41 41 41 41 AAAAAAAA.AAAAAAA +| 3744: 41 04 41 41 41 41 41 41 41 41 04 41 41 41 41 41 A.AAAAAAAA.AAAAA +| 3760: 41 41 41 04 41 41 41 41 41 41 41 41 04 41 41 41 AAA.AAAAAAAA.AAA +| 3776: 41 41 41 41 41 04 41 41 41 41 41 41 41 41 04 41 AAAAA.AAAAAAAA.A +| 3792: 41 41 41 41 41 41 41 04 41 41 41 41 41 41 41 41 AAAAAAA.AAAAAAAA +| 3808: 04 41 41 41 41 41 41 41 41 04 41 41 41 41 41 41 .AAAAAAAA.AAAAAA +| 3824: 41 41 04 41 41 41 41 41 41 41 41 04 41 41 41 41 AA.AAAAAAAA.AAAA +| 3840: 41 41 41 41 04 41 41 41 41 41 41 41 41 04 41 41 AAAA.AAAAAAAA.AA +| 3856: 41 41 41 41 41 41 04 41 41 41 41 41 41 41 41 04 AAAAAA.AAAAAAAA. +| 3872: 41 41 41 41 41 41 41 41 04 41 41 41 41 41 41 41 AAAAAAAA.AAAAAAA +| 3888: 41 04 41 41 41 41 41 41 41 41 04 41 41 41 41 41 A.AAAAAAAA.AAAAA +| 3904: 41 41 41 04 41 41 41 41 41 41 41 41 04 41 41 41 AAA.AAAAAAAA.AAA +| 3920: 41 41 41 41 41 04 41 41 41 41 41 41 41 41 04 41 AAAAA.AAAAAAAA.A +| 3936: 41 41 41 41 41 41 41 04 41 41 41 41 41 41 41 41 AAAAAAA.AAAAAAAA +| 3952: 04 41 41 41 41 41 41 41 41 04 41 41 41 41 41 41 .AAAAAAAA.AAAAAA +| 3968: 41 41 04 41 41 41 41 41 41 41 41 04 41 41 41 41 AA.AAAAAAAA.AAAA +| 3984: 41 41 41 41 04 41 41 41 41 41 41 41 41 04 41 41 AAAA.AAAAAAAA.AA +| 4000: 41 41 41 41 41 41 04 41 41 41 41 41 41 41 41 04 AAAAAA.AAAAAAAA. +| 4016: 41 41 41 41 41 41 41 41 04 41 41 41 41 41 41 41 AAAAAAAA.AAAAAAA +| 4032: 41 04 41 41 41 41 41 41 41 41 04 41 41 41 41 41 A.AAAAAAAA.AAAAA +| 4048: 41 41 41 04 41 41 41 41 41 41 41 41 04 41 41 41 AAA.AAAAAAAA.AAA +| 4064: 41 41 41 41 41 04 41 41 41 41 41 41 41 41 04 41 AAAAA.AAAAAAAA.A +| 4080: 41 41 41 41 41 41 41 04 41 41 41 41 41 41 41 41 AAAAAAA.AAAAAAAA +| end poc_oob_write.db +}]} {} +vec1_load db +do_catchsql_test 3.1 { + INSERT INTO t(rowid, v, m) + VALUES (2, vec1_from_json('[5.0,6.0,7.0,8.0]'), 99.0); +} {1 {database disk image is malformed}} +#------------------------------------------------------------------------- +# Tests for corrupt GENERIC format meta-value arrays. Each test case builds +# a table with a single list in %_idx and a GENERIC meta-value array in +# %_meta, corrupts the meta-value array using an SQL UPDATE, then attempts +# one of the following: +# +# .1 a query with a meta-value filter (vec1DoMetaFilters()), +# .2 PRAGMA integrity_check, +# .3 an INSERT that appends to the existing list (vec1ListBuilderLoad() +# followed by vec1WriteMeta()), +# .4 two DELETEs, the second of which causes the list to be compacted +# (vec1ListBuilderCompress()). +# +# GENERIC meta-value arrays are not checked when they are loaded. Corruption +# is only reported if an operation actually trips over it while parsing the +# array. So: +# +# * The INSERT (.3) never parses the existing array, so it succeeds +# unless the 8-byte header itself is corrupt. +# +# * Corruption that leaves every value parseable within the bounds of +# the array, such as extra data following the last value, is not +# reported by .1 or .4. PRAGMA integrity_check (.2) only reports it if +# the values it parses do not match those in %_base. +# +# The meta-value array contains one value of each type. Its layout, with +# 1-based byte offsets, is: +# +# 1..8 header (flags=GENERIC|HASNULL, nEntry=7) +# 9..12 'one' 0B 6F6E65 +# 13..14 2 01 02 +# 15..23 3.5 04 400C000000000000 +# 24 NULL 00 +# 25..27 x'0102' 0A 0102 +# 28..32 100000 02 000186A0 +# 33..41 10000000000 03 00000002540BE400 +# +proc vec1corrupt_build {} { + reset_db + vec1_load db + execsql { + CREATE VIRTUAL TABLE v1 USING vec1(v, m); + INSERT INTO v1(rowid, v, m) VALUES + (1, vec1_from_json('[1,0,0,0]'), 'one'), + (2, vec1_from_json('[0,1,0,0]'), 2), + (3, vec1_from_json('[0,0,1,0]'), 3.5), + (4, vec1_from_json('[0,0,0,1]'), NULL), + (5, vec1_from_json('[1,1,0,0]'), x'0102'), + (6, vec1_from_json('[1,1,1,0]'), 100000), + (7, vec1_from_json('[1,1,1,1]'), 10000000000); + INSERT INTO v1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); + } +} +# Return the varint encoding of integer $v, as written by vec1PutVarint(), +# as a hex string. +proc vec1corrupt_varint {v} { + set res [format %02X [expr {$v & 0x7F}]] + set v [expr {$v >> 7}] + while {$v>0} { + set res "[format %02X [expr {($v & 0x7F) | 0x80}]]$res" + set v [expr {$v >> 7}] + } + set res +} +# Expected results for the 4 operations described above. +# +# vec1corrupt_ok: results for an array that is either not corrupt, +# or is corrupt in a way that is never tripped over. +# +# vec1corrupt_parse: results for an array with a valid header, but that +# cannot be parsed. Everything except the INSERT +# reports corruption. +# +# vec1corrupt_header: results for an array with a corrupt header. All +# 4 operations report corruption. +# +set vec1corrupt_malformed {1 {database disk image is malformed}} +set vec1corrupt_intck [list "v1: error reading meta-list id=1,meta=0"] +set vec1corrupt_ok [list {0 1} ok {0 {}} {0 {}}] +set vec1corrupt_parse [list \ + $vec1corrupt_malformed $vec1corrupt_intck {0 {}} $vec1corrupt_malformed +] +set vec1corrupt_header [list \ + $vec1corrupt_malformed $vec1corrupt_intck \ + $vec1corrupt_malformed $vec1corrupt_malformed +] +# Run the 4 operations described above against the meta-value array +# produced by SQL expression $expr (which may refer to the uncorrupted +# array as "val"). Test case names are $tn.1 through $tn.4. Parameter +# $lExpect is a list of the 4 expected results. +proc vec1corrupt_generic_test {tn expr lExpect} { + set corrupt "UPDATE v1_meta SET val = ($expr) WHERE id=256" + + vec1corrupt_build + execsql $corrupt + do_catchsql_test $tn.1 { + SELECT rowid FROM v1(vec1_from_json('[1,0,0,0]'), 10) WHERE m='one' + } [lindex $lExpect 0] + + vec1corrupt_build + execsql $corrupt + do_execsql_test $tn.2 { + PRAGMA integrity_check + } [lindex $lExpect 1] + + vec1corrupt_build + execsql $corrupt + do_catchsql_test $tn.3 { + INSERT INTO v1(rowid, v, m) VALUES(8, vec1_from_json('[1,2,3,4]'), 'eight') + } [lindex $lExpect 2] + + vec1corrupt_build + execsql $corrupt + do_catchsql_test $tn.4 { + DELETE FROM v1 WHERE rowid=1; + DELETE FROM v1 WHERE rowid=2; + } [lindex $lExpect 3] +} + +# Check the layout of the meta-value array is as described above. And +# that the uncorrupted array works. +vec1corrupt_build +do_execsql_test 4.0 { + SELECT id, hex(val) FROM v1_meta; +} {256 + 00000011000000070B6F6E65010204400C000000000000000A010202000186A00300000002540BE400 +} +vec1corrupt_generic_test 4.1 { val } $vec1corrupt_ok + +# Truncate the array at every possible point. Truncating it to less than +# 8 bytes corrupts the header. +for {set n 0} {$n<41} {incr n} { + set lExpect [expr {$n<8 ? $vec1corrupt_header : $vec1corrupt_parse}] + vec1corrupt_generic_test 4.2.$n " + unhex(substr(hex(val), 1, $n*2)) + " $lExpect +} + +# Extra data following the last value. This is never tripped over. +vec1corrupt_generic_test 4.3.1 { unhex(hex(val) || '00') } $vec1corrupt_ok +vec1corrupt_generic_test 4.3.2 { + unhex(hex(val) || '0B6F6E65') +} $vec1corrupt_ok + +# Bad text/blob length varints for the first value. Lengths that are too +# small, too large, negative when cast to a 32-bit int, or a varint that +# runs off the end of the array. +# +# Case 9 is a length just smaller than 2^31. When added to the offset of +# the value within the array (8), plus the size of the varint (5), this +# overflows a 32-bit signed int. +# +# Case 2 makes the first value 1 byte longer than it should be. This +# misaligns the rest of the array, but by chance all 7 values can still +# be parsed without running off the end of it. So the query and DELETE +# succeed, and integrity_check only notices because the parsed values do +# not match those in %_base. +set vec1corrupt_misaligned [list \ + {0 {}} [list "v1: meta-value value mismatch for row 1"] {0 {}} {0 {}} +] +foreach {tn varint lExpect} [list \ + 1 09 $vec1corrupt_parse \ + 2 0D $vec1corrupt_misaligned \ + 3 8F7F $vec1corrupt_parse \ + 4 FF $vec1corrupt_parse \ + 5 [vec1corrupt_varint [expr {0x1FFFFFFFF}]] $vec1corrupt_parse \ + 6 [vec1corrupt_varint [expr {2*0xFFFFFF00 + 5}]] $vec1corrupt_parse \ + 7 [vec1corrupt_varint [expr {2*0x7FFFFFFF + 5}]] $vec1corrupt_parse \ + 8 FFFFFFFFFFFFFFFFFF7F $vec1corrupt_parse \ + 9 [vec1corrupt_varint [expr {2*0x7FFFFFF6 + 5}]] $vec1corrupt_parse \ +] { + vec1corrupt_generic_test 4.4.$tn " + unhex(substr(hex(val), 1, 16) || '$varint' || substr(hex(val), 19)) + " $lExpect +} + +# Bad blob length varint for the 5th value. +foreach {tn varint} [list \ + 1 08 \ + 2 [vec1corrupt_varint [expr {2*0xFFFFFF00 + 6}]] \ +] { + vec1corrupt_generic_test 4.5.$tn " + unhex(substr(hex(val), 1, 48) || '$varint' || substr(hex(val), 51)) + " $vec1corrupt_parse +} + +# A varint that is not terminated before the end of the array. +vec1corrupt_generic_test 4.6.1 { + unhex(substr(hex(val), 1, 16) || 'FFFFFFFF') +} $vec1corrupt_parse +vec1corrupt_generic_test 4.6.2 { + unhex(substr(hex(val), 1, 64) || 'FFFFFFFFFFFFFFFFFF') +} $vec1corrupt_parse + +#------------------------------------------------------------------------- +# A vector in the %_base table that is shorter than the model's vector +# size. The integrity-check must detect this as corruption. +# +reset_db +vec1_load db + +do_execsql_test 5.0 { + CREATE VIRTUAL TABLE v1 USING vec1; + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1000 + ) + INSERT INTO v1(rowid, vector) SELECT i, random_vector(128) FROM s; +} + +do_test 5.1 { + set T [db one { + SELECT vec1_train(vector, '{codesize:16, distance:"cos"}') FROM v1 + }] + execsql { INSERT INTO v1(cmd, vector) VALUES('rebuild', $T) } +} {} + +do_execsql_test 5.2 { + PRAGMA integrity_check +} {ok} + +# Truncate the vector stored in one %_base row to 4 bytes. Without the +# length check in the right place, the integrity-check below reads 128 +# floats from this 4 byte buffer. +do_execsql_test 5.3 { + UPDATE v1_base SET vector = x'00000000' WHERE id=1; +} {} + +do_execsql_test 5.4 { + PRAGMA integrity_check +} {{v1: vector in %_base row 1 is wrong size}} finish_test - diff --git a/ext/vec1/tool/vec1_algo_test.c b/ext/vec1/tool/vec1_algo_test.c index 42de52f242..001be12d23 100644 --- a/ext/vec1/tool/vec1_algo_test.c +++ b/ext/vec1/tool/vec1_algo_test.c @@ -645,7 +645,7 @@ static void xLloydsTiled( } for(; iin+nByte>pBuf->nAlloc ){ + if( nByte>pBuf->nAlloc-pBuf->n ){ sqlite3_int64 nNew = pBuf->nAlloc; unsigned char *aNew = 0; @@ -2020,7 +2020,7 @@ static float *vec1TrainingVector(Vec1TrainVectors *p, int iVec){ ** nCode vectors, each of nCodeElem elements. Return the index of the best ** match for aSub in aCodebook[]. */ -static int vec1PqBestMatch( +static int vec1BestMatchSimple( const float *aCodebook, /* Codebook of nCodeElem element vectors */ int nCode, /* Number of vectors in aCodebook */ const float *aSub, /* nCodeElem element vector */ @@ -2044,6 +2044,21 @@ static int vec1PqBestMatch( return iBest; } +/* +** Relative tolerance used by vec1BucketIsNearTie(). +*/ +#define VEC1_BUCKET_TOLERANCE 0.00001 + +/* +** fBest is the distance from a vector to its nearest IVF centroid, as +** found by vec1BestMatchSimple(). fDist is the distance from the same +** vector to some other centroid. Return true if the two distances are +** close enough that rounding differences could account for the difference. +*/ +static int vec1BucketIsNearTie(double fBest, double fDist){ + return (fDist - fBest) <= (fBest * VEC1_BUCKET_TOLERANCE); +} + /* ** Return a pseudo-random positive 32-bit value. */ @@ -2698,10 +2713,10 @@ static void vec1LloydsWork(void *pCtx){ } #if 0 - /* Check (assert) using vec1PqBestMatch() that the fancy code above + /* Check (assert) using vec1BestMatchSimple() that the fancy code above ** actually worked. */ for(vv=0; vvaCentroid, p->nK, aVec[vv], nElem, 0 ); if( iBest2!=aBest[vv] ){ @@ -2722,7 +2737,9 @@ static void vec1LloydsWork(void *pCtx){ ** part of a batch of VEC1_MULTIMATCH_NVEC. */ for(; iiiEof; ii++){ const float *aSub = vec1TrainingVector(pVec, ii); - int iBest = vec1PqBestMatch(p->aCentroid, p->nK, aSub, nElem, &p->fTotal); + int iBest = vec1BestMatchSimple( + p->aCentroid, p->nK, aSub, nElem, &p->fTotal + ); assert( iBest>=0 && iBestnK ); p->aCount[iBest]++; vec1AddInPlace(&p->aSum[iBest * nElem], aSub, nElem); @@ -4418,7 +4435,7 @@ static void vec1CovarianceWork(void *pArg){ for(; iipVec, ii); const float *aSub = &aVec[iSubOff]; - int iBest = vec1PqBestMatch( + int iBest = vec1BestMatchSimple( aCode, VEC1_PQ_CODEBOOK_SZ, aSub, pJob->nCodeElem, &pJob->fTotalDist ); float *aSubHat = &aCode[iBest * pJob->nCodeElem]; @@ -4882,7 +4899,7 @@ static void vec1TrainResidualWorker(void *pCtx){ for(ii=p->iFirst; iiiEof; ii++){ float *a = vec1TrainingVector(p->pVec, ii); - int iBest = vec1PqBestMatch(p->aCentroid, p->nCentroid, a, nElem, 0); + int iBest = vec1BestMatchSimple(p->aCentroid, p->nCentroid, a, nElem, 0); vec1SubInPlace(a, &p->aCentroid[iBest * nElem], nElem); } } @@ -5421,6 +5438,11 @@ static void vec1ModelFreeTCent(Vec1Model *pMod){ #define VEC1_META_4BYTEMAX +2147483646 #define VEC1_META_4BYTENULL +2147483647 +/* When a meta-value array is loaded from disk, at least VEC1_META_PADDING +** extra bytes of memory are allocated. This is to allow readers to safely +** overread the record slightly if it is corrupt. */ +#define VEC1_META_PADDING 20 + typedef struct Vec1Tab Vec1Tab; typedef struct Vec1Csr Vec1Csr; @@ -7020,13 +7042,21 @@ static int vec1PutVarint(u8 *aBuf, u64 v){ } static int vec1GetVarint(const u8 *aBuf, u64 *piVal){ - int nRet = 0; u64 out = 0; - do { - out = (out<<7) + (aBuf[nRet] & 0x7F); - } while( aBuf[nRet++] & 0x80 ); + int ii = 0; + + out = (aBuf[0] & 0x7F); + if( aBuf[0] & 0x80 ){ + for(ii=1; ii<9; ii++){ + out = (out<<7) + (aBuf[ii] & 0x7F); + if( (aBuf[ii] & 0x80)==0 ){ + break; + } + } + } + *piVal = out; - return nRet; + return ii+1; } /* @@ -7122,6 +7152,7 @@ static void vec1BinaryQuantLUTBuild( */ static int vec1ReadMeta( Vec1Tab *pTab, /* Vec1 virtual table */ + u32 nExpect, /* Expected number of entries */ Vec1Buffer *pBuf, /* Buffer to read into */ i64 iId, /* Id of %_idx row */ int iMeta /* Index of meta-column */ @@ -7137,18 +7168,47 @@ static int vec1ReadMeta( sqlite3_bind_int64(pStmt, 1, iMetaId); if( SQLITE_ROW==sqlite3_step(pStmt) ){ int nByte = sqlite3_column_bytes(pStmt, 0); - rc = vec1BufferGrow(pBuf, nByte); + rc = vec1BufferGrow(pBuf, nByte + VEC1_META_PADDING); if( rc==SQLITE_OK ){ const u8 *a = sqlite3_column_blob(pStmt, 0); memcpy(pBuf->a, a, nByte); + memset(&pBuf->a[nByte], 0, VEC1_META_PADDING); pBuf->n = nByte; } } - vec1StmtReset(&rc, pStmt); - if( rc==SQLITE_OK && pBuf->n==0 ){ - rc = VEC1_CORRUPT; + + /* Check, so far as is possible, that the meta-value blob is the + ** expected size. It is not possible to check if the meta-value blob + ** uses the GENERIC format, due to the variable length fields. Code + ** that handles GENERIC meta-value blobs has to dynamically verify + ** the buffer size. */ + if( rc==SQLITE_OK ){ + if( pBuf->na[0]) & VEC1_META_TYPEMASK); + u32 nEntry = vec1GetU32(&pBuf->a[4]); + i64 sz = 0; + if( nEntry!=nExpect ){ + rc = VEC1_CORRUPT; + } + if( tflag==VEC1_META_1BYTEINT ){ + sz = 1; + }else if( tflag==VEC1_META_4BYTEINT ){ + sz = sizeof_u32; + }else if( tflag==VEC1_META_REAL ){ + sz = sizeof_f64; + }else if( tflag!=VEC1_META_GENERIC ){ + rc = VEC1_CORRUPT; + } + + if( sz && pBuf->n!=(VEC1_META_SZHDR + sz*nEntry) ){ + rc = VEC1_CORRUPT; + } + } } + } return rc; @@ -7162,11 +7222,12 @@ static int vec1ReadMeta( ** 4: Real value. 8 byte payload. */ static int vec1MetaValueRead( - Vec1Buffer *pMeta, - int *piOff, + Vec1Buffer *pMeta, + int *piOff, Vec1MetaValue *pVal ){ - int iOff = *piOff; + i64 iOff = (i64)*piOff; + switch( pMeta->a[iOff] ){ case 0: { pVal->eType = SQLITE_NULL; @@ -7207,14 +7268,18 @@ static int vec1MetaValueRead( u64 eType = 0; iOff += vec1GetVarint(&pMeta->a[iOff], &eType); pVal->pPtr = &pMeta->a[iOff]; - pVal->iVal = (eType-5) / 2; - iOff += (int)pVal->iVal; + pVal->iVal = (((eType-5) / 2) & (u64)0xFFFFFFFF); + iOff += pVal->iVal; pVal->eType = (eType & 0x01) ? SQLITE_TEXT : SQLITE_BLOB; break; } } - *piOff = iOff; + if( iOff>pMeta->n || iOff<0 ){ + return VEC1_CORRUPT; + } + + *piOff = (int)iOff; return SQLITE_OK; } @@ -7907,7 +7972,7 @@ static VEC1_NOINLINE int vec1DoMetaFilters( for(ii=0; rc==SQLITE_OK && iinFilter; ii++){ Vec1Filter *pFilter = &pQuery->aFilter[ii]; VEC1_QINSTR_START(pTab, VEC1_QINSTR_METAREAD); - rc = vec1ReadMeta(pTab, pMeta, iId, pFilter->iMeta); + rc = vec1ReadMeta(pTab, nEntry, pMeta, iId, pFilter->iMeta); VEC1_QINSTR_STOP(pTab, VEC1_QINSTR_METAREAD); VEC1_QINSTR_START(pTab, VEC1_QINSTR_METASCAN); @@ -7944,7 +8009,8 @@ static VEC1_NOINLINE int vec1DoMetaFilters( int iOff = VEC1_META_SZHDR; for(jj=0; jja[jj / 8] |= (1 << (jj % 8)); } @@ -8042,7 +8108,7 @@ static void vec1EncodeVectorPQSimple( for(ii=0; iiaModel[ii*nCodeElem*VEC1_PQ_CODEBOOK_SZ]; const float *aSub = &aVec[ii*nCodeElem]; - aOut[ii] = (u8)vec1PqBestMatch( + aOut[ii] = (u8)vec1BestMatchSimple( pCodebook, VEC1_PQ_CODEBOOK_SZ, aSub, nCodeElem, pfTotalDist ); } @@ -9476,7 +9542,7 @@ static void vec1DistanceStats(sqlite3_context *ctx, Vec1Csr *pCsr){ /* Find bucket and coarse error if applicable. */ if( pMod->hdr.nBucket>1 ){ - iBucket = vec1PqBestMatch( + iBucket = vec1BestMatchSimple( pMod->aCentroid, pMod->hdr.nBucket, aVec, nElem, &fCoarseError ); if( aTransform!=pTab->aTmpVec ){ @@ -9805,7 +9871,8 @@ static int vec1WriteMeta( if( rc!=SQLITE_OK ) return rc; while( iInbuf.n ){ - vec1MetaValueRead(&p->buf, &iIn, &val); + rc = vec1MetaValueRead(&p->buf, &iIn, &val); + if( rc!=SQLITE_OK ) break; if( val.eType==SQLITE_NULL ){ val.iVal = iNull; }else if( p->format==VEC1_META_REAL ){ @@ -9839,7 +9906,7 @@ static int vec1WriteMeta( ** A serialized meta-value is stored in buffer aBuf. Return the size in ** bytes of the meta value. */ -static int vec1MetaValueSize(const u8 *aBuf){ +static i64 vec1MetaValueSize(const u8 *aBuf){ switch( aBuf[0] ){ case 0: return 1; /* NULL */ case 1: return 2; /* 1-byte integer */ @@ -9855,27 +9922,11 @@ static int vec1MetaValueSize(const u8 *aBuf){ */ i64 V = 0; int n = vec1GetVarint(aBuf, (u64*)&V); - return (int)(((V-5)/2) + n); + return (((V-5)/2) & (i64)0xFFFFFFFF) + n; } } } -#ifndef NDEBUG -/* -** This may be used in assert() statements to check that the meta-value -** list contains the expected number of elements. -*/ -static int vec1MetaValueCheck(Vec1Buffer *pBuf, int nEntry){ - int iOff = 0; - int ii; - for(ii=0; iia[iOff]); - } - assert( iOff==pBuf->n ); - return 1; -} -#endif - static int vec1ListBuilderLoad( Vec1ListBuilder *p, i64 iId, @@ -9932,7 +9983,7 @@ static int vec1ListBuilderLoad( /* Load the array for each meta-data column from the %_meta table */ for(ii=0; rc==SQLITE_OK && iinMeta; ii++){ Vec1MetaBuilder *pMeta = &p->aMeta[ii]; - rc = vec1ReadMeta(pTab, &pMeta->buf, iId, ii); + rc = vec1ReadMeta(pTab, nEntry, &pMeta->buf, iId, ii); if( rc==SQLITE_OK ){ u32 f = vec1GetU32(pMeta->buf.a); pMeta->format = (f & VEC1_META_TYPEMASK); @@ -10159,9 +10210,12 @@ static int vec1ListBuilderCompress( Vec1MetaBuilder *pMeta = &p->aMeta[iMeta]; u8 *a = pMeta->buf.a; - if( pMeta->format==VEC1_META_GENERIC ){ - int nIn = vec1MetaValueSize(&a[aMetaIn[iMeta]]); + i64 nIn = vec1MetaValueSize(&a[aMetaIn[iMeta]]); + if( nIn<=0 || nIn>pMeta->buf.n-aMetaIn[iMeta] ){ + sqlite3_free(aMetaIn); + return VEC1_CORRUPT; + } if( bTombstone==0 ){ if( aMetaIn[iMeta]!=aMetaOut[iMeta] ){ memmove(&a[ aMetaOut[iMeta] ], &a[ aMetaIn[iMeta] ], nIn); @@ -10380,7 +10434,7 @@ static void vec1QuantizeVector( aVec = vec1TransformInputVector(pMod, aTmp, aVector); if( nBucket>0 ){ int nElem = pMod->hdr.nElem; - iBucket = vec1PqBestMatch(pMod->aCentroid, nBucket, aVec, nElem, 0); + iBucket = vec1BestMatchSimple(pMod->aCentroid, nBucket, aVec, nElem, 0); if( pMod->hdr.nCodebook>0 && (pMod->hdr.flags & VEC1_MODEL_RESIDUAL) ){ vec1Sub(aTmp, aVec, &pMod->aCentroid[iBucket*nElem], nElem); aVec = aTmp; @@ -10758,6 +10812,10 @@ static int vec1WriterMetaFromStmt( /* ** Read nMeta values from buffer pBuf. Use them as the meta values associated ** with the (compressed) vector just appended to bucket iBucket. +** +** This is only called on a buffer that has been constructed in process, +** not one loaded from disk, so it is safe to assume that pBuf really does +** contain nMeta well-formed values. */ static int vec1WriterMetaFromPacked( Vec1Writer *pWriter, /* Writer object accumulating changes */ @@ -10769,7 +10827,7 @@ static int vec1WriterMetaFromPacked( int iOff = *piOff; for(ii=0; iipTab->nMeta; ii++){ Vec1MetaBuilder *pTo = &pWriter->aBld[iBucket].aMeta[ii]; - int n = vec1MetaValueSize(&pBuf->a[iOff]); + i64 n = vec1MetaValueSize(&pBuf->a[iOff]); int rc = vec1BufferGrow(&pTo->buf, n); vec1MetaValueUpdateFlags(&pBuf->a[iOff], &pTo->flags); if( rc!=SQLITE_OK ) return rc; @@ -11497,6 +11555,70 @@ static int vec1SpecialInsert( */ #define VEC1_TOMBSTONE_THRESHOLD(nEntry) ((nEntry * 2) / 10) +/* +** Locate the index entry for row iRowid, for writing. Array aVec[] is the +** row's transformed vector. +** +** The bucket selected for aVec[] by vec1BestMatchSimple() is searched +** first. If the model has no buckets, this is bucket 0 and it is the only +** bucket searched. Otherwise, if the row is not found there, it may have +** been assigned to a different bucket by code that rounds differently - +** either a different routine, such as the batched routines used by +** 'rebuild', or the same routine on a different platform. In that case +** all other buckets that are near-ties with the selected bucket (see +** vec1BucketIsNearTie()) are searched, followed by all remaining buckets. +** +** If the row is found, (*pLoc) is populated and (*piBucket) is set to +** the bucket it was found in. If it is not found, (*pLoc) is zeroed and +** SQLITE_OK returned. The caller must then treat the row as missing. +*/ +static int vec1DeleteFindRowid( + Vec1Tab *pTab, /* Table to search */ + const float *aVec, /* Transformed vector */ + i64 iRowid, /* Rowid to locate */ + int *piBucket, /* OUT: Bucket containing iRowid */ + Vec1RowidLocation *pLoc /* OUT: Location of iRowid */ +){ + const Vec1Model *pMod = &pTab->mod; + const int nElem = (int)pMod->hdr.nElem; + const int nBucket = (int)pMod->hdr.nBucket; + int iBest = 0; + double fBest = 0.0; + int rc = SQLITE_OK; + + int iPass; + + if( nBucket>0 ){ + iBest = vec1BestMatchSimple(pMod->aCentroid, nBucket, aVec, nElem, &fBest); + } + rc = vec1FindByRowid(pTab, iBest, 1, iRowid, pLoc); + if( rc!=SQLITE_OK || pLoc->pList || pLoc->pBlob ){ + *piBucket = iBest; + return rc; + } + + /* Pass 0 searches buckets that are near-ties with iBest. Pass 1 searches + ** all others. */ + for(iPass=0; iPass<2; iPass++){ + int ii; + for(ii=0; iiaCentroid[ii*nElem], nElem); + if( vec1BucketIsNearTie(fBest, fDist)==(iPass==0) ){ + rc = vec1FindByRowid(pTab, ii, 1, iRowid, pLoc); + if( rc!=SQLITE_OK || pLoc->pList || pLoc->pBlob ){ + *piBucket = ii; + return rc; + } + } + } + } + } + + return SQLITE_OK; +} + /* ** Delete the %_base entry for the row with rowid=iRowid. Also zero-out ** the corresponding %_idx entry. @@ -11510,37 +11632,34 @@ static int vec1DeleteByRowid(Vec1Tab *pTab, i64 iRowid){ rc = vec1GetSql(pTab, VEC1_SQL_DEL_LOOKUP_BASE, &pDelete); if( rc==SQLITE_OK ){ - int iBucket = 0; + + /* Delete the row from the %_base table. Also set variable iBucket to + ** the index of the bucket containing the vector. Statement pDelete is + ** not reset until the end of this function, as aTransform[] may point + ** to the vector it returns. */ sqlite3_bind_int64(pDelete, 1, iRowid); - if( SQLITE_ROW==sqlite3_step(pDelete) && pTab->mod.hdr.nBucket>0 ){ + if( (SQLITE_ROW==sqlite3_step(pDelete)) + && (pTab->mod.hdr.flags & VEC1_MODEL_INDEX) + ){ + int iBucket = 0; + Vec1RowidLocation loc; + if( pTab->mod.hdr.nCodebook==0 ){ iBucket = sqlite3_column_int(pDelete, 0); + rc = vec1FindByRowid(pTab, iBucket, 1, iRowid, &loc); }else{ const u8 *aVec = sqlite3_column_blob(pDelete, 0); int nVec = sqlite3_column_bytes(pDelete, 0); - const float *aTransform = 0; - - aTransform = vec1TransformInputVector( - &pTab->mod, pTab->aTmpVec, (const float*)aVec - ); if( nVec!=(pTab->cfg.nElem*sizeof_f32) ){ rc = VEC1_CORRUPT; }else{ - iBucket = vec1PqBestMatch( - pTab->mod.aCentroid, pTab->mod.hdr.nBucket, - aTransform, pTab->cfg.nElem, 0 + const float *aTransform = vec1TransformInputVector( + &pTab->mod, pTab->aTmpVec, (const float*)aVec ); + rc = vec1DeleteFindRowid(pTab, aTransform, iRowid, &iBucket, &loc); } } - } - - vec1StmtReset(&rc, pDelete); - - if( rc==SQLITE_OK && (pTab->mod.hdr.flags & VEC1_MODEL_INDEX) ){ - Vec1RowidLocation loc; - - rc = vec1FindByRowid(pTab, iBucket, 1, iRowid, &loc); if( rc==SQLITE_OK ){ if( loc.pList==0 && loc.pBlob==0 ){ @@ -11551,7 +11670,7 @@ static int vec1DeleteByRowid(Vec1Tab *pTab, i64 iRowid){ ** the threshold. So reorganize the list and remove iRowid in ** one go. */ if( loc.pList ){ - vec1ListBuilderCompress(loc.pList, iRowid); + rc = vec1ListBuilderCompress(loc.pList, iRowid); }else{ /* Load the list into memory, compress it so there are no ** tombstones, then write it back to the database. */ @@ -11606,6 +11725,8 @@ static int vec1DeleteByRowid(Vec1Tab *pTab, i64 iRowid){ } } } + + vec1StmtReset(&rc, pDelete); } return rc; @@ -11910,7 +12031,7 @@ static int vec1IntegrityMetaValue( vec1MetaValueRead(pBuf, &iOff, &val); } - if( pVal ){ + if( rc==SQLITE_OK && pVal ){ int eType = sqlite3_value_type(pVal); int bMatch = 0; switch( eType ){ @@ -11962,7 +12083,7 @@ static int vec1IntegrityMetaValue( } /* -** Return true if the difference between f1 and f2 is greater than +** Return true if the difference between f1 and f2 is smaller than ** (fTol * max(f1, f2)). Or false if they differ by more than this. */ static int vec1FloatWithinTolerance(float f1, float f2, float fTol){ @@ -12075,6 +12196,65 @@ static int vec1IntegrityCheckEncoded( return 0; } +/* +** Check that a vector with transformed value aVec[] may legitimately be +** stored in bucket iBucket. +** +** If vec1BestMatchSimple() selects bucket iBucket for aVec[], then it +** may. Otherwise, the vector may have been assigned to its bucket by +** different code - either a different routine, such as the batched +** routines used by 'rebuild', or the same routine on a different platform. +** Rounding differences mean such code may select a different bucket when +** aVec[] is (almost) equidistant from two or more centroids. So iBucket +** is also accepted if the distance from aVec[] to centroid iBucket is +** within a small tolerance of the distance to the centroid selected by +** vec1BestMatchSimple(). +** +** Return non-zero if aVec[] may not be stored in bucket iBucket, or zero +** otherwise. +*/ +static int vec1IntegrityCheckBucket( + const Vec1Model *pMod, /* Model used to quantize vectors */ + const float *aVec, /* Transformed vector */ + int iBucket /* Bucket the vector is stored in */ +){ + const int nElem = (int)pMod->hdr.nElem; + const int nBucket = (int)pMod->hdr.nBucket; + double fBest = 0.0; + double fDist = 0.0; + int iBest = 0; + + if( nBucket==0 ) return (iBucket!=0); + if( iBucket<0 || iBucket>=nBucket ) return 1; + + iBest = vec1BestMatchSimple(pMod->aCentroid, nBucket, aVec, nElem, &fBest); + if( iBest==iBucket ) return 0; + + fDist = vec1L2Dist(aVec, &pMod->aCentroid[iBucket*nElem], nElem); + return vec1BucketIsNearTie(fBest, fDist)==0; +} + +/* +** The buffer passed as the only argument contains a meta-value array. If +** it is of type GENERIC, test that it is well-formed. Return VEC1_CORRUPT +** if it is not, or SQLITE_OK if it looks ok. +*/ +static int vec1IntegrityCheckMetaArray(Vec1Buffer *pBuf){ + int rc = SQLITE_OK; + int bGeneric = vec1GetU32(pBuf->a) & VEC1_META_GENERIC; + if( bGeneric ){ + u32 nEntry = vec1GetU32(&pBuf->a[4]); + u32 ii; + int iOff = VEC1_META_SZHDR; + for(ii=0; rc==SQLITE_OK && iinMeta; iMeta++){ - rc = vec1ReadMeta(pTab, &aBufMeta[iMeta], iId, iMeta); + rc = vec1ReadMeta(pTab, nEntry, &aBufMeta[iMeta], iId, iMeta); + + if( rc==SQLITE_OK ){ + rc = vec1IntegrityCheckMetaArray(&aBufMeta[iMeta]); + } if( rc!=SQLITE_OK ){ if( rc==SQLITE_CORRUPT_VTAB ){ const char *zFmt = "%s: error reading meta-list id=%lld,meta=%d"; @@ -12236,13 +12420,8 @@ static int vec1IntegrityMethod( const u8 *aBaseVec = sqlite3_column_blob(pLookup, 1); const float *aTransform = 0; int nBaseVec = sqlite3_column_bytes(pLookup, 1); - int iCalc = 0; assert( nCodebook>0 ); - - aTransform = vec1TransformInputVector( - &pTab->mod, pTab->aTmpVec, (const float*)aBaseVec - ); /* Check the vector is the right size. */ if( nBaseVec!=nElem*sizeof_f32 ){ @@ -12250,14 +12429,13 @@ static int vec1IntegrityMethod( zErr = sqlite3_mprintf(zFmt, zTabName, iRowid); goto integrity_failed; } + + aTransform = vec1TransformInputVector( + &pTab->mod, pTab->aTmpVec, (const float*)aBaseVec + ); /* Check the vector is in the right bucket. */ - if( pMod->hdr.nBucket>0 ){ - iCalc = vec1PqBestMatch(pMod->aCentroid, - pMod->hdr.nBucket, aTransform, nElem, 0 - ); - } - if( iCalc!=iBucket ){ + if( vec1IntegrityCheckBucket(pMod, aTransform, iBucket) ){ const char *zFmt = "%s: vector from row %lld is in wrong bucket"; zErr = sqlite3_mprintf(zFmt, zTabName, iRowid); goto integrity_failed; @@ -12271,7 +12449,8 @@ static int vec1IntegrityMethod( ** aPQ[] just contains the nCodebook bytes of encoded vector. */ const float *aEnc = (const float*)aTransform; if( (pMod->hdr.flags & VEC1_MODEL_RESIDUAL) ){ - vec1Sub(aResidual, aEnc, &pMod->aCentroid[iCalc*nElem], nElem); + const float *aCent = &pMod->aCentroid[iBucket*nElem]; + vec1Sub(aResidual, aEnc, aCent, nElem); aEnc = aResidual; } if( vec1IntegrityCheckEncoded(&rc, pMod, aEnc, ii, aBlob, &tmp) ){ diff --git a/manifest b/manifest index 269dd1c367..746a52f022 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Enhance\sOP_MustBeInt\sso\sthat\sit\sis\sable\sto\shandle\s-9223372036854775808.0.\n[bugs:/info/2026-10-11T20:33:51Z|Bug\s2026-10-11T20:33:51Z].\s\sSee\nalso\s[987ba5a3dc5f2a22]. -D 2026-10-13T10:51:49.307 +C Merge\sfixes\sto\svec1\smade\son\strunk\sinto\sthis\sbranch. +D 2026-10-13T14:59:41.559 F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea @@ -594,10 +594,11 @@ F ext/vec1/test/vec1alter.test 6cb7ea2677d79a4ea040f27f5cd8902060bbd00fb2f9f94f3 F ext/vec1/test/vec1binary.test dd618dbb9c4f522e1f85a5d58a05ddfc13402302c379cb35b8b9d33300db5886 F ext/vec1/test/vec1binary2.test 2013dec736e39439672c1ea9113cdc0ab72a36b191b1afe3600c51f4b7ab4757 F ext/vec1/test/vec1blocksize.test dd7d9460f5cac98bc5d2c18f1211a503fb0625afed8286a2becdfcfebf0afbdc +F ext/vec1/test/vec1bucket.test 5af9afe8c4b8ec7e0a84ade0df7fb0163226145e3f47b72a16ea2e047178eec6 F ext/vec1/test/vec1cat.test 8cea568d8823519044053d39f4eec8d77a806bf85e1e14817b448729038306de F ext/vec1/test/vec1cat2.test 9e0d9a9cfa33b6aade9150c73064be53e52de9d693f53b7600e7696ed8022e02 F ext/vec1/test/vec1config.test fa0d8e3c83fd5ceeb36b7c481f9207e15f17020357ecde1b243fe03b12cea1b0 -F ext/vec1/test/vec1corrupt.test 8711434b742673c57154332cbfefb83f83b2d40d2399f9d7c3f1a2e8676ece25 +F ext/vec1/test/vec1corrupt.test 59bbe16f0dd5cfda27c4d851a1fda14d8a13d6fa1eead79d587172d3170d628e F ext/vec1/test/vec1create.test fdf7dc7d4e62d5654c1ac27bb9aea9451f024280dce5d0ec97d2698a040b3914 F ext/vec1/test/vec1delete.test 1dc56bbc8a6753991dbc8cdca558a933ae6280c01bee261b1b2561e0390b57e6 F ext/vec1/test/vec1error.test 3d1b74cd535dd5c9d303309f0b18ec606b100139a6483cbbd495b0aa3a9c8a09 @@ -637,11 +638,11 @@ F ext/vec1/test/vec1train.test e65abfb7c92a651b07b209ea3e9a0da43acc371c449788493 F ext/vec1/test/vec1update.test 77d5b85b859393f0d8fbc2f556a75c06bee7b23a19b5af7c3c40dc443f811e65 F ext/vec1/test/vec1update2.test 4fb868f051df89a35f7e58725699259150292fe972253088f3feb10ee7c50945 F ext/vec1/test/vec1version.test 7d17cc84c0a3c8706080abb3484bad8a6eb118c8d4946c7846cde5c4e6cfc89e -F ext/vec1/tool/vec1_algo_test.c c62b828a3e46a39a8656f26b74804906cc3160758f8afc6ed89f507b832af302 +F ext/vec1/tool/vec1_algo_test.c 2777488a8b9ab9545732562c93988aa2424d7afd8b4895d0f03d20c9edf93727 F ext/vec1/tool/vec1bench.tcl a117caee3cb99712f0477fe2fa85c94c260866951d3a9d667b62631b480c2d2d F ext/vec1/tool/vec1bench_addcols.tcl 8c509253d07b179928f2ee32e3591a2c70d13c914217e1328c01afa148717051 F ext/vec1/tool/vec1bench_vibe2db.py 84ddb0236bc3d02a0393abce41986a1b7dad7f0d83884576f8318c2883e3aed3 -F ext/vec1/vec1.c e74bc93b4f03f34037458dd5ea87b9a69cf73eec34f3831e8b11a3306d00c008 +F ext/vec1/vec1.c ef90ba7178fbb02a33165f40f3a9ca0893bb397c992a844e34d2f655e60743ba F ext/wasm/GNUmakefile 36aa205fd33a1bbba2405c7538bdfd2dff802c0e11ddf1aedddd6aacd57a3fc8 F ext/wasm/README-dist.txt f01081a850ce38a56706af6b481e3a7878e24e42b314cfcd4b129f0f8427066a F ext/wasm/README.md 2e87804e12c98f1d194b7a06162a88441d33bb443efcfe00dc6565a780d2f259 @@ -2286,8 +2287,11 @@ F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee F tool/warnings.sh a554d13f6e5cf3760f041b87939e3d616ec6961859c3245e8ef701d1eafc2ca2 F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f F tool/winmain.c 00c8fb88e365c9017db14c73d3c78af62194d9644feaf60e220ab0f411f3604c -P c32cf3e355c4656a4415c7f3cf4de4701f9da6aa5f928487e257293b6e78b274 -R 3136bca19759717338d9a74b4d983307 -U drh -Z 5d8992118ed199f548ad6963bcfaad6d +P 5e488c8da369cdf43e945078f3c9475f2cc5d71c099c85424f7c28ff0d4a7d61 +Q +54e6ae3ae6ac533277def3105162d4578ab4499aec6fbca65000289fbdb9df78 +Q +aab150f4b39aa8dfa0df9161368b5f3b50859b2657be1bf55d06b67a03005c6b +Q +fa41e6201609f40ce9d6ec28693e4cd143363a78f072476586f6de7fd90aed66 +R a2223b2f600edb5df53e2eba4350b227 +U dan +Z 4b3f3ef0e41450389c3b073e8868aca1 # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index d4b65e9371..90c004f1f1 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -5e488c8da369cdf43e945078f3c9475f2cc5d71c099c85424f7c28ff0d4a7d61 +2ca1a93f01b777276494cf2bd6f7aaa86deeb2d8a43dbcd5dccc3a31318482a3