From e32fbe0c230c60b77e42672d17520030ff85446f Mon Sep 17 00:00:00 2001 From: dan Date: Wed, 30 Sep 2026 14:49:16 +0000 Subject: [PATCH 1/6] Import vec1 sources into directory ext/vec1/ of this project. FossilOrigin-Name: 3a0876d973868be7b6f4156a434cd09098c7ab1abdc7f0ef4f3d12a03de68b24 --- Makefile.in | 1 + Makefile.msc | 31 + auto.def | 13 + ext/vec1/Makefile | 63 + ext/vec1/Makefile.msc | 19 + ext/vec1/test/vec1_common.tcl | 125 + ext/vec1/test/vec1aa.test | 264 + ext/vec1/test/vec1ab.test | 115 + ext/vec1/test/vec1ac.test | 100 + ext/vec1/test/vec1alter.test | 80 + ext/vec1/test/vec1binary.test | 162 + ext/vec1/test/vec1binary2.test | 95 + ext/vec1/test/vec1blocksize.test | 65 + ext/vec1/test/vec1cat.test | 247 + ext/vec1/test/vec1cat2.test | 125 + ext/vec1/test/vec1config.test | 113 + ext/vec1/test/vec1corrupt.test | 57 + ext/vec1/test/vec1create.test | 114 + ext/vec1/test/vec1delete.test | 279 + ext/vec1/test/vec1error.test | 85 + ext/vec1/test/vec1fault1.test | 213 + ext/vec1/test/vec1fault2.test | 49 + ext/vec1/test/vec1fault3.test | 106 + ext/vec1/test/vec1fault4.test | 149 + ext/vec1/test/vec1fault5.test | 42 + ext/vec1/test/vec1fault6.test | 46 + ext/vec1/test/vec1fault7.test | 65 + ext/vec1/test/vec1fault8.test | 124 + ext/vec1/test/vec1fault9.test | 82 + ext/vec1/test/vec1fixed.test | 111 + ext/vec1/test/vec1flat.test | 390 + ext/vec1/test/vec1func.test | 115 + ext/vec1/test/vec1growth.test | 114 + ext/vec1/test/vec1intck.test | 293 + ext/vec1/test/vec1intck2.test | 40 + ext/vec1/test/vec1ivf.test | 63 + ext/vec1/test/vec1join.test | 46 + ext/vec1/test/vec1limit.test | 97 + ext/vec1/test/vec1meta.test | 252 + ext/vec1/test/vec1meta2.test | 394 + ext/vec1/test/vec1meta3.test | 304 + ext/vec1/test/vec1nan.test | 48 + ext/vec1/test/vec1pq.test | 143 + ext/vec1/test/vec1query.test | 147 + ext/vec1/test/vec1rollback.test | 148 + ext/vec1/test/vec1rotate.test | 56 + ext/vec1/test/vec1rotate2.test | 42 + ext/vec1/test/vec1round.test | 303 + ext/vec1/test/vec1scalar.test | 77 + ext/vec1/test/vec1stream.test | 121 + ext/vec1/test/vec1stream2.test | 87 + ext/vec1/test/vec1train.test | 160 + ext/vec1/test/vec1update.test | 124 + ext/vec1/test/vec1update2.test | 61 + ext/vec1/test/vec1version.test | 118 + ext/vec1/tool/vec1_algo_test.c | 787 ++ ext/vec1/tool/vec1bench.tcl | 707 ++ ext/vec1/tool/vec1bench_addcols.tcl | 121 + ext/vec1/tool/vec1bench_vibe2db.py | 91 + ext/vec1/vec1.c | 12938 ++++++++++++++++++++++++++ main.mk | 35 + manifest | 82 +- manifest.tags | 4 +- manifest.uuid | 2 +- test/permutations.test | 1 + 65 files changed, 21637 insertions(+), 14 deletions(-) create mode 100644 ext/vec1/Makefile create mode 100644 ext/vec1/Makefile.msc create mode 100644 ext/vec1/test/vec1_common.tcl create mode 100644 ext/vec1/test/vec1aa.test create mode 100644 ext/vec1/test/vec1ab.test create mode 100644 ext/vec1/test/vec1ac.test create mode 100644 ext/vec1/test/vec1alter.test create mode 100644 ext/vec1/test/vec1binary.test create mode 100644 ext/vec1/test/vec1binary2.test create mode 100644 ext/vec1/test/vec1blocksize.test create mode 100644 ext/vec1/test/vec1cat.test create mode 100644 ext/vec1/test/vec1cat2.test create mode 100644 ext/vec1/test/vec1config.test create mode 100644 ext/vec1/test/vec1corrupt.test create mode 100644 ext/vec1/test/vec1create.test create mode 100644 ext/vec1/test/vec1delete.test create mode 100644 ext/vec1/test/vec1error.test create mode 100644 ext/vec1/test/vec1fault1.test create mode 100644 ext/vec1/test/vec1fault2.test create mode 100644 ext/vec1/test/vec1fault3.test create mode 100644 ext/vec1/test/vec1fault4.test create mode 100644 ext/vec1/test/vec1fault5.test create mode 100644 ext/vec1/test/vec1fault6.test create mode 100644 ext/vec1/test/vec1fault7.test create mode 100644 ext/vec1/test/vec1fault8.test create mode 100644 ext/vec1/test/vec1fault9.test create mode 100644 ext/vec1/test/vec1fixed.test create mode 100644 ext/vec1/test/vec1flat.test create mode 100644 ext/vec1/test/vec1func.test create mode 100644 ext/vec1/test/vec1growth.test create mode 100644 ext/vec1/test/vec1intck.test create mode 100644 ext/vec1/test/vec1intck2.test create mode 100644 ext/vec1/test/vec1ivf.test create mode 100644 ext/vec1/test/vec1join.test create mode 100644 ext/vec1/test/vec1limit.test create mode 100644 ext/vec1/test/vec1meta.test create mode 100644 ext/vec1/test/vec1meta2.test create mode 100644 ext/vec1/test/vec1meta3.test create mode 100644 ext/vec1/test/vec1nan.test create mode 100644 ext/vec1/test/vec1pq.test create mode 100644 ext/vec1/test/vec1query.test create mode 100644 ext/vec1/test/vec1rollback.test create mode 100644 ext/vec1/test/vec1rotate.test create mode 100644 ext/vec1/test/vec1rotate2.test create mode 100644 ext/vec1/test/vec1round.test create mode 100644 ext/vec1/test/vec1scalar.test create mode 100644 ext/vec1/test/vec1stream.test create mode 100644 ext/vec1/test/vec1stream2.test create mode 100644 ext/vec1/test/vec1train.test create mode 100644 ext/vec1/test/vec1update.test create mode 100644 ext/vec1/test/vec1update2.test create mode 100644 ext/vec1/test/vec1version.test create mode 100644 ext/vec1/tool/vec1_algo_test.c create mode 100644 ext/vec1/tool/vec1bench.tcl create mode 100644 ext/vec1/tool/vec1bench_addcols.tcl create mode 100644 ext/vec1/tool/vec1bench_vibe2db.py create mode 100644 ext/vec1/vec1.c diff --git a/Makefile.in b/Makefile.in index 4496613a8d..b75e6de869 100644 --- a/Makefile.in +++ b/Makefile.in @@ -108,6 +108,7 @@ LDFLAGS.readline = @LDFLAGS_READLINE@ CFLAGS.readline = @CFLAGS_READLINE@ LDFLAGS.icu = @LDFLAGS_ICU@ LDFLAGS.rt = @LDFLAGS_RT@ +CFLAGS.vec1.avx2 = @CFLAGS_VEC1_AVX2@ CFLAGS.icu = @CFLAGS_ICU@ LDFLAGS.libsqlite3.soname = @LDFLAGS_LIBSQLITE3_SONAME@ # soname: see https://sqlite.org/src/forumpost/5a3b44f510df8ded diff --git a/Makefile.msc b/Makefile.msc index 406bfb04aa..ba744e662d 100644 --- a/Makefile.msc +++ b/Makefile.msc @@ -2474,6 +2474,36 @@ fts5_ext.lo: fts5.c $(HDR) $(EXTHDR) fts5.dll: fts5_ext.lo $(LD) $(LDFLAGS) $(LTLINKOPTS) $(LTLIBPATHS) /DLL /OUT:$@ fts5_ext.lo +# vec1 loadable extension. +# +# If VEC1_AVX2 is non-zero (the default, except when targeting ARM), +# vec1.c is compiled twice - once with scalar code and once with AVX2 - +# and both objects linked into a single DLL that picks one at runtime +# based on the CPU. Otherwise a single scalar-only build is done. +# +!IFNDEF VEC1_AVX2 +!IF "$(PLATFORM)"=="arm" || "$(PLATFORM)"=="ARM" || "$(PLATFORM)"=="arm64" || "$(PLATFORM)"=="ARM64" +VEC1_AVX2 = 0 +!ELSEIF "$(VSCMD_ARG_TGT_ARCH)"=="arm" || "$(VSCMD_ARG_TGT_ARCH)"=="arm64" +VEC1_AVX2 = 0 +!ELSE +VEC1_AVX2 = 1 +!ENDIF +!ENDIF + +VEC1_SRC = $(TOP)\ext\vec1\vec1.c + +!IF $(VEC1_AVX2)!=0 +vec1.dll: $(VEC1_SRC) $(SQLITE3H) + $(TCC) -DVEC1SIMD=SCALAR -Fovec1scalar.lo -Fdvec1.pdb -c $(VEC1_SRC) + $(TCC) -DVEC1SIMD=AVX2 -arch:AVX2 -Fovec1avx2.lo -Fdvec1.pdb -c $(VEC1_SRC) + $(LD) $(LDFLAGS) $(LTLINKOPTS) $(LTLIBPATHS) /DLL /OUT:$@ vec1scalar.lo vec1avx2.lo +!ELSE +vec1.dll: $(VEC1_SRC) $(SQLITE3H) + $(TCC) -Fovec1.lo -Fdvec1.pdb -c $(VEC1_SRC) + $(LD) $(LDFLAGS) $(LTLINKOPTS) $(LTLIBPATHS) /DLL /OUT:$@ vec1.lo +!ENDIF + sqlite3rbu.lo: $(TOP)\ext\rbu\sqlite3rbu.c $(HDR) $(EXTHDR) $(LTCOMPILE) -DSQLITE_CORE -c $(TOP)\ext\rbu\sqlite3rbu.c @@ -2954,6 +2984,7 @@ clean: del /Q kvtest.exe ossshell.exe scrub.exe 2>NUL del /Q showshm.exe sqlite3_checker.* sqlite3_expert.exe 2>NUL del /Q fts5.* fts5parse.* 2>NUL + del /Q vec1.* 2>NUL del /q src-verify.exe 2>NUL del /q jimsh.exe jimsh0.exe 2>NUL -$(TCLSH_CMD) test/testrunner.tcl clean diff --git a/auto.def b/auto.def index 214ef22304..416cc7a87d 100644 --- a/auto.def +++ b/auto.def @@ -66,4 +66,17 @@ sqlite-configure canonical { proj-define-for-opt static-tclsqlite3 STATIC_TCLSQLITE3 "Statically link tclsqlite3?" proj-define-for-opt static-cli-shell STATIC_CLI_SHELL "Statically link CLI shell?" + # vec1 (ext/vec1) is only built on request ("make vec1.so"). If the + # compiler supports AVX2 it is built as a multi-arch extension that + # selects AVX2 or scalar code at runtime. Use [cctest] rather than + # [cc-check-flags] so the flags are not appended to later tests. + msg-checking "Checking for AVX2 flags for vec1... " + if {[cctest -cflags {-mavx2 -mfma}]} { + msg-result "-mavx2 -mfma" + define CFLAGS_VEC1_AVX2 {-mavx2 -mfma} + } else { + msg-result "no" + define CFLAGS_VEC1_AVX2 "" + } + }; # sqlite-configure diff --git a/ext/vec1/Makefile b/ext/vec1/Makefile new file mode 100644 index 0000000000..1a05a253fc --- /dev/null +++ b/ext/vec1/Makefile @@ -0,0 +1,63 @@ + +CC = gcc + +CFLAGS += -O3 +CFLAGS += -DNDEBUG +CFLAGS += -Wextra -Werror +CFLAGS += -g + +#CFLAGS += -DVEC1_THREADS=0 +#CFLAGS += -fprofile-arcs -ftest-coverage -fprofile-update=atomic -lgcov +#CFLAGS += -fno-inline + +#ASANFLAGS = -fsanitize=address,undefined -fno-sanitize-recover=undefined +#CFLAGS += $(ASANFLAGS) + +SIMDCFLAGS = -mavx2 -mfma + +SRC = vec1.c + +vec1.so: $(SRC) + $(CC) $(CFLAGS) $(SIMDCFLAGS) $(SRC) -o vec1.so -shared -fPIC -lm + +vec1.s: $(SRC) + $(CC) $(CFLAGS) -g0 $(SIMDCFLAGS) $(SRC) -S + +# Method for building multi-arch loadable libraries. +# +# To build a multi-arch library, the "vec1.c" file must be compiled twice. +# Once with VEC1SIMD defined as SCALAR, and once as AVX2. The -DVEC1SMD=AVX2 +# compilation should pass flags to enable AVX2 support, the -DVEC1SMD=SCALAR +# build should not pass any such flags. Then both objects are linked into +# a single *.so file. The result works on hardware that supports AVX2, and +# also on hardware that does not. +# +# Eventually, when AVX-512 support is added, there will be a third +# compilation of vec1.c with flags to support AVX-512. +# +vec1multi.so: $(SRC) + $(CC) -DVEC1SIMD=SCALAR -c $(CFLAGS) $(SRC) -o vec1scalar.o -fPIC + $(CC) -DVEC1SIMD=AVX2 -c -mavx2 -mfma $(CFLAGS) $(SRC) -o vec1avx2.o -fPIC + $(CC) vec1scalar.o vec1avx2.o -o vec1multi.so -shared -fPIC + +vec1_faiss.so: vec1_faiss.cpp + g++ $(CFLAGS) vec1_faiss.cpp -o vec1_faiss.so -shared -fPIC -lfaiss + +vec1testscalar.so: $(SRC) + $(CC) $(CFLAGS) -DVEC1_TEST_SCALAR $(SRC) -o vec1testscalar.so -shared -fPIC -lm + +vec1.a: $(SRC) + $(CC) -DVEC1_STATIC $(CFLAGS) $(SIMDCFLAGS) -c $(SRC) -fPIC + ar rcs vec1.a vec1.o + +vec1_algo_test: tool/vec1_algo_test.c $(SRC) + $(CC) -I. $(CFLAGS) $(SIMDCFLAGS) tool/vec1_algo_test.c -o vec1_algo_test -lm + +clean: + rm -f vec1.so vec1_faiss.so vec1multi.so vec1testscalar.so + rm -f vec1.a + rm -f vec1.o vec1scalar.o vec1avx2.o + rm -f vec1.so-vec1.gcda vec1.so-vec1.gcno + rm -f vec1.s + + diff --git a/ext/vec1/Makefile.msc b/ext/vec1/Makefile.msc new file mode 100644 index 0000000000..bf42ed74d9 --- /dev/null +++ b/ext/vec1/Makefile.msc @@ -0,0 +1,19 @@ + + +CFLAGS = /Zi /O2 /DNDEBUG /W4 +SIMDCFLAGS = /arch:AVX2 + +all: vec1.dll vec1multi.dll + +vec1.dll: vec1.c + cl $(CFLAGS) $(SIMDCFLAGS) vec1.c -link -dll -out:vec1.dll + +vec1multi.dll: vec1.c + cl /c /DVEC1SIMD=SCALAR $(CFLAGS) vec1.c /Fovec1scalar.obj + cl /c /DVEC1SIMD=AVX2 $(CFLAGS) /arch:AVX2 vec1.c /Fovec1avx2.obj + link /DLL vec1scalar.obj vec1avx2.obj /OUT:vec1multi.dll + +clean: + del /Q vec1.dll + del /Q vec1scalar.obj + del /Q vec1multi.obj diff --git a/ext/vec1/test/vec1_common.tcl b/ext/vec1/test/vec1_common.tcl new file mode 100644 index 0000000000..066bc78931 --- /dev/null +++ b/ext/vec1/test/vec1_common.tcl @@ -0,0 +1,125 @@ + +if {![info exists testdir]} { + set testdir [file join [file dirname [info script]] .. .. .. test] +} +source $testdir/tester.tcl + +set dir [file dirname [info nameofexecutable]] +set vec1_shlib [file join $dir vec1] + + +proc vec1_load_only {db} { + if {[catch {$db one {SELECT vec1_info()}}]} { + global vec1_shlib + $db enable_load_extension 1 + if {[catch { sqlite3_load_extension $db $vec1_shlib }]} { + $db eval { SELECT load_extension($vec1_shlib) } + } + $db enable_load_extension 0 + } +} + +sqlite3 db_test_vec1 "" +set res [catch { vec1_load_only db_test_vec1 }] +db_test_vec1 close +if {$res} { + finish_test + return -code return +} + +proc vec1_load {db} { + if {[catch {$db one {SELECT vec1_info()}}]} { + global vec1_shlib + $db enable_load_extension 1 + $db eval { SELECT load_extension($vec1_shlib) } + $db enable_load_extension 0 + } + $db func random_vector random_vector + $db func random_vector2 random_vector2 + $db func rvec rvec +} + +proc vec1_load_scalar {db} { + global vec1_shlib + $db enable_load_extension 1 + set scalar "${vec1_shlib}testscalar" + sqlite3_load_extension $db $scalar + $db enable_load_extension 0 +} + +proc random_vector {n {scale 1.0}} { + set v [list] + for {set i 0} {$i < $n} {incr i} { + lappend v [expr {((rand() * 2.0) - 1.0) * $scale}] + } + binary format f$n $v +} + +proc random_vector2 {n} { + set v [list] + for {set i 0} {$i < $n} {incr i} { + lappend v [expr {abs(rand() * 10.0)}] + } + binary format f$n $v +} + +proc rvec {n i} { + set lPrime { + 23 29 31 37 41 43 47 53 + 59 61 67 71 73 79 83 89 + 97 101 103 107 109 113 127 131 + 137 139 149 151 157 163 167 173 + 179 181 191 193 197 199 211 223 + 227 229 233 239 241 251 257 263 + 269 271 277 281 283 293 307 311 + 313 317 331 337 347 349 353 359 + 367 373 379 383 389 397 401 409 + 419 421 431 433 439 443 449 457 + 461 463 467 479 487 491 499 503 + 509 521 523 541 547 557 563 569 + 571 577 587 593 599 601 607 613 + 617 619 631 641 643 647 653 659 + 661 673 677 683 691 701 709 719 + 727 733 739 743 751 757 761 769 + } + + if {$n>[llength $lPrime]} { + error "n value too large ($n)" + } + + set i [expr $i * 769] + + set lDim [list] + for {set iDim 0} {$iDim<$n} {incr iDim} { + set x [lindex $lPrime $iDim] + lappend lDim [expr ($i % $x) - $x/2] + } + + binary format r* $lDim +} +db func rvec rvec + +proc model2tbl {nm vec1tbl} { + set model [db one "SELECT val FROM ${vec1tbl}_model WHERE id=1"] + + binary scan $model IIIIII iVersion flags nElem nCodebook nBucket eDistance + + set nBytePerVector [expr $nElem*4] + set iOff [expr 4 * 6] + if {($flags & 0x08)==0 && $nCodebook>0} { + incr iOff [expr 4 * $nElem * 256] + } + #puts "nBytePerVector=$nBytePerVector" + #puts "nBucket=$nBucket" + #puts "sz=[string length $model]" + #puts "iOff=$iOff" + + db eval "CREATE TABLE $nm (ibucket INTEGER PRIMARY KEY, coarse BLOB)" + for {set iBucket 0} {$iBucket<$nBucket} {incr iBucket} { + set vec [string range $model $iOff [expr $iOff+$nBytePerVector-1]] + db eval "INSERT INTO $nm VALUES(\$iBucket, \$vec)" + incr iOff $nBytePerVector + } +} + + diff --git a/ext/vec1/test/vec1aa.test b/ext/vec1/test/vec1aa.test new file mode 100644 index 0000000000..97af91b151 --- /dev/null +++ b/ext/vec1/test/vec1aa.test @@ -0,0 +1,264 @@ +# 2026-01-17 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1aa +vec1_load db + +do_execsql_test 1.0 { + CREATE VIRTUAL TABLE v1 USING vec1; +} + +do_execsql_test 1.0.1 { SELECT vec1_config('nthread'); } {1} +do_execsql_test 1.0.2 { SELECT vec1_config('nprobe'); } {0.05} + +do_execsql_test 1.1 { + INSERT INTO v1(cmd, vector) VALUES('rebuild', '{index:"flat"}'); + INSERT INTO v1(rowid, vector) VALUES( + 1, vec1_from_json('[1,2,3,4,5,6,7,8]') + ); +} + +do_execsql_test 1.2 { PRAGMA integrity_check } {ok} + +do_execsql_test 1.3 { + DELETE FROM v1_base; + PRAGMA integrity_check; +} {{v1: %_idx row 1 missing from %_base}} + +do_execsql_test 1.4 { + DROP TABLE v1; +} + +do_execsql_test 1.5 { + CREATE VIRTUAL TABLE v1 USING vec1; + INSERT INTO v1(cmd, vector) VALUES('rebuild', '{index:"flat"}'); + INSERT INTO v1(rowid, vector) VALUES(1, vec1_from_json('[1,2,3,4]')); + BEGIN; + SAVEPOINT aaa; + INSERT INTO v1(rowid, vector) VALUES(2, vec1_from_json('[1,2,3,4]')); + ROLLBACK TO aaa; + COMMIT; + + PRAGMA integrity_check; +} {ok} + +do_execsql_test 1.6 { + BEGIN; + INSERT INTO v1(rowid, vector) VALUES(3, vec1_from_json('[1,2,3,4]')); + ROLLBACK; + INSERT INTO v1(rowid, vector) VALUES(4, vec1_from_json('[1,2,3,4]')); + PRAGMA integrity_check; +} {ok} + +#------------------------------------------------------------------------- + +#reset_db +#vec1_load db + +#do_execsql_test 2.0 { +# CREATE VIRTUAL TABLE v1 USING vec1; +# INSERT INTO v1(cmd, vector) VALUES('rebuild', 'flat'); +# INSERT INTO v1(rowid, vector) VALUES(1, vec1_from_json('[1,2,3,4]')); +# INSERT INTO v1(rowid, vector) VALUES(2, vec1_from_json('[4,3,2,1]')); +# SELECT count(*) fROM v1_idx; +#} {1} + +#------------------------------------------------------------------------- + +reset_db +vec1_load db + +do_execsql_test 3.0 { + CREATE VIRTUAL TABLE v1 USING vec1; + INSERT INTO v1(cmd, vector) VALUES('rebuild', '{index:"flat"}'); + + BEGIN; + INSERT INTO v1(rowid, vector) VALUES(1, random_vector(8)); + INSERT INTO v1(rowid, vector) VALUES(2, random_vector(8)); + INSERT INTO v1(rowid, vector) VALUES(3, random_vector(8)); + COMMIT; + + PRAGMA integrity_check; +} {ok} + +do_execsql_test 3.1 { + DELETE FROM v1 WHERE rowid=2; + PRAGMA integrity_check; +} {ok} + +do_execsql_test 3.2 { + SELECT rowid FROM v1 +} {1 3} + +do_execsql_test 3.3 { + SELECT rowid FROM v1( random_vector(8), 5 ) ORDER BY 1; +} {1 3} + +do_execsql_test 3.4 { + WITH s(i) AS ( SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<100 ) + INSERT INTO v1 SELECT random_vector(8); + WITH s(i) AS ( SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<100 ) + INSERT INTO v1 SELECT random_vector(8); + WITH s(i) AS ( SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<100 ) + INSERT INTO v1 SELECT random_vector(8); +} + +do_execsql_test 3.5 { + DELETE FROM v1 WHERE rowid IN (5, 105, 205, 77, 98, 222); + PRAGMA integrity_check; +} {ok} + +#------------------------------------------------------------------------- + +reset_db +vec1_load db +do_execsql_test 4.0 { + CREATE VIRTUAL TABLE v1 USING vec1; + INSERT INTO v1(cmd, vector) VALUES('rebuild', '{index:"flat"}'); +} + +do_execsql_test 4.1 { + BEGIN; + INSERT INTO v1 (rowid, vector) VALUES( 0x1_0000_0001, random_vector(8) ); + INSERT INTO v1 (rowid, vector) VALUES( 0x1_0000_0002, random_vector(8) ); + INSERT INTO v1 (rowid, vector) VALUES( 0x1_0000_0003, random_vector(8) ); + INSERT INTO v1 (rowid, vector) VALUES( 0x1_0000_0004, random_vector(8) ); + COMMIT; +} + +do_execsql_test 4.2 { + PRAGMA integrity_check; +} {ok} + +do_execsql_test 4.3 { + DELETE FROM v1 WHERE rowid = 0x1_0000_0003; +} {} + +do_execsql_test 4.4 { + PRAGMA integrity_check; +} {ok} + +#------------------------------------------------------------------------- +reset_db +vec1_load db +do_execsql_test 4.0 { + CREATE VIRTUAL TABLE v1 USING vec1; + INSERT INTO v1(cmd, vector) VALUES('rebuild', '{index:"flat"}'); +} + +do_execsql_test 4.1 { + INSERT INTO v1 VALUES( random_vector( 16 ) ); +} + +do_execsql_test 4.2 { + INSERT INTO v1 VALUES( random_vector( 16 ) ); + SELECT count(*) FROM v1_idx; +} {1} + +do_execsql_test 4.3 { + PRAGMA integrity_check +} {ok} + +#------------------------------------------------------------------------- +# +reset_db +vec1_load db + +do_execsql_test 5.0 { + CREATE VIRTUAL TABLE v1 USING vec1; + INSERT INTO v1(cmd, vector) VALUES('rebuild', '{index:"flat"}'); +} + +do_execsql_test 5.1 { + INSERT INTO v1 VALUES( random_vector( 16 ) ); + INSERT INTO v1 VALUES( random_vector( 16 ) ); + INSERT INTO v1 VALUES( random_vector( 16 ) ); +} + +do_execsql_test 5.2 { + BEGIN; + INSERT INTO v1(rowid, vector) VALUES(11, random_vector( 16 ) ); + INSERT INTO v1(rowid, vector) VALUES(12, random_vector( 16 ) ); + DELETE FROM v1 WHERE rowid=11; + COMMIT; +} {} + +do_execsql_test 5.3 { + PRAGMA integrity_check +} {ok} + +#------------------------------------------------------------------------- +# +reset_db +vec1_load db + +do_execsql_test 6.0 { + CREATE VIRTUAL TABLE v1 USING vec1(x); + INSERT INTO v1 VALUES( random_vector(22) ); +} +do_catchsql_test 6.1 { + INSERT INTO v1 VALUES( random_vector(23) ); +} {1 {vec1: unexpected vector blob size 92 bytes, expected 88}} + +#------------------------------------------------------------------------- +# +reset_db +vec1_load db + +do_execsql_test 6.0 { + CREATE VIRTUAL TABLE v1 USING vec1(x); +} +do_catchsql_test 6.1 { + INSERT INTO v1(cmd, arg) VALUES('rebuild', '{distance:"l2"}'); +} {1 {vec1: required parameter missing: index}} + +do_execsql_test 6.2 { + INSERT INTO v1 VALUES(random_vector(16)); + INSERT INTO v1 VALUES(random_vector(16)); + INSERT INTO v1 VALUES(random_vector(16)); + INSERT INTO v1 VALUES(random_vector(16)); + INSERT INTO v1 VALUES(random_vector(16)); + + UPDATE v1_base SET vector = random_vector(15) WHERE id=4; +} + +do_catchsql_test 6.1 { + INSERT INTO v1(cmd, arg) VALUES('rebuild', '{index:"flat",distance:"l2"}'); +} {1 {vec1: unexpected vector size in v1_base: 60}} + +#------------------------------------------------------------------------- +# +reset_db +vec1_load db + +do_execsql_test 7.0 { + CREATE VIRTUAL TABLE v1 USING vec1(x); +} + +do_catchsql_test 7.1 { + INSERT INTO v1(x) VALUES('sometextvalu'); +} {1 {vec1: unexpected vector type TEXT, expected BLOB}} +do_execsql_test 7.2 { + INSERT INTO v1(x) VALUES( random_vector(4) ); +} {} +do_catchsql_test 7.3 { + INSERT INTO v1(x) VALUES('sometextvaluABCD'); +} {1 {vec1: unexpected vector type TEXT, expected BLOB}} +do_catchsql_test 7.4 { + UPDATE v1 SET x='sometextvaluABCD'; +} {1 {vec1: unexpected vector type TEXT, expected BLOB}} + + +finish_test + diff --git a/ext/vec1/test/vec1ab.test b/ext/vec1/test/vec1ab.test new file mode 100644 index 0000000000..c5aa441d20 --- /dev/null +++ b/ext/vec1/test/vec1ab.test @@ -0,0 +1,115 @@ +# 2026-01-17 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1ab +vec1_load db + +do_execsql_test 1.0 { + CREATE VIRTUAL TABLE v1 USING vec1; + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<10000 + ) + INSERT INTO v1(rowid, vector) SELECT NULL, random_vector(128) FROM s; +} + +foreach {tn training} { + 1.1 "flat" + 1.2 "{ codesize: 16 }" + 1.3 "{ codesize: 16 }" + 1.4 "{ codesize: 8 }" + 1.5 "{ codesize: 8 , nbucket: 32 }" + 1.6 "{ codesize: 16 , nbucket: 32 }" + 1.7 "{ codesize: 8 , nbucket: 32 }" +} { + do_test $tn.1 { + if {$training=="flat"} { + set T {{index:"flat"}} + } else { + set T [db one { + SELECT vec1_train(vector, $training) FROM v1 WHERE rowid < 1000 + }] + } + set {} {} + } {} + + do_execsql_test $tn.2 { + INSERT INTO v1(cmd, vector) VALUES('rebuild', $T); + } + + do_execsql_test $tn.3 { + PRAGMA integrity_check + } ok + + db close + sqlite3 db test.db + vec1_load db + + do_execsql_test $tn.4 { + PRAGMA integrity_check + } ok + + do_execsql_test $tn.5 { + SELECT sum(good) FROM ( + SELECT rowid IN ( + SELECT rowid FROM v1(o.vector, '{ K: 10 }') + ) AS good FROM v1 AS o LIMIT 100 + ); + } {100} +} + +do_execsql_test 2.0 { + CREATE VIRTUAL TABLE v2 USING vec1; + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<10000 + ) + INSERT INTO v2(rowid, vector) + SELECT 0x3_FFFF_FFFF + i, random_vector(128) FROM s; + + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<50 + ) + INSERT INTO v2(rowid, vector) + SELECT i - 0x3_FFFF_FFFF, random_vector(128) FROM s; +} + +do_execsql_test 2.1 { + INSERT INTO v2(cmd, vector) VALUES('rebuild', '{index:"flat"}'); +} + +do_execsql_test 2.2 { + SELECT sum(good) FROM ( + SELECT rowid IN ( + SELECT rowid FROM v2(o.vector, '{ K: 10 }') + ) AS good FROM v2 AS o LIMIT 100 + ); +} {100} + +do_execsql_test 2.3 { + INSERT INTO v2(cmd, vector) VALUES('rebuild', ( + SELECT vec1_train(vector, '{ codesize: 16 }') FROM ( + SELECT vector FROM v2 LIMIT 1000 + ) + )); +} + +do_execsql_test 2.4 { + SELECT sum(good) FROM ( + SELECT rowid IN ( + SELECT rowid FROM v2(o.vector, '{ K: 10 }') + ) AS good FROM v2 AS o LIMIT 100 + ); +} {100} + + +finish_test diff --git a/ext/vec1/test/vec1ac.test b/ext/vec1/test/vec1ac.test new file mode 100644 index 0000000000..cd935f92f1 --- /dev/null +++ b/ext/vec1/test/vec1ac.test @@ -0,0 +1,100 @@ +# 2026-01-17 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1ac +vec1_load db + +db func normalize normalize +proc normalize {vec} { + binary scan $vec f* aVal + set sum 0.0 + foreach v $aVal { + set sum [expr $sum + ($v*$v)] + } + set sz [expr sqrt($sum)] + set aNew [list] + foreach v $aVal { + lappend aNew [expr $v/$sz] + } + binary format f* $aNew +} + +db func rescale rescale +proc rescale {vec mul} { + binary scan $vec f* aVal + set aNew [list] + foreach v $aVal { + lappend aNew [expr $v*$mul] + } + binary format f* $aNew +} + +do_execsql_test 1.0 { + CREATE TABLE t1(k INTEGER PRIMARY KEY, v VECTOR); + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<10000 + ) + INSERT INTO t1 SELECT NULL, random_vector2(128) FROM s; + + CREATE VIRTUAL TABLE v1 USING vec1; + INSERT INTO v1(rowid, vector) SELECT k, v FROM t1; +} + +foreach {tn cfg} { + 1 { {nbucket:0, codesize:0, distance:"cos"} } + 2 { {nbucket:0, codesize:32, distance:"cos"} } + 3 { {nbucket:32, codesize:32, distance:"cos"} } +} { + + do_execsql_test 1.$tn.1 { + INSERT INTO v1(cmd, vector) VALUES('rebuild', ( + SELECT vec1_train(vector, $cfg) FROM v1 + )); + } {} + + do_execsql_test 1.$tn.2 { + SELECT sum( + rowid IN ( SELECT rowid FROM v1( normalize( o.vector ), '{K:10}' ) ) + ) + FROM v1 AS o WHERE rowid BETWEEN 600 AND 700; + } {101} + + do_execsql_test 1.$tn.3 { + SELECT sum( + rowid IN ( SELECT rowid FROM v1( rescale( o.vector, 0.01 ), '{K:10}' ) ) + ) + FROM v1 AS o WHERE rowid BETWEEN 600 AND 700; + } {101} +} + +do_execsql_test 2.0 { + CREATE VIRTUAL TABLE v2 USING vec1; + INSERT INTO v2(rowid, vector) SELECT k, v FROM t1; + INSERT INTO v2(cmd, vector) VALUES('rebuild', ( + SELECT vec1_train(vector, '{nbucket:32, codesize:32, distance:"cos"}') + FROM v2 + )); +} + +do_execsql_test 2.1 { + PRAGMA integrity_check; +} {ok} + +do_execsql_test 2.2 { + DELETE FROM v2 WHERE (rowid % 45)==0; + PRAGMA integrity_check; +} {ok} + + +finish_test diff --git a/ext/vec1/test/vec1alter.test b/ext/vec1/test/vec1alter.test new file mode 100644 index 0000000000..2382669633 --- /dev/null +++ b/ext/vec1/test/vec1alter.test @@ -0,0 +1,80 @@ +# 2026-08-04 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1alter +vec1_load db + +do_execsql_test 1.0 { + CREATE VIRTUAL TABLE v1 USING vec1(v); + INSERT INTO v1(rowid, v) VALUES(11, random_vector(8)); + INSERT INTO v1(rowid, v) VALUES(12, random_vector(8)); +} + +do_execsql_test 1.1 { + ALTER TABLE v1 RENAME TO v2; +} + +do_execsql_test 1.2 { + SELECT rowid FROM v2 +} {11 12} + +do_execsql_test 1.3 { + PRAGMA integrity_check +} {ok} + +do_execsql_test 1.4 { + PRAGMA writable_schema; +} {0} + +do_execsql_test 1.5 { + PRAGMA writable_schema = 1; + PRAGMA writable_schema; +} {1} + +do_execsql_test 1.6 { + ALTER TABLE v2 RENAME TO v3; +} + +do_execsql_test 1.7 { + PRAGMA writable_schema; +} {1} + +do_execsql_test 1.8 { + CREATE VIRTUAL TABLE v1 USING vec1(v); + INSERT INTO v1(rowid, v) VALUES(-14, random_vector(18)); + INSERT INTO v1(rowid, v) VALUES(-15, random_vector(18)); +} + +do_execsql_test 1.9 { + SELECT rowid FROM v1 +} {-15 -14} + +do_test 1.10 { sqlite3_db_config db DEFENSIVE } {0} +do_test 1.11 { sqlite3_db_config db DEFENSIVE 1 } {1} + +do_execsql_test 1.12 { + ALTER TABLE v3 RENAME TO v4; +} + +do_test 1.13 { sqlite3_db_config db DEFENSIVE } {1} + +do_execsql_test 1.14 { + SELECT tbl_name, name, sql FROM sqlite_schema WHERE name='v4_idx_idx' +} { + v4_idx v4_idx_idx + {CREATE INDEX 'v4_idx_idx' ON 'v4_idx'(bucket, first, last)} +} + +finish_test + diff --git a/ext/vec1/test/vec1binary.test b/ext/vec1/test/vec1binary.test new file mode 100644 index 0000000000..b8e2313c39 --- /dev/null +++ b/ext/vec1/test/vec1binary.test @@ -0,0 +1,162 @@ +# 2026-01-17 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1binary + + +foreach {tn nElem codesize} { + 1 16 2 + 2 16 1 + 3 48 6 + 4 48 3 +} { + reset_db + vec1_load db + expr srand(0) + do_execsql_test 1.$tn.0 { + CREATE VIRTUAL TABLE v1 USING vec1(v); + } + + set nRow [expr 40] + set nLimit [expr 40] + + do_execsql_test 1.$tn.1 { + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<$nRow + ) + INSERT INTO v1 SELECT random_vector($nElem) FROM s; + } + + set jarg "{quantizer:\"bq\", nbucket: 8, codesize: $codesize}" + do_execsql_test 1.$tn.2 { + INSERT INTO v1(cmd, arg) VALUES('rebuild', ( + SELECT vec1_train(v, $jarg) FROM v1 + )); + } + + set v [db one {SELECT random_vector($nElem)}] + + set res [db eval { + SELECT rowid FROM v1 ORDER by vec1_l2_distance($v, v) LIMIT $nLimit + }] + + do_execsql_test 1.$tn.2 { + SELECT rowid FROM v1($v, '{K:50, nprobe:8}') + ORDER BY vec1_l2_distance(v, $v) + LIMIT $nLimit + } $res + + do_execsql_test 1.$tn.3 { + PRAGMA integrity_check + } {ok} +} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 2.1 { + CREATE VIRTUAL TABLE v1 USING vec1(v); + INSERT INTO v1(cmd, arg) VALUES('rebuild', ( + WITH s(i) AS ( SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<$nRow ) + SELECT vec1_train(random_vector(8), '{quantizer:"bq", nbucket:8}') FROM s + )); + + INSERT INTO v1 VALUES( random_vector(8) ); +} + +set v [db one {SELECT random_vector(8)}] + +do_execsql_test 2.2 { + SELECT rowid FROM v1($v, '{nprobe:8}') LIMIT $nLimit +} {1} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 2.1 { + CREATE VIRTUAL TABLE v1 USING vec1(abc); + INSERT INTO v1(cmd, arg) VALUES('rebuild', ( + WITH s(i) AS ( SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<$nRow ) + SELECT vec1_train(random_vector(64), '{quantizer:"bq", nbucket:2}') FROM s + )); + + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1000 + ) + INSERT INTO v1 SELECT random_vector(64) FROM s; +} + +do_execsql_test 2.2 { + PRAGMA integrity_check +} {ok} + +foreach id [db eval {SELECT rowid FROM v1}] { + db eval { DELETE FROM v1 WHERE rowid = $id } + if {($id % 1)==0} { + do_execsql_test 2.3.$id { + PRAGMA integrity_check + } {ok} + } +} + +#------------------------------------------------------------------------- +# Now check the version number in model headers. This should be 7 when +# using a binary quantizer, or 4 otherwise. +# +reset_db +vec1_load db + +foreach {tn jarg iVersion} { + 1 "{nbucket:2, codesize:8}" 4 + 2 "{nbucket:2, codesize:8, quantizer:\"bq\"}" 7 + 3 "{nbucket:2, codesize:8, quantizer:\"pq\"}" 4 +} { + do_test 2.$tn { + set M [db one { + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<512 + ) + SELECT vec1_train(random_vector(16), $jarg) FROM s; + }] + + binary scan $M I version + set version + } $iVersion +} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 3.1 { + CREATE VIRTUAL TABLE v1 USING vec1(v); + WITH s(i) AS ( SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1000 ) + INSERT INTO v1 SELECT random_vector(100) FROM s; + + INSERT INTO v1(cmd, arg) VALUES('rebuild', ( + SELECT vec1_train(v, '{quantizer:"bq", nbucket:2, codesize:8}') FROM v1 + )); +} + +set v [db one {SELECT random_vector(100)}] +do_execsql_test 3.2 { + SELECT count(*) FROM ( + SELECT rowid FROM v1($v, '{nprobe:2}') LIMIT 5 + ); +} {5} + +finish_test + diff --git a/ext/vec1/test/vec1binary2.test b/ext/vec1/test/vec1binary2.test new file mode 100644 index 0000000000..30b7fabaa4 --- /dev/null +++ b/ext/vec1/test/vec1binary2.test @@ -0,0 +1,95 @@ +# 2026-01-17 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1binary2 + +vec1_load db +expr srand(0) + +proc random_item {L} { + set ii [expr {int(abs(rand()) * [llength $L])}] + lindex $L $ii +} + +proc random_binvec {n {opts {1 -1}}} { + set lRet [list] + for {set ii 0} {$ii < $n} {incr ii} { + lappend lRet [random_item $opts] + } + + binary format f$n $lRet +} + +db func random_binvec random_binvec + +set nQuery 100 + +foreach {tn N opts nbucket} { + + 1 8 {1 -1} 0 + 2 16 {1 -1} 0 + 3 24 {1 -1} 0 + 4 29 {1 -1} 0 + 5 9 {1 -1} 0 + 6 201 {1 -1} 0 + + 7 8 {2 3} 1 + 8 33 {-400 -155} 1 +} { + + do_execsql_test 1.$tn.0 { + DROP TABLE IF EXISTS v1; + + CREATE VIRTUAL TABLE v1 USING vec1(v); + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<5 + ) + INSERT INTO v1(v) SELECT random_binvec($N, $opts) FROM s; + } + + set spec "{quantizer:\"bq\", nbucket:$nbucket, wht:0}" + do_execsql_test 1.$tn.1 { + INSERT INTO v1(cmd, arg) VALUES('rebuild', ( + SELECT vec1_train(v, $spec) FROM v1 + )); + } {} + + do_execsql_test 1.$tn.2 { + PRAGMA integrity_check + } {ok} + + for {set j 0} {$j < $nQuery} {incr j} { + set q [random_binvec $N $opts] + set expect [db one { + SELECT r FROM ( + SELECT group_concat(rowid, ' ') AS r, vec1_l2_distance($q, v) FROM v1 + GROUP BY 2 + ORDER BY 2 ASC + LIMIT 1 + ) + }] + + set nExpect [llength $expect] + + do_execsql_test 1.$tn.2.$j. { + SELECT r FROM ( + SELECT rowid AS r FROM v1($q, $nExpect) + ) ORDER BY 1 + } $expect + } + +} + +finish_test + diff --git a/ext/vec1/test/vec1blocksize.test b/ext/vec1/test/vec1blocksize.test new file mode 100644 index 0000000000..b8a02912c5 --- /dev/null +++ b/ext/vec1/test/vec1blocksize.test @@ -0,0 +1,65 @@ +# 2026-04-02 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1blocksize +vec1_load db + +do_execsql_test 1.0 { + CREATE VIRTUAL TABLE v1 USING vec1; +} + +do_catchsql_test 1.1 { + INSERT INTO v1(cmd, arg) VALUES('blocksize', 64); +} {1 {'blocksize' requires an integer value between 1024 and 16777216}} + +do_catchsql_test 1.2 { + INSERT INTO v1(cmd, arg) VALUES('blocksize', 1023); +} {1 {'blocksize' requires an integer value between 1024 and 16777216}} + +do_catchsql_test 1.3 { + INSERT INTO v1(cmd, arg) VALUES('blocksize', 16777217); +} {1 {'blocksize' requires an integer value between 1024 and 16777216}} + +do_execsql_test 1.4 { + INSERT INTO v1(cmd, arg) VALUES('blocksize', 1024); + INSERT INTO v1(cmd, arg) VALUES('blocksize', 16777216); +} {} + +do_catchsql_test 2.1 { + INSERT INTO v1(cmd, arg) VALUES('blocksize-min', 64); +} {1 {'blocksize-min' requires an integer value between 1024 and 16777216}} + +do_catchsql_test 2.2 { + INSERT INTO v1(cmd, arg) VALUES('blocksize-min', 1023); +} {1 {'blocksize-min' requires an integer value between 1024 and 16777216}} + +do_catchsql_test 2.3 { + INSERT INTO v1(cmd, arg) VALUES('blocksize-min', 16777217); +} {1 {'blocksize-min' requires an integer value between 1024 and 16777216}} + +do_execsql_test 2.4 { + INSERT INTO v1(cmd, arg) VALUES('blocksize-min', 1024); + INSERT INTO v1(cmd, arg) VALUES('blocksize-min', 16777216); +} {} + +do_catchsql_test 3.0 { + INSERT INTO v1(cmd, arg) VALUES('not-blocksize', 1024); +} {1 {vec1: unknown command: not-blocksize}} + +do_catchsql_test 3.1 { + INSERT INTO v1(cmd, arg) VALUES('blocksize', 'not integer'); +} {1 {'blocksize' requires an integer value between 1024 and 16777216}} + +finish_test + diff --git a/ext/vec1/test/vec1cat.test b/ext/vec1/test/vec1cat.test new file mode 100644 index 0000000000..9cf74c1c40 --- /dev/null +++ b/ext/vec1/test/vec1cat.test @@ -0,0 +1,247 @@ +# 2026-01-17 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1cat +vec1_load db + +do_execsql_test 1.0 { + PRAGMA table_info = vec1cat +} { + 0 database {} 0 {} 0 + 1 name {} 0 {} 0 + 2 model {} 0 {} 0 + 3 qprofile {} 0 {} 0 +} + +do_execsql_test 1.1 { + CREATE VIRTUAL TABLE x1 USING vec1(x); + SELECT database, name FROM vec1cat; +} { + main x1 +} + +db close +sqlite3 db test.db +vec1_load db + +do_execsql_test 1.2 { + SELECT database, name, model FROM vec1cat; +} { + main x1 {{"index": "none", "distance": "l2"}} +} + +do_execsql_test 1.3 { + INSERT INTO x1(cmd, arg) VALUES( 'rebuild', '{index:"flat", distance:"cos"}'); +} + +do_execsql_test 1.4 { + SELECT database, name, model FROM vec1cat; +} { + main x1 {{"index": "flat", "distance": "cos"}} +} + +db close +sqlite3 db test.db +vec1_load db + +do_execsql_test 1.5 { + SELECT database, name, model FROM vec1cat; +} { + main x1 {{"index": "flat", "distance": "cos"}} +} + +do_execsql_test 1.6 { + INSERT INTO x1(cmd, arg) VALUES('rebuild', ( + WITH s(i) AS (SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1000) + SELECT vec1_train(random_vector(16), '{nbucket:8, codesize:8}') FROM s + )); + SELECT database, name, model FROM vec1cat; +} { + main x1 + {{"index": "trained", "distance": "l2", "nbucket": 8, "quantizer": "pq", "codesize": 8, "residual": true, "wht": false}} +} + +do_execsql_test 1.7 { + CREATE VIRTUAL TABLE temp.x2 USING vec1(x); + SELECT rowid, database, name, model FROM vec1cat; +} { + 1 temp x2 + {{"index": "none", "distance": "l2"}} + 2 main x1 + {{"index": "trained", "distance": "l2", "nbucket": 8, "quantizer": "pq", "codesize": 8, "residual": true, "wht": false}} +} + +foreach {tn spec res} { + + 1 {nbucket:0, codesize:8} { + "index": "trained", "distance": "l2", "nbucket": 0, + "quantizer": "pq", "codesize": 8, + "residual": false, "wht": false + } + + 2 {nbucket:8, codesize:0} { + "index": "trained", "distance": "l2", "nbucket": 8, + "quantizer": "none", "codesize": 0, + "residual": false, "wht": false + } + + 3 {nbucket:0, codesize:8, opq:1} { + "index": "trained", "distance": "l2", "nbucket": 0, + "quantizer": "opq", "codesize": 8, + "residual": false, "wht": false + } + + 4 {codesize:2, quantizer: "bq", distance: "cos"} { + "index": "trained", "distance": "cos", "nbucket": 0, + "quantizer": "bq", "codesize": 2, + "residual": false, "wht": true + } + + 5 {codesize:1, nbucket: 16, quantizer: "bq", distance: "l2", wht: 0} { + "index": "trained", "distance": "l2", "nbucket": 16, + "quantizer": "bq", "codesize": 1, + "residual": true, "wht": false + } + + 6 {codesize:0, nbucket: 0} { + "index":"flat", "distance":"l2" + } + + 7 {} { + "index":"flat", "distance":"l2" + } + +} { + + set model [db one { + WITH s(i) AS (SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1000) + SELECT vec1_train(random_vector(16), '{'||$spec||'}') FROM s + }] + + set output [db one { SELECT json('{' || $res || '}') }] + + do_execsql_test 1.8.$tn.1 { + INSERT INTO x1(cmd, arg) VALUES( 'rebuild', $model ); + SELECT json(model) FROM vec1cat WHERE name='x1'; + } [list $output] + + if {[db one {SELECT json_extract($output, '$.index')=='trained'}]} { + set model [db one { + WITH s(i) AS (SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1000) + SELECT vec1_train(random_vector(16), $output) FROM s + }] + } else { + set model $output + } + do_execsql_test 1.8.$tn.2 { + INSERT INTO x1(cmd, arg) VALUES( 'rebuild', $model ); + SELECT json(model) FROM vec1cat WHERE name='x1'; + } [list $output] +} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 2.0 { + CREATE VIRTUAL TABLE x1 USING vec1(x); + CREATE VIRTUAL TABLE x2 USING vec1(y); + CREATE VIRTUAL TABLE x3 USING vec1(z); +} + +do_execsql_test 2.1 { + SELECT rowid, name FROM vec1cat +} { + 1 x3 2 x2 3 x1 +} + +do_test 2.2 { + list [catch { + db eval { + SELECT rowid, name FROM vec1cat + } { + if {$name=="x2"} { + db eval { + DROP TABLE x2; + DROP TABLE x1; + } + } + } + } msg] $msg +} {1 {database table is locked}} + +do_execsql_test 2.3 { + SELECT qprofile FROM vec1cat WHERE name='x1'; +} {{{ + "total": {"cnt": 0, "cycle": 0}, + "init": {"cnt": 0, "cycle": 0}, + "coarse": {"cnt": 0, "cycle": 0}, + "idxread": {"cnt": 0, "cycle": 0}, + "metaread": {"cnt": 0, "cycle": 0}, + "metascan": {"cnt": 0, "cycle": 0}, + "lut": {"cnt": 0, "cycle": 0}, + "pqscan": {"cnt": 0, "cycle": 0}, + "finalsort": {"cnt": 0, "cycle": 0} +}}} + +#------------------------------------------------------------------------- +reset_db +vec1_load db +do_execsql_test 3.0 { + CREATE VIRTUAL TABLE x2 USING vec1(y); + INSERT INTO x2 VALUES( random_vector(13) ); + INSERT INTO x2 VALUES( random_vector(13) ); + INSERT INTO x2 VALUES( random_vector(13) ); + INSERT INTO x2 VALUES( random_vector(13) ); + INSERT INTO x2 VALUES( random_vector(13) ); + + INSERT INTO x2(cmd, arg) VALUES('rebuild', ( + WITH s(i) AS ( VALUES (1) UNION ALL SELECT i+1 FROM s WHERE i<512 ) + SELECT vec1_train( random_vector(13), '{nbucket:8}' ) FROM s + )) +} + +do_execsql_test 3.1 { + SELECT count(distance) FROM x2; +} 5 + +#------------------------------------------------------------------------- +reset_db +vec1_load db +do_execsql_test 4.0 { + CREATE VIRTUAL TABLE x1 USING vec1(y, a); + + INSERT INTO x1(cmd, arg) VALUES('rebuild', ( + WITH s(i) AS ( VALUES (1) UNION ALL SELECT i+1 FROM s WHERE i<512 ) + SELECT vec1_train( + random_vector(32), '{nbucket:8, codesize:8, distance:"cos"}' + ) FROM s + )) +} + +do_execsql_test 4.1 { + INSERT INTO x1 (rowid, y, a) VALUES(10, random_vector(32), 4556); +} + +do_execsql_test 4.2 { + SELECT key FROM json_each( (SELECT distance FROM x1) ) +} {bucket coarse_error reconstruction_error code} + +do_execsql_test 4.3 { + SELECT typeof(distance) FROM x1(random_vector(32), '{nprobe:8, K:5}') +} {real} + + +finish_test + diff --git a/ext/vec1/test/vec1cat2.test b/ext/vec1/test/vec1cat2.test new file mode 100644 index 0000000000..d644194c1b --- /dev/null +++ b/ext/vec1/test/vec1cat2.test @@ -0,0 +1,125 @@ +# 2026-08-20 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1cat2 +vec1_load db + + +foreach {tn training expect} { + 1 { codesize: 16 } { + "index": "trained", + "distance": "l2", + "nbucket": 0, + "quantizer": "pq", + "codesize": 16, + "residual": false, + "wht": false + } + + 2 { quantizer: "pq" } { + "index": "trained", + "distance": "l2", + "nbucket": 0, + "quantizer": "pq", + "codesize": 16, + "residual": false, + "wht": false + } + + 3 { quantizer: "opq" } { + "index": "trained", + "distance": "l2", + "nbucket": 0, + "quantizer": "opq", + "codesize": 32, + "residual": false, + "wht": false + } + + 4 { quantizer: "bq" } { + "index": "trained", + "distance": "l2", + "nbucket": 0, + "quantizer": "bq", + "codesize": 16, + "residual": false, + "wht": true + } + + 5 { nbucket: 10 } { + "index": "trained", + "distance": "l2", + "nbucket": 10, + "quantizer": "none", + "codesize": 0, + "residual": false, + "wht": false + } + + 6 { nbucket: 10, quantizer: "pq" } { + "index": "trained", + "distance": "l2", + "nbucket": 10, + "quantizer": "pq", + "codesize": 16, + "residual": true, + "wht": false + } + + 7 { quantizer: "pq", codesize: 0 } { + "index": "trained", + "distance": "l2", + "nbucket": 0, + "quantizer": "pq", + "codesize": 16, + "residual": false, + "wht": false + } +} { + + do_execsql_test 1.$tn.1 { + DROP TABLE IF EXISTS v1; + CREATE VIRTUAL TABLE v1 USING vec1 (v); + } + + do_execsql_test 1.$tn.2 { + + WITH s(i) AS ( SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<512 ), + data(v) AS ( SELECT random_vector(128) FROM s) + + INSERT INTO v1(cmd, arg) VALUES('rebuild', ( + SELECT vec1_train(v, '{' || $training || '}') FROM data + )); + + SELECT json(model) FROM vec1cat; + } [db eval { SELECT json( '{' || $expect || '}' )} ] + + do_execsql_test 1.$tn.3 { + + WITH s(i) AS ( SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<512 ), + data(v) AS ( SELECT random_vector(128) FROM s) + + INSERT INTO v1(cmd, arg) VALUES('rebuild', ( + SELECT vec1_train(v, (SELECT model FROM vec1cat)) FROM data + )); + + SELECT json(model) FROM vec1cat; + } [db eval { SELECT json( '{' || $expect || '}' )} ] + +} + + + +finish_test + diff --git a/ext/vec1/test/vec1config.test b/ext/vec1/test/vec1config.test new file mode 100644 index 0000000000..5e8cc92bc4 --- /dev/null +++ b/ext/vec1/test/vec1config.test @@ -0,0 +1,113 @@ +# 2026-04-01 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1config +vec1_load db + +do_execsql_test 1.0 { SELECT vec1_config('nthread'); } {1} +do_execsql_test 1.1 { SELECT vec1_config('nprobe'); } {0.05} + +do_catchsql_test 1.2 { + SELECT vec1_config('nosuch'); +} {1 {vec1: no such parameter: nosuch}} + +do_catchsql_test 1.3.1 { + SELECT vec1_config('nthread', 0); +} {1 {vec1: nthread requires an integer value between 1 and 128}} +do_catchsql_test 1.3.2 { + SELECT vec1_config('nthread', 129); +} {1 {vec1: nthread requires an integer value between 1 and 128}} +do_test 1.3.3 { + catch { execsql { SELECT vec1_config('nthread', 128) } } +} {0} +do_catchsql_test 1.3.4 { + SELECT vec1_config('nthread', 1); +} {0 1} + +do_catchsql_test 1.4.1 { + SELECT vec1_config('nprobe', -1); +} {1 {vec1: nprobe must be a numeric value greater than 0.0}} +do_catchsql_test 1.4.2 { + SELECT vec1_config('nprobe', 0.0); +} {1 {vec1: nprobe must be a numeric value greater than 0.0}} +do_catchsql_test 1.4.3 { + SELECT vec1_config('nprobe', 4.0); +} {0 4.0} +do_catchsql_test 1.4.4 { + SELECT vec1_config('nprobe', 'hello'); +} {1 {vec1: nprobe must be a numeric value greater than 0.0}} +do_catchsql_test 1.4.5 { + SELECT vec1_config('nprobe', '1hello'); +} {1 {vec1: nprobe must be a numeric value greater than 0.0}} +do_catchsql_test 1.4.6 { + SELECT vec1_config('nprobe', NULL); +} {1 {vec1: nprobe must be a numeric value greater than 0.0}} + +do_catchsql_test 1.5.1 { + SELECT vec1_config('nthread', 'notaninteger'); +} {1 {vec1: nthread requires an integer value between 1 and 128}} + +#------------------------------------------------------------------------- + +do_execsql_test 2.0 { + CREATE VIRTUAL TABLE x1 USING vec1(v); +} + +do_catchsql_test 2.1.1 { + SELECT * FROM x1(random_vector(8), '{nprobe: -1, K: 500}') +} {1 {vec1: nprobe must be a numeric value greater than 0.0}} +do_catchsql_test 2.1.2 { + SELECT * FROM x1(random_vector(8), '{nprobe: 0.0, K: 500}') +} {1 {vec1: nprobe must be a numeric value greater than 0.0}} +do_catchsql_test 2.1.3 { + SELECT * FROM x1(random_vector(8), '{nprobe: "world", K: 500}') +} {1 {vec1: nprobe must be a numeric value greater than 0.0}} +do_catchsql_test 2.1.4 { + SELECT * FROM x1(random_vector(8), '{nprobe: "2world", K: 500}') +} {1 {vec1: nprobe must be a numeric value greater than 0.0}} +do_catchsql_test 2.1.5 { + SELECT * FROM x1(random_vector(8), '{nprobe: 0.5, K: 500}') +} {0 {}} + +do_catchsql_test 2.2.1 { + SELECT * FROM x1(random_vector(8), '{nprobe: 0.2, K: -1}') +} {1 {vec1: K must be an integer value between 0 and 33554432}} + +do_catchsql_test 2.2.2 { + SELECT * FROM x1(random_vector(8), -1) +} {1 {vec1: K must be an integer value between 0 and 33554432}} +do_catchsql_test 2.2.3 { + SELECT * FROM x1(random_vector(8), '1hello') +} {1 {malformed JSON}} +do_catchsql_test 2.2.4 { + SELECT * FROM x1(random_vector(8), '5') +} {0 {}} +do_catchsql_test 2.2.5 { + SELECT * FROM x1(random_vector(8), 5.0) +} {0 {}} +do_catchsql_test 2.2.6 { + SELECT * FROM x1(random_vector(8), '64.0') +} {0 {}} +do_catchsql_test 2.2.7 { + SELECT * FROM x1(random_vector(8), 4.5) +} {1 {vec1: K must be an integer value between 0 and 33554432}} + +do_catchsql_test 2.2.8 { + SELECT * FROM x1(random_vector(8), '{K:4.5}') +} {1 {vec1: K must be an integer value between 0 and 33554432}} + + + +finish_test + diff --git a/ext/vec1/test/vec1corrupt.test b/ext/vec1/test/vec1corrupt.test new file mode 100644 index 0000000000..8cd0b3883a --- /dev/null +++ b/ext/vec1/test/vec1corrupt.test @@ -0,0 +1,57 @@ +# 2026-08-21 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1corrupt +vec1_load db + +do_execsql_test 1.0 { + CREATE VIRTUAL TABLE v1 USING vec1; + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<100 + ) + INSERT INTO v1(rowid, vector) SELECT NULL, random_vector(128) FROM s; + + INSERT INTO v1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); +} + +do_execsql_test 1.1 { + PRAGMA integrity_check +} {ok} + +do_execsql_test 1.2 { + UPDATE v1_idx SET val = unhex( + substr( hex(val), 1, 8 ) || '40' || substr( hex(val), 11 ) + ); +} {} + +do_catchsql_test 1.3 { + SELECT count(rowid) FROM v1( random_vector(128), 100 ); +} {1 {database disk image is malformed}} + +do_execsql_test 1.3 { + PRAGMA integrity_check +} {{v1: %_idx entry id=1 is corrupt}} + +#------------------------------------------------------------------------- + + + + + + + + + +finish_test + diff --git a/ext/vec1/test/vec1create.test b/ext/vec1/test/vec1create.test new file mode 100644 index 0000000000..ff7d0b22de --- /dev/null +++ b/ext/vec1/test/vec1create.test @@ -0,0 +1,114 @@ +# 2026-08-19 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1config +vec1_load db + +do_execsql_test 1.0 { + CREATE VIRTUAL TABLE v1 USING vec1("vector column", "meta column"); +} + +do_execsql_test 1.1 { + SELECT v1."vector column", v1."meta column" FROM v1 +} + + +foreach {tn cols err} { + 1 { vector BLOB } {vec1: trailing characters after column name} + 2 { v, col BLOB } {vec1: trailing characters after column name} + 3 { v, [col BLOB]x } {vec1: trailing characters after column name} + 4 { "vector col" BLOB } {vec1: trailing characters after column name} + 5 { v, +i } {vec1: near "+": syntax error} +} { + execsql { DROP TABLE IF EXISTS v1 } + do_catchsql_test 1.2.$tn " + CREATE VIRTUAL TABLE v1 USING vec1( $cols ) + " [list 1 $err] +} + +foreach {tn cols lCol} { + 1 { vector } {vector} + 2 { "vector col" } {{vector col}} + 3 { v, 'meta 1' } {v {meta 1}} + 4 { v, 'Dave''s col' } {v {Dave's col}} + 5 { v, [column [[[] } {v {column [[[}} +} { + execsql { DROP TABLE IF EXISTS v1 } + + do_test 1.3.$tn { + execsql "CREATE VIRTUAL TABLE v1 USING vec1( $cols );" + set ret [list] + db eval { PRAGMA table_info = v1 } a { + lappend ret $a(name) + } + set ret + } $lCol +} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 2.0 { + CREATE VIRTUAL TABLE "Count Duckula's 11" USING vec1( + "vector column", "meta column" + ); +} + +do_execsql_test 2.1 { + SELECT * FROM "Count Duckula's 11"; +} + +do_execsql_test 2.2 { + ALTER TABLE "Count Duckula's 11" RENAME TO "Bill's table"; +} + +do_execsql_test 2.3 { + INSERT INTO "Bill's table" VALUES(random_vector(10), 1100); + INSERT INTO "Bill's table" VALUES(random_vector(10), 'text'); + INSERT INTO "Bill's table"(cmd, arg) VALUES('rebuild', '{index:"flat"}'); + PRAGMA integrity_check; +} {ok} + +do_execsql_test 2.4 { + ALTER TABLE "Bill's table" RENAME TO "a b c ''' d e f"; + PRAGMA integrity_check; +} {ok} + +#------------------------------------------------------------------------- +# Try to create tables with reserved column names "rowid" or "distance". +# Report https://sqlite.org/vec1/forumpost/05e888d619 +# +reset_db +vec1_load db + +breakpoint +foreach {tn cols err} { + 1 "rowid" {1 {vec1: reserved column name: rowid}} + 2 "ROWID" {1 {vec1: reserved column name: ROWID}} + 3 " [rowid] " {1 {vec1: reserved column name: [rowid]}} + 4 " \"rowid\" " {1 {vec1: reserved column name: "rowid"}} + 5 " 'roWid' " {1 {vec1: reserved column name: 'roWid'}} + 6 " [ rowid]" {0 {}} + 5 " 'ro''Wid' " {0 {}} +} { + execsql { DROP TABLE IF EXISTS g1 } + do_catchsql_test 3.$tn " + CREATE VIRTUAL TABLE g1 USING vec1 ( $cols ) + " $err +} + + +finish_test + diff --git a/ext/vec1/test/vec1delete.test b/ext/vec1/test/vec1delete.test new file mode 100644 index 0000000000..74353fcd1b --- /dev/null +++ b/ext/vec1/test/vec1delete.test @@ -0,0 +1,279 @@ +# 2026-04-02 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1delete +vec1_load db + +do_execsql_test 1.0 { + CREATE VIRTUAL TABLE v1 USING vec1; +} + +do_execsql_test 1.1 { + INSERT INTO v1(rowid, vector) VALUES(1, random_vector(128)); + INSERT INTO v1(rowid, vector) VALUES(2, random_vector(128)); + INSERT INTO v1(rowid, vector) VALUES(3, random_vector(128)); + INSERT INTO v1(rowid, vector) VALUES(4, random_vector(128)); + INSERT INTO v1(rowid, vector) VALUES(5, random_vector(128)); + + INSERT INTO v1(cmd, vector) VALUES('rebuild', ( + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<512 + ) SELECT vec1_train( random_vector(128), '{nbucket:4,codesize:8}' ) FROM s + )); +} + +do_execsql_test 1.2 { + INSERT INTO v1_base VALUES(6, random_vector(128)); +} + +do_catchsql_test 1.3 { + DELETE FROM v1 WHERE rowid=6 +} {1 {database disk image is malformed}} + +do_execsql_test 1.4 { + DELETE FROM v1_base WHERE rowid=6; + UPDATE v1_base SET vector = random_vector(32) WHERE id=5; +} {} + +do_catchsql_test 1.5 { + DELETE FROM v1 WHERE rowid=5 +} {1 {database disk image is malformed}} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 2.0 { + CREATE VIRTUAL TABLE v1 USING vec1; +} + +do_execsql_test 2.1 { + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1 + ) INSERT INTO v1(rowid, vector) SELECT i, random_vector(32) FROM s; + + INSERT INTO v1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); +} + +do_execsql_test 2.2 { + DELETE FROM v1 WHERE rowid=1; +} + +do_execsql_test 2.3 { + SELECT rowid FROM v1(random_vector(32), 10); +} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 2.0 { + CREATE VIRTUAL TABLE v1 USING vec1; + INSERT INTO v1(cmd, vector) VALUES('rebuild', ( + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1000 + ) SELECT vec1_train( random_vector(32), '{codesize:8}' ) FROM s + )); +} +do_execsql_test 2.1 { + BEGIN; + 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(32) FROM s; +} + +do_test 2.2 { + for {set ii 1} {$ii < 1000} {incr ii 2} { + execsql { + DELETE FROM v1 WHERE rowid=$ii + } + } + execsql COMMIT +} {} + +do_execsql_test 2.3 { + PRAGMA integrity_check; +} {ok} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 3.0 { + CREATE VIRTUAL TABLE v1 USING vec1(a, b); + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<500 + ) INSERT INTO v1(rowid, a) SELECT i, random_vector(10) FROM s; + + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<500 + ) INSERT INTO v1(rowid, a) SELECT i*10_000_000_000, random_vector(10) FROM s; + UPDATE v1 SET b=rowid; + + INSERT INTO v1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); +} + +do_execsql_test 3.1.1 { + SELECT rowid FROM v1(random_vector(10)) WHERE b=250 LIMIT 10; +} {250} +do_execsql_test 3.1.2 { + DELETE FROM v1 WHERE rowid = 250; +} {} +do_execsql_test 3.1.3 { + SELECT rowid FROM v1(random_vector(10)) WHERE b=250 LIMIT 10; +} {} + +do_execsql_test 3.2.0 { + SELECT rowid FROM v1 WHERE b=250*10_000_000_000 LIMIT 10; +} {2500000000000} +do_execsql_test 3.2.1 { + SELECT rowid FROM v1 (random_vector(10)) WHERE b=250*10_000_000_000 LIMIT 10; +} {2500000000000} +do_execsql_test 3.2.2 { + DELETE FROM v1 WHERE rowid = 250*10_000_000_000; +} {} +do_execsql_test 3.2.3 { + SELECT rowid FROM v1(random_vector(10)) WHERE b=250*10_000_000_000 LIMIT 10; +} {} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 4.0 { + CREATE VIRTUAL TABLE v1 USING vec1(a, b); + INSERT INTO v1(rowid, a, b) + VALUES(250*10_000_000_000, random_vector(10), 250*10_000_000_000); + + INSERT INTO v1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); +} + +do_execsql_test 4.1.0 { + SELECT rowid FROM v1 WHERE b=250*10_000_000_000 LIMIT 10; +} {2500000000000} +do_execsql_test 4.1.1 { + SELECT rowid FROM v1 (random_vector(10)) WHERE b=250*10_000_000_000 LIMIT 10; +} {2500000000000} +do_execsql_test 4.1.2 { + DELETE FROM v1 WHERE rowid = 250*10_000_000_000; +} {} +do_execsql_test 4.1.3 { + SELECT rowid FROM v1(random_vector(10)) WHERE b=250*10_000_000_000 LIMIT 10; +} {} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 5.0 { + CREATE VIRTUAL TABLE v1 USING vec1(a, b); + INSERT INTO v1 VALUES(random_vector(32), 'one'); + INSERT INTO v1 VALUES(random_vector(32), 'two'); + INSERT INTO v1 VALUES(random_vector(32), 'three'); + INSERT INTO v1 VALUES(random_vector(32), 'four'); + INSERT INTO v1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); +} + +do_execsql_test 5.1 { + BEGIN; + INSERT INTO v1(rowid, a, b) + VALUES(-20_000_000_000, random_vector(32), 'five'); + INSERT INTO v1(rowid, a, b) + VALUES(-30_000_000_000, random_vector(32), 'six'); + INSERT INTO v1(rowid, a, b) + VALUES(-40_000_000_000, random_vector(32), 'seven'); + DELETE FROM v1 WHERE rowid=3; +} + +do_execsql_test 5.2 { + SELECT rowid, length(a), b FROM v1 ORDER BY 1 +} { + -40000000000 128 seven + -30000000000 128 six + -20000000000 128 five + 1 128 one + 2 128 two + 4 128 four +} + +do_execsql_test 5.3 { + COMMIT; + PRAGMA integrity_check +} {ok} + +do_execsql_test 5.4 { + INSERT INTO v1_base(id, vector, c0) VALUES(11, 0, 'nan'); +} + +do_execsql_test 5.5 { + SELECT length(a) FROM v1 WHERE rowid=11 +} {{}} + +do_execsql_test 5.6 { + SELECT distance FROM v1 WHERE rowid=11 +} {{}} + +do_execsql_test 5.7 { + SELECT distance FROM v1 WHERE rowid=4 +} {{{bucket:0, coarse_error:0.000000, reconstruction_error:0.000000, code: null}}} + +do_execsql_test 5.8 { + SELECT distance, distance FROM v1 WHERE rowid=4 +} { + {{bucket:0, coarse_error:0.000000, reconstruction_error:0.000000, code: null}} + {{bucket:0, coarse_error:0.000000, reconstruction_error:0.000000, code: null}} +} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 6.0 { + CREATE VIRTUAL TABLE x1 USING vec1( v ); + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<400 + ) + INSERT INTO x1(rowid, v) + SELECT CASE WHEN i%2 THEN -i ELSE +i END, random_vector(8) FROM s; + + INSERT INTO x1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); +} + +do_execsql_test 6.1 { + PRAGMA integrity_check +} {ok} + +do_execsql_test 6.2 { + INSERT INTO x1(rowid, v) VALUES(0, random_vector(8)); +} {} + +do_execsql_test 6.3 { + DELETE FROM x1 WHERE abs(rowid)%10 = 1; +} {} + +do_execsql_test 6.4 { + PRAGMA integrity_check +} {ok} + +do_execsql_test 6.5 { + DELETE FROM x1 WHERE rowid = 0; +} {} + +do_execsql_test 6.6 { + PRAGMA integrity_check +} {ok} + + +finish_test + diff --git a/ext/vec1/test/vec1error.test b/ext/vec1/test/vec1error.test new file mode 100644 index 0000000000..a1eb042a53 --- /dev/null +++ b/ext/vec1/test/vec1error.test @@ -0,0 +1,85 @@ +# 2026-08-04 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1error +vec1_load db + +do_execsql_test 1.0 { + CREATE VIRTUAL TABLE v1 USING vec1(v); + INSERT INTO v1(rowid, v) VALUES(11, random_vector(8)); + INSERT INTO v1(rowid, v) VALUES(12, random_vector(8)); +} + +do_catchsql_test 1.1 { + SELECT rowid FROM v1(random_vector(8), '{K:10, nprobes:10}'); +} {1 {unrecognized option 'nprobes', should be one of nprobe, K, streaming, or nprobe_slack}} + +do_catchsql_test 1.2 { + SELECT vec1_train(v, '{quantizer:"donuts"}') FROM v1; +} {1 {unrecognized quantizer 'donuts', should be one of none, pq, opq, or bq}} + +do_catchsql_test 1.3 { + SELECT vec1_train(v, '{nthread:0}') FROM v1; +} {1 {vec1: nthread must be set to an integer value between 1 and 128}} + +#------------------------------------------------------------------------- +# Ensure that INSERT is not confused by a vector for which the squared +# L2 is close to INFINITY for all centroids. +# +reset_db +vec1_load db + +do_execsql_test 2.0 { + CREATE VIRTUAL TABLE v1 USING vec1(a); + WITH s(i) AS ( + VALUES(1) UNION ALL SELECT i+1 FROM s WHERE i<1000 + ) + INSERT INTO v1(rowid, a) SELECT i, random_vector(4) FROM s; + + INSERT INTO v1(cmd, arg) VALUES('rebuild', ( + SELECT vec1_train(a, '{nbucket:8}') FROM v1 + )); +} + +do_execsql_test 2.1 { + INSERT INTO v1(a) VALUES( vec1_from_json('[1e30, 1e30, 1e30, 1e30]') ); +} + +#------------------------------------------------------------------------- +# +reset_db +vec1_load db + +do_test 3.1 { sqlite3_db_config db TRUSTED_SCHEMA 0 } {0} + +do_execsql_test 3.2 { + CREATE VIRTUAL TABLE t1 USING vec1(x); + INSERT INTO t1 VALUES( random_vector(1024) ); + INSERT INTO t1 VALUES( random_vector(1024) ); + + CREATE VIEW v1 AS SELECT rowid, x FROM t1; +} + +do_execsql_test 3.3 { + SELECT rowid FROM t1 +} {1 2} + +do_execsql_test 3.4 { + SELECT rowid FROM v1 +} {1 2} + + + +finish_test + diff --git a/ext/vec1/test/vec1fault1.test b/ext/vec1/test/vec1fault1.test new file mode 100644 index 0000000000..c8abecc076 --- /dev/null +++ b/ext/vec1/test/vec1fault1.test @@ -0,0 +1,213 @@ +# 2026-04-01 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1fault1 +vec1_load db + +do_execsql_test 1.0 { + CREATE VIRTUAL TABLE mytbl USING vec1(a, b); + INSERT INTO mytbl(cmd, arg) VALUES('rebuild', '{index:"flat"}'); +} +faultsim_save_and_close + +do_faultsim_test 1 -faults oom* -prep { + faultsim_restore_and_reopen + vec1_load db + execsql { PRAGMA table_info = mytbl } +} -body { + execsql { + INSERT INTO mytbl VALUES(random_vector(128), 555); + } +} -test { + faultsim_test_result {0 {}} \ + {1 {unable to open a temporary database file for storing temporary tables}} +} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 2.0 { + CREATE VIRTUAL TABLE mytbl USING vec1(a, b); + WITH s(i) AS ( SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<600 ) + INSERT INTO mytbl SELECT random_vector(8), i FROM s; + INSERT INTO mytbl(cmd, arg) VALUES('rebuild', ( + SELECT vec1_train(a, '{codesize:8, nbucket:2}') FROM mytbl + )); +} + +do_faultsim_test 2 -faults oom* -body { + execsql { + PRAGMA integrity_check + } +} -test { + faultsim_test_result {0 ok} \ + {1 {unable to open a temporary database file for storing temporary tables}} +} + +#------------------------------------------------------------------------- +# OOM error while loading the extension. +# +reset_db + +breakpoint +if {[catch {db one {SELECT vec1_info()}}]} { + do_faultsim_test 3 -faults oom* -prep { + reset_db + } -body { + vec1_load_only db + } -test { + faultsim_test_result {0 {}} {1 {}} {1 {error during initialization: }} + } +} + +#------------------------------------------------------------------------- +# OOM error while deleting lots of rows +# +reset_db +vec1_load db + +do_execsql_test 4.0 { + CREATE VIRTUAL TABLE mytbl USING vec1(a); + WITH s(i) AS ( SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<100 ) + INSERT INTO mytbl SELECT random_vector(8) FROM s; + INSERT INTO mytbl(cmd, arg) VALUES('rebuild', '{index:"flat"}'); +} +faultsim_save_and_close + +do_faultsim_test 4 -faults oom* -prep { + faultsim_restore_and_reopen + vec1_load db + db eval { PRAGMA table_info = mytbl } +} -body { + execsql { + DELETE FROM mytbl WHERE rowid BETWEEN 30 AND 60 + } +} -test { + faultsim_test_result {0 {}} +} + +#------------------------------------------------------------------------- +# OOM error in vec1cat query +# +reset_db +vec1_load db + +do_execsql_test 5.0 { + CREATE VIRTUAL TABLE mytbl USING vec1(a, b); + CREATE VIRTUAL TABLE x1 USING vec1(a, b); +} + +set P {{{ + "total": {"cnt": 0, "cycle": 0}, + "init": {"cnt": 0, "cycle": 0}, + "coarse": {"cnt": 0, "cycle": 0}, + "idxread": {"cnt": 0, "cycle": 0}, + "metaread": {"cnt": 0, "cycle": 0}, + "metascan": {"cnt": 0, "cycle": 0}, + "lut": {"cnt": 0, "cycle": 0}, + "pqscan": {"cnt": 0, "cycle": 0}, + "finalsort": {"cnt": 0, "cycle": 0} +}}} + +do_faultsim_test 5 -faults oom-* -prep { + execsql { PRAGMA table_info = mytbl } +} -body { + execsql { + SELECT rowid, model, qprofile FROM vec1cat + } +} -test { + faultsim_test_result [subst {0 {1 {{"index": "none", "distance": "l2"}} $::P 2 {{"index": "none", "distance": "l2"}} $::P}}] +} + +#------------------------------------------------------------------------- +# OOM error in vec1FlatToNone() +# +reset_db +vec1_load db + +do_execsql_test 6.0 { + CREATE VIRTUAL TABLE hello USING vec1(a, b); + WITH s(i) AS ( SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<100 ) + INSERT INTO hello SELECT random_vector(8), i*100 FROM s; + + INSERT INTO hello(cmd, arg) VALUES('rebuild', '{index:"flat"}'); +} + +do_faultsim_test 6 -faults oom-* -prep { + sqlite3 db test.db + vec1_load db + execsql { PRAGMA table_info = mytbl } +} -body { + execsql { + INSERT INTO hello(cmd, arg) VALUES('rebuild', '{index:"flat"}'); + } +} -test { + faultsim_test_result {0 {}} +} + +#------------------------------------------------------------------------- +# OOM error in vec1FlatToNone() +# +reset_db +vec1_load db + +do_execsql_test 7.0 { + CREATE VIRTUAL TABLE hello USING vec1(a, b); + WITH s(i) AS ( SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1100 ) + INSERT INTO hello SELECT random_vector(8), i*100 FROM s; +} + + +set mod [db one { + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<600 + ) + SELECT vec1_train(random_vector(8), '{codesize:8}') FROM s +}] + +do_faultsim_test 7 -faults oom* -prep { + sqlite3 db test.db + vec1_load db + execsql { PRAGMA table_info = mytbl } +} -body { + execsql { + INSERT INTO hello(cmd, arg) VALUES('rebuild', $::mod); + } +} -test { + faultsim_test_result {0 {}} +} + +set mod [db one { + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<600 + ) + SELECT vec1_train(random_vector(8), '{nbucket:8}') FROM s +}] + +do_faultsim_test 8 -faults oom* -prep { + sqlite3 db test.db + vec1_load db + execsql { PRAGMA table_info = mytbl } +} -body { + execsql { + INSERT INTO hello(cmd, arg) VALUES('rebuild', $::mod); + } +} -test { + faultsim_test_result {0 {}} +} + + +finish_test + diff --git a/ext/vec1/test/vec1fault2.test b/ext/vec1/test/vec1fault2.test new file mode 100644 index 0000000000..1d20a2766d --- /dev/null +++ b/ext/vec1/test/vec1fault2.test @@ -0,0 +1,49 @@ +# 2026-04-01 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1fault2 + +#------------------------------------------------------------------------- +# OOM error in multi-threaded 'rebuild'. +# +reset_db +vec1_load db + +do_execsql_test 1.0 { + CREATE VIRTUAL TABLE mytbl USING vec1(a, b); + WITH s(i) AS ( SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<600 ) + INSERT INTO mytbl SELECT random_vector(8), i FROM s; +} +faultsim_save_and_close + +proc progress {percent msg} { } + +do_faultsim_test 1 -faults oom-t* -prep { + faultsim_restore_and_reopen + vec1_load db + db func progress progress + db eval { PRAGMA table_info = mytbl } + db eval { SELECT vec1_config('nthread', 2) } +} -body { + execsql { + INSERT INTO mytbl(cmd, arg) VALUES('rebuild', ( + SELECT vec1_train(a, '{codesize:8, progress: "progress"}') FROM mytbl + )); + } +} -test { + faultsim_test_result {0 {}} +} + +finish_test + diff --git a/ext/vec1/test/vec1fault3.test b/ext/vec1/test/vec1fault3.test new file mode 100644 index 0000000000..fe08e28ea4 --- /dev/null +++ b/ext/vec1/test/vec1fault3.test @@ -0,0 +1,106 @@ +# 2026-04-01 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1fault3 +vec1_load db + +do_execsql_test 1.0 { + CREATE VIRTUAL TABLE t1 USING vec1(a, b, c, d, e); + INSERT INTO t1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); +} +faultsim_save_and_close + +do_faultsim_test 1 -faults oom-t* -prep { + faultsim_restore_and_reopen + vec1_load db + db eval { PRAGMA table_info = t1 } + execsql { + INSERT INTO t1 VALUES(random_vector(8), NULL, $f, 'helloworld', X'123456') + } +} -body { + execsql BEGIN + for {set ii 0} {$ii < 1000} {incr ii} { + set f [expr rand()] + execsql { + INSERT INTO t1 VALUES(random_vector(8), NULL, $f, 'helloworld', X'123456') + } + } + execsql COMMIT +} -test { + faultsim_test_result {0 {}} + catchsql ROLLBACK +} + +do_faultsim_test 2 -faults oom* -prep { + faultsim_restore_and_reopen + vec1_load db +} -body { + execsql { + BEGIN; + INSERT INTO t1(rowid, a) VALUES(5555, random_vector(24)); + INSERT INTO t1(rowid, a) VALUES(20_000_000_000, random_vector(24)); + COMMIT; + } + +} -test { + faultsim_test_result {0 {}} + catchsql ROLLBACK +} + +#-------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 3.0 { + CREATE VIRTUAL TABLE t1 USING vec1(a, b); + INSERT INTO t1(cmd, arg) VALUES('rebuild', ( + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1000 + ) SELECT vec1_train( random_vector(16), '{codesize:8}' ) FROM s + )); +} +faultsim_save_and_close + +do_faultsim_test 3.1 -faults oom* -prep { + faultsim_restore_and_reopen + vec1_load db + execsql { + BEGIN; + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<100 + ) INSERT INTO t1(a, b) SELECT random_vector(16), i FROM s; + } +} -body { + for {set ii 1} {$ii < 30} {incr ii} { + execsql { DELETE FROM t1 WHERE rowid=$ii } + } +} -test { + faultsim_test_result {0 {}} + catchsql ROLLBACK +} + +do_faultsim_test 3.2 -faults oom* -prep { + faultsim_restore_and_reopen + vec1_load db + execsql { PRAGMA table_info = t1 } +} -body { + execsql { + INSERT INTO t1 VALUES( random_vector(16), 1100 ); + } +} -test { + faultsim_test_result {0 {}} +} + +finish_test + diff --git a/ext/vec1/test/vec1fault4.test b/ext/vec1/test/vec1fault4.test new file mode 100644 index 0000000000..2a31d3ece2 --- /dev/null +++ b/ext/vec1/test/vec1fault4.test @@ -0,0 +1,149 @@ +# 2026-04-01 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1fault4 +vec1_load db + +do_execsql_test 1.0 { + CREATE VIRTUAL TABLE t1 USING vec1(a, b); + INSERT INTO t1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<10000 + ) + INSERT INTO t1 SELECT random_vector(13), i+0.5 FROM s; +} +faultsim_save_and_close + +do_faultsim_test 1 -faults oom* -prep { + faultsim_restore_and_reopen + vec1_load db + db eval { PRAGMA table_info = t1 } +} -body { + execsql { + SELECT count(*) FROM t1(random_vector(13), 13); + } +} -test { + faultsim_test_result {0 13} +} + +do_faultsim_test 2 -faults oom* -prep { + faultsim_restore_and_reopen + vec1_load db + db eval { PRAGMA table_info = t1 } +} -body { + execsql { + SELECT count(*) FROM t1(random_vector(13), '{K:5000,streaming:true}'); + } +} -test { + faultsim_test_result {0 10000} +} + +do_execsql_test 2.1 { + INSERT INTO t1(cmd, arg) VALUES('rebuild', ( + SELECT vec1_train(a, '{nbucket:8}') FROM t1 WHERE rowid<1000 + )); +} +faultsim_save_and_close + +do_faultsim_test 2.2 -faults oom* -prep { + faultsim_restore_and_reopen + vec1_load db + db eval { PRAGMA table_info = t1 } +} -body { + execsql { + SELECT count(*) FROM t1( + random_vector(13), '{K:5000,streaming:true,nprobe:2}' + ); + } +} -test { + faultsim_test_result {0 10000} +} + + +faultsim_restore_and_reopen +vec1_load db +do_execsql_test 3.0 { + INSERT INTO t1(cmd, arg) VALUES('rebuild', ( + SELECT vec1_train(a, '{codesize:13}') FROM t1 WHERE rowid<1000 + )); +} +faultsim_save_and_close + +breakpoint +do_faultsim_test 3 -faults oom-t* -prep { + faultsim_restore_and_reopen + vec1_load db + db eval { PRAGMA table_info = t1 } +} -body { + execsql { + SELECT count(*) FROM t1(random_vector(13), 13); + } +} -test { + faultsim_test_result {0 13} +} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 4.0 { + CREATE VIRTUAL TABLE t1 USING vec1(a, b); + INSERT INTO t1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); + INSERT INTO t1(a, b) VALUES( random_vector(13), 1.123 ); +} +faultsim_save_and_close + +do_faultsim_test 4 -faults oom-t* -prep { + faultsim_restore_and_reopen + vec1_load db + db eval { PRAGMA table_info = t1 } +} -body { + execsql { + INSERT INTO t1(a, b) VALUES( random_vector(13), 'text' ); + } +} -test { + faultsim_test_result {0 {}} +} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 5.0 { + CREATE TABLE t1(a); + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<100 + ) + INSERT INTO t1 SELECT random_vector(12) FROM s; +} + +proc progress {args} {} + +faultsim_save_and_close +do_faultsim_test 5 -faults oom-t* -prep { + faultsim_restore_and_reopen + vec1_load db + db func progress progress + db eval { SELECT count(*) FROM t1 } +} -body { + execsql { + SELECT length( vec1_train(a, '{nbucket:8, progress:"progress"}') ) FROM t1 + } +} -test { + faultsim_test_result {0 408} +} + + +finish_test + diff --git a/ext/vec1/test/vec1fault5.test b/ext/vec1/test/vec1fault5.test new file mode 100644 index 0000000000..413ac4b6e6 --- /dev/null +++ b/ext/vec1/test/vec1fault5.test @@ -0,0 +1,42 @@ +# 2026-04-01 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1fault5 +vec1_load db + +do_execsql_test 1.0 { + CREATE TABLE t1(a); + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<512 + ) INSERT INTO t1 SELECT random_vector(16) FROM s; +} +do_execsql_test 1.1 { + SELECT min(length(a)) FROM t1 +} 64 + +do_execsql_test 1.2 { + SELECT length( vec1_train(a, '{codesize:8, opq:1, nopq_round:2,nbucket:2}') ) FROM t1; +} {17560} + +do_faultsim_test 1.3 -faults oom-t* -prep { +} -body { + execsql { + SELECT length( vec1_train(a, '{codesize:8, opq:1, nopq_round:2,nbucket:2}') ) FROM t1; + } +} -test { + faultsim_test_result {0 17560} +} + +finish_test + diff --git a/ext/vec1/test/vec1fault6.test b/ext/vec1/test/vec1fault6.test new file mode 100644 index 0000000000..2a680bc0da --- /dev/null +++ b/ext/vec1/test/vec1fault6.test @@ -0,0 +1,46 @@ +# 2026-04-01 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1fault6 +vec1_load db + +faultsim_save_and_close +do_faultsim_test 1 -faults oom* -prep { + faultsim_restore_and_reopen + vec1_load db +} -body { + execsql { + CREATE VIRTUAL TABLE v1 USING vec1(v, a, b); + } +} -test { + faultsim_test_result {0 {}} +} + +reset_db +vec1_load db +do_execsql_test 2.0 { + CREATE VIRTUAL TABLE x1 USING vec1(a, b, c); +} +faultsim_save_and_close +do_faultsim_test 2 -faults oom* -prep { + faultsim_restore_and_reopen + vec1_load db +} -body { + execsql { DROP TABLE x1 } +} -test { + faultsim_test_result {0 {}} +} + +finish_test + diff --git a/ext/vec1/test/vec1fault7.test b/ext/vec1/test/vec1fault7.test new file mode 100644 index 0000000000..631daf216b --- /dev/null +++ b/ext/vec1/test/vec1fault7.test @@ -0,0 +1,65 @@ +# 2026-04-01 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1fault7 + +vec1_load db + +do_execsql_test 1 { + CREATE VIRTUAL TABLE t1 USING vec1(v, b); + INSERT INTO t1 VALUES(random_vector(8), 10); + INSERT INTO t1 VALUES(random_vector(8), 20); + INSERT INTO t1 VALUES(random_vector(8), 30); + INSERT INTO t1 VALUES(random_vector(8), 40); + INSERT INTO t1 VALUES(random_vector(8), 50); + + INSERT INTO t1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); +} + +faultsim_save_and_close +do_faultsim_test 1 -faults oom* -prep { + faultsim_restore_and_reopen + vec1_load db +} -body { + execsql { + SELECT rowid, length(v) + FROM t1(random_vector(8), '{K:10}') + WHERE b IN (10, 30, 50) + ORDER BY 1; + } +} -test { + faultsim_test_result {0 {1 32 3 32 5 32}} +} + +do_execsql_test 2 { + INSERT INTO t1(cmd, arg) VALUES('rebuild', '{index:"none"}'); +} + +faultsim_save_and_close +do_faultsim_test 2 -faults oom* -prep { + faultsim_restore_and_reopen + vec1_load db +} -body { + execsql { + SELECT rowid + FROM t1(random_vector(8), '{K:10}') + WHERE b IN (10, 30, 50) + ORDER BY 1; + } +} -test { + faultsim_test_result {0 {1 3 5}} +} + +finish_test + diff --git a/ext/vec1/test/vec1fault8.test b/ext/vec1/test/vec1fault8.test new file mode 100644 index 0000000000..b8c9b06b7e --- /dev/null +++ b/ext/vec1/test/vec1fault8.test @@ -0,0 +1,124 @@ +# 2026-04-01 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1fault8 +vec1_load db + +do_execsql_test 1 { + CREATE VIRTUAL TABLE t1 USING vec1(v, b); + INSERT INTO t1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); +} + +faultsim_save_and_close +do_faultsim_test 1 -faults oom* -prep { + faultsim_restore_and_reopen + vec1_load db +} -body { + execsql { + BEGIN; + INSERT INTO t1 VALUES( random_vector(4), 1.5 ); + INSERT INTO t1 VALUES( random_vector(4), 2.5 ); + INSERT INTO t1 VALUES( random_vector(4), 3.5 ); + INSERT INTO t1 VALUES( random_vector(4), 4.5 ); + COMMIT; + } +} -test { + faultsim_test_result {0 {}} +} + +reset_db +vec1_load db +do_execsql_test 2.0 { + CREATE VIRTUAL TABLE t1 USING vec1(v, b); + INSERT INTO t1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); + INSERT INTO t1 VALUES( random_vector(10), -10_000_000_000 ); +} + +faultsim_save_and_close +do_faultsim_test 2 -faults oom* -prep { + faultsim_restore_and_reopen + vec1_load db +} -body { + execsql { + DELETE FROM t1 WHERE rowid=1; + } +} -test { + faultsim_test_result {0 {}} +} + +#-------------------------------------------------------------------------- +reset_db +vec1_load db +do_execsql_test 3.0 { + CREATE VIRTUAL TABLE t1 USING vec1(v, b); + INSERT INTO t1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); + INSERT INTO t1 VALUES( random_vector(10), 'hello'); + INSERT INTO t1 VALUES( random_vector(10), 'world'); + INSERT INTO t1 VALUES( random_vector(10), 'today'); +} + +do_execsql_test 3.1 { + SELECT rowid, length(v), length(v) FROM t1(random_vector(10), 5) + WHERE b IN ('hello', 'world', 'today') + ORDER BY 1 +} {1 40 40 2 40 40 3 40 40} + +faultsim_save_and_close +do_faultsim_test 3.1 -faults oom* -prep { + faultsim_restore_and_reopen + vec1_load db +} -body { + execsql { + SELECT rowid, length(v) FROM t1(random_vector(10), 5) + WHERE b IN ('hello', 'world', 'today') + ORDER BY 1 + } +} -test { + faultsim_test_result {0 {1 40 2 40 3 40}} +} + +do_faultsim_test 3.3 -faults oom* -prep { + faultsim_restore_and_reopen + vec1_load db +} -body { + execsql { + SELECT rowid, length(distance) BETWEEN 60 AND 80, b FROM t1; + } +} -test { + faultsim_test_result {0 {1 1 hello 2 1 world 3 1 today}} +} + +do_faultsim_test 3.4 -faults oom* -prep { + faultsim_restore_and_reopen + vec1_load db +} -body { + execsql { + SELECT rowid, b FROM t1(random_vector(10), 5) ORDER BY 1; + } +} -test { + faultsim_test_result {0 {1 hello 2 world 3 today}} +} + +reset_db +vec1_load db +do_faultsim_test 4 -faults oom* -body { + execsql { + SELECT vec1_info() IS NOT NULL + } +} -test { + faultsim_test_result {0 1} +} + +finish_test + diff --git a/ext/vec1/test/vec1fault9.test b/ext/vec1/test/vec1fault9.test new file mode 100644 index 0000000000..a0d50b42f6 --- /dev/null +++ b/ext/vec1/test/vec1fault9.test @@ -0,0 +1,82 @@ +# 2026-04-01 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1fault9 +vec1_load db + +do_faultsim_test 1 -faults oom* -body { + execsql { + SELECT vec1_to_json( vec1_from_json('[1,2,3,4,5,6,7,8,9,10]') ); + } +} -test { + faultsim_test_result {0 {{[1,2,3,4,5,6,7,8,9,10]}}} +} + +do_execsql_test 2 { + CREATE VIRTUAL TABLE x1 USING vec1(v, b); + WITH s(i) AS ( SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<20 ) + INSERT INTO x1 + SELECT rvec(12, i), CASE WHEN i%2 THEN 'one' ELSE 'two' END FROM s; + INSERT INTO x1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); +} + +do_faultsim_test 2 -faults oom* -body { + sqlite3 db test.db + sqlite3_db_config_lookaside db 0 0 0 + vec1_load db + set one one + execsql { + SELECT rowid FROM x1(rvec(12, 4000), 200) WHERE b=$one ORDER BY 1 + } +} -test { + faultsim_test_result \ + {0 {1 3 5 7 9 11 13 15 17 19}} \ + {1 {}} \ + {1 {error during initialization: }} +} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 3 { + CREATE VIRTUAL TABLE x1 USING vec1(v, b); + WITH s(i) AS ( SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<20 ) + INSERT INTO x1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); +} + +faultsim_save_and_close + +breakpoint +do_faultsim_test 3 -faults oom* -prep { + faultsim_restore_and_reopen + sqlite3_db_config_lookaside db 0 0 0 + vec1_load db +} -body { + set value "this_is_a_good_value" + execsql { + INSERT INTO x1 VALUES( rvec(44, 156), $value ); + } +} -test { + if {$testrc==0} { + set r [db one { + SELECT rowid FROM x1(rvec(44, 13), 10) WHERE b='this_is_a_good_value'; + }] + if {$r!=1} { error "unexected!" } + } + faultsim_test_result {0 {}} +} + +finish_test + diff --git a/ext/vec1/test/vec1fixed.test b/ext/vec1/test/vec1fixed.test new file mode 100644 index 0000000000..0bd7b67f2d --- /dev/null +++ b/ext/vec1/test/vec1fixed.test @@ -0,0 +1,111 @@ +# 2026-04-02 +# +# 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. +# +#*********************************************************************** +# +# A vec1 table "fixes" the vector size when either: +# +# * the first vector is written to the table, or +# * the table is trained by a model that specifies a vector size. +# +# This file contains tests of these two cases. +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1fixed +vec1_load db + +do_execsql_test 1.0 { + CREATE VIRTUAL TABLE v1 USING vec1; +} + +do_execsql_test 1.1 { + INSERT INTO v1(cmd, vector) VALUES('rebuild', ( + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<512 + ) SELECT vec1_train( random_vector(128), '{nbucket:4}' ) FROM s + )); +} + +do_catchsql_test 1.2 { + INSERT INTO v1 VALUES(random_vector(127)); +} {1 {vec1: unexpected vector blob size 508 bytes, expected 512}} + +do_catchsql_test 1.3 { + INSERT INTO v1(cmd, vector) VALUES('rebuild', ( + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<512 + ) SELECT vec1_train( random_vector(64), '{codesize:16}' ) FROM s + )); +} {1 {vec1: model/data vector size mismatch: 64/128}} + +do_catchsql_test 1.4 { + SELECT rowid FROM v1(random_vector(129)) +} {1 {vec1: unexpected vector blob size 516 bytes, expected 512}} + +#------------------------------------------------------------------------- +# Similar to tests 1.*, except fix the vector size by inserting a row. +# +reset_db +vec1_load db + +do_execsql_test 2.0 { + CREATE VIRTUAL TABLE v1 USING vec1; +} + +do_execsql_test 2.1 { + INSERT INTO v1 VALUES( random_vector(128) ); +} + +do_catchsql_test 2.2 { + INSERT INTO v1 VALUES(random_vector(127)); +} {1 {vec1: unexpected vector blob size 508 bytes, expected 512}} + +do_catchsql_test 2.3 { + INSERT INTO v1(cmd, vector) VALUES('rebuild', ( + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<512 + ) SELECT vec1_train( random_vector(64), '{codesize:16}' ) FROM s + )); +} {1 {vec1: model/data vector size mismatch: 64/128}} + +do_catchsql_test 2.4 { + SELECT rowid FROM v1(random_vector(129)) +} {1 {vec1: unexpected vector blob size 516 bytes, expected 512}} + +do_execsql_test 2.5 { + INSERT INTO v1 VALUES( random_vector(128) ); +} + +#------------------------------------------------------------------------- +# +reset_db +vec1_load db + +do_execsql_test 3.0 { + CREATE VIRTUAL TABLE v1 USING vec1; +} + +do_catchsql_test 3.1 { + INSERT INTO v1 VALUES( random_vector(1) ); +} {1 {invalid vector size: 4 bytes}} +do_catchsql_test 3.2 { + INSERT INTO v1 VALUES( random_vector(4097) ); +} {1 {invalid vector size: 16388 bytes}} +do_catchsql_test 3.3 { + INSERT INTO v1 VALUES( randomblob(207) ); +} {1 {invalid vector size: 207 bytes}} + +do_execsql_test 3.4 { + INSERT INTO v1 VALUES( random_vector(4096) ); +} + + +finish_test + diff --git a/ext/vec1/test/vec1flat.test b/ext/vec1/test/vec1flat.test new file mode 100644 index 0000000000..21f3cdfcad --- /dev/null +++ b/ext/vec1/test/vec1flat.test @@ -0,0 +1,390 @@ +# 2026-01-17 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1flat +vec1_load db + + +do_execsql_test 1.0 { + CREATE TABLE base(vector); + CREATE VIRTUAL TABLE v1 USING vec1; + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1000 + ) + INSERT INTO base(vector) SELECT random_vector(32) FROM s; + INSERT INTO v1(vector) SELECT vector FROM base; +} + +set rnd [db one {SELECT random_vector(32)}] + +foreach {tn idx func type} { + 1 { {index:"flat", distance: "l2"} } vec1_l2_distance integer + 2 { {index:"flat", distance: "cos"} } vec1_cos_distance integer + 3 { {index:"none", distance: "l2"} } vec1_l2_distance blob + 4 { {index:"none", distance: "cos"} } vec1_cos_distance blob +} { + do_execsql_test 1.$tn.1 { + INSERT INTO v1(cmd, vector) VALUES('rebuild', $idx); + } + + do_execsql_test 1.$tn.2 { + PRAGMA integrity_check + } {ok} + + set expect [db eval " + SELECT rowid FROM base ORDER BY $func (vector, \$rnd) LIMIT 50 + "] + + do_execsql_test 1.$tn.3 { + SELECT rowid FROM v1($rnd, '{K:50}') + } $expect + + do_execsql_test 1.$tn.4 { + SELECT distinct typeof(vector) FROM v1_base + } $type + + do_execsql_test 1.$tn.5 { + DELETE FROM v1; + SELECT rowid FROM v1($rnd, '{K:50}'); + } {} + + do_execsql_test 1.$tn.6 { + INSERT INTO v1(vector) SELECT vector FROM base; + SELECT rowid FROM v1($rnd, '{K:50}'); + } $expect + + do_execsql_test 1.$tn.7 { + SELECT distinct typeof(vector) FROM v1_base + } $type + + set expect [db eval " + SELECT rowid, $func (vector, \$rnd) FROM base + ORDER BY $func (vector, \$rnd) LIMIT 50 + "] + do_execsql_test 1.$tn.8 { + SELECT rowid, distance FROM v1($rnd, '{K:50}') + } $expect + + do_execsql_test 1.$tn.9 { + SELECT count( distance ) FROM v1 + } {1000} + +} + +do_execsql_test 1.5 { + DELETE FROM v1; + INSERT INTO v1(vector) SELECT vector FROM base LIMIT 10; + INSERT INTO v1(cmd, vector) VALUES('rebuild', '{index:"flat"}'); +} + +do_execsql_test 1.5.1 { + SELECT rowid FROM v1; +} {1 2 3 4 5 6 7 8 9 10} + +do_execsql_test 1.5.2 { + BEGIN; + DELETE FROM v1 WHERE rowid IN (3, 7); + SELECT rowid FROM v1; +} {1 2 4 5 6 8 9 10} + +do_execsql_test 1.5.3 { + COMMIT; + SELECT rowid FROM v1; +} {1 2 4 5 6 8 9 10} + +do_execsql_test 1.5.4 { + INSERT INTO v1(rowid, vector) VALUES(11, random_vector(32)); + SELECT rowid FROM v1; +} {1 2 4 5 6 8 9 10 11} + +do_catchsql_test 1.5.5 { + INSERT INTO v1(rowid, vector) VALUES(8, random_vector(32)); +} {1 {constraint failed}} + +do_execsql_test 1.6.1 { + SELECT rowid, length(vector) FROM v1 WHERE rowid=5; +} {5 128} + +do_execsql_test 1.6.2 { + SELECT rowid, length(vector) FROM v1 WHERE rowid=7; +} {} + +#------------------------------------------------------------------------ +# Now try tests on {codesize:0, nbucket:32} indexes. These are also +# flat, as they store full vectors in the index. +# +reset_db +vec1_load db + +set rnd [db one {SELECT random_vector(32)}] + +do_execsql_test 2.0 { + CREATE TABLE base(vector); + CREATE VIRTUAL TABLE v1 USING vec1; + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1000 + ) + INSERT INTO base(vector) SELECT random_vector(32) FROM s; + INSERT INTO v1(vector) SELECT vector FROM base; +} + +do_execsql_test 2.1 { + INSERT INTO v1(cmd, vector) VALUES('rebuild', ( + SELECT vec1_train(vector, '{codesize:0, nbucket:32}') FROM base + )); +} + +do_execsql_test 2.2 { + PRAGMA integrity_check +} {ok} + +do_execsql_test 2.3.1 { + SELECT rowid FROM v1 WHERE rowid=123 +} {123} +do_execsql_test 2.3.2 { + DELETE FROM v1 WHERE rowid=123; + PRAGMA integrity_check; +} {ok} +do_execsql_test 2.3.3 { + SELECT rowid FROM v1 WHERE rowid=123 +} {} + +do_execsql_test 2.4.1 { + SELECT rowid, length(vector) FROM v1 WHERE rowid=789 +} {789 128} +do_execsql_test 2.4.2 { + SELECT count(*), sum(v1.vector=base.vector) + FROM v1 CROSS JOIN base + WHERE v1.rowid=base.rowid; +} {999 999} +do_execsql_test 2.4.3 { + SELECT count(*), sum(v1.vector=base.vector) + FROM base CROSS JOIN v1 + WHERE v1.rowid=base.rowid; +} {999 999} + +do_execsql_test 2.5.1 { + DELETE FROM v1; + INSERT INTO v1(vector) SELECT vector FROM base; + PRAGMA integrity_check; +} {ok} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 3.0 { + CREATE VIRTUAL TABLE "vector table" USING vec1(a); +} {} + +do_catchsql_test 3.1 { + INSERT INTO "vector table"(cmd, arg) VALUES('rebuild', '{index=flat}'); +} {1 {vec1: malformed JSON}} + +do_catchsql_test 3.2 { + INSERT INTO "vector table"(cmd, arg) VALUES('rebuild', X'001122334455'); +} {1 {vec1: model too small - 6 bytes}} + +#------------------------------------------------------------------------- +reset_db +vec1_load db +do_execsql_test 4.0 { + CREATE VIRTUAL TABLE vvv USING vec1(a, b); + WITH s(i) AS ( + VALUES(1) UNION ALL SELECT i+1 FROM s WHERE i<100 + ) + INSERT INTO vvv(rowid, a, b) + SELECT i * 1_000_000_000, random_vector(25), i FROM s; + + SELECT sum(rowid) FROM vvv; +} {5050000000000} + +do_execsql_test 4.1 { + INSERT INTO vvv(cmd, arg) VALUES('rebuild', '{index:"flat"}'); +} {} + +do_execsql_test 4.2 { + DELETE FROM vvv WHERE rowid IN ( + 21 * 1_000_000_000, 53 * 1_000_000_000, 2 * 1_000_000_000 + ); + SELECT sum(rowid) FROM vvv; +} {4974000000000} + +do_execsql_test 4.3 { + INSERT INTO vvv(cmd, arg) VALUES('rebuild', '{index:"none"}'); + SELECT sum(rowid) FROM vvv; +} {4974000000000} + +do_execsql_test 4.4 { + INSERT INTO vvv(cmd, arg) VALUES('rebuild', '{index:"flat"}'); + SELECT sum(rowid) FROM vvv; +} {4974000000000} + +# Corrupt the index entry +do_execsql_test 4.5.1 { + SELECT rowid FROM vvv_idx +} {1 2} +do_execsql_test 4.5.2 { + UPDATE vvv_idx SET val = substr(val, 1, 5000) WHERE rowid=2; +} {} +do_catchsql_test 4.5.3 { + INSERT INTO vvv(cmd, arg) VALUES('rebuild', '{index:"none"}'); +} {1 {database disk image is malformed}} +do_catchsql_test 4.5.4 { + SELECT rowid FROM vvv(random_vector(25)) LIMIT 100 +} {1 {database disk image is malformed}} + +do_execsql_test 4.6.1 { + UPDATE vvv_idx SET val = substr(val, 1, 11) WHERE rowid=2; +} {} +do_catchsql_test 4.6.2 { + INSERT INTO vvv(cmd, arg) VALUES('rebuild', '{index:"none"}'); +} {1 {database disk image is malformed}} +do_catchsql_test 4.6.3 { + SELECT rowid FROM vvv(random_vector(25)) LIMIT 100 +} {1 {database disk image is malformed}} + +#------------------------------------------------------------------------- +# This time corrupt the meta-values list. +# +reset_db +vec1_load db +do_execsql_test 5.0 { + CREATE VIRTUAL TABLE vvv USING vec1(a, b); + WITH s(i) AS ( + VALUES(1) UNION ALL SELECT i+1 FROM s WHERE i<100 + ) + INSERT INTO vvv(a, b) SELECT random_vector(25), i FROM s; + INSERT INTO vvv(cmd, arg) VALUES('rebuild', '{index:"flat"}'); +} {} + +do_execsql_test 5.1 { + UPDATE vvv_meta SET val = substr(val, 1, 50); +} + +do_catchsql_test 5.2 { + SELECT rowid FROM vvv(random_vector(25)) WHERE b=222 LIMIT 10 +} {1 {database disk image is malformed}} + +#------------------------------------------------------------------------- +# +reset_db +vec1_load db +do_execsql_test 6.0 { + CREATE VIRTUAL TABLE vvv USING vec1(a, b); + INSERT INTO vvv(a, b) VALUES( random_vector(25), 'first' ); + INSERT INTO vvv(a, b) VALUES( random_vector(25), 'second' ); + INSERT INTO vvv(a, b) VALUES( random_vector(25), 'third' ); + INSERT INTO vvv(cmd, arg) VALUES('rebuild', '{index:"flat"}'); +} {} + +do_execsql_test 6.1 { + SELECT rowid, length(a) FROM vvv(random_vector(25), 3) ORDER BY 1 +} {1 100 2 100 3 100} + +do_execsql_test 6.2 { + DELETE FROM vvv_base WHERE id=2; +} + +do_catchsql_test 6.3 { + SELECT rowid, length(a) FROM vvv(random_vector(25), 3) ORDER BY 1 +} {1 {database disk image is malformed}} + +do_execsql_test 6.4 { + INSERT INTO vvv_base(id, vector, c0) VALUES(2, 0, 'second'); + SELECT rowid, length(a) FROM vvv(random_vector(25), 3) ORDER BY 1 +} {1 100 2 100 3 100} + +do_execsql_test 6.5 { + INSERT INTO vvv(rowid, a, b) VALUES(400, random_vector(25), 'four hundred'); + INSERT INTO vvv_base(id, vector, c0) VALUES(4, 0, 'fourth'); + SELECT rowid, length(a) FROM vvv(random_vector(25), 4) ORDER BY 1 +} {1 100 2 100 3 100 400 100} + +do_execsql_test 6.6 { + SELECT rowid, length(a) FROM vvv ORDER BY 1 +} {1 100 2 100 3 100 4 {} 400 100} + + +do_execsql_test 6.7 { + DELETE FROM vvv_base WHERE id=4; + INSERT INTO vvv(a, b) VALUES( random_vector(25), 'fifth' ); +} + +do_execsql_test 6.8 { + UPDATE vvv_idx SET val = substr(val, 1, 400); +} + +do_catchsql_test 6.9 { + INSERT INTO vvv(a, b) VALUES( random_vector(25), 'sixth' ); +} {1 {database disk image is malformed}} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 7.0 { + CREATE VIRTUAL TABLE vvv USING vec1(a, b); + INSERT INTO vvv(a, b) VALUES( random_vector(25), 'first' ); + INSERT INTO vvv(a, b) VALUES( random_vector(25), 'second' ); + INSERT INTO vvv(a, b) VALUES( random_vector(25), 'third' ); + INSERT INTO vvv(cmd, arg) VALUES('rebuild', '{index:"flat"}'); +} {} + +do_execsql_test 7.1 { + INSERT INTO vvv(cmd, arg) VALUES('rebuild', NULL); +} + +do_execsql_test 7.2 { + PRAGMA integrity_check +} {ok} + +do_execsql_test 7.3 { + BEGIN; + INSERT INTO vvv(cmd, arg) VALUES('rebuild', NULL); +} + +do_execsql_test 7.4 { + PRAGMA integrity_check +} {ok} + +do_execsql_test 7.5 { + COMMIT +} + +do_execsql_test 7.6 { + PRAGMA integrity_check +} {ok} + +do_catchsql_test 7.7 { + BEGIN; + INSERT INTO vvv(cmd, arg) VALUES('rebuild', ( + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<500 + ) + SELECT vec1_train(random_vector(22), '{nbucket:4}') FROM s + )); +} {1 {vec1: model/data vector size mismatch: 22/25}} + +do_test 7.7.1 { + sqlite3_get_autocommit db +} 0 + +do_execsql_test 7.8 { + COMMIT; + PRAGMA integrity_check; +} {ok} + +finish_test + diff --git a/ext/vec1/test/vec1func.test b/ext/vec1/test/vec1func.test new file mode 100644 index 0000000000..f3acf42d2e --- /dev/null +++ b/ext/vec1/test/vec1func.test @@ -0,0 +1,115 @@ +# 2026-01-17 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1func +vec1_load db + +do_execsql_test 1.0 { + SELECT vec1_to_json( + vec1_from_json('[1.0, 2.0, 3.0, 4.0]') + ) +} {{[1,2,3,4]}} + +do_execsql_test 1.1 { + SELECT quote(vec1_from_json('[1.0, 2.0, 3.0, 4.0]')) +} {X'0000803F000000400000404000008040'} + +do_catchsql_test 1.2 { + SELECT vec1_to_json(X'') +} {1 {vec1: invalid vector argument}} + +do_catchsql_test 1.3 { + SELECT vec1_to_json(X'0000803F000000400000404000008040FF') +} {1 {vec1: invalid vector argument}} + +do_execsql_test 1.4 { + SELECT vec1_to_json(X'0000803F000000400000404000008040') +} {[1,2,3,4]} + +do_execsql_test 1.5 { + SELECT vec1_to_json_i(X'0000803F000000400000404000008040') +} {[1065353216,1073741824,1077936128,1082130432]} + +#------------------------------------------------------------------------- + +foreach {tn in out} { + 1 " [1,2,3] " "0 {{[1,2,3]}}" + 2 " [ 1 , 2 , 3 ] " "0 {{[1,2,3]}}" + 3 " {1,2,3} " "1 {error parsing json vector}" + 4 " [1,2,3" "1 {error parsing json vector}" + 5 " [1,2, ]" "1 {error parsing json vector}" + 6 " [1,2,3] x " "1 {error parsing json vector}" +} { + do_catchsql_test 2.$tn { + SELECT vec1_to_json( vec1_from_json($in) ) + } $out +} + +#------------------------------------------------------------------------- + +do_execsql_test 3.0 { + SELECT vec1_cos_distance(random_vector(9), random_vector(9)) + BETWEEN 0.0 AND 2.0 +} {1} + +do_catchsql_test 3.1 { + SELECT vec1_cos_distance(random_vector(9), random_vector(8)) +} {1 {vec1_cos_distance: bad arguments}} +do_catchsql_test 3.2 { + SELECT vec1_cos_distance(randomblob(37), randomblob(37)) +} {1 {vec1_cos_distance: bad arguments}} + +do_catchsql_test 3.3 { + SELECT vec1_l2_distance(random_vector(9), random_vector(8)) +} {1 {vec1_l2_distance: bad arguments}} +do_catchsql_test 3.4 { + SELECT vec1_l2_distance(randomblob(37), randomblob(37)) +} {1 {vec1_l2_distance: bad arguments}} + +#------------------------------------------------------------------------- +# Test that vectors of zero magnitude are considered orthogonal to all +# others, including itself. + +do_execsql_test 4.0 { + SELECT vec1_cos_distance(random_vector(9), zeroblob(4*9)); +} {1.0} +do_execsql_test 4.1 { + SELECT vec1_cos_distance(zeroblob(4*9), random_vector(9)); +} {1.0} + +#------------------------------------------------------------------------- +# Test that non-blob arguments are errors. +# +foreach {tn a2 bErr} { + 0 { (X'12345678', X'12345678') } 0 + 1 { (X'12345678', '12345678') } 1 + 2 { ('12345678', '12345678') } 1 + 3 { ('12345678', X'12345678') } 1 +} { + do_test 5.$tn.1 { + lindex [catchsql "SELECT vec1_l2_distance $a2"] 0 + } $bErr + do_test 5.$tn.2 { + lindex [catchsql "SELECT vec1_cos_distance $a2"] 0 + } $bErr +} + +#------------------------------------------------------------------------- +# + +do_execsql_test 6.0 { SELECT vec1_from_json(NULL) } {{}} +do_execsql_test 6.1 { SELECT vec1_from_json(NULL) IS NULL } {1} + +finish_test + diff --git a/ext/vec1/test/vec1growth.test b/ext/vec1/test/vec1growth.test new file mode 100644 index 0000000000..9276692d7d --- /dev/null +++ b/ext/vec1/test/vec1growth.test @@ -0,0 +1,114 @@ +# 2026-03-31 +# +# 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. +# +#*********************************************************************** +# +# This file contains tests for vec1. Specifically, it tests that the +# index does not grow indefinitely if there are lots of inserts/deletes +# and updates on the table. +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1growth +vec1_load db + +foreach {tn nMeta} { + 1 0 + 2 1 + 3 2 +} { + + set lMeta [list] + set cols "" + set exprs "" + for {set ii 0} {$ii<$nMeta} {incr ii} { + lappend lMeta c$ii + append cols ", c$ii" + append exprs ", i+$ii" + } + + do_execsql_test 1.$tn.0 " + DROP TABLE IF EXISTS v1; + CREATE VIRTUAL TABLE v1 USING vec1(vec $cols); + " + + do_execsql_test 1.1 " + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1_000 + ) + INSERT INTO v1(rowid, vec $cols) SELECT i, random_vector(32) $exprs FROM s; + " + + do_execsql_test 1.2 { + INSERT INTO v1(cmd, arg) VALUES('rebuild', ( + SELECT vec1_train(vec, '{nbucket:16, codesize:0}') FROM v1 + )); + } + + do_execsql_test 1.2.ic { + PRAGMA integrity_check + } {ok} + + # Get the original number of bytes of index data. + # + set nOrig [db one { SELECT sum( length(val) ) FROM v1_idx }] + set nMax [expr {($nOrig * 6)/5}] + + proc randint {min max} { + expr { int(abs(rand()) * (1+$max-$min)) } + } + + for {set iIter 0} {$iIter<20} {incr iIter} { + do_test 1.$tn.3.$iIter { + for {set ii 1} {$ii <= 1000} {incr ii} { + set r [randint 1 1000] + execsql { UPDATE v1 SET vec=random_vector(32) WHERE rowid=$r } + } + set nByte [db one { SELECT sum( length(val) ) FROM v1_idx }] + expr $nByte<=$nMax + } {1} + + do_execsql_test 1.$tn.3.$iIter.ic.(sz=$nByte,max=$nMax) { + PRAGMA integrity_check + } {ok} + } + +} + +reset_db +vec1_load db + +do_execsql_test 2.0 { + CREATE VIRTUAL TABLE vec USING vec1(a, b); + INSERT INTO vec(cmd, arg) VALUES('rebuild', '{index:"flat"}'); + BEGIN; +} +do_test 2.1 { + for {set ii 1} {$ii<=128} {incr ii} { + execsql { INSERT INTO vec(a, b) VALUES( random_vector(16), NULL ) } + } +} {} + +do_test 2.1 { + for {set ii 128} {$ii>1} {incr ii -1} { + execsql { DELETE FROM vec WHERE rowid=$ii } + } +} {} + +do_execsql_test 2.2 { + COMMIT; + PRAGMA integrity_check; +} {ok} + +do_execsql_test 2.3 { + SELECT count(*) FROM vec; +} 1 + +finish_test + diff --git a/ext/vec1/test/vec1intck.test b/ext/vec1/test/vec1intck.test new file mode 100644 index 0000000000..c2d5e01144 --- /dev/null +++ b/ext/vec1/test/vec1intck.test @@ -0,0 +1,293 @@ +# 2026-04-01 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1intck +vec1_load db + +do_execsql_test 1.0 { + CREATE VIRTUAL TABLE mytbl USING vec1(a, b); + WITH s(i) AS ( SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1000 ) + INSERT INTO mytbl SELECT random_vector(32), 'val'||i FROM s; + + INSERT INTO mytbl(cmd, arg) VALUES('rebuild', ( + SELECT vec1_train(a, '{codesize:8}') FROM mytbl + )); +} + +do_execsql_test 1.1 { + SELECT length(val) FROM mytbl_idx; +} {12076} + +do_execsql_test 1.2 { + PRAGMA integrity_check +} {ok} + +proc set_ntombstone {val n} { + set be32 [binary format I $n] + return [string replace $val 8 11 $be32] +} +db funct set_ntombstone set_ntombstone + +do_execsql_test 1.3 { + UPDATE mytbl_idx SET val = set_ntombstone(val, 4); +} {} + +do_execsql_test 1.4 { + PRAGMA integrity_check +} {{mytbl: bad tombstone count}} + +do_execsql_test 2.0 { + INSERT INTO mytbl(cmd) VALUES('rebuild'); +} + +proc zero_pq {val i} { + set iOff [expr 12 + 1000*4 + $i] + set ret $val + for {set jj 0} {$jj < 8} {incr jj} { + set ret [string replace $ret $iOff $iOff [binary format c 0]] + incr iOff 16 + } + return $ret +} +db func zero_pq zero_pq + +do_execsql_test 2.1 { + UPDATE mytbl_idx SET val = zero_pq(val, 4); +} {} + +do_execsql_test 2.2 { + PRAGMA integrity_check +} {{mytbl: %_idx PQ does not match calculated PQ for row 5}} + +#------------------------------------------------------------------ + +do_execsql_test 3.0 { + INSERT INTO mytbl(cmd, arg) VALUES('rebuild', ( + SELECT vec1_train(a, '{nbucket:16,codesize:32}') FROM mytbl + )); +} + +# Replace the vector for row 555 with a copy of the bucket vector that +# is most distant from the current vector. The idea being to pick a +# value guaranteed to be assigned to a different bucket than the current +# vector. +model2tbl model mytbl +do_execsql_test 3.1 { + UPDATE mytbl_base SET vector = + (SELECT coarse FROM model ORDER BY vec1_l2_distance(vector, coarse) DESC) + WHERE id=555; +} + +do_execsql_test 3.2 { + PRAGMA integrity_check +} {{mytbl: vector from row 555 is in wrong bucket}} + +do_execsql_test 3.3 { + UPDATE mytbl_base SET vector = random_vector(31) WHERE id=555; +} + +do_execsql_test 3.4 { + PRAGMA integrity_check +} {{mytbl: vector in %_base row 555 is wrong size}} + +do_execsql_test 3.5 { + UPDATE mytbl_base SET vector = random_vector(32); + INSERT INTO mytbl(cmd, arg) VALUES('rebuild', ( + SELECT vec1_train(a, '{nbucket:16,codesize:32}') FROM mytbl + )); +} + +do_execsql_test 3.6 { + UPDATE mytbl_base SET c0 = 'hello world' WHERE id=555 +} + +do_execsql_test 3.7 { + PRAGMA integrity_check +} {{mytbl: meta-value value mismatch for row 555}} + +do_execsql_test 3.8 { + UPDATE mytbl_base SET vector = random_vector(32); + INSERT INTO mytbl(cmd, arg) VALUES('rebuild', ( + SELECT vec1_train(a, '{codesize:16}') FROM mytbl + )); +} + +proc shorten {val} { + string range $val 0 end-1 +} +db func shorten shorten + +do_execsql_test 3.9 { + UPDATE mytbl_idx SET val = shorten(val); + PRAGMA integrity_check; +} { + {mytbl: %_idx entry id=1 is corrupt} +} + +do_execsql_test 3.10 { + INSERT INTO mytbl(cmd, arg) VALUES('rebuild', ( + SELECT vec1_train(a, '{nbucket:16}') FROM mytbl + )); +} + +do_execsql_test 3.11 { + UPDATE mytbl_base SET vector = vector+1 WHERE id=555; + PRAGMA integrity_check; +} { + {mytbl: bucket value in %_base row 555 is incorrect} +} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 4.0 { + CREATE VIRTUAL TABLE v1 USING vec1(x, a, b); + WITH s(i) AS ( SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<100 ) + INSERT INTO v1(rowid, x, a, b) + SELECT 10_000_000_000 + i, random_vector(8), 20_000_000_000+i, i FROM s; + INSERT INTO v1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); +} + +do_execsql_test 4.1 { + SELECT count(*) FROM v1; + DELETE FROM v1 WHERE rowid = 10_000_000_000 + 35; + SELECT count(*) FROM v1; +} {100 99} + +do_execsql_test 4.2 { + PRAGMA integrity_check +} {ok} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 5.0 { + CREATE VIRTUAL TABLE v1 USING vec1(x, a); + INSERT INTO v1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); + + INSERT INTO v1 VALUES(random_vector(8), 'helloworld'); + INSERT INTO v1 VALUES(random_vector(8), 4.5); + INSERT INTO v1 VALUES(random_vector(8), 1000); + INSERT INTO v1 VALUES(random_vector(8), X'12345678'); + + PRAGMA integrity_check +} {ok} + +foreach {tn update err} { + 1 { UPDATE v1_base SET c0='helloWORLD' WHERE id=1 } + {v1: meta-value value mismatch for row 1} + + 2 { UPDATE v1_base SET c0='helloworld2' WHERE id=1 } + {v1: meta-value value mismatch for row 1} + + 3 { UPDATE v1_base SET c0=4.5 WHERE id=1 } + {v1: meta-value value mismatch for row 1} + + 4 { UPDATE v1_base SET c0=5.5 WHERE id=2 } + {v1: meta-value value mismatch for row 2} + + 5 { UPDATE v1_base SET c0=1001 WHERE id=3 } + {v1: meta-value value mismatch for row 3} + + 6 { UPDATE v1_base SET c0=5 WHERE id=2 } + {v1: meta-value value mismatch for row 2} + + 7 { UPDATE v1_base SET c0='textvalue' WHERE id=2 } + {v1: meta-value value mismatch for row 2} + + 8 { UPDATE v1_base SET c0=X'ABCDEF' WHERE id=2 } + {v1: meta-value value mismatch for row 2} + + 9 { UPDATE v1_base SET c0=X'ABCDEF' WHERE id=4 } + {v1: meta-value value mismatch for row 4} + + 10 { UPDATE v1_base SET c0=X'00ABCDEF' WHERE id=4 } + {v1: meta-value value mismatch for row 4} + + 11 { DELETE FROM v1_base WHERE id=1 } + {v1: %_idx row 1 missing from %_base} + + 12 { INSERT INTO v1_base(id, vector, c0) VALUES(555,random_vector(8),'xyz') } + {v1: wrong number of entries in %_base - have 5, expect 4} + +} { + execsql BEGIN + execsql $update + do_execsql_test 5.$tn { + PRAGMA integrity_check + } [list $err] + execsql ROLLBACK +} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +execsql { SELECT vec1_config('nthread', 4) } + +do_execsql_test 6.0 { + CREATE VIRTUAL TABLE v1 USING vec1(x, a); + + INSERT INTO v1 VALUES(random_vector(8), 1.2); + INSERT INTO v1 VALUES(random_vector(8), 4.5); + INSERT INTO v1 VALUES(random_vector(8), 1000); + + INSERT INTO v1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); + + PRAGMA integrity_check; +} {ok} + +do_execsql_test 6.1 { + UPDATE v1 SET a = (rowid*1000); + INSERT INTO v1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); + INSERT INTO v1 VALUES(random_vector(8), NULL); + PRAGMA integrity_check; +} {ok} + +do_execsql_test 6.2.1 { + INSERT INTO v1 VALUES(random_vector(8), 1.234); + PRAGMA integrity_check; +} {ok} + +do_execsql_test 6.2.2 { + INSERT INTO v1 VALUES(random_vector(8), 'text'); + PRAGMA integrity_check; +} {ok} + +do_execsql_test 6.3 { + UPDATE v1 SET a = (rowid*1000); + INSERT INTO v1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); + INSERT INTO v1 VALUES(random_vector(8), 10_000_000_000); + PRAGMA integrity_check; +} {ok} + +do_execsql_test 6.4 { + UPDATE v1 SET a = (rowid*1000); + INSERT INTO v1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); + INSERT INTO v1 VALUES(random_vector(8), -10_000_000_000); + PRAGMA integrity_check; +} {ok} + +do_execsql_test 6.5 { + UPDATE v1 SET a = rowid; + INSERT INTO v1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); + INSERT INTO v1 VALUES(random_vector(8), -1); + PRAGMA integrity_check; +} {ok} + + +finish_test + diff --git a/ext/vec1/test/vec1intck2.test b/ext/vec1/test/vec1intck2.test new file mode 100644 index 0000000000..06006e5186 --- /dev/null +++ b/ext/vec1/test/vec1intck2.test @@ -0,0 +1,40 @@ +# 2026-04-01 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1intck2 +vec1_load db + +do_execsql_test 1.0 { + CREATE VIRTUAL TABLE mytbl USING vec1(a, b); + WITH s(i) AS ( SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1000 ) + INSERT INTO mytbl SELECT random_vector(32), 'val'||i FROM s; +} + +foreach {tn spec} { + 1 "{codesize: 8, nbucket: 8}" + 2 "{codesize: 8, nbucket: 8, residual: 0}" +} { + do_execsql_test 1.$tn.1 { + INSERT INTO mytbl(cmd, arg) VALUES('rebuild', ( + SELECT vec1_train(a, $spec) FROM mytbl + )); + } + + do_execsql_test 1.$tn.2 { + PRAGMA integrity_check + } {ok} +} + +finish_test + diff --git a/ext/vec1/test/vec1ivf.test b/ext/vec1/test/vec1ivf.test new file mode 100644 index 0000000000..75473f55dd --- /dev/null +++ b/ext/vec1/test/vec1ivf.test @@ -0,0 +1,63 @@ +# 2026-01-17 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1flat +vec1_load db + + +do_execsql_test 1.0 { + CREATE VIRTUAL TABLE v1 USING vec1(v); + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1000 + ) + INSERT INTO v1(v) SELECT random_vector(32) FROM s; + INSERT INTO v1(cmd, arg) VALUES('rebuild', ( + SELECT vec1_train(v, '{nbucket:24, codesize:0}') FROM v1 + )); +} + +db close +sqlite3 db test.db +vec1_load db + +do_execsql_test 1.1 { + SELECT length(v) FROM v1 LIMIT 1 +} {128} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 2.0 { + CREATE VIRTUAL TABLE v2 USING vec1(v); + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1000 + ) + INSERT INTO v2(v) SELECT random_vector(7) FROM s; +} + +breakpoint +do_execsql_test 2.1 { + INSERT INTO v2(cmd, arg) VALUES('rebuild', ( + SELECT vec1_train(v, '{nbucket:24, codesize:0}') FROM v2 + )); +} + +set v [db one { SELECT v FROM v2 WHERE rowid = 499 }] + +do_execsql_test 2.1 { +} + +finish_test + diff --git a/ext/vec1/test/vec1join.test b/ext/vec1/test/vec1join.test new file mode 100644 index 0000000000..bccf71a696 --- /dev/null +++ b/ext/vec1/test/vec1join.test @@ -0,0 +1,46 @@ +# 2026-08-04 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1join +vec1_load db + +do_execsql_test 1.0 { + CREATE VIRTUAL TABLE v1 USING vec1(a); + INSERT INTO v1(rowid, a) VALUES(1, vec1_from_json('[1,0,0,0]')); + INSERT INTO v1(rowid, a) VALUES(2, vec1_from_json('[0,1,0,0]')); + INSERT INTO v1(rowid, a) VALUES(3, vec1_from_json('[0,0,1,0]')); + INSERT INTO v1(rowid, a) VALUES(4, vec1_from_json('[0,0,0,1]')); + INSERT INTO v1(rowid, a) VALUES(5, vec1_from_json('[1,1,0,0]')); + INSERT INTO v1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); + CREATE TABLE q(id INTEGER PRIMARY KEY, qv BLOB); + INSERT INTO q VALUES(1, vec1_from_json('[1,0,0,0]')); +} + +# Q1: literal query vector +do_execsql_test 1.1 { + SELECT rowid FROM v1(vec1_from_json('[1,0,0,0]'), '{K:2}'); +} {1 5} + +# Q2: vector from table q, via CROSS JOIN +do_execsql_test 1.2 { + SELECT v.rowid FROM q CROSS JOIN v1(q.qv, '{K:2}') AS v; +} {1 5} + +# Q3: vector from table q, via CROSS JOIN +do_execsql_test 1.3 { + SELECT v.rowid FROM q, v1(q.qv, '{K:2}') AS v; +} {1 5} + +finish_test + diff --git a/ext/vec1/test/vec1limit.test b/ext/vec1/test/vec1limit.test new file mode 100644 index 0000000000..0d1ccf994d --- /dev/null +++ b/ext/vec1/test/vec1limit.test @@ -0,0 +1,97 @@ +# 2026-03-11 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1limit +vec1_load db + + +do_execsql_test 1.0 { + CREATE VIRTUAL TABLE v1 USING vec1(v, a, b); + INSERT INTO v1 VALUES(random_vector(16), 555, 111); + DELETE FROM v1; +} + +do_catchsql_test 1.1 { + SELECT * FROM v1(:q) +} {1 {vec1: unexpected vector blob size 0 bytes, expected 64}} + +set q [db one {SELECT random_vector(16)}] +foreach {tn sql} { + 1 { SELECT * FROM v1($q) } + 2 { SELECT * FROM v1($q) ORDER BY vec1_l2_distance(v, $q) LIMIT 10 } + 3 { SELECT * FROM v1($q) WHERE a!=b LIMIT 10 } + 4 { SELECT * FROM v1($q) LIMIT -1 } + 5 { SELECT * FROM v1($q) WHERE a LIKE 'p%' LIMIT 10 } +} { + do_catchsql_test 1.2.$tn $sql [list \ + 1 {vec1: no K value or visible LIMIT clause} + ] +} + +foreach {tn sql} { + 1 { SELECT * FROM v1($q) LIMIT 10 } + 2 { SELECT * FROM v1($q) WHERE a=555 LIMIT 10 } +} { + do_execsql_test 1.3.$tn $sql {} +} + +#------------------------------------------------------------------------- + +reset_db +vec1_load db + +do_execsql_test 2.0 { + CREATE TABLE corpus(id INTEGER PRIMARY KEY, v BLOB, title TEXT, cat TEXT); + INSERT INTO corpus(id,v,title,cat) + SELECT i, vec1_from_json( + '['|| (i%17) ||','|| (i%13) ||','|| (i%7) ||','|| (i%5) + ||','|| (i%23) ||','|| (i%3) ||','|| (i%11) ||','|| (i%19) ||']' + ), 'runbook '||i, + CASE WHEN i%3=0 THEN 'ops' WHEN i%3=1 THEN 'kb' ELSE 'security' END + FROM ( + WITH RECURSIVE r(i) AS (SELECT 1 UNION ALL SELECT i+1 FROM r WHERE i<2000) + SELECT i FROM r + ); + CREATE VIRTUAL TABLE docs USING vec1(embedding, title, category); + INSERT INTO docs(rowid, embedding, title, category) + SELECT id, v, title, cat FROM corpus; + INSERT INTO docs(cmd,arg) VALUES('rebuild',( + SELECT vec1_train(v,'{nbucket:8}') FROM corpus + )); +} + +foreach {tn K} { + 1 576460752303423616 + 2 1152921504606852096 + 3 1152921504606847232 + 4 1152921504606848000 + 5 1152921504606847232 +} { + set J "{K:$K, nprobe: 99}" + set K [expr $K] + + do_catchsql_test 2.1.$tn.1 { + SELECT count(*) FROM docs( vec1_from_json('[1,2,3,4,5,6,7,8]'), $J ); + } {1 {vec1: K must be an integer value between 0 and 33554432}} + + do_catchsql_test 2.1.$tn.2 { + SELECT count(*) FROM docs( vec1_from_json('[1,2,3,4,5,6,7,8]'), $K ); + } {1 {vec1: K must be an integer value between 0 and 33554432}} +} + + + + +finish_test + diff --git a/ext/vec1/test/vec1meta.test b/ext/vec1/test/vec1meta.test new file mode 100644 index 0000000000..28a2e417c8 --- /dev/null +++ b/ext/vec1/test/vec1meta.test @@ -0,0 +1,252 @@ +# 2026-01-17 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1meta +vec1_load db + +do_execsql_test 1.0 { + CREATE VIRTUAL TABLE v1 USING vec1(v, a, b); +} + +do_execsql_test 1.1 { + SELECT v, a, b FROM v1 +} + +do_execsql_test 1.2 { + PRAGMA table_info = v1_base; +} { + 0 id INTEGER 0 {} 1 + 1 vector BLOB 0 {} 0 + 2 c0 {} 0 {} 0 + 3 c1 {} 0 {} 0 +} + +do_execsql_test 1.3 { + PRAGMA table_xinfo = v1; +} { + 0 cmd {} 0 {} 0 1 + 1 arg {} 0 {} 0 1 + 2 distance {} 0 {} 0 1 + 3 v BLOB 0 {} 0 0 + 4 a {} 0 {} 0 0 + 5 b {} 0 {} 0 0 +} + +do_execsql_test 1.4 { + INSERT INTO v1 VALUES(random_vector(32), 'A', 'B'); +} + +do_execsql_test 1.5 { + SELECT length(v), a, b FROM v1 +} {128 A B} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +# There should be a limit of 256 meta-value columns. Check that this is +# enforced. +do_test 2.0 { + set lInt [list] + set res "" + for {set ii 0} {$ii < 5000} {incr ii} { + lappend lInt "c$ii" + set ct "CREATE VIRTUAL TABLE v$ii USING vec1 (v, [join $lInt ,])" + if {[catch {execsql $ct} res]} { + break + } + } + list $ii $res +} {256 {too many meta-data columns (max is 256)}} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 3.0 { + CREATE VIRTUAL TABLE v1 USING vec1(v, meta, m2); + INSERT INTO v1(cmd, v) VALUES('rebuild', '{index:"flat"}'); + INSERT INTO v1 VALUES(random_vector(32), 1234, 0); + INSERT INTO v1 VALUES(random_vector(32), 4321, 1); +} + +do_execsql_test 3.1 { + PRAGMA integrity_check; +} {ok} + +do_execsql_test 3.2 { + INSERT INTO v1(cmd, v) VALUES('rebuild', '{index:"flat"}'); +} + +do_execsql_test 3.3 { + PRAGMA integrity_check; +} {ok} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 4.1 { + CREATE VIRTUAL TABLE v1 USING vec1(v, m1, m2); + INSERT INTO v1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<5000 + ) + INSERT INTO v1 SELECT random_vector(64), i, i+1 FROM s; + PRAGMA integrity_check; +} {ok} + +set T [db one {SELECT vec1_train(v, '{nbucket:32, codesize:16}') FROM v1}] + +do_execsql_test 4.2 { + INSERT INTO v1(cmd, arg) VALUES('rebuild', $T); + PRAGMA integrity_check; +} {ok} + +execsql { SELECT vec1_config('nthread', 2) } +do_execsql_test 4.3 { + PRAGMA integrity_check; +} {ok} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 5.1 { + CREATE VIRTUAL TABLE v1 USING vec1(v, m1, m2); + INSERT INTO v1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); + BEGIN; + INSERT INTO v1(v, m1, m2) VALUES(random_vector(16), 1, 10); + INSERT INTO v1(v, m1, m2) VALUES(random_vector(16), 2, 20); + INSERT INTO v1(v, m1, m2) VALUES(random_vector(16), 3, 10); + INSERT INTO v1(v, m1, m2) VALUES(random_vector(16), 1, 20); + INSERT INTO v1(v, m1, m2) VALUES(random_vector(16), 2, 10); + INSERT INTO v1(v, m1, m2) VALUES(random_vector(16), 3, 20); + INSERT INTO v1(v, m1, m2) VALUES(random_vector(16), 1, 10); + INSERT INTO v1(v, m1, m2) VALUES(random_vector(16), 2, 20); + INSERT INTO v1(v, m1, m2) VALUES(random_vector(16), 3, 10); + COMMIT; + PRAGMA integrity_check; +} {ok} + +do_execsql_test 5.2.1 { + SELECT rowid FROM v1(random_vector(16), 3) WHERE m1=3 ORDER BY 1 +} {3 6 9} +do_execsql_test 5.2.2 { + SELECT rowid FROM v1(random_vector(16), 4) WHERE m2=20 ORDER BY 1 +} {2 4 6 8} +do_execsql_test 5.2.3 { + SELECT rowid FROM v1(random_vector(16), 3) WHERE m1=3.0 ORDER BY 1 +} {3 6 9} +do_execsql_test 5.2.4 { + SELECT rowid FROM v1(random_vector(16), 4) WHERE m2=20.0 ORDER BY 1 +} {2 4 6 8} + +do_execsql_test 5.3 { + INSERT INTO v1(cmd, arg) VALUES('rebuild', '{index:"none"}'); +} + +do_execsql_test 5.4.1 { + SELECT rowid FROM v1(random_vector(16), 3) WHERE m1=3 ORDER BY 1 +} {3 6 9} +do_execsql_test 5.4.2 { + SELECT rowid FROM v1(random_vector(16), 4) WHERE m2=20 ORDER BY 1 +} {2 4 6 8} +do_execsql_test 5.4.3 { + SELECT rowid FROM v1(random_vector(16), 3) WHERE m1=3.0 ORDER BY 1 +} {3 6 9} +do_execsql_test 5.4.4 { + SELECT rowid FROM v1(random_vector(16), 4) WHERE m2=20.0 ORDER BY 1 +} {2 4 6 8} + +do_execsql_test 5.5 { + SELECT rowid FROM v1(random_vector(16), 4) WHERE m2=x'1234' ORDER BY 1 +} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +set nRow [expr 200] + +do_execsql_test 6.0 { + CREATE VIRTUAL TABLE x1 USING vec1(v, x); + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<$nRow + ) + INSERT INTO x1 SELECT random_vector(8), i FROM s; + + INSERT INTO x1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); +} + +do_test 6.1 { + for {set ii 1} {$ii<=$nRow} {incr ii} { + set val [db one { + SELECT rowid FROM x1(random_vector(8), 1) WHERE x=$ii + }] + if {$val!=$ii} { error "failed for row $ii ($val)" } + } +} {} + +do_execsql_test 6.2 { + PRAGMA integrity_check +} {ok} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +set nRow [expr 200] + +do_execsql_test 7.0 { + CREATE VIRTUAL TABLE x1 USING vec1(v, x); + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<$nRow + ) + INSERT INTO x1 SELECT random_vector(8), (i%7) FROM s; + UPDATE x1 SET x=NULL WHERE x=6; + INSERT INTO x1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); +} + +do_execsql_test 7.1 { + PRAGMA integrity_check +} {ok} + + +set nExpect [expr {$nRow/7}] +do_execsql_test 7.2 { + SELECT count(*) FROM x1(random_vector(8), $nExpect) WHERE x IS NULL +} $nExpect + +for {set ii 0} {$ii<6} {incr ii} { + do_execsql_test 7.3.$ii { + SELECT count(*) FROM x1(random_vector(8), $nExpect) WHERE x = $ii + } $nExpect +} + +set nExpect [expr $nExpect+1] +do_execsql_test 7.4 { + SELECT rowid FROM x1(random_vector(8), $nExpect) + WHERE x IS NULL ORDER BY 1 +} [db eval { SELECT rowid FROM x1 WHERE (rowid%7)==6 }] +for {set ii 0} {$ii<6} {incr ii} { + do_execsql_test 7.5.$ii { + SELECT rowid FROM x1(random_vector(8), $nExpect) + WHERE x = $ii ORDER BY 1 + } [db eval { SELECT rowid FROM x1 WHERE (rowid%7)==$ii }] +} + + +finish_test + diff --git a/ext/vec1/test/vec1meta2.test b/ext/vec1/test/vec1meta2.test new file mode 100644 index 0000000000..c35297002c --- /dev/null +++ b/ext/vec1/test/vec1meta2.test @@ -0,0 +1,394 @@ +# 2026-03-12 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1meta2 +vec1_load db + +do_execsql_test 1.0 { + CREATE VIRTUAL TABLE v1 USING vec1(v, m1); + INSERT INTO v1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); + INSERT INTO v1(rowid, v, m1) VALUES + (1, random_vector(8), '456'), + (2, random_vector(8), 456), + (3, random_vector(8), 456.0), + (4, random_vector(8), NULL), + (5, random_vector(8), 'some text'), + (6, random_vector(8), 'some other text'), + (7, random_vector(8), 'this text'), + (8, random_vector(8), X'ABCDEF'), + (9, random_vector(8), X'AC'); + PRAGMA integrity_check; +} {ok} + +foreach {tn where res} { + 1 "m1 = 456" {2 3} + 2 "m1 = 'this text'" {7} + 3 "m1 = 'some text'" {5} + 4 "m1 = NULL" {} + 5 "m1 IS ?" {4} + 6 "m1 > 555" {1 5 6 7 8 9} + 7 "m1 >= 'some text'" {5 7 8 9} + 8 "m1 = '456'" {1} + 9 "m1 = X'ABCDEF'" {8} + 10 "m1 = X'ac'" {9} + 11 "m1 >= X'abcdef'" {8 9} + 12 "m1 > X'abcdef'" {9} + 13 "m1 IS NULL" {4} + 14 "m1 IS NOT NULL" {1 2 3 5 6 7 8 9} + 15 "m1 < 555" {2 3} + 16 "m1 <= '456'" {1 2 3} + 17 "m1 < 123.2" {} + 18 "m1 < 123" {} + 19 "m1 < 1123.2" {2 3} + 20 "m1 < 456.0" {} + 21 "m1 IS 456" {2 3} + + 22 "m1 > 'this'" {7 8 9} + 23 "m1 = ''" {} + 24 "m1 IS 'this text is'" {} + + 22 "m1 > X'ABCDEF1234'" {9} + 23 "m1 > X'ABCD'" {8 9} + 24 "m1 >= X''" {8 9} + 25 "m1 ISNULL" {4} +} { + set K [llength $res] + if {$K==0} {set K 100} + + do_execsql_test 1.1.$tn " + SELECT * FROM ( + SELECT rowid FROM v1(random_vector(8)) WHERE $where LIMIT $K + ) ORDER BY 1 LIMIT -1 + " $res +} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +proc rval {} { + expr (rand() * 10.0) +} +db func rval rval + +do_execsql_test 2.0 { + CREATE TABLE t1(v BLOB, m1, m2); + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<100 + ) + INSERT INTO t1 SELECT random_vector(12), rval(), rval() FROM s; + UPDATE t1 SET m1=NULL WHERE (rowid%7)==0; + UPDATE t1 SET m2=NULL WHERE (rowid%8)==0; + + CREATE VIRTUAL TABLE v1 USING vec1(v, m1, m2); + INSERT INTO v1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); + INSERT INTO v1(rowid, v, m1, m2) SELECT rowid, v, m1, m2 FROM t1; +} + +# Check the flags on the meta value arrays are set to 0x18. +# +# 0x08=real values. +# 0x10=contains null. +# +do_execsql_test 2.1 { + SELECT hex( substr(val, 1, 4) ) FROM v1_meta +} {00000018 00000018} + +foreach t {2 3} { + foreach {tn expr} { + 1 "m1 IS NULL" + 2 "m1 < 1.0" + 3 "m1 BETWEEN -2.5 AND 2.5" + 4 "m1 < 4 AND m2 >= 4" + 5 "m1 <= 'abc'" + 6 "m1 IS NOT NULL" + 7 "m1 < 1.1 AND m2 > 2.1" + } { + set expect [db eval "SELECT rowid FROM t1 WHERE $expr ORDER BY 1"] + do_execsql_test 2.$t.$tn.[llength $expect] " + SELECT rowid FROM v1(random_vector(12), 100) WHERE $expr ORDER BY 1 + " $expect + } + + do_execsql_test 2.$t.x { + PRAGMA integrity_check + } {ok} + + do_execsql_test 2.$t.y { + INSERT INTO t1 VALUES(random_vector(12), 'abc', NULL); + INSERT INTO v1(rowid, v, m1, m2) + SELECT rowid, v, m1, m2 FROM t1 WHERE rowid=last_insert_rowid(); + + SELECT hex( substr(val, 1, 4) ) FROM v1_meta + } {00000011 00000018} +} + +db func rval rval +proc rval {} { + expr {int(rand() * 255.0)} +} +do_execsql_test 2.4 { + DELETE FROM t1; + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<300 + ) + INSERT INTO t1 SELECT random_vector(12), rval(), rval() FROM s; + UPDATE t1 SET m1=NULL WHERE m1<6 OR rowid=43; + UPDATE t1 SET m2=NULL WHERE m2<6 OR rowid=43; + + DELETE FROM v1; + INSERT INTO v1(rowid, v, m1, m2) SELECT rowid, v, m1, m2 FROM t1; + INSERT INTO v1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); +} + +do_execsql_test 2.5 { + SELECT hex( substr(val, 1, 4) ) FROM v1_meta +} {00000012 00000012} + +foreach t {6 7 8} { + foreach {tn expr} { + + 1 "m1 IS NULL" + 2 "m1 = 11" + 3 "m1 < 22" + 4 "m1 < 254" + 5 "m1 > 180" + 6 "m1 IS NULL AND m2 IS NULL" + 7 "m1 = 10 AND m2>10" + 8 "m1>0 AND m2<100" + 9 "m1>=0" + 10 "m1>=130" + 11 "m1<=130" + + 12 "m1<='text'" + 13 "m1>'text'" + 14 "m1>1000" + + 15 "m1 = 100.0" + 16 "m1 > 100.0" + 17 "m1 < 100.0" + + 18 "m1 > 89.2" + 19 "m1 < 89.9" + 20 "m1 >= 89.9" + 21 "m1 BETWEEN -1 AND 201.5" + 22 "m1 = 101.2" + 23 "m2 IS NOT NULL" + 24 "m2 < NULL" + 25 "m1 >= -1" + 26 "m1 <= 255" + + 27 "m1 < 255.5" + 28 "m1 < 253.5" + 29 "m1 = NULL" + + 30 "m2 >= 0" + 31 "m2 >= 3.99" + 32 "m2 >= 13.99" + 33 "m2 >= 200" + 34 "m2 >= 300" + + 35 "m2 <= 0" + 36 "m2 <= 3.99" + 37 "m2 <= 135.99" + 38 "m2 <= 200" + 39 "m2 <= 300" + 40 "m2 <= -10_000_000_000" + + 41 "m1 IN (10, 20, 30)" + 42 "m1 IN ('10', 20, 30)" + 43 "m1 IN (NULL, NULL)" + 44 "m1 IN (NULL, 100, 101, 102)" + 45 "m1 IN (1000, NULL, 8, -456.123)" + + 46 "m2 IN (10, 20, 30)" + 47 "m2 IN ('10', 20, 30)" + 48 "m2 IN (NULL, NULL)" + 49 "m2 IN (NULL, 100, 101, 102)" + 50 "m2 IN (1000, NULL, 8, -456.123)" + + 51 "m2 = X'ABCD'" + 52 "m1 < X'ABCD'" + + 53 "m1 IS 132.5" + + 54 "m1 > -4.5" + 55 "m2 >= -4.5" + 56 "m2 < -4.5" + 57 "m2 <= -4.5" + 58 "m2 IS 132" + + } { + set expect [db eval "SELECT rowid FROM t1 WHERE $expr ORDER BY 1"] + do_execsql_test 2.$t.$tn.[llength $expect] " + SELECT rowid FROM ( + SELECT rowid FROM v1(random_vector(12)) WHERE $expr LIMIT 1000 + ) ORDER BY 1 LIMIT -1; + " $expect + } + + do_execsql_test 2.$t.x { + PRAGMA integrity_check + } {ok} + + if {$t==6} { + do_execsql_test 2.$t.y { + INSERT INTO t1 VALUES(random_vector(12), 1234, 'xyz'); + INSERT INTO v1(rowid, v, m1, m2) + SELECT rowid, v, m1, m2 FROM t1 WHERE rowid=last_insert_rowid(); + + SELECT hex( substr(val, 1, 4) ) FROM v1_meta + } {00000014 00000011} + } else { + do_execsql_test 2.$t.z { + INSERT INTO v1(cmd, arg) VALUES('rebuild', '{index:"none"}'); + } + } +} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +proc rval {} { + expr int(rand() * 100) * 100 +} +db func rval rval + +do_execsql_test 3.0 { + CREATE VIRTUAL TABLE v1 USING vec1(v, m1, m2); + CREATE TABLE t1(v, m1, m2); + + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<300 + ) + INSERT INTO t1 SELECT random_vector(15), rval(), rval() FROM s; + UPDATE t1 SET m1=NULL WHERE m1<500 OR rowid=43; + UPDATE t1 SET m2=NULL WHERE m2<500 OR rowid=43; + + INSERT INTO v1 SELECT * FROM t1; + INSERT INTO v1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); +} + +foreach t {1 2} { + foreach {tn expr} { + 1 "m1 = 4500" + 2 "m1 IS NULL" + 3 "m1 IS NOT NULL" + + 4 "m1 < 1000" + 5 "m2 > -1000" + + 4 "m1 < 'text'" + 5 "m2 > 'text'" + + 6 "m1 < 4500.5" + 7 "m1 <= 4500.5" + + 8 "m1 = NULL" + + 9 "m2 IN (1000, 1100, 1200, 1300)" + 10 "m2 IN (3000, 3100, 3200, 3300, NULL, 'text')" + 11 "m2 IN (10_000_000_000, 2147483647, -2147483649)" + + } { + set expect [db eval "SELECT rowid FROM t1 WHERE $expr ORDER BY 1"] + do_execsql_test 3.$t.$tn.[llength $expect] " + SELECT rowid FROM ( + SELECT rowid FROM v1(random_vector(15)) WHERE $expr LIMIT 1000 + ) ORDER BY 1 LIMIT -1; + " $expect + } +} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 4.0 { + CREATE VIRTUAL TABLE vec1tbl USING vec1(v, t); + INSERT INTO vec1tbl VALUES( + random_vector(8), hex(randomblob(100000)) + ); + SELECT length(t) FROM vec1tbl; +} {200000} + +do_execsql_test 4.1 { + INSERT INTO vec1tbl(cmd, arg) VALUES('rebuild', '{index:"flat"}'); + PRAGMA integrity_check; +} {ok} + +do_execsql_test 4.2 { + DELETE FROM vec1tbl_meta; + PRAGMA integrity_check; +} {{vec1tbl: error reading meta-list id=1,meta=0}} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 5.0 { + CREATE VIRTUAL TABLE vec1tbl USING vec1(v, t, u); + WITH s(i) AS ( SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<317 ) + INSERT INTO vec1tbl SELECT random_vector(15), 120, NULL FROM s; + + INSERT INTO vec1tbl(cmd, arg) VALUES('rebuild', '{index:"flat"}'); +} {} + +foreach {tn expr} { + 1 "t = 120" + 2 "u > 120" + 3 "t > 100" + 4 "t >= 101" + 5 "u >= 101" + 6 "t IN (100, 120, 140)" +} { + set res [db eval "SELECT rowid FROM vec1tbl WHERE $expr ORDER BY 1"] + do_execsql_test 5.1.$tn " + SELECT rowid FROM vec1tbl(random_vector(15), '{K:500}') + WHERE $expr ORDER BY 1 + " $res +} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 5.0 { + CREATE VIRTUAL TABLE vec1tbl USING vec1(v, t, u, x); + INSERT INTO vec1tbl VALUES(random_vector(15), NULL, 2147483647, 1); + INSERT INTO vec1tbl VALUES(random_vector(15), 520, 2147483646, 2); + INSERT INTO vec1tbl VALUES(random_vector(15), NULL, 2147483645, 255); + INSERT INTO vec1tbl VALUES(random_vector(15), NULL, NULL, NULL); + INSERT INTO vec1tbl(cmd, arg) VALUES('rebuild', '{index:"flat"}'); +} {} + +foreach {tn expr} { + 1 "u = 2147483647" + 2 "u IS NULL" + 3 "t = 2147483647" + 4 "t IS NULL" + 5 "x = 255" + 6 "x IS NULL" +} { + set res [db eval "SELECT rowid FROM vec1tbl WHERE $expr ORDER BY 1"] + do_execsql_test 5.1.$tn " + SELECT rowid FROM vec1tbl(random_vector(15), '{K:500}') + WHERE $expr ORDER BY 1 + " $res +} + + + +finish_test diff --git a/ext/vec1/test/vec1meta3.test b/ext/vec1/test/vec1meta3.test new file mode 100644 index 0000000000..9842f22c58 --- /dev/null +++ b/ext/vec1/test/vec1meta3.test @@ -0,0 +1,304 @@ +# 2026-01-17 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1meta3 +vec1_load db + +proc create_table {name nMeta} { + set meta "" + for {set i 0} {$i<$nMeta} {incr i} { + append meta ", m$i" + } + execsql "CREATE VIRTUAL TABLE $name USING vec1(v $meta)" +} + +do_test 1.0 { + create_table v1 100 +} {} + +do_test 1.1 { + create_table v2 256 +} {} + +do_test 1.2 { + list [catch { create_table v3 257 } msg] $msg +} {1 {too many meta-data columns (max is 256)}} + +do_test 1.3 { + set vals "" + for {set i 0} {$i<256} {incr i} { + append vals ", $i" + } + execsql "INSERT INTO v2 VALUES( random_vector(8) $vals )" +} {} + +do_test 1.4 { + for {set i 0} {$i<256} {incr i} { + set r [execsql "SELECT rowid FROM v2( random_vector(8), 1 ) WHERE m$i = $i"] + if {$r!="1"} { error "unexpected: $r" } + } + set {} {} +} {} + +do_execsql_test 1.5 { + SELECT rowid FROM v2( random_vector(8), 1 ) + WHERE m1 = 1 AND m2 = 2 AND m5 = 5 AND m4 = 4 AND m3 = 3; +} {1} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 2.0 { + CREATE VIRTUAL TABLE x1 USING vec1(v, a, b); + CREATE TABLE t1(z); + + INSERT INTO x1 VALUES(random_vector(8), 10, 20); + INSERT INTO t1 VALUES(10); +} + +do_execsql_test 2.1 { + SELECT x1.b FROM x1(random_vector(8), 5), t1 WHERE z=a; +} {20} + +do_execsql_test 2.1 { + SELECT x1.b FROM x1(random_vector(8), 5) ORDER BY x1.rowid LIMIT 5 +} {20} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 3.0 { + CREATE VIRTUAL TABLE x1 USING vec1(v, a, b); + INSERT INTO x1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); + + INSERT INTO x1 VALUES(random_vector(8), 10, 20); + INSERT INTO x1 VALUES(random_vector(8), 20, 30); +} + +do_execsql_test 3.1 { + INSERT INTO x1 VALUES(random_vector(8), 20_000_000_000, 40); +} {} + +#------------------------------------------------------------------------- +# https://sqlite.org/vec1/forumpost/0fd4e91444 +# +reset_db +vec1_load db + +do_execsql_test 4.0 { + CREATE VIRTUAL TABLE d_blob USING vec1(embedding, title, checksum); + CREATE VIRTUAL TABLE d_text USING vec1(embedding, title, checksum); + CREATE VIRTUAL TABLE d_none USING vec1(embedding, title, checksum); + + INSERT INTO d_blob(cmd,arg) VALUES('rebuild','{index:"flat"}'); + INSERT INTO d_text(cmd,arg) VALUES('rebuild','{index:"flat"}'); + INSERT INTO d_none(cmd,arg) VALUES('rebuild','{index:"none"}'); +} + +do_execsql_test 4.1 { + WITH RECURSIVE r(i) AS ( SELECT 1 UNION ALL SELECT i+1 FROM r WHERE i<200 ) + INSERT INTO d_blob(rowid, embedding, title, checksum) + SELECT i, vec1_from_json('['||i||','||(i*2)||',0,0]'), 'runbook '||i, + CAST(printf('%08d', i) AS BLOB) + FROM r; + + WITH RECURSIVE r(i) AS ( SELECT 1 UNION ALL SELECT i+1 FROM r WHERE i<200 ) + INSERT INTO d_text(rowid, embedding, title, checksum) + SELECT i, vec1_from_json('['||i||','||(i*2)||',0,0]'), 'runbook '||i, + printf('%08d', i) FROM r; + + WITH RECURSIVE r(i) AS ( SELECT 1 UNION ALL SELECT i+1 FROM r WHERE i<200 ) + INSERT INTO d_none(rowid, embedding, title, checksum) + SELECT i, vec1_from_json('['||i||','||(i*2)||',0,0]'), 'runbook '||i, + CAST(printf('%08d', i) AS BLOB) + FROM r; +} + +foreach tab {d_none d_text d_blob} { + do_execsql_test 4.2.$tab.1 " + DELETE FROM $tab WHERE rowid%2<>0; + " + + # Check that the filtered query can see the 100 survivors + do_execsql_test 4.2.$tab.2 [subst -nocommands { + SELECT count(*) + FROM $tab (vec1_from_json('[9,18,0,0]'), 900) + WHERE checksum IS NOT NULL + }] {100} + +} + +do_execsql_test 4.3.1 { + SELECT count(*) + FROM d_blob(vec1_from_json('[9,18,0,0]'),'{K:900}') + WHERE checksum = CAST('00000100' AS BLOB) +} 1 + +do_execsql_test 4.3.2 { + SELECT count(*) + FROM d_none(vec1_from_json('[9,18,0,0]'),'{K:900}') + WHERE checksum = CAST('00000100' AS BLOB) +} 1 + +do_execsql_test 4.3.3 { + SELECT count(*) + FROM d_text(vec1_from_json('[9,18,0,0]'),'{K:900}') + WHERE checksum = '00000100' +} 1 + +do_execsql_test 4.4 { + PRAGMA integrity_check +} {ok} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 5.0 { + CREATE VIRTUAL TABLE v1 USING vec1(v, a); + INSERT INTO v1 VALUES(random_vector(8), 1); + INSERT INTO v1 VALUES(random_vector(8), 2); + INSERT INTO v1 VALUES(random_vector(8), 3); + INSERT INTO v1 VALUES(random_vector(8), 4); + INSERT INTO v1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); +} + +do_execsql_test 5.1 { + SELECT rowid FROM v1( random_vector(8), 100 ) WHERE a=500; +} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 6.1.1 { + CREATE VIRTUAL TABLE v1 USING vec1(v, t); + WITH s(i) AS ( SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1000 ) + INSERT INTO v1 SELECT random_vector(16), 'tag' || hex(randomblob(1000)) FROM s; +} + +do_execsql_test 6.1.2 { + INSERT INTO v1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); + PRAGMA integrity_check; +} {ok} + +do_execsql_test 6.1.3 { + INSERT INTO v1(cmd, arg) VALUES('rebuild', '{index:"none"}'); +} {} + +sqlite3_limit db SQLITE_LIMIT_LENGTH 1000000 + +do_catchsql_test 6.1.4 { + INSERT INTO v1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); +} {1 {string or blob too big}} + +do_execsql_test 6.1.5 { + PRAGMA integrity_check; +} {ok} + +sqlite3_limit db SQLITE_LIMIT_LENGTH 100000000 + +do_execsql_test 6.1.6 { + INSERT INTO v1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); + PRAGMA integrity_check; +} {ok} + +do_execsql_test 6.2.0 { + DROP TABLE IF EXISTS v1; + CREATE VIRTUAL TABLE v1 USING vec1(v, t); + WITH s(i) AS ( SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1000 ) + INSERT INTO v1 SELECT random_vector(200), 'tag' || hex(randomblob(1)) FROM s; +} + +sqlite3_limit db SQLITE_LIMIT_LENGTH 500000 + +do_catchsql_test 6.2.1 { + INSERT INTO v1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); +} {1 {string or blob too big}} + +do_execsql_test 6.2.2 { + PRAGMA integrity_check; +} {ok} + +sqlite3_limit db SQLITE_LIMIT_LENGTH 100000000 + +do_execsql_test 6.2.3 { + INSERT INTO v1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); + UPDATE v1_idx SET val = NULL; +} + +do_execsql_test 6.2.4 { + PRAGMA integrity_check; +} {{v1: %_idx entry id=1 is corrupt}} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 7.0 { + CREATE VIRTUAL TABLE b1 USING vec1(c1, c2); + INSERT INTO b1 VALUES(random_vector(10), 'hello'); + INSERT INTO b1 VALUES(random_vector(10), 'world'); + INSERT INTO b1 VALUES(random_vector(10), 'one'); + INSERT INTO b1 VALUES(random_vector(10), 'one'); + INSERT INTO b1 VALUES(random_vector(10), 'world'); + INSERT INTO b1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); +} + +do_execsql_test 7.1 { + SELECT rowid FROM b1(random_vector(10), 10) + WHERE c2 = 'ONE' COLLATE nocase + ORDER BY 1 +} {3 4} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 8.0 { + CREATE VIRTUAL TABLE docs USING vec1(embedding, title, size_kb, priority); + INSERT INTO docs(rowid, embedding, title, size_kb, priority) + SELECT i, + vec1_from_json('['||i||',0,0,0]'), + 'runbook '||i, (i%5)*100, (i%3)+1 + FROM ( + WITH RECURSIVE r(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM r WHERE i<60 + ) + SELECT i FROM r + ); +} + +foreach {tn where res} { + 1 "priority IN (1,2) AND size_kb=100" 8 + 2 "size_kb=100 AND priority IN (1,2)" 8 + 4 "size_kb IN (100,200) AND priority = 1" 8 + 5 "priority IN (1,2)" 20 + 6 "size_kb = 100" 12 + 3 "priority IN (1,2) AND size_kb > 50" 20 +} { +breakpoint + do_execsql_test 8.1.$tn " + SELECT count(*) + FROM docs( vec1_from_json('\[1,0,0,0\]'), 20 ) + WHERE $where + " [list $res] +} + + + +finish_test + diff --git a/ext/vec1/test/vec1nan.test b/ext/vec1/test/vec1nan.test new file mode 100644 index 0000000000..038b3b46c3 --- /dev/null +++ b/ext/vec1/test/vec1nan.test @@ -0,0 +1,48 @@ +# 2026-08-04 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1nan +vec1_load db + +do_execsql_test 1.0 { + CREATE VIRTUAL TABLE vv USING vec1(v); +} +do_execsql_test 1.1 { + SELECT * FROM vv( vec1_from_json('[1,2,3,4,5,6,7,8,9]'), 10); +} {} + +do_catchsql_test 1.2 { + SELECT * FROM vv(vec1_from_json('[1,2,3,4,5,6,7,8,NaN]'), 10); +} {1 {vec1: non-finite value not allowed in vector}} + +do_catchsql_test 1.3 { + INSERT INTO vv(v) VALUES(vec1_from_json('[1,2,3,4,NaN,5,6,7,8,9]')); +} {1 {vec1: non-finite value not allowed in vector}} + +do_execsql_test 1.4 { + CREATE TABLE tr(x); + WITH s(i) AS ( + VALUES(1) UNION ALL SELECT i+1 FROM s WHERE i<1000 + ) + INSERT INTO tr SELECT random_vector(9) FROM s; + INSERT INTO tr(x) VALUES(vec1_from_json('[1,2,3,4,NaN,5,6,7,8]')); +} + +do_catchsql_test 1.5 { + SELECT vec1_train(x, '{codesize:4}') FROM tr +} {1 {vec1_train: non-finite value not allowed in vector}} + + +finish_test + diff --git a/ext/vec1/test/vec1pq.test b/ext/vec1/test/vec1pq.test new file mode 100644 index 0000000000..5761b44deb --- /dev/null +++ b/ext/vec1/test/vec1pq.test @@ -0,0 +1,143 @@ +# 2026-01-17 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1pq +vec1_load db + +do_execsql_test 1.0 { + CREATE VIRTUAL TABLE v1 USING vec1(v, a1); +} + +do_execsql_test 1.1 { + CREATE TABLE t1(x); + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1000 + ) + INSERT INTO t1 SELECT random_vector(24) FROM s; +} + +do_execsql_test 1.2 { + INSERT INTO v1(cmd, arg) VALUES('rebuild', ( + SELECT vec1_train(x, '{codesize:8}') FROM t1 + )); +} + +do_test 1.3 { + foreach {rowid v} [db eval {SELECT rowid, x FROM t1}] { + db eval { INSERT INTO v1 VALUES($v, $rowid) } + } + + execsql { PRAGMA integrity_check } +} {ok} + +do_execsql_test 1.4 { + SELECT rowid, a1 FROM v1(random_vector(24), '{K:5}') WHERE a1 = 555 +} {555 555} + +do_execsql_test 1.5 { + DELETE FROM v1 WHERE rowid=444; + SELECT rowid, a1 FROM v1(random_vector(24), '{K:5}') WHERE a1 = 444; +} + +do_execsql_test 1.6 { + BEGIN; + INSERT INTO v1(rowid, v) VALUES(10_000_000_000, random_vector(24)); + INSERT INTO v1(rowid, v) VALUES(0, random_vector(24)); + INSERT INTO v1(rowid, v) VALUES(15_000_000_000, random_vector(24)); + COMMIT; + PRAGMA integrity_check; +} {ok} + +do_execsql_test 2.0 { + CREATE VIRTUAL TABLE v2 USING vec1(v, a2); +} + +do_execsql_test 2.1 { + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1000 + ) + INSERT INTO v2(rowid, v, a2) SELECT + i*10_000_000_000, random_vector(27), i*10_000_000_000 + FROM s; +} + +do_execsql_test 2.2 { + INSERT INTO v2(cmd, arg) VALUES('rebuild', ( + SELECT vec1_train(v, '{codesize:9,nbucket:2,residual:0}') FROM v2 + )); + SELECT vec1_config('nprobe', 2); +} {2.0} + +do_execsql_test 2.3 { + SELECT rowid, a2 FROM v2(random_vector(27), '{K:5}') + WHERE a2 = 5_550_000_000_000 +} {5550000000000 5550000000000} + +do_execsql_test 2.4 { + DELETE FROM v2 WHERE rowid=5550000000000; + SELECT rowid, a2 FROM v2(random_vector(27), '{K:5}') + WHERE a2 = 5_550_000_000_000 +} {} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 3.1 { + CREATE VIRTUAL TABLE v1 USING vec1(x); + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1000 + ) + INSERT INTO v1 SELECT random_vector(32) FROM s; +} + +do_execsql_test 3.2 { + SELECT length( vec1_train(x, '{codesize:11}') ) FROM v1; +} {33816} + +do_execsql_test 3.3 { + INSERT INTO v1(cmd, arg) VALUES('rebuild', ( + SELECT vec1_train(x, '{codesize:11}') FROM v1 + )); +} {} + +do_execsql_test 3.4 { + PRAGMA integrity_check +} {ok} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 4.0 { + CREATE VIRTUAL TABLE docs USING vec1(e); + INSERT INTO docs(rowid, e) VALUES(1, random_vector(200, 10000.0)); + WITH RECURSIVE r(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM r WHERE i<600 + ) + INSERT INTO docs(rowid, e) SELECT i+1, random_vector(200) FROM r; +} + +do_execsql_test 4.1 { + INSERT INTO docs(cmd, arg) VALUES('rebuild', ( + SELECT vec1_train(e, '{nbucket:2, quantizer:"pq"}') FROM docs + )); +} + +do_execsql_test 4.2 { + PRAGMA integrity_check +} {ok} + +finish_test + diff --git a/ext/vec1/test/vec1query.test b/ext/vec1/test/vec1query.test new file mode 100644 index 0000000000..3b43c022e2 --- /dev/null +++ b/ext/vec1/test/vec1query.test @@ -0,0 +1,147 @@ +# 2026-01-17 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1query +vec1_load db + +set lCentroid [list] +proc allocate_centroids {nCentroid nElem} { + global lCentroid + for {set ii 0} {$ii < $nCentroid} {incr ii} { + lappend lCentroid [randvec $nElem 10.0] + } +} + +proc randvec {nElem {scale 1.0}} { + set vec [list] + for {set ii 0} {$ii < $nElem} {incr ii} { + lappend vec [expr {rand() * $scale}] + } + return $vec +} + +proc addvec {a b} { + if {[llength $a]!=[llength $b]} { + error "vectors are different lengths" + } + set ret [list] + foreach ia $a ib $b { + lappend ret [expr $ia + $ib] + } + return $ret +} + +proc cvec {nDim} { + global lCentroid + if {[llength $lCentroid]==0} { + allocate_centroids 16 $nDim + } + + set ii [expr int(abs(rand()) * [llength $lCentroid])] + set ret [addvec [lindex $lCentroid $ii] [randvec $nDim 1.0]] + + binary format r* $ret +} + +db func cvec cvec + + +do_execsql_test 1.0 { + CREATE VIRTUAL TABLE v1 USING vec1(v); +} + +do_execsql_test 1.1 { + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1000 + ) + INSERT INTO v1 SELECT cvec(24) FROM s; +} + +do_execsql_test 1.2 { + INSERT INTO v1(cmd, arg) VALUES('rebuild', ( + SELECT vec1_train(v, '{codesize:0, nbucket:20, distance:"l2"}') FROM v1 + )); +} + +set nRep 100 + +proc testquery {v p} { + db one {SELECT + (SELECT rowid FROM v1($v, $p) ORDER BY vec1_l2_distance(v, $v)) + == + (SELECT rowid FROM v1 ORDER BY vec1_l2_distance(v, $v)) + } +} + +do_test 1.3 { + set nTry 0 + set nFind 0 + for {set ii 0} {$ii < $nRep} {incr ii} { + set v [cvec 24] + set p "{K:50, nprobe:4}" + incr nTry 1 + incr nFind [testquery $v $p] + } + + expr {((double($nFind) / $nTry) >= 0.98) ? "ok" : "$nFind/$nTry"} +} {ok} + +do_test 1.4 { + set nTry 0 + set nFind 0 + for {set ii 0} {$ii < $nRep} {incr ii} { + set v [random_vector 24] + set p "{K:50, nprobe:4}" + incr nTry 1 + incr nFind [testquery $v $p] + } + + expr {((double($nFind) / $nTry) >= 0.99) ? "ok" : "$nFind/$nTry"} +} {ok} + +model2tbl model v1 + +if {1 || [set_test_counter errors]>0} { + set ii 0 + foreach c $lCentroid { + set bin [binary format r* $c] + puts -nonewline "CENTROID $ii: " + set L [ + db eval { + SELECT format('%.1f', vec1_l2_distance($bin, coarse)) AS d FROM model + } + ] + puts [lrange [lsort $L] 0 0] + incr ii + } +} + +do_execsql_test 1.5 { + PRAGMA integrity_check +} {ok} + +set v [db one {SELECT random_vector(24)}] +set p "{K:50,nprobe:null}" +do_catchsql_test 1.6 { + SELECT (SELECT rowid FROM v1($v, $p) ORDER BY vec1_cos_distance(v, $v)) + ==(SELECT rowid FROM v1 ORDER BY vec1_cos_distance(v, $v)) +} {1 {vec1: null value in json object}} + +do_catchsql_test 1.7 { + SELECT rowid FROM v1($v, -10) +} {1 {vec1: K must be an integer value between 0 and 33554432}} + + +finish_test + diff --git a/ext/vec1/test/vec1rollback.test b/ext/vec1/test/vec1rollback.test new file mode 100644 index 0000000000..ee1bd629e3 --- /dev/null +++ b/ext/vec1/test/vec1rollback.test @@ -0,0 +1,148 @@ +# 2026-08-04 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1rollback +vec1_load db + +do_execsql_test 1.0 { + CREATE VIRTUAL TABLE v1 USING vec1(v); +} + +do_catchsql_test 1.1 { + INSERT INTO v1(rowid, v) VALUES + (1, random_vector(8)), + (2, random_vector(8)), + (3, random_vector(7)); +} {1 {vec1: unexpected vector blob size 28 bytes, expected 32}} + +do_execsql_test 1.2 { + INSERT INTO v1(rowid, v) VALUES(4, random_vector(8)); +} + +sqlite3 db2 test.db +vec1_load db2 + +do_test 1.3 { + catchsql { + INSERT INTO v1(rowid, v) VALUES(5, random_vector(4)); + } db2 +} {1 {vec1: unexpected vector blob size 16 bytes, expected 32}} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 2.0 { + CREATE VIRTUAL TABLE v1 USING vec1(v); +} + +do_catchsql_test 2.1 { + BEGIN; + INSERT INTO v1(rowid, v) VALUES + (1, random_vector(8)), + (2, random_vector(8)), + (3, random_vector(7)); +} {1 {vec1: unexpected vector blob size 28 bytes, expected 32}} + +do_execsql_test 2.2 { + INSERT INTO v1(rowid, v) VALUES(4, random_vector(8)); + COMMIT; +} + +sqlite3 db2 test.db +vec1_load db2 + +do_test 2.3 { + catchsql { + INSERT INTO v1(rowid, v) VALUES(5, random_vector(4)); + } db2 +} {1 {vec1: unexpected vector blob size 16 bytes, expected 32}} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 2.0 { + CREATE VIRTUAL TABLE v1 USING vec1(v); + WITH s(i) AS ( + VALUES(1) UNION ALL SELECT i+1 FROM s WHERE i<1000 + ) + INSERT INTO v1 SELECT random_vector(24) FROM s; +} + +do_execsql_test 2.1 { + BEGIN; + INSERT INTO v1(cmd, arg) VALUES('rebuild', ( + SELECT vec1_train(v, '{codesize:8}') FROM v1 + )); + ROLLBACK; +} + +sqlite3 db2 test.db +vec1_load db2 +do_execsql_test -db db2 2.2 { + INSERT INTO v1(cmd, arg) VALUES('rebuild', ( + SELECT vec1_train(v, '{codesize:4}') FROM v1 + )); +} + +do_execsql_test 2.3 { + INSERT INTO v1(v) VALUES( random_vector(24) ); +} + +do_execsql_test 2.4 { + PRAGMA integrity_check; +} {ok} + +do_execsql_test 2.5 { + BEGIN; + SAVEPOINT one; + INSERT INTO v1(cmd, arg) VALUES('rebuild', ( + SELECT vec1_train(v, '{codesize:8}') FROM v1 + )); + ROLLBACK TO one; + COMMIT; +} + +do_execsql_test -db db2 2.6 { + INSERT INTO v1(cmd, arg) VALUES('rebuild', ( + SELECT vec1_train(v, '{codesize:4}') FROM v1 + )); +} + +do_execsql_test 2.7 { + INSERT INTO v1(v) VALUES( random_vector(24) ); +} + +do_execsql_test 2.8 { + PRAGMA integrity_check; +} {ok} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 3.0 { + BEGIN; + CREATE VIRTUAL TABLE staging USING vec1(a, b, c); + ROLLBACK; +} + +do_execsql_test 3.1 { + SAVEPOINT nightly; + CREATE VIRTUAL TABLE staging USING vec1(a, b, c); + ROLLBACK TO nightly; +} + +finish_test diff --git a/ext/vec1/test/vec1rotate.test b/ext/vec1/test/vec1rotate.test new file mode 100644 index 0000000000..8bdb6322c0 --- /dev/null +++ b/ext/vec1/test/vec1rotate.test @@ -0,0 +1,56 @@ +# 2026-03-14 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1rotate +vec1_load db + +do_execsql_test 1.0 { + CREATE VIRTUAL TABLE v1 USING vec1(v); + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1000 + ) + INSERT INTO v1(rowid, v) SELECT i, random_vector(14) FROM s; + + INSERT INTO v1(cmd, arg) + SELECT + 'rebuild', + vec1_train(v, '{codesize:14, opq:true, svd_verify:true, distance:"cos"}') + FROM v1; +} + +set v [db one {SELECT random_vector(14)}] +do_execsql_test 1.1 { + SELECT + (SELECT rowid FROM v1($v, 1))== + (SELECT rowid FROM v1 ORDER BY vec1_cos_distance($v, v)) + ; +} {1} + +do_execsql_test 2.0 { + DROP TABLE IF EXISTS v1; + CREATE VIRTUAL TABLE v1 USING vec1(v); + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<10000 + ) + INSERT INTO v1(rowid, v) SELECT i, random_vector(64) FROM s; + + INSERT INTO v1(cmd, arg) + SELECT + 'rebuild', + vec1_train(v, '{codesize:8, opq:true, svd_verify:true}') + FROM v1; +} + +finish_test + diff --git a/ext/vec1/test/vec1rotate2.test b/ext/vec1/test/vec1rotate2.test new file mode 100644 index 0000000000..78384fe69a --- /dev/null +++ b/ext/vec1/test/vec1rotate2.test @@ -0,0 +1,42 @@ +# 2026-03-14 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1rotate2 +vec1_load db + +do_execsql_test 1.0 { + CREATE VIRTUAL TABLE v1 USING vec1(v); + + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<34000 + ) + INSERT INTO v1(rowid, v) SELECT i, random_vector(256) FROM s; +} + +proc progress {percent msg} { + puts "${percent}%: $msg" +} +db func progress progress + +do_test 1.1 { + set T [db one { + SELECT + vec1_train(v, '{codesize:16, opq:1, nopq_round:1, progress:"progress"}') + FROM v1 + }] + set {} {} +} {} + +finish_test + diff --git a/ext/vec1/test/vec1round.test b/ext/vec1/test/vec1round.test new file mode 100644 index 0000000000..4c01f3d8f7 --- /dev/null +++ b/ext/vec1/test/vec1round.test @@ -0,0 +1,303 @@ +# 2026-08-21 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1round +vec1_load db + +proc write_db {db} { + $db eval { + CREATE VIRTUAL TABLE docs USING vec1(embedding, title, category); + INSERT INTO docs(rowid,embedding,title,category) VALUES(757, + vec1_from_json('[78,15,13,9,3,1,2,1]'), + 'runbook 757: rotate the search index signing key','ops' + ); + INSERT INTO docs(cmd, arg) VALUES('rebuild', unhex( + '0000000400000001000000080000000400000020000000010000A841000018420000BA4200008040' || + '0000504200004E42000090410000084100008A4200000442ABAA1642ABAA12420000264200006841' || + '00008942000048420000A44200008C416EDB7E42499224410000D4400000434233339D426666E640' || + '000080400000A8410000B04200002C420000BF420000784155559042ABAA86410000364200003242' || + '0000DC410000D8416666BE42333327420000F841000080405555B74255551942B76DA142DBB6D541' || + '6EDB6E429224254200003442000090410000A642000040400000924200009C410000404000001442' || + '66661042CDCCCC41000038420000D841000028410000803F00003842000012420000C64100005841' || + '00007A4200008C410000724200004C42000010414992E4400000004200002042ABAA8C42ABAA4E42' || + '666688429A99B94000001642000036420000B2420000C441000084420000A8410000404200004040' || + '00005A4200004A42000089420000264200009642000040420000B6420000E0410000994200002242' || + '00000E42000008410000544200000A420000764200000A4255556142ABAA2A400000DC41ABAA8641' || + '0080B24200000C4200008E42000000416EDB06429224054200005C420000E041ABAA2A4155551142' || + '000058410000AC4100002041000032420000B7420000CC410000C0400000FC410000184200003041' || + '0000003F00000841000068420000F8416666AE41CDCC4C4055559E42ABAABE41ABAAAA41ABAA3A42' || + '0000AF4200004E420000803F00002B420000A441000010410000F040000078419A99993FCDCCD441' || + '000040410000E841ABAA7A42ABAAAA3E00004841000040410000BD4200003D420000884100002A42' || + '0000A54200003A4200008C410000CC410000B0400000C03F0000704100000040CDCC48426666FE41' || + '000028420000484200009A420000B64100007442000040420000A841000038410000164200009441' || + '000094410000B04000002C420000C0410000B84100002041ABAA584200002E420000844100000642' || + '0000A742000038410000F4410000F041000032420000FC419A99B1419A99A1410000BF4200000242' || + '0000884100004E420000D04100004E420000914200001242000010410000D0410000E44100001242' || + '000098420000984100002E420000E0400000CE4100007441B76D91424992A43F0000BC420000B041' || + 'ABAA6042000036420000D0400000BC410000B3420000003FABAAAE42ABAA06420000B84200004041' || + '00008B4200006040000022420000AC410000A8410000E8410000AC42000060410000B34200008C41' || + 'CDCC2C4233330742ABAA6A400000384200005841000040420000824200003A42CDCC1C426666EE41' || + 'CDCC2C419A99194100004A420000B04000006E420000684133332742666622420000B84000009840' || + '000095420000EC413333634133331F4200004A425555434200004E420000C44100000A4200002A42' || + '0000A1420000003F3333A9423333A3410000B342000002420000FC410000944166668E419A998941' || + '0000AE420000B44100001C420000A0410000AB420000C84055558642000050410000F4410000A841' || + 'B76D4F42DBB625420000BF4200001A42ABAA5642ABAAAA3E00009C4200004C42008076420000D840' || + '00004442000034420000E0410000803F33334342CDCC1C42000094420000E0400000804000004041' || + '0000B24200001041922461426EDB164200001E420000DA416666A6426666EE41000036420000003F' || + '0000C0400000204200008242000018420000B74200003742000040420000A84100008C420000C841' || + '0000004100004E4200008F42000032420000803F0000904100000C420000F8410000CA410000BB41' || + '00002040000020400000004000002041000016420000094200007C420000D84100009F4200002E42' || + 'ABAA7A41555575419A999742666686400000B64200000040ABAABA42ABAA1E4200009A420000D040' || + '0000C441000002420000BF420000D04000009B42000002425555B54155554142333327426666F641' || + '92240542DBB6B541000098420000F8410000944100000000000084420000604055556541ABAA5A41' || + '0000B64200002C42000028410000944100004C4200001842ABAA5E420000803F5555854255554142' || + 'ABAA80425555D53F00009842000038415555B541ABAAB2413333A341CDCC944155554541ABAA1642' || + '000084420000F04100009242000028410000D04100002C426666A2426666E64055559D41ABAAAA3F' || + '000096420000AC410000204000004E420000E84100003842ABAA3A415555394255557541ABAA4642' || + '0000A24200001841000072420000C4415555B541555555410000684255553F42000091420000DC41' || + '55550541ABAA2A420000003F000046420000A8420000D841000068410000FC4133333F42CDCC3C42' || + '0000BC4100002242CDCC7C413333B340CDCC68429A9999400000E8410000C8415555E541ABAA9A41' || + 'CDCC0C41CDCC08420000003F00000A4200009C42000070410000A0410000E0400000BA4200001042' || + '33337B42CDCC3042000060400000E8410000BB420000F4410000804200004C42ABAAC241ABAA4642' || + '0000AD420000EC41555505425555D5400000944100000E4200009C41000034420000B84200004040' || + '00008C4100005841000088420000B8410000924200003842000085420000003F92248B4225497241' || + 'ABAAB442ABAA12420000BC420000A040ABAA204200001A42000000420000E04100009C4100007841' || + '33335341333373400000803F0000803F0000AB42000022420000BC4200006041000042420000EC41' || + '0000AB4200000042ABAA3042555527420000B0420000F8410000AB42000046420000DE4100004441' || + '0000003F000040409A9909410000104100003041000060416EDB46412549B2400000E0410000C03F' || + '0000003FABAA72410000C0410000E0400000F0410000184166660641CDCC4C405555D53FABAACA40' || + '9A99C1419A99193F0000824100004C4155551540000078410000B841000038410000884100002041' || + 'ABAA8A405555B540666686409A99993F0000A04100002040ABAA2A41ABAA3A41ABAADA41ABAA0A41' || + 'B76DDB3E6EDB363F5555D54000008040000060410000D0400000B84100007841CDCCEC4066666641' || + '0000F041000060405555B5415555B540ABAA2A3F000050416EDB7641499224405555D54155556541' || + '000098415555954000007841ABAAAA3E00000000ABAA1A410000A841000018410000404000000841' || + '00007041000080410000C44100005041ABAAE2410000E040CDCCCC419A9919400000CC4100008041' || + '00005841000060410000D8400000444100006041000048410000004100003041ABAA3A4100000841' || + '5555B541000000000000A041ABAA2A3E0000B040000010410000D0400000003F0000904100008041' || + '0000803F00004041B76DAB41499264409224C9419224A94000002C410000E03F000080410000B040' || + '00004841000080410000A041000030416EDB06419224C93F0000003F0000F0409A999941CDCC7C41' || + '55558D41ABAAEA400000504100007041ABAAAA41ABAA6A4100002841000078410000B44100005841' || + '000008410000504133333340CDCCCC3E00002041ABAA1A41ABAAB241ABAA0A41B76DC34192242941' || + '0000D841555515400000EC41000000003333634133331341ABAA2A40555535400000104100002041' || + '00004841000040405555D5410000804033338B41000000000000A0410000A0400000B6410000E840' || + '0000A040000098409A9959400000604100002041ABAA2A3F00008E41000010400000C04100007841' || + 'ABAA824100007041CDCC4C413333234100007041ABAA3A415555954000005041000078410000F040' || + '0000B041000020410000F0410000A0400000F041000070410000E641000050400000E040CDCCEC40' || + '0000804000000841398ECB411CC7F1405555D541000030410000A040ABAA6A415555C54100001041' || + '0000A8415555D5400000EC410000304100007041ABAA6A41000070410000504100009E4100006441' || + '0000D041666616410000D84100005041000068410000804000006041000020400000E8410000C040' || + '00000000000030410000F041ABAAEA400000F0419A9949410000E841000040410000DC4100008041' || + '0000C040000070410000D8410000E0400000C8410000000000009441000040410000504100008040' || + '0000E8410000C03F00003841000000009A998941CDCC8C40000008410000B0403333DB4100002041' || + '00000040000020410000D041000048411CC7C1400000A040ABAA2A40000030410000804000004041' || + '0000D4410000803F00009C4100000041ABAA6A400000804100008C4100003041ABAA2A410000C040' || + '000050410000C03F000040400000C040000000000000404100000000000000400000D0410000C040' || + '0000C441000040410000104100006841000090410000B0400000C441000060415555D53F00006041' || + '0000C03F000080400000E041ABAAAA405555B541ABAAAA3F0000944100001041CDCCCC3E6666A640' || + '0000C841000070410000A0400000F04000000040000000000000E040000010410000004000004041' || + '0000A041000050410000F0410000C03F0000C0400000F0400000AC41000040415555C541ABAA6A40' || + '5555E5415555754155558541ABAA0A410000104100004041333393410000803FABAA0A41ABAAEA40' || + '0000D840000010400000D8410000A040000088410000C0400000D04000002041ABAA0A4100000000' || + '0000E041000030410000803F555515415555AD41000080410000E841000010410000E84155555541' || + '0000000000006041000000400000504100009041000070410000C040000060410000D44100000000' || + '0000004100000041ABAA4A41ABAAEA400000803F00008041CDCCCC3F000000400000184100003041' || + '0000E04100000041ABAA6A4055557540555585410000604100000040000000410000304100005041' || + '000000410000404100005041000050410000D041000070410000BC41000040400000A84100005041' || + '0000404100008040000098419A9919400000B0410000A0400000604100000041000024410000E840' || + '000068410000A04000006041000030410000A0400000104000003041000090400000044100007C41' || + '0000A040000080410000C8400000C0400000B841000020410000A8410000C03F000000400000A040' || + '0000C040000040400000E040000030410000A041000010410000404100004041000098410000E040' || + '00009841000020410000E0410000604100005041000040410000904100005841000080400000E040' || + '0000D8410000C0400000E0410000000055559540000020410000803F00000000000070410000C040' || + '0000A8410000A0400000404000004041ABAACA400000804100005841000000000000884100004040' || + '55559540000000000000803F000030410000804000007041000060410000803F000098410000C040' || + '000088410000803FABAA1A41ABAA6A400000B0410000004000000041000020410000804100008040' || + '0000A040000030410000A0410000E04000004041ABAAAA3F00003041000018410000000000008040' || + '0000404000005041000020410000A0400000A841000000410000B041000030410000984100005041' || + '0000304100004040000070410000204100008841000080410000C040000030410000404100003041' || + '000000000000000000003041000040400000A0400000A040000010410000803F000000000000A040' || + '0000A040000000000000E0400000804000000000000000400000E040000000400000404000000040' || + '0000E04000000000000040410000C04000000040000000400000C04000000000000020410000803F' || + '000000400000C040000010410000C0400000004100000000000040400000A0400000E0400000803F' || + '000000410000803F000030410000A0400000A0400000C0400000C0400000C0400000304100008040' || + '0000803F000040400000803F0000803F00004040000000000000A040000080400000C0400000803F' || + '000010410000A040000080400000404000004040000080400000803F0000A0400000E0400000A040' || + '0000A040000000400000204100000000000000400000A0400000A040000040400000404100000040' || + '0000304100000000000080400000803F000000410000404000002041000040400000204100000040' || + '0000E040000040400000C04000000040000040410000803F00001041000000400000004000000000' || + '0000E0400000C040000040410000804000000041000000400000C040000080400000000000004040' || + '000000410000A04000002041000080400000104100004040000040400000803F0000004000008040' || + '000030410000C04000000041000080400000404000004040000080400000C040000000000000C040' || + '000080400000A0400000104100008040000040400000C0400000803F00000040000020410000A040' || + '000030410000803F0000804000000000000000000000803F0000A0400000803F0000C04000004040' || + '0000803F0000C0400000803F000000000000104100000000000020410000C0400000404100000000' || + '000080400000004000003041000000400000803F000080400000004000004040000000400000803F' || + '000040410000A0400000C0400000A040000040410000404000000000000080400000804000008040' || + '000000410000C0400000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000000000000040000080400000803F00000040000000000000404000000040' || + '00004040000000000000803F000000000000803F0000004000000040000000400000000000000000' || + '000000400000803F0000804000000040000000000000803F0000803F0000803F000040400000803F' || + '00008040000000000000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F0000803F' || + '0000803F0000803F0000803F0000803F6F7AB342D39B0A42DFF4C641BDE9ED40210BC9400B594840' || + '59C80240B290853FC4C383402D2D01429796F6401E1E3E41E2E1E1407878384000000040F1F0703F' || + '9D73224274CE1942A594D240CE39A740638CD14095524A40082104400821843F322990421296FB40' || + '877EF340877EF3407FB3B54012963B40C8A4F83F232C773FA6A59941D3D2D240D3D2BA4178783841' || + '7878B8400F0F4F40C4C30340F1F0703FCA6BA8400DE539426C28EF401BCA03416C28DF406C284F40' || + 'A2BC0640515E833F3BB10B41ECC43641C54E804176624F410000E040C54E4C40B113FB3F4FEC843F' || + 'F1F04A42B5B422424B4BCB4197963E416969C9405A5A3A407878F83F8887873F0000E84100000040' || + '0000204100006041000000416666664000000040CDCC4C3FB5B47C42F1F03C42B5B4B4405A5ADA40' || + '0000C040A6A54540000000400000803F33330142CDCC12423333B7419A99A9409A99B14000003040' || + '666606403333733F175DCC41E9A24342E9A2CB40A38B1E4174D10540D1455740000000400000803F' || + '00C025420000AE4000001941000090400000A04000004C400000F03F0000883F254962429EE73942' || + '5555C541310C43408661B84062184640621806407A9E673F00A06F420000DE400000BE410000D640' || + '0000A740000046400000064000008C3F43169A4116B290410000A04091858C40F5A6B7407AD35B40' || + 'EA4DEF3F16B2903F1296A7423E8DD0410000A040734F03413E8DB04009CB3D4000000040DDD3883F' || + '33B30A429A99CB410000B84066662A413333B740CDCC2440000008409A99793F00006841DBB63142' || + 'DBB6B5413DCFE3409EE7B94049922440499204400000803FB497004109ED65405F42FB40721CC740' || + 'ABAA8A402FA13D40D15E0240BE84763F00002E42000084410000C441555521410000C84055552540' || + '000000400000803F008066420000FB415555E940ABAA10410000A84055554540000000405555853F' || + 'ABAA3E42BCBB314233336341555525413333D340BCBB3B40EFEEEE3F4444843FCDCCAA4254823C41' || + '95209F4158FB06419520CF40A8043940A804F93F90A7843FDFF4DE414E6FC2416F7ACB41EA4D2F41' || + '6F7AD3400B5948404E6FFA3F9CDE743F000091425D742F4274D1B941BAE8B2404617BD402FBA4840' || + '5D741140175D743F0000E8400000D4410000B2410000B040ABAACE4055554D40ABAA02400000803F' || + '721C9242ABAAC6418EE39E41ABAA16411CC7C9408EE3484000000040398E833F0AD72F423333EB41' || + '00008041F6289C40A470DD40E17A5440000000408FC2753FB1DCAF42232C2F42734FE340B9A72141' || + '232C9740000040409F46D83F232C773F5D74A941A38B1042BAE8E2402FBA0841E9A2CB4046173D40' || + '175DF43F0000803F9A99B141CDCC8441ABAAAA41111171407777C7409A993940444404404444843F' + )); + } +} + +do_test 1.0 { + write_db db +} {} + +do_execsql_test 1.1 { + PRAGMA integrity_check +} {ok} + +db close +sqlite3 db test.db + +if {[catch { vec1_load_scalar db }]} { + puts "Skipping tests... vec1testscalar.so not built" + finish_test + return +} + +breakpoint +do_execsql_test 1.2 { + PRAGMA integrity_check +} {ok} + +do_execsql_test 1.3 { DROP TABLE docs } + +do_test 1.4 { + write_db db +} {} + +do_execsql_test 1.5 { + PRAGMA integrity_check +} {ok} + +db close +sqlite3 db test.db +vec1_load db + +do_execsql_test 1.6 { + PRAGMA integrity_check +} {ok} + + +finish_test + diff --git a/ext/vec1/test/vec1scalar.test b/ext/vec1/test/vec1scalar.test new file mode 100644 index 0000000000..e128a5243f --- /dev/null +++ b/ext/vec1/test/vec1scalar.test @@ -0,0 +1,77 @@ +# 2026-04-01 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1scalar + +if {[catch { vec1_load_scalar db }]} { + puts "Skipping tests... vec1testscalar.so not built" + finish_test + return +} + +do_execsql_test 1.0 { + CREATE VIRTUAL TABLE docs USING vec1(embedding, title, category); +} +do_execsql_test 1.1 { + INSERT INTO docs(rowid, embedding, title, category) + SELECT i, vec1_from_json('['||(SELECT group_concat(printf('%!.9g', + sin(i*0.7+j*1.3)*0.5 + cos(i*0.11*j+0.3)*0.5),',') + FROM (WITH RECURSIVE q(j) AS (SELECT 0 UNION ALL SELECT j+1 FROM q WHERE j<31) + SELECT j FROM q))||']'), + 'runbook '||i, + CASE i%4 WHEN 0 THEN 'ops' WHEN 1 THEN 'kb' WHEN 2 THEN 'sec' ELSE 'net' END + FROM ( + WITH RECURSIVE r(i) AS (SELECT 1 UNION ALL SELECT i+1 FROM r WHERE i<700) + SELECT i FROM r + ); +} + +do_execsql_test 1.2 { + INSERT INTO docs(cmd, arg) VALUES('rebuild', ( + SELECT vec1_train( + embedding,'{nbucket:8,codesize:8,quantizer:"bq",residual:0}' + ) FROM docs + )); +} + +do_execsql_test 1.3 { + PRAGMA integrity_check +} {ok} + +db close +sqlite3 db test.db +vec1_load db + +do_execsql_test 1.4 { + PRAGMA integrity_check +} {ok} + +do_execsql_test 1.5 { + INSERT INTO docs(cmd, arg) VALUES('rebuild', ( + SELECT vec1_train( + embedding,'{nbucket:8,codesize:8,quantizer:"bq",residual:0}' + ) FROM docs + )); +} + +db close +sqlite3 db test.db +vec1_load_scalar db + +do_execsql_test 1.6 { + PRAGMA integrity_check +} {ok} + +finish_test + diff --git a/ext/vec1/test/vec1stream.test b/ext/vec1/test/vec1stream.test new file mode 100644 index 0000000000..d865f1b703 --- /dev/null +++ b/ext/vec1/test/vec1stream.test @@ -0,0 +1,121 @@ +# 2026-03-14 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1stream +vec1_load db + +do_execsql_test 1.0 { + CREATE VIRTUAL TABLE v1 USING vec1(v); + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1000 + ) + INSERT INTO v1(rowid, v) SELECT i, random_vector(16) FROM s; + + INSERT INTO v1(cmd, arg) + SELECT 'rebuild', vec1_train(v, '{nbucket:32, codesize:0}') FROM v1; +} + +do_test 1.1 { + llength [ + db eval { + SELECT rowid FROM v1(random_vector(16), '{K:10, nprobe: 1, streaming:0}') + } + ] +} {10} + +do_test 1.2 { + llength [ + db eval { + SELECT rowid FROM v1( + random_vector(16), '{K:1000, nprobe: 32, streaming:0}' + ) + } + ] +} {1000} + +do_test 1.3 { + llength [ + db eval { + SELECT rowid FROM v1(random_vector(16), '{K:10, nprobe: 1, streaming:1}') + } + ] +} {1000} + +proc repeat {str n} { + string repeat $str $n +} +db func repeat repeat + +do_execsql_test 2.0 { + CREATE VIRTUAL TABLE v2 USING vec1(v, m); + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1000 + ) + INSERT INTO v2(rowid, v, m) SELECT i, random_vector(16), repeat(i,200) FROM s; + + INSERT INTO v2(cmd, arg) + SELECT 'rebuild', vec1_train(v, '{nbucket:32, codesize:0}') FROM v1; +} + +breakpoint +do_test 2.1 { + llength [ + db eval { + SELECT rowid + FROM v2(random_vector(16), '{K:10, nprobe: 1, streaming:1}') + WHERE m=repeat(555,200) + } + ] +} {1} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +db func vec vec +proc vec {args} { binary format r* $args } + +proc sequence {n} { + set ret [list] + for {set i 1} {$i <= $n} {incr i} { lappend ret $i } + set ret +} + +do_execsql_test 3.0 { + CREATE VIRTUAL TABLE docs USING vec1(embedding, title, category); + + WITH RECURSIVE r(i) AS (SELECT 1 UNION ALL SELECT i+1 FROM r WHERE i<400) + INSERT INTO docs(rowid, embedding, title, category) + SELECT i, + vec(i%37, i%53, i%71, i%29), + 'runbook '||i, + CASE WHEN i%3=0 THEN 'ops' ELSE 'kb' END + FROM r; + + INSERT INTO docs(cmd,arg) VALUES('rebuild', ( + SELECT vec1_train(embedding,'{nbucket:16,quantizer:"none"}') FROM docs + )); +} + +do_execsql_test 3.1 { PRAGMA integrity_check } {ok} + +for {set ii 1} {$ii <= 16} {incr ii} { + set j "{K:10,streaming:1,nprobe:$ii}" + do_execsql_test 3.2.$ii { + SELECT rowid FROM docs(vec(5,5,5,5), $j) ORDER BY 1 + } [sequence 400] +} + +finish_test + diff --git a/ext/vec1/test/vec1stream2.test b/ext/vec1/test/vec1stream2.test new file mode 100644 index 0000000000..53f44e27ba --- /dev/null +++ b/ext/vec1/test/vec1stream2.test @@ -0,0 +1,87 @@ +# 2026-03-14 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1stream2 +vec1_load db + + +do_execsql_test 1.0 { + CREATE VIRTUAL TABLE t1 USING vec1(v); + WITH s(i) AS ( SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1000 ) + INSERT INTO t1 SELECT rvec(16, i) FROM s; + INSERT INTO t1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); +} + +foreach {tn2 idx} { + 1 { + INSERT INTO t1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); + } + 2 { + INSERT INTO t1(cmd, arg) VALUES('rebuild', ( + SELECT vec1_train(v, '{nbucket:8}') FROM t1 + )); + SELECT vec1_config('nprobe', 2); + } + 3 { + INSERT INTO t1(cmd, arg) VALUES('rebuild', ( + SELECT vec1_train(v, '{nbucket:8, codesize:4}') FROM t1 + )); + SELECT vec1_config('nprobe', 4); + } +} { + foreach {tn iVec} { + 1 498753 + 2 722456 + 3 462190 + 4 981077 + 5 606275 + 6 239435 + 7 508863 + 8 795393 + 9 163905 + 10 697229 + } { + db eval $idx + + for {set nRes 2} {$nRes < 100} {incr nRes 3} { + set expect [db eval { SELECT rowid FROM t1( rvec(16, $iVec), $nRes ) }] + + if {$nRes>[llength $expect]} break + + do_test 1.$tn2.$tn.$nRes.1 { + set J "{K:$nRes, streaming:1}" + set all [db eval { SELECT rowid FROM t1( rvec(16, $iVec), $J) }] + lrange $all 0 [expr $nRes-1] + } $expect + + set expect [db eval { SELECT rowid FROM t1 ORDER BY 1 }] + + do_test 1.$tn2.$tn.$nRes.2 { + set J "{K:$nRes, streaming:1}" + set all [db eval { SELECT rowid FROM t1( rvec(16, $iVec), $J) }] + lsort -integer $all + } $expect + + do_test 1.$tn2.$tn.$nRes.3 { + set J "{K:$nRes, streaming:1, nprobe_slack:0.2}" + set all [db eval { SELECT rowid FROM t1( rvec(16, $iVec), $J) }] + lsort -integer $all + } $expect + } + } +} + + +finish_test + diff --git a/ext/vec1/test/vec1train.test b/ext/vec1/test/vec1train.test new file mode 100644 index 0000000000..863b954019 --- /dev/null +++ b/ext/vec1/test/vec1train.test @@ -0,0 +1,160 @@ +# 2026-04-01 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1train +vec1_load db + +do_execsql_test 1.0 { + CREATE TABLE t1(a); + WITH s(i) AS ( + VALUES(1) UNION ALL SELECT i+1 FROM s WHERE i<1000 + ) INSERT INTO t1 SELECT random_vector(16) FROM s; +} + +do_catchsql_test 1.1 { + SELECT vec1_train(a, '{codesize: "large"}') FROM t1 +} {1 {vec1: codesize must be set to an integer value between 1 and 256, or 0}} + +do_catchsql_test 1.2 { + SELECT vec1_train(a, '{codesize: 16, nthread: -1}') FROM t1 +} {1 {vec1: nthread must be set to an integer value between 1 and 128}} + +do_catchsql_test 1.3 { + SELECT vec1_train(a, '{codesize: 1024}') FROM t1 +} {1 {vec1: codesize must be set to an integer value between 1 and 256, or 0}} + +do_catchsql_test 1.4 { + SELECT vec1_train(a, '{codesize: 8, opq: "yes"}') FROM t1 +} {1 {vec1: opq must be set to a boolean value}} + +do_catchsql_test 1.5 { + SELECT vec1_train(a, '{codesize: 8, opq: 555}') FROM t1 +} {1 {vec1: opq must be set to a boolean value}} + +do_execsql_test 2.0 { + CREATE VIRTUAL TABLE u1 USING vec1(a); +} + +do_catchsql_test 2.1 { + INSERT INTO u1(cmd, arg) VALUES('rebuild', '{param: 1234}'); +} {1 {vec1: unknown parameter: param}} + +do_execsql_test 3.0 { + CREATE TABLE t2(x); + INSERT INTO t2 VALUES(random_vector(16)); + INSERT INTO t2 VALUES(random_vector(16)); + INSERT INTO t2 VALUES(random_vector(16)); + + INSERT INTO t2 VALUES(''); +} + +do_catchsql_test 3.1 { + SELECT vec1_train(x, '{codesize: 8}') FROM t2 +} {1 {vec1_train: bad argument}} + +do_execsql_test 3.2.0 { + UPDATE t2 SET x=randomblob(7) WHERE rowid=4; +} +do_catchsql_test 3.2.1 { + SELECT vec1_train(x, '{codesize: 8}') FROM t2 +} {1 {vec1_train: bad argument}} + +do_execsql_test 3.3.0 { + UPDATE t2 SET x=random_vector(20) WHERE rowid=4; +} +do_catchsql_test 3.3.1 { + SELECT vec1_train(x, '{codesize: 8}') FROM t2 +} {1 {vec1_train: bad argument}} +do_catchsql_test 3.3.2 { + SELECT vec1_train(x) FROM t2 +} {1 {vec1_train: bad argument}} + +do_execsql_test 3.4.0 { + UPDATE t2 SET x=random_vector(16) WHERE rowid=4; +} +#do_catchsql_test 3.4.1 { +# SELECT vec1_train(x, '{codesize: 12}') FROM t2 +#} {1 {vec1_train: vector size (16) must be a multiple of codesize (12)}} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 4.0 { + CREATE TABLE x1(v); +} + +do_catchsql_test 4.1 { + SELECT vec1_train(v, '{nbucket: 8}') FROM x1 +} {1 {too few training vectors (have 0, require more than that)}} + +do_execsql_test 4.2 { + WITH s(i) AS ( + VALUES(1) UNION ALL SELECT i+1 FROM s WHERE i<100 + ) INSERT INTO x1 SELECT random_vector(16) FROM s; +} + +do_catchsql_test 4.3 { + SELECT vec1_train(v, '{nbucket: 8, progress: "nofunc"}') FROM x1 +} {1 {no such function: nofunc}} + +proc errorfunc {args} { + error "this is an error!" +} +db func errorfunc errorfunc + +do_catchsql_test 4.4 { + SELECT vec1_train(v, '{nbucket: 8, progress: "errorfunc"}') FROM x1 +} {1 {this is an error!}} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 5.0 { + CREATE VIRTUAL TABLE v1 USING vec1(a); + WITH s(i) AS ( + VALUES(1) UNION ALL SELECT i+1 FROM s WHERE i<1000 + ) INSERT INTO v1 SELECT random_vector(16) FROM s; +} + +do_catchsql_test 5.1 { + INSERT INTO v1(cmd, arg) VALUES( + 'rebuild', (SELECT substr( ( + SELECT vec1_train(a, '{codesize: 8}') FROM v1 + ), 1, 2000)) + ); +} {1 {vec1: model size mismatch: have 2000, expected 16408}} + +#------------------------------------------------------------------------- +reset_db +vec1_load db +do_execsql_test 6.0 { + CREATE VIRTUAL TABLE v1 USING vec1(a); + WITH s(i) AS ( + VALUES(1) UNION ALL SELECT i+1 FROM s WHERE i<12 + ) INSERT INTO v1 SELECT random_vector(16) FROM s; +} + +proc myprofile {args} {} +db func myprofile myprofile + +do_catchsql_test 6.1 { + INSERT INTO v1(cmd, arg) VALUES('rebuild', ( + SELECT vec1_train(a, '{quantizer:"pq", profile: "myprofile"}') FROM v1 + )); +} {1 {too few training vectors (require 512, have 12)}} + +finish_test + diff --git a/ext/vec1/test/vec1update.test b/ext/vec1/test/vec1update.test new file mode 100644 index 0000000000..571d9304f9 --- /dev/null +++ b/ext/vec1/test/vec1update.test @@ -0,0 +1,124 @@ +# 2026-08-04 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1update +vec1_load db + +do_execsql_test 1.0 { + CREATE VIRTUAL TABLE v1 USING vec1(v); + INSERT INTO v1(rowid, v) VALUES(11, random_vector(8)); + INSERT INTO v1(rowid, v) VALUES(12, random_vector(8)); + + INSERT INTO v1(cmd, v) VALUES('rebuild', '{index:"flat"}'); +} + +do_execsql_test 1.1 { + PRAGMA integrity_check +} {ok} + +do_catchsql_test 1.2.1 { + UPDATE v1 SET v = random_vector(9) WHERE rowid=12; +} {1 {vec1: unexpected vector blob size 36 bytes, expected 32}} + +do_execsql_test 1.2.2 { + PRAGMA integrity_check +} {ok} + +do_execsql_test 1.2.3 { + SELECT rowid, length(v) from v1; +} {11 32 12 32} + +do_execsql_test 1.3.1 { + BEGIN; + INSERT INTO v1(rowid, v) VALUES(13, random_vector(8)); +} {} + +do_catchsql_test 1.3.2 { + UPDATE v1 SET v = random_vector(9) WHERE rowid=12; +} {1 {vec1: unexpected vector blob size 36 bytes, expected 32}} + +do_execsql_test 1.3.3 { + COMMIT; + PRAGMA integrity_check; +} {ok} + +do_execsql_test 1.3.4 { + SELECT rowid, length(v) from v1; +} {11 32 12 32 13 32} + +do_execsql_test 1.3.5 { + SELECT rowid, length(v) from v1; +} {11 32 12 32 13 32} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 2.0 { + CREATE VIRTUAL TABLE v1 USING vec1(v); + INSERT INTO v1(rowid, v) VALUES(11, random_vector(8)); + INSERT INTO v1(rowid, v) VALUES(12, random_vector(8)); + + INSERT INTO v1(cmd, v) VALUES('rebuild', '{index:"flat"}'); +} + +# explain_i { UPDATE v1 SET rowid=11 WHERE rowid=12; } + +do_execsql_test 2.1 { + BEGIN; + INSERT INTO v1(rowid, v) VALUES(13, random_vector(8)); +} +do_catchsql_test 2.2 { + UPDATE v1 SET rowid=11 WHERE rowid=12; +} {1 {constraint failed}} + +do_execsql_test 2.3 { + COMMIT; + PRAGMA integrity_check +} {ok} + +do_execsql_test 2.4 { + SELECT rowid, length(v) FROM v1 +} {11 32 12 32 13 32} + +#------------------------------------------------------------------------- +reset_db +vec1_load db + +do_execsql_test 3.0 { + CREATE VIRTUAL TABLE v1 USING vec1(v, t); + WITH s(i) AS (VALUES(1) UNION ALL SELECT i+1 FROM s WHERE i<1000) + INSERT INTO v1 SELECT random_vector(16), '' FROM s; + + INSERT INTO v1(cmd, arg) VALUES('rebuild', ( + SELECT vec1_train(v, '{quantizer:"bq", nbucket: 1}') FROM v1 + )); +} + +do_execsql_test 3.1 { SELECT count(distance) FROM v1 } {1000} + +do_execsql_test 3.2 { + UPDATE v1 SET t='text510' WHERE rowid IN (412, 667, 2); +} + +do_execsql_test 3.3 { + PRAGMA integrity_check +} {ok} + +do_execsql_test 3.4 { + SELECT rowid FROM v1(random_vector(16), 100) WHERE t='text510' ORDER BY 1 +} {2 412 667} + +finish_test + diff --git a/ext/vec1/test/vec1update2.test b/ext/vec1/test/vec1update2.test new file mode 100644 index 0000000000..d9671e3cce --- /dev/null +++ b/ext/vec1/test/vec1update2.test @@ -0,0 +1,61 @@ +# 2026-08-04 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1update2 +vec1_load db + +do_execsql_test 1.0 { + CREATE VIRTUAL TABLE v1 USING vec1(v, x); + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<200 + ) + INSERT INTO v1 SELECT random_vector(16), 'tag' || hex(randomblob(64)) FROM s; + INSERT INTO v1(cmd, arg) VALUES('blocksize', 2048); + INSERT INTO v1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); +} + +do_execsql_test 1.1 { + SELECT sum(length(val)) FROM v1_idx; + SELECT sum(length(val)) FROM v1_meta; +} {13684 26656} + +do_execsql_test 1.2 { + SELECT count(*) FROM v1_idx; + SELECT count(*) FROM v1_meta; +} {7 7} + +do_execsql_test 1.3 { + UPDATE v1 SET v = random_vector(16); + UPDATE v1 SET v = random_vector(16); + UPDATE v1 SET v = random_vector(16); + UPDATE v1 SET v = random_vector(16); +} + +do_execsql_test 1.4 { + SELECT count(*) FROM v1_idx; + SELECT count(*) FROM v1_meta; +} {7 7} + +do_execsql_test 1.5 { + INSERT INTO v1(cmd, arg) VALUES('rebuild', '{index:"flat"}'); +} + +do_execsql_test 1.6 { + SELECT count(*) FROM v1_idx; + SELECT count(*) FROM v1_meta; +} {7 7} + + +finish_test + diff --git a/ext/vec1/test/vec1version.test b/ext/vec1/test/vec1version.test new file mode 100644 index 0000000000..a2f2deb0dc --- /dev/null +++ b/ext/vec1/test/vec1version.test @@ -0,0 +1,118 @@ +# 2026-01-17 +# +# 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. +# +#*********************************************************************** +# +# + +source [file join [file dirname [info script]] vec1_common.tcl] +set testprefix vec1version +vec1_load db + +do_execsql_test 1.0 { + CREATE VIRTUAL TABLE v1 USING vec1; +} + +do_execsql_test 1.1 { + INSERT INTO v1(rowid, vector) VALUES(1, random_vector(128)); + INSERT INTO v1(rowid, vector) VALUES(2, random_vector(128)); + INSERT INTO v1(rowid, vector) VALUES(3, random_vector(128)); + INSERT INTO v1(rowid, vector) VALUES(4, random_vector(128)); + INSERT INTO v1(rowid, vector) VALUES(5, random_vector(128)); + + INSERT INTO v1(cmd, vector) VALUES('rebuild', '{index:"flat"}'); +} + +do_execsql_test 1.2 { PRAGMA integrity_check } {ok} + +do_execsql_test 1.3 { + SELECT * FROM v1_config WHERE id=5; +} {5 4} + +do_execsql_test 1.4 { + UPDATE v1_config SET val=2 WHERE id=5; +} {} + +db close +sqlite3 db test.db +vec1_load db + +do_catchsql_test 1.5 { + SELECT rowid FROM v1(random_vector(128)); +} {1 {unrecognized index version}} + +do_execsql_test 1.6 { + UPDATE v1_config SET val=7 WHERE id=5; +} {} + +do_catchsql_test 1.7 { + SELECT rowid FROM v1(random_vector(128)); +} {1 {unsupported index version - requires vec1 0.7}} + +do_execsql_test 2.0 { + CREATE VIRTUAL TABLE v2 USING vec1; + INSERT INTO v2(rowid, vector) VALUES(5, random_vector(128)); + INSERT INTO v2_config(id, val) VALUES(17, 'hello'); +} + +db close +sqlite3 db test.db +vec1_load db + +do_catchsql_test 2.1 { + SELECT rowid FROM v2(random_vector(128), 4); +} {1 {unrecognized config entry: 17}} + +#-------------------------------------------------------------------------- + +do_execsql_test 3.0 { + CREATE VIRTUAL TABLE v3 USING vec1; +} + +do_execsql_test 3.1 { + INSERT INTO v3(rowid, vector) VALUES(1, random_vector(128)); + INSERT INTO v3(rowid, vector) VALUES(2, random_vector(128)); + INSERT INTO v3(rowid, vector) VALUES(3, random_vector(128)); + INSERT INTO v3(rowid, vector) VALUES(4, random_vector(128)); + INSERT INTO v3(rowid, vector) VALUES(5, random_vector(128)); + + INSERT INTO v3(cmd, vector) VALUES('rebuild', '{index:"flat"}'); +} + +do_test 3.2 { + set model [db one {SELECT val FROM v3_model WHERE id=1}] + set model [string replace $model 0 3 [binary format I 1005]] + db eval { UPDATE v3_model SET val = $model WHERE id=1 } +} {} + +db close +sqlite3 db test.db +vec1_load db + +do_catchsql_test 3.3 { + SELECT rowid FROM v3(random_vector(128), 4); +} {1 {vec1: unsupported model version - require vec1 1.5}} + +do_test 3.4 { + set model [db one {SELECT val FROM v3_model WHERE id=1}] + set model [string replace $model 0 3 [binary format I 2]] + db eval { UPDATE v3_model SET val = $model WHERE id=1 } +} {} + +db close +sqlite3 db test.db +vec1_load db + +do_catchsql_test 3.5 { + SELECT rowid FROM v3(random_vector(128), 4); +} {1 {vec1: unrecognized model version}} + + +finish_test + diff --git a/ext/vec1/tool/vec1_algo_test.c b/ext/vec1/tool/vec1_algo_test.c new file mode 100644 index 0000000000..42de52f242 --- /dev/null +++ b/ext/vec1/tool/vec1_algo_test.c @@ -0,0 +1,787 @@ + +#include +#include +#include +#include +#include +#include + +#include "vec1.c" + + +#ifdef VEC1_HAVE_NEON +/* +** Run nIter iterations of a loop containing N independent, back-to-back +** FMA chains (N float32x4_t accumulators, each updated once per +** iteration) and return the elapsed time on vec1HardwareTimer(). +** +** N is a literal compile-time constant at every call site (see +** MAKE_FMA_TEST below), so the "for(c...)" loop below is unrolled by +** the compiler into straight-line code - there is no loop-control +** instruction diluting the measurement, and for N==1 this is a single +** dependent chain (true FMA latency); for larger N it is N independent +** chains (FMA throughput, once N is large enough to hide the latency). +*/ +#define MAKE_FMA_TEST(N) \ +static u64 vec1FmaTest##N(int nIter){ \ + float32x4_t acc[N]; \ + float32x4_t a = vdupq_n_f32(1.000001f); \ + float32x4_t b = vdupq_n_f32(0.999999f); \ + int c, r; \ + u64 t; \ + for(c=0; cbestRate ) bestRate = fmaPerTick; + } + + nOptimal = (int)ceil(latency * bestRate); + + printf("\n"); + printf("FMA latency (N=1, dependent chain): %.3f ticks\n", latency); + printf("FMA throughput (best of N tested): %.3f FMA/tick\n", bestRate); + printf("Little's Law: accumulators needed = latency * throughput\n"); + printf(" = %.3f * %.3f = %.2f -> use %d independent accumulators\n", + latency, bestRate, latency*bestRate, nOptimal); +} +#endif /* VEC1_HAVE_NEON */ + +/* +** Populate aTrans[] with transposed version of the array of centroids in +** aCentroid[]. +*/ +static void vec1TransposeCentroids( + const float *aCentroid, /* Packed array of centroids */ + int nElem, /* Number of elements in each vector */ + int nVector, /* Number of centroid vectors */ + int nWidth, /* Transpose width */ + float *aTrans /* Populate this array with transformation */ +){ + int c, d; + float *pOut = aTrans; + + /* nVector must be an integer multiple of nWidth */ + assert( (nVector % nWidth)==0 ); + + for(c=0; c> 8) * (1.0 / 16777216.0); // 24-bit mantissa +} + +static void vec1RandomArray(Vec1Random *p, float *aRnd, int nRnd){ + int ii; + for(ii=0; iifBest && (fDist-fBest)/fDist>fTol) + || (fDistfTol) + ){ + fprintf(stderr, "algorithm FAILED...\n"); + return 1; + } + } + } +#endif + + printf("Cycles: "); + if( t>(1000*1000*1000) ){ + printf("%.2fG\n", (double)t / 1000000000.0); + }else if( t>(10*1000*1000) ){ + printf("%dM\n", (int)(t / (1000*1000))); + }else if( t>(1*1000*1000) ){ + printf("%.2fM\n", (double)t / (1000*1000)); + }else{ + printf("%d\n", (int)t); + } + + return 0; +} + + + + diff --git a/ext/vec1/tool/vec1bench.tcl b/ext/vec1/tool/vec1bench.tcl new file mode 100644 index 0000000000..e58963645d --- /dev/null +++ b/ext/vec1/tool/vec1bench.tcl @@ -0,0 +1,707 @@ + +package require sqlite3 + +set DEFAULT(--nthread) 16 +set DEFAULT(--max-train) 100000 +set DEFAULT(--train-function) vec1_train +set DEFAULT(--index) {nbucket 1024 codesize 32 opq 1} +set DEFAULT(--query) "" +set DEFAULT(--meta-data) 0 +set DEFAULT(--svd-verify) 0 +set DEFAULT(--report) "" +set DEFAULT(--repeat) 1 + +proc usage {} { + global DEFAULT + puts stderr "usage: $::argv0 ?OPTIONS? DATABASE ?SHLIB1...?" + puts stderr "options:" + foreach o [array names DEFAULT] { + set display $DEFAULT($o) + if { [string is integer $display]==0 } { set display "\"$display\"" } + puts stderr " [format {% -20s} $o](default: $display)" + } + exit 1 +} + +proc process_args {lArg} { + global C + global DEFAULT + array set C [array get DEFAULT] + + for {set i 0} {$i < [expr [llength $lArg]-1]} {incr i} { + set a [lindex $lArg $i] + if {[string range $a 0 0]!="-"} break + if {[string range $a 1 1]!="-"} { set a "-$a" } + + unset -nocomplain m + foreach o [array names C] { + if {[string match ${a}* $o]} { + if {[info exists m]} usage + set m $o + } + } + if {[info exists m]==0} usage + + incr i + set C($m) [lindex $lArg $i] + } + + lrange $lArg $i end +} + + +# Process command line arguments and open the database. And load the +# vec1.so dynamic extension(s). +# +set lExt [process_args $argv] +if {[llength $lExt]==0} usage +set C(dbname) [lindex $lExt 0] +set C(lExt) [lrange $lExt 1 end] + +proc open_db {fname} { + global C + set lExt $C(lExt); + + sqlite3 db $fname + db enable_load_extension 1 + if {[llength $lExt]==0 && [catch {db eval {SELECT vec1_info()}}]} { + set shlib [file join [file dirname [info nameofexecutable]] vec1] + db eval { SELECT load_extension($shlib) } + } else { + foreach shlib $lExt { + db eval { SELECT load_extension($shlib) } + } + } + db enable_load_extension 0 +} + +proc main { } { + global C + + if {$C(dbname)=="release"} { + if {[catch release_test msg]} { + puts stderr "ERROR: $msg" + } + return + } + + open_db $C(dbname) + check_db_looks_ok + summarize_db + build_nearest_if_required + + set r $C(--repeat) + set lResult [list] + for {set i 0} {$i < $r} {incr i} { + if {$C(--index) != ""} { + test_build_index $C(--index) + } + + if {$C(--query) != ""} { + lappend lResult [test_query_index {*}$C(--query)] + } + } + if {$C(--query) != ""} { + if {$r==1} { + print_query_timings + } else { + puts "recall: [lsort -integer $lResult]" + } + exit + } + + array set I $C(--index) + if {[info exists I(codesize)] && $I(codesize)==0} { + # If the command-line specified a code-size of 0, then do not bother + # varying K, as reranking won't help anyway. + foreach nprobe {16 32 48 64} { + test_query_index 1 1 $nprobe + test_query_index 10 10 $nprobe + } + } else { + foreach nprobe {16 32 48 64} { + test_query_index 1 1 $nprobe + test_query_index 10 1 $nprobe + test_query_index 50 1 $nprobe + test_query_index 100 1 $nprobe + test_query_index 200 1 $nprobe + test_query_index 300 1 $nprobe + test_query_index 10 10 $nprobe + test_query_index 50 10 $nprobe + test_query_index 100 10 $nprobe + test_query_index 200 10 $nprobe + test_query_index 300 10 $nprobe + test_query_index 100 100 $nprobe + test_query_index 200 100 $nprobe + test_query_index 400 100 $nprobe + test_query_index 600 100 $nprobe + test_query_index 800 100 $nprobe + test_query_index 1000 100 $nprobe + } + } + + print_report +} + +proc release_test_one {varname name index lConfig} { + global T + upvar $varname lReport + + test_build_index $index + foreach {K recall nprobe} $lConfig { + test_query_index $K $recall $nprobe + } + + set times [format_us $T(train)]/[format_us $T(build)] + lappend lReport "$name {$index}: train/build: $times" + foreach {K recall nprobe} $lConfig { + set r $T(recall,$recall,$nprobe,$K) + set q $T(qps,$recall,$nprobe,$K) + set res "[format {% -6s} $nprobe/$K] -> ${r}@$recall ${q}qps " + lappend lReport " $res" + } +} + +proc release_test {} { + global C + set C(--nthread) 8 + + set lReq [list \ + sift1m.db \ + landmark-dino-768-cosine.db \ + agnews-mxbai-1024-euclidean.db \ + ] + + set nMinByte [expr 1024*1024] + + # Check that all required databases are present in cwd + foreach f $lReq { + if {[file exists $f]==0 || [file size $f]<$nMinByte} { + error "missing required db file \"$f\"" + } + } + + open_db sift1m.db + build_nearest_if_required + release_test_one lRep SIFT1M "nbucket 1024 codesize 16 opq 0" { + 1 1 16 + 100 1 16 + 1 1 48 + 100 1 48 + } + release_test_one lRep SIFT1M "nbucket 0 codesize 16 opq 0" { + 1 1 16 + 100 1 16 + } + release_test_one lRep SIFT1M "nbucket 1024 codesize 0 opq 0" { + 1 1 16 + 1 1 48 + } + + open_db landmark-dino-768-cosine.db + build_nearest_if_required + release_test_one lRep LANDMARK "nbucket 1024 codesize 48 opq 1" { + 1 1 16 + 100 1 48 + } + + open_db agnews-mxbai-1024-euclidean.db + build_nearest_if_required + release_test_one lRep AGNEWS "nbucket 1024 codesize 32 opq 1" { + 1 1 16 + 100 1 48 + } + + foreach ln $lRep { puts $ln } + + set fd [open vec1.release.txt a+] + puts $fd "--------------------------------------------------------------" + foreach ln $lRep { puts $fd $ln } + close $fd +} + +proc format_cycles {c} { + if {$c>(1000*1000*1000)} { + set ret "[format %.2f [expr $c/1000000000.0]]G" + } elseif {$c>(10*1000000)} { + set ret "[format %d [expr $c/1000000]]M" + } elseif {$c>(1000000)} { + set ret "[format %.2f [expr $c/1000000.0]]M" + } elseif {$c>(10*1000)} { + set ret "[format %d [expr $c/1000]]K" + } elseif {$c>1000} { + set ret "[format %.2f [expr $c/1000.0]]K" + } else { + set ret $c + } + + return $ret +} + +# Read the qprofile information for table "v1" from eponymous virtual +# table vec1cat. Print a report to stdout based on the values read. +# +proc print_query_timings {} { + + set lTopic [list] + db eval { + SELECT fullkey, value FROM json_tree( + (SELECT qprofile FROM vec1cat WHERE name='v1') + ) WHERE type='integer'; + } { + if {$value>0} { + set key [string range $fullkey 2 end] + set A($key) $value + + foreach {topic field} [split $key .] { + if {$field=="cnt" && $topic!="total"} { lappend lTopic $topic } + } + } + } + + set nTotal $A(total.cycle); + set total_percent 0.0 + foreach name $lTopic { + set cnt $A(${name}.cnt) + set cycle $A(${name}.cycle) + + set percent [expr (100.0 * $cycle) / $nTotal] + set total_percent [expr $total_percent + $percent] + set c [format_cycles $cycle] + puts "[format {% -12s%8s%6.2f%%} $name: $c $percent] ($cnt)" + } + puts [format {% -12s%6.2f%%} total: $total_percent] +} + + +# Argument $t is some number of microseconds. Return a string making +# this easy for humans to read - e.g. "123us" or "12.1ms". +# +proc format_us {t} { + set ret "${t}us" + if {$t>(10*1000000)} { + set ret "[format %.1f [expr $t/1000000.0]]s" + } elseif {$t>1000000} { + set ret "[format %.2f [expr $t/1000000.0]]s" + } elseif {$t>100000} { + set ret "[format %d [expr $t/1000]]ms" + } elseif {$t>10000} { + set ret "[format %.1f [expr $t/1000.0]]ms" + } elseif {$t>1000} { + set ret "[format %.2f [expr $t/1000.0]]ms" + } else { + set ret "${t}us" + } + return $ret +} + +# Add commas to integer argument $i to make it easier to read. +# e.g. 12345 -> "12,345" +# +proc format_int {i} { + set ret [list] + while {$i>0} { + if {$i>=1000} { + set ret [concat [format %.3d [expr $i % 1000]] $ret] + } else { + set ret [concat $i $ret] + } + set i [expr $i / 1000] + } + join $ret , +} + +proc my_time {caption script} { + puts -nonewline "$caption..." + flush stdout + set t [uplevel [list time $script]] + set t [lindex $t 0] + + puts " [format_us $t]" + + return $t +} + + +# Check that the __meta__ table looks ok. This command checks that there +# are "train", "test", "neighbors" and "distances" tables, with types +# float32, float32, int64 and float64, respectively, in the db. If not, +# it prints out and error and exits. +# +proc check_db_looks_ok {} { + db eval { + WITH expect(nm, tp) AS ( + SELECT 'train', 'float32' UNION ALL + SELECT 'test', 'float32' UNION ALL + SELECT 'neighbors', 'int64' UNION ALL + SELECT 'neighbors', 'int32' + ), + have(nm, tp) AS ( + SELECT table_name, dtype + FROM __meta__ + WHERE table_name IN ('train', 'test', 'neighbors') + ) + + SELECT (count(*)==3) as 'ok' FROM have WHERE (nm, tp) IN ( + SELECT nm, tp FROM expect + ); + } { + if {$ok==0} { + puts stderr "database does not look right. bailing out..." + exit -1 + } + } +} + +proc get_distance_function {} { + set dm [db one {SELECT value FROM meta WHERE field='distance'}] + + set f(cosine) vec1_cos_distance + set f(angular) vec1_cos_distance + set f(euclidean) vec1_l2_distance + set f(normalized) vec1_l2_distance + + if {[info exists f($dm)]==0} { + puts "Unrecognized distance metric: $dm" + exit 1 + } + return $f($dm) +} + +# Output a summary of the db for the user to read. +# +proc summarize_db {} { + set nQuery [db one {SELECT count(*) FROM test}] + set nVector [db one {SELECT count(*) FROM train}] + set nDim [db one {SELECT length(vec)/4 FROM train LIMIT 1}] + set dm [db one {SELECT 'dist=' || value FROM meta WHERE field='distance'}] + set cmp "$dm -> [get_distance_function]" + puts "Test is $nQuery queries against $nVector ${nDim}-d vectors ($cmp)" +} + +proc vec_to_json_i64 {blob} { + binary scan $blob w* L + set ret "\[[join $L ,]\]" + return $ret +} +proc vec_to_json_i32 {blob} { + binary scan $blob i* L + set ret "\[[join $L ,]\]" + return $ret +} + +proc myvar {var} { + upvar $var v + if {[info exists v]==0} { return "" } + return $v +} + +proc print_report {} { + global T + global C + + proc vkey_cmp {lhs rhs} { + foreach {lr lp} [split $lhs ,] {} + foreach {rr rp} [split $rhs ,] {} + + if {$rr==$lr} { + if {$lp<$rp} { return -1 } + if {$lp>$rp} { return +1 } + } + return [expr $rr - $lr] + } + + foreach k [array names T qps*] { + foreach {q recall nprobe k} [split $k ,] {} + set RECALL($recall) 1 + set V($recall,$nprobe) 1 + set H($k) 1 + } + + if {$C(--report)==""} { + set fd stdout + } else { + set fd [open $C(--report) a] + } + + set nVector [format_int [db one {SELECT count(*) FROM train}]] + set nSample [format_int $T(nTrain)] + set nDim [db one {SELECT length(vec)/4 FROM train LIMIT 1}] + set zDataset [file tail $C(dbname)] + regsub {\..*} $zDataset {} zDataset + + puts $fd "" + + puts $fd "

Dataset: $zDataset ($nVector ${nDim}d vectors)

" + puts $fd "" + puts $fd "
ParameterValue" + puts $fd "
Vec1 Version[db one {SELECT vec1_info()}]" + puts $fd "
Index Parameters$T(spec)" + puts $fd "
Training time[format_us $T(train)] ($nSample samples, $C(--nthread) threads)" + puts $fd "
Build time[format_us $T(build)] ($C(--nthread) threads)" + puts $fd "
" + puts $fd "" + + foreach r [lsort -integer [array names RECALL]] { + puts $fd "

Recall@$r results:" + puts $fd "" + puts $fd "" + + catch { array unset K } + catch { array unset N } + foreach key [array names T qps*] { + foreach {q recall nprobe k} [split $key ,] {} + if {$r==$recall} { + set K($k) 1 + set N($nprobe) 1 + } + } + + puts -nonewline $fd "
" + foreach k [lsort -integer [array names K]] { + puts -nonewline $fd "K=$k" + } + puts $fd "" + puts $fd "
nprobe" + foreach k [lsort -integer [array names K]] { + puts -nonewline $fd "RecallQPS" + } + puts $fd "" + + foreach nprobe [lsort -integer [array names N]] { + puts -nonewline $fd "
$nprobe" + foreach k [lsort -integer [array names K]] { + puts -nonewline $fd "[format %.3f $T(recall,$r,$nprobe,$k)]" + puts -nonewline $fd "$T(qps,$r,$nprobe,$k)" + } + puts $fd "" + } + puts $fd "
" + } + + puts $fd "" + puts $fd "" + puts $fd "" + + if {$fd!="stdout"} { + puts "Report output to $C(--report)" + close $fd + } +} + +# See if the "nearest" table has been built yet. If not, create and +# populate it now. It contains one row per test vector. +# +proc build_nearest_if_required {} { + set dm [db one {SELECT value FROM meta WHERE field='distance'}] + + set fmt [db one {SELECT dtype FROM __meta__ WHERE table_name='neighbors'}] + if {$fmt=="int64"} { + db func ivec_to_json vec_to_json_i64 + } else { + db func ivec_to_json vec_to_json_i32 + } + + set func [get_distance_function] + db eval BEGIN + if {[db one {SELECT count(*) FROM sqlite_schema WHERE name='nearest'}]==0} { + db eval " + CREATE TABLE nearest( + id INTEGER PRIMARY KEY, nn1 JSON, nn10 JSON, nn100 JSON + ); + + WITH groundtruth(qid, val, dist) AS ( + SELECT q.id, j.value, $func (q.vec, b.vec) + FROM test q, + neighbors g, + json_each(ivec_to_json(g.vec)) j, + train b + WHERE q.id=g.id /* AND j.key<20 */ AND b.id=j.value + ), + ranked(qid, val, rank) AS ( + SELECT qid, val, rank() OVER ( + PARTITION BY qid ORDER BY dist + ) FROM groundtruth + ) + INSERT INTO nearest + SELECT qid, + json_group_array(val) FILTER (WHERE rank=1), + json_group_array(val) FILTER (WHERE rank<=10), + json_group_array(val) + FROM ranked + GROUP BY qid + " + } + db eval COMMIT +} + +proc test_build_index {lSpec} { + global C + global T + + set func [get_distance_function] + set DIST(vec1_l2_distance) "\"L2\"" + set DIST(vec1_cos_distance) "\"cos\"" + + set cols "" + set values "" + if {$C(--meta-data)==1} { + set cols ", m1" + set values ", 100" + } elseif {$C(--meta-data)==4} { + set cols ", m4" + set values ", 10000" + } + + # Drop any old "v1" table that may exist. And create the new one. + # + db eval " + DROP TABLE IF EXISTS v1; + CREATE VIRTUAL TABLE v1 USING vec1(vector $cols); + SELECT vec1_config('nthread', $C(--nthread) ); + INSERT INTO v1(rowid, vector $cols) SELECT id, vec $values FROM train; + " + + if {$lSpec=="flat"} { + db eval " + INSERT INTO v1(cmd, arg) + VALUES('rebuild', '{index:\"flat\",distance: $DIST($func)}') + " + return + } + + set pairs [list] + foreach {k v} $lSpec { + set val $v + if {[string is double $val]==0} { + set val "\"$v\"" + } + lappend pairs "$k:$val" + } + set showspec "{[join $pairs {, }]}" + lappend pairs "svd_verify: $C(--svd-verify)" + lappend pairs "progress: \"myprogress\"" + lappend pairs "profile: \"myprofile\"" + set spec "{[join $pairs {, }]}" + + # Command to handle vec1_train() progress callbacks. + # + unset -nocomplain ::myprogressms + proc myprogress {percent msg} { + catch { + if {[info exists ::myprogressms]==0} { + set ::myprogressms [clock milli] + } + set ms [expr [clock milli] - $::myprogressms] + puts "${ms}ms: $percent%: $msg" + } xyz + } + db func myprogress myprogress + + # Command to handle vec1_train() profile callbacks. + # + proc myprofile {p} { + puts "profile: $p" + } + db func myprofile myprofile + + # Check if a "learn" table exists. If it does not, use data from "train" + # to train the model. + # + if {[db one {SELECT count(*) FROM sqlite_schema WHERE name='learn'}]==0} { + set learn "train" + } else { + set learn "learn" + } + set nTotal [db one "SELECT count(*) FROM $learn"] + set nTrain [expr {$nTotal > $C(--max-train) ? $C(--max-train) : $nTotal}] + + set caption "$C(--train-function)($spec) on $nTrain/$nTotal \"$learn\" rows" + + set T(nTrain) $nTrain + set T(spec) $showspec + set T(train) [my_time "Training: $caption" { + puts "" + set model [db one [subst { + SELECT $C(--train-function)(vec, '$spec') FROM $learn + WHERE ( + (abs(random()) / 9223372036854775807.0) * $nTotal <= $nTrain + ) + }]] + }] + + set T(build) [my_time "Building index" { + db eval { + INSERT INTO v1(cmd, vector) VALUES('rebuild', $model); + } + }] +} + +proc test_query_index {K {recall 1} {nprobe 0.05} } { + global T + global C + + set nMmap [db one { PRAGMA mmap_size = 20000000000 }] + puts "Using [expr ${nMmap}/(1024*1024)]MB mmap region" + db eval { + PRAGMA synchronous = off; + SELECT rowid FROM v1 LIMIT 1; + SELECT vec1_config('nprobe', $nprobe); + } + + set func [get_distance_function] + + set nn "nn$recall" + set sql1 [subst " + SELECT vec, $nn AS nn, test.id AS id + FROM test JOIN nearest ON (test.id=nearest.id) + "] + + #set spec "{K:$K,nprobe:$nprobe,nprobe_slack:0.9}" + set spec "{K:$K,nprobe:$nprobe}" + + set tail "" + if {$C(--meta-data)==1} { + append tail "WHERE m1=100" + } elseif {$C(--meta-data)==4} { + append tail "WHERE m4=10000" + } + + if {$K!=$recall} { + append tail [subst -novar { + ORDER BY [set func] ($vec, v1.vector) + LIMIT [set recall]}] + } + + set sql2 [subst -novar { + SELECT coalesce(sum( ans IN (SELECT value FROM json_each($nn) )), 0) FROM ( + SELECT v1.rowid AS ans + FROM v1($vec, $spec) [set tail] + ) + }] + + set nQuery [db one {SELECT count(*) FROM test}] + set caption "Testing $nQuery recall@$recall queries $spec" + + set s 0 + set t [my_time $caption { + set nQuery 0 + db eval $sql1 { + set nGood [db one $sql2] + # if {$nGood!=$recall} { puts "id=$id nGood=$nGood" } + incr s $nGood + incr nQuery + } + puts -nonewline "($s / [expr $recall*$nQuery]) " + }] + + set qps [expr ($nQuery*1000000 / $t)] + set T(qps,$recall,$nprobe,$K) $qps + set T(recall,$recall,$nprobe,$K) [expr (($s * 1.0) / ($nQuery * $recall))] + + return $s +} + +main +exit + diff --git a/ext/vec1/tool/vec1bench_addcols.tcl b/ext/vec1/tool/vec1bench_addcols.tcl new file mode 100644 index 0000000000..c9a7038823 --- /dev/null +++ b/ext/vec1/tool/vec1bench_addcols.tcl @@ -0,0 +1,121 @@ + +# +# This script edits a vec1bench database to add constant dimensions to +# the data, query and (if any) training vectors in the db. This should +# not change the results of any query, but did at one point confound +# the OPQ training code. +# + +package require sqlite3 + +set DEFAULT(--ncol) 1 +set DEFAULT(--zero) 0 + +proc usage {} { + global DEFAULT + puts stderr "usage: $::argv0 ?OPTIONS? DATABASE" + puts stderr "options:" + foreach o [array names DEFAULT] { + set display $DEFAULT($o) + if { [string is integer $display]==0 } { set display "\"$display\"" } + puts stderr " [format {% -20s} $o](default: $display)" + } + exit 1 +} + +proc process_args {lArg} { + global C + global DEFAULT + array set C [array get DEFAULT] + + for {set i 0} {$i < [expr [llength $lArg]-1]} {incr i} { + set a [lindex $lArg $i] + if {[string range $a 0 0]!="-"} break + if {[string range $a 1 1]!="-"} { set a "-$a" } + + unset -nocomplain m + foreach o [array names C] { + if {[string match ${a}* $o]} { + if {[info exists m]} usage + set m $o + } + } + if {[info exists m]==0} usage + + incr i + set C($m) [lindex $lArg $i] + } + + lrange $lArg $i end +} + + +# Check that the __meta__ table looks ok. This command checks that there +# are "train", "test", "neighbors" and "distances" tables, with types +# float32, float32, int64 and float64, respectively, in the db. If not, +# it prints out and error and exits. +# +proc check_db_looks_ok {} { + db eval { + WITH expect(nm, tp) AS ( + SELECT 'train', 'float32' UNION ALL + SELECT 'test', 'float32' UNION ALL + SELECT 'neighbors', 'int64' UNION ALL + SELECT 'neighbors', 'int32' + ), + have(nm, tp) AS ( + SELECT table_name, dtype + FROM __meta__ + WHERE table_name IN ('train', 'test', 'neighbors') + ) + + SELECT (count(*)==3) as 'ok' FROM have WHERE (nm, tp) IN ( + SELECT nm, tp FROM expect + ); + } { + if {$ok==0} { + puts stderr "database does not look right. bailing out..." + exit -1 + } + } +} + + +# Process command line arguments and open the database. And load the +# vec1.so dynamic extension(s). +# +set lExt [process_args $argv] +if {[llength $lExt]!=1} usage +set C(dbname) [lindex $lExt 0] + +proc main { } { + global C + + sqlite3 db $C(dbname) + + check_db_looks_ok + + set nCol $C(--ncol) + set zstr [string repeat 00000000 $nCol] + + db transaction { + foreach tbl {test train learn} { + if {$C(--zero)} { + catch { db eval "UPDATE $tbl SET vec = unhex( hex(vec) || '$zstr')" } + } else { + catch { db eval " + UPDATE $tbl SET vec = unhex( hex(vec) || substr(hex(vec), 1, 8*$nCol) ) + " } + } + } + } + if {$C(--zero)} { + puts "Appended $nCol 0.0 floats to all vectors" + } else { + puts "Appended duplicate of first $nCol floats to all vectors" + } +} + +main +exit + diff --git a/ext/vec1/tool/vec1bench_vibe2db.py b/ext/vec1/tool/vec1bench_vibe2db.py new file mode 100644 index 0000000000..089c76e442 --- /dev/null +++ b/ext/vec1/tool/vec1bench_vibe2db.py @@ -0,0 +1,91 @@ + +import h5py +import sqlite3 +import numpy as np +import os +import sys + +def sqlite_type_for(dtype): + # Everything goes in as BLOB; dtype is recorded separately + return "BLOB" + +def sanitize(name): + # HDF5 paths like "/train" → "train" + return name.strip("/").replace("/", "_") + +def main(h5_path, sqlite_path): + if os.path.exists(sqlite_path): + os.remove(sqlite_path) + + conn = sqlite3.connect(sqlite_path) + cur = conn.cursor() + + with h5py.File(h5_path, "r") as f: + + dm = "unknown" + cur.execute("CREATE TABLE meta(field TEXT, value ANY)") + if "distance" in f.attrs: + dm = f.attrs["distance"] + if isinstance(dm, bytes): + dm = dm.decode("utf-8") + cur.execute("INSERT INTO meta VALUES('distance', ?)", (dm,)) + + print("VIBE distance metric:", dm) + + def visit(name, obj): + if not isinstance(obj, h5py.Dataset): + return + + + + data = obj[()] + table = sanitize(name) + + print(f"Importing {name} → table '{table}' shape={data.shape} dtype={data.dtype}") + + # Create table + cur.execute(f""" + CREATE TABLE {table} ( + id INTEGER PRIMARY KEY, + vec BLOB NOT NULL + ) + """) + + # Metadata table (created once) + cur.execute(""" + CREATE TABLE IF NOT EXISTS __meta__ ( + table_name TEXT PRIMARY KEY, + dtype TEXT, + shape TEXT + ) + """) + + cur.execute( + "INSERT INTO __meta__ VALUES (?, ?, ?)", + (table, str(data.dtype), str(data.shape)) + ) + + # Insert rows + if data.ndim == 1: + rows = [(i, data[i].tobytes()) for i in range(data.shape[0])] + else: + rows = [(i, data[i].tobytes()) for i in range(data.shape[0])] + + cur.executemany( + f"INSERT INTO {table} (id, vec) VALUES (?, ?)", + rows + ) + + conn.commit() + + f.visititems(visit) + + conn.close() + print("Done.") + +if __name__ == "__main__": + if len(sys.argv) != 3: + print("usage: hdf5_to_sqlite.py input.hdf5 output.sqlite") + sys.exit(1) + + main(sys.argv[1], sys.argv[2]) diff --git a/ext/vec1/vec1.c b/ext/vec1/vec1.c new file mode 100644 index 0000000000..c21cb372b1 --- /dev/null +++ b/ext/vec1/vec1.c @@ -0,0 +1,12938 @@ +/* +** 2026-02-04 +** +** 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. +** +************************************************************************* +** +** This file contains the SQLite vector ANN extension "vec1". To build +** as a loadable module on x86_64 with gcc: +** +** gcc -O3 -DNDEBUG -mavx2 -mfma vec1.c -shared -fPIC -o vec1.so +** +** Documentation available at: +** +** https://sqlite.org/vec1 +*/ + +/* +** The two distance metrics supported by the system. +*/ +#define VEC1_DISTANCE_L2 1 +#define VEC1_DISTANCE_COS 2 + +/* +** Default values for training parameters. +*/ +#define VEC1_TRAINING_DEFAULT_CODESIZE 0 +#define VEC1_TRAINING_DEFAULT_NBUCKET 0 +#define VEC1_TRAINING_DEFAULT_SVD_VERIFY 0 +#define VEC1_TRAINING_DEFAULT_OPQ 0 +#define VEC1_TRAINING_DEFAULT_NOPQ_ROUND 5 +#define VEC1_TRAINING_DEFAULT_RESIDUAL 1 +#define VEC1_TRAINING_DEFAULT_DISTANCE VEC1_DISTANCE_L2 + +/* +** Max and min values for various parameters. +** +** VEC1_MAX_K: +** At present, a KANN query with an index allocates K*16 bytes for +** the heap data structure used to accumulate the K best matches. +** In its default configuration, sqlite3_malloc() is limited to +** 1GB allocations. So limit K so as to at least avoid SQLITE_NOMEM +** errors in the default configuration. +*/ +#define VEC1_MAX_K 32*1024*1024 + +/* +** Software version. Returned by "SELECT vec1_info()". +*/ +#define VEC1_VERSION "0.7" + +/* +** Values for Vec1ModelHeader.iVersion: +** +** VEC1_HEADER_VERSION: +** Used by default, if nothing below applies. +** +** VEC1_HEADER_VERSION_BIN: +** Used if the model uses a binary-quantizer. +*/ +#define VEC1_HEADER_VERSION (0*1000 + 4) +#define VEC1_HEADER_VERSION_BIN (0*1000 + 7) + +/* +** Shadow tables version. Stored in %_config table. Corresponds to the +** minimum version that matches the current shadow table setup. +*/ +#define VEC1_CURRENT_FMTVERSION (0*1000 + 4) + + +#include "sqlite3.h" + +/* +** Unless VEC1_STATIC is defined, compile as a loadable extension. +*/ +#ifndef VEC1_STATIC + +# include "sqlite3ext.h" + SQLITE_EXTENSION_INIT3 + +#endif + +/* System include files */ +# include +# include +# include +# include +# include +# include + +/* Standard sized types. */ +typedef sqlite3_int64 i64; +typedef sqlite3_uint64 u64; +typedef unsigned char u8; +typedef unsigned int u32; +typedef unsigned short u16; +typedef float f32; +typedef int i32; +typedef char i8; + +#if !defined(VEC1_TEST_SCALAR) + +#define VERTICAL_SUM_GENERIC(aAcc, nAcc, I) { \ + int c; \ + for(c=4; c1 ) aAcc[0] = I (aAcc[0], aAcc[1]); \ +} + +#ifdef __AVX2__ + +# define VEC1_HAVE_AVX2 1 + +# define VEC1_TRANSPOSE_WIDTH 32 +# define VEC1_NTRANSPOSE_WIDTH 8 + +/* +** Dimensions used for vec1BestMatchN() and vec1BestMatchTransposeN(). +*/ +# define VEC1_MULTIMATCH_NVEC 8 +# define VEC1_MULTIMATCH_NCENT 8 + +/* AVX2 builds should use transposed arrays of centroids where possible. +** This avoids horizontal sums (expensive by design on AVX2) and frees +** up registers, allowing more concurrent FMA and other operations. */ +# define VEC1_USE_TRANSPOSITION 1 + +/* +** When assembling the covariance matrix (part of OPQ rotation training), +** deal with (VEC1_COVAR_MULTIMATCH_MULT * VEC1_MULTIMATCH_NVEC) vectors +** at a time. See vec1CovarianceWork() for details. +*/ +# define VEC1_COVAR_MULTIMATCH_MULT 1 + +# include + +# ifdef __FMA__ +# define FMADD(a,b,c) _mm256_fmadd_ps((a),(b),(c)) +# define FMADD64(a,b,c) _mm256_fmadd_pd((a),(b),(c)) +# else +# define FMADD(a,b,c) _mm256_add_ps((c), _mm256_mul_ps((a),(b))) +# define FMADD64(a,b,c) _mm256_add_pd((c), _mm256_mul_pd((a),(b))) +# endif + +/* Load 8 unaligned floats. Or 4 unaligned doubles. */ +# define LOADU(a) _mm256_loadu_ps(a) +# define LOADU64(a) _mm256_loadu_pd(a) + +/* +** Parameter vec is a __m256 register containing 8 floats. Parameter +** s is a float. This macro sets s to the sum of the 8 values in vec. i.e. +** +** s = vec[0] + vec[1] + vec[2] + vec[3] +** + vec[4] + vec[5] + vec[6] + vec[7]; +*/ +# define HORIZONTAL_SUM(s, vec) \ + { \ + __m128 vc_high = _mm256_extractf128_ps(vec, 1); \ + __m128 vc_low = _mm256_castps256_ps128(vec); \ + __m128 vc128 = _mm_add_ps(vc_high, vc_low); \ + vc128 = _mm_hadd_ps(vc128, vc128); \ + vc128 = _mm_hadd_ps(vc128, vc128); \ + s = _mm_cvtss_f32(vc128); \ + } + +/* +** Parameter vec is a __m256 register containing 4 doubles. Parameter +** s is a double. This macro sets s to the sum of the 4 values in vec. i.e. +** +** s = vec[0] + vec[1] + vec[2] + vec[3] +*/ +# define HORIZONTAL_SUM64(s, vec) \ + { \ + __m128d _lo = _mm256_castpd256_pd128(vec); \ + __m128d _hi = _mm256_extractf128_pd(vec, 1); \ + __m128d _sum = _mm_add_pd(_lo, _hi); \ + (s) = _mm_cvtsd_f64(_mm_hadd_pd(_sum, _sum)); \ + } + +/* +** aAcc[] is an nAcc element array of type _m256 containing 8 32-bit floats. +** This macro sums the vectors and writes the results to aAcc[0]. +*/ +# define VERTICAL_SUM(aAcc, nAcc) VERTICAL_SUM_GENERIC(aAcc,nAcc,_mm256_add_ps) + +#elif defined(__aarch64__) + +# define VEC1_HAVE_NEON 1 + +# define VEC1_TRANSPOSE_WIDTH 32 + +# define VEC1_MULTIMATCH_NVEC 4 +# define VEC1_MULTIMATCH_NCENT 4 + +# define VEC1_COVAR_MULTIMATCH_MULT 4 + +# include + +/* +** aAcc[] is an nAcc element array of type float32x4. This macro sums the +** vectors and writes the results to aAcc[0]. +*/ +#define VERTICAL_SUM(aAcc, nAcc) VERTICAL_SUM_GENERIC(aAcc, nAcc, vaddq_f32) + +#endif + +#endif /* VEC1_TEST_SCALAR */ + + +#ifndef VEC1_TRANSPOSE_WIDTH + +/* Scalar fallback. Even scalar needs transpose widths. */ +# define VEC1_TRANSPOSE_WIDTH 8 + +# define VEC1_MULTIMATCH_NVEC 4 +# define VEC1_MULTIMATCH_NCENT 4 + +# define VEC1_COVAR_MULTIMATCH_MULT 1 + +#endif + +#define VEC1_LARGEST_INT64 (0xffffffff|(((i64)0x7fffffff)<<32)) +#define VEC1_SMALLEST_INT64 (((i64)-1) - VEC1_LARGEST_INT64) + +#define SWAP(T, a, b) { T tmp; tmp = a; a = b; b = tmp; } + +/* +** Return a hardware counter value. These are used to calculate the +** percentages in the performance reports from vec1cat.qprofile and the +** final progress callback from within vec1_train(). +*/ +static u64 vec1HardwareTimer(void){ + u64 ret = 0; + +#if !defined(__STRICT_ANSI__) +# if defined(__aarch64__) + __asm__ __volatile__("mrs %0, cntvct_el0" : "=r"(ret)); +# elif defined(__GNUC__) && defined(__x86_64__) + unsigned int lo, hi; + __asm__ __volatile__("rdtscp" : "=a"(lo), "=d"(hi) :: "%rcx"); + ret = ((u64)hi << 32) | lo; +# endif +#endif + + return ret; +} + + +/* +** Figure out which threading model to use. Define VEC1_THREADS_WINDOWS +** to use windows threads, VEC1_THREADS_PTHREADS to use pthreads, or +** neither to use no threads. +*/ +#if !defined(VEC1_THREADS) || VEC1_THREADS!=0 +# if defined(_WIN32) +# define VEC1_THREADS_WINDOWS 1 +# define VEC1_THREADS 1 +# elif defined(__unix__) || defined(__APPLE__) || defined(__linux__) +# define VEC1_THREADS_PTHREADS +# define VEC1_THREADS 1 +# else +# define VEC1_THREADS 0 +# endif +#endif + +#define size_of_array(X) ((int)(sizeof(X)/sizeof((X)[0]))) +#define MAX(a,b) ((ab) ? (b) : (a)) + +#define UNUSED_PARAMETER(x) (void)(x) +#define UNUSED_PARAMETER2(x,y) (void)(x),(void)(y) + +typedef struct Vec1Tab Vec1Tab; +typedef struct Vec1TabList Vec1TabList; + +/* +** An instance of the following object is allocated when registering +** the vec1 module with a database handle. A pointer is passed as the +** the context pointer to sqlite3_create_module_v2(). It maintains a +** list of all Vec1Tab tables attached to the database handle. +** +** This object does not need a mutex, as it belongs to a single db handle. +** The db handle mutex ensures exclusive access. +*/ +struct Vec1TabList { + int nRef; + sqlite3 *db; + Vec1Tab *pFirst; + int nThread; /* Number of threads to use for various ops */ + double nProbeArg; /* Default "nprobe" for queries */ +}; + +/* +** This structure represents a model-header. When serialized, the format +** is a packed array of six 32-bit big-endian integers, in the order +** shown in the struct below. +*/ +typedef struct Vec1ModelHeader Vec1ModelHeader; +struct Vec1ModelHeader { + u32 iVersion; /* Version number currently 0004 */ + u32 flags; + u32 nElem; /* Size of vectors in elements */ + u32 nCodebook; /* Number of codebooks for PQ */ + u32 nBucket; /* Number of buckets for IVF */ + u32 eDistance; /* A VEC1_DISTANCE_XXX value */ +}; +#define VEC1_HEADER_SIZE (6*sizeof_u32) + +/* +** Candidate values for the "flags" field of a Vec1ModelHeader. As follows: +** +** VEC1_MODEL_INDEX: +** Set for any configuration except "none". +** +** VEC1_MODEL_ROTATE: +** Set if model includes a full rotation matrix. +** +** VEC1_MODEL_RESIDUAL: +** Set if residual, not full, vectors are encoded. +** +** VEC1_MODEL_BITQUANT: +** Set to use bitwise quantization instead of PQ. Compressed vectors are +** a 32-bit float (the norm) followed by bitwise sketch of the vector +** or residual vector. +** +** VEC1_MODEL_WHT: +** Do Walsh Hadamard transform. +*/ +#define VEC1_MODEL_INDEX 0x01 +#define VEC1_MODEL_ROTATE 0x02 +#define VEC1_MODEL_RESIDUAL 0x04 +#define VEC1_MODEL_BITQUANT 0x08 +#define VEC1_MODEL_WHT 0x10 + +/* +** Use these instead of actual sizeof() macros to avoid signed/unsigned +** compiler warnings. +*/ +#define sizeof_u16 2 +#define sizeof_u32 4 +#define sizeof_f32 4 +#define sizeof_f64 8 + +/* +** The three types of vector elements (that will eventually be) supported. +*/ +#define VEC1_TYPE_FLOAT32 0 +#define VEC1_TYPE_INT32 1 +#define VEC1_TYPE_INT8 2 + +/* +** Maximum allowed threads for both vec1 and vec1_train(). +*/ +#define VEC1_MAX_NTHREAD 128 + +static int vec1CorruptError(){ + return SQLITE_CORRUPT_VTAB; +} +#define VEC1_CORRUPT vec1CorruptError(); + +static void vec1PutU32(u8 *aBuf, u32 val){ + aBuf[0] = (val >> 24) & 0xFF; + aBuf[1] = (val >> 16) & 0xFF; + aBuf[2] = (val >> 8) & 0xFF; + aBuf[3] = (val >> 0) & 0xFF; +} + +/* +** Write a model header into buffer aBuf[]. +*/ +static void vec1HeaderWrite( + u8 *aBuf, + int flags, + int nElem, + int nCodebook, + int nBucket, + int eDistance +){ + /* VEC1_HEADER_VERSION_BIN if the model uses a binary quantizer, + ** VEC1_HEADER_VERSION otherwise. */ + u32 eHdrVersion = VEC1_HEADER_VERSION; + if( flags & VEC1_MODEL_BITQUANT ){ + eHdrVersion = VEC1_HEADER_VERSION_BIN; + } + + vec1PutU32(&aBuf[0], eHdrVersion); + vec1PutU32(&aBuf[4], flags); + vec1PutU32(&aBuf[8], nElem); + vec1PutU32(&aBuf[12], nCodebook); + vec1PutU32(&aBuf[16], nBucket); + vec1PutU32(&aBuf[20], eDistance); +} + + +static u32 vec1GetU32(const u8 *aBuf){ + return (((u32)aBuf[0]) << 24) + + (((u32)aBuf[1]) << 16) + + (((u32)aBuf[2]) << 8) + + (((u32)aBuf[3]) << 0); +} + +/* +** Deserialize buffer aBuf into object p. +*/ +static void vec1HeaderRead(const u8 *aBuf, Vec1ModelHeader *p){ + p->iVersion = vec1GetU32(&aBuf[0]); + p->flags = vec1GetU32(&aBuf[4]); + p->nElem = vec1GetU32(&aBuf[8]); + p->nCodebook = vec1GetU32(&aBuf[12]); + p->nBucket = vec1GetU32(&aBuf[16]); + p->eDistance = vec1GetU32(&aBuf[20]); +} + +/* +** Version of sqlite3_free() that can be used as a destructor function with +** things like sqlite3_bind_blob(). +*/ +static void vec1SqliteFree(void *x){ + sqlite3_free(x); +} + +typedef struct Vec1AnnResult Vec1AnnResult; +struct Vec1AnnResult { + sqlite3_int64 iRowid; + double fDist; +}; + +typedef struct Vec1AnnHeap Vec1AnnHeap; +struct Vec1AnnHeap { + Vec1AnnResult *aRes; /* Heap array */ + i64 nRes; /* Current number of elements */ + i64 nMax; /* Capacity */ + i64 nAlloc; /* Current allocated size of aRes[] */ + int rc; /* Error code (perhaps SQLITE_NOMEM) */ + int bStreaming; + double fMin; /* Current threshold for admittance */ +}; + +typedef struct Vec1Buffer Vec1Buffer; +struct Vec1Buffer { + unsigned char *a; + int n; + int nAlloc; +}; + +/* +** Return the dot-product of vectors A and B, each d elements in size. +*/ +static float vec1DotProduct( + int d, + const float *A, + const float *B +){ + float sum = 0.0f; + int j = 0; + +#ifdef VEC1_HAVE_AVX2 +# ifndef VEC1_DOT_NACC +# define VEC1_DOT_NACC 4 +# endif + const int nAcc = VEC1_DOT_NACC; + int cc; + + /* Create and initialize to zero nAcc accumulator registers. */ + __m256 acc[VEC1_DOT_NACC]; + for(cc=0; cc y using row-major matrix A. +*/ +static void vec1RotateVector( + int d, + const float* A, /* d x d, row-major */ + const float* x, /* input vector, length d */ + float* y /* output vector, length d */ +){ + int i; + for(i=0; iaRes = (Vec1AnnResult *)sqlite3_malloc64(sizeof(Vec1AnnResult) * nAlloc); + if( p->aRes==0 ){ + return SQLITE_NOMEM; + } + p->nRes = 0; + p->nMax = nMax; + p->nAlloc = nAlloc; + p->bStreaming = bStreaming; + p->fMin = INFINITY; + return SQLITE_OK; +} + +/* +** A new element, element i, has just been added to the heap as a new leaf +** node. This function repairs the heap by bubbling-up from the new node +** to the root. +*/ +static void vec1HeapBubbleUp(Vec1AnnHeap *p, int i){ + int idx = i; + while( idx>0 ){ + int idxParent = (idx-1)/2; + if( p->aRes[idxParent].fDist>p->aRes[idx].fDist ) break; + SWAP(Vec1AnnResult, p->aRes[idx], p->aRes[idxParent]); + idx = idxParent; + } +} + +/* +** A new element has just replaced the heap root node (i.e. the previous +** entry with the largest distance in the heap has been replaced by a +** smaller distance. This function repairs the heap by bubbling-down from +** the new root towards the leaves. +*/ +static void vec1HeapBubbleDown(Vec1AnnHeap *p){ + int idx = 0; + for(;;){ + int idxLeft = idx*2 + 1; + int idxRight = idxLeft + 1; + int idxMax = idx; + + if( idxLeftnMax && p->aRes[idxLeft].fDist>p->aRes[idxMax].fDist ){ + idxMax = idxLeft; + } + if( idxRightnMax && p->aRes[idxRight].fDist>p->aRes[idxMax].fDist ){ + idxMax = idxRight; + } + if( idxMax==idx ) break; + + SWAP(Vec1AnnResult, p->aRes[idx], p->aRes[idxMax]); + idx = idxMax; + } +} + +/* +** Add an entry to the heap structure. If the heap is already full, the +** entry with the largest fDist value seen so far is discarded. +*/ +static void vec1HeapInsert(Vec1AnnHeap *p, sqlite3_int64 iRowid, double fDist){ + if( p->nResnMax ){ + p->aRes[p->nRes].iRowid = iRowid; + p->aRes[p->nRes].fDist = fDist; + vec1HeapBubbleUp(p, (int)p->nRes); + p->nRes++; + }else if( p->bStreaming ){ + + if( p->nRes>=p->nAlloc ){ + i64 nNew = p->nAlloc * 2; + Vec1AnnResult *aNew = 0; + if( p->rc==SQLITE_OK ){ + i64 nByte = nNew*sizeof(Vec1AnnResult); + aNew = (Vec1AnnResult*)sqlite3_realloc64(p->aRes, nByte); + } + if( aNew==0 ){ + p->rc = SQLITE_NOMEM; + return; + } + p->aRes = aNew; + p->nAlloc = nNew; + } + + p->aRes[p->nRes].iRowid = iRowid; + p->aRes[p->nRes].fDist = fDist; + if( fDistaRes[0].fDist ){ + SWAP(Vec1AnnResult, p->aRes[0], p->aRes[p->nRes]); + vec1HeapBubbleDown(p); + } + p->nRes++; + }else if( fDistaRes[0].fDist ){ + p->aRes[0].iRowid = iRowid; + p->aRes[0].fDist = fDist; + vec1HeapBubbleDown(p); + p->fMin = p->aRes[0].fDist; + } +} + + +/* +** Ensure there is room in the buffer for another nByte bytes of data. That +** is, make sure the allocated size is at least (pBuf->n + nByte). +** +** Return SQLITE_OK if successful, or SQLITE_NOMEM if an OOM error is +** encountered. +*/ +static int vec1BufferGrow(Vec1Buffer *pBuf, sqlite3_int64 nByte){ + if( pBuf->n+nByte>pBuf->nAlloc ){ + sqlite3_int64 nNew = pBuf->nAlloc; + unsigned char *aNew = 0; + + if( nNew==0 ) nNew = nByte + 100; + while( nNew < (pBuf->n + nByte) ){ + nNew = nNew * 2; + } + aNew = sqlite3_realloc64(pBuf->a, nNew); + if( aNew==0 ){ + return SQLITE_NOMEM; + } + pBuf->a = aNew; + pBuf->nAlloc = (int)nNew; + } + + return SQLITE_OK; +} + +/* +** Ensure that the buffer passed as the first argument has at least nByte +** bytes of space allocated. +*/ +static int vec1BufferSize(Vec1Buffer *pBuf, sqlite3_int64 nByte){ + if( nByte>pBuf->nAlloc ){ + u8 *aNew = sqlite3_realloc64(pBuf->a, nByte); + if( aNew==0 ){ + return SQLITE_NOMEM; + } + pBuf->a = aNew; + pBuf->nAlloc = (int)nByte; + } + return SQLITE_OK; +} + +/* +** Free the allocation and zero the buffer structure. +*/ +static void vec1BufferFree(Vec1Buffer *pBuf){ + sqlite3_free(pBuf->a); + memset(pBuf, 0, sizeof(Vec1Buffer)); +} + + +/* +** Set the error message in context object pCtx to the result of formatting +** zFmt printf() style. +*/ +static void vec1ResultErrorF( + sqlite3_context *pCtx, /* Context to set error in */ + const char *zFmt, /* printf() style format for error message */ + ... /* Trailing arguments for printf() */ +){ + char *zMsg = 0; + va_list ap; + va_start(ap, zFmt); + zMsg = sqlite3_vmprintf(zFmt, ap); + va_end(ap); + sqlite3_result_error(pCtx, zMsg, -1); + sqlite3_free(zMsg); +} + +/* +** Attempt to allocate and zero a block of nByte bytes of memory. If +** succesful, return a pointer to the new allocation. It is the responsibility +** of the caller to eventually free the allocation using sqlite3_free(). +** +** If the allocation attempt fails, return 0 and leave an OOM error in +** context object pCtx. +*/ +static void *vec1ContextMalloc(sqlite3_context *pCtx, sqlite3_int64 nByte){ + void *pRet = sqlite3_malloc64(nByte); + if( pRet==0 ){ + sqlite3_result_error_nomem(pCtx); + }else{ + memset(pRet, 0, nByte); + } + return pRet; +} + +/* +** Implementation of scalar function vec1_info(). +*/ +static void vec1InfoFunc( + sqlite3_context *pCtx, + int nVal, + sqlite3_value **aVal +){ + char *z = 0; + const char *zSimd = 0; + const char *zThreads = 0; + + UNUSED_PARAMETER2(nVal, aVal); + +#if defined(VEC1_HAVE_AVX2) + zSimd = "AVX2"; +#elif defined(VEC1_HAVE_NEON) + zSimd = "NEON"; +#else + zSimd = "Scalar"; +#endif + +#if VEC1_THREADS + zThreads = "multi-threaded"; +#else + zThreads = "single-threaded"; +#endif + + z = sqlite3_mprintf("version %s (%s, %s)", VEC1_VERSION, zSimd, zThreads); + + if( z==0 ){ + sqlite3_result_error_nomem(pCtx); + }else{ + sqlite3_result_text(pCtx, z, -1, vec1SqliteFree); + } +} + + +/* +** Return true if character c counts as whitespace. This is used +** by vec1_from_json(). +*/ +static int vec1_isspace(char c){ + return (c==' '); +} + +/* +** Implementation of scalar function vec1_from_json(). Converts a json array +** of numbers into a vector in vec1 native format (32-bit little-endian +** floats). +*/ +static void vec1FromJsonFunc( + sqlite3_context *pCtx, + int nVal, + sqlite3_value **aVal +){ + const char *zJson = (const char*)sqlite3_value_text(aVal[0]); + int nJson = sqlite3_value_bytes(aVal[0]); + const char *p = zJson; + + assert( nVal==1 ); + UNUSED_PARAMETER(nVal); + + float *aVec = 0; + int nVec = 0; + + /* vec1_from_json(NULL) returns NULL */ + if( sqlite3_value_type(aVal[0])==SQLITE_NULL ) return; + + /* Allocate space for the return value */ + aVec = (float*)sqlite3_malloc(sizeof_f32*(nJson/2)); + if( !aVec ){ + sqlite3_result_error_nomem(pCtx); + return; + } + + while( vec1_isspace(*p) ) p++; + if( *p!='[' ) goto parse_failed; + p++; + + while( 1 ){ + char *p2 = 0; + double rVal = 0.0; + + while( vec1_isspace(*p) ) p++; + rVal = strtod(p, &p2); + if( p==p2 ) goto parse_failed; + + p = p2; + aVec[nVec] = (float)rVal; + nVec++; + + while( vec1_isspace(*p) ) p++; + if( *p==']' ){ + p++; + break; + } + if( *p!=',' ) goto parse_failed; + p++; + } + + while( vec1_isspace(*p) ) p++; + if( *p ) goto parse_failed; + + sqlite3_result_blob(pCtx, (void*)aVec, nVec*sizeof_f32, vec1SqliteFree); + return; + + parse_failed: + sqlite3_free(aVec); + sqlite3_result_error(pCtx, "error parsing json vector", -1); +} + +/* +** SQL Scalar Functions: +** +** vec1_to_json(BLOB) +** vec1_to_json_f(BLOB) +** vec1_to_json_i(BLOB) +*/ +static void vec1ToJsonGeneric( + sqlite3_context *pCtx, + int eType, + int nBytePerElem, + sqlite3_value *pVal +){ + int ii; + sqlite3 *db = sqlite3_context_db_handle(pCtx); + sqlite3_str *pStr = 0; + + int n = sqlite3_value_bytes(pVal); + const void *a = sqlite3_value_blob(pVal); + + if( n==0 || (n%nBytePerElem)!=0 ){ + sqlite3_result_error(pCtx, "vec1: invalid vector argument", -1); + return; + } + n = n / nBytePerElem; + + pStr = sqlite3_str_new(db); + if( eType==VEC1_TYPE_FLOAT32 ){ + const float *aFloat = (const float*)a; + sqlite3_str_appendf(pStr, "[%.9g", aFloat[0]); + for(ii=1; ii> 56) & 0xFF; + aBuf[1] = (val >> 48) & 0xFF; + aBuf[2] = (val >> 40) & 0xFF; + aBuf[3] = (val >> 32) & 0xFF; + aBuf[4] = (val >> 24) & 0xFF; + aBuf[5] = (val >> 16) & 0xFF; + aBuf[6] = (val >> 8) & 0xFF; + aBuf[7] = (val >> 0) & 0xFF; +} + +/* +** Read and return a 64-bit unsigned big-endian integer from buffer aBuf[]. +*/ +static u64 vec1GetU64(const u8 *aBuf){ + return (((u64)aBuf[0]) << 56) + + (((u64)aBuf[1]) << 48) + + (((u64)aBuf[2]) << 40) + + (((u64)aBuf[3]) << 32) + + (((u64)aBuf[4]) << 24) + + (((u64)aBuf[5]) << 16) + + (((u64)aBuf[6]) << 8) + + (((u64)aBuf[7]) << 0); +} + +/* +** Subtract a2 from a1 and leave the result in aOut. i.e. +** +** aOut = a1 - a2 +*/ +static void vec1Sub(float *aOut, const float *a1, const float *a2, int n){ + int i; + for(i=0; i + +/* +** Unix version of thread object. +*/ +struct Vec1Thread { + pthread_t tid; +}; + +/* +** Unix version of mutex object. +*/ +struct Vec1Mutex { + pthread_mutex_t mutex; +}; + +/* +** Unix version of condition variable. +*/ +struct Vec1Cond { + pthread_cond_t cond; +}; + +/* +** Create a new thread running xTask(pIn). If successful, set *ppThread +** to the new thread handle and return SQLITE_OK. Otherwise, if an +** error occurs, set *ppThread to 0 and return either SQLITE_ERROR +** or SQLITE_NOMEM as appropriate. +*/ +static int vec1ThreadCreate( + Vec1Thread **ppThread, + void *(*xTask)(void*), + void *pIn +){ + Vec1Thread *p = sqlite3_malloc(sizeof(Vec1Thread)); + if( p==0 ){ *ppThread = 0; return SQLITE_NOMEM; } + if( pthread_create(&p->tid, 0, xTask, pIn)!=0 ){ + sqlite3_free(p); + *ppThread = 0; + return SQLITE_ERROR; + } + *ppThread = p; + return SQLITE_OK; +} + +/* +** Join the thread pThread. If successful set *ppOut to the value +** returned by the thread's xTask routine and return SQLITE_OK. +** Otherwise, return SQLITE_ERROR and set *ppOut to 0. +** +** The thread handle may not be used after this function has been +** called. The results of doing so are undefined. +*/ +static int vec1ThreadJoin(Vec1Thread *pThread, void **ppOut){ + int rc = SQLITE_OK; + if( pthread_join(pThread->tid, ppOut)!=0 ){ + rc = SQLITE_ERROR; + if( ppOut ) *ppOut = 0; + } + sqlite3_free(pThread); + return rc; +} + +/* +** Create a new mutex. If successful, set *ppMutex to the new +** mutex handle and return SQLITE_OK. Otherwise, set *ppMutex to 0 +** and return an SQLite error code - SQLITE_NOMEM or SQLITE_ERROR as +** appropriate. +*/ +static int vec1MutexCreate(Vec1Mutex **ppMutex){ + Vec1Mutex *p = sqlite3_malloc(sizeof(Vec1Mutex)); + if( p==0 ){ *ppMutex = 0; return SQLITE_NOMEM; } + if( pthread_mutex_init(&p->mutex, 0)!=0 ){ + sqlite3_free(p); + *ppMutex = 0; + return SQLITE_ERROR; + } + *ppMutex = p; + return SQLITE_OK; +} + + +/* +** Destroy (free) the mutex passed as the only argument. +*/ +static void vec1MutexDestroy(Vec1Mutex *pMutex){ + pthread_mutex_destroy(&pMutex->mutex); + sqlite3_free(pMutex); +} + + +/* +** Enter the mutex passed as the only argument. The results of calling +** this function on a mutex that is already held are undefined. +*/ +static void vec1MutexEnter(Vec1Mutex *pMutex){ + pthread_mutex_lock(&pMutex->mutex); +} + + +/* +** Leave the mutex passed as the only argument. +*/ +static void vec1MutexLeave(Vec1Mutex *pMutex){ + pthread_mutex_unlock(&pMutex->mutex); +} + + +/* +** Create a new posix style condition variable. +*/ +static int vec1CondCreate(Vec1Cond **ppCond){ + Vec1Cond *p = sqlite3_malloc(sizeof(Vec1Cond)); + if( p==0 ){ *ppCond = 0; return SQLITE_NOMEM; } + if( pthread_cond_init(&p->cond, 0)!=0 ){ + sqlite3_free(p); + *ppCond = 0; + return SQLITE_ERROR; + } + *ppCond = p; + return SQLITE_OK; +} + + +/* +** Destroy the condition variable passed as the only argument. +*/ +static void vec1CondDestroy(Vec1Cond *pCond){ + pthread_cond_destroy(&pCond->cond); + sqlite3_free(pCond); +} + +/* +** Mutex pMutex must be held when this is called. It causes the +** thread to atomically relinquish the mutex and wait on the +** condition variable. When the condition is signaled by another +** thread calling vec1CondBroadcast(), this thread will reaquire +** mutex pMutex and continue. +*/ +static void vec1CondWait(Vec1Mutex *pMutex, Vec1Cond *pCond){ + pthread_cond_wait(&pCond->cond, &pMutex->mutex); +} + +/* +** Wake up threads waiting on the condition variable passed as +** the only argument. +*/ +static void vec1CondBroadcast(Vec1Cond *pCond){ + pthread_cond_broadcast(&pCond->cond); +} + + +#endif /* SQLITE_OS_UNIX && defined(SQLITE_MUTEX_PTHREADS) */ +/******************************** End Unix Pthreads *************************/ + + +/********************************* Win32 Threads ****************************/ +#if defined(VEC1_THREADS_WINDOWS) + +#include + +struct Vec1Thread { + HANDLE hThread; + void *(*xTask)(void*); + void *pIn; + void *pOut; +}; + +struct Vec1Mutex { + CRITICAL_SECTION cs; +}; + +struct Vec1Cond { + CONDITION_VARIABLE cv; +}; + +/* Windows thread trampoline - WINAPI calling convention */ +static DWORD WINAPI vec1ThreadMain(LPVOID pArg){ + Vec1Thread *p = (Vec1Thread*)pArg; + p->pOut = p->xTask(p->pIn); + return 0; +} + +static int vec1ThreadCreate( + Vec1Thread **ppThread, + void *(*xTask)(void*), + void *pIn +){ + Vec1Thread *p = sqlite3_malloc(sizeof(Vec1Thread)); + if( p==0 ){ *ppThread = 0; return SQLITE_NOMEM; } + p->xTask = xTask; + p->pIn = pIn; + p->pOut = 0; + p->hThread = CreateThread(NULL, 0, vec1ThreadMain, p, 0, NULL); + if( p->hThread==NULL ){ + sqlite3_free(p); + *ppThread = 0; + return SQLITE_ERROR; + } + *ppThread = p; + return SQLITE_OK; +} + +static int vec1ThreadJoin(Vec1Thread *pThread, void **ppOut){ + int rc = SQLITE_OK; + if( WaitForSingleObject(pThread->hThread, INFINITE)!=WAIT_OBJECT_0 ){ + rc = SQLITE_ERROR; + if( ppOut ) *ppOut = 0; + }else{ + if( ppOut ) *ppOut = pThread->pOut; + } + CloseHandle(pThread->hThread); + sqlite3_free(pThread); + return rc; +} + +static int vec1MutexCreate(Vec1Mutex **ppMutex){ + Vec1Mutex *p = sqlite3_malloc(sizeof(Vec1Mutex)); + if( p==0 ){ *ppMutex = 0; return SQLITE_NOMEM; } + InitializeCriticalSection(&p->cs); + *ppMutex = p; + return SQLITE_OK; +} + +static void vec1MutexDestroy(Vec1Mutex *pMutex){ + DeleteCriticalSection(&pMutex->cs); + sqlite3_free(pMutex); +} + +static void vec1MutexEnter(Vec1Mutex *pMutex){ + EnterCriticalSection(&pMutex->cs); +} + +static void vec1MutexLeave(Vec1Mutex *pMutex){ + LeaveCriticalSection(&pMutex->cs); +} + +static int vec1CondCreate(Vec1Cond **ppCond){ + Vec1Cond *p = sqlite3_malloc(sizeof(Vec1Cond)); + if( p==0 ){ *ppCond = 0; return SQLITE_NOMEM; } + InitializeConditionVariable(&p->cv); + *ppCond = p; + return SQLITE_OK; +} + +static void vec1CondDestroy(Vec1Cond *pCond){ + /* Windows condition variables have no destroy function */ + sqlite3_free(pCond); +} + +static void vec1CondWait(Vec1Mutex *pMutex, Vec1Cond *pCond){ + SleepConditionVariableCS(&pCond->cv, &pMutex->cs, INFINITE); +} + +static void vec1CondBroadcast(Vec1Cond *pCond){ + WakeAllConditionVariable(&pCond->cv); +} + + + +#endif /* SQLITE_OS_WIN_THREADS */ +/******************************** End Win32 Threads *************************/ + +/* +***************************************************************************** +*****************************************************************************/ + +typedef struct Vec1JobQueue Vec1JobQueue; + +#if VEC1_THREADS + +typedef struct Vec1Job Vec1Job; + +struct Vec1Job { + void *pCtx; + void (*xWork)(void *pCtx); + int (*xFinish)(void *pCtx, int); + Vec1Job *pJobNext; +}; + +/* +** nJob is the number of jobs on either the pWaiting or pFinished list, +** or else being worked on by a thread. nJob is incremented when the +** job is added to pWaiting, decremented when it is removed from pFinished. +** Always by the main thread. +*/ +struct Vec1JobQueue { + Vec1Mutex *pMutex; + Vec1Cond *pCond1; + Vec1Cond *pCond2; + Vec1Thread **apWorker; + int nWorker; /* Number of worker threads */ + int bDone; /* True if threads should exit */ + int nJob; /* Total number of unfinished jobs */ + + Vec1Job *pWaiting; /* List of jobs waiting for xWork() */ + Vec1Job *pFinished; /* List of jobs waiting for xFinish() */ + Vec1Job *pFree; /* List of spare Vec1Job structures */ +}; + +static void *vec1JobWorkerMain(void *pCtx){ + Vec1JobQueue *p = (Vec1JobQueue*)pCtx; + + while( 1 ){ + Vec1Job *pJob = 0; + + /* Try to find a new job to work on. */ + vec1MutexEnter(p->pMutex); + while( p->pWaiting==0 && p->bDone==0 ){ + vec1CondWait(p->pMutex, p->pCond1); + } + pJob = p->pWaiting; + if( pJob ) p->pWaiting = pJob->pJobNext; + vec1MutexLeave(p->pMutex); + + /* If pJob is 0, we're done. Thread should exit. */ + if( pJob==0 ) break; + + /* Do the work for this job. */ + pJob->xWork(pJob->pCtx); + + /* Put the job on the finished list. If it is currently empty, the + ** main thread may be waiting. Signal it in that case. */ + vec1MutexEnter(p->pMutex); + if( p->pFinished==0 ) vec1CondBroadcast(p->pCond2); + pJob->pJobNext = p->pFinished; + p->pFinished = pJob; + vec1MutexLeave(p->pMutex); + } + + return 0; +} + +static int vec1JobQueueFinishJobs(Vec1JobQueue *pQueue, int rcin){ + int rc = rcin; + + if( pQueue==0 ) return rc; + + while( 1 ){ + Vec1Job *pFin = 0; + Vec1Job *pWork = 0; + + /* Try to find a job to help with. If there is nothing, wait on pCond2. */ + vec1MutexEnter(pQueue->pMutex); + while( pQueue->pFinished==0 && pQueue->pWaiting==0 && pQueue->nJob>0 ){ + vec1CondWait(pQueue->pMutex, pQueue->pCond2); + } + if( pQueue->pFinished ){ + pFin = pQueue->pFinished; + pQueue->pFinished = pFin->pJobNext; + pQueue->nJob--; + }else if( pQueue->pWaiting ){ + pWork = pQueue->pWaiting; + pQueue->pWaiting = pWork->pJobNext; + pQueue->nJob--; + } + vec1MutexLeave(pQueue->pMutex); + + if( pFin==0 && pWork==0 ) break; + if( pWork ){ + pWork->xWork(pWork->pCtx); + pFin = pWork; + } + + { + int rc2 = pFin->xFinish(pFin->pCtx, rc); + if( rc==SQLITE_OK ) rc = rc2; + } + + vec1MutexEnter(pQueue->pMutex); + pFin->pJobNext = pQueue->pFree; + pQueue->pFree = pFin; + vec1MutexLeave(pQueue->pMutex); + } + + return rc; +} + + +static void vec1JobQueueFree(Vec1JobQueue *pQueue){ + if( pQueue ){ + if( pQueue->pMutex && pQueue->pCond1 ){ + int ii; + vec1JobQueueFinishJobs(pQueue, SQLITE_ERROR); + + vec1MutexEnter(pQueue->pMutex); + pQueue->bDone = 1; + vec1CondBroadcast(pQueue->pCond1); + vec1MutexLeave(pQueue->pMutex); + for(ii=0; iinWorker; ii++){ + void *dummy = 0; + vec1ThreadJoin(pQueue->apWorker[ii], &dummy); + } + } + + if( pQueue->pMutex ) vec1MutexDestroy(pQueue->pMutex); + if( pQueue->pCond1 ) vec1CondDestroy(pQueue->pCond1); + if( pQueue->pCond2 ) vec1CondDestroy(pQueue->pCond2); + + assert( pQueue->pWaiting==0 ); + assert( pQueue->pFinished==0 ); + assert( pQueue->pFree!=0 ); + sqlite3_free(pQueue); + } +} + + +/* +** Allocate a new job-queue. +*/ +static Vec1JobQueue *vec1JobQueueNew(int nWorker){ + int rc = SQLITE_OK; + Vec1JobQueue *pNew = 0; + int nJob = nWorker + 8; + + int nByte = + sizeof(Vec1JobQueue) + + sizeof(Vec1Thread*)*nWorker + + sizeof(Vec1Job) * nJob; + + assert( nWorker>=0 ); + pNew = (Vec1JobQueue*)vec1MallocZero(nByte); + if( pNew==0 ){ + rc = SQLITE_NOMEM; + }else{ + int ii; + Vec1Job *pJob; + + pNew->apWorker = (Vec1Thread**)&pNew[1]; + pJob = (Vec1Job*)&pNew->apWorker[nWorker]; + for(ii=0; iipJobNext = pNew->pFree; + pNew->pFree = pJob; + pJob++; + } + + rc = vec1MutexCreate(&pNew->pMutex); + if( rc==SQLITE_OK ) rc = vec1CondCreate(&pNew->pCond1); + if( rc==SQLITE_OK ) rc = vec1CondCreate(&pNew->pCond2); + for(ii=0; rc==SQLITE_OK && iiapWorker[ii], vec1JobWorkerMain, pNew); + if( rc==SQLITE_OK ){ + pNew->nWorker++; + } + } + } + + if( rc!=SQLITE_OK ){ + vec1JobQueueFree(pNew); + pNew = 0; + } + + return pNew; +} + +static int vec1DummyFinal(void *pCtx, int rcin){ + UNUSED_PARAMETER(pCtx); + return rcin; +} + +static int vec1JobQueueAddJob( + Vec1JobQueue *pQueue, + void (*xWork)(void *pCtx), + int (*xFinish)(void *pCtx, int), + void *pCtx +){ + Vec1Job *pFree = 0; + Vec1Job *pFinish = 0; + Vec1Job *pWork = 0; + int rc = SQLITE_OK; + + if( xFinish==0 ) xFinish = vec1DummyFinal; + if( pQueue==0 ){ + xWork(pCtx); + return xFinish(pCtx, SQLITE_OK); + } + + /* Check if this thread needs to do any xFinish() or xWork() calls. And + ** try to get a free Vec1Job object at the same time. */ + vec1MutexEnter(pQueue->pMutex); + + assert( pQueue->pWaiting || pQueue->pFree || pQueue->pFinished ); + if( pQueue->pFree ){ + pFree = pQueue->pFree; + pQueue->pFree = pFree->pJobNext; + }else if( pQueue->pFinished ){ + pFinish = pQueue->pFinished; + pQueue->pFinished = pFinish->pJobNext; + pQueue->nJob--; + }else{ + pWork = pQueue->pWaiting; + pQueue->pWaiting = pWork->pJobNext; + pQueue->nJob--; + } + + vec1MutexLeave(pQueue->pMutex); + + /* At most 1 of pFree, pFinish and pWork may be non NULL */ + assert( (pFree!=0)+(pFinish!=0)+(pWork!=0)<=1 ); + + if( pWork ){ + pWork->xWork(pWork->pCtx); + pFinish = pWork; + } + if( pFinish ){ + rc = pFinish->xFinish(pFinish->pCtx, SQLITE_OK); + pFree = pFinish; + } + assert( pFree ); + + pFree->pCtx = pCtx; + pFree->xWork = xWork; + pFree->xFinish = xFinish; + vec1MutexEnter(pQueue->pMutex); + if( pQueue->pWaiting==0 ) vec1CondBroadcast(pQueue->pCond1); + pFree->pJobNext = pQueue->pWaiting; + pQueue->pWaiting = pFree; + pQueue->nJob++; + vec1MutexLeave(pQueue->pMutex); + + return rc; +} + +#else /* VEC1_THREADS=0 */ + +/* +** Dummy implementation of job-queue for single-threaded builds. +*/ +static int vec1JobQueueFinishJobs(Vec1JobQueue *pQueue, int rcin){ + return rcin; +} +static void vec1JobQueueFree(Vec1JobQueue *pQueue){} +static Vec1JobQueue *vec1JobQueueNew(int a){ return 0; } +static int vec1JobQueueAddJob( + Vec1JobQueue *pQueue, + void (*xWork)(void *pCtx), + int (*xFinish)(void *pCtx, int), + void *pCtx +){ + xWork(pCtx); + return xFinish ? xFinish(pCtx, SQLITE_OK) : SQLITE_OK; +} + +#endif + +/* +** Minimum and maximum values allowed for the 'blocksize' command. +*/ +#define VEC1_BLOCKSIZE_MIN 1024 +#define VEC1_BLOCKSIZE_MAX (16*1024*1024) + +#define VEC1_BLOCKSIZE_DEFAULT (1024*1024) +#define VEC1_BLOCKSIZE_MIN_DEFAULT (16*1024) + +#define VEC1_VECSIZE_MIN 2 +#define VEC1_VECSIZE_MAX 4096 + +/* +** Number of codes in each codebook. This is always 256. +*/ +#define VEC1_PQ_CODEBOOK_SZ 256 + +#define VEC1_MIN_CODESIZE 1 +#define VEC1_MAX_CODESIZE 256 + +#define VEC1_MIN_BUCKET 0 +#define VEC1_MAX_BUCKET 65536 + +#define VEC1_MIN_NOPQ_ROUND 1 +#define VEC1_MAX_NOPQ_ROUND 32 + +#define VEC1_PQ_KMEANS_NITER 30 +#define VEC1_PQ_KMEANS_STOPVAL 0.999 + +/* +** Minimum number of vectors required for PQ training. +*/ +#define VEC1_PQ_TRAINING_SET_MIN (VEC1_PQ_CODEBOOK_SZ * 2) + +#define VEC1_TRAINING_TOTAL 0 +#define VEC1_TRAINING_ROT_CODEBOOKS 1 +#define VEC1_TRAINING_ROT_COVARIANCE 2 +#define VEC1_TRAINING_ROT_JACOBI 3 +#define VEC1_TRAINING_ROT_VECTORS 4 + +#define VEC1_TRAINING_COARSE_INIT 5 +#define VEC1_TRAINING_COARSE_KMEANS 6 + +#define VEC1_TRAINING_RESIDUALS 7 +#define VEC1_TRAINING_CODEBOOKS 8 + +#define VEC1_TRAINING_NTIMER 9 + + +/* +** When accumulating training data, allocate space in chunks of +** VEC1_TRAINING_SZCHUNK. Allow at most VEC1_TRAINING_MAXCHUNK chunks of +** training data. With default values, the limit is 64GiB of training +** vectors. +*/ +#define VEC1_TRAINING_MAXCHUNK 1024 +#define VEC1_TRAINING_SZCHUNK (32*1024*1024) + +/* +** Values for Vec1TrainCtx.eQuant +*/ +#define VEC1_QUANTIZE_NONE 0 +#define VEC1_QUANTIZE_PQ 1 +#define VEC1_QUANTIZE_OPQ 2 +#define VEC1_QUANTIZE_BQ 3 + +/* +** Training vectors collected by xStep() are stored in an instance +** of this structure. +*/ +typedef struct Vec1TrainVectors Vec1TrainVectors; +struct Vec1TrainVectors { + int nElem; /* Number of elements in each vector */ + int nVec; /* Number of vectors in chunk array */ + int nChunk; /* Number of chunks allocated so far */ + int nVecPerChunk; /* Number of vectors in each chunk */ + float *aChunk[VEC1_TRAINING_MAXCHUNK]; +}; + +/* +** Context object for the vec1_train() aggregate function. +** +** eQuant: +** A VEC1_QUANTIZE_XYZ value. Set by the "quantizer" option. +** +** nCodesize: +** For PQ or OPQ, the number of vector sub-spaces. For BQ, the number of +** bytes required to store the bit-vector. For 1 bit per dimension this +** value is set to ((d+7)/8). +** +** nBucket: +** Number of IVF buckets used by coarse quantizer. Set to 0 to disable +** coarse quant altogether. +*/ +typedef struct Vec1TrainCtx Vec1TrainCtx; +struct Vec1TrainCtx { + /* Training vectors collected by xStep() */ + Vec1TrainVectors tv; + + /* Training parameters configured by json argument */ + int eDistance; /* VEC1_DISTANCE_L2 or COS */ + int eQuant; /* VEC1_QUANTIZE_XX value */ + int nCodesize; /* Number of vector sub-spaces */ + int nBucket; /* Number of IVF buckets */ + + int bOpq; + + int bResidual; + int bSvdVerify; + int bWHT; /* Use WHT transformation */ + int nThread; /* Number of CPU threads to use */ + int nOpqRound; /* Number of OPQ rounds */ + char *zLogFunction; /* Name of user SQL function for logging */ + char *zProfileFunction; /* Name of user SQL function for profile */ + sqlite3_stmt *pLogStmt; /* SELECT zLogFunction(?, ?) */ + + sqlite3_context *pCtx; /* Context object for errors */ + int rc; /* Error code for training operation */ + + /* Used for calculating "percent" value passed to "SELECT zLogFunction()" */ + int nTotalWork; + int nCompletedWork; + + /* Calculated based on training parameters + size of first vector */ + int nCodeElem; /* Number of elements in codebook vectors */ + + u64 aTime[VEC1_TRAINING_NTIMER]; +}; + +#define VEC1_TRAINING_WORK_OPQ 2 +#define VEC1_TRAINING_WORK_COARSE1 1 +#define VEC1_TRAINING_WORK_COARSE2 1 +#define VEC1_TRAINING_WORK_CODEBOOKS 1 + +/* +** Return a pointer to the requested training vector. The results are +** undefined if parameter iVec is out of range. +*/ +static float *vec1TrainingVector(Vec1TrainVectors *p, int iVec){ + int iChunk = iVec / p->nVecPerChunk; + int iOff = (iVec % p->nVecPerChunk) * p->nElem; + return &((p->aChunk[iChunk])[iOff]); +} + +/* +** aSub[] is a vector of nCodeElem elements. aCodebook[] is a codebook of +** nCode vectors, each of nCodeElem elements. Return the index of the best +** match for aSub in aCodebook[]. +*/ +static int vec1PqBestMatch( + const float *aCodebook, /* Codebook of nCodeElem element vectors */ + int nCode, /* Number of vectors in aCodebook */ + const float *aSub, /* nCodeElem element vector */ + int nCodeElem, /* Size of vectors in elements */ + double *pfBest /* Distance to best match */ +){ + double fBestDist = INFINITY; + int iBest = 0; + int ii; + + for(ii=0; ii=0 && iBestaVec, p->nElem, i) + +typedef struct LloydsJob LloydsJob; +struct LloydsJob { + /* Data to train on */ + Vec1TrainVectors *pVec; + int iFirst; /* First vector for this job */ + int iEof; /* Index of last vector +1 for this job */ + + int nK; /* Number of centroids */ + const float *aCentroid; /* Centroids */ +#ifdef VEC1_USE_TRANSPOSITION + float *aTrans; /* Transposed centroids */ +#endif + + /* Outputs */ + double fTotal; + float *aSum; + int *aCount; +}; + +/* +** Context object for simple PRNG. See functions vec1RandomInit() and +** vec1RandomNext() for details. +*/ +typedef struct Vec1Random Vec1Random; +struct Vec1Random { + u64 state; + u64 inc; +}; + +static u32 vec1RandomNext(Vec1Random *p) { + u32 xorshifted, rot; + u64 old = p->state; + p->state = old * 6364136223846793005ULL + p->inc; + xorshifted = (u32)(((old >> 18u) ^ old) >> 27u); + rot = (u32)(old >> 59u); + return ((xorshifted >> rot) | (xorshifted << ((-rot) & 31))) & 0x7FFFFFFF; +} + +static void vec1RandomInit(Vec1Random *p, u64 seed, u64 id){ + p->state = 0; + p->inc = (id << 1u) | 1u; + vec1RandomNext(p); + p->state += seed; + vec1RandomNext(p); +} + + +typedef struct KMeansInitJob KMeansInitJob; +struct KMeansInitJob { + int iFirst; /* First vector for this job */ + int iEof; /* Index of last vector +1 for this job */ + double fTotalMin; /* Total of aMin[iFirst..iEof-1] */ + + Vec1TrainVectors *pVec; + u32 *aWeight; + + float *aMin; + int *aBest; + + float *aCentroid; + int nCentroid; + int iFirstNew; +}; + +static void vec1KMeansInitWork(void *pCtx){ + KMeansInitJob *p = (KMeansInitJob*)pCtx; + const int nElem = p->pVec->nElem; + double fTotalMin = 0.0; + int ii; + + for(ii=p->iFirst; iiiEof; ii++){ + int jj; + float *vec = vec1TrainingVector(p->pVec, ii); + for(jj=p->iFirstNew; jjnCentroid; jj++){ + float *cent = &p->aCentroid[jj * nElem]; + float dist = vec1L2Dist(vec, cent, nElem); + if( jj==0 || distaMin[ii] ){ + p->aMin[ii] = dist; + if( p->aBest ) p->aBest[ii] = jj; + } + } + fTotalMin += ((double)p->aMin[ii] * (p->aWeight ? p->aWeight[ii] : 1)); + } + + p->fTotalMin = fTotalMin; +} + +/* +** Macro used to assign vectors to jobs in cases where an algorithm +** parallelizes across vectors. The pJob object must have at least the +** following member: +** +** struct XyzJob { +** ... +** Vec1TrainVectors *pVec; // Training vectors to use a subset of +** int iFirst; // index of first vector in pVec to use +** int iEof; // 1 past index of last vector in pVec to use +** ... +** }; +*/ +#define ASSIGN_VECTORS_TO_JOB(pV, pJob, iJob, nJob) \ + pJob->pVec = (pV); \ + pJob->iFirst = (iJob==0) ? 0 : pJob[-1].iEof; \ + pJob->iEof = pJob->iFirst + ((pV)->nVec - pJob->iFirst) / (nJob - iJob); \ + assert( pJob->iEof<=(pV)->nVec ); \ + assert( (iJob==nJob-1)==(pJob->iEof==(pV)->nVec) ); + +/* +** +*/ +static int vec1KMeansInitOne( + Vec1TrainVectors *pVec, + u32 *aWeight, + Vec1JobQueue *pQueue, /* Job queue to use (NULL -> 1 thread) */ + int nK, /* Required number of outputs */ + int nPerRound, /* Number of centroids per round */ + int nTail, /* Number of tail rounds */ + float *aOut, /* Array to populate with centroids */ + u32 *aHist /* Array to populate with histogram */ +){ + const int nJob = (pQueue ? pQueue->nWorker + 1 : 1); + const int nElem = pVec->nElem; + const int nVByte = nElem * sizeof_f32; + + Vec1Random rnd; + + int nByte; /* Bytes of space to allocate */ + KMeansInitJob *aJob = 0; + float *aMin = 0; + int *aBest = 0; + int ii; /* Loop counter for per-job loop */ + int rr; /* Loop counter for per-round loop */ + int iNext; + int nCentroid = 0; + int nNewCentroid = 0; + float *vec = 0; + + vec1RandomInit(&rnd, vec1Rand31(), 0); + + nByte = + (sizeof(KMeansInitJob) * nJob) + + (pVec->nVec * sizeof_f32) + + (aHist ? pVec->nVec * sizeof_u32 : 0); + aJob = vec1MallocZero(nByte); + if( aJob==0 ){ + return SQLITE_NOMEM; + } + aMin = (float*)&aJob[nJob]; + if( aHist ){ + aBest = (int*)&aMin[pVec->nVec]; + } + + for(ii=0, iNext=0; iiaMin = aMin; + pJob->aBest = aBest; + pJob->aWeight = aWeight; + pJob->aCentroid = aOut; + } + + /* First centroid is allocated by random selection. */ + vec = vec1TrainingVector(pVec, vec1RandomNext(&rnd)%pVec->nVec); + memcpy(aOut, vec, nVByte); + nCentroid = nNewCentroid = 1; + + for(rr=1; rrnVec-1; vv++){ + fTarget -= (aMin[vv] * (aWeight ? aWeight[vv] : 1)); + if( fTarget<=0.0 ) break; + } + memcpy(&aOut[nCentroid*nElem], vec1TrainingVector(pVec, vv), nVByte); + nCentroid++; + }else{ + /* KMeans || case */ + int cc; + vv = vec1RandomNext(&rnd) % pVec->nVec; + for(cc=0; ccnVec; + } + + memcpy(&aOut[nCentroid*nElem], vec1TrainingVector(pVec, vv), nVByte); + nCentroid++; + } + } + } + + if( aHist ){ + int vv; + for(vv=0; vvnVec; vv++){ + aHist[aBest[vv]]++; + } + } + + sqlite3_free(aJob); + return SQLITE_OK; +} + +/* +** Implementation of KMeans || initialization. +*/ +static int vec1KMeansParallel( + Vec1TrainVectors *pVec, + Vec1JobQueue *pQueue, /* Job queue to use (NULL -> 1 thread) */ + int nK, /* Required number of outputs */ + float *aOut /* Array to populate with centroids */ +){ + int rc = SQLITE_OK; + const int nCent = nK * 2; + const int nByte = nCent * (pVec->nElem * sizeof_f32 + sizeof_u32); + float *aCent; + + aCent = (float*)vec1MallocZero(nByte); + if( aCent==0 ){ + rc = SQLITE_NOMEM; + }else{ + u32 *aWeight = (u32*)&aCent[pVec->nElem * nCent]; + rc = vec1KMeansInitOne(pVec, 0, pQueue, nCent, nCent/10, 0, aCent,aWeight); + if( rc==SQLITE_OK ){ + Vec1TrainVectors vv; + memset(&vv, 0, sizeof(vv)); + vv.nElem = pVec->nElem; + vv.nVec = nCent; + vv.nVecPerChunk = nCent; + vv.nChunk = 1; + vv.aChunk[0] = aCent; + rc = vec1KMeansInitOne(&vv, aWeight, pQueue, nK, nK, 0, aOut, 0); + } + sqlite3_free(aCent); + } + + return rc; +} + +static int vec1KMeansPlusPlus( + Vec1TrainVectors *pVec, + Vec1JobQueue *pQueue, /* Job queue to use (NULL -> 1 thread) */ + int nK, /* Required number of outputs */ + float *aOut /* Array to populate with centroids */ +){ + const int nPerRound = 8; + const int nTailRound = 32; + return vec1KMeansInitOne(pVec, 0, pQueue, nK, nPerRound, nTailRound, aOut, 0); +} + +#if defined(_MSC_VER) +# define VEC1_NOINLINE __declspec(noinline) +#else +# define VEC1_NOINLINE __attribute__((noinline)) +#endif + +static VEC1_NOINLINE void vec1AddInPlaceScalar( + float * a1, + const float * a2, + int n +){ + int i; + for(i=0; ipVec; + int nElem = pVec->nElem; + int ii; + + p->fTotal = 0.0f; + memset(p->aSum, 0, sizeof_f32 * p->nK * nElem); + memset(p->aCount, 0, sizeof(int) * p->nK); + + /* Loop through those vectors that are our responsibility, + ** VEC1_MULTIMATCH_NVEC vectors at a time. Each iteration of this outer + ** loop will compare VEC1_MULTIMATCH_NVEC training vectors to all candidate + ** centroids, and update p->aCount[] and p->aSum[] with the results. */ + ii = p->iFirst; + for( ; ii<=(p->iEof-VEC1_MULTIMATCH_NVEC); ii+=VEC1_MULTIMATCH_NVEC){ + int vv; + int iCent; + + const float *aVec[VEC1_MULTIMATCH_NVEC]; + int aBest[VEC1_MULTIMATCH_NVEC]; + + /* Initialize the training vector array with the VEC1_MULTIMATCH_NVEC + ** training vectors that will be considered by this loop. */ + for(vv=0; vvaCentroid, p->aTrans, p->nK, aVec, aBest, &p->fTotal + ); + + /* Scalar array aBest[] now contains the index of the best centroid + ** for each of the VEC1_MULTIMATCH_NVEC training vectors being considered + ** by this loop. Update aCount[] and aSum[] avvordingly. */ + for(vv=0; vvaCount[iBest]++; + vec1AddInPlace(&p->aSum[iBest*nElem], aVec[vv], nElem); + } + +#if 0 + /* Check (assert) using vec1PqBestMatch() that the fancy code above + ** actually worked. */ + for(vv=0; vvaCentroid, p->nK, aVec[vv], nElem, 0 + ); + if( iBest2!=aBest[vv] ){ + const float *c1 = &p->aCentroid[aBest[vv] * nElem]; + const float *c2 = &p->aCentroid[iBest2 * nElem]; + float d1 = vec1L2Dist(aVec[vv], c1, nElem); + float d2 = vec1L2Dist(aVec[vv], c2, nElem); + assert( d1==d2 + || (d1>=d2 && ((d1 - d2) / d1) < 0.00001) + || (d2>d1 && ((d2 - d1) / d2) < 0.00001) + ); + } + } +#endif + } + + /* Scalar tail - handle training vectors that were not done above as + ** 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); + assert( iBest>=0 && iBestnK ); + p->aCount[iBest]++; + vec1AddInPlace(&p->aSum[iBest * nElem], aSub, nElem); + } +} + +/* +** Parameter aIn points to an array of centroids containing nK vectors, +** each nElem elements in size. i.e. so that element j of vector i is +** stored in aIn[i*nElem + j]. Total size of the array is nK*nElem values. +** +** Parameter aOut[] also points to an array of nK*nElem elements. This +** function populates that array with the transposed version of the codebook +** before returning. +*/ +static void vec1CodebookTranspose( + const float *aIn, /* Centroid array in on-disk format */ + int nElem, /* Number of elements in each vectors */ + int nWidth, /* Transpose width */ + int nK, /* Number of centroids */ + float *aOut /* Populate this array with transformation */ +){ + int K, d; + float *pOut = aOut; + + /* If nK is not divisible by nWidth, just ignore any tail vectors. */ + assert( (nK % nWidth)==0 || nWidth==VEC1_MULTIMATCH_NCENT ); + nK = (nK / nWidth) * nWidth; + + for(K=0; KpLogStmt && p->rc==SQLITE_OK ){ + int percent = ((100*p->nCompletedWork) / p->nTotalWork); + char *zMsg = 0; + va_list ap; + va_start(ap, zFmt); + zMsg = sqlite3_vmprintf(zFmt, ap); + va_end(ap); + + if( zMsg==0 ){ + p->rc = SQLITE_NOMEM; + }else{ + sqlite3_bind_int(p->pLogStmt, 1, percent); + sqlite3_bind_text(p->pLogStmt, 2, zMsg, -1, SQLITE_TRANSIENT); + sqlite3_step(p->pLogStmt); + sqlite3_free(zMsg); + p->rc = sqlite3_reset(p->pLogStmt); + } + + if( p->rc==SQLITE_NOMEM ){ + sqlite3_result_error_nomem(p->pCtx); + }else if( p->rc!=SQLITE_OK ){ + sqlite3 *db = sqlite3_db_handle(p->pLogStmt); + vec1ResultErrorF(p->pCtx, "%s", sqlite3_errmsg(db)); + } + } +} + +#define START_TRAINING_TIMER(p, i) { p->aTime[i] -= vec1HardwareTimer(); } +#define END_TRAINING_TIMER(p, i) { p->aTime[i] += vec1HardwareTimer(); } + +static int vec1Ann1KMeans( + Vec1TrainCtx *p, /* Training data + parameters */ + Vec1TrainVectors *pVec, /* Vectors to train K-Means on */ + Vec1JobQueue *pQueue, /* Job-queue, or NULL for single thread */ + int nK, /* Value of K in K-means */ + float *aCode /* OUT: Populate this array */ +){ + i64 nByte = 0; + int ii; + int iIter; + int nElem = pVec->nElem; + int *pCsr = 0; + double fPrevTotal = 0.0; + + int *aCount = 0; + float *aSum = 0; + + int nVecPerJob = 0; + LloydsJob *aJob = 0; + int rc = SQLITE_OK; + int nThread = p ? p->nThread : 1; + + /* Allocate array of jobs to use for multi-threaded Lloyd's algorithm. + ** We use this even if there are no worker threads - in that case + ** allocate an array of 1. The algorithm parallelizes across vectors - each + ** jobs is assigned a subset of the vectors to work on. + ** + ** Each job requires the following working space: + ** + ** + aCount -> (sizeof(int) * K) bytes + ** + aSum -> (sizeof_f32 * nElem * K) bytes + */ + nByte = nThread * ( + sizeof(LloydsJob) + + nElem * nK * sizeof_f32 + /* aSum[] */ + nK * sizeof(int) /* aCount[] */ + ); + + aJob = (LloydsJob*)sqlite3_malloc64(nByte); + if( aJob==0 ) return SQLITE_NOMEM; + memset(aJob, 0, sizeof(LloydsJob) * nThread); + + pCsr = (int*)&aJob[nThread]; + + /* Initialize each job object in the array */ + for(ii=0; iinK = nK; + pJob->aCentroid = aCode; + pJob->aSum = (float*)pCsr; + pCsr += (nK * nElem); + pJob->aCount = pCsr; + pCsr += nK; + } + aCount = aJob[0].aCount; + aSum = aJob[0].aSum; + +#ifdef VEC1_USE_TRANSPOSITION + /* If this build uses transposed arrays of centroids, allocate space + ** for the array here, and give each job a pointer to it. */ + aJob[0].aTrans = (float*)sqlite3_malloc64( sizeof_f32 * (nElem * nK) ); + if( aJob[0].aTrans==0 ){ + rc = SQLITE_NOMEM; + goto vec1_kmeans_out; + } + for(ii=1; iinCompletedWork += VEC1_TRAINING_WORK_COARSE1; + } + + /* Initial centroids have now been chosen. So do K-means iteration to + ** improve the centroids. */ + if( p ){ + START_TRAINING_TIMER(p, VEC1_TRAINING_COARSE_KMEANS); + vec1TrainLog(p, "k-means iterations for coarse quant"); + } + for(iIter=0; iItereDistance==VEC1_DISTANCE_COS ){ + /* If this call is training a coarse quantizer for cosine distance, + ** use "spherical k-means". This means that instead of just updating + ** the centroid to be the average of all points assigned to it, we + ** do that and then normalize the result - so that the centroid + ** remains on the surface of the unit sphere. */ + memcpy(aCode, aSum, sizeof_f32 * nK * nElem); + for(ii=0; ii0 ){ + int iElem; + for(iElem=0; iElem0 && (fTotal / fPrevTotal)>=VEC1_PQ_KMEANS_STOPVAL ){ + iIter = VEC1_PQ_KMEANS_NITER; + } + fPrevTotal = fTotal; + } + if( p ){ + END_TRAINING_TIMER(p, VEC1_TRAINING_COARSE_KMEANS); + p->nCompletedWork += VEC1_TRAINING_WORK_COARSE2; + } + + vec1_kmeans_out: +#ifdef VEC1_USE_TRANSPOSITION + sqlite3_free( aJob[0].aTrans ); +#endif + sqlite3_free(aJob); + return rc; +} + +/* +** This function is called to train a coarse quantizer from within a +** vec1_train() invocation. Output array aCentroid[], which must be +** (p->tv.nElem * p->nBucket) elements in size, is packed with +** the p->nBucket centroids to use for coarse quantization before +** returning. +*/ +static void vec1TrainCoarseQuant( + Vec1TrainCtx *p, /* Training data + parameters */ + Vec1JobQueue *pQueue, /* Job-queue, or NULL for single thread */ + float *aCentroid /* OUT: Populate this array */ +){ + if( p->rc==SQLITE_OK ){ + int rc = vec1Ann1KMeans(p, &p->tv, pQueue, p->nBucket, aCentroid); + assert( rc==SQLITE_OK || rc==SQLITE_NOMEM ); + if( p->rc==SQLITE_OK && rc!=SQLITE_OK ){ + p->rc = rc; + sqlite3_result_error_nomem(p->pCtx); + } + } +} + +static int vec1ConfigInt( + int eType, + i64 iVal, /* Proposed parameter value */ + int nMin, /* Minimum allowed value */ + int nMax, /* Maximum allowed value */ + int bAllowZero, /* True if 0 is also allowed */ + const char *zName, /* Name of parameter */ + char **pzErr /* OUT: Error message */ +){ + int rc = SQLITE_OK; + if( eType!=SQLITE_INTEGER + || (!(bAllowZero && iVal==0) && (iValnMax)) + ){ + *pzErr = sqlite3_mprintf( + "vec1: %s must be set to an integer value between %d and %d%s", + zName, nMin, nMax, (bAllowZero ? ", or 0" : "") + ); + rc = SQLITE_ERROR; + } + return rc; +} + +static int vec1ConfigBool( + int *pbOut, + int eType, + i64 iVal, + const char *zName, + char **pzErr +){ + int rc = SQLITE_OK; + if( eType==SQLITE_INTEGER && (iVal==0 || iVal==1) ){ + *pbOut = (iVal!=0); + }else{ + *pzErr = sqlite3_mprintf("vec1: %s must be set to a boolean value", zName); + rc = SQLITE_ERROR; + } + return rc; +} + +static int vec1ConfigEnum( + int *piOpt, /* OUT: Index of selected value */ + const char *zVal, /* Candidate value */ + const char **azOpt, /* Array of options */ + const char *zName, /* Name of parameter */ + char **pzErr /* OUT: Error message */ +){ + int ii; + char *zErr = 0; + for(ii=0; azOpt[ii]; ii++){ + if( sqlite3_stricmp(zVal, azOpt[ii])==0 ){ + *piOpt = ii; + return SQLITE_OK; + } + } + + zErr = sqlite3_mprintf("unrecognized %s '%s', should be one of", zName, zVal); + for(ii=0; azOpt[ii]; ii++){ + if( azOpt[ii+1] ){ + zErr = sqlite3_mprintf("%z %s,", zErr, azOpt[ii]); + }else{ + zErr = sqlite3_mprintf("%z or %s", zErr, azOpt[ii]); + } + } + *pzErr = zErr; + return SQLITE_ERROR; +} + +static int vec1ConfigDistance( + int *peDist, /* OUT: VEC1_DISTANCE_L2 or DISTANCE_COS */ + const char *zVal, /* Candidate value */ + char **pzErr /* OUT: Error message */ +){ + const char *azDist[] = {"l2", "cos", 0}; + int iOpt = 0; + int rc = vec1ConfigEnum(&iOpt, zVal, azDist, "distance", pzErr); + assert( VEC1_DISTANCE_L2==1 ); + assert( VEC1_DISTANCE_COS==2 ); + *peDist = iOpt+1; + return rc; +} + +/* +** Parameter zVal points to a string that was the value for the "quantize" +** field of a training configuration object. If the string is a recognized +** value for this field, output parameter (*peQuant) is set to the +** corresponding VEC1_QUANTIZE_XXX value and SQLITE OK returned. +** +** Otherwise, if the value cannot be recognized, (*pzErr) is set to point +** to a buffer containing an English language error message and SQLITE_ERROR +** is returned. It is the responsibility of the caller to eventually free +** the (*pzErr) buffer using sqlite3_free(). The final value of (*peQuant) +** is undefined in this case. +*/ +static int vec1ConfigQuantizer( + int *peQuant, /* OUT: VEC1_QUANT_XXX value */ + const char *zVal, /* Candidate value */ + char **pzErr /* OUT: Error message */ +){ + const char *azQuant[] = {"none", "pq", "opq", "bq", 0}; + + assert( sqlite3_stricmp("none", azQuant[VEC1_QUANTIZE_NONE])==0 ); + assert( sqlite3_stricmp("pq", azQuant[VEC1_QUANTIZE_PQ])==0 ); + assert( sqlite3_stricmp("opq", azQuant[VEC1_QUANTIZE_OPQ])==0 ); + assert( sqlite3_stricmp("bq", azQuant[VEC1_QUANTIZE_BQ])==0 ); + + return vec1ConfigEnum(peQuant, zVal, azQuant, "quantizer", pzErr); +} + +static int vec1Ann1TrainCfg( + void *pCtx, + const char *zOpt, + int eType, + i64 iVal, + double fVal, + const char *zVal, + char **pz +){ + int rc = SQLITE_OK; + Vec1TrainCtx *p = (Vec1TrainCtx*)pCtx; + + const char *azOpt[] = { + "distance", /* 0 */ + "codesize", /* 1 */ + "nbucket", /* 2 */ + "quantizer", /* 3 */ + + "nthread", /* 4 */ + "svd_verify", /* 5 */ + "opq", /* 6 */ + "progress", /* 7 */ + "residual", /* 8 */ + "wht", /* 9 */ + "nopq_round", /* 10 */ + "profile", /* 11 */ + + "index", /* 12 */ + 0 + }; + int eOpt = -1; + + UNUSED_PARAMETER(fVal); + + rc = vec1ConfigEnum(&eOpt, zOpt, azOpt, "option", pz); + if( rc==SQLITE_OK ){ + switch( eOpt ){ + case 0: assert( sqlite3_stricmp("distance", zOpt)==0 ); + rc = vec1ConfigDistance(&p->eDistance, zVal, pz); + break; + + case 1: assert( sqlite3_stricmp("codesize", zOpt)==0 ); + rc = vec1ConfigInt(eType, iVal, + VEC1_MIN_CODESIZE, VEC1_MAX_CODESIZE, 1, zOpt, pz + ); + p->nCodesize = (int)iVal; + break; + + case 2: assert( sqlite3_stricmp("nbucket", zOpt)==0 ); + rc = vec1ConfigInt(eType, iVal, + VEC1_MIN_BUCKET, VEC1_MAX_BUCKET, 1, zOpt, pz + ); + p->nBucket = (int)iVal; + break; + + case 3: assert( sqlite3_stricmp("quantizer", zOpt)==0 ); + rc = vec1ConfigQuantizer(&p->eQuant, zVal, pz); + break; + + case 4: assert( sqlite3_stricmp("nthread", zOpt)==0 ); + rc = vec1ConfigInt(eType, iVal, 1, VEC1_MAX_NTHREAD, 0, zOpt, pz); +#if VEC1_THREADS + p->nThread = (int)iVal; +#endif + break; + + case 5: assert( sqlite3_stricmp("svd_verify", zOpt)==0 ); + rc = vec1ConfigBool(&p->bSvdVerify, eType, iVal, zOpt, pz); + break; + + case 6: assert( sqlite3_stricmp("opq", zOpt)==0 ); + rc = vec1ConfigBool(&p->bOpq, eType, iVal, zOpt, pz); + break; + + case 7: assert( sqlite3_stricmp("progress", zOpt)==0 ); + sqlite3_free(p->zLogFunction); + p->zLogFunction = sqlite3_mprintf("%s", zVal); + if( p->zLogFunction==0 ) rc = SQLITE_NOMEM; + break; + + case 8: assert( sqlite3_stricmp("residual", zOpt)==0 ); + rc = vec1ConfigBool(&p->bResidual, eType, iVal, zOpt, pz); + break; + + case 9: assert( sqlite3_stricmp("wht", zOpt)==0 ); + rc = vec1ConfigBool(&p->bWHT, eType, iVal, zOpt, pz); + break; + + case 10: assert( sqlite3_stricmp("nopq_round", zOpt)==0 ); + rc = vec1ConfigInt(eType, iVal, + VEC1_MIN_NOPQ_ROUND, VEC1_MAX_NOPQ_ROUND, 0, zOpt, pz + ); + p->nOpqRound = (int)iVal; + break; + + case 11: assert( sqlite3_stricmp("profile", zOpt)==0 ); + sqlite3_free(p->zProfileFunction); + p->zProfileFunction = sqlite3_mprintf("%s", zVal); + if( p->zProfileFunction==0 ) rc = SQLITE_NOMEM; + break; + + default: assert( sqlite3_stricmp("index", zOpt)==0 ); + assert( eOpt==12 ); /* index */ + if( sqlite3_stricmp("trained", zVal) ){ + *pz = sqlite3_mprintf( + "unrecognized index '%s', should be 'trained'", zVal + ); + rc = SQLITE_ERROR; + } + break; + } + } + + return rc; +} + +#define VEC1_INDEX_NONE 1 +#define VEC1_INDEX_FLAT 2 + +typedef struct Vec1FlatIndex Vec1FlatIndex; +struct Vec1FlatIndex { + int eDistance; + int eIndex; +}; + +static int vec1FlatIndexCfg( + void *pCtx, + const char *zOpt, + int eType, + i64 iVal, + double fVal, + const char *zVal, + char **pz +){ + int rc = SQLITE_OK; + Vec1FlatIndex *p = (Vec1FlatIndex*)pCtx; + + UNUSED_PARAMETER(eType); + UNUSED_PARAMETER(iVal); + UNUSED_PARAMETER(fVal); + + if( sqlite3_stricmp("distance", zOpt)==0 ){ + rc = vec1ConfigDistance(&p->eDistance, zVal, pz); + }else + if( sqlite3_stricmp("index", zOpt)==0 ){ + const char *azIdx[] = {"none", "flat", 0}; + int iOpt = 0; + rc = vec1ConfigEnum(&iOpt, zVal, azIdx, "index", pz); + assert( VEC1_INDEX_NONE==1 ); + assert( VEC1_INDEX_FLAT==2 ); + p->eIndex = iOpt+1; + }else{ + *pz = sqlite3_mprintf("unknown parameter: %s", zOpt); + rc = SQLITE_ERROR; + } + + return rc; +} + +/* +** aVec[] is a vector nElem elements in size. If any of these elements is +** non-finite - either NaN or an Infinity value - return SQLITE_ERROR. +** Return SQLITE_OK if all elements are finite. +*/ +static int vec1VectorIsNonFinite(const float *aVec, int nElem){ + static const u32 MASK = 0x7F800000; + int ii = 0; + +#if defined(VEC1_HAVE_AVX2) + const __m256i exp_mask = _mm256_set1_epi32(MASK); + for(; ii<=(nElem-8); ii+=8){ + __m256i bits = _mm256_loadu_si256((__m256i*)&aVec[ii]); + __m256i exp_bits = _mm256_and_si256(bits, exp_mask); + __m256i nonfinite = _mm256_cmpeq_epi32(exp_bits, exp_mask); + if( _mm256_movemask_ps(_mm256_castsi256_ps(nonfinite)) ){ + goto error_out; + } + } +#endif + +#if defined(VEC1_HAVE_NEON) + const uint32x4_t exp_mask = vdupq_n_u32(MASK); + for(; ii<=(nElem-4); ii+=4){ + uint32x4_t bits = vld1q_u32((u32*)&aVec[ii]); + uint32x4_t exp_bits = vandq_u32(bits, exp_mask); + uint32x4_t nonfinite = vceqq_u32(exp_bits, exp_mask); + if( vmaxvq_u32(nonfinite) ){ + goto error_out; + } + } +#endif + + for(; iitv.nElem!=0 && p->tv.nElem!=(int)(n/sizeof_f32)) + ){ + sqlite3_result_error(pCtx, "vec1_train: bad argument", -1); + return; + } + if( vec1VectorIsNonFinite((const float*)a, (n/sizeof_f32)) ){ + sqlite3_result_error(pCtx, + "vec1_train: non-finite value not allowed in vector", -1 + ); + return; + } + + /* If this is the first call to xStep() for this aggregate, initialize + ** the various "training parameters" fields of Vec1TrainCtx. */ + if( p->tv.nElem==0 ){ + p->tv.nElem = n/sizeof_f32; + p->tv.nVecPerChunk = (VEC1_TRAINING_SZCHUNK / (p->tv.nElem*sizeof_f32)); + + /* Configure default values */ + p->eDistance = VEC1_TRAINING_DEFAULT_DISTANCE; + p->nBucket = VEC1_TRAINING_DEFAULT_NBUCKET; + p->nThread = pList->nThread; + p->nOpqRound = VEC1_TRAINING_DEFAULT_NOPQ_ROUND; + p->bSvdVerify = VEC1_TRAINING_DEFAULT_SVD_VERIFY; + p->bResidual = VEC1_TRAINING_DEFAULT_RESIDUAL; + p->eQuant = -1; + p->nCodesize = -1; + p->bWHT = -1; + p->bOpq = -1; + + /* If a configuration object was passed, update the Vec1TrainCtx with + ** its field values now. */ + if( nArg==2 ){ + sqlite3 *db = sqlite3_context_db_handle(pCtx); + const char *zCfg = (const char*)sqlite3_value_text(aArg[1]); + char *zErr = 0; + int rc = vec1ParseJsonConfig(db, zCfg, vec1Ann1TrainCfg, (void*)p, &zErr); + if( rc ){ + if( zErr!=0 ){ + vec1ResultErrorF(pCtx, "%z", zErr); + }else{ + sqlite3_result_error_code(pCtx, rc); + } + return; + } + } + + /* If no quantizer was specified, select one automatically now. */ + if( p->eQuant<0 ){ + if( p->nCodesize<=0 ){ + p->eQuant = VEC1_QUANTIZE_NONE; + }else if( p->bOpq>0 ){ + p->eQuant = VEC1_QUANTIZE_OPQ; + }else{ + p->eQuant = VEC1_QUANTIZE_PQ; + } + } + + /* If no "wht" value was explicitly specified, enable the Walsh-Hadamard + ** Transform for binary quantizers. Disable it for other quantizers. */ + if( p->bWHT<0 ) p->bWHT = (p->eQuant==VEC1_QUANTIZE_BQ); + + /* If no value was specified for "codesize", choose a default. This + ** is 32 for PQ or OPQ, 0 for "none" quantizer, and (nElem+7)/8 for BQ. */ + if( p->eQuant==VEC1_QUANTIZE_NONE ){ + p->nCodesize = 0; + p->bResidual = 0; + }else if( p->eQuant==VEC1_QUANTIZE_BQ ){ + if( p->nCodesize<=0 || p->nCodesize>=((p->tv.nElem+7)/8) ){ + /* No explicit codesize. Or an explicit codesize large enough to + ** use 1 bit per dimension. So do that - use 1 bit per dimension. */ + p->nCodesize = (p->tv.nElem + 7) / 8; + }else{ + /* Explicit codesize too small for 1 bit per dimension. So use 1 bit + ** for each two dimensions. */ + p->nCodesize = (p->tv.nElem + 15) / 16; + } + }else if( p->nCodesize<=0 ){ + if( p->eQuant==VEC1_QUANTIZE_PQ ){ + p->nCodesize = (p->tv.nElem + 7) / 8; + }else{ + assert( p->eQuant==VEC1_QUANTIZE_OPQ ); + p->nCodesize = 32; + } + } + + if( p->eQuant==VEC1_QUANTIZE_PQ || p->eQuant==VEC1_QUANTIZE_OPQ ){ + /* Usually, tv.nElem will be an integer multiple of nCodebook. In + ** this case set nCodeElem is just set to (tv.nElem / nCodebook) and + ** nCodebook remains as configured. + ** + ** Or, if tv.nElem is not a multiple of nCodebook, set nCodeElem to + ** the smallest value that allows us to cover the entire vector with + ** nCodebook codebooks. Then reduce nCodebook to the minimum number + ** of codebooks required to cover the vector with nCodeElem + ** sub-vectors. */ + p->nCodeElem = ((p->tv.nElem+p->nCodesize-1) / p->nCodesize); + p->nCodesize = ((p->tv.nElem+p->nCodeElem-1) / p->nCodeElem); + } + } + + iChunk = (p->tv.nVec+1)/p->tv.nVecPerChunk; + if( iChunk>=p->tv.nChunk ){ + p->tv.aChunk[iChunk] = (float*)sqlite3_malloc(VEC1_TRAINING_SZCHUNK); + if( p->tv.aChunk[iChunk]==0 ){ + sqlite3_result_error_nomem(pCtx); + return; + } + p->tv.nChunk++; + } + + /* Append a copy of the vector to Vec1TrainCtx.aVec[]. */ + memcpy(vec1TrainingVector(&p->tv, p->tv.nVec), sqlite3_value_blob(aArg[0]),n); + p->tv.nVec++; +} + +/* +** Normalize all the training vectors. +*/ +static void vec1TrainNormalizeAll(Vec1TrainCtx *p){ + int ii; + for(ii=0; iitv.nVec; ii++){ + vec1NormalizeVector(vec1TrainingVector(&p->tv, ii), p->tv.nElem); + } +} + +static void vec1FreeVectors(Vec1TrainVectors *pFree){ + if( pFree ){ + int ii; + for(ii=0; iinChunk; ii++){ + sqlite3_free(pFree->aChunk[ii]); + } + sqlite3_free(pFree); + } +} + +/* +** An instance of the following structure is allocated for each PQ codebook +** to train. Each codebook can then be trained in a separate thread. +*/ +typedef struct Vec1CodebookJob Vec1CodebookJob; +struct Vec1CodebookJob { + Vec1TrainCtx *p; + Vec1TrainVectors *pTrain; + int iOff; + float *aCode; + int rc; +}; + +static void vec1CodebookWork(void *pCtx){ + Vec1CodebookJob *pJob = (Vec1CodebookJob*)pCtx; + Vec1TrainCtx *p = pJob->p; + int nByte = sizeof(Vec1TrainVectors); + + Vec1TrainVectors *pVec = (Vec1TrainVectors*)vec1MallocZero(nByte); + + if( pVec==0 ){ + pJob->rc = SQLITE_NOMEM; + }else{ + int ii; + int nShort = 0; + int nBytePerVec = p->nCodeElem * sizeof_f32; + pVec->nElem = p->nCodeElem; + pVec->nVec = pJob->pTrain->nVec; + pVec->nVecPerChunk = (VEC1_TRAINING_SZCHUNK / nBytePerVec); + pVec->nChunk = (pVec->nVec+pVec->nVecPerChunk-1) / pVec->nVecPerChunk; + + /* If the training vector size is not an integer multiple of the + ** sub-vector size and this is the rightmost sub-vector (the one that + ** will need to be padded with 0.0 values), set nShort to non-zero. */ + if( (pJob->iOff + p->nCodeElem)>pJob->pTrain->nElem ){ + nShort = pJob->pTrain->nElem - pJob->iOff; + } + + /* Allocate chunks for the new Vec1TrainVectors structure. If nShort + ** is set, also zero them out. */ + for(ii=0; iinChunk; ii++){ + /* Allocate space for this chunk. Either VEC1_TRAINING_SZCHUNK or + ** enough space for all remaining sub-vectors, whichever is smaller. */ + int nByte = MIN(VEC1_TRAINING_SZCHUNK, + (pVec->nVec - (ii * pVec->nVecPerChunk)) * nBytePerVec + ); + pVec->aChunk[ii] = (float*)sqlite3_malloc(nByte); + + if( pVec->aChunk[ii]==0 ){ + pJob->rc = SQLITE_NOMEM; + break; + } + if( nShort>0 ){ + memset(pVec->aChunk[ii], 0, nByte); + } + } + + if( pJob->rc==SQLITE_OK ){ + int nCopy = (nShort ? nShort : pVec->nElem) * sizeof_f32; + for(ii=0; iinVec; ii++){ + float *aVec = vec1TrainingVector(pJob->pTrain, ii); + memcpy(vec1TrainingVector(pVec, ii), &aVec[pJob->iOff], nCopy); + } + + pJob->rc = vec1Ann1KMeans(0, pVec, 0, VEC1_PQ_CODEBOOK_SZ, pJob->aCode); + } + + vec1FreeVectors(pVec); + } +} + +static int vec1CodebookFinal(void *pCtx, int rcin){ + UNUSED_PARAMETER(rcin); + return ((Vec1CodebookJob*)pCtx)->rc; +} + +static void vec1TrainPQCodebooks( + Vec1TrainCtx *p, + Vec1TrainVectors *pVec, + Vec1JobQueue *pQueue, + float *aBook +){ + if( p->rc==SQLITE_OK ){ + Vec1CodebookJob *aJob = 0; + + aJob = vec1MallocZero(sizeof(Vec1CodebookJob) * p->nCodesize); + if( aJob ){ + int ii; + int rc = SQLITE_OK; + for(ii=0; rc==SQLITE_OK && iinCodesize; ii++){ + Vec1CodebookJob *pJob = &aJob[ii]; + pJob->p = p; + pJob->iOff = ii * p->nCodeElem; + pJob->aCode = &aBook[ii * p->nCodeElem * VEC1_PQ_CODEBOOK_SZ]; + pJob->rc = SQLITE_OK; + pJob->pTrain = pVec; + rc = vec1JobQueueAddJob( + pQueue, vec1CodebookWork, vec1CodebookFinal, pJob + ); + } + p->rc = vec1JobQueueFinishJobs(pQueue, rc); + }else{ + p->rc = SQLITE_NOMEM; + } + + if( p->rc!=SQLITE_OK ){ + sqlite3_result_error_code(p->pCtx, p->rc); + } + sqlite3_free(aJob); + } +} + +static void vec1TransposeMatrix(double *aMatrix, int nElem) { + for (int i = 0; i < nElem - 1; i++) { + for (int j = i + 1; j < nElem; j++) { + double tmp = aMatrix[i * nElem + j]; + aMatrix[i * nElem + j] = aMatrix[j * nElem + i]; + aMatrix[j * nElem + i] = tmp; + } + } +} +static int vec1JacobiDoOnePair( + int p, int q, + double *A, + int d, + double *Vt, + double fTol +){ + double alpha = 0.0, beta = 0.0, gamma = 0.0; + int r; + + double *Ap = A + p*d; + double *Aq = A + q*d; + + assert( p=fTol*sqrt(alpha*beta) ){ + double zeta = (beta - alpha) / (2.0 * gamma); + double t, c, s; + if (zeta >= 0.0){ + t = 1.0 / ( zeta + sqrt(1.0 + zeta * zeta)); + }else{ + t = -1.0 / (-zeta + sqrt(1.0 + zeta * zeta)); + } + + /* Damping factor apparently sometimes required by parallel Jacobi. + ** Wait to see if we have a problem first I suppose... */ + /* t = t*0.5; */ + c = 1.0 / sqrt(1.0 + t * t); + s = -c * t; + + /* Rotate columns p and q of A */ + r = 0; +#ifdef VEC1_HAVE_AVX2 + __m256d vc = _mm256_set1_pd(c); + __m256d vs = _mm256_set1_pd(s); + __m256d vms = _mm256_set1_pd(-s); + for(; rnThread + (p->nThread==1); + int nBlk = nJob*2; + int nByte = nJob * sizeof(Vec1JacobiJob); + + Vec1JacobiJob *aJob = (Vec1JacobiJob*)vec1MallocZero(nByte); + *pnJob = 0; + if( aJob==0 ){ + p->rc = SQLITE_NOMEM; + sqlite3_result_error_nomem(p->pCtx); + }else{ + int ii; + int nColPerBlk = (nElem + nBlk-1)/ nBlk; + for(ii=0; iiA = A; + pJob->nElem = nElem; + pJob->Vt = Vt; + pJob->fTol = fTol; + pJob->iJob = ii; + pJob->nColPerBlk = nColPerBlk; + pJob->nBlk = nBlk; + } + + *pnJob = nJob; + *pnStep = nBlk-1; + } + return aJob; +} + +static void vec1JacobiWork(void *pArg){ + Vec1JacobiJob *pJob = (Vec1JacobiJob*)pArg; + int iBlk1 = 0; + int iBlk2 = 0; + int p0, p1, q0, q1; + int p; + + vec1RoundRobinPair(pJob->nBlk, pJob->r, pJob->iJob, &iBlk1, &iBlk2); + if( iBlk1>iBlk2 ){ + SWAP(int, iBlk1, iBlk2); + } + + p0 = iBlk1 * pJob->nColPerBlk; + q0 = iBlk2 * pJob->nColPerBlk; + p1 = MIN(p0 + pJob->nColPerBlk, pJob->nElem); + q1 = MIN(q0 + pJob->nColPerBlk, pJob->nElem); + + /* Do cross-block pairs */ + for(p=p0; pnApply += vec1JacobiDoOnePair( + p, q, pJob->A, pJob->nElem, pJob->Vt, pJob->fTol + ); + } + } + + /* If this is round 0, also do intra-block pairs */ + if( pJob->r==0 ){ + for(p=p0; pnApply += vec1JacobiDoOnePair( + p, q, pJob->A, pJob->nElem, pJob->Vt, pJob->fTol + ); + } + } + for(p=q0; pnApply += vec1JacobiDoOnePair( + p, q, pJob->A, pJob->nElem, pJob->Vt, pJob->fTol + ); + } + } + } +} + +/* +** This routine computes SVD of a column-major square matrix A (d x d): +** +** A = U * diag(S) * Vt +** +** Parameters: +** A - input [d*d], column-major. MODIFIED BY THIS ROUTINE +** d - matrix dimension +** U - output [d*d], column-major. Columns = left singular vectors. +** S - output [d]. Singular values, not sorted. +** Vt - output [d*d], column-major storage of V^T. +** Row j of V^T (right singular vector j) occupies +** column j in memory: elements Vt[j*d .. j*d+d-1]. +** nMaxIter - max sweeps +** fTol - convergence tolerance +** +** Returns number of sweeps performed. +**/ +static void vec1JacobiSVD( + Vec1TrainCtx *pTrain, + Vec1JobQueue *pQueue, + double *A, + int d, + double *U, + double *S, + double *Vt, + int nMaxIter, /* Maximum number of sweeps */ + double fTol +){ + int j = 0; /* Iterator */ + int nSweep = 0; /* Number of sweeps performed so far */ + int nStep = 0; /* Number of steps per sweep */ + + Vec1JacobiJob *aJob = 0; + int nJob = 0; + + assert( nMaxIter>0 && fTol>0.0 ); + aJob = vec1JacobiAlloc(pTrain, A, d, Vt, fTol, &nJob, &nStep); + if( aJob==0 ) return; + + /* Vt is initialised to an identity matrix. */ + memset(Vt, 0, d * d * sizeof(double)); + for(j=0; j0 ) break; + } + if( j==nJob ){ + nSweep = nMaxIter; + } + } + + /* Vt was accumulated in row-major format. Flip it back to column-major */ + vec1TransposeMatrix(Vt, d); + + /* + ** Columns of A are now orthogonal. The singular value j is the norm + ** of column j of A. Column j of U is column j of A, normalised. + */ + for(j=0; jfTol ){ + for (r=0; rthreshold || fOrthVT>threshold || fReconstruction>threshold ){ + vec1ResultErrorF(pCtx, "vec1: SVD error - " + "fOrthU=%f, fOrthVT=%f, fReconstruction=%f", + fOrthU, fOrthVT, fReconstruction + ); + return SQLITE_ERROR; + } + + return SQLITE_OK; +} + +typedef struct Vec1CovarianceJob Vec1CovarianceJob; +struct Vec1CovarianceJob { + double *aM; /* output matrix - each job writes distinct rows */ + float *aBook; /* codebook */ + + Vec1TrainVectors *pVec; + + int nCodeElem; /* Number of elements in each subspace */ + int iSub; /* Our job to do this subspace */ + double fTotalDist; /* private accumulator */ +#ifdef VEC1_USE_TRANSPOSITION + float *aTrans; /* Transposed version of this job's codebook */ +#endif +}; + +/* +** One job per sub-space. +*/ +static Vec1CovarianceJob *vec1CovarianceAlloc( + Vec1TrainCtx *p, + double *aM, + Vec1TrainVectors *pVec, + float *aBook, + int *pnJob /* OUT: number of jobs */ +){ + int nByte = sizeof(Vec1CovarianceJob) * p->nCodesize; + Vec1CovarianceJob *aJob; + +#ifdef VEC1_USE_TRANSPOSITION + /* Add space for each job to create a transposed version of its codebook */ + nByte += sizeof_f32 * p->nCodeElem * VEC1_PQ_CODEBOOK_SZ * p->nCodesize; +#endif + aJob = (Vec1CovarianceJob*)vec1MallocZero(nByte); + + assert( p->rc==SQLITE_OK ); + *pnJob = 0; + if( aJob ){ + int ii; +#ifdef VEC1_USE_TRANSPOSITION + float *aTrans = (float*)&aJob[p->nCodesize]; + for(ii=0; iinCodesize; ii++){ + aJob[ii].aTrans = &aTrans[ii * p->nCodeElem * VEC1_PQ_CODEBOOK_SZ]; + } +#endif + for(ii=0; iinCodesize; ii++){ + Vec1CovarianceJob *pJob = &aJob[ii]; + pJob->aM = aM; + pJob->aBook = aBook; + pJob->pVec = pVec; + pJob->nCodeElem = p->nCodeElem; + pJob->iSub = ii; + } + *pnJob = p->nCodesize; + }else{ + sqlite3_result_error_nomem(p->pCtx); + p->rc = SQLITE_NOMEM; + } + + return aJob; +} + +#if defined(VEC1_HAVE_AVX2) +/* +** Convert each element of the 8-float vector fVec to a double, and add +** each scalar value to the corresponding aOut[] element. +*/ +static void vec1AddToDoubleArrayAVX2(double *aOut, __m256 fVec){ + __m256d lo = _mm256_cvtps_pd(_mm256_castps256_ps128(fVec) ); + __m256d hi = _mm256_cvtps_pd(_mm256_extractf128_ps(fVec, 1)); + __m256d lo1 = LOADU64(&aOut[0]); + __m256d hi1 = LOADU64(&aOut[4]); + _mm256_storeu_pd(&aOut[0], _mm256_add_pd(lo, lo1)); + _mm256_storeu_pd(&aOut[4], _mm256_add_pd(hi, hi1)); +} +#endif + +#if defined(VEC1_HAVE_NEON) +/* +** Convert each element of the 4-float vector fVec to a double, and add +** each scalar value to the corresponding aOut[] element. +*/ +static void vec1AddToDoubleArrayNEON(double *aOut, float32x4_t fVec){ + float64x2_t lo = vcvt_f64_f32(vget_low_f32(fVec)); + float64x2_t hi = vcvt_high_f64_f32(fVec); + float64x2_t lo1 = vld1q_f64(&aOut[0]); + float64x2_t hi1 = vld1q_f64(&aOut[2]); + vst1q_f64(&aOut[0], vaddq_f64(lo, lo1)); + vst1q_f64(&aOut[2], vaddq_f64(hi, hi1)); +} +#endif + +/* +** The number of vectors handled by each pass in vec1CovarianceWork(). +*/ +#define VEC1_COVAR_NVEC (VEC1_MULTIMATCH_NVEC * VEC1_COVAR_MULTIMATCH_MULT) + +/* +** For each vv in (0..VEC1_COVAR_NVEC-1), and each ii in +** (0..nElem-1), do the following: +** +** aOut[ii] = aOut[ii] + SUM(aScalar[vv] * aIn[vv][ii]) +*/ +static void vec1CovarianceAccumulateN( + double *aOut, + const float *aIn[VEC1_COVAR_NVEC], + const float aScalar[VEC1_COVAR_NVEC], + int nElem +){ + int ii = 0; +#ifdef VEC1_HAVE_AVX2 +#define VEC1_COVAR_NACC 8 + __m256 vs[VEC1_COVAR_NVEC]; + int vv; + for(vv=0; vviSub*pJob->nCodeElem; + float *aCode = &pJob->aBook[iSubOff * VEC1_PQ_CODEBOOK_SZ]; + int ii, kk; + + int nVec = pJob->pVec->nVec; + int nElem = pJob->pVec->nElem; + int nKK = MIN(pJob->nCodeElem, nElem - iSubOff); + +#if defined(VEC1_USE_TRANSPOSITION) + /* Populate this job's transposed copy of its slice of the codebook. */ + vec1CodebookTranspose( + aCode, pJob->nCodeElem, VEC1_NTRANSPOSE_WIDTH, VEC1_PQ_CODEBOOK_SZ, + pJob->aTrans + ); +#endif + + /* Loop through the training vectors that are our responsibility, + ** VEC1_COVAR_NVEC at a time. Each iteration of this outer loop + ** compares VEC1_COVAR_NVEC sub-vectors to all candidate codebook + ** entries (VEC1_MULTIMATCH_NVEC at a time, as that is the width the + ** best-match routines operate on), then updates pJob->aM[] with the + ** results. */ + ii = 0; + for(; ii<=(nVec-VEC1_COVAR_NVEC); ii+=VEC1_COVAR_NVEC){ + int vv, iBatch; + const float *aVec[VEC1_COVAR_NVEC]; + const float *aSub[VEC1_COVAR_NVEC]; + int aBest[VEC1_COVAR_NVEC]; + const float *aSubHat[VEC1_COVAR_NVEC]; + + for(vv=0; vvpVec, ii+vv); + aSub[vv] = &aVec[vv][iSubOff]; + } + + for(iBatch=0; iBatchnCodeElem, aCode, pJob->aTrans, VEC1_PQ_CODEBOOK_SZ, + &aSub[off], &aBest[off], &pJob->fTotalDist + ); + } + + for(vv=0; vvnCodeElem]; + } + + /* For each row of pJob->aM[], gather the VEC1_COVAR_NVEC + ** scalars that apply to it and stream through the whole row once, + ** rather than once per training vector. */ + for(kk=0; kkaM[(kk+iSubOff) * nElem], aVec, aScalar, nElem + ); + } + } + + /* Scalar tail - handle training vectors not covered by the batched + ** loop above because nVec is not a multiple of VEC1_COVAR_NVEC. */ + for(; iipVec, ii); + const float *aSub = &aVec[iSubOff]; + int iBest = vec1PqBestMatch( + aCode, VEC1_PQ_CODEBOOK_SZ, aSub, pJob->nCodeElem, &pJob->fTotalDist + ); + float *aSubHat = &aCode[iBest * pJob->nCodeElem]; + + for(kk=0; kkaM[(kk+iSubOff) * nElem]; + int iVec; + for(iVec=0; iVecnVec vectors, each p->nElem elements each. +*/ +static void vec1PqFindRotation( + Vec1TrainCtx *p, + Vec1JobQueue *pQueue, + Vec1TrainVectors *pTrain, /* Rotated vectors to train on */ + float *aBook, /* Current codebook */ + float *aRotation /* OUT: best rotation */ +){ + if( p->rc==SQLITE_OK ){ + double *aM = 0; + double *aU = 0; + double *aVT = 0; + double *aS = 0; + double *aW = 0; + int ii; + int nElem2 = (p->tv.nElem * p->tv.nElem); + + Vec1CovarianceJob *aJob = 0; + int nJob = 0; + + /* Allocate space for 4 nElem*nElem matrices - W, M, U and VT. And one + ** nElem vector - S. */ + aM = (double*)vec1MallocZero( + 4 * nElem2 * sizeof(double) + + p->tv.nElem * sizeof(double) + ); + if( aM==0 ){ + p->rc = SQLITE_NOMEM; + sqlite3_result_error_nomem(p->pCtx); + return; + } + aVT = &aM[nElem2]; + aU = &aVT[nElem2]; + aW = &aU[nElem2]; + aS = &aW[nElem2]; + + /* Step 1. Calculate the cross-covariance matrix M between training set X + ** and the quantized version of the training set X^. i.e. the sum of the + ** following for all x in X: + ** + ** xT . x^ + ** + ** (x1) (x1*x1^, x1*x2^, x1*x3^) + ** (x2) . (x1^, x2^, x3^) == (x2*x1^, x2*x2^, x2*x3^) + ** (x3) (x3*x1^, x3*x2^, x3*x3^) + ** + ** Because the covariance matrix is going to LAPACK, it has to be in + ** column-major order. So aM[1]==(x2*x1^). + */ + START_TRAINING_TIMER(p, VEC1_TRAINING_ROT_COVARIANCE); + aJob = vec1CovarianceAlloc(p, aM, pTrain, aBook, &nJob); + if( aJob ){ + for(ii=0; iitv.nElem, aU, aS, aVT, 50, 1e-14); + END_TRAINING_TIMER(p, VEC1_TRAINING_ROT_JACOBI); + vec1MatrixMultiply(aRotation, aU, aVT, p->tv.nElem); + + if( p->rc==SQLITE_OK && p->bSvdVerify ){ + p->rc = vec1SVDSelfTest(p->pCtx, aM, aU, aS, aVT, p->tv.nElem); + } + sqlite3_free(aM); + } +} + +typedef struct Vec1RotationJob Vec1RotationJob; +struct Vec1RotationJob { + + Vec1TrainVectors *pVec; /* Input vectors */ + Vec1TrainVectors *pOut; /* Output vectors */ + + int iFirst; + int iEof; + + const float *aRotation; /* nElem*nElem rotation matrix */ +#ifdef VEC1_USE_TRANSPOSITION + const float *aRotationNT; /* Transposed rotation matrix */ +#endif +}; + +static Vec1RotationJob *vec1RotationAlloc( + Vec1TrainCtx *p, + const float *aRotation, + const float *aRotationNT, + Vec1TrainVectors *pOut /* Store rotated vectors here */ +){ + int nByte = p->nThread * sizeof(Vec1RotationJob); + Vec1RotationJob *aJob = vec1MallocZero(nByte); + if( aJob ){ + int iNext = 0; /* First vector for next job */ + int ii; + for(ii=0; iinThread; ii++){ + Vec1RotationJob *pJob = &aJob[ii]; + ASSIGN_VECTORS_TO_JOB(&p->tv, pJob, ii, p->nThread); + pJob->aRotation = aRotation; + pJob->pOut = pOut; +#ifdef VEC1_USE_TRANSPOSITION + pJob->aRotationNT = aRotationNT; +#else + (void)aRotationNT; +#endif + } + + } + return aJob; +} + +#if defined(VEC1_USE_TRANSPOSITION) +static void vec1RotateVectorTransposeN( + float *aOut[VEC1_MULTIMATCH_NVEC], + const float *aIn[VEC1_MULTIMATCH_NVEC], + int nElem, + const float *aRotation, + const float *aRotationNT +){ + int vv; + int d; + int ii; + + /* ii is the first output element in the VEC1_MULTIMATCH_NVEC output + ** vectors. Each iteration of this outer loop populates 8 elements + ** of each of them */ + for(ii=0; ii<=(nElem-8); ii+=8){ + const float *aNT = &aRotationNT[(ii * nElem)]; + __m256 aAcc[VEC1_MULTIMATCH_NVEC]; + + for(vv=0; vvpVec->nElem; + + int ii = pJob->iFirst; + for(; ii<=(pJob->iEof-VEC1_MULTIMATCH_NVEC); ii+=VEC1_MULTIMATCH_NVEC){ + const float *aIn[VEC1_MULTIMATCH_NVEC]; + float *aOut[VEC1_MULTIMATCH_NVEC]; + int vv; + for(vv=0; vvpVec, vv+ii); + aOut[vv] = vec1TrainingVector(pJob->pOut, vv+ii); + } + +#ifdef VEC1_USE_TRANSPOSITION + vec1RotateVectorTransposeN( + aOut, aIn, nElem, pJob->aRotation, pJob->aRotationNT + ); +#else + vec1RotateVectorN(aOut, aIn, nElem, pJob->aRotation); +#endif + } + + for(; iiiEof; ii++){ + const float *pIn = vec1TrainingVector(pJob->pVec, ii); + float *pOut = vec1TrainingVector(pJob->pOut, ii); + vec1RotateVector(nElem, pJob->aRotation, pIn, pOut); + } +} + +/* +** Create and return a deep copy of the training vectors passed as the +** only argument. +*/ +static Vec1TrainVectors *vec1CopyVectors(Vec1TrainVectors *pIn){ + Vec1TrainVectors *pRet = 0; + int ii; + + pRet = (Vec1TrainVectors*)vec1MallocZero(sizeof(Vec1TrainVectors)); + if( pRet==0 ) return pRet; + pRet->nElem = pIn->nElem; + pRet->nVec = pIn->nVec; + pRet->nChunk = pIn->nChunk; + pRet->nVecPerChunk = pIn->nVecPerChunk; + + for(ii=0; iinChunk; ii++){ + int nByte = pRet->nVecPerChunk * pIn->nElem * sizeof_f32; + pRet->aChunk[ii] = (float*)sqlite3_malloc(VEC1_TRAINING_SZCHUNK); + if( pRet->aChunk[ii]==0 ){ + vec1FreeVectors(pRet); + return 0; + } + if( ii==pIn->nChunk-1 ){ + nByte = (pRet->nVec - ii*pRet->nVecPerChunk) * pRet->nElem*sizeof_f32; + } + assert( nByte<=VEC1_TRAINING_SZCHUNK ); + memcpy(pRet->aChunk[ii], pIn->aChunk[ii], nByte); + } + + return pRet; +} + +static void vec1TrainRotation( + Vec1TrainCtx *p, + Vec1JobQueue *pQueue, + float *aBook, + float *aRotation +){ + const int nElem = p->tv.nElem; + const int nMatrixByte = sizeof_f32 * nElem * nElem; + int iRound = 0; /* Current OPQ round */ + int rc = SQLITE_OK; + int ii; + float *aRot = (float*)sqlite3_malloc(nMatrixByte * 2); + float *aRot2 = &aRot[nElem*nElem]; + Vec1TrainVectors *pTrain = vec1CopyVectors(&p->tv); + Vec1RotationJob *aJob = 0; + aJob = vec1RotationAlloc(p, aRotation, aRot2, pTrain); + + if( pTrain==0 || aRot==0 || aJob==0 ){ + sqlite3_result_error_nomem(p->pCtx); + p->rc = SQLITE_NOMEM; + } + + for(iRound=0; iRoundnOpqRound; iRound++){ + vec1TrainLog(p, "OPQ round %d/%d", iRound+1, p->nOpqRound); + START_TRAINING_TIMER(p, VEC1_TRAINING_ROT_CODEBOOKS); + vec1TrainPQCodebooks(p, pTrain, pQueue, aBook); + END_TRAINING_TIMER(p, VEC1_TRAINING_ROT_CODEBOOKS); + + vec1PqFindRotation(p, pQueue, pTrain, aBook, aRot); + + if( p->rc==SQLITE_OK ){ + if( iRound==0 ){ + memcpy(aRotation, aRot, nMatrixByte); + }else{ + memcpy(aRot2, aRotation, nMatrixByte); + vec1MatrixMultiplyF(aRotation, aRot2, aRot, nElem); + } + + START_TRAINING_TIMER(p, VEC1_TRAINING_ROT_VECTORS); +#ifdef VEC1_USE_TRANSPOSITION + vec1CodebookTranspose(aRotation, + nElem, VEC1_NTRANSPOSE_WIDTH, nElem, aRot2 + ); +#endif + for(ii=0; iinThread; ii++){ + vec1JobQueueAddJob(pQueue, vec1RotationWork, 0, &aJob[ii]); + } + vec1JobQueueFinishJobs(pQueue, SQLITE_OK); + END_TRAINING_TIMER(p, VEC1_TRAINING_ROT_VECTORS); + } + p->nCompletedWork += VEC1_TRAINING_WORK_OPQ; + } + + if( p->rc==SQLITE_OK ){ + for(ii=0; iitv.nChunk; ii++){ + SWAP(float*, p->tv.aChunk[ii], pTrain->aChunk[ii]); + } + } + sqlite3_free(aJob); + sqlite3_free(aRot); + vec1FreeVectors(pTrain); +} + +static void vec1TrainLogTimes(Vec1TrainCtx *p){ + double fTotal = (double)p->aTime[VEC1_TRAINING_TOTAL]; + if( p->zProfileFunction ){ + struct Counter { + const char *zName; + int iTime; + } aCounter[] = { + { "rot_codebooks", VEC1_TRAINING_ROT_CODEBOOKS }, + { "rot_covariance", VEC1_TRAINING_ROT_COVARIANCE }, + { "rot_jacobi", VEC1_TRAINING_ROT_JACOBI }, + { "rot_vectors", VEC1_TRAINING_ROT_VECTORS }, + { "coarse_init", VEC1_TRAINING_COARSE_INIT }, + { "coarse_lloyds", VEC1_TRAINING_COARSE_KMEANS }, + { "residuals", VEC1_TRAINING_RESIDUALS }, + { "codebooks", VEC1_TRAINING_CODEBOOKS } + }; + int icall; + for(icall=0; icall<2; icall++){ + int ii; + char *zSql = 0; + + sqlite3_str *pSql = sqlite3_str_new(0); + sqlite3_str_appendf(pSql, "SELECT \"%w\"('{", p->zProfileFunction); + if( icall==0 ){ + for(ii=0; ii0) ? ", " : ""), + aCounter[ii].zName, p->aTime[ aCounter[ii].iTime ]*100.00 / fTotal + ); + } + }else{ + for(ii=0; iiaTime[ aCounter[ii].iTime ]; + sqlite3_str_appendf(pSql, "%s\"%s\": ", + ((ii>0) ? ", " : ""), aCounter[ii].zName + ); + if( val>(1000*1000*1000) ){ + sqlite3_str_appendf(pSql, "\"%.2fG\"", (double)val / 1000000000.0); + }else if( val>(10*1000*1000) ){ + sqlite3_str_appendf(pSql, "\"%dM\"", (int)(val / (1000*1000))); + }else if( val>(1*1000*1000) ){ + sqlite3_str_appendf(pSql, "\"%.2fM\"", (double)val / 1000000.0); + }else if( val>(10*1000) ){ + sqlite3_str_appendf(pSql, "\"%dK\"", (int)(val / (1000))); + }else if( val>(1000) ){ + sqlite3_str_appendf(pSql, "\"%.2fK\"", (double)val / 1000.0); + }else{ + sqlite3_str_appendf(pSql, "\"%d\"", (int)val); + } + } + } + + sqlite3_str_appendf(pSql, "}')"); + zSql = sqlite3_str_finish(pSql); + + if( zSql==0 ){ + sqlite3_result_error_nomem(p->pCtx); + break; + }else{ + sqlite3 *db = sqlite3_context_db_handle(p->pCtx); + char *zErrmsg = 0; + int rc = sqlite3_exec(db, zSql, 0, 0, &zErrmsg); + if( rc!=SQLITE_OK ){ + sqlite3_result_error(p->pCtx, zErrmsg, -1); + sqlite3_result_error_code(p->pCtx, rc); + break; + } + sqlite3_free(zErrmsg); + sqlite3_free(zSql); + } + } + } +} + +typedef struct Vec1ResidualJob Vec1ResidualJob; +struct Vec1ResidualJob { + /* Data to train on */ + const Vec1TrainCtx *p; + Vec1TrainVectors *pVec; + int iFirst; + int iEof; + const float *aCentroid; + int nCentroid; +}; + +static void vec1TrainResidualWorker(void *pCtx){ + Vec1ResidualJob *p = (Vec1ResidualJob*)pCtx; + int ii; + int nElem = p->pVec->nElem; + + for(ii=p->iFirst; iiiEof; ii++){ + float *a = vec1TrainingVector(p->pVec, ii); + int iBest = vec1PqBestMatch(p->aCentroid, p->nCentroid, a, nElem, 0); + vec1SubInPlace(a, &p->aCentroid[iBest * nElem], nElem); + } +} + +static void vec1TrainFindResiduals( + Vec1TrainCtx *p, + Vec1JobQueue *pQueue, + const float *aCentroid +){ + if( p->rc==SQLITE_OK ){ + int ii; + int nJob = p->nThread; + Vec1ResidualJob *aJob = 0; + int rc = SQLITE_OK; + + assert( nJob>=1 ); + aJob = (Vec1ResidualJob*)vec1MallocZero(sizeof(Vec1ResidualJob) * nJob); + if( aJob==0 ){ + sqlite3_result_error_nomem(p->pCtx); + p->rc = SQLITE_NOMEM; + return; + } + + for(ii=0; iitv, pJob, ii, nJob); + pJob->p = p; + pJob->aCentroid = aCentroid; + pJob->nCentroid = p->nBucket; + vec1JobQueueAddJob(pQueue, vec1TrainResidualWorker, 0, (void*)pJob); + } + + vec1JobQueueFinishJobs(pQueue, rc); + sqlite3_free(aJob); + } +} + +static void vec1TrainPrepareLog(Vec1TrainCtx *p){ + if( p->zLogFunction ){ + char *zSql = sqlite3_mprintf("SELECT %s(?, ?)", p->zLogFunction); + if( zSql==0 ){ + sqlite3_result_error_nomem(p->pCtx); + p->rc = SQLITE_NOMEM; + }else{ + sqlite3 *db = sqlite3_context_db_handle(p->pCtx); + p->rc = sqlite3_prepare_v2(db, zSql, -1, &p->pLogStmt, 0); + sqlite3_free(zSql); + if( p->rc==SQLITE_ERROR ){ + sqlite3_result_error(p->pCtx, sqlite3_errmsg(db), -1); + }else if( p->rc!=SQLITE_OK ){ + sqlite3_result_error_code(p->pCtx, p->rc); + } + } + } +} + +static void vec1TrainWorkInit(Vec1TrainCtx *p){ + int nTotal = 0; + + if( p->eQuant==VEC1_QUANTIZE_OPQ ){ + nTotal += VEC1_TRAINING_WORK_OPQ * p->nOpqRound; + } + if( p->nBucket>0 ){ + nTotal += VEC1_TRAINING_WORK_COARSE1 + VEC1_TRAINING_WORK_COARSE2; + } + if( p->eQuant==VEC1_QUANTIZE_PQ || p->eQuant==VEC1_QUANTIZE_OPQ ){ + nTotal += VEC1_TRAINING_WORK_CODEBOOKS; + } + + p->nTotalWork = (nTotal ? nTotal : 1); +} + +/* +** Return true if n is a power of 2, or false otherwise. +*/ +static int vec1IsPow2(int n) { + return n > 0 && (n & (n-1)) == 0; +} + +/* +** Apply Walsh-Hadamard Transform to n element vector x. n must +** be a power of 2. +*/ +static void vec1WHT(float *x, int n){ + assert( vec1IsPow2(n) ); + for (int stride = 1; stride < n; stride <<= 1) { + for (int i = 0; i < n; i += stride << 1) { + for (int j = i; j < i + stride; j++) { + float a = x[j]; + float b = x[j + stride]; + x[j] = a + b; + x[j+stride] = a - b; + } + } + } +} + +/* +** Apply Walsh-Hadamard transform to nElem element vector aVec. +*/ +static void vec1ApplyWHT(float *aVec, int nElem){ + if (vec1IsPow2(nElem)) { + vec1WHT(aVec, nElem); + }else{ + int p = 1; + while( (p<<1)tv.nVec; ii++){ + vec1ApplyWHT(vec1TrainingVector(&p->tv, ii), p->tv.nElem); + } +} + +/* +** The xFinal() method for the vec1_train() aggregate function. +** +** Output a trained model blob. +*/ +static void vec1TrainFinal(sqlite3_context *pCtx){ + Vec1TrainCtx *p = 0; + float *aBook = 0; /* Code books */ + float *aCentroid = 0; /* Centroids */ + float *aRotation = 0; /* Rotation */ + + int nByte = 0; + u8 *aByte = 0; + int nReqVectors = 0; + + float *pCsr = 0; + int rc = SQLITE_OK; + Vec1JobQueue *pQueue = 0; + u32 flags = 0; + int ii; + + u32 nHdrCodebook = 0; /* Codebooks= value stored in header */ + int bPQ = 0; + + /* Check that sufficient training vectors were provided. */ + p = (Vec1TrainCtx*)sqlite3_aggregate_context(pCtx, sizeof(*p)); + if( p==0 ) return; + p->pCtx = pCtx; + bPQ = (p->eQuant==VEC1_QUANTIZE_PQ || p->eQuant==VEC1_QUANTIZE_OPQ); + + if( p->tv.nVec==0 ){ + vec1ResultErrorF(pCtx, + "too few training vectors (have 0, require more than that)" + ); + goto train_final_out; + } + if( p->nBucket || bPQ ){ + int nReq = p->nBucket * 4; + if( bPQ ) nReq = MAX(VEC1_PQ_TRAINING_SET_MIN, nReq); + if( p->tv.nVectv.nVec + ); + goto train_final_out; + } + } + + vec1TrainWorkInit(p); + vec1TrainPrepareLog(p); + vec1TrainLog(p, "vectors loaded, starting training"); + if( p->rc!=SQLITE_OK ) goto train_final_out; + + START_TRAINING_TIMER(p, VEC1_TRAINING_TOTAL); + + /* If this is training for "cos" distance, not "l2", normalize all + ** training vectors before proceding as usual. */ + if( p->eDistance==VEC1_DISTANCE_COS ){ + vec1TrainNormalizeAll(p); + } + + /* Allocate space for the model. Space is required for (A) the header and + ** coarse centroids, (B) the PQ codebooks and (C) the OPQ rotation + ** matrix. */ + nByte = VEC1_HEADER_SIZE + (sizeof_f32 * p->nBucket * p->tv.nElem); /* A */ + if( bPQ ){ + nByte += sizeof_f32*p->nCodesize*p->nCodeElem*VEC1_PQ_CODEBOOK_SZ; /* B */ + } + if( p->eQuant==VEC1_QUANTIZE_OPQ ){ + nByte += sizeof_f32 * p->tv.nElem * p->tv.nElem; /* C */ + } + + aByte = (u8*)vec1ContextMalloc(pCtx, nByte); + if( aByte==0 ) goto train_final_out; + pCsr = (float*)&aByte[VEC1_HEADER_SIZE]; + if( bPQ ){ + aBook = pCsr; + pCsr += (p->nCodesize * p->nCodeElem * VEC1_PQ_CODEBOOK_SZ); + } + if( p->nBucket>0 ){ + aCentroid = pCsr; + pCsr += (p->nBucket * p->tv.nElem); + } + if( p->eQuant==VEC1_QUANTIZE_OPQ ){ + aRotation = pCsr; + pCsr += (p->tv.nElem * p->tv.nElem); + } + assert( (u8*)pCsr==&aByte[nByte] ); + + /* Allocate a jobs queue, if multi-threading is required. If it is not + ** required, leave pQueue set to NULL. Things will proceed in a single + ** thread in that case. */ +#if VEC1_THREADS + if( p->nThread>1 ){ + pQueue = vec1JobQueueNew(p->nThread-1); + if( pQueue==0 ){ + sqlite3_result_error_nomem(pCtx); + goto train_final_out; + } + } +#endif + + /* Write the model header into the output buffer. */ + flags = VEC1_MODEL_INDEX + | ((p->nBucket>0 && p->bResidual) ? VEC1_MODEL_RESIDUAL : 0) + | ((p->eQuant==VEC1_QUANTIZE_OPQ) ? VEC1_MODEL_ROTATE : 0) + | (p->bWHT ? VEC1_MODEL_WHT : 0) + | ((p->eQuant==VEC1_QUANTIZE_BQ) ? VEC1_MODEL_BITQUANT : 0); + vec1HeaderWrite(aByte, flags, + p->tv.nElem, p->nCodesize, p->nBucket, p->eDistance + ); + + /* If an OPQ rotation is require, calculate one now */ + if( p->eQuant==VEC1_QUANTIZE_OPQ ){ + vec1TrainRotation(p, pQueue, aBook, aRotation); + }else if( p->bWHT ){ + vec1TrainApplyWHT(p); + } + + if( p->nBucket>0 ){ + vec1TrainCoarseQuant(p, pQueue, aCentroid); + + /* For each training vector, find the closest centroid. Then subtract + ** the centroid from the training vector, to leave the residual vector. + ** This is what the PQ step trains on. */ + if( bPQ && p->bResidual ){ + vec1TrainLog(p, "calculating residuals"); + START_TRAINING_TIMER(p, VEC1_TRAINING_RESIDUALS); + vec1TrainFindResiduals(p, pQueue, aCentroid); + END_TRAINING_TIMER(p, VEC1_TRAINING_RESIDUALS); + } + } + + /* Now train the PQ codebooks, if required. Each codebook is trained in + ** parallel, using a separate thread if threads were configured. */ + if( bPQ ){ + vec1TrainLog(p, "PQ codebook training"); + START_TRAINING_TIMER(p, VEC1_TRAINING_CODEBOOKS); + vec1TrainPQCodebooks(p, &p->tv, pQueue, aBook); + p->nCompletedWork += VEC1_TRAINING_WORK_CODEBOOKS; + END_TRAINING_TIMER(p, VEC1_TRAINING_CODEBOOKS); + } + + /* Return the result blob. */ + if( p->rc==SQLITE_OK ){ + sqlite3_result_blob(pCtx, (void*)aByte, nByte, vec1SqliteFree); + aByte = 0; + } + + train_final_out: + END_TRAINING_TIMER(p, VEC1_TRAINING_TOTAL); + vec1TrainLog(p, "training complete"); + vec1TrainLogTimes(p); + sqlite3_finalize(p->pLogStmt); + sqlite3_free(p->zLogFunction); + sqlite3_free(p->zProfileFunction); + for(ii=0; iitv.nChunk; ii++){ + sqlite3_free(p->tv.aChunk[ii]); + } + + vec1JobQueueFree(pQueue); + sqlite3_free(aByte); +} + + +typedef struct Vec1Model Vec1Model; +struct Vec1Model { + Vec1ModelHeader hdr; + const float *aModel; + const float *aCentroid; + const float *aRotation; + + int nCodeElem; + float *aModelT; + +#ifdef VEC1_USE_TRANSPOSITION + float *aCentroidNT; + float *aModelNT; + float *aRotationNT; +#endif +}; + +static int vec1DecodeModel( + const u8 *aBlob, + int nByte, + Vec1Model *pMod, + char **pzErr +){ + const u8 *pCsr = aBlob; + int nCodebook = 0; + + if( nBytehdr); + pCsr += VEC1_HEADER_SIZE; + + assert( VEC1_HEADER_VERSION_BIN>VEC1_HEADER_VERSION ); + if( pMod->hdr.iVersion!=VEC1_HEADER_VERSION + && pMod->hdr.iVersion!=VEC1_HEADER_VERSION_BIN + ){ + if( pMod->hdr.iVersionhdr.iVersion / 1000), (pMod->hdr.iVersion % 1000) + ); + } + return SQLITE_ERROR; + } + + /* Check header fields are within the acceptable ranges. This also serves + ** to ensure that the 32-bit arithmetic used to calculate offsets below + ** does not overflow. */ + if( pMod->hdr.nElem>VEC1_VECSIZE_MAX + || pMod->hdr.nBucket>VEC1_MAX_BUCKET + || pMod->hdr.nCodebook>pMod->hdr.nElem + ){ + *pzErr = sqlite3_mprintf("vec1: model is corrupt"); + return SQLITE_ERROR; + } + + nCodebook = pMod->hdr.nCodebook; + if( nCodebook>0 ){ + if( (pMod->hdr.flags & VEC1_MODEL_BITQUANT)==0 ){ + pMod->nCodeElem = (pMod->hdr.nElem + nCodebook-1)/nCodebook; + pMod->aModel = (float*)pCsr; + pCsr += (nCodebook*pMod->nCodeElem*VEC1_PQ_CODEBOOK_SZ * sizeof_f32); + }else{ + if( ((pMod->hdr.nElem + 7)/8)==(u32)nCodebook ){ + pMod->nCodeElem = 8; + }else if( ((pMod->hdr.nElem + 15)/16)==(u32)nCodebook ){ + pMod->nCodeElem = 16; + }else{ + *pzErr = sqlite3_mprintf("vec1: unrecognized bq codesize"); + return SQLITE_ERROR; + } + } + } + if( pMod->hdr.nBucket>0 ){ + pMod->aCentroid = (float*)pCsr; + pCsr += (pMod->hdr.nElem * pMod->hdr.nBucket * sizeof_f32); + } + if( (pMod->hdr.flags & VEC1_MODEL_ROTATE) ){ + pMod->aRotation = (float*)pCsr; + pCsr += (pMod->hdr.nElem * pMod->hdr.nElem * sizeof_f32); + } + + if( (pCsr - aBlob)!=nByte ){ + *pzErr = sqlite3_mprintf( + "vec1: model size mismatch: have %d, expected %d", nByte, (pCsr-aBlob) + ); + return SQLITE_ERROR; + } + + return SQLITE_OK; +} + +#if defined(VEC1_USE_TRANSPOSITION) +static int vec1ModelAllocTCent(Vec1Model *pMod){ + int rc = SQLITE_OK; + if( pMod->aCentroidNT==0 && (pMod->hdr.nBucket>0 || pMod->aModel) ){ + int nElem = pMod->hdr.nElem; + int nByte = sizeof_f32 * nElem * pMod->hdr.nBucket; + if( pMod->aModel ){ + int nFloatPerCodebook = pMod->nCodeElem * pMod->hdr.nCodebook; + nByte += (VEC1_PQ_CODEBOOK_SZ * nFloatPerCodebook * sizeof_f32); + if( pMod->aRotation ){ + nByte += (nElem * nElem) * sizeof_f32; + } + } + pMod->aCentroidNT = (float*)sqlite3_malloc(nByte); + if( pMod->aCentroidNT==0 ){ + rc = SQLITE_NOMEM; + }else{ + assert( pMod->aCentroid || pMod->hdr.nBucket==0 ); + vec1CodebookTranspose(pMod->aCentroid, nElem, + VEC1_NTRANSPOSE_WIDTH, pMod->hdr.nBucket, pMod->aCentroidNT + ); + + if( pMod->aModel ){ + u32 M; + int szCodebook = pMod->nCodeElem * VEC1_PQ_CODEBOOK_SZ; + pMod->aModelNT = &pMod->aCentroidNT[nElem * pMod->hdr.nBucket]; + for(M=0; Mhdr.nCodebook; M++){ + vec1CodebookTranspose( + &pMod->aModel[szCodebook*M], + pMod->nCodeElem, VEC1_NTRANSPOSE_WIDTH, VEC1_PQ_CODEBOOK_SZ, + &pMod->aModelNT[szCodebook*M] + ); + } + + if( pMod->aRotation ){ + /* Create a transposed version of the rotation matrix. */ + pMod->aRotationNT = &pMod->aModelNT[szCodebook * M]; + vec1CodebookTranspose(pMod->aRotation, + nElem, VEC1_NTRANSPOSE_WIDTH, nElem, pMod->aRotationNT + ); + } + } + } + } + return rc; +} + +static void vec1ModelFreeTCent(Vec1Model *pMod){ + sqlite3_free(pMod->aCentroidNT); + pMod->aCentroidNT = 0; + pMod->aModelNT = 0; + pMod->aRotationNT = 0; +} +#else +# define vec1ModelAllocTCent(pMod) SQLITE_OK +# define vec1ModelFreeTCent(pMod) +#endif + +/************************************************************************* +** Start of virtual table code +*/ + +/* %_IDX TABLE LIST FORMAT: +** +** Each list begins with 3 32-bit unsigned integers in big-endian format. +** As follows: +** +** bytes 0..3: Flags value. +** bytes 4..7: Total number of entries in list. +** bytes 8..11: Number of tombstone entries in list +** +** Following these is an array of rowid values - either unsigned 32-bit +** values or 64-bit signed values, depending on the flags set in the +** first field value of the list. In the 32-bit format, the tombstone +** value is 0xFFFFFFFF. For the 64-bit format, it is 0. Rowids are stored +** in big-endian format. +** +** There are two formats - one that uses 32-bit rowids, and another that +** uses 64-bit rowids. Format consists of one or more blocks. Each block +** stores 16 PQ codes: +** +** * 16 rowid values - either 16 * 32-bit unsigned or 16 * 64-bit +** signed. The only way to know which is to read the first/last +** column values from the %_idx table. +** +** * The lookup value for the first codebook for each of the 16 +** codes in the block. Followed by the lookup value for the second +** codebook for each of the 16 codes, and so on. +** +** A PQ code list always consists of an exact number of blocks. In the +** 32-bit rowid format, a rowid value of 0xFFFFFFFF indicates an invalid +** entry. In the 64-bit format, an invalid entry is one with a rowid value +** of 0. +*/ + +/* %_META TABLE LIST FORMAT: +** +** Meta table lists always begin with 2 32-bit unsigned integers in +** big-endian format: +** +** Bytes 0..3: 32-bit big-endian flags field. +** Bytes 4..7: 32-bit big-endian nEntry field. +** +** The flags field is set to a combination of VEC1_META_XXX flags. The +** nEntry fields is set to the number of entries in the list. The format +** of the rest of the list is different depending on the flags: +** +** VEC1_META_GENERIC: +** +** This is the fallback format used if none of the other formats can be +** used. It encodes each value separately as a varint type followed by +** encoded data. Specifically, for type values 0, 1 and 2: +** +** 0: NULL value. 0 byte payload. +** 1: Unsigned integer value in range [0..254]. 1 byte payload. +** 2: Integer value. 4 byte payload in range [-2147483647..+2147483647]. +** 3: Integer value. 8 byte payload. +** 4: Real value. 8 byte payload. +** +** Odd values larger than 2 are text values, with size in bytes of +** (type - 2)/2. Even values larger than 2 are blobs. +** +** VEC1_META_1BYTEINT: +** This format may be used if all values in the list are either NULL or +** integers between 0 and 254, inclusive. Each value is stored as +** a 1 byte unsigned value, with 255 (0xFF) representing NULL. +** +** VEC1_META_4BYTEINT: +** This format may be used if all values in the list are either NULL or +** integers between -2147483648 and +2147483646, inclusive. Each value is +** stored as a 4 byte two's complement value, with +2147483647 +** (0x7FFFFFFF) representing NULL. +** +** VEC1_META_REAL: +** This format may be used if all values in the list are real or NULL. +** Bit pattern 0x7FF8000000000001 is used for NULL. +** +** Flag VEC1_META_HASNULL is set if the list contains at least one NULL +** value. +*/ +#define VEC1_META_GENERIC 0x01 +#define VEC1_META_1BYTEINT 0x02 +#define VEC1_META_4BYTEINT 0x04 +#define VEC1_META_REAL 0x08 +#define VEC1_META_HASNULL 0x10 + +#define VEC1_META_TYPEMASK ( \ + VEC1_META_GENERIC | VEC1_META_1BYTEINT | VEC1_META_4BYTEINT | VEC1_META_REAL \ +) + +#define VEC1_META_SZHDR 8 +#define VEC1_META_1BYTENULL 255 + +#define VEC1_META_REALNULL 0x7FF8000000000001 + +#define VEC1_META_4BYTEMIN -2147483648 +#define VEC1_META_4BYTEMAX +2147483646 +#define VEC1_META_4BYTENULL +2147483647 + +typedef struct Vec1Tab Vec1Tab; +typedef struct Vec1Csr Vec1Csr; + +/* +** Flags that may be part of the first 32-bit unsigned integer of a PQ list. +*/ +#define VEC1_LIST_64BIT 0x01 /* Set for 64-bit rowids, clear for 32-bit */ +#define VEC1_LIST_SORTED 0x02 /* True if rowid array is in sorted order */ + +/* Size of list header in byte */ +#define VEC1_LIST_SZHDR (3*sizeof_u32) + +#define VEC1_TOMBSTONE_32 0xFFFFFFFF +#define VEC1_TOMBSTONE_64 0 + + +/* +** Fields in the virtual table: +*/ +#define VEC1_COLUMN_CMD 0 +#define VEC1_COLUMN_ARG 1 +#define VEC1_COLUMN_DISTANCE 2 +#define VEC1_COLUMN_VECTOR 3 +/* Followed by Vec1Tab.nMeta meta-data columns */ + +/* +** MODEL (0): +** This integer value is set to 1 when a model is first provided to +** the table, and incremented each time the model is updated thereafter. +** +** BLOCKSIZE (1): +** The config entry for the target size (in bytes) for chunks stored in +** the %_idx table. +** +** FMTVERSION (5): +** This is an integer - the current format version. Currently this is +** always 1, but will be increased if the format changes. If a client +** finds a format it does not understand, it has to rebuild the index. +*/ + +#define VEC1_CONFIG_MODEL 0 +#define VEC1_CONFIG_BLOCKSIZE 1 +#define VEC1_CONFIG_NELEM 2 +#define VEC1_CONFIG_BLOCKSIZE_MIN 3 +#define VEC1_CONFIG_FMTVERSION 5 + + + +/* +** Return the shadow-schema used by the vec1_ann table named zTab. Parameter +** zDb must be the name of the database (e.g. "main", "temp" or the name +** of an attached database). +*/ +static char *vec1ShadowSchema(const char *zDb, const char *zTab, int nMeta){ + const char zFmt[] = + + /* Configuration table. This contains persistent configuration parameters, + ** including the training data. Keys must be one of the VEC1_CONFIG_XXX + ** values defined above. */ + "CREATE TABLE %Q.'%q_config'(id INTEGER PRIMARY KEY, val ANY);" + + /* Base table. This contains the raw data as provided by the user. */ + "CREATE TABLE %Q.'%q_base'(" + "id INTEGER PRIMARY KEY, " + "vector BLOB%z" + ");" + + /* Index data */ + "CREATE TABLE %Q.'%q_idx'(" + "id INTEGER PRIMARY KEY, "/* Id of blob */ + "bucket INTEGER, " /* Index of bucket for this blob (or NULL) */ + "first INTEGER, " /* Smallest rowid in blob */ + "last INTEGER, " /* Largest rowid in blob */ + "val BLOB" /* Blob containing rowids + PQ codes */ + ");" + "CREATE INDEX %Q.'%q_idx_idx' ON '%q_idx'(bucket, first, last);" + + /* Training data (codebooks and centroids). This is stored as a single + ** blob with id=1, in the same format as returned by vec1_train(). */ + "CREATE TABLE %Q.'%q_model'(id INTEGER PRIMARY KEY, val BLOB);" + + /* Meta table. Contains arrays of meta values stored in parallel with + ** arrays in the %_idx table. For column iMeta and %_idx array id=iId, + ** the id used in this table is ((iId<<8) + iMeta). */ + "CREATE TABLE %Q.'%q_meta'(" + "id INTEGER PRIMARY KEY, " + "val BLOB" + ");" + ; + + int ii; + char *zMeta = 0; + for(ii=0; iinMeta meta-list builders */ +}; + +#define VEC1_BLOCKED_NONE 0 +#define VEC1_BLOCKED_PQ 1 +#define VEC1_BLOCKED_BIN 2 + +/* +** An object of this type is used to accumulate one or more new vector +** index entries in memory before they are flushed to disk. +*/ +typedef struct Vec1Writer Vec1Writer; +struct Vec1Writer { + Vec1Tab *pTab; /* Vec1 table object */ + int bRebuild; /* True if this is a 'rebuild' op */ + float *aResidual; /* Temp space for residual vector */ + u8 *aPQ; /* Temp space for PQ */ + int nBld; /* Size of aBld[] */ + Vec1ListBuilder *aBld; /* Array of list builders */ +}; + +#define VEC1_INSTRUMENT_QUERIES 1 + +#ifdef VEC1_INSTRUMENT_QUERIES + +# define VEC1_QINSTR_TOTAL 0 +# define VEC1_QINSTR_INITQUERY 1 +# define VEC1_QINSTR_COARSE 2 +# define VEC1_QINSTR_IDXREAD 3 +# define VEC1_QINSTR_METAREAD 4 +# define VEC1_QINSTR_METASCAN 5 +# define VEC1_QINSTR_LUT 6 +# define VEC1_QINSTR_PQSCAN 7 +# define VEC1_QINSTR_FINALSORT 8 + +# define VEC1_QINSTR_NCOUNTER 9 + + +# define VEC1_QINSTR_START(pTab, eCounter) { \ + (pTab)->aCycle[eCounter] -= vec1HardwareTimer(); \ +} + +# define VEC1_QINSTR_STOP(pTab, eCounter) { \ + (pTab)->aCount[eCounter]++; \ + (pTab)->aCycle[eCounter] += vec1HardwareTimer(); \ +} + +#else +# define VEC1_QINSTR_STOP(pTab, eCounter) +# define VEC1_QINSTR_START(pTab, eCounter) +#endif + +#define VEC1_META_COLUMN_BITS 8 +#define VEC1_MAX_META_COLUMNS (1<base.zErrMsg); + pTab->base.zErrMsg = zMsg; +} + +/* +** Prepare the SQL statement passed as the second argument. If successful, +** set (*pStmt) to point to the prepared statement handle and return +** SQLITE_OK. It is the responsibility of the caller to eventually finalize +** the statement using sqlite3_finalize(). +** +** Or, if an error occurs, set (*ppStmt) to NULL, return an SQLite error +** code and leave an error message in the virtual table object. +*/ +static int vec1PrepareSql( + Vec1Tab *pTab, + sqlite3_stmt **ppStmt, + const char *zFmt, + ... +){ + char *zSql = 0; + int rc = SQLITE_OK; + va_list ap; + + va_start(ap, zFmt); + zSql = sqlite3_vmprintf(zFmt, ap); + va_end(ap); + + *ppStmt = 0; + if( zSql ){ + rc = sqlite3_prepare_v2(pTab->db, zSql, -1, ppStmt, 0); + sqlite3_free(zSql); + if( rc!=SQLITE_OK ){ + vec1VtabError(pTab, "%s", sqlite3_errmsg(pTab->db)); + } + }else{ + rc = SQLITE_NOMEM; + } + return rc; +} + +#define VEC1_SQL_SCAN_BASE 1 +#define VEC1_SQL_REPLACE_IDX 2 + +#define VEC1_SQL_SCAN_IDX 3 +#define VEC1_SQL_CNT_IDX 4 + +#define VEC1_SQL_FILTER_IDX 5 + +#define VEC1_SQL_WRITE_CONFIG 5 +#define VEC1_SQL_INCR_CONFIG 6 +#define VEC1_SQL_LOAD_CONFIG 7 + + + + +static int vec1GetSql(Vec1Tab *pTab, int eSql, sqlite3_stmt **ppStmt){ + const char *azSql[] = { +#define VEC1_SQL_INSERT_BASE 0 + "INSERT INTO %Q.'%q_base' VALUES(?, ?%s)", + "SELECT * FROM %Q.'%q_base' ORDER BY 1", + "REPLACE INTO %Q.'%q_idx'(id, bucket, first, last, val)VALUES(?,?,?,?,?)", + + "SELECT val, bucket, rowid FROM %Q.'%q_idx'", + "SELECT sum( length(val) / ? ) FROM %Q.'%q_idx'", + + "REPLACE INTO %Q.'%q_config'(id, val) VALUES(?, ?)", + "INSERT INTO %Q.'%q_config'(id, val) VALUES(?, 1) " + "ON CONFLICT(id) DO UPDATE SET val=val+1", + + "SELECT id, val FROM %Q.'%q_config'", + +#define VEC1_SQL_LOOKUP_BASE 8 + "SELECT * FROM %Q.'%q_base' WHERE id=?", + +#define VEC1_SQL_EDIT_IDX 9 + "SELECT id, min( length(val) ) FROM %Q.'%q_idx' WHERE bucket=?", + +#define VEC1_SQL_DEL_LOOKUP_BASE 10 + /* Used when removing a row by id. This statement deletes the %_base + ** entry and returns the vector so that the user may use it to + ** find the %_idx entry to delete. */ + "DELETE FROM %Q.'%q_base' WHERE id=? RETURNING vector", + +#define VEC1_SQL_SEARCH_IDX 11 + "SELECT id FROM %Q.'%q_idx' WHERE bucket=? AND ? BETWEEN first AND last", + +#define VEC1_SQL_SEARCH_IDX_FOR_INSERT 12 + "SELECT id, first, last, val " + "FROM %Q.'%q_idx' WHERE bucket=? AND length(val)=0 && eSqlaStmt) ); + + if( pTab->aStmt[eSql]==0 ){ + + if( eSql==VEC1_SQL_INSERT_BASE ){ + int ii; + char *zExtra = 0; + for(ii=0; iinMeta; ii++){ + zExtra = vec1MPrintf(&rc, "%z,?", zExtra); + } + if( rc==SQLITE_OK ){ + rc = vec1PrepareSql( + pTab, &pTab->aStmt[eSql], azSql[eSql], + pTab->zDb, pTab->zName, zExtra + ); + } + sqlite3_free(zExtra); + }else{ + rc = vec1PrepareSql( + pTab, &pTab->aStmt[eSql], azSql[eSql], pTab->zDb, pTab->zName + ); + } + } + + *ppStmt = pTab->aStmt[eSql]; + return rc; +} + +static void vec1UnloadModel(Vec1Tab *pTab){ + sqlite3_free(pTab->pModBlob); + sqlite3_free(pTab->mod.aModelT); + vec1ModelFreeTCent(&pTab->mod); + pTab->pModBlob = 0; + pTab->cfg.iModelVersion = 0; + memset(&pTab->mod, 0, sizeof(pTab->mod)); +} + +static void vec1UnloadConfig(Vec1Tab *pTab){ + pTab->cfg.nElem = 0; +} + +/* +** xDisconnect method. Free the virtual table object passed as the only +** argument. +*/ +static int vec1DisconnectMethod(sqlite3_vtab *pVtab){ + Vec1Tab *pTab = (Vec1Tab*)pVtab; + int ii; + + /* Remove this table object from the database handle list of vec1 tables */ + if( pTab->pTabList ){ + Vec1Tab **pp = &pTab->pTabList->pFirst; + while( *pp!=pTab ) pp = &(*pp)->pTabNext; + *pp = pTab->pTabNext; + } + + for(ii=0; iiaStmt); ii++){ + sqlite3_finalize(pTab->aStmt[ii]); + } + vec1UnloadModel(pTab); + sqlite3_free(pTab->zDb); + sqlite3_free(pTab->zName); + sqlite3_free(pTab->zTrainTbl); + sqlite3_free(pTab->zIdxTbl); + sqlite3_free(pTab->aTmpVec); + sqlite3_free(pTab); + return SQLITE_OK; +} + +static int vec1SqlExec(Vec1Tab *pTab, const char *zSqlfmt, char **pzErr){ + char *zSql = 0; + int rc = SQLITE_OK; + + zSql = sqlite3_mprintf(zSqlfmt, pTab->zDb, pTab->zName); + if( zSql==0 ){ + rc = SQLITE_NOMEM; + }else{ + rc = sqlite3_exec(pTab->db, zSql, 0, 0, pzErr); + sqlite3_free(zSql); + } + + return rc; +} + +static void vec1StmtReset(int *pRc, sqlite3_stmt *pStmt){ + int rc = sqlite3_reset(pStmt); + if( *pRc==SQLITE_OK ) *pRc = rc; +} + +static void vec1StmtFinalize(int *pRc, sqlite3_stmt *pStmt){ + int rc = sqlite3_finalize(pStmt); + if( *pRc==SQLITE_OK ) *pRc = rc; +} + +/* +** Return true if character c is an ASCII range whitespace character. +*/ +static int vec1IsWhitespace(char c){ + return (c==' ' || c=='\t' || c=='\r' || c=='\n'); +} + +/* +** Check that zName is a legal name for a vec1 column. A legal column name +** is any single token or quoted string, with no trailing garbage. For +** the purposes of this function, a token is a contiguous sequence of +** non-whitespace characters that do not begin with a quote character. +** +** If zName is a legal name (or an illegal name that will be detected by +** sqlite3_declare_vtab()), SQLITE_OK is returned and (*pzErr) is left +** unchanged. Or, if it is not a legal name, SQLITE_ERROR is returned, and +** *pzErr is left to point to a buffer containing an English language +** error message. It is the responsibility of the caller to eventually +** free this buffer using sqlite3_free(). +*/ +static int vec1CheckColumnName(const char *zName, char **pzErr){ + char aName[16]; + int nName = 0; + const char *z = zName; + char q = 0; + + while( vec1IsWhitespace(*z) ) z++; + + if( (*z)=='"' || (*z)=='\'' || (*z)=='`' ){ + char q = *z++; + while( 1 ){ + if( (*z)=='\0' ){ + *pzErr = sqlite3_mprintf("vec1: unmatched quote character"); + return SQLITE_ERROR; + } + if( (*z)==q ){ + z++; + if( (*z)!=q ) break; + } + if( nName<(int)sizeof(aName) ) aName[nName++] = *z; + z++; + } + } + else if( (*z)=='[' ){ + z++; + while( (*z) && (*z)!=']' ){ + if( nName<(int)sizeof(aName) ) aName[nName++] = *z; + z++; + } + if( (*z)=='\0' ){ + *pzErr = sqlite3_mprintf("vec1: unmatched [ character"); + return SQLITE_ERROR; + } + z++; + } + else { + while( (*z) && 0==vec1IsWhitespace(*z) ){ + if( nName<(int)sizeof(aName) ) aName[nName++] = *z; + z++; + } + } + + while( vec1IsWhitespace(*z) ) z++; + if( (*z)!='\0' ){ + *pzErr = sqlite3_mprintf("vec1: trailing characters after column name"); + return SQLITE_ERROR; + } + + if( nName==5 && 0==sqlite3_strnicmp(aName, "rowid", 5) + || nName==8 && 0==sqlite3_strnicmp(aName, "distance", 8) + ){ + *pzErr = sqlite3_mprintf("vec1: reserved column name: %s", zName); + return SQLITE_ERROR; + } + + return SQLITE_OK; +} + +/* +** Invoke sqlite3_declare_vtab() for this table. +*/ +static int vec1DeclareVtab( + sqlite3 *db, + int argc, + const char *const* argv, + char **pzErr +){ + const char *zVector = "vector"; + char *zExtra = 0; + char *zCreate = 0; + int rc = SQLITE_OK; + int ii; + + for(ii=3; rc==SQLITE_OK && ii3 ){ + zVector = argv[3]; + } + for(ii=4; iizDb = vec1Strdup(&rc, argv[1]); + pTab->zName = vec1Strdup(&rc, argv[2]); + pTab->zTrainTbl = vec1MPrintf(&rc, "%s_model", argv[2]); + pTab->zIdxTbl = vec1MPrintf(&rc, "%s_idx", argv[2]); + pTab->db = db; + pTab->mod.hdr.eDistance = VEC1_DISTANCE_L2; + + pTab->nMeta = MAX(argc-4, 0); + if( pTab->nMeta>VEC1_MAX_META_COLUMNS ){ + *pzErr = sqlite3_mprintf( + "too many meta-data columns (max is %d)", VEC1_MAX_META_COLUMNS + ); + rc = SQLITE_ERROR; + } + + if( rc==SQLITE_OK ){ + rc = vec1DeclareVtab(db, argc, argv, pzErr); + } + + if( rc==SQLITE_OK && bCreate ){ + char *zShadowSchema = vec1ShadowSchema(pTab->zDb, pTab->zName, pTab->nMeta); + if( zShadowSchema==0 ){ + rc = SQLITE_NOMEM; + }else{ + rc = sqlite3_exec(db, zShadowSchema, 0, 0, pzErr); + sqlite3_free(zShadowSchema); + } + if( rc==SQLITE_OK ){ + sqlite3_stmt *pStmt = 0; + rc = vec1GetSql(pTab, VEC1_SQL_WRITE_CONFIG, &pStmt); + if( rc==SQLITE_OK ){ + sqlite3_bind_int(pStmt, 1, VEC1_CONFIG_FMTVERSION); + sqlite3_bind_int(pStmt, 2, VEC1_CURRENT_FMTVERSION); + sqlite3_step(pStmt); + vec1StmtReset(&rc, pStmt); + } + } + } + + if( rc!=SQLITE_OK ){ + if( pTab->base.zErrMsg ){ + *pzErr = pTab->base.zErrMsg; + pTab->base.zErrMsg = 0; + } + vec1DisconnectMethod((sqlite3_vtab*)pTab); + return rc; + } + + /* Add the new table to the database handle list of vec1 tables. It + ** will be eventually removed by vec1DisconnectMethod(). */ + pTab->pTabList = pTabList; + pTab->pTabNext = pTabList->pFirst; + pTabList->pFirst = pTab; + + *ppVtab = &pTab->base; + return SQLITE_OK; +} + +/* +** xCreate / xConnect +*/ +static int vec1ConnectMethod( + sqlite3 *db, + void *p, /* Pointer to Vec1TabList for this db */ + int argc, + const char *const *argv, + sqlite3_vtab **ppVtab, + char **pzErr +){ + return vec1CreateConnect(0, (Vec1TabList*)p, db, argc, argv, ppVtab, pzErr); +} + +static int vec1CreateMethod( + sqlite3 *db, + void *p, /* Pointer to Vec1TabList for this db */ + int argc, + const char *const *argv, + sqlite3_vtab **ppVtab, + char **pzErr +){ + return vec1CreateConnect(1, (Vec1TabList*)p, db, argc, argv, ppVtab, pzErr); +} + +static int vec1DestroyMethod(sqlite3_vtab *pVtab){ + Vec1Tab *pTab = (Vec1Tab*)pVtab; + int rc = SQLITE_OK; + + char *zDrop = vec1DropShadowSchema(pTab->zDb, pTab->zName); + if( zDrop==0 ){ + rc = SQLITE_NOMEM; + }else{ + rc = sqlite3_exec(pTab->db, zDrop, 0, 0, 0); + sqlite3_free(zDrop); + } + + if( rc==SQLITE_OK ){ + vec1DisconnectMethod(pVtab); + } + + return rc; +} + +/* +** Cursor methods +*/ +static int vec1Open( + sqlite3_vtab *pVtab, + sqlite3_vtab_cursor **ppCursor +){ + Vec1Csr *pCur; + + UNUSED_PARAMETER(pVtab); + + pCur = sqlite3_malloc(sizeof(*pCur)); + if( pCur==0 ){ + return SQLITE_NOMEM; + } + memset(pCur, 0, sizeof(*pCur)); + + *ppCursor = &pCur->base; + return SQLITE_OK; +} + +static void vec1QueryFree(Vec1Query *pQuery){ + if( pQuery ){ + int ii; + for(ii=0; iinFilter; ii++){ + sqlite3_free(pQuery->aFilter[ii].pPtr); + } + sqlite3_free(pQuery->heap.aRes); + sqlite3_free(pQuery->aBucket); + sqlite3_free(pQuery); + } +} + +static void vec1ResetCsr(Vec1Csr *pCsr){ + vec1QueryFree(pCsr->pQuery); + sqlite3_finalize(pCsr->pStmt); + pCsr->pStmt = 0; + pCsr->bSeek = 0; + sqlite3_free(pCsr->zDistance); + pCsr->zDistance = 0; + pCsr->bSqlNNQuery = 0; +} + +static int vec1Close(sqlite3_vtab_cursor *cur){ + Vec1Csr *pCsr = (Vec1Csr*)cur; + vec1ResetCsr(pCsr); + sqlite3_free(pCsr); + return SQLITE_OK; +} + +static void vec1FlatIterNext(Vec1FlatIter *pIter){ + while( 1 ){ + pIter->ii++; + if( pIter->ii>=pIter->nEntry ){ + pIter->iRowid = 0; + pIter->aVec = 0; + break; + }else{ + i64 iRowid = 0; + int iOff = VEC1_LIST_SZHDR + pIter->ii * pIter->szRowid; + if( pIter->szRowid==4 ){ + iRowid = (i64)vec1GetU32(&pIter->aBlob[iOff]); + if( iRowid==VEC1_TOMBSTONE_32 ) continue; + }else{ + iRowid = (i64)vec1GetU64(&pIter->aBlob[iOff]); + if( iRowid==VEC1_TOMBSTONE_64 ) continue; + } + + iOff = VEC1_LIST_SZHDR + + (pIter->nEntry * pIter->szRowid) + + (pIter->ii * pIter->szVec); + pIter->aVec = &pIter->aBlob[iOff]; + pIter->iRowid = iRowid; + break; + } + } +} + +/* +** Return the encoded vector size for the model. Two cases: +** +** a) PQ encoding. In this case encoded vector size is hdr.nCodebook. +** +** b) Bitwise encoding. In this case encoded vector size is 4 bytes + +** 1 bit per dimension. +*/ +static int vec1EncodedVectorSize(const Vec1Model *pMod){ + int ret = (int)pMod->hdr.nCodebook; + if( pMod->hdr.flags & VEC1_MODEL_BITQUANT ) ret += sizeof_f32; + return ret; +} + +/* +** nBlob byte buffer aBlob[] contains a list read from the %_idx table. +** This function returns zero if the list size matches the embedded +** number-of-entries field, and so the list may be accessed safely without +** bounds checking, or non-zero otherwise. +*/ +static int vec1CheckIdxSize(Vec1Tab *p, const u8 *aBlob, i64 nBlob){ + int bRet = 1; + + /* If the blob is smaller than 8 bytes, it must be corrupt */ + if( nBlob>=VEC1_LIST_SZHDR ){ + + /* Read the flags and number-of-entries fields */ + u32 flags = vec1GetU32(&aBlob[0]); + i64 nEntry = (int)vec1GetU32(&aBlob[4]); + const i64 szRowid = ((flags & VEC1_LIST_64BIT) ? 8 : 4); + + if( p->mod.hdr.nCodebook>0 ){ + const i64 nBlk = ((nEntry+VEC1_PQ_BLOCKSIZE-1) / VEC1_PQ_BLOCKSIZE); + const i64 szBlk = VEC1_PQ_BLOCKSIZE * vec1EncodedVectorSize(&p->mod); + bRet = (nBlob!=(VEC1_LIST_SZHDR + nEntry*szRowid + nBlk*szBlk)); + }else{ + const i64 szVec = p->cfg.nElem*sizeof_f32; + bRet = (nBlob!=(VEC1_LIST_SZHDR + nEntry*szRowid + nEntry*szVec)); + } + } + return bRet; +} + +static int vec1FlatIterStart( + Vec1Tab *pTab, + Vec1FlatIter *pIter, + const u8 *aBlob, + int nBlob +){ + u32 flags; + + pIter->aBlob = aBlob; + pIter->nBlob = nBlob; + pIter->ii = 0; + + if( vec1CheckIdxSize(pTab, aBlob, nBlob) ){ + return VEC1_CORRUPT; + } + + flags = vec1GetU32(aBlob); + pIter->szRowid = (flags & VEC1_LIST_64BIT) ? 8 : 4; + pIter->nEntry = vec1GetU32(&aBlob[4]); + pIter->szVec = (pTab->cfg.nElem * sizeof_f32); + pIter->ii = -1; + vec1FlatIterNext(pIter); + return SQLITE_OK; +} + +/* +** Merge the two sorted subarrays a[p..q-1] and a[q..r-1] using +** the temporary buffer aTmp. +*/ +static void vec1Merge( + Vec1AnnResult *a, + Vec1AnnResult *aTmp, + int p, + int q, + int r +){ + int i = p; + int j = q; + int k = p; + + while( i1 ){ + i64 nByte = sizeof(Vec1AnnResult) * n; + Vec1AnnResult *aTmp = (Vec1AnnResult*)sqlite3_malloc64(nByte); + if( aTmp==0 ){ + rc = SQLITE_NOMEM; + }else{ + vec1MergeSort(a, aTmp, 0, (int)n); + sqlite3_free(aTmp); + } + } + + return rc; +} + +static int vec1DoKANNBucket(Vec1Query*,int,Vec1Buffer*,Vec1Buffer*,Vec1Buffer*); + +static int vec1StreamingNext(Vec1Csr *pCsr){ + int rc = SQLITE_OK; + Vec1Query *pQuery = pCsr->pQuery; + Vec1AnnHeap *pHeap = &pQuery->heap; + + int ii; + int nSort = (int)(pHeap->nRes - pCsr->nCurrentRes); + + { + i64 nRes = pHeap->nRes; + pHeap->nRes = 0; + for(ii=pCsr->nCurrentRes; iiaRes[ii].iRowid, pHeap->aRes[ii].fDist); + } + } + + while( pQuery->nBucket>0 && nSort<=(pQuery->nOrigRes/2) ){ + Vec1Buffer a = {0,0,0}; + Vec1Buffer b = {0,0,0}; + Vec1Buffer c = {0,0,0}; + int iBest = 0; + int iBucket = 0; + + /* Find the nearest remaining bucket to the query vector. Remove it + ** from the aBucket[] array. */ + for(ii=1; iinBucket; ii++){ + if( pQuery->aBucket[ii].fDistaBucket[iBest].fDist ){ + iBest = ii; + } + } + iBucket = pQuery->aBucket[iBest].iBucket; + pQuery->nBucket--; + if( iBest!=pQuery->nBucket ){ + pQuery->aBucket[iBest] = pQuery->aBucket[pQuery->nBucket]; + } + + rc = vec1DoKANNBucket(pQuery, iBucket, &a, &b, &c); + + vec1BufferFree(&a); + vec1BufferFree(&b); + vec1BufferFree(&c); + if( rc!=SQLITE_OK ) return rc; + } + + nSort = (int)MIN(pHeap->nMax, pHeap->nRes); + rc = vec1AnnResultSort(pHeap->aRes, nSort); + if( rc==SQLITE_OK ){ + pCsr->iCurrentRes = 0; + pCsr->nCurrentRes = nSort; + } + + return rc; +} + +/* +** xNext() method for vec1 cursors. +*/ +static int vec1NextMethod(sqlite3_vtab_cursor *cur){ + int rc = SQLITE_OK; + Vec1Csr *pCsr = (Vec1Csr *)cur; + + if( pCsr->zDistance ){ + sqlite3_free(pCsr->zDistance ); + pCsr->zDistance = 0; + } + + if( pCsr->pQuery ){ + pCsr->iCurrentRes++; + pCsr->bSeek = 0; + if( pCsr->iCurrentRes>=pCsr->nCurrentRes ){ + /* If this is a streaming query, try to get some more results. */ + if( pCsr->pQuery->bStreaming ){ + rc = vec1StreamingNext(pCsr); + } + + /* If this was not a streaming query, or no further results are + ** available, set pStmt to 0 to indicate EOF. */ + if( pCsr->iCurrentRes>=pCsr->nCurrentRes ){ + sqlite3_finalize(pCsr->pStmt); + pCsr->pStmt = 0; + } + } + sqlite3_reset(pCsr->pStmt); + }else{ + rc = sqlite3_step(pCsr->pStmt); + if( rc!=SQLITE_ROW ){ + rc = sqlite3_finalize(pCsr->pStmt); + pCsr->pStmt = 0; + }else{ + rc = SQLITE_OK; + } + } + return rc; +} + +static int vec1ModelTransform( + Vec1Model *pMod, + float **paOut +){ + if( pMod->aModel ){ + const int nCodebook = pMod->hdr.nCodebook; + const int nCodeElem = pMod->nCodeElem; + int M, d, K; + + float *pOut = (float*)sqlite3_malloc( + sizeof_f32 * nCodebook * nCodeElem * VEC1_PQ_CODEBOOK_SZ + ); + if( pOut==0 ) return SQLITE_NOMEM; + *paOut = pOut; + + /* For VEC1_TRANSPOSE_WIDTH==8, the order of elements in output is: + ** + ** aModel[M=0][d=0] [K=0..7] + ** aModel[M=0][d=1] [K=0..7] + ** aModel[M=0][d=2] [K=0..7] + ** ... + ** aModel[M=0][d=(nCodeElem-1)][K=0..7] + ** aModel[M=0][d=0] [K=8..15] + ** aModel[M=0][d=1] [K=8..15] + ** aModel[M=0][d=2] [K=8..15] + ** ... + ** aModel[M=0][d=(nCodeElem-1)][K=248..255] + ** aModel[M=1][d=0] [K=0..7] + ** ... + */ + for(M=0; MaModel[ M * nCodeElem * VEC1_PQ_CODEBOOK_SZ ], + nCodeElem, VEC1_TRANSPOSE_WIDTH, VEC1_PQ_CODEBOOK_SZ, + &pOut[ M * nCodeElem * VEC1_PQ_CODEBOOK_SZ ] + ); + } + } + + return SQLITE_OK; +} + + +/* +** If it is not already loaded, load the PQ model from disk. +*/ +static int vec1LoadModel(Vec1Tab *pTab){ + int rc = SQLITE_OK; + + sqlite3 *db = pTab->db; + sqlite3_blob *pBlob = 0; + + vec1UnloadModel(pTab); + rc = sqlite3_blob_open(db, pTab->zDb, pTab->zTrainTbl, "val", 1, 0, &pBlob); + if( rc==SQLITE_OK ){ + int nByte = sqlite3_blob_bytes(pBlob); + void *pData = sqlite3_malloc(nByte); + if( pData==0 ){ + rc = SQLITE_NOMEM; + }else{ + rc = sqlite3_blob_read(pBlob, pData, nByte, 0); + } + if( rc==SQLITE_OK ){ + rc = vec1DecodeModel(pData, nByte, &pTab->mod, &pTab->base.zErrMsg); + } + if( rc==SQLITE_OK ){ + pTab->pModBlob = pData; + }else{ + sqlite3_free(pData); + } + sqlite3_blob_close(pBlob); + } + + if( rc==SQLITE_OK ){ + rc = vec1ModelTransform(&pTab->mod, &pTab->mod.aModelT); + } + + return rc; +} + +#if 0 +static int vec1AnnResultPartition(Vec1AnnResult *aArr, int iLeft, int iRight){ + double fPivot = aArr[iRight].fDist; /* choose last element */ + int iSmall = iLeft; /* Next small element goes here */ + int ii; + for(ii=iLeft; iiK ){ + iRight = iPivot - 1; + } else { + iLeft = iPivot + 1; + } + } +} +#endif + +/* +** Merge the two sorted subarrays a[p..q-1] and a[q..r-1] using +** the temporary buffer aTmp. +*/ +static void vec1BucketMerge( + Vec1BucketResult *a, + Vec1BucketResult *aTmp, + int p, + int q, + int r +){ + int i = p; + int j = q; + int k = p; + + while( i1 ){ + i64 nByte = sizeof(Vec1BucketResult) * n; + Vec1BucketResult *aTmp = (Vec1BucketResult*)sqlite3_malloc64(nByte); + if( aTmp==0 ){ + rc = SQLITE_NOMEM; + }else{ + vec1BucketMergeSort(a, aTmp, 0, (int)n); + sqlite3_free(aTmp); + } + } + + return rc; +} + + +/* +** Partition the part of the array between elements iLeft and iRight +** (inclusive). Return the index of the partition key. +*/ +static int vec1BucketPartition(Vec1BucketResult *aArr, int iLeft, int iRight){ + float fPivot = aArr[iRight].fDist; /* choose last element */ + int iSmall = iLeft; /* Next small element goes here */ + int ii; + for(ii=iLeft; ii=0 ); + while( 1 ){ + int iPivot = vec1BucketPartition(aArr, iLeft, iRight); + if( iPivot==K ){ + return; + }else if( iPivot>K ){ + iRight = iPivot - 1; + } else { + iLeft = iPivot + 1; + } + } +} + +static int vec1FindBuckets( + Vec1Tab *pTab, + const float *aVec, + int nProbe, + Vec1BucketResult **paBucket +){ + int nBucket = pTab->mod.hdr.nBucket; + int nElem = pTab->mod.hdr.nElem; + Vec1BucketResult *aRes = 0; + int rc = SQLITE_OK; + + aRes = (Vec1BucketResult*)sqlite3_malloc(sizeof(Vec1BucketResult) * nBucket); + + if( aRes ){ + int ii; + for(ii=0; iimod.aCentroid[ii*nElem]; + aRes[ii].iBucket = ii; + aRes[ii].fDist = (float)vec1L2Dist(aVec, aCentroid, nElem); + } + + if( nProbe=aRes[nProbe].fDist ); + } + } +#endif +#if 0 + for(ii=0; iihdr.flags & VEC1_MODEL_ROTATE) ){ + vec1RotateVector(pMod->hdr.nElem, pMod->aRotation, aInput, aTmp); + if( pMod->hdr.eDistance==VEC1_DISTANCE_COS ){ + vec1NormalizeVector(aTmp, pMod->hdr.nElem); + } + aRet = aTmp; + } + else if( (pMod->hdr.flags & VEC1_MODEL_WHT) ){ + memcpy(aTmp, aInput, pMod->hdr.nElem * sizeof_f32); + vec1ApplyWHT(aTmp, pMod->hdr.nElem); + aRet = aTmp; + } + else if( pMod->hdr.eDistance==VEC1_DISTANCE_COS ){ + memcpy(aTmp, aInput, pMod->hdr.nElem * sizeof_f32); + vec1NormalizeVector(aTmp, pMod->hdr.nElem); + aRet = aTmp; + } + + if( aRet!=aTmp + && pMod->hdr.nCodebook>0 + && (pMod->hdr.nCodebook*pMod->nCodeElem)!=pMod->hdr.nElem + ){ + memcpy(aTmp, aRet, pMod->hdr.nElem * sizeof_f32); + aRet = aTmp; + } + + return aRet; +} + +/* +** Return true if vec1TransformRequired() is not a no-op. i.e. if any +** transformation is required for input vectors. +*/ +static int vec1TransformRequired(const Vec1Model *pMod){ + Vec1ModelHeader const *p = &pMod->hdr; + return + (p->flags & (VEC1_MODEL_ROTATE|VEC1_MODEL_WHT)) + || (p->eDistance==VEC1_DISTANCE_COS); +} + + +/* +** This routine is the core of both the LUT builder and the vector encoder. +** +** pIn: +** pIn is an array of VEC1_TRANSPOSE_WIDTH nCodeElem-dimension vectors in +** column major format. That is to say, pIn[0] is the first element of +** the first vector, pIn[1] is the first element of the second vector, +** and so on. +** +** In other words, to find element i of vector v: +** +** pIn[i * VEC1_TRANSPOSE_WIDTH + v] +** +** aVec: +** A single vector with nCodeElem elements.. +** +** nCodeElem: +** Size of vectors in elements. +** +** aOut: +** Array of VEC1_TRANSPOSE_WIDTH. Populated with the square of the L2 distance +** between aVec[] and each of the vectors in pIn. +*/ +static void vec1ModelTDist( + const float *pIn, + const float *aVec, + int nCodeElem, + float *aOut +){ + int d; + +#if defined(VEC1_HAVE_AVX2) + __m256 acc[ VEC1_TRANSPOSE_WIDTH / 8 ]; + const int nAcc = (VEC1_TRANSPOSE_WIDTH / 8); + int c; + + for(c=0; c>7)&0x7f)|0x80; + aBuf[1] = v&0x7f; + return 2; + } + { + u8 a[10]; + int ii; + int nRet; + for(nRet=0; v; nRet++){ + a[nRet] = (v&0x7F); + v = v >> 7; + } + for(ii=0; iimod.hdr.nCodebook; + const int nCodeElem = pTab->mod.nCodeElem; + int M, K; + float *pIn = pTab->mod.aModelT; + + for(M=0; Mcfg.nElem; + int nCodebook = pTab->mod.hdr.nCodebook; + const int nCodeElem = pTab->mod.nCodeElem; + + int M, K; + float c = 1.0 / sqrtf((float)nVec); + + assert( nCodeElem==8 || nCodeElem==16 ); + + for(M=0; M=0 && iMetan = 0; + sqlite3_bind_int64(pStmt, 1, iMetaId); + if( SQLITE_ROW==sqlite3_step(pStmt) ){ + int nByte = sqlite3_column_bytes(pStmt, 0); + rc = vec1BufferGrow(pBuf, nByte); + if( rc==SQLITE_OK ){ + const u8 *a = sqlite3_column_blob(pStmt, 0); + memcpy(pBuf->a, a, nByte); + pBuf->n = nByte; + } + } + + vec1StmtReset(&rc, pStmt); + if( rc==SQLITE_OK && pBuf->n==0 ){ + rc = VEC1_CORRUPT; + } + } + + return rc; +} + +/* +** 0: NULL value. 0 byte payload. +** 1: Integer value. 1 byte payload. +** 2: Integer value. 4 byte payload. +** 3: Integer value. 8 byte payload. +** 4: Real value. 8 byte payload. +*/ +static int vec1MetaValueRead( + Vec1Buffer *pMeta, + int *piOff, + Vec1MetaValue *pVal +){ + int iOff = *piOff; + switch( pMeta->a[iOff] ){ + case 0: { + pVal->eType = SQLITE_NULL; + iOff++; + break; + } + + case 1: { + pVal->eType = SQLITE_INTEGER; + pVal->iVal = (i64)(u8)pMeta->a[iOff+1]; + iOff += 2; + break; + } + + case 2: { + pVal->eType = SQLITE_INTEGER; + pVal->iVal = (int)vec1GetU32(&pMeta->a[iOff+1]); + iOff += 5; + break; + } + + case 3: { + pVal->eType = SQLITE_INTEGER; + pVal->iVal = (i64)vec1GetU64(&pMeta->a[iOff+1]); + iOff += 9; + break; + } + + case 4: { + u64 iVal = vec1GetU64(&pMeta->a[iOff+1]); + memcpy(&pVal->fVal, &iVal, sizeof(u64)); + pVal->eType = SQLITE_FLOAT; + iOff += 9; + break; + } + + default: { + u64 eType = 0; + iOff += vec1GetVarint(&pMeta->a[iOff], &eType); + pVal->pPtr = &pMeta->a[iOff]; + pVal->iVal = (eType-5) / 2; + iOff += (int)pVal->iVal; + pVal->eType = (eType & 0x01) ? SQLITE_TEXT : SQLITE_BLOB; + break; + } + } + + *piOff = iOff; + return SQLITE_OK; +} + +/* +** Compare numeric values a and b. Return -1, 0 or +1 if a is smaller than, +** equal to, or greater than b, respectively. i.e. (a) - (b). +*/ +#define COMPARE(a, b) ((a)==(b) ? 0 : ((a)>(b) ? +1 : -1)) + +/* +** Return 0 if the meta value passes the filter, or 1 if it is excluded +** by the filter. This is the slow path that can do any operator for +** any types of values. +*/ +static int vec1MetaValueFilter(Vec1Filter *pFilter, Vec1MetaValue *pMeta){ + int cmp = 0; + + if( pFilter->vop==VEC1_OP_IN ){ + int ii; + for(ii=0; iiiVal; ii++){ + if( vec1MetaValueFilter(&pFilter[ii+1], pMeta)==0 ) return 0; + } + return 1; + } + + if( pMeta->eType==SQLITE_INTEGER ){ + switch( pFilter->eType ){ + case SQLITE_NULL: return 1; + + case SQLITE_INTEGER: { + cmp = COMPARE(pFilter->iVal, pMeta->iVal); + break; + } + case SQLITE_FLOAT: { + cmp = COMPARE(pFilter->fVal, (double)pMeta->iVal); + break; + } + default: { + /* All text and blob are bigger than all numbers */ + cmp = +1; + break; + } + } + } + else if( pMeta->eType==SQLITE_FLOAT ){ + switch( pFilter->eType ){ + case SQLITE_NULL: return 1; + + case SQLITE_INTEGER: { + cmp = COMPARE((double)pFilter->iVal, pMeta->fVal); + break; + } + case SQLITE_FLOAT: { + cmp = COMPARE(pFilter->fVal, pMeta->fVal); + break; + } + default: { + /* All text and blob are bigger than all numbers */ + cmp = +1; + break; + } + } + }else if( pMeta->eType==SQLITE_NULL ){ + if( pFilter->vop==VEC1_OP_IS && pFilter->eType==SQLITE_NULL ) return 0; + return 1; + }else if( pMeta->eType==SQLITE_TEXT ){ + switch( pFilter->eType ){ + case SQLITE_NULL: return 1; + + case SQLITE_INTEGER: + case SQLITE_FLOAT: { + cmp = -1; + break; + } + + case SQLITE_TEXT: { + i64 n = MIN(pMeta->iVal, pFilter->iVal); + if( n>0 ){ + cmp = memcmp(pFilter->pPtr, pMeta->pPtr, n); + } + if( cmp==0 ) cmp = COMPARE(pFilter->iVal, pMeta->iVal); + break; + } + + default: { + assert( pFilter->eType==SQLITE_BLOB ); + cmp = 1; + break; + } + } + }else{ + assert( pMeta->eType==SQLITE_BLOB ); + switch( pFilter->eType ){ + case SQLITE_NULL: return 1; + + case SQLITE_INTEGER: + case SQLITE_FLOAT: + case SQLITE_TEXT: { + cmp = -1; + break; + } + + default: { + i64 n = MIN(pMeta->iVal, pFilter->iVal); + if( n>0 ) cmp = memcmp(pFilter->pPtr, pMeta->pPtr, n); + if( cmp==0 ) cmp = COMPARE(pFilter->iVal, pMeta->iVal); + assert( pFilter->eType==SQLITE_BLOB ); + break; + } + } + } + + switch( pFilter->vop ){ + case VEC1_OP_IS: + case VEC1_OP_EQ: return !(cmp==0); + case VEC1_OP_LT: return !(cmp>0); + case VEC1_OP_LE: return !(cmp>=0); + case VEC1_OP_GT: return !(cmp<0); + case VEC1_OP_GE: return !(cmp<=0); + } + + assert( pFilter->vop==VEC1_OP_NOTNULL ); + return 0; +} + +/* +** This is called as part of meta-data filtering when the meta-data field +** in question is stored in either VEC1_META_1BYTEINT or VEC1_META_4BYTEINT +** format. +** +** iMin, iMax, eNull: +** These three parameters describe the integer format used to store +** the meta-data field (either VEC1_META_1BYTEINT or VEC1_META_4BYTEINT). +** They pass the minimum integer value, maximum possible integer value +** and the value used to indicate NULL. So, for the two formats, they +** are: +** +** VEC1_META_1BYTEINT: 0, 254, 255. +** VEC1_META_4BYTEINT: -2147483648, +2147483646, +2147483647 +** +** pOp: +** Output parameter. If it is guaranteed that no entries in the meta-data +** field will match the filter (i.e. so zero rows will be returned), set +** this to 0. Otherwise, set it to one of the VEC1_OP_XXX constants to +** indicate the operation that should be used to test each value in the +** meta-data field. +** +** pRval: +** OUTPUT parameter. If (*pOp) is VEC1_OP_IN, then this is set to the +** number of entries in the (*paRval) array output. Otherwise, it is +** set to the integer value to use as the RHS of the comparison when +** testing each meta-data value. +** +** paRval: +** OUTPUT parameter. If (*pOp) is VEC1_OP_IN, an array of values to +** compare each meta-data value to. Otherwise, 0. +** +** Return SQLITE_OK if successful, or an SQLite error code (SQLITE_NOMEM) +** if an error occurs. +*/ +static int vec1FilterToIntOp( + Vec1Filter *pFilter, /* The constraint doing the filtering */ + i64 iMin, i64 iMax, i64 eNull, /* Meta-data format description */ + int *pRval, + int **paRval, + u8 *pOp +){ + /* Set the following three values as follows: + ** + ** res: 0 if the result can be determined to be false for all list + ** entries. 1 if it is true for all non-null list entries. Or + ** -1 if scanning the list using op and rval is required to + ** determine which list entries match the condition. + ** + ** op: If res is -1, one of VEC1_OP_EQ, VEC1_OP_GT, VEC1_OP_GE, + ** VEC1_OP_LT or VEC1_OP_LE. + ** + ** rval: A value to compare using op with all list entries. + ** + ** Then, if res is not 0 or 1, run through the list comparing each + ** list value with value rval using operation op. + */ + int res = -1; + int rval = 0; + u8 op = 0; + + assert( (iMin==0 && iMax==254 && eNull==255) || ( + iMin==VEC1_META_4BYTEMIN + && iMax==VEC1_META_4BYTEMAX + && eNull==VEC1_META_4BYTENULL + )); + + if( pFilter->vop==VEC1_OP_IN ){ + int rc = SQLITE_OK; + int *aRval = (int*)vec1MallocZero(sizeof_u32 * (pFilter->iVal+1)); + if( aRval==0 ){ + return SQLITE_NOMEM; + }else{ + int ii; + int nRval = 0; + for(ii=0; iiiVal; ii++){ + vec1FilterToIntOp(&pFilter[ii+1], iMin, iMax, eNull, &rval, 0, &op); + if( op==VEC1_OP_EQ ){ + aRval[nRval++] = rval; + } + } + op = VEC1_OP_IN; + rval = nRval; + *paRval = aRval; + } + }else if( pFilter->vop==VEC1_OP_NOTNULL ){ + res = 1; + }else if( pFilter->eType==SQLITE_NULL && pFilter->vop==VEC1_OP_IS ){ + rval = eNull; + op = VEC1_OP_EQ; + + }else if( pFilter->eType!=SQLITE_NULL ){ + i64 iVal = pFilter->iVal; + + if( pFilter->eType==SQLITE_TEXT || pFilter->eType==SQLITE_BLOB ){ + /* All text and blob values are greater than all 1-byte ints */ + iVal = iMax+10; + }else if( pFilter->eType==SQLITE_FLOAT ){ + iVal = (i64)pFilter->fVal; + if( (double)iVal!=pFilter->fVal ){ + if( pFilter->vop==VEC1_OP_EQ || pFilter->vop==VEC1_OP_IS ){ + iVal = iMin-10; + }else if( pFilter->vop==VEC1_OP_LT || pFilter->vop==VEC1_OP_GE ){ + iVal = iVal+1; + } + } + } + + if( iValvop==VEC1_OP_GT || pFilter->vop==VEC1_OP_GE); + }else if( iVal>iMax ){ + res = (pFilter->vop==VEC1_OP_LT || pFilter->vop==VEC1_OP_LE); + }else{ + rval = (int)iVal; + op = (pFilter->vop==VEC1_OP_IS ? VEC1_OP_EQ : pFilter->vop); + } + } + + if( res>0 ){ + assert( eNull==(iMax+1) ); + op = VEC1_OP_LT; + rval = eNull; + } + + *pRval = rval; + *pOp = op; + return SQLITE_OK; +} + + +static void vec1MetaFilterIntTail( + const u8 *aMeta, + int iBitOff, + u8 *aBitmask, + int nEntry, + int szInt, + int rval, + int *aRval, + u8 op +){ + const int eNull = szInt==1 ? VEC1_META_1BYTENULL : VEC1_META_4BYTENULL; + int jj; + assert( szInt==1 || szInt==4 ); + for(jj=0; jjrval) && iVal!=eNull; + break; + + case VEC1_OP_LT: + bSet = (iVal=rval) && iVal!=eNull; + break; + + case VEC1_OP_LE: + bSet = (iVal<=rval); + break; + + default: { + int ii; + assert( op==VEC1_OP_IN ); + for(ii=0; ii0x01 */ + hi = vshr_n_u8(hi, 7); + m1 = (u16)vaddlvq_u16(vmull_u8(lo, w)); + m2 = (u16)vaddlvq_u16(vmull_u8(hi, w)); + } + + aBitmask[(jj/8)] |= ~m1; + aBitmask[(jj/8)+1] |= ~m2; + } +#endif + + vec1MetaFilterIntTail( + &a[jj], jj, aBitmask, nEntry-jj, 1, rval, aRval, op + ); +} + +static void vec1MetaFilterScan4ByteArray( + const int *a, + u8 *aBitmask, + int nEntry, + int rval, int *aRval, u8 op +){ + int jj = 0; + +#if defined(VEC1_HAVE_AVX2) + const __m256i rhs = _mm256_set1_epi32(rval); + const __m256i null = _mm256_set1_epi32(VEC1_META_4BYTENULL); + + /* Shuffle mask used to swap the byte-order of the 32-bit integers + ** in this meta-data list if required. There are no big-endian AVX2 + ** systems, so no need for a runtime check here. */ + const __m256i swap = _mm256_setr_epi8( + 3,2,1,0, 7,6,5,4, 11,10,9,8, 15,14,13,12, + 3,2,1,0, 7,6,5,4, 11,10,9,8, 15,14,13,12 + ); + + for(; jj<(nEntry-7); jj+=8){ + __m256i lhs = _mm256_loadu_si256((__m256i*)&a[jj]); + u8 mask = 0; + + /* Shuffle the 8 32-bit int values to little-endian order */ + lhs = _mm256_shuffle_epi8(lhs, swap); + + switch( op ){ + + case VEC1_OP_EQ: { + __m256i cmp = _mm256_cmpeq_epi32(lhs, rhs); + mask = (u8)_mm256_movemask_ps(_mm256_castsi256_ps(cmp)); + break; + } + + case VEC1_OP_GT: { + __m256i cmp = _mm256_cmpgt_epi32(lhs, rhs); + mask = (u8)_mm256_movemask_ps(_mm256_castsi256_ps(cmp)); + cmp = _mm256_cmpeq_epi32(lhs, null); + mask &= ~(u8)_mm256_movemask_ps(_mm256_castsi256_ps(cmp)); + break; + } + + case VEC1_OP_LT: { + __m256i cmp = _mm256_cmpgt_epi32(rhs, lhs); + mask = (u8)_mm256_movemask_ps(_mm256_castsi256_ps(cmp)); + break; + } + + case VEC1_OP_GE: { + __m256i cmp = _mm256_cmpgt_epi32(rhs, lhs); + mask = ~(u8)_mm256_movemask_ps(_mm256_castsi256_ps(cmp)); + cmp = _mm256_cmpeq_epi32(lhs, null); + mask &= ~(u8)_mm256_movemask_ps(_mm256_castsi256_ps(cmp)); + break; + } + + case VEC1_OP_LE: { + __m256i cmp = _mm256_cmpgt_epi32(lhs, rhs); + mask = ~(u8)_mm256_movemask_ps(_mm256_castsi256_ps(cmp)); + break; + } + + default: { + int ii; + assert( op==VEC1_OP_IN ); + for(ii=0; iin<(VEC1_META_SZHDR+(nEntry*szInt)) ){ + rc = VEC1_CORRUPT; + }else{ + int *aRval = 0; + int rval = 0; + u8 op = 0; + + rc = vec1FilterToIntOp(pFilter, + (szInt==4 ? VEC1_META_4BYTEMIN : 0), + (szInt==4 ? VEC1_META_4BYTEMAX : 254), + (szInt==4 ? VEC1_META_4BYTENULL : VEC1_META_1BYTENULL), + &rval, &aRval, &op + ); + if( rc==SQLITE_OK ){ + if( op==0 ){ + /* No entries will match. Set the "filtered-out" bit for all rows. */ + memset(pBitmask->a, 0xFF, pBitmask->n); + }else if( szInt==4 ){ + const int *a = (const int*)&pMeta->a[VEC1_META_SZHDR]; + vec1MetaFilterScan4ByteArray(a, pBitmask->a, nEntry, rval, aRval, op); + }else{ + assert( szInt==1 ); + const u8 *a = (const u8*)&pMeta->a[VEC1_META_SZHDR]; + vec1MetaFilterScan1ByteArray(a, pBitmask->a, nEntry, rval, aRval, op); + } + } + sqlite3_free(aRval); + } + + return rc; +} + +static VEC1_NOINLINE int vec1DoMetaFilters( + Vec1Tab *pTab, + Vec1Query *pQuery, + i64 iId, + int nEntry, + Vec1Buffer *pMeta, + Vec1Buffer *pBitmask +){ + int rc = SQLITE_OK; + int ii; + + /* Grow and zero the bitmask array. */ + int nByte = (nEntry+7)/8; + pBitmask->n = 0; + rc = vec1BufferGrow(pBitmask, nByte); + if( rc==SQLITE_OK ){ + memset(pBitmask->a, 0, nByte); + } + pBitmask->n = nByte; + + assert( pQuery->nFilter>0 ); + + 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); + VEC1_QINSTR_STOP(pTab, VEC1_QINSTR_METAREAD); + + VEC1_QINSTR_START(pTab, VEC1_QINSTR_METASCAN); + if( rc==SQLITE_OK ){ + int jj = 0; + u32 f = vec1GetU32(pMeta->a); + + if( f & VEC1_META_1BYTEINT ){ + /* Single-byte integer format */ + rc = vec1MetaFilterIntList(pFilter, 1, nEntry, pMeta, pBitmask); + }else if( f & VEC1_META_4BYTEINT ){ + /* 4-byte integer format */ + rc = vec1MetaFilterIntList(pFilter, 4, nEntry, pMeta, pBitmask); + }else if( f & VEC1_META_REAL ){ + Vec1MetaValue val = {0, 0, 0, 0}; + for(jj=0; jja[VEC1_META_SZHDR+jj*sizeof_f64] + ); + + if( iVal==VEC1_META_REALNULL ){ + val.eType = SQLITE_NULL; + }else{ + val.eType = SQLITE_FLOAT; + memcpy(&val.fVal, &iVal, sizeof(u64)); + } + + if( vec1MetaValueFilter(pFilter, &val) ){ + pBitmask->a[jj / 8] |= (1 << (jj % 8)); + } + } + }else{ + /* Generic format */ + int iOff = VEC1_META_SZHDR; + for(jj=0; jja[jj / 8] |= (1 << (jj % 8)); + } + } + } + } + + if( pFilter->vop==VEC1_OP_IN ){ + ii += pFilter->iVal; + } + VEC1_QINSTR_STOP(pTab, VEC1_QINSTR_METASCAN); + } + return rc; +} + +static void vec1EncodeVectorBit( + const Vec1Model *pMod, /* Model */ + const float *aVec, /* Input vector */ + u8 *aCode /* OUT: Encoded vector */ +){ + const int nElem = (int)pMod->hdr.nElem; + float norm; + int ii; + u8 *pOut; + + assert( pMod->hdr.flags & VEC1_MODEL_BITQUANT ); + norm = vec1VectorNorm2(aVec, nElem); + norm = sqrtf(norm); + memcpy(aCode, &norm, sizeof_f32); + + assert( pMod->nCodeElem==8 || pMod->nCodeElem==16 ); + + pOut = &aCode[4]; + for(ii=0; iinCodeElem){ + u8 v = 0; + int jj; + if( pMod->nCodeElem==8 ){ + for(jj=0; jj<8; jj++){ + if( aVec[ii+jj]>0.0 ){ + v |= (1<0.0 ){ + v |= (1<hdr.nCodebook; + const int nCodeElem = pMod->nCodeElem; + const float *pIn = pMod->aModelT; + int M, K; + + for(M=0; Mhdr.nCodebook; + const int nCodeElem = pMod->nCodeElem; + int ii = 0; + + for(ii=0; iiaModel[ii*nCodeElem*VEC1_PQ_CODEBOOK_SZ]; + const float *aSub = &aVec[ii*nCodeElem]; + aOut[ii] = (u8)vec1PqBestMatch( + pCodebook, VEC1_PQ_CODEBOOK_SZ, aSub, nCodeElem, pfTotalDist + ); + } +} + + +static void vec1EncodeVector( + const Vec1Model *pMod, + const float *aVec, + u8 *aCode +){ + if( pMod->hdr.flags & VEC1_MODEL_BITQUANT ){ + vec1EncodeVectorBit(pMod, aVec, aCode); + }else{ + vec1EncodeVectorPQ(pMod, aVec, aCode); + } +} + +/* +** Buffer aCode[] contains a vector quantized according to model pMod. +** This function (lossily) reconstructs the original vector and writes +** it to buffer aVec[] before returning. +*/ +static void vec1DecodeVector( + const Vec1Model *pMod, /* Model used to encode vector */ + const u8 *aCode, /* Encoded vector */ + float *aVec /* Populate this buffer with full vector */ +){ + const int nElem = (int)pMod->hdr.nElem; + if( pMod->hdr.flags & VEC1_MODEL_BITQUANT ){ + float fNorm = 0.0; + float fVal = 0.0; + int ii; + + /* The first 4 bytes of the encoded vector store the norm of the vector, + ** as a 32-bit float. */ + memcpy(&fNorm, aCode, sizeof_f32); + + /* The reconstructed vector uses the following value as the magnitude + ** of each dimension. The value is -ve if the corresponding bit is clear, + ** or +ve if it is set. */ + fVal = sqrt((fNorm*fNorm) / nElem); + + assert( pMod->nCodeElem==8 || pMod->nCodeElem==16 ); + for(ii=0; iinCodeElem==8 ){ + if( aCode[sizeof_f32 + (ii/8)] & (1 << (ii%8)) ){ + aVec[ii] = fVal; + }else{ + aVec[ii] = -1 * fVal; + } + }else{ + if( aCode[sizeof_f32 + (ii/16)] & (1 << ((ii/2)%8)) ){ + aVec[ii] = fVal; + }else{ + aVec[ii] = -1 * fVal; + } + } + } + }else{ + int ii; + for(ii=0; ii<(int)pMod->hdr.nCodebook; ii++){ + const float *aCopy = &pMod->aModel[ + (ii * VEC1_PQ_CODEBOOK_SZ + aCode[ii]) * pMod->nCodeElem + ]; + int nCopy = MIN(pMod->nCodeElem, nElem - ii*pMod->nCodeElem); + memcpy(&aVec[ii * pMod->nCodeElem], aCopy, nCopy*sizeof_f32); + } + } +} + +/* +** Return true if any value in aAcc[] is less than fMin. False otherwise. +*/ +static int vec1AnyLessThan(const float aAcc[16], float fMin){ +#if defined(VEC1_HAVE_AVX2) + __m256 vMin = _mm256_set1_ps(fMin); + __m256 cmp0 = _mm256_cmp_ps(_mm256_loadu_ps(aAcc), vMin, _CMP_LT_OQ); + __m256 cmp1 = _mm256_cmp_ps(_mm256_loadu_ps(aAcc+8), vMin, _CMP_LT_OQ); + __m256 mask = _mm256_or_ps(cmp0, cmp1); + return _mm256_movemask_ps(mask)!=0; +#elif defined(VEC1_HAVE_NEON) + float32x4_t vMin = vdupq_n_f32(fMin); + uint32x4_t cmp0 = vcltq_f32(vld1q_f32(aAcc), vMin); + uint32x4_t cmp1 = vcltq_f32(vld1q_f32(aAcc+4), vMin); + uint32x4_t cmp2 = vcltq_f32(vld1q_f32(aAcc+8), vMin); + uint32x4_t cmp3 = vcltq_f32(vld1q_f32(aAcc+12), vMin); + uint32x4_t mask = vorrq_u32(vorrq_u32(cmp0, cmp1), vorrq_u32(cmp2, cmp3)); + return vmaxvq_u32(mask)!=0; +#else + int ii; + for(ii=0; ii<16; ii++){ + if( aAcc[ii]bStreaming==0 ){ + iEarly = (nCodebook/4); + } + + assert( nNorm==sizeof_f32 || nNorm==0 ); + + /* One iteration of this loop for each block in the blob. */ + for(iOff=VEC1_LIST_SZHDR + nEntry*szRowid; + iOff=iEarly && (iBook & 0x03)==0x03 + && vec1AnyLessThan(aAcc, pHeap->fMin)==0 + ){ + goto skip; + } + } + + if( nNorm ){ + int jj; + for(jj=0; jjfMin + && (aMask==0 || (aMask[(iBlk*VEC1_PQ_BLOCKSIZE+ii)/8] & (1 << (ii%8)))==0) + ){ + i64 iRowid = 0; + if( szRowid==4 ){ + iRowid = vec1GetU32(&aRowid[ii * szRowid]); + if( iRowid==VEC1_TOMBSTONE_32 ) continue; + }else{ + iRowid = vec1GetU64(&aRowid[ii * szRowid]); + if( iRowid==VEC1_TOMBSTONE_64 ) continue; + } + vec1HeapInsert(pHeap, iRowid, dist); + } + } + + skip:; /* semi-colon required to avoid C23 warning/error */ + } +} + +static int vec1DoKANNBucket( + Vec1Query *pQuery, + int iBucket, + Vec1Buffer *pDist, + Vec1Buffer *pMeta, + Vec1Buffer *pBitmask +){ + Vec1Tab *pTab = pQuery->pTab; + Vec1AnnHeap *pHeap = &pQuery->heap; + + const int nElem = pTab->cfg.nElem; + const int bUseBQ = (pTab->mod.hdr.flags & VEC1_MODEL_BITQUANT) ? 1 : 0; + const int nCodebook = (int)pTab->mod.hdr.nCodebook; + const int bUsePQ = (nCodebook>0 && bUseBQ==0); + + sqlite3_stmt *pScanIdx = 0; + int rc = SQLITE_OK; + float *aDist = 0; + float fResidualNorm2 = 0.0; + + if( iBucket<0 ){ + rc = vec1GetSql(pTab, VEC1_SQL_SCAN_IDX, &pScanIdx); + }else{ + rc = vec1GetSql(pTab, VEC1_SCAN_BUCKET, &pScanIdx); + if( rc==SQLITE_OK ){ + sqlite3_bind_int(pScanIdx, 1, iBucket); + } + } + + assert( 0==pDist->n || 0==(pTab->mod.hdr.flags & VEC1_MODEL_RESIDUAL) ); + if( (bUsePQ || bUseBQ) ){ + /* If using PQ or BQ codes, build a LUT for this bucket. */ + if( rc==SQLITE_OK && pDist->n==0 ){ + const float *aEncode = pQuery->aTransform; + int nByte = (bUsePQ ? + vec1PQLUTSize(nCodebook) : /* Product quant */ + vec1BinaryQuantSize(nElem) /* Binary quant */ + ); + + rc = vec1BufferSize(pDist, nByte); + if( rc!=SQLITE_OK ) return rc; + aDist = (float*)pDist->a; + + VEC1_QINSTR_START(pTab, VEC1_QINSTR_LUT); + if( pTab->mod.hdr.flags & VEC1_MODEL_RESIDUAL ){ + float *aTmp = pTab->aTmpVec; + assert( iBucket>=0 && iBucket<(int)pTab->mod.hdr.nBucket ); + vec1Sub(aTmp, aEncode, &pTab->mod.aCentroid[iBucket*nElem], nElem); + aEncode = aTmp; + } + if( bUsePQ ){ + vec1PQLUTBuild(pTab, aEncode, aDist); + }else{ + vec1BinaryQuantLUTBuild(pTab, aEncode, aDist); + fResidualNorm2 = vec1VectorNorm2(aEncode, pTab->cfg.nElem); + vec1EncodeVectorBit(&pTab->mod, aEncode, pQuery->aBit); + } + VEC1_QINSTR_STOP(pTab, VEC1_QINSTR_LUT); + pDist->n = nByte; + } + aDist = (float*)pDist->a; + } + + while( rc==SQLITE_OK ){ + i64 iId = 0; + int res; + + /* Load the next index entry from the %_idx table */ + VEC1_QINSTR_START(pTab, VEC1_QINSTR_IDXREAD); + res = sqlite3_step(pScanIdx); + VEC1_QINSTR_STOP(pTab, VEC1_QINSTR_IDXREAD); + if( res!=SQLITE_ROW ) break; + + const u8 *aBlob = (const u8*)sqlite3_column_blob(pScanIdx, 0); + int nBlob = sqlite3_column_bytes(pScanIdx, 0); + int nEntry = 0; + int flags = 0; + int ii; + int szRowid = 4; + + if( vec1CheckIdxSize(pTab, aBlob, nBlob) ){ + rc = VEC1_CORRUPT; + break; + } + + nEntry = vec1GetU32(&aBlob[4]); + iId = sqlite3_column_int64(pScanIdx, 2); + if( pQuery->nFilter>0 ){ + rc = vec1DoMetaFilters(pTab, pQuery, iId, nEntry, pMeta, pBitmask); + if( rc!=SQLITE_OK ) break; + } + + /* Loop through each of the PQ vectors in aBlob[] */ + VEC1_QINSTR_START(pTab, VEC1_QINSTR_PQSCAN); + flags = vec1GetU32(&aBlob[0]); + if( flags & VEC1_LIST_64BIT ) szRowid = 8; + if( aDist ){ + vec1ScanBlocked( + pHeap, pBitmask->a, aDist, nCodebook, aBlob, nBlob, szRowid, + nEntry, (bUseBQ ? sizeof_f32 : 0), fResidualNorm2 + ); + }else{ + const int szVec = sizeof_f32*nElem; + assert( bUseBQ==0 && bUsePQ==0 ); + for(ii=0; iin>0 && (pBitmask->a[ii/8] & (1 << (ii % 8))) ){ + continue; + } + + if( szRowid==4 ){ + iRowid = vec1GetU32(&aBlob[iRowidOff]); + if( iRowid==VEC1_TOMBSTONE_32 ) continue; + }else{ + iRowid = vec1GetU64(&aBlob[iRowidOff]); + if( iRowid==VEC1_TOMBSTONE_64 ) continue; + } + + if( pTab->mod.hdr.eDistance==VEC1_DISTANCE_COS ){ + fDist = vec1CosDist(pQuery->aVector, aFull, nElem); + }else{ + fDist = vec1L2Dist(pQuery->aVector, aFull, nElem); + } + + vec1HeapInsert(pHeap, iRowid, fDist); + } + } + VEC1_QINSTR_STOP(pTab, VEC1_QINSTR_PQSCAN); + } + + vec1StmtReset(&rc, pScanIdx); + if( rc==SQLITE_OK ) rc = pHeap->rc; + return rc; +} + +static int vec1DoKANNQuery(Vec1Csr *pCsr){ + int rc = SQLITE_OK; + Vec1Tab *pTab = (Vec1Tab*)(pCsr->base.pVtab); + Vec1Query *pQuery = pCsr->pQuery; + Vec1AnnHeap *pHeap = &pQuery->heap; + + /* Table of distances used for ANN queries against the PQ quantized + ** vectors in the %_idx table. This is an array of (nCodebook * 256) + ** float values. The 256 distances for each codebook are stored + ** contiguously, so to lookup code iCode in codebook iBook: + ** + ** aDist[(iBook * 256) + iCode] + */ + + Vec1BucketResult *aBucket = 0; + const float *aVector = pQuery->aVector; + int iProbe = 0; + + Vec1Buffer meta = {0, 0, 0}; /* Buffer to load meta arrays */ + Vec1Buffer bitmask = {0, 0, 0}; /* Buffer for meta filter bitmask */ + Vec1Buffer dist = {0, 0, 0}; /* Buffer for LUT */ + + VEC1_QINSTR_START(pTab, VEC1_QINSTR_INITQUERY); + + /* Transform the input vector if required. If no transformation is + ** required, then pQuery->aTransform already points to the same buffer + ** as pQuery->aVector. */ + assert( vec1TransformRequired(&pTab->mod)==(pQuery->aTransform!=aVector) ); + vec1TransformInputVector(&pTab->mod, pQuery->aTransform, aVector); + + rc = vec1HeapInit(pHeap, pQuery->K, pQuery->bStreaming); + + VEC1_QINSTR_STOP(pTab, VEC1_QINSTR_INITQUERY); + + /* Do the coarse quantizer step. If nbucket is not 0, this block allocates + ** the aBucket[] array. The first nProbe elements of which are the ones + ** that should be scanned by this call. The others may be used later if + ** this is a streaming query, but are usually ignored. */ + VEC1_QINSTR_START(pTab, VEC1_QINSTR_COARSE); + if( rc==SQLITE_OK ){ + if( pTab->mod.hdr.nBucket==0 ){ + assert( pQuery->nProbe==1 ); + }else{ + assert( pQuery->nProbe>=1 && pQuery->nProbe<=(int)pTab->mod.hdr.nBucket ); + rc = vec1FindBuckets(pTab, pQuery->aTransform, pQuery->nProbe, &aBucket); + } + } + VEC1_QINSTR_STOP(pTab, VEC1_QINSTR_COARSE); + + for(iProbe=0; iProbenProbe && rc==SQLITE_OK; iProbe++){ + int iBucket = -1; + if( pTab->mod.hdr.nBucket>0 ){ + iBucket = aBucket[iProbe].iBucket; + if( pQuery->nProbeSlack>0.0 + && aBucket[iProbe].fDist>aBucket[0].fDist*(1.0+pQuery->nProbeSlack) + ){ + break; + } + } + if( pTab->mod.hdr.flags & VEC1_MODEL_RESIDUAL ){ + dist.n = 0; + } + rc = vec1DoKANNBucket(pQuery, iBucket, &dist, &meta, &bitmask); + } + + VEC1_QINSTR_START(pTab, VEC1_QINSTR_FINALSORT); + + if( rc==SQLITE_OK ){ + i64 nSort = MIN( pHeap->nRes, pQuery->K ); + + if( pQuery->bStreaming ){ + /* For a streaming query, set the pQuery->aBucket[] array to contain + ** the remaining buckets - the ones that have not yet been scanned. */ + pQuery->nBucket = pTab->mod.hdr.nBucket - iProbe; + if( pQuery->nBucket>0 ){ + int nCopy = sizeof(Vec1BucketResult) * pQuery->nBucket; + memmove(aBucket, &aBucket[iProbe], nCopy); + pQuery->aBucket = aBucket; + aBucket = 0; + } + + pQuery->nOrigRes = (int)MAX(16, pHeap->nRes); + } + + /* For a streaming query, all candidate vectors were added to the + ** aRes[] array. The heap has guaranteed that the first nSort elements + ** of the Vec1AnnHeap.aRes[] array have distances <= than the remaining + ** elements. So it is still correct just to sort the first nSort + ** elements of aRes[] and set nCurrentRes=nSort to configure the first + ** block of rows to return. */ + rc = vec1AnnResultSort(pHeap->aRes, nSort); + pCsr->nCurrentRes = (int)nSort; + } + sqlite3_free(aBucket); + vec1BufferFree(&meta); + vec1BufferFree(&bitmask); + vec1BufferFree(&dist); + VEC1_QINSTR_STOP(pTab, VEC1_QINSTR_FINALSORT); + + return rc; +} + +/* +** Load the contents of the %_config table into the pTab->cfg structure. +** if the iModelVersion field has changed since %_config was last loaded, +** also load the model from the %_model table. +*/ +static int vec1LoadConfig(Vec1Tab *pTab){ + sqlite3_stmt *pStmt = 0; + int rc = SQLITE_OK; + int nReq = 0; + + pTab->cfg.nBlocksize = VEC1_BLOCKSIZE_DEFAULT; + pTab->cfg.nBlocksizeMin = VEC1_BLOCKSIZE_MIN_DEFAULT; + pTab->cfg.nElem = 0; + + rc = vec1GetSql(pTab, VEC1_SQL_LOAD_CONFIG, &pStmt); + if( rc==SQLITE_OK ){ + while( sqlite3_step(pStmt)==SQLITE_ROW ){ + int iField = sqlite3_column_int(pStmt, 0); + switch( iField ){ + case VEC1_CONFIG_MODEL: { + i64 iNew = sqlite3_column_int64(pStmt, 1); + if( pTab->cfg.iModelVersion!=iNew ){ + rc = vec1LoadModel(pTab); + if( rc==SQLITE_OK ){ + pTab->cfg.iModelVersion = iNew; + } + } + break; + } + + case VEC1_CONFIG_BLOCKSIZE: { + pTab->cfg.nBlocksize = sqlite3_column_int(pStmt, 1); + pTab->cfg.nBlocksize = MAX(VEC1_BLOCKSIZE_MIN, pTab->cfg.nBlocksize); + pTab->cfg.nBlocksize = MIN(VEC1_BLOCKSIZE_MAX, pTab->cfg.nBlocksize); + break; + } + + case VEC1_CONFIG_BLOCKSIZE_MIN: { + int nMin = sqlite3_column_int(pStmt, 1); + nMin = MAX(VEC1_BLOCKSIZE_MIN, nMin); + nMin = MIN(VEC1_BLOCKSIZE_MAX, nMin); + pTab->cfg.nBlocksizeMin = nMin; + break; + } + + case VEC1_CONFIG_NELEM: { + pTab->cfg.nElem = sqlite3_column_int(pStmt, 1); + break; + } + + case VEC1_CONFIG_FMTVERSION: { + int iVersion = sqlite3_column_int(pStmt, 1); + if( iVersionVEC1_CURRENT_FMTVERSION ){ + vec1VtabError(pTab, + "unsupported index version - requires vec1 %d.%d", + (iVersion / 1000), (iVersion % 1000) + ); + rc = SQLITE_ERROR; + } + break; + } + + default: { + vec1VtabError(pTab, "unrecognized config entry: %d", iField); + rc = SQLITE_ERROR; + break; + } + } + } + + vec1StmtReset(&rc, pStmt); + } + + nReq = MAX(pTab->mod.hdr.nElem, pTab->mod.nCodeElem*pTab->mod.hdr.nCodebook); + if( (rc==SQLITE_OK) && nReq>0 && nReq!=pTab->nTmpVec ){ + sqlite3_free(pTab->aTmpVec); + pTab->aTmpVec = (float*)vec1MallocZero(nReq * sizeof_f32); + if( pTab->aTmpVec ){ + pTab->nTmpVec = nReq; + }else{ + pTab->nTmpVec = 0; + rc = SQLITE_NOMEM; + } + } + + if( rc==SQLITE_OK + && pTab->mod.hdr.nElem!=0 + && (u32)pTab->cfg.nElem!=pTab->mod.hdr.nElem + ){ + vec1VtabError(pTab, "model/data vector size mismatch: %d/%d", + pTab->mod.hdr.nElem, pTab->cfg.nElem + ); + vec1UnloadModel(pTab); + rc = SQLITE_ERROR; + } + + return rc; +} + +/* +** Load the configuration if it is not already loaded. The configuration +** is assumed not to be loaded if cfg.nElem==0. +** +** Return SQLITE_OK if successful, or an SQLite error code otherwise. If +** an error occurs, this function may leave an error message in the virtual +** table object. +*/ +static int vec1LoadConfigIfRequired(Vec1Tab *pTab){ + int rc = SQLITE_OK; + if( pTab->cfg.nElem==0 ){ + rc = vec1LoadConfig(pTab); + if( rc!=SQLITE_OK ){ + pTab->cfg.nElem = 0; + } + } + return rc; +} + +static int vec1InterpretNProbe(Vec1Tab *pTab, double fVal){ + int nProbe; + if( fVal>=1.0 ){ + nProbe = (int)fVal; + }else{ + nProbe = (int)(fVal * pTab->mod.hdr.nBucket); + } + nProbe = MIN(nProbe, (int)pTab->mod.hdr.nBucket); + nProbe = MAX(nProbe, 1); + return nProbe; +} + +static int vec1ParseQueryParamCb( + void *pCtx, + const char *zOpt, + int eType, + i64 iVal, + double fVal, + const char *zVal, + char **pzErr +){ + Vec1Query *pQuery = (Vec1Query*)pCtx; + const char *azOpt[] = { + "nprobe", /* 0 */ + "K", /* 1 */ + "streaming", /* 2 */ + "nprobe_slack", /* 3 */ + 0 + }; + int iOpt = 0; + + UNUSED_PARAMETER2(iVal, eType); + UNUSED_PARAMETER2(pzErr, zVal); + + int rc = vec1ConfigEnum(&iOpt, zOpt, azOpt, "option", pzErr); + if( rc==SQLITE_OK ){ + switch( iOpt ){ + case 0: assert( sqlite3_stricmp("nprobe", zOpt)==0 ); + if( fVal<=0.0 ){ + *pzErr = sqlite3_mprintf( + "vec1: nprobe must be a numeric value greater than 0.0" + ); + rc = SQLITE_ERROR; + } + pQuery->nProbe = vec1InterpretNProbe(pQuery->pTab, fVal); + break; + + case 1: assert( sqlite3_stricmp("K", zOpt)==0 ); + if( eType==SQLITE_INTEGER ){ + pQuery->K = iVal; + }else{ + pQuery->K = -1; + } + break; + + case 2: assert( sqlite3_stricmp("streaming", zOpt)==0 ); + pQuery->bStreaming = ((int)fVal) ? 1 : 0; + break; + + default: assert( sqlite3_stricmp("nprobe_slack", zOpt)==0 ); + pQuery->nProbeSlack = fVal; + break; + } + } + + return rc; +} + +static int vec1ModelIsFlat(Vec1Model *pMod){ + return ((pMod->hdr.flags & VEC1_MODEL_INDEX) && pMod->hdr.nCodebook==0); +} + +static const char aHexDigit[] = { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', +}; + +static int vec1DecodeMetaIdx(const char *z){ + int iRet = 0; + assert( (z[0]>='0' && z[0]<='9') || (z[0]>='a' && z[0]<='f') ); + assert( (z[1]>='0' && z[1]<='9') || (z[1]>='a' && z[1]<='f') ); + + if( z[0]<='9' ){ + iRet = (int)(z[0] - '0'); + }else{ + iRet = (int)(10 + z[0] - 'a'); + } + iRet = iRet << 4; + if( z[1]<='9' ){ + iRet |= (int)(z[1] - '0'); + }else{ + iRet |= (int)(10 + z[1] - 'a'); + } + return iRet; +} + +/* +** This is invoked when a value of type eType (which is not SQLITE_BLOB) +** has been passed to vec1 as a vector. This is an error. Configure the +** table with an error message and return SQLITE_ERROR. +*/ +static int vec1VectorTypeError(Vec1Tab *pTab, int eType){ + const char *azType[] = {0, "INTEGER", "FLOAT", "TEXT", "BLOB", "NULL"}; + assert( eType!=SQLITE_BLOB && eType>=1 && eType<=5 ); + vec1VtabError(pTab, + "vec1: unexpected vector type %s, expected BLOB", azType[eType] + ); + return SQLITE_ERROR; +} + +/* +** This is invoked when a blob nHave bytes in size has been passed to vec1 +** as a vector when a (pTab->cfg.nElem*sizeof_f32) blob was expected. +** Configure the table with an error message and return SQLITE_ERROR. +*/ +static int vec1VectorSizeError(Vec1Tab *pTab, int nHave){ + assert( pTab->cfg.nElem>0 && (pTab->cfg.nElem*sizeof_f32)!=nHave ); + vec1VtabError(pTab, + "vec1: unexpected vector blob size %d bytes, expected %d", + nHave, pTab->cfg.nElem*sizeof_f32 + ); + return SQLITE_ERROR; +} + +static int vec1FilterArraySize( + const char *idxStr, + sqlite3_value **argv, + int *pnFilter +){ + int ii; + int nFilter = 0; + int iArg = 1; + int rc = SQLITE_OK; + + for(ii=0; idxStr[ii]; ii++){ + char c = idxStr[ii]; + if( c==VEC1_OP_IN ){ + sqlite3_value *pDummy = 0; + sqlite3_value *pList = argv[iArg]; + nFilter++; + for(rc=sqlite3_vtab_in_first(pList, &pDummy); + rc==SQLITE_OK; + rc=sqlite3_vtab_in_next(pList, &pDummy) + ){ + assert( pDummy!=0 ); + nFilter++; + } + if( rc==SQLITE_DONE ) rc = SQLITE_OK; + ii += 2; + }else if( c!=VEC1_OP_PARAMS && c!=VEC1_OP_LIMIT ){ + ii += 2; + nFilter++; + } + iArg++; + } + + *pnFilter = nFilter; + return rc; +} + +static void *vec1Dup(int *pRc, const void *p, int n){ + void *pRet = 0; + assert( *pRc==SQLITE_OK ); + if( p==0 ){ + *pRc = SQLITE_NOMEM; + }else{ + pRet = vec1MallocZero(n+1); + if( pRet==0 ){ + *pRc = SQLITE_NOMEM; + }else if( n>0 ){ + memcpy(pRet, p, n); + } + } + return pRet; +} + +static void vec1ValueToFilter( + int *pRc, + sqlite3_value *pVal, + Vec1Filter *pFilter +){ + pFilter->eType = sqlite3_value_type(pVal); + switch( pFilter->eType ){ + case SQLITE_NULL: + break; + case SQLITE_INTEGER: + pFilter->iVal = sqlite3_value_int64(pVal); + break; + case SQLITE_FLOAT: + pFilter->fVal = sqlite3_value_double(pVal); + break; + case SQLITE_TEXT: + pFilter->iVal = sqlite3_value_bytes(pVal); + pFilter->pPtr = (u8*)vec1Dup( + pRc, sqlite3_value_text(pVal), pFilter->iVal + ); + break; + default: { + int nByte = sqlite3_value_bytes(pVal); + assert( pFilter->eType==SQLITE_BLOB ); + pFilter->iVal = (i64)nByte; + pFilter->pPtr = (u8*)vec1Dup( + pRc, nByte? sqlite3_value_blob(pVal) : (const void*)"", nByte + ); + break; + } + } +} + +/* +** Return true if value pVal contains an integer - either an SQL integer, +** text that looks like an integer, or a double that can be converted +** to an integer without loss of data. +*/ +static int vecIsIntegerValue(sqlite3_value *pVal){ + if( sqlite3_value_numeric_type(pVal)==SQLITE_INTEGER ) return 1; + if( sqlite3_value_numeric_type(pVal)==SQLITE_FLOAT ){ + double fVal = sqlite3_value_double(pVal); + if( fVal==(double)((i64)fVal) ) return 1; + } + return 0; +} + +static int vec1SetupKANNQuery( + Vec1Tab *pTab, + const char *idxStr, /* idxStr passed to xFilter */ + sqlite3_value **argv, /* Parameter values passed to xFilter */ + Vec1Query **ppOut /* OUT: Query object */ +){ + const int nElem = pTab->cfg.nElem; + const int nSpace = MAX(pTab->cfg.nElem, pTab->nTmpVec); + int nMax = 0; /* Max possible meta-data filters */ + int nVec = sqlite3_value_bytes(argv[0]); + const u8 *aVec = sqlite3_value_blob(argv[0]); + + int iArg = 1; + int iIdxStr = 0; + int rc = SQLITE_OK; + Vec1Query *p = 0; + + int bTransform = vec1TransformRequired(&pTab->mod); + int nBit = 0; + + if( pTab->mod.hdr.flags & VEC1_MODEL_BITQUANT ){ + nBit = sizeof_f32 + pTab->mod.hdr.nCodebook; + } + + /* Determine the required size of the Vec1Query.aFilter[] array. */ + rc = vec1FilterArraySize(idxStr, argv, &nMax); + + p = (Vec1Query*)vec1MallocZero( + sizeof(Vec1Query) /* Vec1Query object itself */ + + nMax * sizeof(Vec1Filter) /* Vec1Query.aFilter[] */ + + nSpace * sizeof_f32 /* Vec1Query.aVector[] */ + + bTransform * (nSpace * sizeof_f32) /* aTransform[] */ + + nBit /* aBit[] */ + ); + if( p==0 ) return SQLITE_NOMEM; + p->aFilter = (Vec1Filter*)&p[1]; + p->aVector = (float*)&p->aFilter[nMax]; + if( bTransform ){ + p->aTransform = &p->aVector[nSpace]; + }else{ + p->aTransform = p->aVector; + } + if( nBit ){ + p->aBit = (u8*)&p->aTransform[nSpace]; + } + + /* Initialize default query parameters. + ** + ** Use VEC1_SMALLEST_INT64 to signify that no K value has been supplied + ** If somebody specifies this value via a JSON query parameter, they + ** may get a slightly inaccurate error message. So be it. */ + p->pTab = pTab; + p->K = VEC1_SMALLEST_INT64; + p->nProbe = vec1InterpretNProbe(pTab, pTab->pTabList->nProbeArg); + p->bStreaming = 0; + + /* Check the query vector is the right size and copy it into the + ** aVector[] buffer. + ** + ** Omit this check if nElem==0. This occurs when the table has not yet + ** received the first vector. So carry on checking the remainder of the + ** parameters, but don't worry about checking the input vector or + ** copying it into aVector[], as the query will return zero rows in + ** any case. */ + if( nElem!=0 ){ + if( nVec!=(nElem*sizeof_f32) ){ + rc = vec1VectorSizeError(pTab, nVec); + }else{ + memcpy(p->aVector, aVec, nVec); + } + } + + /* Check that the query vector does not contain any NaN or Infinity values */ + if( rc==SQLITE_OK ){ + rc = vec1VectorIsNonFinite((float*)aVec, (nVec/sizeof_f32)); + if( rc!=SQLITE_OK ){ + vec1VtabError(pTab, "vec1: non-finite value not allowed in vector"); + } + } + + /* Check if parameters where supplied via hidden column "arg". If so, + ** the first character of idxStr is 'P' - parameters. */ + if( rc==SQLITE_OK && idxStr[0]==VEC1_OP_PARAMS ){ + sqlite3_value *pVal = argv[iArg++]; + if( vecIsIntegerValue(pVal) ){ + p->K = sqlite3_value_int64(pVal); + }else if( sqlite3_value_type(pVal)==SQLITE_TEXT ){ + assert( pTab->base.zErrMsg==0 ); + rc = vec1ParseJsonConfig(pTab->db, + (const char*)sqlite3_value_text(pVal), vec1ParseQueryParamCb, + (void*)p, &pTab->base.zErrMsg + ); + }else{ + p->K = -1; + } + iIdxStr = 1; + } + + /* Process remaining constraints, if any. */ + if( rc==SQLITE_OK && idxStr[iIdxStr]!='\0' ){ + char c; + while( (c = idxStr[iIdxStr++])!='\0' ){ + sqlite3_value *pVal = argv[iArg++]; + if( c==VEC1_OP_LIMIT ){ + int iVal = sqlite3_value_int(pVal); + if( iVal>=0 && (p->K==VEC1_SMALLEST_INT64 || p->K>0) ){ + /* A non-negative LIMIT clause. If no K value was specified, use + ** this value as K. Or, if a K value was specified, use the + ** minimum of K and this value. */ + p->K = ((p->K < 0) ? iVal : MIN(p->K, iVal)); + } + }else{ + /* Filter on a meta value. The operand character is followed by + ** 2 hexadecimal digits encoding the index of the meta column + ** on the LHS of the constraint. */ + Vec1Filter *pFilter = &p->aFilter[p->nFilter++]; + pFilter->vop = c; + pFilter->iMeta = vec1DecodeMetaIdx(&idxStr[iIdxStr]); + iIdxStr += 2; + if( c==VEC1_OP_IN ){ + sqlite3_value *pInVal = 0; + assert( pFilter->iVal==0 ); + for(rc=sqlite3_vtab_in_first(pVal, &pInVal); + rc==SQLITE_OK; + rc=sqlite3_vtab_in_next(pVal, &pInVal) + ){ + Vec1Filter *pInFilter = &p->aFilter[p->nFilter++]; + vec1ValueToFilter(&rc, pInVal, pInFilter); + if( rc!=SQLITE_OK ) break; + pInFilter->vop = VEC1_OP_EQ; + pInFilter->iMeta = pFilter->iMeta; + pFilter->iVal++; + } + if( rc==SQLITE_DONE ) rc = SQLITE_OK; + + }else if( c==VEC1_OP_NOTNULL ){ + pFilter->eType = SQLITE_BLOB; + pFilter->iVal = 0; + }else if( c==VEC1_OP_ISNULL ){ + pFilter->eType = SQLITE_NULL; + pFilter->vop = VEC1_OP_IS; + + }else{ + vec1ValueToFilter(&rc, pVal, pFilter); + } + } + } + } + assert( rc!=SQLITE_OK || p->nFilter==nMax ); + + /* Check that a usable K value has been specified. Either explicitly or + ** via a visible LIMIT clause. */ + if( rc==SQLITE_OK ){ + if( p->K==VEC1_SMALLEST_INT64 ){ + vec1VtabError(pTab, "vec1: no K value or visible LIMIT clause"); + rc = SQLITE_ERROR; + }else if( p->K<0 || p->K>VEC1_MAX_K ){ + vec1VtabError(pTab, + "vec1: K must be an integer value between 0 and %d", VEC1_MAX_K + ); + rc = SQLITE_ERROR; + } + } + + /* If an error occurred, free any dynamic allocations and zero the output + ** structure before returning. Otherwise, sort the aFilter[] array by + ** iMeta value. */ + if( rc!=SQLITE_OK ){ + vec1QueryFree(p); + p = 0; + }else if( 0 ){ + /* Sort aFilter[] by Vec1Filter.iMeta value. The sort must be stable, + ** in order to avoid disrupting the arguments to a VEC1_OP_IN filter. */ + int i1, i2; + for(i1=0; i1nFilter; i1++){ + Vec1Filter tmp = p->aFilter[i1]; + for(i2=i1-1; i2>=0 && p->aFilter[i2].iMeta>tmp.iMeta; i2--){ + p->aFilter[i2+1] = p->aFilter[i2]; + } + p->aFilter[i2+1] = tmp; + } + +#ifndef NDEBUG + /* Assert() that the sort worked. */ + for(i1=1; i1nFilter; i1++){ + assert( p->aFilter[i1-1].iMeta<=p->aFilter[i1].iMeta ); + } +#endif + } + + *ppOut = p; + return rc; +} + +/* +** This function is called in index:"none" mode. +*/ +static int vec1QueryToSql(Vec1Csr *pCsr){ + Vec1Tab *pTab = (Vec1Tab*)(pCsr->base.pVtab); + Vec1Query *pQuery = pCsr->pQuery; + int ii; + int rc = SQLITE_OK; + char *zWhere = 0; + + int iNextVar = 1; + + sqlite3_str *pWhere = sqlite3_str_new(pTab->db); + const char *zAnd = ""; + + for(ii=0; iinFilter; ii++){ + Vec1Filter *p = &pQuery->aFilter[ii]; + int bRhs = 1; + const char *zOp = ""; + if( p->vop==VEC1_OP_IN ){ + const char *zComma = ""; + int jj; + sqlite3_str_appendf(pWhere, "%sc%d IN(?%d", zAnd, p->iMeta, iNextVar++); + for(jj=2; jj<=p->iVal; jj++){ + sqlite3_str_appendf(pWhere, ", ?%d", iNextVar++); + } + ii += p->iVal; + sqlite3_str_appendf(pWhere, ")"); + }else{ + switch( p->vop ){ + case VEC1_OP_EQ: zOp = "="; break; + case VEC1_OP_LT: zOp = "<"; break; + case VEC1_OP_GT: zOp = ">"; break; + case VEC1_OP_LE: zOp = "<="; break; + case VEC1_OP_GE: zOp = ">="; break; + case VEC1_OP_IS: zOp = "IS"; break; + default: + assert( p->vop==VEC1_OP_NOTNULL ); + zOp = "IS NOT NULL"; + bRhs = 0; + break; + } + + sqlite3_str_appendf(pWhere, "%sc%d %s", zAnd, p->iMeta, zOp); + + if( bRhs ){ + sqlite3_str_appendf(pWhere, " ?%d", iNextVar++); + } + } + zAnd = " AND "; + } + + rc = sqlite3_str_errcode(pWhere); + zWhere = sqlite3_str_finish(pWhere); + + if( rc==SQLITE_OK ){ + const char *zDistance = "vec1_l2_distance"; + if( pTab->mod.hdr.eDistance==VEC1_DISTANCE_COS ){ + zDistance = "vec1_cos_distance"; + } + + rc = vec1PrepareSql(pTab, &pCsr->pStmt, + "SELECT *, %s(?%d, vector) AS d FROM %Q.'%q_base'%s%s " + "ORDER BY d LIMIT %lld", + zDistance, iNextVar, + pTab->zDb, pTab->zName, + zWhere ? " WHERE " : "", zWhere, + pQuery->K + ); + } + if( rc==SQLITE_OK ){ + int n = pTab->cfg.nElem * sizeof_f32; + rc = sqlite3_bind_blob( + pCsr->pStmt, iNextVar, pQuery->aVector, n, SQLITE_TRANSIENT + ); + iNextVar = 1; + for(ii=0; rc==SQLITE_OK && iinFilter; ii++){ + Vec1Filter *p = &pQuery->aFilter[ii]; + if( p->vop!=VEC1_OP_IN && p->vop!=VEC1_OP_NOTNULL ){ + switch( p->eType ){ + case SQLITE_NULL: + break; + case SQLITE_INTEGER: + sqlite3_bind_int64(pCsr->pStmt, iNextVar, p->iVal); + break; + case SQLITE_FLOAT: + sqlite3_bind_double(pCsr->pStmt, iNextVar, p->fVal); + break; + case SQLITE_TEXT: + rc = sqlite3_bind_text(pCsr->pStmt, + iNextVar, (const char*)p->pPtr, p->iVal, SQLITE_TRANSIENT + ); + break; + default: + assert( p->eType==SQLITE_BLOB ); + rc = sqlite3_bind_blob( + pCsr->pStmt, iNextVar, p->pPtr, p->iVal, SQLITE_TRANSIENT + ); + break; + } + + iNextVar++; + } + } + } + + vec1QueryFree(pQuery); + pCsr->pQuery = 0; + pCsr->bSqlNNQuery = 1; + sqlite3_free(zWhere); + return rc; +} + +/* +** There are three basic query plans supported by xBestIndex: +** +** 0: Full table scan. +** 1: Lookup by rowid. +** 2: K-nearest-neighbours search. +*/ +static int vec1FilterMethod( + sqlite3_vtab_cursor *cur, + int idxNum, + const char *idxStr, + int argc, + sqlite3_value **argv +){ + Vec1Csr *pCsr = (Vec1Csr *)cur; + Vec1Tab *pTab = (Vec1Tab*)(cur->pVtab); + int rc = SQLITE_OK; + + UNUSED_PARAMETER(argc); + assert( ((idxNum==0 || idxNum==1) && argc==0) + || ((idxNum==2 || idxNum==3) && argc==1) + || (idxNum==5 && argc>=1) + ); + vec1ResetCsr(pCsr); + + if( pCsr->bLoadConfig==0 && (idxNum & 0x01) ){ + rc = vec1LoadConfig(pTab); + if( rc!=SQLITE_OK ) return rc; + pCsr->bLoadConfig = 1; + } + + if( idxNum==0 || idxNum==1 ){ + /* Full-table scan. */ + rc = vec1PrepareSql(pTab, &pCsr->pStmt, + "SELECT * FROM %Q.'%q_base'", pTab->zDb, pTab->zName + ); + }else if( idxNum==2 || idxNum==3 ){ + /* Lookup by rowid */ + rc = vec1PrepareSql(pTab, &pCsr->pStmt, + "SELECT * FROM %Q.'%q_base' WHERE rowid=?", + pTab->zDb, pTab->zName + ); + if( rc==SQLITE_OK ){ + sqlite3_bind_value(pCsr->pStmt, 1, argv[0]); + } + }else{ + /* ANN query */ + Vec1Query *pQuery = 0; + + assert( idxNum==5 ); + + VEC1_QINSTR_START(pTab, VEC1_QINSTR_TOTAL); + + /* Parse the query parameters. */ + rc = vec1SetupKANNQuery(pTab, idxStr, argv, &pQuery); + pCsr->pQuery = pQuery; + + if( rc==SQLITE_OK ){ + if( (pTab->mod.hdr.flags & VEC1_MODEL_INDEX)==0 ){ + rc = vec1QueryToSql(pCsr); + assert( pCsr->pQuery==0 ); + assert( pCsr->bSqlNNQuery ); + }else{ + rc = vec1PrepareSql(pTab, &pCsr->pStmt, + "SELECT * FROM %Q.'%q_base' WHERE id=?", + pTab->zDb, pTab->zName + ); + if( rc==SQLITE_OK ){ + rc = vec1DoKANNQuery(pCsr); + pCsr->iCurrentRes = -1; + } + } + } + + VEC1_QINSTR_STOP(pTab, VEC1_QINSTR_TOTAL); + } + + if( rc==SQLITE_OK ){ + rc = vec1NextMethod(cur); + } + return rc; +} + +static int vec1EofMethod(sqlite3_vtab_cursor *cur){ + Vec1Csr *pCur = (Vec1Csr *)cur; + return (pCur->pStmt==0); +} + +static int vec1SeekCsr(Vec1Csr *pCsr){ + int rc = SQLITE_OK; + if( pCsr->pQuery && pCsr->bSeek==0 ){ + i64 iRowid = pCsr->pQuery->heap.aRes[pCsr->iCurrentRes].iRowid; + sqlite3_bind_int64(pCsr->pStmt, 1, iRowid); + if( SQLITE_ROW!=sqlite3_step(pCsr->pStmt) ){ + rc = sqlite3_finalize(pCsr->pStmt); + if( rc==SQLITE_OK ){ + rc = VEC1_CORRUPT; + } + pCsr->pStmt = 0; + } + pCsr->bSeek = 1; + } + return rc; +} + +typedef struct Vec1RowidLocation Vec1RowidLocation; +struct Vec1RowidLocation { + int iEntry; + int nEntry; + int nTombstone; + int szRowid; + + Vec1ListBuilder *pList; + sqlite3_blob *pBlob; + i64 iIdx; +}; + +static int vec1FindByRowid( + Vec1Tab *pTab, + int iBucket, /* Row belongs to this bucket */ + int bWrite, /* Use writable blob-handle */ + i64 iRowid, /* Rowid to locate */ + Vec1RowidLocation *pOut /* OUT: Result */ +){ + int rc = SQLITE_OK; + Vec1Buffer buf = {0, 0, 0}; + sqlite3_stmt *pSearch = 0; /* For looping through index entries */ + sqlite3_blob *pBlob = 0; + + assert( (pTab->mod.hdr.flags & VEC1_MODEL_INDEX) ); + + memset(pOut, 0, sizeof(*pOut)); + + /* Check to see if the index entry for this vector is currently + ** in main memory. */ + if( pTab->pWriter ){ + Vec1ListBuilder *pBld = &pTab->pWriter->aBld[iBucket]; + int ii; + for(ii=0; iibufRowid.n; ii+=pBld->szRowid){ + i64 iRead; + if( pBld->szRowid==4 ){ + iRead = vec1GetU32(&pBld->bufRowid.a[ii]); + }else{ + iRead = vec1GetU64(&pBld->bufRowid.a[ii]); + } + if( iRead==iRowid ){ + pOut->iEntry = (ii / pBld->szRowid); + pOut->nEntry = (pBld->bufRowid.n / pBld->szRowid); + pOut->nTombstone = pBld->nTombstone; + pOut->szRowid = pBld->szRowid; + pOut->pList = pBld; + return SQLITE_OK; + } + } + } + + rc = vec1GetSql(pTab, VEC1_SQL_SEARCH_IDX, &pSearch); + if( rc==SQLITE_OK ){ + sqlite3_bind_int(pSearch, 1, iBucket); + sqlite3_bind_int64(pSearch, 2, iRowid); + } + + while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pSearch) ){ + int nEntry = 0; + u32 nTombstone = 0; + u32 flags = 0; + int szRowid = 4; + i64 iIdx = sqlite3_column_int64(pSearch, 0); + + rc = sqlite3_blob_open( + pTab->db, pTab->zDb, pTab->zIdxTbl, "val", iIdx, bWrite, &pBlob + ); + if( rc==SQLITE_OK ){ + u8 a[VEC1_LIST_SZHDR]; + rc = sqlite3_blob_read(pBlob, a, VEC1_LIST_SZHDR, 0); + flags = vec1GetU32(&a[0]); + nEntry = (int)vec1GetU32(&a[4]); + nTombstone = vec1GetU32(&a[8]); + if( flags & VEC1_LIST_64BIT ) szRowid = 8; + } + + /* Now scan through the array of rowids at the start of the %_idx entry + ** to find the rowid to be deleted. But, do not do this if the rowid + ** happens to have the same value as the tombstone for this %_idx entry. */ + if( (szRowid==4 && iRowid!=VEC1_TOMBSTONE_32) + || (szRowid==8 && iRowid!=VEC1_TOMBSTONE_64) + ){ + if( rc==SQLITE_OK ){ + buf.n = 0; + rc = vec1BufferGrow(&buf, VEC1_LIST_SZHDR+szRowid*nEntry); + } + if( rc==SQLITE_OK ){ + rc = sqlite3_blob_read( + pBlob, &buf.a[VEC1_LIST_SZHDR], nEntry*szRowid, VEC1_LIST_SZHDR + ); + } + + if( rc==SQLITE_OK ){ + int ii; + for(ii=0; iiiEntry = ii; + pOut->nEntry = nEntry; + pOut->nTombstone = nTombstone; + pOut->szRowid = szRowid; + pOut->pBlob = pBlob; + pOut->iIdx = iIdx; + pBlob = 0; + goto findbyrowid_done; + } + } + } + } + (void)sqlite3_blob_close(pBlob); + } + + findbyrowid_done: + vec1StmtReset(&rc, pSearch); + vec1BufferFree(&buf); + return rc; +} + +/* +** Obtain the full vector associated with the current row of cursor pCsr. +*/ +static int vec1GetVector( + Vec1Csr *pCsr, + const u8 **paVec, /* OUT: Pointer to vector buffer */ + int *pnVec, /* OUT: Size of (*paVec) in bytes */ + u8 **pFree /* OUT: Buffer for caller to free (or NULL) */ +){ + int rc = SQLITE_OK; + Vec1Tab *pTab = (Vec1Tab*)(pCsr->base.pVtab); + rc = vec1SeekCsr(pCsr); + if( rc==SQLITE_OK ){ + if( vec1ModelIsFlat(&pTab->mod) ){ + i64 iRowid = sqlite3_column_int64(pCsr->pStmt, 0); + int iBucket = sqlite3_column_int(pCsr->pStmt, 1); + Vec1RowidLocation loc; + rc = vec1FindByRowid(pTab, iBucket, 0, iRowid, &loc); + if( rc==SQLITE_OK ){ + int nVec = sizeof_f32 * pTab->cfg.nElem; + u8 *aVec = (u8*)sqlite3_malloc(nVec); + if( aVec==0 ){ + rc = SQLITE_NOMEM; + }else if( loc.pList ){ + memcpy(aVec, &loc.pList->bufData.a[nVec * loc.iEntry], nVec); + }else if( loc.pBlob ){ + rc = sqlite3_blob_read(loc.pBlob, aVec, nVec, + VEC1_LIST_SZHDR + (loc.nEntry*loc.szRowid) + (loc.iEntry * nVec) + ); + }else{ + sqlite3_free(aVec); + nVec = 0; + aVec = 0; + } + + if( rc!=SQLITE_OK ){ + sqlite3_free(aVec); + *paVec = 0; + *pnVec = 0; + *pFree = 0; + }else{ + *paVec = *pFree = aVec; + *pnVec = nVec; + } + (void)sqlite3_blob_close(loc.pBlob); + } + }else{ + *paVec = sqlite3_column_blob(pCsr->pStmt, 1); + *pnVec = sqlite3_column_bytes(pCsr->pStmt, 1); + *pFree = 0; + } + } + return rc; +} + +/* +** This function quantizes vector aIn[], then dequantizes it and finds +** the squared L2 distance between the original and the round-tripped +** vector. If successful, output parameter (*prError) is set to the +** squared distance value and SQLITE_OK is returned. Or, if an error +** occurs, an SQLite error code (SQLITE_NOMEM) is returned. +*/ +static int vec1QuantizeReconstructionError2( + Vec1Model *pMod, /* Model to use for quantization */ + const float *aIn, /* Input vector */ + char **pzEnc, + double *prError /* OUT: Round trip error */ +){ + const int nElem = (int)pMod->hdr.nElem; + float *aRound = 0; + u8 *aCode = 0; + int nCode = vec1EncodedVectorSize(pMod); + char *zEnc = 0; + int iEnc = 0; + int ii; + + zEnc = (char*)vec1MallocZero( + nElem * 8 + + nElem * sizeof_f32 + /* aRound[] */ + nCode /* aCode[] */ + ); + if( zEnc==0 ){ + return SQLITE_NOMEM; + } + aRound = (float*)&zEnc[nElem * 8]; + aCode = (u8*)&aRound[nElem]; + + vec1EncodeVector(pMod, aIn, aCode); + vec1DecodeVector(pMod, aCode, aRound); + + *prError = vec1L2Dist(aIn, aRound, nElem); + zEnc[iEnc++] = '['; + for(ii=0; ii=100 ) zEnc[iEnc++] = (aCode[ii] / 100) + '0'; + if( aCode[ii]>=10 ) zEnc[iEnc++] = ((aCode[ii] / 10) % 10) + '0'; + zEnc[iEnc++] = (aCode[ii] % 10) + '0'; + } + zEnc[iEnc++] = ']'; + + *pzEnc = zEnc; + return SQLITE_OK; +} + +static void vec1DistanceStats(sqlite3_context *ctx, Vec1Csr *pCsr){ + int rc = SQLITE_OK; + + if( pCsr->zDistance==0 ){ + const float *aVec = 0; + int nVec = 0; + u8 *pFree = 0; + rc = vec1GetVector(pCsr, (const u8**)&aVec, &nVec, &pFree); + if( rc==SQLITE_OK && aVec ){ + Vec1Tab *pTab = (Vec1Tab*)(pCsr->base.pVtab); + int nElem = pTab->cfg.nElem; + Vec1Model *pMod = &pTab->mod; + int iBucket = 0; + double fCoarseError = 0.0; + double fReconError = 0.0; + double fNorm = 0.0; + const float *aTransform = 0; + char *zEnc = 0; + + if( pMod->hdr.nBucket>1 || pMod->hdr.nCodebook>0 ){ + aTransform = vec1TransformInputVector(pMod, pTab->aTmpVec, aVec); + fNorm = vec1VectorNorm2(aTransform, nElem); + } + + /* Find bucket and coarse error if applicable. */ + if( pMod->hdr.nBucket>1 ){ + iBucket = vec1PqBestMatch( + pMod->aCentroid, pMod->hdr.nBucket, aVec, nElem, &fCoarseError + ); + if( aTransform!=pTab->aTmpVec ){ + memcpy(pTab->aTmpVec, aTransform, nElem*sizeof_f32); + aTransform = pTab->aTmpVec; + } + if( pTab->mod.hdr.flags & VEC1_MODEL_RESIDUAL ){ + vec1SubInPlace(pTab->aTmpVec, &pMod->aCentroid[iBucket*nElem], nElem); + } + } + + /* Find reconstruction error if applicable. */ + if( pMod->hdr.nCodebook>0 ){ + rc = vec1QuantizeReconstructionError2( + pMod, aTransform, &zEnc, &fReconError + ); + } + + pCsr->zDistance = vec1MPrintf(&rc, + "{bucket:%d, coarse_error:%f, reconstruction_error:%f, code: %s}", + iBucket, + fNorm==0.0 ? 0.0 : sqrt(fCoarseError/fNorm), + fNorm==0.0 ? 0.0 : sqrt(fReconError/fNorm), + zEnc ? zEnc : "null" + ); + sqlite3_free(zEnc); + } + sqlite3_free(pFree); + } + + if( rc!=SQLITE_OK ){ + sqlite3_result_error_code(ctx, rc); + }else{ + sqlite3_result_text(ctx, pCsr->zDistance, -1, SQLITE_TRANSIENT); + } +} + +static int vec1ColumnMethod( + sqlite3_vtab_cursor *cur, + sqlite3_context *ctx, + int iCol +){ + Vec1Csr *pCsr = (Vec1Csr *)cur; + int rc = SQLITE_OK; + if( iCol==VEC1_COLUMN_DISTANCE ){ + if( pCsr->pQuery ){ + double fDist = pCsr->pQuery->heap.aRes[pCsr->iCurrentRes].fDist; + sqlite3_result_double(ctx, fDist); + }else if( pCsr->bSqlNNQuery ){ + int ii = sqlite3_column_count(pCsr->pStmt)-1; + sqlite3_result_value(ctx, sqlite3_column_value(pCsr->pStmt, ii)); + }else{ + vec1DistanceStats(ctx, pCsr); + } + }else if( iCol==VEC1_COLUMN_VECTOR ){ + const u8 *aVec = 0; + int nVec = 0; + u8 *pFree = 0; + rc = vec1GetVector(pCsr, &aVec, &nVec, &pFree); + if( rc==SQLITE_OK ){ + sqlite3_result_blob(ctx, aVec, nVec, SQLITE_TRANSIENT); + }else{ + sqlite3_result_error_code(ctx, rc); + } + sqlite3_free(pFree); + }else if( iCol>VEC1_COLUMN_VECTOR ){ + rc = vec1SeekCsr(pCsr); + if( rc==SQLITE_OK ){ + sqlite3_result_value(ctx, sqlite3_column_value(pCsr->pStmt, iCol-2)); + } + } + + return rc; +} + +static int vec1Rowid( + sqlite3_vtab_cursor *cur, + sqlite3_int64 *pRowid +){ + Vec1Csr *pCsr = (Vec1Csr *)cur; + if( pCsr->pQuery ){ + *pRowid = pCsr->pQuery->heap.aRes[pCsr->iCurrentRes].iRowid; + }else{ + *pRowid = sqlite3_column_int64(pCsr->pStmt, 0); + } + return SQLITE_OK; +} + +/* +** Return true if constraint iCons of pInfo uses the "binary" collation +** sequence. Or false it it uses anything else. +*/ +static int vec1IsBinary(sqlite3_index_info *pInfo, int iCons){ + return 0==sqlite3_stricmp("binary", sqlite3_vtab_collation(pInfo, iCons)); +} + + +/* +** xBestIndex implementation. Three plans are supported: +** +** + Full scan. (idxNum==0 or idxNum=1) +** + Lookup by rowid . (idxNum==2 or idxNum=3) +** + KANN search. (idxNum==5) +** +** For KANN search, meta-data constraints are supported. They are encoded +** in idxStr. Specifically, for each handled constraint, there is one +** character to identify the operation, usually followed by one or more +** characters to identify the column the constraint applies to. +** +** idxStr format: +*/ +static int vec1BestIndexMethod( + sqlite3_vtab *pVtab, + sqlite3_index_info *pIdxInfo +){ + struct Vec1IdxOp { + int idxop; + char vec1op; + } aOp[] = { + { SQLITE_INDEX_CONSTRAINT_EQ, VEC1_OP_EQ }, + { SQLITE_INDEX_CONSTRAINT_LT, VEC1_OP_LT }, + { SQLITE_INDEX_CONSTRAINT_GT, VEC1_OP_GT }, + { SQLITE_INDEX_CONSTRAINT_LE, VEC1_OP_LE }, + { SQLITE_INDEX_CONSTRAINT_GE, VEC1_OP_GE }, + { SQLITE_INDEX_CONSTRAINT_IS, VEC1_OP_IS }, + { SQLITE_INDEX_CONSTRAINT_ISNULL, VEC1_OP_ISNULL }, + { SQLITE_INDEX_CONSTRAINT_ISNOTNULL, VEC1_OP_NOTNULL }, + }; + const int bDistance = ( + pIdxInfo->colUsed & ((1<nConstraint; ii++){ + struct sqlite3_index_constraint *p = &pIdxInfo->aConstraint[ii]; + if( p->op==SQLITE_INDEX_CONSTRAINT_EQ ){ + if( p->usable ){ + if( p->iColumn==VEC1_COLUMN_CMD ) iCmd = ii; + else if( p->iColumn==VEC1_COLUMN_ARG ) iArg = ii; + else if( p->iColumn<0 ) iRowid = ii; + }else{ + if( p->iColumn==VEC1_COLUMN_CMD || p->iColumn==VEC1_COLUMN_ARG ){ + return SQLITE_CONSTRAINT; + } + } + } + } + + if( iCmd>=0 ){ + int iLimit = -1; + char *idxStr = vec1MallocZero((pIdxInfo->nConstraint*8)+1); + int iStr = 0; + int argvIndex = 0; + + if( idxStr==0 ) return SQLITE_NOMEM; + pIdxInfo->idxNum = 5; + pIdxInfo->needToFreeIdxStr = 1; + pIdxInfo->idxStr = idxStr; + pIdxInfo->aConstraintUsage[iCmd].argvIndex = (++argvIndex); + pIdxInfo->aConstraintUsage[iCmd].omit = 1; + if( iArg>=0 ){ + pIdxInfo->aConstraintUsage[iArg].argvIndex = (++argvIndex); + pIdxInfo->aConstraintUsage[iArg].omit = 1; + idxStr[iStr++] = VEC1_OP_PARAMS; + } + + /* Meta-data constraints */ + for(ii=0; iinConstraint; ii++){ + struct sqlite3_index_constraint *p = &pIdxInfo->aConstraint[ii]; + if( p->usable==0 ) continue; + if( p->iColumn>VEC1_COLUMN_VECTOR ){ + int jj; + if( 0==vec1IsBinary(pIdxInfo, ii) ) continue; + for(jj=0; jjop==aOp[jj].idxop ){ + int iMeta = (p->iColumn - 1 - VEC1_COLUMN_VECTOR); + if( jj==0 && sqlite3_vtab_in(pIdxInfo, ii, -1) ){ + idxStr[iStr++] = VEC1_OP_IN; + sqlite3_vtab_in(pIdxInfo, ii, 1); + }else{ + idxStr[iStr++] = aOp[jj].vec1op; + } + idxStr[iStr++] = aHexDigit[iMeta>>4]; + idxStr[iStr++] = aHexDigit[iMeta&0xF]; + pIdxInfo->aConstraintUsage[ii].argvIndex = (++argvIndex); + pIdxInfo->aConstraintUsage[ii].omit = 1; + break; + } + } + } + if( p->op==SQLITE_INDEX_CONSTRAINT_LIMIT ){ + iLimit = ii; + } + } + + if( iLimit>=0 + && pIdxInfo->nOrderBy==0 + && argvIndex==(pIdxInfo->nConstraint-1) + ){ + idxStr[iStr++] = VEC1_OP_LIMIT; + pIdxInfo->aConstraintUsage[iLimit].argvIndex = (++argvIndex); + pIdxInfo->aConstraintUsage[iLimit].omit = 1; + } + }else if( iRowid>=0 ){ + pIdxInfo->idxNum = (bDistance ? 3 : 2); + pIdxInfo->aConstraintUsage[iRowid].argvIndex = 1; + pIdxInfo->aConstraintUsage[iRowid].omit = 1; + pIdxInfo->idxFlags |= SQLITE_INDEX_SCAN_UNIQUE; + + pIdxInfo->estimatedCost = 10.0; + pIdxInfo->estimatedRows = 1; + }else if( bDistance ){ + pIdxInfo->idxNum = 1; + } + + return SQLITE_OK; +} + +static void vec1ListBuilderInitMetaArray(Vec1ListBuilder *p){ + const u32 flags = VEC1_META_1BYTEINT|VEC1_META_4BYTEINT|VEC1_META_REAL; + int ii; + for(ii=0; iipTab->nMeta; ii++){ + p->aMeta[ii].flags = flags; + p->aMeta[ii].format = VEC1_META_GENERIC; + p->aMeta[ii].buf.n = VEC1_META_SZHDR; + } +} + +static int vec1ListBuilderInit(Vec1ListBuilder *p, Vec1Tab *pTab, int iBucket){ + int rc = SQLITE_OK; + + p->pTab = pTab; + p->iBucket = iBucket; + p->szRowid = 4; + p->bSorted = 1; + + if( pTab->mod.hdr.nCodebook ){ + if( pTab->mod.hdr.flags & VEC1_MODEL_BITQUANT ){ + p->eBlocked = VEC1_BLOCKED_BIN; + }else{ + p->eBlocked = VEC1_BLOCKED_PQ; + } + p->nVectorSize = vec1EncodedVectorSize(&pTab->mod); + }else{ + p->nVectorSize = pTab->cfg.nElem * sizeof_f32; + p->eBlocked = VEC1_BLOCKED_NONE; + } + + if( pTab->nMeta>0 ){ + int nMeta = pTab->nMeta; + p->aMeta = (Vec1MetaBuilder*)vec1MallocZero(sizeof(Vec1MetaBuilder)*nMeta); + if( p->aMeta==0 ){ + rc = SQLITE_NOMEM; + }else{ + vec1ListBuilderInitMetaArray(p); + } + } + + return rc; +} + +static int vec1WriteMetaBlob( + Vec1Tab *pTab, /* Vec1 virtual table */ + Vec1Buffer *pBuf, /* Buffer to write from */ + i64 iId, /* Id of %_idx row */ + int iMeta /* Index of meta-column */ +){ + sqlite3_stmt *pStmt = 0; + int rc = vec1GetSql(pTab, VEC1_SQL_WRITE_META, &pStmt); + + assert( iMeta>=0 && iMetaa, pBuf->n, SQLITE_STATIC); + sqlite3_step(pStmt); + vec1StmtReset(&rc, pStmt); + sqlite3_bind_null(pStmt, 2); + } + + return rc; +} + +static int vec1WriteMeta( + Vec1Tab *pTab, /* Vec1 virtual table */ + Vec1MetaBuilder *p, /* Buffer to write from */ + int nEntry, + i64 iId, /* Id of %_idx row */ + int iMeta /* Index of meta-column */ +){ + u32 f = 0; + + if( (p->format & VEC1_META_GENERIC) + && (p->flags & (VEC1_META_1BYTEINT|VEC1_META_4BYTEINT|VEC1_META_REAL)) + ){ + int iIn = VEC1_META_SZHDR; + int iOut = VEC1_META_SZHDR; + Vec1MetaValue val = {0,0,0,0}; + Vec1Buffer buf = {0, 0, 0}; + + int rc = SQLITE_OK; + int szElem; + i64 iNull; + if( p->flags & VEC1_META_1BYTEINT ){ + szElem = 1; + iNull = VEC1_META_1BYTENULL; + p->format = VEC1_META_1BYTEINT; + }else if( p->flags & VEC1_META_4BYTEINT ){ + szElem = sizeof_u32; + iNull = VEC1_META_4BYTENULL; + p->format = VEC1_META_4BYTEINT; + }else{ + szElem = sizeof_f64; + iNull = VEC1_META_REALNULL; + p->format = VEC1_META_REAL; + } + + rc = vec1BufferSize(&buf, VEC1_META_SZHDR + nEntry * szElem); + if( rc!=SQLITE_OK ) return rc; + + while( iInbuf.n ){ + vec1MetaValueRead(&p->buf, &iIn, &val); + if( val.eType==SQLITE_NULL ){ + val.iVal = iNull; + }else if( p->format==VEC1_META_REAL ){ + if( val.eType==SQLITE_INTEGER ){ + val.fVal = (double)val.iVal; + } + memcpy(&val.iVal, &val.fVal, sizeof_f64); + } + if( szElem==1 ){ + buf.a[iOut] = (val.iVal & 0xFF); + }else if( szElem==sizeof_u32 ){ + vec1PutU32(&buf.a[iOut], (u32)val.iVal); + }else{ + vec1PutU64(&buf.a[iOut], (u64)val.iVal); + } + iOut += szElem; + } + buf.n = iOut; + + SWAP(Vec1Buffer, p->buf, buf); + vec1BufferFree(&buf); + } + + f = (p->format | (p->flags & VEC1_META_HASNULL)); + vec1PutU32(&p->buf.a[0], f); + vec1PutU32(&p->buf.a[4], nEntry); + return vec1WriteMetaBlob(pTab, &p->buf, iId, iMeta); +} + +/* +** A serialized meta-value is stored in buffer aBuf. Return the size in +** bytes of the meta value. +*/ +static int vec1MetaValueSize(const u8 *aBuf){ + switch( aBuf[0] ){ + case 0: return 1; /* NULL */ + case 1: return 2; /* 1-byte integer */ + case 2: return 5; /* 4-byte float */ + case 3: return 9; /* 8-byte integer */ + case 4: return 9; /* 8-byte real */ + default: { + /* Either a blob or text field. If the size of the value in bytes + ** is N, then the varint value V is: + ** + ** text (odd V values) : V = (2N + 5) -> N = (V - 5) / 2 + ** blob (even V values): V = (2N + 6) -> N = (V - 6) / 2 + */ + i64 V = 0; + int n = vec1GetVarint(aBuf, (u64*)&V); + return (int)(((V-5)/2) + n); + } + } + + assert( 0 ); + return 0; +} + +#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, + i64 iFirst, + i64 iLast, + const u8 *aVal, int nVal +){ + Vec1Tab *pTab = p->pTab; + int nEntry = 0; + int flags = 0; + int rc = SQLITE_OK; + int nRowid = 0; + int nData = 0; + int ii; /* Iterator variable */ + + assert( p->bufRowid.n==0 ); + assert( p->szRowid==4 ); + + if( vec1CheckIdxSize(pTab, aVal, nVal) ){ + return VEC1_CORRUPT; + } + flags = vec1GetU32(&aVal[0]); + nEntry = vec1GetU32(&aVal[4]); + p->nTombstone = vec1GetU32(&aVal[8]); + if( flags & VEC1_LIST_64BIT ) p->szRowid = 8; + + /* Set Vec1ListBuilder.eBlocked */ + if( pTab->mod.hdr.flags & VEC1_MODEL_BITQUANT ){ + p->eBlocked = VEC1_BLOCKED_BIN; + }else if( pTab->mod.hdr.nCodebook>0 ){ + p->eBlocked = VEC1_BLOCKED_PQ; + }else{ + p->eBlocked = VEC1_BLOCKED_NONE; + } + + p->bSorted = ((flags & VEC1_LIST_SORTED)!=0); + p->iFirst = iFirst; + p->iLast = iLast; + p->iId = iId; + + nRowid = nEntry * p->szRowid; + nData = nVal - VEC1_LIST_SZHDR - nRowid; + rc = vec1BufferGrow(&p->bufRowid, nRowid); + if( rc==SQLITE_OK ){ + memcpy(p->bufRowid.a, &aVal[VEC1_LIST_SZHDR], nRowid); + p->bufRowid.n = nRowid; + rc = vec1BufferGrow(&p->bufData, nData); + } + if( rc==SQLITE_OK ){ + memcpy(p->bufData.a, &aVal[VEC1_LIST_SZHDR+nRowid], nData); + p->bufData.n = nData; + } + + /* 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); + if( rc==SQLITE_OK ){ + u32 f = vec1GetU32(pMeta->buf.a); + pMeta->format = (f & VEC1_META_TYPEMASK); + pMeta->flags = f; + } + } + + return rc; +} + +static int vec1ListBuilderFlush(Vec1ListBuilder *p){ + int rc = SQLITE_OK; + if( p->bufRowid.n>0 ){ + int nEntry = p->bufRowid.n / p->szRowid; + i64 iId = 0; + int ii; + sqlite3_stmt *pStmt = 0; + u8 *aBlob = 0; + int nBlob = 0; + + nBlob = VEC1_LIST_SZHDR + p->bufRowid.n + p->bufData.n; + aBlob = (u8*)sqlite3_malloc(nBlob); + if( aBlob==0 ){ + rc = SQLITE_NOMEM; + }else{ + int flags = 0; + if( p->bSorted ) flags |= VEC1_LIST_SORTED; + if( p->szRowid==8 ) flags |= VEC1_LIST_64BIT; + vec1PutU32(&aBlob[0], flags); + vec1PutU32(&aBlob[4], nEntry); + vec1PutU32(&aBlob[8], p->nTombstone); + memcpy(&aBlob[VEC1_LIST_SZHDR], p->bufRowid.a, p->bufRowid.n); + memcpy(&aBlob[VEC1_LIST_SZHDR+p->bufRowid.n], p->bufData.a, p->bufData.n); + + rc = vec1GetSql(p->pTab, VEC1_SQL_REPLACE_IDX, &pStmt); + if( rc==SQLITE_OK ){ + if( p->iId==0 ){ + sqlite3_bind_null(pStmt, 1); + }else{ + sqlite3_bind_int64(pStmt, 1, p->iId); + } + sqlite3_bind_int(pStmt, 2, p->iBucket); + sqlite3_bind_int64(pStmt, 3, p->iFirst); + sqlite3_bind_int64(pStmt, 4, p->iLast); + + rc = sqlite3_bind_blob(pStmt, 5, aBlob, nBlob, SQLITE_STATIC); + sqlite3_step(pStmt); + vec1StmtReset(&rc, pStmt); + if( rc==SQLITE_OK ){ + iId = sqlite3_last_insert_rowid(p->pTab->db); + } + } + sqlite3_free(aBlob); + + /* If an error occurred within the sqlite3_step() call above, then the + ** transaction may have been rolled back and (*p) freed. So take + ** care not to access it again if rc!=SQLITE_OK. */ + if( rc==SQLITE_OK ){ + p->bufRowid.n = 0; + p->bufData.n = 0; + p->iId = 0; + p->iId = 0; + } + } + + for(ii=0; rc==SQLITE_OK && iipTab->nMeta; ii++){ + rc = vec1WriteMeta(p->pTab, &p->aMeta[ii], nEntry, iId, ii); + if( rc==SQLITE_OK ) p->aMeta[ii].buf.n = VEC1_META_SZHDR; + } + + /* Same reasoning as above - if rc!=SQLITE_OK, then the transaction + ** may have been rolled back and (*p) freed already. */ + if( rc==SQLITE_OK ){ + p->bSorted = 1; + } + }else if( p->iId>0 ){ + sqlite3_stmt *pDelete = 0; + rc = vec1GetSql(p->pTab, VEC1_SQL_DELETE_FROM_IDX, &pDelete); + if( rc==SQLITE_OK ){ + sqlite3_bind_int64(pDelete, 1, p->iId); + sqlite3_step(pDelete); + vec1StmtReset(&rc, pDelete); + } + if( p->pTab->nMeta>0 && rc==SQLITE_OK ){ + rc = vec1GetSql(p->pTab, VEC1_SQL_DELETE_FROM_META, &pDelete); + if( rc==SQLITE_OK ){ + sqlite3_bind_int64(pDelete, 1, p->iId); + sqlite3_step(pDelete); + vec1StmtReset(&rc, pDelete); + } + } + } + return rc; +} + + +/* +** Return the offset of the first field of vector ii within a blocked +** storage buffer. Each vector is nVectorSize bytes in size (including +** the norm for binary-quantized vectors). szElem0 is the size of the +** first element in each vector (4 for binary-quant, 1 for product-quant). +*/ +#define VEC1_LIST_BLOCKED_OFFSET(ii, nVectorSize) ( \ + ((ii / VEC1_PQ_BLOCKSIZE) * VEC1_PQ_BLOCKSIZE * nVectorSize) + \ + (ii % VEC1_PQ_BLOCKSIZE) \ +) + +/* +** Return the offset of the norm field for vector ii within a blocked +** buffer containing binary quanitized vectors. Each quantized vector +** is nCodebook bytes in size, not including the 4 byte norm. +*/ +#define VEC1_LIST_NORM_OFFSET(ii, nCodebook) ( \ + ((ii / VEC1_PQ_BLOCKSIZE) * VEC1_PQ_BLOCKSIZE * (nCodebook+sizeof_f32)) + \ + (ii % VEC1_PQ_BLOCKSIZE) * sizeof_f32 \ +) + +#define VEC1_LIST_CODE_OFFSET(ii, nCodebook) ( \ + ((ii / VEC1_PQ_BLOCKSIZE) * VEC1_PQ_BLOCKSIZE * (nCodebook+sizeof_f32)) + \ + (VEC1_PQ_BLOCKSIZE * sizeof_f32) + \ + (ii % VEC1_PQ_BLOCKSIZE) \ +) + +/* +** Shuffle the contents of the list-builder around so that it +** contains 0 tombstones. +*/ +static int vec1ListBuilderCompress( + Vec1ListBuilder *p, + i64 iDel +){ + const int nMeta = p->pTab->nMeta; + int ii; + int iOut = 0; + int nEntry = p->bufRowid.n / p->szRowid; + int *aMetaIn = 0; /* Meta-data input offsets */ + int *aMetaOut = 0; /* Meta-data output offsets */ + + i64 tomb1 = p->szRowid==8 ? VEC1_TOMBSTONE_64 : VEC1_TOMBSTONE_32; + i64 tomb2 = iDel; + + /* Allocate space for aMetaIn[] and aMetaOut[] if requried */ + if( nMeta>0 ){ + aMetaIn = vec1MallocZero(nMeta * 2 * sizeof(int)); + if( aMetaIn==0 ) return SQLITE_NOMEM; + aMetaOut = &aMetaIn[nMeta]; + for(ii=0; iiiFirst = VEC1_LARGEST_INT64; + p->iLast = VEC1_SMALLEST_INT64; + + for(ii=0; iiszRowid*ii; + i64 iRowid; + int bTombstone = 0; + int iMeta; /* Iterator used for meta-data columns */ + + if( p->szRowid==8 ){ + iRowid = vec1GetU64(&p->bufRowid.a[iOff]); + }else{ + iRowid = vec1GetU32(&p->bufRowid.a[iOff]); + } + if( iRowid==tomb1 || iRowid==tomb2 ) bTombstone = 1; + + if( bTombstone==0 ){ + p->iFirst = MIN(p->iFirst, iRowid); + p->iLast = MAX(p->iLast, iRowid); + + if( ii!=iOut ){ + int iOff2 = p->szRowid*iOut; + u8 *a = p->bufData.a; + int iFrom, iTo; + + memcpy(&p->bufRowid.a[iOff2], &p->bufRowid.a[iOff], p->szRowid); + if( p->eBlocked==VEC1_BLOCKED_NONE ){ + int i1 = p->nVectorSize * iOut; + int i2 = p->nVectorSize * ii; + memcpy(&p->bufData.a[i1], &p->bufData.a[i2], p->nVectorSize); + }else{ + int nCodebook; /* Size of quantized vector without norm */ + int jj; + + if( p->eBlocked==VEC1_BLOCKED_BIN ){ + nCodebook = p->nVectorSize - sizeof_f32; + memcpy(&a[ VEC1_LIST_NORM_OFFSET(iOut, nCodebook) ], + &a[ VEC1_LIST_NORM_OFFSET(ii, nCodebook) ], + sizeof_f32 + ); + iFrom = VEC1_LIST_CODE_OFFSET(ii, nCodebook); + iTo = VEC1_LIST_CODE_OFFSET(iOut, nCodebook); + }else{ + assert( p->eBlocked==VEC1_BLOCKED_PQ ); + nCodebook = p->nVectorSize; + iFrom = VEC1_LIST_BLOCKED_OFFSET(ii, nCodebook); + iTo = VEC1_LIST_BLOCKED_OFFSET(iOut, nCodebook); + } + + for(jj=0; jjbufData.a[iFrom + jj*VEC1_PQ_BLOCKSIZE]; + p->bufData.a[iTo + jj*VEC1_PQ_BLOCKSIZE] = v; + } + } + } + iOut++; + } + + for(iMeta=0; iMetaaMeta[iMeta]; + u8 *a = pMeta->buf.a; + + + if( pMeta->format==VEC1_META_GENERIC ){ + int nIn = vec1MetaValueSize(&a[aMetaIn[iMeta]]); + if( bTombstone==0 ){ + if( aMetaIn[iMeta]!=aMetaOut[iMeta] ){ + memmove(&a[ aMetaOut[iMeta] ], &a[ aMetaIn[iMeta] ], nIn); + } + aMetaOut[iMeta] += nIn; + } + aMetaIn[iMeta] += nIn; + }else{ + int szElem = + (pMeta->format==VEC1_META_1BYTEINT) ? 1 : + (pMeta->format==VEC1_META_4BYTEINT) ? sizeof_u32 : sizeof_f64 + ; + if( bTombstone==0 ){ + memmove(&a[ aMetaOut[iMeta] ], &a[ aMetaIn[iMeta] ], szElem); + aMetaOut[iMeta] += szElem; + } + aMetaIn[iMeta] += szElem; + } + } + } + + p->nTombstone = 0; + + p->bufRowid.n = iOut*p->szRowid; + if( p->eBlocked==VEC1_BLOCKED_NONE ){ + p->bufData.n = p->nVectorSize * iOut; + }else{ + p->bufData.n = p->nVectorSize * VEC1_PQ_BLOCKSIZE * ( + ((iOut+VEC1_PQ_BLOCKSIZE-1)/VEC1_PQ_BLOCKSIZE) + ); + } + for(ii=0; iiaMeta[ii].buf.n = aMetaOut[ii]; + } + + sqlite3_free(aMetaIn); + return SQLITE_OK; +} + +/* +** Append a rowid/[PQ|vector] pair to a list builder object. +*/ +static int vec1ListBuilderAdd( + Vec1ListBuilder *p, + i64 iRowid, /* Rowid value to add to builder */ + const u8 *aPQ /* Pointer to buffer containing PQ enc. */ +){ + int rc = SQLITE_OK; + int nFinal = 0; + const int vs = p->nVectorSize; + + assert( p->szRowid==4 || p->szRowid==8 ); + + /* Check if adding this entry would cause the final blob to be larger + ** than the configured 'blocksize'. If so, flush it to disk. + ** + ** If the proposed rowid may not be added to the current list because + ** it is too large or a tombstone value, flush the current list to disk + ** in that case as well. + */ + nFinal = p->bufRowid.n + p->bufData.n + p->szRowid + p->nVectorSize; + if( nFinal>p->pTab->cfg.nBlocksize + || (p->szRowid==4 && (iRowid<0 || iRowid>=VEC1_TOMBSTONE_32)) + || (p->szRowid==8 && iRowid==VEC1_TOMBSTONE_64) + ){ + rc = vec1ListBuilderFlush(p); + if( rc!=SQLITE_OK ) return rc; + p->szRowid = ((iRowid<0 || iRowid>=VEC1_TOMBSTONE_32) ? 8 : 4); + p->bSorted = 1; + p->iId = 0; + p->nTombstone = 0; + vec1ListBuilderInitMetaArray(p); + } + + /* Update Vec1ListBuilder.iFirst, iLast and bSorted to account for the + ** rowid value being added by this call. */ + if( p->bufRowid.n==0 ){ + p->iLast = p->iFirst = iRowid; + }else if( iRowidiLast ){ + p->bSorted = 0; + p->iFirst = MIN(p->iFirst, iRowid); + }else{ + p->iLast = iRowid; + } + + /* Ensure that there is room in the two buffers for this entry. The + ** rowids buffer always grows by szRowid bytes. How much the data + ** buffer grows by depends on whether it is blocked or not. */ + rc = vec1BufferGrow(&p->bufRowid, p->szRowid); + if( rc==SQLITE_OK ){ + int nReq = 0; + if( p->eBlocked==VEC1_BLOCKED_NONE ){ + rc = vec1BufferGrow(&p->bufData, p->nVectorSize); + }else{ + int nEntry = (p->bufRowid.n / p->szRowid); + if( (nEntry % VEC1_PQ_BLOCKSIZE)==0 ){ + int nReq = (VEC1_PQ_BLOCKSIZE * p->nVectorSize); + rc = vec1BufferGrow(&p->bufData, nReq); + if( rc==SQLITE_OK ){ + memset(&p->bufData.a[p->bufData.n], 0, nReq); + p->bufData.n += nReq; + } + } + } + } + + if( rc==SQLITE_OK ){ + + /* Write the vector or PQ data */ + if( p->eBlocked==VEC1_BLOCKED_NONE ){ + memcpy(&p->bufData.a[p->bufData.n], aPQ, p->nVectorSize); + p->bufData.n += p->nVectorSize; + }else{ + const int iEntry = (p->bufRowid.n / p->szRowid); + const int iBlkOff = (iEntry / VEC1_PQ_BLOCKSIZE) * vs * VEC1_PQ_BLOCKSIZE; + const int iBlkEntry = iEntry % VEC1_PQ_BLOCKSIZE; + + int ii = 0; + int iOff = iBlkOff + iBlkEntry; + + if( p->eBlocked==VEC1_BLOCKED_BIN ){ + /* In this case, the first 4 bytes of the encoded vector is its norm. + ** Each block begins with an array of VEC1_PQ_BLOCKSIZE norm values. + ** Copy the norm for this vector in now. */ + const int iNorm = iBlkOff + (sizeof_f32 * iBlkEntry); + memcpy(&p->bufData.a[iNorm], aPQ, sizeof_f32); + ii = 4; + iOff += (sizeof_f32 * VEC1_PQ_BLOCKSIZE) - ii*VEC1_PQ_BLOCKSIZE; + } + + /* Write each byte of the PQ or binary quant code */ + for(/* no-op */; iinVectorSize; ii++){ + p->bufData.a[iOff + ii*VEC1_PQ_BLOCKSIZE] = aPQ[ii]; + } + } + + /* Write the rowid */ + if( p->szRowid==8 ){ + vec1PutU64(&p->bufRowid.a[p->bufRowid.n], iRowid); + }else{ + vec1PutU32(&p->bufRowid.a[p->bufRowid.n], (u32)iRowid); + } + p->bufRowid.n += p->szRowid; + } + + return rc; +} + +static void vec1ListBuilderFree(Vec1ListBuilder *p){ + int ii; + vec1BufferFree(&p->bufRowid); + vec1BufferFree(&p->bufData); + if( p->aMeta ){ + for(ii=0; iipTab->nMeta; ii++){ + vec1BufferFree(&p->aMeta[ii].buf); + } + } + sqlite3_free(p->aMeta); +} + +/* +** Allocate a new writer object for vec1 table pTab. +*/ +static int vec1WriterAlloc( + Vec1Tab *pTab, /* Vec1 table */ + int bRebuild, /* True if this is 'rebuild' op */ + Vec1Writer **pp /* OUT: New writer object */ +){ + int rc = SQLITE_OK; + Vec1Writer *p = 0; + int nBld = pTab->mod.hdr.nBucket ? pTab->mod.hdr.nBucket : 1; + int nEncoded = vec1EncodedVectorSize(&pTab->mod); + + int nByte = + sizeof(Vec1Writer) + /* Vec1Writer */ + sizeof(Vec1ListBuilder) * nBld + /* Vec1Writer.aBld */ + sizeof_f32 * pTab->nTmpVec + /* Vec1Writer.aResidual */ + nEncoded; /* Vec1Writer.aPQ */ + + p = (Vec1Writer*)vec1MallocZero(nByte); + if( p==0 ){ + rc = SQLITE_NOMEM; + }else{ + memset(p, 0, nByte); + p->aBld = (Vec1ListBuilder*)&p[1]; + p->aResidual = (float*)&p->aBld[nBld]; + p->aPQ = (u8*)&p->aResidual[pTab->nTmpVec]; + p->pTab = pTab; + p->bRebuild = bRebuild; + p->nBld = nBld; + if( bRebuild ){ + int ii; + for(ii=0; rc==SQLITE_OK && iiaBld[ii], pTab, ii); + } + } + } + *pp = p; + + return rc; +} + +/* +** Quantize a vector so that it can be written to the index. +*/ +static void vec1QuantizeVector( + const Vec1Model *pMod, /* Current model */ + float *aTmp, /* Temporary space - same dim as vectors */ + const float *aVector, /* Vector to quantize */ + int *piBucket, /* OUT: Bucket to put vector in */ + u8 *aCode /* OUT: Write PQ code (if any) here */ +){ + const int nBucket = pMod->hdr.nBucket; + const float *aVec; + int iBucket = 0; + + aVec = vec1TransformInputVector(pMod, aTmp, aVector); + if( nBucket>0 ){ + int nElem = pMod->hdr.nElem; + iBucket = vec1PqBestMatch(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; + } + } + + if( pMod->hdr.nCodebook>0 ){ + vec1EncodeVector(pMod, aVec, aCode); + } + + *piBucket = iBucket; +} + + +static int vec1WriterVector( + Vec1Writer *p, /* Writer object to write to */ + i64 iRowid, /* Rowid of new entry */ + const float *aVector, /* New vector */ + int *piBucket +){ + Vec1Tab *pTab = p->pTab; + const u8 *aStore; + Vec1ListBuilder *pBld = 0; + const Vec1Model *pMod = &pTab->mod; + int rc = SQLITE_OK; + int iBld = *piBucket; + + /* Transform and quantize the vector */ + if( iBld<0 ){ + vec1QuantizeVector(pMod, p->aResidual, aVector, &iBld, p->aPQ); + } + + if( pMod->hdr.nCodebook>0 ){ + aStore = p->aPQ; + }else{ + /* If the index is storing full vectors, not PQ codes, store the original, + ** not the transformed vector. */ + aStore = (const u8*)aVector; + } + + pBld = &p->aBld[iBld]; + if( pBld->pTab==0 ){ + /* Attempt to load blob smaller than 'blocksize-min' belonging to + ** this bucket to append to. */ + sqlite3_stmt *pSearch = 0; + rc = vec1ListBuilderInit(pBld, pTab, iBld); + if( rc==SQLITE_OK ){ + rc = vec1GetSql(pTab, VEC1_SQL_SEARCH_IDX_FOR_INSERT, &pSearch); + } + if( rc==SQLITE_OK ){ + sqlite3_bind_int(pSearch, 1, iBld); + sqlite3_bind_int(pSearch, 2, pTab->cfg.nBlocksizeMin); + if( SQLITE_ROW==sqlite3_step(pSearch) ){ + i64 iId = sqlite3_column_int64(pSearch, 0); + i64 iFirst = sqlite3_column_int64(pSearch, 1); + i64 iLast = sqlite3_column_int64(pSearch, 2); + const u8 *aVal = (const u8*)sqlite3_column_blob(pSearch, 3); + int nVal = sqlite3_column_bytes(pSearch, 3); + rc = vec1ListBuilderLoad(pBld, iId, iFirst, iLast, aVal, nVal); + } + vec1StmtReset(&rc, pSearch); + } + } + + if( rc==SQLITE_OK ){ + rc = vec1ListBuilderAdd(pBld, iRowid, aStore); + } + + *piBucket = iBld; + return rc; +} + +/* +** Serialize value pVal and append it to buffer pBuf. Vec1 uses a serialization +** format based on SQLite's. First a varint indicating the type and size +** of the value, followed by the data for the value itself. Text values +** are always encoded using utf-8. +** +** 0: NULL value. 0 byte payload. +** 1: Integer value (unsigned). 1 byte payload. +** 2: Integer value (signed). 4 byte payload. +** 3: Integer value (signed). 8 byte payload. +** 4: Real value. 8 byte payload. +** +** Greater than 4 and odd is a text value. Payload size ((v-5)/2). Greater +** than 4 and even is a blob value. Payload size ((v-6)/2). +*/ +static int vec1AppendMetaValue( + Vec1Buffer *pBuf, + sqlite3_value *pVal +){ + int rc = SQLITE_OK; + switch( sqlite3_value_type(pVal) ){ + case SQLITE_NULL: { + rc = vec1BufferGrow(pBuf, 1); + if( rc==SQLITE_OK ){ + pBuf->a[pBuf->n++] = 0x00; + } + break; + } + + case SQLITE_INTEGER: { + i64 iVal = sqlite3_value_int64(pVal); + rc = vec1BufferGrow(pBuf, 9); + if( rc==SQLITE_OK ){ + if( iVal>=0 && iVal<=254 ){ + pBuf->a[pBuf->n++] = 0x01; + pBuf->a[pBuf->n++] = (iVal & 0xFF); + } + else if( iVal>=VEC1_META_4BYTEMIN && iVal<=VEC1_META_4BYTEMAX ){ + pBuf->a[pBuf->n++] = 0x02; + vec1PutU32(&pBuf->a[pBuf->n], (int)iVal); + pBuf->n += 4; + }else{ + pBuf->a[pBuf->n++] = 0x03; + vec1PutU64(&pBuf->a[pBuf->n], iVal); + pBuf->n += 8; + } + } + break; + } + + case SQLITE_FLOAT: { + double fVal = sqlite3_value_double(pVal); + rc = vec1BufferGrow(pBuf, 9); + if( rc==SQLITE_OK ){ + i64 iVal; + memcpy(&iVal, &fVal, sizeof(fVal)); + pBuf->a[pBuf->n++] = 0x04; + vec1PutU64(&pBuf->a[pBuf->n], iVal); + pBuf->n += 8; + } + break; + } + + case SQLITE_TEXT: { + const char *z = (const char*)sqlite3_value_text(pVal); + int n = sqlite3_value_bytes(pVal); + rc = vec1BufferGrow(pBuf, n+5); + if( rc==SQLITE_OK ){ + pBuf->n += vec1PutVarint(&pBuf->a[pBuf->n], (n*2 + 5)); + memcpy(&pBuf->a[pBuf->n], z, n); + pBuf->n += n; + } + break; + } + + default: { + const u8 *z = (const u8*)sqlite3_value_blob(pVal); + int n = sqlite3_value_bytes(pVal); + rc = vec1BufferGrow(pBuf, n+5); + if( rc==SQLITE_OK ){ + pBuf->n += vec1PutVarint(&pBuf->a[pBuf->n], (n*2 + 6)); + memcpy(&pBuf->a[pBuf->n], z, n); + pBuf->n += n; + } + assert( sqlite3_value_type(pVal)==SQLITE_BLOB ); + break; + } + } + + return rc; +} + +static void vec1MetaValueUpdateFlags(const u8 *aMeta, u32 *pFlags){ + switch( aMeta[0] ){ + case 0: /* NULL value */ + *pFlags |= VEC1_META_HASNULL; + break; + + case 1: /* 1-byte integer */ + break; + + case 2: /* 4-byte integer */ + *pFlags &= ~(VEC1_META_1BYTEINT); + break; + + case 4: /* 8-byte real */ + *pFlags &= ~(VEC1_META_1BYTEINT|VEC1_META_4BYTEINT); + break; + + default: + *pFlags &= ~(VEC1_META_1BYTEINT|VEC1_META_4BYTEINT|VEC1_META_REAL); + break; + } +} + +/* +** The meta-list builder passed as the only argument currently contains +** a list of values in non-generic format (e.g. VEC1_META_1BYTEINT format). +** This function converts the list to generic format. +** +** Return SQLITE_OK if successful, or an SQLite error code (SQLITE_NOMEM) +** otherwise. +*/ +static int vec1ExpandMetaValue(Vec1MetaBuilder *p){ + Vec1Buffer orig; + int rc = SQLITE_OK; + int bHasNull = 0; + + memcpy(&orig, &p->buf, sizeof(Vec1Buffer)); + p->buf.a = 0; + p->buf.n = VEC1_META_SZHDR; + p->buf.nAlloc = 0; + + assert( p->format==VEC1_META_1BYTEINT + || p->format==VEC1_META_4BYTEINT + || p->format==VEC1_META_REAL + ); + + rc = vec1BufferGrow(&p->buf, (orig.n * 2)); + if( rc==SQLITE_OK ){ + int iIn = VEC1_META_SZHDR; + int iOut = VEC1_META_SZHDR; + + while( iInformat==VEC1_META_1BYTEINT ){ + int v = orig.a[iIn++]; + if( v==VEC1_META_1BYTENULL ){ + p->buf.a[iOut++] = 0x00; + bHasNull = 1; + }else{ + p->buf.a[iOut++] = 0x01; + p->buf.a[iOut++] = (v & 0xFF); + } + }else if( p->format==VEC1_META_4BYTEINT ){ + u32 v = vec1GetU32(&orig.a[iIn]); + iIn += sizeof_u32; + if( v==VEC1_META_4BYTENULL ){ + p->buf.a[iOut++] = 0x00; + bHasNull = 1; + }else{ + p->buf.a[iOut++] = 0x02; + vec1PutU32(&p->buf.a[iOut], (u32)v); + iOut += sizeof_u32; + } + }else{ + u64 v = vec1GetU64(&orig.a[iIn]); + iIn += sizeof_f64; + if( v==VEC1_META_REALNULL ){ + p->buf.a[iOut++] = 0x00; + bHasNull = 1; + }else{ + p->buf.a[iOut++] = 0x04; + vec1PutU64(&p->buf.a[iOut], v); + iOut += sizeof_f64; + } + } + } + + p->buf.n = iOut; + } + + if( p->format==VEC1_META_1BYTEINT ){ + p->flags = VEC1_META_4BYTEINT | VEC1_META_1BYTEINT | VEC1_META_REAL; + }else if( p->format==VEC1_META_4BYTEINT ){ + p->flags = VEC1_META_4BYTEINT | VEC1_META_REAL; + }else{ + p->flags = VEC1_META_REAL; + } + p->format = VEC1_META_GENERIC; + if( bHasNull ){ + p->flags |= VEC1_META_HASNULL; + } + + vec1BufferFree(&orig); + return rc; +} + +static int vec1WriterMetaFromArray( + Vec1Writer *pWriter, /* Writer object accumulating changes */ + int iBucket, /* Bucket to write to */ + sqlite3_value **apMeta /* Meta values associated with prev vector */ +){ + Vec1ListBuilder *p = &pWriter->aBld[iBucket]; + int ii; + int rc = SQLITE_OK; + assert( p->pTab!=0 ); + for(ii=0; rc==SQLITE_OK && iipTab->nMeta; ii++){ + Vec1MetaBuilder *pMeta = &p->aMeta[ii]; + Vec1Buffer *pBuf = &pMeta->buf; + sqlite3_value *pVal = apMeta[ii]; + + if( pMeta->format==VEC1_META_1BYTEINT + || pMeta->format==VEC1_META_4BYTEINT + || pMeta->format==VEC1_META_REAL + ){ + int eType = sqlite3_value_type(pVal); + + rc = vec1BufferGrow(pBuf, sizeof_f64); + if( rc!=SQLITE_OK ) continue; + + if( eType==SQLITE_INTEGER ){ + i64 iVal = sqlite3_value_int64(pVal); + + if( pMeta->format==VEC1_META_1BYTEINT ){ + if( iVal>=0 && iVal<=254 ){ + pBuf->a[pBuf->n++] = (iVal & 0xFF); + pVal = 0; + } + }else{ + if( iVal>=VEC1_META_4BYTEMIN && iVal<=VEC1_META_4BYTEMAX ){ + if( pMeta->format==VEC1_META_4BYTEINT ){ + vec1PutU32(&pBuf->a[pBuf->n], (u32)iVal); + pBuf->n += sizeof_u32; + }else{ + double fVal = (double)iVal; + u64 v; + memcpy(&v, &fVal, sizeof_f64); + vec1PutU64(&pBuf->a[pBuf->n], v); + pBuf->n += sizeof_f64; + } + pVal = 0; + } + } + }else if( eType==SQLITE_FLOAT && pMeta->format==VEC1_META_REAL ){ + double fVal = sqlite3_value_double(pVal); + u64 v; + memcpy(&v, &fVal, sizeof_f64); + vec1PutU64(&pBuf->a[pBuf->n], v); + pBuf->n += sizeof_f64; + pVal = 0; + }else if( eType==SQLITE_NULL ){ + rc = vec1BufferGrow(pBuf, sizeof_f64); + if( pMeta->format==VEC1_META_1BYTEINT ){ + pBuf->a[pBuf->n++] = (VEC1_META_1BYTENULL & 0xFF); + } + else if( pMeta->format==VEC1_META_4BYTEINT ){ + vec1PutU32(&pBuf->a[pBuf->n], (u32)VEC1_META_4BYTENULL); + pBuf->n += sizeof_u32; + }else{ + vec1PutU64(&pBuf->a[pBuf->n], (u64)VEC1_META_REALNULL); + pBuf->n += sizeof_f64; + } + pVal = 0; + } + + if( pVal ){ + rc = vec1ExpandMetaValue(pMeta); + } + } + + if( pMeta->format==VEC1_META_GENERIC ){ + int iOff = pBuf->n; + rc = vec1AppendMetaValue(pBuf, pVal); + if( rc==SQLITE_OK ){ + vec1MetaValueUpdateFlags(&pBuf->a[iOff], &pMeta->flags); + } + } + } + return rc; +} + +static int vec1WriterMetaFromStmt( + Vec1Writer *pWriter, /* Writer object accumulating changes */ + int iBucket, /* Bucket to write to */ + sqlite3_stmt *pStmt +){ + Vec1ListBuilder *p = &pWriter->aBld[iBucket]; + int ii; + int rc = SQLITE_OK; + assert( p->pTab!=0 ); + for(ii=0; rc==SQLITE_OK && iipTab->nMeta; ii++){ + Vec1Buffer *pBuf = &p->aMeta[ii].buf; + sqlite3_value *pVal = sqlite3_column_value(pStmt, ii+2); + int iOff = pBuf->n; + rc = vec1AppendMetaValue(pBuf, pVal); + if( rc==SQLITE_OK ){ + vec1MetaValueUpdateFlags(&pBuf->a[iOff], &p->aMeta[ii].flags); + } + } + return rc; +} + +/* +** Read nMeta values from buffer pBuf. Use them as the meta values associated +** with the (compressed) vector just appended to bucket iBucket. +*/ +static int vec1WriterMetaFromPacked( + Vec1Writer *pWriter, /* Writer object accumulating changes */ + int iBucket, /* Bucket to write to */ + Vec1Buffer *pBuf, + int *piOff /* IN/OUT: Buffer offset */ +){ + int ii; + int iOff = *piOff; + for(ii=0; iipTab->nMeta; ii++){ + Vec1MetaBuilder *pTo = &pWriter->aBld[iBucket].aMeta[ii]; + int n = vec1MetaValueSize(&pBuf->a[iOff]); + int rc = vec1BufferGrow(&pTo->buf, n); + vec1MetaValueUpdateFlags(&pBuf->a[iOff], &pTo->flags); + if( rc!=SQLITE_OK ) return rc; + memcpy(&pTo->buf.a[pTo->buf.n], &pBuf->a[iOff], n); + pTo->buf.n += n; + iOff += n; + } + *piOff = iOff; + return SQLITE_OK; +} + +static int vec1PackMetaValues( + Vec1Tab *pTab, + Vec1Buffer *pBuf, + sqlite3_stmt *pStmt +){ + int ii; + int rc = SQLITE_OK; + for(ii=0; rc==SQLITE_OK && iinMeta; ii++){ + rc = vec1AppendMetaValue(pBuf, sqlite3_column_value(pStmt, 2+ii)); + } + return rc; +} + +static int vec1WriterQuantized( + Vec1Writer *pWriter, + i64 iRowid, + int iBucket, + const u8 *aStore +){ + return vec1ListBuilderAdd(&pWriter->aBld[iBucket], iRowid, aStore); +} + +static int vec1WriterFinish(Vec1Writer *p, int rcin){ + int rc = rcin; + if( p ){ + int ii; + for(ii=0; iinBld; ii++){ + if( rc==SQLITE_OK ){ + rc = vec1ListBuilderFlush(&p->aBld[ii]); + } + vec1ListBuilderFree(&p->aBld[ii]); + } + sqlite3_free(p); + } + return rc; +} + +static void vec1EncodeVectorN( + const Vec1Model *pMod, + const float *aVec[VEC1_MULTIMATCH_NVEC], + u8 *aCode +){ + if( pMod->hdr.flags & VEC1_MODEL_BITQUANT ){ + int nCode = vec1EncodedVectorSize(pMod); + int vv; + for(vv=0; vvhdr.nCodebook; + const int nCodeElem = pMod->nCodeElem; + const float *aSub[VEC1_MULTIMATCH_NVEC]; + int vv; + int k; + for(k=0; kaModel[k * nCodeElem * VEC1_PQ_CODEBOOK_SZ], + &pMod->aModelNT[k * nCodeElem * VEC1_PQ_CODEBOOK_SZ], + VEC1_PQ_CODEBOOK_SZ, aSub, aBest, 0 + ); + + for(vv=0; vvhdr.nElem; + int vv; + + for(vv=0; vvhdr.flags & VEC1_MODEL_ROTATE) ){ + float *aRot[VEC1_MULTIMATCH_NVEC]; + for(vv=0; vvaRotation, pMod->aRotationNT + ); +#else + vec1RotateVectorN(aRot, aOut, nElem, pMod->aRotation); +#endif + + memcpy(aOut, aRot, sizeof(aRot)); + } + + else if( (pMod->hdr.flags & VEC1_MODEL_WHT) ){ + for(vv=0; vvhdr.eDistance==VEC1_DISTANCE_COS || nStride!=nElem ){ + for(vv=0; vvhdr.eDistance==VEC1_DISTANCE_COS ){ + for(vv=0; vvhdr.nElem; + const int nBucket = pMod->hdr.nBucket; + const int nStride = MAX(nElem, pMod->nCodeElem * (int)pMod->hdr.nCodebook); + const float *aVec[VEC1_MULTIMATCH_NVEC]; + int iBucket = 0; + int vv; + + vec1TransformInputVectorN(pMod, aVector, aVec, aTmp, nStride); + + if( nBucket>0 ){ + vec1BestMatchPlatformN( + nElem, pMod->aCentroid, pMod->aCentroidNT, nBucket, aVec, aBucket, 0 + ); + + if( pMod->hdr.nCodebook>0 && (pMod->hdr.flags & VEC1_MODEL_RESIDUAL) ){ + for(vv=0; vvaCentroid[aBucket[vv]*nElem], nElem); + aVec[vv] = tmp; + } + } + + for(vv=0; vv=0 && aBucket[vv]hdr.nCodebook>0 ){ + vec1EncodeVectorN(pMod, aVec, aCode); + } +} + +/* +** Each Vec1QuantizeJob has capacity for this many vectors. +*/ +#define VEC1_QUANTIZE_JOB_SZ 1024 + +typedef struct Vec1QuantizeJob Vec1QuantizeJob; +struct Vec1QuantizeJob { + Vec1Model *pModel; + Vec1Writer *pWriter; + int nVector; /* Number of vectors to quantize */ + i64 *aRowid; /* Array of nVector rowids */ + float *aTmp; /* Temp space for one vector */ + float *aVec; /* Packed vectors */ + int *aBucket; /* OUT: Bucket for each vector */ + u8 *aCode; /* OUT: Packed codes for vectors */ + int nEncoded; /* Size of each encoded vector in bytes */ + Vec1Buffer meta; /* Packed array of meta values */ +}; + +static void vec1QuantizeJob(void *pCtx){ + Vec1QuantizeJob *p = (Vec1QuantizeJob*)pCtx; + const int nQuant = VEC1_MULTIMATCH_NVEC; + const int nElem = p->pModel->hdr.nElem; + int ii = 0; + + /* Handle the bulk of the vectors VEC1_QUANTIZE_JOB_QUANTA at a time. */ + for(; ii<=(p->nVector-nQuant); ii += nQuant){ + float *v = &p->aVec[nElem * ii]; + u8 *a = &p->aCode[p->nEncoded * ii]; + vec1QuantizeVectorN(p->pModel, p->aTmp, v, &p->aBucket[ii], a); + } + + /* Scalar tail - handle remaining vectors one at a time. */ + for(; iinVector; ii++){ + float *v = &p->aVec[nElem * ii]; + u8 *a = &p->aCode[p->nEncoded * ii]; + vec1QuantizeVector(p->pModel, p->aTmp, v, &p->aBucket[ii], a); + } +} + +static int vec1UpdateBase( + Vec1Tab *pTab, + i64 iId, + int iBucket +){ + sqlite3_stmt *pStmt = 0; + int rc = vec1GetSql(pTab, VEC1_SQL_UPDATE_BASE2, &pStmt); + if( rc==SQLITE_OK ){ + sqlite3_bind_int(pStmt, 1, iBucket); + sqlite3_bind_int64(pStmt, 2, iId); + sqlite3_step(pStmt); + vec1StmtReset(&rc, pStmt); + } + return rc; +} + + +static int vec1QuantizeJobFinish(void *pCtx, int rcin){ + Vec1QuantizeJob *p = (Vec1QuantizeJob*)pCtx; + const int nElem = p->pModel->hdr.nElem; + int rc = rcin; + int ii; + int bBucketBase = vec1ModelIsFlat(p->pModel); + int iOffMeta = 0; + + assert( bBucketBase==0 || p->pModel->hdr.nCodebook==0 ); + + for(ii=0; rc==SQLITE_OK && iinVector; ii++){ + int iBucket = p->aBucket[ii]; + const u8 *aStore; + if( p->nEncoded>0 ){ + aStore = &p->aCode[ii * p->nEncoded]; + }else{ + aStore = (const u8*)&p->aVec[ii * nElem]; + } + rc = vec1WriterQuantized(p->pWriter, p->aRowid[ii], iBucket, aStore); + if( bBucketBase && rc==SQLITE_OK ){ + rc = vec1UpdateBase(p->pWriter->pTab, p->aRowid[ii], iBucket); + } + if( rc==SQLITE_OK ){ + rc = vec1WriterMetaFromPacked(p->pWriter, iBucket, &p->meta, &iOffMeta); + } + } + + vec1BufferFree(&p->meta); + sqlite3_free(p); + return rc; +} + +static int vec1UpdateBaseRows( + Vec1Tab *pTab, + i64 iFirst, + i64 iLast +){ + sqlite3_stmt *pStmt = 0; + int rc = vec1GetSql(pTab, VEC1_SQL_ZERO_BASE_RANGE, &pStmt); + if( rc==SQLITE_OK ){ + sqlite3_bind_int64(pStmt, 1, iFirst); + sqlite3_bind_int64(pStmt, 2, iLast); + sqlite3_step(pStmt); + vec1StmtReset(&rc, pStmt); + } + return rc; +} + + +#define VEC1_ROWS_PER_BASE_UPDATE 64 + +/* +** Rebuild the entire ANN index from scratch, based on the current contents +** of the %_base table. +*/ +static int vec1RebuildIndex(Vec1Tab *pTab, int nThread){ + const int nElem = pTab->cfg.nElem; + sqlite3_stmt *pStmt = 0; + int rc = SQLITE_OK; + int bZeroBase = 0; /* True to set base.vector=0 */ + + Vec1Writer *p = 0; /* Write buffer object */ + Vec1JobQueue *pQueue = 0; /* Job queue for multi-threaded rebuilds */ + Vec1QuantizeJob *pQJ = 0; + int nRow = 0; + i64 iFirstInRange = VEC1_SMALLEST_INT64; + + assert( pTab->cfg.nElem==(int)pTab->mod.hdr.nElem || pTab->mod.hdr.nElem==0 ); + + /* Clear any existing ANN index */ + rc = vec1SqlExec(pTab, "DELETE FROM %Q.'%q_idx'", 0); + if( rc==SQLITE_OK ){ + rc = vec1SqlExec(pTab, "DELETE FROM %Q.'%q_meta'", 0); + } + + if( rc!=SQLITE_OK || (pTab->mod.hdr.flags & VEC1_MODEL_INDEX)==0 ) return rc; + + + /* Allocate a writer object. Create transposed centroids if required. */ + rc = vec1WriterAlloc(pTab, 1, &p); + if( rc==SQLITE_OK ){ + rc = vec1ModelAllocTCent(&pTab->mod); + } + + /* If multiple threads have been configured and the model is not 'flat', + ** create a job queue so that the rebuild can use multiple threads. There + ** is nothing for background threads to do for 'flat' indexes. */ + if( vec1ModelIsFlat(&pTab->mod) && pTab->mod.hdr.nBucket==0 ){ + bZeroBase = 1; + } +#if VEC1_THREADS + if( rc==SQLITE_OK && nThread>1 && bZeroBase==0 ){ + pQueue = vec1JobQueueNew(nThread-1); + if( pQueue==0 ){ + rc = SQLITE_NOMEM; + } + } +#endif /* VEC1_THREADS */ + + if( rc==SQLITE_OK ){ + rc = vec1GetSql(pTab, VEC1_SQL_SCAN_BASE, &pStmt); + } + + while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pStmt) ){ + i64 id = sqlite3_column_int64(pStmt, 0); + int nVec = 0; + const float *aVec = 0; + + nRow++; + if( bZeroBase && nRow==VEC1_ROWS_PER_BASE_UPDATE ){ + rc = vec1UpdateBaseRows(pTab, iFirstInRange, id); + if( rc!=SQLITE_OK ) break; + iFirstInRange = id+1; + nRow = 0; + } + + nVec = sqlite3_column_bytes(pStmt, 1); + aVec = (const float*)sqlite3_column_blob(pStmt, 1); + + if( nVec!=(sizeof_f32*nElem) ){ + rc = SQLITE_ERROR; + vec1VtabError( + pTab, "vec1: unexpected vector size in %q_base: %d", pTab->zName, nVec + ); + }else{ + if( bZeroBase ){ + int iBucket = -1; /* Must be -1 for vec1WriterVector()! */ + rc = vec1WriterVector(p, id, aVec, &iBucket); + if( rc==SQLITE_OK ){ + rc = vec1WriterMetaFromStmt(p, iBucket, pStmt); + } + }else{ + if( pQJ==0 ){ + int nEncoded = vec1EncodedVectorSize(&pTab->mod); + int nByte = sizeof(Vec1QuantizeJob) + + sizeof(i64) * VEC1_QUANTIZE_JOB_SZ + /* aRowid */ + pTab->nTmpVec * sizeof_f32 * VEC1_MULTIMATCH_NVEC +/* aTmp */ + nElem * sizeof_f32 * VEC1_QUANTIZE_JOB_SZ + /* aVec */ + sizeof(int) * VEC1_QUANTIZE_JOB_SZ + /* aBucket */ + nEncoded * VEC1_QUANTIZE_JOB_SZ; /* aCode */ + + pQJ = (Vec1QuantizeJob*)sqlite3_malloc(nByte); + if( pQJ==0 ){ + rc = SQLITE_NOMEM; + break; + }else{ + memset(pQJ, 0, sizeof(Vec1QuantizeJob)); + pQJ->pModel = &pTab->mod; + pQJ->pWriter = p; + pQJ->aRowid = (i64*)&pQJ[1]; + pQJ->aTmp = (float*)&pQJ->aRowid[VEC1_QUANTIZE_JOB_SZ]; + pQJ->aVec = &pQJ->aTmp[pTab->nTmpVec * VEC1_MULTIMATCH_NVEC]; + pQJ->aBucket = (int*)&pQJ->aVec[nElem * VEC1_QUANTIZE_JOB_SZ]; + pQJ->aCode = (u8*)&pQJ->aBucket[VEC1_QUANTIZE_JOB_SZ]; + pQJ->nEncoded = nEncoded; + memset(pQJ->aTmp, 0, sizeof_f32*pTab->nTmpVec*VEC1_MULTIMATCH_NVEC); + } + } + + memcpy(&pQJ->aVec[nElem * pQJ->nVector], aVec, nVec); + pQJ->aRowid[pQJ->nVector] = id; + pQJ->nVector++; + rc = vec1PackMetaValues(pTab, &pQJ->meta, pStmt); + + if( rc==SQLITE_OK && pQJ->nVector==VEC1_QUANTIZE_JOB_SZ ){ + rc = vec1JobQueueAddJob( + pQueue, vec1QuantizeJob, vec1QuantizeJobFinish, (void*)pQJ + ); + pQJ = 0; + } + } + } + } + + if( rc==SQLITE_OK && bZeroBase ){ + rc = vec1UpdateBaseRows(pTab, iFirstInRange, VEC1_LARGEST_INT64); + } + + if( pQJ ){ + if( rc==SQLITE_OK ){ + rc = vec1JobQueueAddJob( + pQueue, vec1QuantizeJob, vec1QuantizeJobFinish, (void*)pQJ + ); + }else{ + vec1BufferFree(&pQJ->meta); + sqlite3_free(pQJ); + } + } + rc = vec1JobQueueFinishJobs(pQueue, rc); + vec1JobQueueFree(pQueue); + + rc = vec1WriterFinish(p, rc); + vec1StmtReset(&rc, pStmt); + vec1ModelFreeTCent(&pTab->mod); + return rc; +} + +/* +** The table currently has a flat index installed - an index with +** nCodesize==0 - and bucket numbers instead of vectors in the %_base +** table. This function scans the index, copying full vectors back into +** the %_base table and deleting %_idx entries as it goes. +** +** Return SQLITE_OK if successful, or an SQLite error code otherwise. +*/ +static int vec1FlatToNone(Vec1Tab *pTab){ + int rc = SQLITE_OK; + sqlite3_stmt *pStmt = 0; + sqlite3_stmt *pUp = 0; + sqlite3_stmt *pDel = 0; + + rc = vec1GetSql(pTab, VEC1_SQL_SCAN_IDX, &pStmt); + if( rc==SQLITE_OK ){ + rc = vec1GetSql(pTab, VEC1_SQL_UPDATE_BASE, &pUp); + } + if( rc==SQLITE_OK ){ + rc = vec1GetSql(pTab, VEC1_SQL_DELETE_FROM_IDX, &pDel); + } + while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pStmt) ){ + Vec1FlatIter iter; + const u8 *aBlob = (const u8*)sqlite3_column_blob(pStmt, 0); + int nBlob = sqlite3_column_bytes(pStmt, 0); + i64 iRowid = sqlite3_column_int64(pStmt, 2); + rc = vec1FlatIterStart(pTab, &iter, aBlob, nBlob); + + while( rc==SQLITE_OK && iter.aVec!=0 ){ + rc = sqlite3_bind_blob(pUp, 1, iter.aVec, iter.szVec, SQLITE_STATIC); + sqlite3_bind_int64(pUp, 2, iter.iRowid); + sqlite3_step(pUp); + vec1StmtReset(&rc, pUp); + vec1FlatIterNext(&iter); + } + + if( rc==SQLITE_OK ){ + sqlite3_bind_int64(pDel, 1, iRowid); + sqlite3_step(pDel); + vec1StmtReset(&rc, pDel); + } + } + if( pUp ) sqlite3_clear_bindings(pUp); + vec1StmtReset(&rc, pStmt); + + return rc; +} + +/* +** Symbols for the commands the vtab accepts. These must match the array +** in vec1InterpretCmdName(). +*/ +#define VEC1_CMD_BLOCKSIZE 0 +#define VEC1_CMD_REBUILD 1 +#define VEC1_CMD_BLOCKSIZE_MIN 2 + +static int vec1InterpretCmdName(Vec1Tab *pTab, const char *zCmd, int *piCmd){ + const char *azCmd[] = { + "blocksize", + "rebuild", + "blocksize-min", + 0 + }; + int iCmd; + + for(iCmd=0; iCmd=iMin && iVal<=iMax ) return SQLITE_OK; + } + + vec1VtabError( + pTab, "'%s' requires an integer value between %lld and %lld", + zName, iMin, iMax + ); + return SQLITE_ERROR; +} + +static int vec1WriteConfigI64(Vec1Tab *pTab, int iField, i64 iVal){ + sqlite3_stmt *pStmt = 0; + int rc = SQLITE_OK; + + rc = vec1GetSql(pTab, VEC1_SQL_WRITE_CONFIG, &pStmt); + if( rc==SQLITE_OK ){ + sqlite3_bind_int(pStmt, 1, iField); + sqlite3_bind_int64(pStmt, 2, iVal); + sqlite3_step(pStmt); + rc = sqlite3_reset(pStmt); + } + + return rc; +} + +/* +** Fix the vector size to nVec *bytes*. This is done when the first +** vector is inserted into the table. Return SQLITE_OK if successful, or +** an SQLite error code if an error occurs. +*/ +static int vec1FixVectorSize(Vec1Tab *pTab, int nVec){ + int rc = SQLITE_OK; + + if( (nVec % sizeof_f32) + || (nVec<(VEC1_VECSIZE_MIN*sizeof_f32)) + || (nVec>(VEC1_VECSIZE_MAX*sizeof_f32)) + ){ + vec1VtabError(pTab, "invalid vector size: %d bytes", nVec); + rc = SQLITE_ERROR; + }else{ + int nElem = nVec/sizeof_f32; + rc = vec1WriteConfigI64(pTab, VEC1_CONFIG_NELEM, nElem); + if( rc==SQLITE_OK ) pTab->cfg.nElem = nElem; + } + + return rc; +} + + +/* +** Handle a "special" INSERT command on a vec1 table. A special INSERT takes +** the form: +** +** INSERT INTO vec1_table(cmd, vector) VALUES('cmd-name', pArg); +** +** The zCmd argument to this function is the text value specified for column +** "cmd" in the INSERT statement. Parameter pArg is the pArg value from the +** INSERT. +** +** Return SQLITE_OK if successful, or an SQLite error code otherwise. If +** an error code is returned, an error message may be left in pTab. +*/ +static int vec1SpecialInsert( + Vec1Tab *pTab, + const char *zCmd, + sqlite3_value *pArg +){ + int rc = SQLITE_OK; + int iCmd = 0; + + rc = vec1InterpretCmdName(pTab, zCmd, &iCmd); + + if( rc==SQLITE_OK ){ + switch( iCmd ){ + case VEC1_CMD_BLOCKSIZE: { + rc = vec1InterpretInteger( + pTab, pArg, "blocksize", VEC1_BLOCKSIZE_MIN, VEC1_BLOCKSIZE_MAX + ); + if( rc==SQLITE_OK ){ + rc = vec1WriteConfigI64( + pTab, VEC1_CONFIG_BLOCKSIZE, sqlite3_value_int64(pArg) + ); + } + break; + }; + case VEC1_CMD_BLOCKSIZE_MIN: { + rc = vec1InterpretInteger( + pTab, pArg, "blocksize-min", VEC1_BLOCKSIZE_MIN, VEC1_BLOCKSIZE_MAX + ); + if( rc==SQLITE_OK ){ + rc = vec1WriteConfigI64( + pTab, VEC1_CONFIG_BLOCKSIZE_MIN, sqlite3_value_int64(pArg) + ); + } + break; + } + + default: assert( iCmd==VEC1_CMD_REBUILD ); { + + /* TODO: Should this be disallowed if there are active readers? */ + if( rc==SQLITE_OK && sqlite3_value_type(pArg)!=SQLITE_NULL ){ + u8 aFlat[VEC1_HEADER_SIZE]; + Vec1Model mod; + int nByte = 0; + const u8 *aBlob = 0; + sqlite3_stmt *pStmt = 0; + + memset(&mod, 0, sizeof(mod)); + if( sqlite3_value_type(pArg)==SQLITE_BLOB ){ + nByte = sqlite3_value_bytes(pArg); + aBlob = sqlite3_value_blob(pArg); + }else{ + Vec1FlatIndex idx = {0, 0}; + const char *zText = (const char*)sqlite3_value_text(pArg); + char *zErr = 0; + + idx.eIndex = 0; + idx.eDistance = VEC1_DISTANCE_L2; + rc = vec1ParseJsonConfig( + pTab->db, zText, vec1FlatIndexCfg, &idx, &zErr + ); + if( rc==SQLITE_OK && idx.eIndex==0 ){ + zErr = sqlite3_mprintf("required parameter missing: index"); + rc = SQLITE_ERROR; + } + if( zErr!=0 ){ + vec1VtabError(pTab, "vec1: %z", zErr); + } + if( rc==SQLITE_OK ){ + vec1HeaderWrite(aFlat, + ((idx.eIndex==VEC1_INDEX_FLAT) ? VEC1_MODEL_INDEX : 0), + 0, 0, 0, idx.eDistance + ); + aBlob = aFlat; + nByte = VEC1_HEADER_SIZE; + } + } + + /* Decode the model blob here. Just to check if doing so causes any + ** errors. If it does, refuse to store it. */ + if( rc==SQLITE_OK ){ + rc = vec1DecodeModel(aBlob, nByte, &mod, &pTab->base.zErrMsg); + } + + if( rc==SQLITE_OK && mod.hdr.nElem>0 ){ + if( pTab->cfg.nElem==0 ){ + rc = vec1FixVectorSize(pTab, mod.hdr.nElem*sizeof_f32); + }else if( pTab->cfg.nElem!=(int)mod.hdr.nElem ){ + vec1VtabError(pTab, + "vec1: model/data vector size mismatch: %d/%d", + (int)mod.hdr.nElem, pTab->cfg.nElem + ); + rc = SQLITE_ERROR; + } + } + + /* Store the new model in the %_model table. */ + if( rc==SQLITE_OK ){ + const char zStore[] = "REPLACE INTO %Q.'%q_model' VALUES(1, ?)"; + rc = vec1PrepareSql(pTab, &pStmt, zStore, pTab->zDb, pTab->zName); + } + if( rc==SQLITE_OK ){ + rc = sqlite3_bind_blob(pStmt, 1, aBlob, nByte, SQLITE_STATIC); + sqlite3_step(pStmt); + vec1StmtFinalize(&rc, pStmt); + } + + /* Increment (or create) the model version number in %_config */ + if( rc==SQLITE_OK ){ + rc = vec1GetSql(pTab, VEC1_SQL_INCR_CONFIG, &pStmt); + } + if( rc==SQLITE_OK ){ + sqlite3_bind_int(pStmt, 1, VEC1_CONFIG_MODEL); + sqlite3_step(pStmt); + rc = sqlite3_reset(pStmt); + } + } + + if( rc==SQLITE_OK && vec1ModelIsFlat(&pTab->mod) ){ + rc = vec1FlatToNone(pTab); + } + if( rc==SQLITE_OK ){ + rc = vec1LoadConfig(pTab); + } + if( rc==SQLITE_OK ){ + rc = vec1RebuildIndex(pTab, pTab->pTabList->nThread); + } + } + } + } + + return rc; +} + +/* +** The maximum permitted number of tombstone entries in list containing +** nEntry entries. If this threshold is exceeded, the list is reorganized +** to reduce the number of tombstones to 0. +*/ +#define VEC1_TOMBSTONE_THRESHOLD(nEntry) ((nEntry * 2) / 10) + +/* +** Delete the %_base entry for the row with rowid=iRowid. Also zero-out +** the corresponding %_idx entry. +*/ +static int vec1DeleteByRowid(Vec1Tab *pTab, i64 iRowid){ + const u8 aTomb4[4] = {0xFF, 0xFF, 0xFF, 0xFF}; + const u8 aTomb8[8] = {0, 0, 0, 0, 0, 0, 0, 0}; + + sqlite3_stmt *pDelete = 0; + int rc = SQLITE_OK; + + rc = vec1GetSql(pTab, VEC1_SQL_DEL_LOOKUP_BASE, &pDelete); + if( rc==SQLITE_OK ){ + int iBucket = 0; + sqlite3_bind_int64(pDelete, 1, iRowid); + if( SQLITE_ROW==sqlite3_step(pDelete) && pTab->mod.hdr.nBucket>0 ){ + if( pTab->mod.hdr.nCodebook==0 ){ + iBucket = sqlite3_column_int(pDelete, 0); + }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 + ); + } + } + } + + 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 ){ + /* Missing row. This is an error. */ + rc = VEC1_CORRUPT; + }else if( (loc.nTombstone+1) > VEC1_TOMBSTONE_THRESHOLD(loc.nEntry) ){ + /* This delete would cause the list's tombstone count to exceed + ** the threshold. So reorganize the list and remove iRowid in + ** one go. */ + if( loc.pList ){ + vec1ListBuilderCompress(loc.pList, iRowid); + }else{ + /* Load the list into memory, compress it so there are no + ** tombstones, then write it back to the database. */ + Vec1ListBuilder list; + int nVal = sqlite3_blob_bytes(loc.pBlob); + u8 *aVal = sqlite3_malloc(nVal); + memset(&list, 0, sizeof(list)); + if( aVal==0 ){ + rc = SQLITE_NOMEM; + }else{ + rc = sqlite3_blob_read(loc.pBlob, aVal, nVal, 0); + } + if( rc==SQLITE_OK ){ + rc = vec1ListBuilderInit(&list, pTab, iBucket); + } + if( rc==SQLITE_OK ){ + /* Initialize iFirst and iLast to 0. vec1ListBuilderCompress() + ** will set the correct values. */ + rc = vec1ListBuilderLoad(&list, loc.iIdx, 0, 0, aVal, nVal); + } + if( rc==SQLITE_OK ){ + rc = vec1ListBuilderCompress(&list, iRowid); + } + if( rc==SQLITE_OK ){ + rc = vec1ListBuilderFlush(&list); + } + vec1ListBuilderFree(&list); + sqlite3_free(aVal); + } + }else{ + /* Replace the rowid with a tombstone and increment the tombstone + ** count. */ + const u8 *aTomb = loc.szRowid==4 ? aTomb4 : aTomb8; + if( loc.pList ){ + int iOff = loc.iEntry * loc.szRowid; + memcpy(&loc.pList->bufRowid.a[iOff], aTomb, loc.szRowid); + loc.pList->nTombstone++; + }else{ + int iOff = VEC1_LIST_SZHDR + (loc.iEntry * loc.szRowid); + rc = sqlite3_blob_write(loc.pBlob, aTomb, loc.szRowid, iOff); + if( rc==SQLITE_OK ){ + u8 aTS[4]; + vec1PutU32(aTS, loc.nTombstone+1); + rc = sqlite3_blob_write(loc.pBlob, aTS, sizeof(aTS), 8); + } + } + } + + if( loc.pBlob ){ + int rc2 = sqlite3_blob_close(loc.pBlob); + if( rc==SQLITE_OK ) rc = rc2; + } + } + } + } + + return rc; +} + +/* +** xUpdate +** +** argc/argv meanings: +** argc==1 DELETE +** argc>1 && argv[0]==NULL INSERT +** argc>1 && argv[0]!=NULL UPDATE +** +** argv[0] = old rowid (or NULL) +** argv[1] = new rowid (or NULL to auto-assign) +** argv[2+] = column values +*/ +static int vec1UpdateMethod( + sqlite3_vtab *pVtab, + int argc, + sqlite3_value **argv, + sqlite3_int64 *pRowid +){ + int rc = SQLITE_OK; + Vec1Tab *pTab = (Vec1Tab*)pVtab; + const int iCmd = 2 + VEC1_COLUMN_CMD; + int bDeleteRow = (argc==1); /* Delete row at end of this function */ + + /* Ensure the configuration has been loaded. */ + if( (rc = vec1LoadConfigIfRequired(pTab))!=SQLITE_OK ) return rc; + + if( argc>1 ){ + int nVec = sqlite3_value_bytes(argv[2+VEC1_COLUMN_VECTOR]); + const void *aVec = sqlite3_value_blob(argv[2+VEC1_COLUMN_VECTOR]); + int eType = sqlite3_value_type(argv[2+VEC1_COLUMN_VECTOR]); + + /* Check for a special INSERT. Handle these separately. */ + if( sqlite3_value_type(argv[iCmd])!=SQLITE_NULL ){ + const char *zCmd = (char*)sqlite3_value_text(argv[iCmd]); + sqlite3_value *pVal = argv[2+VEC1_COLUMN_ARG]; + if( sqlite3_value_type(pVal)==SQLITE_NULL ){ + pVal = argv[2+VEC1_COLUMN_VECTOR]; + } + return vec1SpecialInsert(pTab, zCmd, pVal); + } + + /* Check that the supplied vector is the right type and size. If this + ** is the first vector written to this table, fix the vector size + ** now. */ + if( eType!=SQLITE_BLOB ){ + rc = vec1VectorTypeError(pTab, eType); + }else if( pTab->cfg.nElem>0 ){ + if( nVec!=pTab->cfg.nElem*sizeof_f32 ){ + rc = vec1VectorSizeError(pTab, nVec); + } + }else{ + rc = vec1FixVectorSize(pTab, nVec); + } + + /* Check that the new vector does not contain any NaN or Infinity values */ + if( rc==SQLITE_OK ){ + rc = vec1VectorIsNonFinite((float*)aVec, pTab->cfg.nElem); + if( rc!=SQLITE_OK ){ + vec1VtabError(pTab, "vec1: non-finite value not allowed in vector"); + } + } + + /* If this is an UPDATE that does not modify the rowid, then delete + ** the existing row now. Or, if it is an UPDATE that does modify + ** the rowid, schedule deletion of the existing row at the end + ** of this function. */ + if( rc==SQLITE_OK && sqlite3_value_type(argv[0])==SQLITE_INTEGER ){ + if( sqlite3_value_int64(argv[0])==sqlite3_value_int64(argv[1]) ){ + rc = vec1DeleteByRowid(pTab, sqlite3_value_int64(argv[0])); + }else{ + bDeleteRow = 1; + } + } + + /* If this is an INSERT or UPDATE, write the new row. */ + if( rc==SQLITE_OK ){ + int nVec = sqlite3_value_bytes(argv[2+VEC1_COLUMN_VECTOR]); + int iBucket = 0; + + /* If there is an index (i.e. not index:"none"), then allocate an + ** index writer. */ + if( rc==SQLITE_OK + && (pTab->mod.hdr.flags & VEC1_MODEL_INDEX) + && pTab->pWriter==0 + ){ + rc = vec1WriterAlloc(pTab, 0, &pTab->pWriter); + assert( rc!=SQLITE_OK || pTab->pWriter!=0 ); + } + + /* Write to the %_base table. */ + if( rc==SQLITE_OK ){ + sqlite3_stmt *pInsert = 0; + + /* If required, quantize the vector. This is because in + ** some configurations, the bucket is written into the %_base + ** table, so it must be determined now, before the base table entry + ** is written */ + if( pTab->pWriter ){ + Vec1Writer *p = pTab->pWriter; + vec1QuantizeVector(&pTab->mod, p->aResidual, aVec, &iBucket, p->aPQ); + } + + *pRowid = 0; + rc = vec1GetSql(pTab, VEC1_SQL_INSERT_BASE, &pInsert); + if( rc==SQLITE_OK ){ + int ii; + sqlite3_bind_value(pInsert, 1, argv[1]); + if( vec1ModelIsFlat(&pTab->mod) ){ + sqlite3_bind_int(pInsert, 2, iBucket); + }else{ + sqlite3_bind_blob(pInsert, 2, aVec, nVec, SQLITE_STATIC); + } + for(ii=0; iinMeta; ii++){ + sqlite3_bind_value(pInsert, 3+ii, argv[2+VEC1_COLUMN_VECTOR+1+ii]); + } + + sqlite3_step(pInsert); + rc = sqlite3_reset(pInsert); + *pRowid = sqlite3_last_insert_rowid(pTab->db); + } + } + + /* If the table has been trained, write the %_idx entry. And, if + ** there are any meta-data columns, the %_meta entries as well. */ + if( pTab->mod.hdr.flags & VEC1_MODEL_INDEX ){ + if( rc==SQLITE_OK ){ + rc = vec1WriterVector(pTab->pWriter, *pRowid, aVec, &iBucket); + } + if( rc==SQLITE_OK ){ + sqlite3_value **aMeta = &argv[2+VEC1_COLUMN_VECTOR+1]; + rc = vec1WriterMetaFromArray(pTab->pWriter, iBucket, aMeta); + } + } + } + } + + if( rc==SQLITE_OK && bDeleteRow ){ + rc = vec1DeleteByRowid(pTab, sqlite3_value_int64(argv[0])); + } + + return rc; +} + +static int vec1FinishWriter(Vec1Tab *pTab, int bDiscard){ + int rc = SQLITE_OK; + if( bDiscard ){ + vec1WriterFinish(pTab->pWriter, SQLITE_ERROR); + }else{ + rc = vec1WriterFinish(pTab->pWriter, SQLITE_OK); + } + pTab->pWriter = 0; + return rc; +} + +static int vec1SyncMethod(sqlite3_vtab *pVtab){ + int rc = SQLITE_OK; + Vec1Tab *pTab = (Vec1Tab*)pVtab; + if( (rc = vec1LoadConfigIfRequired(pTab))!=SQLITE_OK ) return rc; + return vec1FinishWriter(pTab, 0); +} + +static int vec1BeginMethod(sqlite3_vtab *pVtab){ + Vec1Tab *pTab = (Vec1Tab*)pVtab; + int rc = SQLITE_OK; + rc = vec1LoadConfig(pTab); + return rc; +} + +/* +** xRename method for vec1 virtual table. This renames all shadow tables +** and the %_idx_idx index on the %_idx table. +*/ +static int vec1RenameMethod(sqlite3_vtab *pVtab, const char *zNew){ + const char zFmt[] = + "ALTER TABLE %Q.'%q_config' RENAME TO '%q_config';" + "ALTER TABLE %Q.'%q_base' RENAME TO '%q_base';" + "ALTER TABLE %Q.'%q_idx' RENAME TO '%q_idx';" + "ALTER TABLE %Q.'%q_model' RENAME TO '%q_model';" + "ALTER TABLE %Q.'%q_meta' RENAME TO '%q_meta';" + "UPDATE %Q.sqlite_schema SET name = '%q_idx_idx', sql=" + " 'CREATE INDEX ' || quote('%q_idx_idx') || " + " ' ON ' || quote('%q_idx') || '(bucket, first, last)'" + "WHERE type='index' AND name='%q_idx_idx';" + ; + + Vec1Tab *pTab = (Vec1Tab*)pVtab;/* Virtual table object */ + int rc = SQLITE_OK; /* Return value */ + int bDefensive = 0; /* Initial value of DEFENSIVE */ + int bWritable = 0; /* Initial value of WRITABLE_SCHEMA */ + char *zSql = 0; /* SQL script to rename shadow objects */ + + /* Query the initial values of the DEFENSIVE and WRITABLE_SCHEMA db-config + ** options. Then clear DEFENSIVE and set WRITABLE_SCHEMA so that we can + ** UPDATE the sqlite_schema entry of the index directly. */ + sqlite3_db_config(pTab->db, SQLITE_DBCONFIG_DEFENSIVE, -1, &bDefensive); + sqlite3_db_config(pTab->db, SQLITE_DBCONFIG_WRITABLE_SCHEMA, -1, &bWritable); + sqlite3_db_config(pTab->db, SQLITE_DBCONFIG_DEFENSIVE, 0, (int*)0); + sqlite3_db_config(pTab->db, SQLITE_DBCONFIG_WRITABLE_SCHEMA, 1, (int*)0); + + /* Run the SQL script to rename the shadow tables and index */ + zSql = sqlite3_mprintf(zFmt, + pTab->zDb, pTab->zName, zNew, /* %_config */ + pTab->zDb, pTab->zName, zNew, /* %_base */ + pTab->zDb, pTab->zName, zNew, /* %_idx */ + pTab->zDb, pTab->zName, zNew, /* %_model */ + pTab->zDb, pTab->zName, zNew, /* %_meta */ + pTab->zDb, zNew, zNew, zNew, pTab->zName + ); + if( zSql==0 ){ + rc = SQLITE_NOMEM; + }else{ + rc = sqlite3_exec(pTab->db, zSql, 0, 0, &pTab->base.zErrMsg); + sqlite3_free(zSql); + } + + /* Restore the values of DEFENSIVE and WRITABLE_SCHEMA */ + sqlite3_db_config(pTab->db,SQLITE_DBCONFIG_DEFENSIVE, bDefensive, (int*)0); + sqlite3_db_config(pTab->db,SQLITE_DBCONFIG_WRITABLE_SCHEMA,bWritable,(int*)0); + + return rc; +} + +static int vec1RollbackMethod(sqlite3_vtab *pVtab){ + Vec1Tab *pTab = (Vec1Tab*)pVtab; + vec1UnloadModel(pTab); + return vec1FinishWriter(pTab, 1); +} +static int vec1SavepointMethod(sqlite3_vtab *pVtab, int iSavepoint){ + Vec1Tab *pTab = (Vec1Tab*)pVtab; + int rc = SQLITE_OK; + UNUSED_PARAMETER(iSavepoint); + if( (rc = vec1LoadConfigIfRequired(pTab))!=SQLITE_OK ) return rc; + return vec1FinishWriter(pTab, 0); +} +static int vec1ReleaseMethod(sqlite3_vtab *pVtab, int iSavepoint){ + Vec1Tab *pTab = (Vec1Tab*)pVtab; + int rc = SQLITE_OK; + UNUSED_PARAMETER(iSavepoint); + if( (rc = vec1LoadConfigIfRequired(pTab))!=SQLITE_OK ) return rc; + return vec1FinishWriter(pTab, 0); +} +static int vec1RollbackToMethod(sqlite3_vtab *pVtab, int iSavepoint){ + Vec1Tab *pTab = (Vec1Tab*)pVtab; + UNUSED_PARAMETER(iSavepoint); + vec1FinishWriter(pTab, 1); + vec1UnloadConfig(pTab); + return SQLITE_OK; +} + +/* +** This function is used by the integrity-check code to handle meta-data. +** Argument pBuf contains a meta-data record, complete with VEC1_META_SZHDR +** byte header. When this function is called, (*piOff) contains the offset +** into this buffer of the next meta-data value to read. +** +** When called, this function reads one meta-data value from (*piOff) and +** advances (*piOff) to point to the next value. If pVal is NULL, it returns +** SQLITE_OK at that point. Or, if pVal is not NULL, it is compared to +** the value just read. If they match, SQLITE_OK is returned. Otherwise, +** SQLITE_ERROR. +*/ +static int vec1IntegrityMetaValue( + Vec1Buffer *pBuf, + int *piOff, + sqlite3_value *pVal +){ + u32 f = vec1GetU32(pBuf->a); + int iOff = *piOff; + Vec1MetaValue val; + int rc = SQLITE_OK; + + memset(&val, 0, sizeof(val)); + if( (f & VEC1_META_1BYTEINT) ){ + val.iVal = pBuf->a[iOff++]; + if( val.iVal==VEC1_META_1BYTENULL ){ + val.eType = SQLITE_NULL; + }else{ + val.eType = SQLITE_INTEGER; + } + }else if( (f & VEC1_META_4BYTEINT) ){ + val.iVal = (int)vec1GetU32(&pBuf->a[iOff]); + iOff += sizeof_u32; + if( (u32)val.iVal==VEC1_META_4BYTENULL ){ + val.eType = SQLITE_NULL; + }else{ + val.eType = SQLITE_INTEGER; + } + }else if( (f & VEC1_META_REAL) ){ + u64 v = vec1GetU64(&pBuf->a[iOff]); + iOff += sizeof_f64; + if( v==VEC1_META_REALNULL ){ + val.eType = SQLITE_NULL; + }else{ + val.eType = SQLITE_FLOAT; + memcpy(&val.fVal, &v, sizeof_f64); + } + }else{ + vec1MetaValueRead(pBuf, &iOff, &val); + } + + if( pVal ){ + int eType = sqlite3_value_type(pVal); + int bMatch = 0; + switch( eType ){ + + case SQLITE_NULL: + bMatch = (val.eType==SQLITE_NULL); + break; + + case SQLITE_INTEGER: { + i64 iVal = sqlite3_value_int64(pVal); + bMatch = ( + (val.eType==SQLITE_INTEGER && val.iVal==iVal) + || (val.eType==SQLITE_FLOAT && val.fVal==(double)iVal) + ); + break; + } + + case SQLITE_FLOAT: { + double fVal = sqlite3_value_double(pVal); + bMatch = (val.eType==SQLITE_FLOAT && val.fVal==fVal); + break; + } + + case SQLITE_TEXT: { + int nVal = sqlite3_value_bytes(pVal); + if( val.eType==SQLITE_TEXT && val.iVal==nVal ){ + const char *zVal = (const char*)sqlite3_value_text(pVal); + bMatch = (0==memcmp(val.pPtr, zVal, nVal)); + } + break; + } + + default: { + int nVal = sqlite3_value_bytes(pVal); + if( val.eType==SQLITE_BLOB && val.iVal==nVal ){ + const u8 *zVal = (const u8*)sqlite3_value_blob(pVal); + bMatch = (0==memcmp(val.pPtr, zVal, nVal)); + } + assert( eType==SQLITE_BLOB ); + break; + } + } + + if( bMatch==0 ) rc = SQLITE_ERROR; + } + + *piOff = iOff; + return rc; +} + +/* +** Return true if the difference between f1 and f2 is greater than +** (fTol * max(f1, f2)). Or false if they differ by more than this. +*/ +static int vec1FloatWithinTolerance(float f1, float f2, float fTol){ + if( f1==0.0 || f2==0.0 ) return 0; + if( f2>f1 ){ + if( ((f2-f1) / f2) > fTol ) return 0; + }else{ + if( ((f1-f2) / f1) > fTol ) return 0; + } + return 1; +} + +/* +** Check that the encoded version of aVec[] matches the encoded vector +** stored at entry iEntry of aBlob[]. For BQ encodings, the bit-quantized +** vector must match exactly and the stored norm must be within a small +** tolerance of the calculated value. For PQ encodings, the encoded vectors +** are compared using their reconstruction errors, allowing for small +** differences in floating-point arithmetic. +** +** Return non-zero if the encoded vector is inconsistent with aVec[] or if +** an error occurs. Set *pRc to SQLITE_NOMEM if temporary space cannot be +** allocated. +*/ +static int vec1IntegrityCheckEncoded( + int *pRc, /* IN/OUT: Error code */ + Vec1Model *pMod, /* Model used to quantize vectors */ + const float *aVec, /* Vector to compare quantized entry to */ + int iEntry, /* Entry of aBlob[] to compare to */ + const u8 *aBlob, /* %_idx.val value */ + Vec1Buffer *pBuf /* Buffer to use for temp space */ +){ + const int nEnc = vec1EncodedVectorSize(pMod); + const int nElem = (int)pMod->hdr.nElem; + + /* Allocate the maximum amount of space required by this function - + ** enough for one full vector and two encoded vectors. */ + int nReq = (nElem * sizeof_f32) + nEnc*2; + if( vec1BufferSize(pBuf, nReq) ){ + *pRc = SQLITE_NOMEM; + return 1; + }else{ + const int nCodebook = pMod->hdr.nCodebook; + float *v1 = (float*)pBuf->a; /* Space for decoded vector */ + u8 *aEnc = (u8*)&v1[nElem]; /* Encoded version of aVec[] */ + u8 *aIdx = &aEnc[nEnc]; /* Encoded vector read from aBlob[] */ + + u32 flags = vec1GetU32(&aBlob[0]); + u32 nEntry = (int)vec1GetU32(&aBlob[4]); + int iOut = 0; + int iOff; + int ii; + + /* Encode the vector into aEnc */ + vec1EncodeVector(pMod, aVec, aEnc); + + /* Read the encoded version of the vector from aBlob[] into aIdx[] */ + iOff = nEntry * ((flags & VEC1_LIST_64BIT) ? 8 : 4) + VEC1_LIST_SZHDR; + if( pMod->hdr.flags & VEC1_MODEL_BITQUANT ){ + int iNorm = iOff + VEC1_LIST_NORM_OFFSET(iEntry, nCodebook); + memcpy(aIdx, &aBlob[iNorm], sizeof_f32); + iOff += VEC1_LIST_CODE_OFFSET(iEntry, nCodebook); + iOut = sizeof_f32; + }else{ + iOff += VEC1_LIST_BLOCKED_OFFSET(iEntry, nCodebook); + } + for(ii=0; iihdr.flags & VEC1_MODEL_BITQUANT ){ + float norm1; + float norm2; + memcpy(&norm1, aEnc, sizeof_f32); + memcpy(&norm2, aIdx, sizeof_f32); + + /* Check the norms are close enough. If not, return 1. */ + if( vec1FloatWithinTolerance(norm1, norm2, fTol)==0 ) return 1; + + /* Check the arrays of bits are identical. If not, return 1. */ + if( memcmp(&aEnc[sizeof_f32], &aIdx[sizeof_f32], nEnc-sizeof_f32) ){ + return 1; + } + }else{ + float e1 = 0.0; /* aEnc[] squared reconstruction error */ + float e2 = 0.0; /* aIdx[] squared reconstruction error */ + vec1DecodeVector(pMod, aEnc, v1); + e1 = vec1L2Dist(v1, aVec, nElem); + vec1DecodeVector(pMod, aIdx, v1); + e2 = vec1L2Dist(v1, aVec, nElem); + if( vec1FloatWithinTolerance(e1, e2, fTol)==0 ) return 1; + } + } + } + + return 0; +} + +/* +** Integrity check method. If the table has been supplied with a model, +** loop through the %_idx table. For each +*/ +static int vec1IntegrityMethod( + sqlite3_vtab *pVTab, + const char *zSchema, + const char *zTabName, + int mFlags, + char **pzErr +){ + Vec1Tab *pTab = (Vec1Tab*)pVTab; + int rc = SQLITE_OK; + sqlite3_stmt *pScan = 0; + sqlite3_stmt *pLookup = 0; + float *aResidual = 0; + char *zErr = 0; + Vec1Buffer *aBufMeta = 0; + Vec1Buffer tmp = {0, 0, 0}; + u8 *aPQ = 0; + + UNUSED_PARAMETER2(mFlags, zSchema); + + rc = vec1LoadConfig(pTab); + if( rc==SQLITE_OK && (pTab->mod.hdr.flags & VEC1_MODEL_INDEX) ){ + int bFlat = vec1ModelIsFlat(&pTab->mod); + Vec1Model *pMod = &pTab->mod; + const int nCodebook = pMod->hdr.nCodebook; + const int nElem = pTab->cfg.nElem; + + int *aiOffMeta = 0; + i64 nTotalEntry = 0; + u8 *aIdxPQ = 0; + + if( nCodebook>0 && pMod->hdr.nBucket>0 ){ + aResidual = vec1MallocZero(pTab->nTmpVec * sizeof_f32); + if( aResidual==0 ){ + rc = SQLITE_NOMEM; + goto integrity_failed; + } + } + + if( nCodebook>0 ){ + aPQ = (u8*)vec1MallocZero((nCodebook+sizeof_f32) * 2); + if( aPQ==0 ){ + rc = SQLITE_NOMEM; + goto integrity_failed; + } + aIdxPQ = &aPQ[nCodebook + sizeof_f32]; + } + + rc = vec1GetSql(pTab, VEC1_SQL_SCAN_IDX, &pScan); + if( rc==SQLITE_OK ){ + rc = vec1GetSql(pTab, VEC1_SQL_LOOKUP_BASE, &pLookup); + } + + /* If there are one or more meta-value columns, allocate space for + ** buffers to load each of them from the database */ + if( rc==SQLITE_OK && pTab->nMeta>0 ){ + int nByte = pTab->nMeta * (sizeof(Vec1Buffer) + sizeof(int)); + aBufMeta = (Vec1Buffer*)vec1MallocZero(nByte); + if( aBufMeta==0 ){ + rc = SQLITE_NOMEM; + }else{ + aiOffMeta = (int*)&aBufMeta[pTab->nMeta]; + } + } + + while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pScan) ){ + const u8 *aBlob = sqlite3_column_blob(pScan, 0); + int nBlob = sqlite3_column_bytes(pScan, 0); + int iBucket = sqlite3_column_int(pScan, 1); + i64 iId = sqlite3_column_int64(pScan, 2); + int szRowid = 4; + + int nEntry = 0; + u32 flags = 0; + int nTombstone = 0; + int ii = 0; + int iMeta = 0; + int nNonTombstone = 0; + + if( vec1CheckIdxSize(pTab, aBlob, nBlob) ){ + const char *zFmt = "%s: %%_idx entry id=%lld is corrupt"; + zErr = sqlite3_mprintf(zFmt, zTabName, sqlite3_column_int64(pScan, 2)); + goto integrity_failed; + } + + flags = vec1GetU32(&aBlob[0]); + nEntry = (int)vec1GetU32(&aBlob[4]); + nTombstone = (int)vec1GetU32(&aBlob[8]); + if( flags & VEC1_LIST_64BIT ){ + szRowid = 8; + } + + /* Load the array for each meta-value column from disk */ + for(iMeta=0; iMetanMeta; iMeta++){ + rc = vec1ReadMeta(pTab, &aBufMeta[iMeta], iId, iMeta); + if( rc!=SQLITE_OK ){ + if( rc==SQLITE_CORRUPT_VTAB ){ + const char *zFmt = "%s: error reading meta-list id=%lld,meta=%d"; + zErr = sqlite3_mprintf(zFmt, zTabName, iId, iMeta); + rc = SQLITE_OK; + } + goto integrity_failed; + } + aiOffMeta[iMeta] = VEC1_META_SZHDR; + } + + for(ii=0; iinMeta; iMeta++){ + vec1IntegrityMetaValue(&aBufMeta[iMeta], &aiOffMeta[iMeta], 0); + } + continue; + } + + nNonTombstone++; + + sqlite3_bind_int64(pLookup, 1, iRowid); + if( sqlite3_step(pLookup)==SQLITE_ROW ){ + bExists = 1; + + /* Check that the meta-value arrays contain the correct data */ + for(iMeta=0; iMetanMeta; iMeta++){ + Vec1Buffer *pBuf = &aBufMeta[iMeta]; + sqlite3_value *pVal = sqlite3_column_value(pLookup, 2+iMeta); + + if( vec1IntegrityMetaValue(pBuf, &aiOffMeta[iMeta], pVal) ){ + const char *zFmt = "%s: meta-value value mismatch for row %lld"; + zErr = sqlite3_mprintf(zFmt, zTabName, iRowid); + goto integrity_failed; + } + } + + if( bFlat ){ + int iBaseBucket = sqlite3_column_int(pLookup, 1); + if( iBaseBucket!=iBucket ){ + const char *zFmt = + "%s: bucket value in %%_base row %lld is incorrect"; + zErr = sqlite3_mprintf(zFmt, zTabName, iRowid); + goto integrity_failed; + } + }else{ + 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 ){ + const char *zFmt = "%s: vector in %%_base row %lld is wrong size"; + zErr = sqlite3_mprintf(zFmt, zTabName, iRowid); + goto integrity_failed; + } + + /* 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 ){ + const char *zFmt = "%s: vector from row %lld is in wrong bucket"; + zErr = sqlite3_mprintf(zFmt, zTabName, iRowid); + goto integrity_failed; + } + + /* Check that the contents of the index entry look right. */ + { + /* Calculate a PQ code based on the transformed vector. Store + ** this in aPQ[]. For binary quantization, aPQ[] contains the + ** norm followed by nCodebook bytes of data. For PQ or OPQ, + ** aPQ[] just contains the nCodebook bytes of encoded vector. */ + const float *aEnc = (const float*)aTransform; + int szElem0 = 0; + if( (pMod->hdr.flags & VEC1_MODEL_RESIDUAL) ){ + vec1Sub(aResidual, aEnc, &pMod->aCentroid[iCalc*nElem], nElem); + aEnc = aResidual; + } + if( vec1IntegrityCheckEncoded(&rc, pMod, aEnc, ii, aBlob, &tmp) ){ + const char *zType; + const char *zFmt = + "%s: %%_idx %s does not match calculated %s for row %lld"; + zType = (pMod->hdr.flags & VEC1_MODEL_BITQUANT) ? "BQ" : "PQ"; + zErr = sqlite3_mprintf(zFmt, zTabName, zType, zType, iRowid); + goto integrity_failed; + } + } + } + } + vec1StmtReset(&rc, pLookup); + + if( rc==SQLITE_OK && bExists==0 ){ + const char *zFmt = "%s: %%_idx row %lld missing from %%_base"; + zErr = sqlite3_mprintf(zFmt, zTabName, iRowid); + goto integrity_failed; + } + } + + if( rc==SQLITE_OK && (nNonTombstone+nTombstone)!=nEntry ){ + const char *zFmt = "%s: bad tombstone count"; + zErr = sqlite3_mprintf(zFmt, zTabName); + goto integrity_failed; + } + + nTotalEntry += nNonTombstone; + } + + if( rc==SQLITE_OK ){ + i64 nGot = -1; + sqlite3_stmt *pStmt = 0; + const char *zCount = "SELECT count(*) FROM %Q.'%q_base'"; + rc = vec1PrepareSql(pTab, &pStmt, zCount, pTab->zDb, pTab->zName); + if( rc==SQLITE_OK ){ + if( sqlite3_step(pStmt)==SQLITE_ROW ){ + nGot = sqlite3_column_int64(pStmt, 0); + } + } + vec1StmtFinalize(&rc, pStmt); + if( rc==SQLITE_OK && nGot!=nTotalEntry ){ + const char *zFmt = + "%s: wrong number of entries in %%_base - have %lld, expect %lld"; + zErr = sqlite3_mprintf(zFmt, zTabName, nGot, nTotalEntry); + goto integrity_failed; + } + } + } + + integrity_failed: + *pzErr = zErr; + sqlite3_free(aResidual); + sqlite3_free(aPQ); + vec1BufferFree(&tmp); + if( aBufMeta ){ + int ii; + for(ii=0; iinMeta; ii++){ + vec1BufferFree(&aBufMeta[ii]); + } + sqlite3_free(aBufMeta); + } + sqlite3_free(pTab->base.zErrMsg); + pTab->base.zErrMsg = 0; + vec1StmtReset(&rc, pScan); + vec1StmtReset(&rc, pLookup); + return rc; +} + +/************************************************************************* +** Start of vec1_cat virtual table. +*/ + +#define VEC1CAT_SCHEMA \ + "CREATE TABLE vec1cat(database, name, model, qprofile)" + +typedef struct vec1cat_vtab vec1cat_vtab; +struct vec1cat_vtab { + sqlite3_vtab base; + Vec1TabList *pList; +}; + +typedef struct vec1cat_cursor vec1cat_cursor; +struct vec1cat_cursor { + sqlite3_vtab_cursor base; + sqlite3_int64 iRowid; +}; + +static int vec1catConnectMethod( + sqlite3 *db, + void *pAux, + int argc, + const char *const *argv, + sqlite3_vtab **ppVtab, + char **pzErr +){ + vec1cat_vtab *pNew; + int rc; + + UNUSED_PARAMETER(pzErr); + UNUSED_PARAMETER2(argv, argc); + + rc = sqlite3_declare_vtab(db, VEC1CAT_SCHEMA); + if( rc==SQLITE_OK ){ + pNew = vec1MallocZero(sizeof(*pNew)); + *ppVtab = (sqlite3_vtab*)pNew; + if( pNew==0 ) return SQLITE_NOMEM; + pNew->pList = (Vec1TabList*)pAux; + } + return rc; +} + +static int vec1catDisconnectMethod(sqlite3_vtab *pVtab){ + vec1cat_vtab *p = (vec1cat_vtab *)pVtab; + sqlite3_free(p); + return SQLITE_OK; +} + +static int vec1catOpenMethod(sqlite3_vtab *pVtab, sqlite3_vtab_cursor **ppCur){ + vec1cat_cursor *pCur; + UNUSED_PARAMETER(pVtab); + pCur = vec1MallocZero(sizeof(*pCur)); + if( pCur==0 ) return SQLITE_NOMEM; + *ppCur = &pCur->base; + return SQLITE_OK; +} + +static int vec1catCloseMethod(sqlite3_vtab_cursor *cur){ + vec1cat_cursor *pCur = (vec1cat_cursor *)cur; + sqlite3_free(pCur); + return SQLITE_OK; +} + +static int vec1catBestIndexMethod( + sqlite3_vtab *pVtab, + sqlite3_index_info *pIdxInfo +){ + UNUSED_PARAMETER(pVtab); + pIdxInfo->estimatedCost = (double)10; + pIdxInfo->estimatedRows = 10; + return SQLITE_OK; +} + +static int vec1catFilterMethod( + sqlite3_vtab_cursor *cur, + int idxNum, + const char *idxStr, + int argc, + sqlite3_value **argv +){ + Vec1TabList *pList = ((vec1cat_vtab*)(cur->pVtab))->pList; + vec1cat_cursor *pCur = (vec1cat_cursor *)cur; + sqlite3_stmt *pSql1 = 0; + int rc = SQLITE_OK; + + const char *zSql1 = "SELECT name FROM pragma_database_list"; + const char *zSql2 = + "WITH tables(name) AS (" + " SELECT name FROM %Q.sqlite_schema " + " WHERE sql LIKE 'CREATE%%VIRTUAL%%USING%%vec1%%'" + ")" + "SELECT * FROM tables CROSS JOIN pragma_table_info(tables.name, %Q)"; + + UNUSED_PARAMETER2(argc, argv); + UNUSED_PARAMETER2(idxStr, idxNum); + + rc = sqlite3_prepare_v2(pList->db, zSql1, -1, &pSql1, 0); + if( rc==SQLITE_OK ){ + while( sqlite3_step(pSql1)==SQLITE_ROW ){ + const char *zDb = (const char*)sqlite3_column_text(pSql1, 0); + sqlite3_stmt *pSql2 = 0; + char *z = vec1MPrintf(&rc, zSql2, zDb, zDb); + if( rc==SQLITE_OK ){ + rc = sqlite3_prepare(pList->db, z, -1, &pSql2, 0); + sqlite3_free(z); + } + if( rc==SQLITE_OK ){ + while( sqlite3_step(pSql2)==SQLITE_ROW ); + vec1StmtFinalize(&rc, pSql2); + } + } + vec1StmtFinalize(&rc, pSql1); + } + + pCur->iRowid = 1; + return rc; +} + +static int vec1catNextMethod(sqlite3_vtab_cursor *cur){ + vec1cat_cursor *pCur = (vec1cat_cursor *)cur; + pCur->iRowid++; + return SQLITE_OK; +} + +static Vec1Tab *vec1catGetTable(vec1cat_cursor *pCur){ + Vec1TabList *pList = ((vec1cat_vtab*)(pCur->base.pVtab))->pList; + Vec1Tab *pTab = pList->pFirst; + int ii; + + for(ii=1; pTab && iiiRowid; ii++){ + pTab = pTab->pTabNext; + } + + return pTab; +} + +static int vec1catEofMethod(sqlite3_vtab_cursor *cur){ + return vec1catGetTable((vec1cat_cursor*)cur)==0; +} + +/* +** xColumn() method for ephemeral virtual table vec1cat. +*/ +static int vec1catColumnMethod( + sqlite3_vtab_cursor *cur, + sqlite3_context *ctx, + int i +){ + vec1cat_cursor *pCur = (vec1cat_cursor*)cur; + Vec1Tab *pTab = vec1catGetTable(pCur); + int rc = SQLITE_OK; + + switch( i ){ + case 0: /* database */ + sqlite3_result_text(ctx, pTab->zDb, -1, SQLITE_TRANSIENT); + break; + + case 1: /* name */ + sqlite3_result_text(ctx, pTab->zName, -1, SQLITE_TRANSIENT); + break; + + case 2: { /* model */ + Vec1ModelHeader *pHdr = &pTab->mod.hdr; + const char *zDist = 0; + char *zRet = 0; + + rc = vec1LoadConfig(pTab); + zDist = ((pHdr->eDistance==VEC1_DISTANCE_L2) ? "l2" : "cos"); + if( pHdr->nCodebook==0 && pHdr->nBucket==0 ){ + zRet = vec1MPrintf(&rc, + "{\"index\": \"%s\", \"distance\": \"%s\"}", + ((pHdr->flags & VEC1_MODEL_INDEX) ? "flat" : "none"), zDist + ); + }else{ + const char *zQuant = "none"; + if( pHdr->nCodebook ){ + if( pHdr->flags & VEC1_MODEL_BITQUANT ){ + zQuant = "bq"; + }else if( pHdr->flags & VEC1_MODEL_ROTATE ){ + zQuant = "opq"; + }else{ + zQuant = "pq"; + } + } + + zRet = vec1MPrintf(&rc, "{" + "\"index\": \"trained\", " + "\"distance\": \"%s\", " + "\"nbucket\": %d, " + "\"quantizer\": \"%s\", " + "\"codesize\": %d, " + "\"residual\": %s, " + "\"wht\": %s}", + zDist, pHdr->nBucket, zQuant, pHdr->nCodebook, + (pHdr->flags & VEC1_MODEL_RESIDUAL) ? "true" : "false", + (pHdr->flags & VEC1_MODEL_WHT) ? "true" : "false" + ); + } + + if( rc==SQLITE_OK ){ + sqlite3_result_text(ctx, zRet, -1, vec1SqliteFree); + }else{ + sqlite3_result_error_code(ctx, rc); + } + break; + } + + default: { /* qprofile */ + const char *azName[VEC1_QINSTR_NCOUNTER] = { + "total", /* VEC1_QINSTR_TOTAL */ + "init", /* VEC1_QINSTR_INITQUERY */ + "coarse", /* VEC1_QINSTR_COARSE */ + "idxread", /* VEC1_QINSTR_IDXREAD */ + "metaread", /* VEC1_QINSTR_METAREAD */ + "metascan", /* VEC1_QINSTR_METASCAN */ + "lut", /* VEC1_QINSTR_LUT */ + "pqscan", /* VEC1_QINSTR_PQSCAN */ + "finalsort" /* VEC1_QINSTR_FINALSORT */ + }; + + char *zRet = 0; + int ii; + + for(ii=0; iiaCount[ii], pTab->aCycle[ii] + ); + } + zRet = vec1MPrintf(&rc, "{%z\n}", zRet); + + if( rc==SQLITE_OK ){ + sqlite3_result_text(ctx, zRet, -1, vec1SqliteFree); + }else{ + sqlite3_result_error_code(ctx, rc); + } + } + } + return SQLITE_OK; +} + +static int vec1catRowidMethod( + sqlite3_vtab_cursor *cur, + sqlite3_int64 *pRowid +){ + vec1cat_cursor *pCur = (vec1cat_cursor *)cur; + *pRowid = pCur->iRowid; + return SQLITE_OK; +} + +/* +** End of vec1_cat virtual table. +*************************************************************************/ + + +/* +** Used to get/set per-connection transient parameters. Syntax: +** +** SELECT vec1_config(PARAM) +** SELECT vec1_config(PARAM, VALUE) +** +** There are currently two parameters: +** +** nthread: +** Default number of threads to use for training and rebuilding. +** +** nprobe: +** Default nprobe value for queries that do not specify one. +** +*/ +static void vec1ConfigFunc( + sqlite3_context *pCtx, + int nVal, + sqlite3_value **aVal +){ + Vec1TabList *pList = (Vec1TabList*)sqlite3_user_data(pCtx); + const char *zParam = (const char*)sqlite3_value_text(aVal[0]); + + if( 0==sqlite3_stricmp(zParam, "nthread") ){ + if( nVal==2 ){ + int nNew = sqlite3_value_int(aVal[1]); + if( nNew<1 || nNew>VEC1_MAX_NTHREAD ){ + vec1ResultErrorF(pCtx, "vec1: " + "nthread requires an integer value between 1 and %d", + VEC1_MAX_NTHREAD + ); + return; + } +#if VEC1_THREADS + pList->nThread = nNew; +#endif + } + sqlite3_result_int(pCtx, pList->nThread); + }else if( 0==sqlite3_stricmp(zParam, "nprobe") ){ + if( nVal==2 ){ + int eNumeric = sqlite3_value_numeric_type(aVal[1]); + double fNew = 0.0; + if( eNumeric==SQLITE_INTEGER || eNumeric==SQLITE_FLOAT ){ + fNew = sqlite3_value_double(aVal[1]); + } + if( fNew<=0.0 ){ + vec1ResultErrorF(pCtx, + "vec1: nprobe must be a numeric value greater than 0.0" + ); + return; + } + pList->nProbeArg = fNew; + } + sqlite3_result_double(pCtx, pList->nProbeArg); + }else{ + vec1ResultErrorF(pCtx, "vec1: no such parameter: %s", zParam); + } +} + +/* +** Destructor for the vec1 module registered using sqlite3_create_module_v2(). +** Free the Vec1TabList structure. SQLite should have ensured any virtual +** tables are closed before this is called. +*/ +static void vec1ListFree(void *pCtx){ + Vec1TabList *pList = (Vec1TabList*)pCtx; + pList->nRef--; + if( pList->nRef==0 ){ + assert( pList->pFirst==0 ); + sqlite3_free(pList); + } +} + +static int initExtension( + sqlite3 *db, + char **pzErr, + const sqlite3_api_routines *pApi +){ + static sqlite3_module vec1catModule = { + 0, /* iVersion */ + 0, /* xCreate */ + vec1catConnectMethod, /* xConnect */ + vec1catBestIndexMethod, /* xBestIndex */ + vec1catDisconnectMethod, /* xDisconnect */ + 0, /* xDestroy */ + vec1catOpenMethod, /* xOpen */ + vec1catCloseMethod, /* xClose */ + vec1catFilterMethod, /* xFilter */ + vec1catNextMethod, /* xNext */ + vec1catEofMethod, /* xEof */ + vec1catColumnMethod, /* xColumn */ + vec1catRowidMethod, /* xRowid */ + 0, /* xUpdate */ + 0, /* xBegin */ + 0, /* xSync */ + 0, /* xCommit */ + 0, /* xRollback */ + 0, /* xFindMethod */ + 0, /* xRename */ + 0, /* xSavepoint */ + 0, /* xRelease */ + 0, /* xRollbackTo */ + 0, /* xShadowName */ + 0, /* xIntegrity */ + }; + + static sqlite3_module vec1Module = { + 4, /* iVersion */ + vec1CreateMethod, /* xCreate */ + vec1ConnectMethod, /* xConnect */ + vec1BestIndexMethod, /* xBestIndex */ + vec1DisconnectMethod, /* xDisconnect */ + vec1DestroyMethod, /* xDestroy */ + vec1Open, /* xOpen */ + vec1Close, /* xClose */ + vec1FilterMethod, /* xFilter */ + vec1NextMethod, /* xNext */ + vec1EofMethod, /* xEof */ + vec1ColumnMethod, /* xColumn */ + vec1Rowid, /* xRowid */ + vec1UpdateMethod, /* xUpdate */ + vec1BeginMethod, /* xBegin */ + vec1SyncMethod, /* xSync */ + 0, /* xCommit */ + vec1RollbackMethod, /* xRollback */ + 0, /* xFindFunction */ + vec1RenameMethod, /* xRename */ + vec1SavepointMethod, /* xSavepoint */ + vec1ReleaseMethod, /* xRelease */ + vec1RollbackToMethod, /* xRollbackTo */ + 0, /* xShadowName */ + vec1IntegrityMethod /* xIntegrity */ + }; + + static struct Func { + const char *zName; + int nArg; + void (*x)(sqlite3_context*,int,sqlite3_value**); + } aFunc[] = { + { "vec1_info", 0, vec1InfoFunc }, + { "vec1_to_json", 1, vec1ToJsonFFunc }, + { "vec1_from_json", 1, vec1FromJsonFunc }, + + { "vec1_l2_distance", 2, vec1DistanceFuncL2 }, + { "vec1_cos_distance", 2, vec1DistanceFuncCos }, + + { "vec1_config", 1, vec1ConfigFunc }, + { "vec1_config", 2, vec1ConfigFunc }, + + /* Currently undocumented functions. These may become documented when + ** datatypes other than 32-bit float are supported for vectors */ + { "vec1_to_json_f", 1, vec1ToJsonFFunc }, + { "vec1_to_json_i", 1, vec1ToJsonIFunc }, + }; + + static struct AggFunc { + const char *zName; + int nArg; + void (*xStep)(sqlite3_context*,int,sqlite3_value**); + void (*xFinal)(sqlite3_context*); + } aAgg[] = { + { "vec1_train", 1, vec1TrainStep, vec1TrainFinal }, + { "vec1_train", 2, vec1TrainStep, vec1TrainFinal }, + }; + + int ii = 0; + int rc = SQLITE_OK; + Vec1TabList *pList = vec1MallocZero(sizeof(Vec1TabList)); + + UNUSED_PARAMETER2(pApi, pzErr); + + /* Assuming the Vec1TabList object was allocated without error, populate + ** it and create the main virtual table module - vec1. */ + if( pList==0 ){ + rc = SQLITE_NOMEM; + }else{ + pList->nThread = 1; + pList->db = db; + pList->nProbeArg = VEC1_QUERY_DEFAULT_NPROBE; + + pList->nRef = 1; + rc = sqlite3_create_module_v2(db, "vec1", &vec1Module, pList, vec1ListFree); + } + + /* Create the other vtab module - vec1cat */ + if( rc==SQLITE_OK ){ + pList->nRef++; + rc = sqlite3_create_module_v2( + db, "vec1cat", &vec1catModule, pList, vec1ListFree + ); + } + + /* Create the scalar functions */ + for(ii=0; rc==SQLITE_OK && iizName, p->nArg, SQLITE_UTF8|SQLITE_INNOCUOUS, (void*)pList, 0, + p->xStep, p->xFinal + ); + } + + /* If an error occurred, unregister everything */ + if( rc!=SQLITE_OK ){ + sqlite3_create_module(db, "vec1", 0, 0); + sqlite3_create_module(db, "vec1cat", 0, 0); + + for(ii=0; ii +static int vec1HasAvx2(void) +{ + int regs[4]; + __cpuid(regs, 1); + int ecx = regs[2]; + if (!(ecx & (1 << 28))) return 0; // AVX + if (!(ecx & (1 << 27))) return 0; // OSXSAVE + unsigned long long xcr0 = _xgetbv(0); + if ((xcr0 & 0x6) != 0x6) return 0; + __cpuidex(regs, 7, 0); + int ebx = regs[1]; + return (ebx & (1 << 5)) != 0; // AVX2 +} +# else +static int vec1HasAvx2(){ + /* Clang/GCC */ + __builtin_cpu_init(); + return __builtin_cpu_supports("avx2"); +} +# endif + +static int initMultiExtension( + sqlite3 *db, + char **pzErr, + const sqlite3_api_routines *pApi +){ + int rc = SQLITE_OK; + if( vec1HasAvx2() ){ + /* Use AVX2 */ + rc = sqlite3Vec1InitAVX2(db, pzErr, pApi); + }else{ + /* Fall back to scalar. */ + rc = initExtension(db, pzErr, pApi); + } + + return rc; +} +# define initExtension initMultiExtension +# endif /* VEC1SIMD==SCALAR */ +#endif /* defined(VEC1SIMD) */ + +#if !defined(VEC1SIMD) || VEC1SIMD==SCALAR + +#ifndef VEC1_STATIC +SQLITE_EXTENSION_INIT1; +#endif + +#ifdef _WIN32 +__declspec(dllexport) +#endif +int sqlite3_extension_init( + sqlite3 *db, + char **pzErrMsg, + const sqlite3_api_routines *pApi +){ +#ifdef SQLITE_EXTENSION_INIT2 + SQLITE_EXTENSION_INIT2(pApi); +#endif + return initExtension(db, pzErrMsg, pApi); +} + +#ifdef VEC1_STATIC +int sqlite3_vec1_extra_init(const char *z){ + sqlite3_auto_extension((void(*)(void))initExtension); + return SQLITE_OK; +} +#endif + +#endif /* !defined(VEC1SIMD) || VEC1SIMD==SCALAR */ + diff --git a/main.mk b/main.mk index 5b2efb2949..4a2c28634c 100644 --- a/main.mk +++ b/main.mk @@ -1786,6 +1786,40 @@ fts5.c: $(FTS5_SRC) $(B.tclsh) fts5.o: fts5.c $(DEPS_OBJ_COMMON) $(EXTHDR) $(T.cc.extension) -c fts5.c +# +# vec1 loadable extension. Not part of the amalgamation or any +# distributed package; built only by explicitly running "make vec1.so". +# +# If $(CFLAGS.vec1.avx2) is not empty (configure sets it to "-mavx2 +# -mfma" when the compiler accepts them), vec1.c is compiled twice - +# once with scalar code and once with AVX2 - and both objects linked +# into a single extension that picks one at runtime based on the CPU. +# Otherwise a single scalar-only build is done. +# +# Deliberately does not use $(T.cc.extension), as that defines +# SQLITE_CORE, which is wrong for a loadable extension. +# +# $(CFLAGS.vec1.opt) is appended after $(CFLAGS), so it overrides any +# -O option there (e.g. configure's default -O2). vec1 relies on the +# unrolling and inlining that -O3 provides. To build with the -O level +# from $(CFLAGS) instead, pass "CFLAGS.vec1.opt=" to make. +# +CFLAGS.vec1.avx2 ?= +CFLAGS.vec1.opt ?= -O3 +T.cc.vec1 = $(T.compile) -I. -I$(TOP)/src $(CFLAGS.vec1.opt) +vec1$(T.dll): $(TOP)/ext/vec1/vec1.c sqlite3.h + @if test -n "$(CFLAGS.vec1.avx2)"; then set -x; \ + $(T.cc.vec1) -DVEC1SIMD=SCALAR -c $(TOP)/ext/vec1/vec1.c \ + -o vec1-scalar.o || exit $$?; \ + $(T.cc.vec1) -DVEC1SIMD=AVX2 $(CFLAGS.vec1.avx2) \ + -c $(TOP)/ext/vec1/vec1.c -o vec1-avx2.o || exit $$?; \ + $(T.compile) $(LDFLAGS.shlib) -o $@ vec1-scalar.o vec1-avx2.o \ + $(LDFLAGS.math) $(LDFLAGS.pthread) || exit $$?; \ + else set -x; \ + $(T.cc.vec1) $(LDFLAGS.shlib) -o $@ $(TOP)/ext/vec1/vec1.c \ + $(LDFLAGS.math) $(LDFLAGS.pthread) || exit $$?; \ + fi + sqlite3rbu.o: $(TOP)/ext/rbu/sqlite3rbu.c $(DEPS_OBJ_COMMON) $(EXTHDR) $(T.cc.extension) -c $(TOP)/ext/rbu/sqlite3rbu.c @@ -2500,6 +2534,7 @@ help: tidy: rm -f *.o *.obj *.c *.da *.bb *.bbg gmon.* *.rws sqlite3$(T.exe) rm -f fts5.h keywordhash.h opcodes.h sqlite3.h sqlite3ext.h sqlite3session.h + rm -f vec1$(T.dll) rm -rf .libs .deps tsrc .target_source rm -f lemon$(B.exe) sqlite*.tar.gz rm -f mkkeywordhash$(B.exe) mksourceid$(B.exe) diff --git a/manifest b/manifest index eb887e9c49..b883842ac7 100644 --- a/manifest +++ b/manifest @@ -1,13 +1,13 @@ -C Do\snot\sinherit\sthe\sOPFS\sSAHPool's\sxSleep\smethod\sfrom\sthe\sdefault\sVFS,\sas\sdoing\sso\scan\scause\sthe\sthread\sto\sfreeze.\sThis\sVFS\sonly\sdeals\swith\scontention\soriginating\sin\sthe\ssame\sthread,\sso\sxSleep()\sneeds\sto\sbe\sa\sno-op.\sAddresses\s[forum:3f0794c5d8|bug\sreport\s2026-09-30T12:52:54Z]. -D 2026-09-30T13:23:56.388 +C Import\svec1\ssources\sinto\sdirectory\sext/vec1/\sof\sthis\sproject. +D 2026-09-30T14:49:16.274 F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea F AGENTS.md 44ddfb38ef23e277888bd70ae378e6cbf5f5cab0e2a4420bd21d89b6caff7a4a F LICENSE.md 6bc480fc673fb4acbc4094e77edb326267dd460162d7723c7f30bee2d3d9e97d -F Makefile.in 1235ff3793bb8a2687dfa34ed541f7d6d5dd215b5a4f36acb6311941afa58651 +F Makefile.in b9e64c91cf8bea2305e1ca88bd3eb022afa93963a8561ea8290c65d4fafec03e F Makefile.linux-generic bd3e3cacd369821a6241d4ea1967395c962dfe3057e38cb0a435cee0e8b789d0 -F Makefile.msc a726ea17c0b7c1e3b2e58dbcd71e89b99b970f05cb4f6672a11e172bdf704313 +F Makefile.msc 7e3d397adf43dcf18efd5348970a085da7d57955f0b72c65b35776ab3f0a81e7 F README.md 8a7487de00e6d6d360cec15cc114cbb397207e1b975901683561506401c6d48c F VERSION 99cf3be5f13d091183e4314b7fc2e0c0e69accfbe64608b45a313338bbdd7b62 F art/icon-243x273.gif 9750b734f82fdb3dc43127753d5e6fbf3b62c9f4e136c2fbf573b2f57ea87af5 @@ -16,7 +16,7 @@ F art/sqlite370.eps aa97a671332b432a54e1d74ff5e8775be34200c2 F art/sqlite370.ico af56c1d00fee7cd4753e8631ed60703ed0fc6e90 F art/sqlite370.jpg d512473dae7e378a67e28ff96a34da7cb331def2 F art/sqlite370.svg 40b7e2fe8aac3add5d56dd86ab8d427a4eca5bcb3fe4f8946cb3794e1821d531 -F auto.def 44a0d1bf09d78355fc88251ccbf8e64e6341fd89c11de68a01c3645e53a2bade +F auto.def 313ae320e59bdd7d7e73e753c0fba8983e6e0cd963cca8e7685f43a0185d1059 F autoconf/Makefile.fallback 22fe523eb36dfce31e0f6349f782eb084e86a5620b2b0b4f84a2d6133f53f5ac F autoconf/Makefile.in efab1f56c7961d301efc030baa4391b9faae38733d94385a3d56b54720a74aba F autoconf/Makefile.msc e6c596e6e63ea17aa7a97eb8ded18cd984c8dd4203766644fbd57b8fcf720225 @@ -584,6 +584,63 @@ F ext/session/sessionwor.test 6fd9a2256442cebde5b2284936ae9e0d54bde692d0f5fd009e F ext/session/sqlite3session.c a5723efc88d340c65b391da8a4d628ca17bec49edf0a11a55653d5d208db6ea1 F ext/session/sqlite3session.h ca7c4422c1514a95056cc8d333217df6b1829d39058126b1de85d10cd62d7a9c F ext/session/test_session.c f465f13c661919f8ba4485a9d0690c4df5bff1cc79e962cfdafa9ff376d5119f +F ext/vec1/Makefile 791bd52e40d5b65026c63c8778dbdfc6d66083457ee50cf873d6483d98f43437 +F ext/vec1/Makefile.msc 66fbdf78ebb987bdbe646e3b1c895fc8785e14c50666f6694b840a0cdd3b7f9e +F ext/vec1/test/vec1_common.tcl 72226d9ee7cea8ad331905b28496392e4771f7b7c9d2890575795eddefc19db3 +F ext/vec1/test/vec1aa.test 1e30f623ae9254590b4bc538447d816809e1e4281060d756391ac616952cb5fa +F ext/vec1/test/vec1ab.test 04a19655e475dff8f9cf0be8140eb1034497fb36ecdf1f979779bf3ee7edfe6f +F ext/vec1/test/vec1ac.test d22cdefd7718e71330b23174523981be78d55645c65c273445e5b8986ce3a6a0 +F ext/vec1/test/vec1alter.test 6cb7ea2677d79a4ea040f27f5cd8902060bbd00fb2f9f94f3715b7a7bab91c69 +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/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/vec1create.test fdf7dc7d4e62d5654c1ac27bb9aea9451f024280dce5d0ec97d2698a040b3914 +F ext/vec1/test/vec1delete.test 1dc56bbc8a6753991dbc8cdca558a933ae6280c01bee261b1b2561e0390b57e6 +F ext/vec1/test/vec1error.test 3d1b74cd535dd5c9d303309f0b18ec606b100139a6483cbbd495b0aa3a9c8a09 +F ext/vec1/test/vec1fault1.test 8b34a9e68a9877db43707cdfdd388f229f8267793475ceb828dd931afe6fd3b6 +F ext/vec1/test/vec1fault2.test 3863f5d553e19a8dafafc2b976c295a62e33075373be204b81a9b714b2062295 +F ext/vec1/test/vec1fault3.test e4c4ff00b361bb9b961c134e252fd5dc364acd3e9d2abbf2b8d8ed4e223b9406 +F ext/vec1/test/vec1fault4.test 3c2bf8b30769118d7158aae9d8f7c13b17faf28245ac17d1b8e1d0977422d1bc +F ext/vec1/test/vec1fault5.test 5f41482c7108b2383ec318f0f23d0b4e2bad5fbe9526f244f3237f0e32b0a706 +F ext/vec1/test/vec1fault6.test 4f86d9fbd31552a1461e2dcdf350b4d5413b5536382a669a0010dea59f0c2865 +F ext/vec1/test/vec1fault7.test 92ebab2b519e79c41be71ef3f9f438f4a3358b475164c41288d674be102a1d24 +F ext/vec1/test/vec1fault8.test a2da772d96539b25574cc4cf0cdcf291931da1a416a6883662e10f5c92275c69 +F ext/vec1/test/vec1fault9.test 150a88218168844e80f1f600c7cd3189f01e05b69f8c899c0ef42997f51846ee +F ext/vec1/test/vec1fixed.test 9f1ecc7a67bfe1afb8a3c9e173c2f7990b14f0ad1bbb869d6fe30d247d3b2271 +F ext/vec1/test/vec1flat.test e6fbbe5dd6a46019d5913a427a9d80e25771694a7db0c51ecb60ecb96c8c49b1 +F ext/vec1/test/vec1func.test e27a3aed8abc7dcea38ec87ccd8f91d30eba551fe1cc1e9f9b0e39cee9ce574d +F ext/vec1/test/vec1growth.test 9dc2abf5a79bd8d23e5718717dce4001ae89d807688b3b1a9f6b1f0d666ece42 +F ext/vec1/test/vec1intck.test 45fd8d3619b8116d2dfc0fbc8ae76c044de176f96a316e16ea1c8dff7d2b9359 +F ext/vec1/test/vec1intck2.test a25d41c108b1c63a20a90125543f5ecb3eb696e6c3716a3c2c6325cd8e18b026 +F ext/vec1/test/vec1ivf.test 32ece2817587ce8a38e2b9798c7bd0c1e991bc65cf59c03bd91a655ff7a81e06 +F ext/vec1/test/vec1join.test 7f13b951303ed6aebf18bc70b70673ab6dbd7ac3ca99a22a47c7cdb369245218 +F ext/vec1/test/vec1limit.test 175972f00c4230896c5e09e0944c8b74ae1b39f08683a8e0538b25ced2c2b1de +F ext/vec1/test/vec1meta.test 5a3ae0338a6168bab4a381817b13425028b6ab04dff270065077be471d77a5fc +F ext/vec1/test/vec1meta2.test 4997a26728d24aaf8219b3c168209df00f8174eec521415ff94dfdd2d7db5da0 +F ext/vec1/test/vec1meta3.test 91907adb451e17b777f526dd39c8cdfd2c5928a47b3a864eb2df6b812a8ce461 +F ext/vec1/test/vec1nan.test 3ff3fe4b0f33a5c1f4129ba318443e7d927fd9dd6fc339349fea3c57fe140a8c +F ext/vec1/test/vec1pq.test 81d74f516971a3883782d2d443efe3f6a7501d4c9f980a758e1c2eb8b5f22885 +F ext/vec1/test/vec1query.test 7120a2824b9f6563f7986c436204870c9d6e4af2bff3a39d38ef277188ef9189 +F ext/vec1/test/vec1rollback.test cb2a117153eb3bd3b97a19e8fce6a880b930862b0b50d240f80569ad593a6764 +F ext/vec1/test/vec1rotate.test a90773e8ef8259f81c793cb335e3c023295231416c37d89e387d524304dadfde +F ext/vec1/test/vec1rotate2.test 7e4e06b3f970190c4ac28256db7aa924796b9a1fe1985941e35db4604c8717a0 +F ext/vec1/test/vec1round.test b38735c7e5b0af833505cc639d60816cfca2143b7461a1b443815c4be2b9d3ed +F ext/vec1/test/vec1scalar.test bf2acbf26a2e9b2494de5e55f049045ae7da4a0352ddeafa6d6bdfd7e6d5a909 +F ext/vec1/test/vec1stream.test 54f325ef75508f2b1ff82d883ac1c12bb4494f652468ef195b736c97640c34a7 +F ext/vec1/test/vec1stream2.test ad0aa9dc70fdccd46b31d6c19352387a04394bb631437a1bc9e169292cc797de +F ext/vec1/test/vec1train.test 5f60a53963ed971b56c7dcc14bcccee433f8f554f1148ed1bff8231047e310b6 +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/vec1bench.tcl de201e8b468de035065dd8dfdaa171d64760693deb19e6d5725e23840e6cca1e +F ext/vec1/tool/vec1bench_addcols.tcl 8c509253d07b179928f2ee32e3591a2c70d13c914217e1328c01afa148717051 +F ext/vec1/tool/vec1bench_vibe2db.py 84ddb0236bc3d02a0393abce41986a1b7dad7f0d83884576f8318c2883e3aed3 +F ext/vec1/vec1.c d6c96e5272447a08847a5031ceac121f3483a2a26283bb651f6c1d85179b0b2b F ext/wasm/GNUmakefile cc28e65dec12f1b992957152d8395484f4fcf7f94e6b6736a09ee4f48e7ec048 F ext/wasm/README-dist.txt f01081a850ce38a56706af6b481e3a7878e24e42b314cfcd4b129f0f8427066a F ext/wasm/README.md 2e87804e12c98f1d194b7a06162a88441d33bb443efcfe00dc6565a780d2f259 @@ -665,7 +722,7 @@ F ext/wasm/tests/opfs/sahpool/index.html be736567fd92d3ecb9754c145755037cbbd2bca F ext/wasm/tests/opfs/sahpool/sahpool-pausing.js f264925cfc82155de38cecb3d204c36e0f6991460fff0cb7c15079454679a4e2 F ext/wasm/tests/opfs/sahpool/sahpool-worker.js bd25a43fc2ab2d1bafd8f2854ad3943ef673f7c3be03e95ecf1612ff6e8e2a61 F magic.txt 5ade0bc977aa135e79e3faaea894d5671b26107cc91e70783aa7dc83f22f3ba0 -F main.mk 54a08023012d78cc2ac7712d1c624ce66461e920c3e0241ebed5383aba6d5361 +F main.mk 86f616b7ef7931c76d421bb76d05c6f90f45fa5d365eac178e19f8a54995671c F make.bat a136fd0b1c93e89854a86d5f4edcf0386d211e5d5ec2434480f6eea436c7420c F mptest/config01.test 3c6adcbc50b991866855f1977ff172eb6d901271 F mptest/config02.test 4415dfe36c48785f751e16e32c20b077c28ae504 @@ -1524,7 +1581,7 @@ F test/pcache.test c8acbedd3b6fd0f9a7ca887a83b11d24a007972b F test/pcache2.test 8a801d2b8e4b0ebb99701f026a67a9e84634c8aa24799a842c44003b93250da1 F test/pendingrace.test e99efc5ab3584da3dfc8cd6a0ec4e5a42214820574f5ea24ee93f1d84655f463 F test/percentile.test fd78896fa882fa4fbf693640097859721f3629926c2ccf804af5bcb7001fd35b -F test/permutations.test 1fd848d38291131d0c7eed41321311ee5d58ae5b7ce6f37a3d79ebaff7e1b95c +F test/permutations.test c919e9d9b9d17faccd543128ac714efbba0606c7051bcb54f264006eecdb4627 F test/pg_common.tcl 3b27542224db1e713ae387459b5d117c836a5f6e328846922993b6d2b7640d9f F test/pragma.test 7d07b7bb76e273215d6a20c4f83c3062cc28976c737ccb70a686025801e86c8f F test/pragma2.test e5d5c176360c321344249354c0c16aec46214c9f @@ -2221,8 +2278,11 @@ F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee F tool/warnings.sh a554d13f6e5cf3760f041b87939e3d616ec6961859c3245e8ef701d1eafc2ca2 F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f F tool/winmain.c 00c8fb88e365c9017db14c73d3c78af62194d9644feaf60e220ab0f411f3604c -P fa44643f65d6815c8c21ec25e7aa6e39a00fdc818d1d058e03123042e45884e1 -R 3a63c51ccb58b2a22c00bdb1b8f5247c -U stephan -Z 901de4bdf6f0c30a65a3330271325068 +P c9dd4d88e44a9eae4ea11d0ce5491c7ac2ac2d60a3bc83ed485812234be374da +R abde61b2c412631e87e77f443f340a02 +T *branch * vec1 +T *sym-vec1 * +T -sym-trunk * +U dan +Z 907e792789daaeb3c2cc5400f2140971 # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.tags b/manifest.tags index bec971799f..755f477a72 100644 --- a/manifest.tags +++ b/manifest.tags @@ -1,2 +1,2 @@ -branch trunk -tag trunk +branch vec1 +tag vec1 diff --git a/manifest.uuid b/manifest.uuid index a3c528ac13..b6e42217f2 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -c9dd4d88e44a9eae4ea11d0ce5491c7ac2ac2d60a3bc83ed485812234be374da +3a0876d973868be7b6f4156a434cd09098c7ab1abdc7f0ef4f3d12a03de68b24 diff --git a/test/permutations.test b/test/permutations.test index 91c41adef7..a3c1b774a6 100644 --- a/test/permutations.test +++ b/test/permutations.test @@ -96,6 +96,7 @@ foreach f [glob -nocomplain \ $testdir/../ext/recover/*.test \ $testdir/../ext/rbu/*.test \ $testdir/../ext/intck/*.test \ + $testdir/../ext/vec1/test/*.test \ ] { lappend alltests $f } From 806fd19e326f85ec547de260d809d71925adaec8 Mon Sep 17 00:00:00 2001 From: dan Date: Wed, 30 Sep 2026 16:38:56 +0000 Subject: [PATCH 2/6] Fix definition of VEC1_META_4BYTEMIN so that it works with msvc. FossilOrigin-Name: c9b4c9c0aaf0c314312381558ed9cf60aac800dfb4f5701d4d2e1f5268b7f081 --- ext/vec1/vec1.c | 2 +- manifest | 15 ++++++--------- manifest.uuid | 2 +- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/ext/vec1/vec1.c b/ext/vec1/vec1.c index c21cb372b1..51fad12d22 100644 --- a/ext/vec1/vec1.c +++ b/ext/vec1/vec1.c @@ -5426,7 +5426,7 @@ static void vec1ModelFreeTCent(Vec1Model *pMod){ #define VEC1_META_REALNULL 0x7FF8000000000001 -#define VEC1_META_4BYTEMIN -2147483648 +#define VEC1_META_4BYTEMIN (-2147483647-1) #define VEC1_META_4BYTEMAX +2147483646 #define VEC1_META_4BYTENULL +2147483647 diff --git a/manifest b/manifest index b883842ac7..679abc716d 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Import\svec1\ssources\sinto\sdirectory\sext/vec1/\sof\sthis\sproject. -D 2026-09-30T14:49:16.274 +C Fix\sdefinition\sof\sVEC1_META_4BYTEMIN\sso\sthat\sit\sworks\swith\smsvc. +D 2026-09-30T16:38:56.907 F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea @@ -640,7 +640,7 @@ F ext/vec1/tool/vec1_algo_test.c c62b828a3e46a39a8656f26b74804906cc3160758f8afc6 F ext/vec1/tool/vec1bench.tcl de201e8b468de035065dd8dfdaa171d64760693deb19e6d5725e23840e6cca1e F ext/vec1/tool/vec1bench_addcols.tcl 8c509253d07b179928f2ee32e3591a2c70d13c914217e1328c01afa148717051 F ext/vec1/tool/vec1bench_vibe2db.py 84ddb0236bc3d02a0393abce41986a1b7dad7f0d83884576f8318c2883e3aed3 -F ext/vec1/vec1.c d6c96e5272447a08847a5031ceac121f3483a2a26283bb651f6c1d85179b0b2b +F ext/vec1/vec1.c 06282d8fc75ede7fdbd747fe0cb5baf6b9c420c76110a5143ef11aac6d26aa3e F ext/wasm/GNUmakefile cc28e65dec12f1b992957152d8395484f4fcf7f94e6b6736a09ee4f48e7ec048 F ext/wasm/README-dist.txt f01081a850ce38a56706af6b481e3a7878e24e42b314cfcd4b129f0f8427066a F ext/wasm/README.md 2e87804e12c98f1d194b7a06162a88441d33bb443efcfe00dc6565a780d2f259 @@ -2278,11 +2278,8 @@ F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee F tool/warnings.sh a554d13f6e5cf3760f041b87939e3d616ec6961859c3245e8ef701d1eafc2ca2 F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f F tool/winmain.c 00c8fb88e365c9017db14c73d3c78af62194d9644feaf60e220ab0f411f3604c -P c9dd4d88e44a9eae4ea11d0ce5491c7ac2ac2d60a3bc83ed485812234be374da -R abde61b2c412631e87e77f443f340a02 -T *branch * vec1 -T *sym-vec1 * -T -sym-trunk * +P 3a0876d973868be7b6f4156a434cd09098c7ab1abdc7f0ef4f3d12a03de68b24 +R e2a6a361772528d8b9fca843be0f8c92 U dan -Z 907e792789daaeb3c2cc5400f2140971 +Z a95742aa236b4568479fbd30ade20f73 # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index b6e42217f2..16ecb45f99 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -3a0876d973868be7b6f4156a434cd09098c7ab1abdc7f0ef4f3d12a03de68b24 +c9b4c9c0aaf0c314312381558ed9cf60aac800dfb4f5701d4d2e1f5268b7f081 From 23a07066638df55189af2eb41e9b0056a37cf208 Mon Sep 17 00:00:00 2001 From: dan Date: Wed, 30 Sep 2026 16:41:58 +0000 Subject: [PATCH 3/6] Fix problems in vec1rollback.test preventing it from running on windows. FossilOrigin-Name: d15cf3890230d5a870e50448c69d640fedcbd15f32788b808cb1f212399e2796 --- ext/vec1/test/vec1rollback.test | 3 +++ manifest | 12 ++++++------ manifest.uuid | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/ext/vec1/test/vec1rollback.test b/ext/vec1/test/vec1rollback.test index ee1bd629e3..90d26154be 100644 --- a/ext/vec1/test/vec1rollback.test +++ b/ext/vec1/test/vec1rollback.test @@ -38,6 +38,7 @@ do_test 1.3 { INSERT INTO v1(rowid, v) VALUES(5, random_vector(4)); } db2 } {1 {vec1: unexpected vector blob size 16 bytes, expected 32}} +db2 close #------------------------------------------------------------------------- reset_db @@ -69,6 +70,7 @@ do_test 2.3 { } db2 } {1 {vec1: unexpected vector blob size 16 bytes, expected 32}} +db2 close #------------------------------------------------------------------------- reset_db vec1_load db @@ -128,6 +130,7 @@ do_execsql_test 2.7 { do_execsql_test 2.8 { PRAGMA integrity_check; } {ok} +db2 close #------------------------------------------------------------------------- reset_db diff --git a/manifest b/manifest index 679abc716d..d1768b250e 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Fix\sdefinition\sof\sVEC1_META_4BYTEMIN\sso\sthat\sit\sworks\swith\smsvc. -D 2026-09-30T16:38:56.907 +C Fix\sproblems\sin\svec1rollback.test\spreventing\sit\sfrom\srunning\son\swindows. +D 2026-09-30T16:41:58.959 F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea @@ -625,7 +625,7 @@ F ext/vec1/test/vec1meta3.test 91907adb451e17b777f526dd39c8cdfd2c5928a47b3a864eb F ext/vec1/test/vec1nan.test 3ff3fe4b0f33a5c1f4129ba318443e7d927fd9dd6fc339349fea3c57fe140a8c F ext/vec1/test/vec1pq.test 81d74f516971a3883782d2d443efe3f6a7501d4c9f980a758e1c2eb8b5f22885 F ext/vec1/test/vec1query.test 7120a2824b9f6563f7986c436204870c9d6e4af2bff3a39d38ef277188ef9189 -F ext/vec1/test/vec1rollback.test cb2a117153eb3bd3b97a19e8fce6a880b930862b0b50d240f80569ad593a6764 +F ext/vec1/test/vec1rollback.test 41565f526e153b24615ed38edda835fbb185f2f07f5e72f5c947add810b33d71 F ext/vec1/test/vec1rotate.test a90773e8ef8259f81c793cb335e3c023295231416c37d89e387d524304dadfde F ext/vec1/test/vec1rotate2.test 7e4e06b3f970190c4ac28256db7aa924796b9a1fe1985941e35db4604c8717a0 F ext/vec1/test/vec1round.test b38735c7e5b0af833505cc639d60816cfca2143b7461a1b443815c4be2b9d3ed @@ -2278,8 +2278,8 @@ F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee F tool/warnings.sh a554d13f6e5cf3760f041b87939e3d616ec6961859c3245e8ef701d1eafc2ca2 F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f F tool/winmain.c 00c8fb88e365c9017db14c73d3c78af62194d9644feaf60e220ab0f411f3604c -P 3a0876d973868be7b6f4156a434cd09098c7ab1abdc7f0ef4f3d12a03de68b24 -R e2a6a361772528d8b9fca843be0f8c92 +P c9b4c9c0aaf0c314312381558ed9cf60aac800dfb4f5701d4d2e1f5268b7f081 +R 535e28302ab61e6c4a8c8f61bccac8e4 U dan -Z a95742aa236b4568479fbd30ade20f73 +Z d4a692ce27d9474f2a4094c3503437bb # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index 16ecb45f99..288559f801 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -c9b4c9c0aaf0c314312381558ed9cf60aac800dfb4f5701d4d2e1f5268b7f081 +d15cf3890230d5a870e50448c69d640fedcbd15f32788b808cb1f212399e2796 From 6ae81db9b803334abd841165fa0b8b6c6c7b5173 Mon Sep 17 00:00:00 2001 From: dan Date: Wed, 30 Sep 2026 17:07:42 +0000 Subject: [PATCH 4/6] Fix a faulty assert() that was failing when running vec1 OOM tests. FossilOrigin-Name: aae661526953196ae078b2fe52d73f8a02ee9aa8f190b3fcdc27b23e4a344475 --- manifest | 12 ++++++------ manifest.uuid | 2 +- src/main.c | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/manifest b/manifest index d1768b250e..d96cafbaa5 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Fix\sproblems\sin\svec1rollback.test\spreventing\sit\sfrom\srunning\son\swindows. -D 2026-09-30T16:41:58.959 +C Fix\sa\sfaulty\sassert()\sthat\swas\sfailing\swhen\srunning\svec1\sOOM\stests. +D 2026-09-30T17:07:42.656 F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea @@ -764,7 +764,7 @@ F src/insert.c 68b898cb17607b36fa751b8985da06531932c502d77186f82842817286c7fd03 F src/json.c 98b654f8316e76bdff4eaf915ba6b95335a8b77e0bcd650a107c7ac86fb16080 F src/legacy.c d7874bc885906868cd51e6c2156698f2754f02d9eee1bae2d687323c3ca8e5aa F src/loadext.c 5cd4cd7ec6e1a46416806899086363fbf95eecd3bfb9a388b436c9a77243c902 -F src/main.c 5e366de50caa6083b0f28ca342970bff909c8086382c21f1cd7b8d92fa7b3590 +F src/main.c 3bdc49a38ba0919016531fc7fa606a103731123497014c80233ff57fd44d4298 F src/malloc.c 422f7e0498e1c9ef967f06283b6f2c0b16db6b905d8e06f6dbc8baaa3e4e6c5a F src/mem0.c 6a55ebe57c46ca1a7d98da93aaa07f99f1059645 F src/mem1.c 3bb59158c38e05f6270e761a9f435bf19827a264c13d1631c58b84bdc96d73b2 @@ -2278,8 +2278,8 @@ F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee F tool/warnings.sh a554d13f6e5cf3760f041b87939e3d616ec6961859c3245e8ef701d1eafc2ca2 F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f F tool/winmain.c 00c8fb88e365c9017db14c73d3c78af62194d9644feaf60e220ab0f411f3604c -P c9b4c9c0aaf0c314312381558ed9cf60aac800dfb4f5701d4d2e1f5268b7f081 -R 535e28302ab61e6c4a8c8f61bccac8e4 +P d15cf3890230d5a870e50448c69d640fedcbd15f32788b808cb1f212399e2796 +R 5f2ad466e906e7a61d707a8804512523 U dan -Z d4a692ce27d9474f2a4094c3503437bb +Z bec2b4cef171ca72a417fe864beafe29 # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index 288559f801..3df617f280 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -d15cf3890230d5a870e50448c69d640fedcbd15f32788b808cb1f212399e2796 +aae661526953196ae078b2fe52d73f8a02ee9aa8f190b3fcdc27b23e4a344475 diff --git a/src/main.c b/src/main.c index 6107adf8a8..b90ce05953 100644 --- a/src/main.c +++ b/src/main.c @@ -2054,7 +2054,7 @@ int sqlite3CreateFunc( if( db->nVdbeActive ){ sqlite3ErrorWithMsg(db, SQLITE_BUSY, "unable to delete/modify user-function due to active statements"); - assert( !db->mallocFailed ); + assert( !db->mallocFailed || db->nVdbeExec>0 ); return SQLITE_BUSY; }else{ sqlite3ExpirePreparedStatements(db, 0); From 9559d8940c56905dc8cbd8f50bd6fe1cad1cf4bf Mon Sep 17 00:00:00 2001 From: dan Date: Wed, 30 Sep 2026 17:38:45 +0000 Subject: [PATCH 5/6] For msvc, __AVX2__ implies __FMA__. FossilOrigin-Name: 2196a779fbf1a3f174a24d29e14823f09de6b743f321ccc845e8782c7987edad --- ext/vec1/vec1.c | 3 ++- manifest | 12 ++++++------ manifest.uuid | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/ext/vec1/vec1.c b/ext/vec1/vec1.c index 51fad12d22..27f9b9d4cc 100644 --- a/ext/vec1/vec1.c +++ b/ext/vec1/vec1.c @@ -139,7 +139,8 @@ typedef char i8; # include -# ifdef __FMA__ +# if defined(__FMA__) || defined(_MSC_VER) + /* With MSVC, __AVX2__ implies __FMA__ */ # define FMADD(a,b,c) _mm256_fmadd_ps((a),(b),(c)) # define FMADD64(a,b,c) _mm256_fmadd_pd((a),(b),(c)) # else diff --git a/manifest b/manifest index d96cafbaa5..c92bab36af 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Fix\sa\sfaulty\sassert()\sthat\swas\sfailing\swhen\srunning\svec1\sOOM\stests. -D 2026-09-30T17:07:42.656 +C For\smsvc,\s__AVX2__\simplies\s__FMA__. +D 2026-09-30T17:38:45.760 F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea @@ -640,7 +640,7 @@ F ext/vec1/tool/vec1_algo_test.c c62b828a3e46a39a8656f26b74804906cc3160758f8afc6 F ext/vec1/tool/vec1bench.tcl de201e8b468de035065dd8dfdaa171d64760693deb19e6d5725e23840e6cca1e F ext/vec1/tool/vec1bench_addcols.tcl 8c509253d07b179928f2ee32e3591a2c70d13c914217e1328c01afa148717051 F ext/vec1/tool/vec1bench_vibe2db.py 84ddb0236bc3d02a0393abce41986a1b7dad7f0d83884576f8318c2883e3aed3 -F ext/vec1/vec1.c 06282d8fc75ede7fdbd747fe0cb5baf6b9c420c76110a5143ef11aac6d26aa3e +F ext/vec1/vec1.c df15944144582049c2a90d966f901231c4f1c3acfe99475292a707f8d608a7d2 F ext/wasm/GNUmakefile cc28e65dec12f1b992957152d8395484f4fcf7f94e6b6736a09ee4f48e7ec048 F ext/wasm/README-dist.txt f01081a850ce38a56706af6b481e3a7878e24e42b314cfcd4b129f0f8427066a F ext/wasm/README.md 2e87804e12c98f1d194b7a06162a88441d33bb443efcfe00dc6565a780d2f259 @@ -2278,8 +2278,8 @@ F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee F tool/warnings.sh a554d13f6e5cf3760f041b87939e3d616ec6961859c3245e8ef701d1eafc2ca2 F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f F tool/winmain.c 00c8fb88e365c9017db14c73d3c78af62194d9644feaf60e220ab0f411f3604c -P d15cf3890230d5a870e50448c69d640fedcbd15f32788b808cb1f212399e2796 -R 5f2ad466e906e7a61d707a8804512523 +P aae661526953196ae078b2fe52d73f8a02ee9aa8f190b3fcdc27b23e4a344475 +R 2e28ba778be5998103a3a3b549e9b002 U dan -Z bec2b4cef171ca72a417fe864beafe29 +Z 16e7d20860c7b10f028a7245bc3c88fe # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index 3df617f280..35a30bdf42 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -aae661526953196ae078b2fe52d73f8a02ee9aa8f190b3fcdc27b23e4a344475 +2196a779fbf1a3f174a24d29e14823f09de6b743f321ccc845e8782c7987edad From 066a18e5c8887611d554b29aba8a67891c93fce1 Mon Sep 17 00:00:00 2001 From: dan Date: Wed, 30 Sep 2026 17:50:20 +0000 Subject: [PATCH 6/6] Fix a typo in the VERTICAL_SUM_GENERIC() macro in vec1.c. FossilOrigin-Name: b55faf3253cd5d2fc69b4e025f4707bda3600c553f85aff152cfd4e71e54c81e --- ext/vec1/vec1.c | 2 +- manifest | 12 ++++++------ manifest.uuid | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ext/vec1/vec1.c b/ext/vec1/vec1.c index 27f9b9d4cc..47fe15c97e 100644 --- a/ext/vec1/vec1.c +++ b/ext/vec1/vec1.c @@ -107,7 +107,7 @@ typedef char i8; #define VERTICAL_SUM_GENERIC(aAcc, nAcc, I) { \ int c; \ - for(c=4; c1 ) aAcc[0] = I (aAcc[0], aAcc[1]); \ } diff --git a/manifest b/manifest index c92bab36af..b99cbb5ace 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C For\smsvc,\s__AVX2__\simplies\s__FMA__. -D 2026-09-30T17:38:45.760 +C Fix\sa\stypo\sin\sthe\sVERTICAL_SUM_GENERIC()\smacro\sin\svec1.c. +D 2026-09-30T17:50:20.990 F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea @@ -640,7 +640,7 @@ F ext/vec1/tool/vec1_algo_test.c c62b828a3e46a39a8656f26b74804906cc3160758f8afc6 F ext/vec1/tool/vec1bench.tcl de201e8b468de035065dd8dfdaa171d64760693deb19e6d5725e23840e6cca1e F ext/vec1/tool/vec1bench_addcols.tcl 8c509253d07b179928f2ee32e3591a2c70d13c914217e1328c01afa148717051 F ext/vec1/tool/vec1bench_vibe2db.py 84ddb0236bc3d02a0393abce41986a1b7dad7f0d83884576f8318c2883e3aed3 -F ext/vec1/vec1.c df15944144582049c2a90d966f901231c4f1c3acfe99475292a707f8d608a7d2 +F ext/vec1/vec1.c 08cd74248dd2707e8a59943007e1dbcac2946e60b9d55c261f4810d1689b17fa F ext/wasm/GNUmakefile cc28e65dec12f1b992957152d8395484f4fcf7f94e6b6736a09ee4f48e7ec048 F ext/wasm/README-dist.txt f01081a850ce38a56706af6b481e3a7878e24e42b314cfcd4b129f0f8427066a F ext/wasm/README.md 2e87804e12c98f1d194b7a06162a88441d33bb443efcfe00dc6565a780d2f259 @@ -2278,8 +2278,8 @@ F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee F tool/warnings.sh a554d13f6e5cf3760f041b87939e3d616ec6961859c3245e8ef701d1eafc2ca2 F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f F tool/winmain.c 00c8fb88e365c9017db14c73d3c78af62194d9644feaf60e220ab0f411f3604c -P aae661526953196ae078b2fe52d73f8a02ee9aa8f190b3fcdc27b23e4a344475 -R 2e28ba778be5998103a3a3b549e9b002 +P 2196a779fbf1a3f174a24d29e14823f09de6b743f321ccc845e8782c7987edad +R 4f51cb9715330141836dd07370684ca4 U dan -Z 16e7d20860c7b10f028a7245bc3c88fe +Z 762aada16ff47f0ef4a985458ec7cc01 # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index 35a30bdf42..24e08e75c9 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -2196a779fbf1a3f174a24d29e14823f09de6b743f321ccc845e8782c7987edad +b55faf3253cd5d2fc69b4e025f4707bda3600c553f85aff152cfd4e71e54c81e