diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 7af139a3..a8ce680d 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -67,6 +67,25 @@ jobs: path: dist/* # after the upload, so dist/test-unit stays out of the artifact - run: make test-unit + # The IVF index is compiled only on request, so the other builds skip its + # tests; this one compiles it in and runs every suite. + build-linux-x86_64-ivf: + needs: memory-gate + runs-on: ubuntu-latest + env: + CPPFLAGS: -DSQLITE_VEC_EXPERIMENTAL_IVF_ENABLE=1 + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6 + with: + enable-cache: true + - run: ./scripts/vendor.sh + - run: make loadable static + - run: uv sync --locked --managed-python --directory tests + - run: make test-loadable python=./tests/.venv/bin/python + - run: make test-unit build-macos-x86_64-extension: runs-on: macos-15-intel steps: diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index dc9897b6..6d51e714 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -28,6 +28,99 @@ Very much a WIP. #### `xyz_vector_chunksNN` +- `rowid INTEGER` +- `vectors BLOB` + +#### `xyz_rescore_chunksNN` + +The vectors of a column indexed by `rescore()`, quantized, in the layout of +`xyz_vector_chunksNN`. + +- `rowid INTEGER` +- `vectors BLOB` + +#### `xyz_rescore_vectorsNN` + +The `float32` vectors of a column indexed by `rescore()`, one row per row of +the table. + +- `rowid INTEGER` +- `vector BLOB` + +#### `xyz_vectorsNN` + +The vectors of a column indexed by `diskann()`, one row per row of the table. + +- `rowid INTEGER` +- `vector BLOB` + +#### `xyz_diskann_nodesNN` + +The graph of a column indexed by `diskann()`, one row per node. A node has +`n_neighbors` slots: `neighbors_validity` holds a bit per slot, set when the +slot holds a neighbor, `neighbor_ids` the neighbor's rowid as a 64-bit integer +in the machine's byte order, and `neighbor_quantized_vectors` its vector +quantized by the column's `neighbor_quantizer`, a bit or a byte per dimension. +The `diskann_medoid_NN` row of `xyz_info` names the node every search starts +at, and is `NULL` while the graph has no node. + +- `rowid INTEGER` +- `neighbors_validity BLOB` +- `neighbor_ids BLOB` +- `neighbor_quantized_vectors BLOB` + +#### `xyz_diskann_bufferNN` + +The vectors of a column indexed by `diskann(..., buffer_threshold=N)` that have +not joined the graph yet. + +- `rowid INTEGER` +- `vector BLOB` + +A table whose every vector column is indexed by `diskann()` keeps no chunks: +its `xyz_chunks` stays empty, and its `xyz_rowids` rows have no `chunk_id` or +`chunk_offset`. + +#### `xyz_ivf_centroidsNN` + +The centroids of a column indexed by `ivf()`, held as its cells hold vectors: +`float32`, or quantized by the column's `quantizer`. The `ivf_trained_N` row of +`xyz_info` is 1 while the column has centroids. + +- `centroid_id INTEGER` +- `centroid BLOB` + +#### `xyz_ivf_cellsNN` + +The rows of a column indexed by `ivf()`, in cells of up to 64. A cell holds +rows of one centroid, or, with `centroid_id` -1, rows no clustering has placed. +`validity` holds a bit per slot, set when the slot holds a row, `rowids` the +row's rowid as a 64-bit integer in the machine's byte order, and `vectors` its +vector as the centroids hold them. `n_vectors` counts the set bits. A query +finds the cells of a centroid through the index of the +`UNIQUE (centroid_id, cell_id)` constraint. + +- `cell_id INTEGER` +- `centroid_id INTEGER` +- `n_vectors INTEGER` +- `validity BLOB` +- `rowids BLOB` +- `vectors BLOB` + +#### `xyz_ivf_rowid_mapNN` + +The cell and slot of each row of a column indexed by `ivf()`. + +- `rowid INTEGER` +- `cell_id INTEGER` +- `slot INTEGER` + +#### `xyz_ivf_vectorsNN` + +The `float32` vectors of a column indexed by `ivf()` with a `quantizer`, one +row per row of the table. Without a quantizer, the cells hold the vectors and +the table does not exist. + - `rowid INTEGER` - `vector BLOB` @@ -52,7 +145,15 @@ The supported way to tell which tables belong to a `vec0` table is by name. A `vec0` table `xyz` owns these tables and no others (`NN` is two digits): - `xyz_info`, `xyz_chunks`, `xyz_rowids`: always -- `xyz_vector_chunksNN`: one per vector column +- `xyz_vector_chunksNN`: one per vector column, except those indexed by + `rescore()`, `diskann()` or `ivf()` +- `xyz_rescore_chunksNN`, `xyz_rescore_vectorsNN`: one each per vector column + indexed by `rescore()` +- `xyz_vectorsNN`, `xyz_diskann_nodesNN`, `xyz_diskann_bufferNN`: one each per + vector column indexed by `diskann()` +- `xyz_ivf_centroidsNN`, `xyz_ivf_cellsNN`, `xyz_ivf_rowid_mapNN`: one each per + vector column indexed by `ivf()`, and `xyz_ivf_vectorsNN` for each of those + with a `quantizer` - `xyz_auxiliary`: only if there are auxiliary columns - `xyz_metadatachunksNN`: one per metadata column - `xyz_metadatatextNN`: one per text metadata column @@ -69,6 +170,15 @@ join sqlite_master as s and substr(s.name, 1, length(v.name) + 1) = v.name || '_' and (substr(s.name, length(v.name) + 2) in ('rowids', 'chunks', 'info', 'auxiliary') or substr(s.name, length(v.name) + 2) glob 'vector_chunks[0-9][0-9]' + or substr(s.name, length(v.name) + 2) glob 'rescore_chunks[0-9][0-9]' + or substr(s.name, length(v.name) + 2) glob 'rescore_vectors[0-9][0-9]' + or substr(s.name, length(v.name) + 2) glob 'vectors[0-9][0-9]' + or substr(s.name, length(v.name) + 2) glob 'diskann_nodes[0-9][0-9]' + or substr(s.name, length(v.name) + 2) glob 'diskann_buffer[0-9][0-9]' + or substr(s.name, length(v.name) + 2) glob 'ivf_centroids[0-9][0-9]' + or substr(s.name, length(v.name) + 2) glob 'ivf_cells[0-9][0-9]' + or substr(s.name, length(v.name) + 2) glob 'ivf_rowid_map[0-9][0-9]' + or substr(s.name, length(v.name) + 2) glob 'ivf_vectors[0-9][0-9]' or substr(s.name, length(v.name) + 2) glob 'metadatachunks[0-9][0-9]' or substr(s.name, length(v.name) + 2) glob 'metadatatext[0-9][0-9]') where v.type = 'table' and v.rootpage = 0 @@ -84,24 +194,26 @@ relied on: - Up to SQLite 3.53, so does a connection that parsed the schema before loading the extension. - Before SQLite 3.37 there is no `PRAGMA table_list` and no prefix marking (see - below), so the vector chunk tables are never shadow tables. + below), so the tables whose suffix has an underscore (`vector_chunksNN`, + `rescore_chunksNN`, `rescore_vectorsNN`, `diskann_nodesNN`, + `diskann_bufferNN`, and the four `ivf_` tables) are never shadow tables. - SQLite finds the virtual table that owns a newly created or newly parsed table by splitting its name at the **last** underscore ([documented](https://www.sqlite.org/vtab.html#the_xshadowname_method)). For `xyz_vector_chunks00` it looks for a virtual table called `xyz_vector`, so `xShadowName` is never asked about it on that path. -- `xShadowName` does report `vector_chunksNN`, and SQLite also marks shadow +- `xShadowName` does report those suffixes, and SQLite also marks shadow tables by name prefix when it parses the virtual table's own schema entry. That only sees tables parsed earlier, which in practice means after a `VACUUM` (it rewrites `sqlite_master` with virtual tables last). A `vec0` table created after that `VACUUM` is not covered until the next one. -- SQLite 3.54 additionally marks by prefix after every `xConnect`. There the - vector chunk tables become `shadow` once the `vec0` table has been used in - the connection, except in the connection that created it. `PRAGMA - table_list` connects every virtual table first, so it always reports them as - `shadow`, but under `SQLITE_DBCONFIG_DEFENSIVE` a direct write to a chunk - table is still accepted until the `vec0` table is first touched. This prefix - matching is not part of the documented contract. +- SQLite 3.54 additionally marks by prefix after every `xConnect`. There those + tables become `shadow` once the `vec0` table has been used in the + connection, except in the connection that created it. `PRAGMA table_list` + connects every virtual table first, so it always reports them as `shadow`, + but under `SQLITE_DBCONFIG_DEFENSIVE` a direct write to one of them is still + accepted until the `vec0` table is first touched. This prefix matching is not + part of the documented contract. Rules that follow from this: diff --git a/CHANGELOG.md b/CHANGELOG.md index 326649bf..84c0db78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## [Unreleased] +### Added + +- A vector column definition accepts `indexed by flat()`, as in `embedding float[768] indexed by flat()`. `flat()` is the exhaustive search that a column without the clause gets. Ported from [asg017@0de765f](https://github.com/asg017/sqlite-vec/commit/0de765f) ([asg017#273](https://github.com/asg017/sqlite-vec/pull/273)). +- A `float` vector column can be declared `indexed by rescore(quantizer=bit)` or `indexed by rescore(quantizer=int8)`, with an optional `oversample=N` from 1 to 128 (8 by default). It also keeps its vectors quantized, in the new `_rescore_chunksNN` shadow table, and its `float` vectors by rowid in `_rescore_vectorsNN`. A KNN query scans the quantized vectors for the `k * oversample` nearest rows, then ranks those by the exact distances of their `float` vectors, to which distance constraints apply. The `oversample=N` command, as in `insert into t(t) values ('oversample=16')`, sets the oversample of a connection's queries. Rowid filters, MMR, auxiliary columns, `UPDATE`, `optimize`, and `ALTER TABLE ... RENAME` work on rescore columns; a table with one cannot have metadata or partition key columns. Ported from [asg017#276](https://github.com/asg017/sqlite-vec/pull/276) ([asg017@ba0db0b](https://github.com/asg017/sqlite-vec/commit/ba0db0b)) with its fixes [asg017@5e4c557](https://github.com/asg017/sqlite-vec/commit/5e4c557), [@82f4eb0](https://github.com/asg017/sqlite-vec/commit/82f4eb0), [@5522e86](https://github.com/asg017/sqlite-vec/commit/5522e86), [@b7fc459](https://github.com/asg017/sqlite-vec/commit/b7fc459), [@6e2c4c6](https://github.com/asg017/sqlite-vec/commit/6e2c4c6), the int8 quantizer of [@575371d](https://github.com/asg017/sqlite-vec/commit/575371d), and the distance constraints of the unmerged [asg017#309](https://github.com/asg017/sqlite-vec/pull/309). Unlike upstream, a point query for a rowid that a rescore table does not have returns no row instead of failing with `Could not fetch vector data`. +- A `float` or `int8` vector column can be declared `indexed by diskann(neighbor_quantizer=binary)` or `indexed by diskann(neighbor_quantizer=int8)`, with optional `n_neighbors=N` (72 by default), `search_list_size=N` (128) or `search_list_size_search=N` and `search_list_size_insert=N`, and `buffer_threshold=N`. The column keeps a graph of its rows in the new `_diskann_nodesNN` shadow table, its vectors by rowid in `_vectorsNN`, and its buffered inserts in `_diskann_bufferNN`, and a KNN query walks the graph instead of reading every vector. The commands `search_list_size=N`, `search_list_size_search=N` and `search_list_size_insert=N` set those sizes for a connection. Rowid filters, distance constraints, MMR, auxiliary columns, `optimize`, and `ALTER TABLE ... RENAME` work on DiskANN columns; a table with one cannot have metadata or partition key columns, and a DiskANN column's vector cannot be updated. A table whose every vector column is indexed by DiskANN keeps no chunks. Ported from [asg017@575371d](https://github.com/asg017/sqlite-vec/commit/575371d) with [asg017@9df59b4](https://github.com/asg017/sqlite-vec/commit/9df59b4), [@82f4eb0](https://github.com/asg017/sqlite-vec/commit/82f4eb0), [@b008654](https://github.com/asg017/sqlite-vec/commit/b008654), [@c36a995](https://github.com/asg017/sqlite-vec/commit/c36a995), [@01b4b2a](https://github.com/asg017/sqlite-vec/commit/01b4b2a), [@b7fc459](https://github.com/asg017/sqlite-vec/commit/b7fc459) and [@fe94171](https://github.com/asg017/sqlite-vec/commit/fe94171). Unlike upstream, an `int8` column's vectors are quantized as `int8` values (upstream reads an `int8` query as floats, past its end), a result's distance is always its exact one, the row every search starts from is replaced, when deleted, by a row of the graph rather than one still waiting in the buffer, rowid filters, distance constraints and MMR apply, errors and interrupts reach the query instead of being skipped, `VACUUM` works after a KNN query, the commands set every DiskANN column of the table, and an abbreviated option such as `n_neigh=16` or `neighbor_quantizer=bin`, a command value such as `search_list_size=12abc`, and a number past an `int` are refused. +- A build compiled with `-DSQLITE_VEC_EXPERIMENTAL_IVF_ENABLE=1` can declare a `float` vector column `indexed by ivf()`, with optional `nlist=N` (128 by default, or 0 to leave the centroids to `set-centroid`), `nprobe=N` (10, at most `nlist`), `quantizer=int8` or `quantizer=binary`, and `oversample=N` (1). The column keeps its rows in cells of up to 64 in the new `_ivf_cellsNN` shadow table, around the centroids in `_ivf_centroidsNN`, with each row's cell and slot in `_ivf_rowid_mapNN` and, with a quantizer, its `float` vector in `_ivf_vectorsNN`; a KNN query reads the cells of the `nprobe` centroids nearest its vector and the cells of rows no clustering has placed. The commands `compute-centroids`, `compute-centroids:{"nlist": N, "max_iterations": N, "seed": N}`, `set-centroid:N`, `assign-vectors`, `clear-centroids` and `nprobe=N` build and tune it. Rowid filters, distance constraints, MMR, auxiliary columns, `optimize`, and `ALTER TABLE ... RENAME` work on IVF columns; a table with one cannot have metadata or partition key columns, and an IVF column's vector cannot be updated. The prebuilt binaries are built without it. Ported from [asg017@e2c38f3](https://github.com/asg017/sqlite-vec/commit/e2c38f3) with [asg017@4bee883](https://github.com/asg017/sqlite-vec/commit/4bee883), [@9df59b4](https://github.com/asg017/sqlite-vec/commit/9df59b4), [@b7fc459](https://github.com/asg017/sqlite-vec/commit/b7fc459), [@6e2c4c6](https://github.com/asg017/sqlite-vec/commit/6e2c4c6), [@8b81f40](https://github.com/asg017/sqlite-vec/commit/8b81f40) and [@fe94171](https://github.com/asg017/sqlite-vec/commit/fe94171), and the fixes of the unmerged [asg017#313](https://github.com/asg017/sqlite-vec/pull/313). Unlike upstream, a quantized column's point query and full scan return its `float` vectors (upstream returns the quantized bytes, or reads past them), a connection finds the centroids another connection computed (upstream caches the column's state per connection and finds no rows), rowid filters and distance constraints apply, a renamed table's cells are found through an index named after the new table, so a table created under the old name works, an `int8` or `bit` column is refused (upstream reads past an `int8` column's vectors), errors and interrupts reach the statement, so an interrupted command fails with `interrupted` and the connection's queries go on finding its rows (upstream reports `SQL logic error`, and its KNN queries then return no rows), `VACUUM` works after a KNN query, the commands apply to every IVF column of the table, and an abbreviated option, a command value such as `nprobe=5x`, and `compute-centroids` options that are not a JSON object of integers are refused. + ### Changed - Reading a vector column without a KNN query, as in `select embedding from v`, reads each chunk's vectors through one blob handle instead of opening the chunk's blob again for every row: selecting every vector of a 10,000-row `float[768]` table took 25 ms instead of 2,080 ms, and `select vec_distance_cosine(embedding, ?) from v` 41 ms instead of 2,093 ms. @@ -10,6 +17,7 @@ - A KNN query picks each chunk's k nearest rows with the heap of upstream's `SQLITE_VEC_EXPERIMENTAL_MIN_IDX` build option ([asg017/sqlite-vec@0de765f](https://github.com/asg017/sqlite-vec/commit/0de765f4570c1d65a0699e8e38d6c5562da9e23e)) instead of one pass over the chunk for every row it keeps, ordered so that it returns the same rows in the same order as before: on a 10,000-row `float[768]` table, `k = 1000` and `k = 4096` take 9.3 ms instead of 17.8 and 18.0 ms, `k = 100` 8.8 ms instead of 9.2, and `k = 1` 8.7 ms either way. A NaN distance ranks after every number instead of where its position put it. A static blob can produce one, and so can a `vec0` table written before 2.1.0, which started rejecting NaN elements. On such a table a NaN row used to push nearer rows out of the result or behind farther ones; queries now return the nearest rows in order, which can leave out a NaN row that a query with `k` smaller than the row count used to return. - A `DELETE` that removes the last row of a chunk now deletes the chunk's rows from the `_chunks`, `_vector_chunksNN`, and `_metadatachunksNN` shadow tables, so their pages are free for reuse without `optimize`; the emptied chunk used to stay until `optimize` removed it. Ported from [asg017@cb147c8](https://github.com/asg017/sqlite-vec/commit/cb147c8) via [vlasky@3a64182](https://github.com/vlasky/sqlite-vec/commit/3a64182) ([asg017#268](https://github.com/asg017/sqlite-vec/pull/268)). - A `DELETE` of a row whose bit in its chunk's validity bitmap is clear now fails with `vec0 deletion error: validity bit is not set`; it used to delete the row when a later row in the same byte of the bitmap was set. Only a damaged `_chunks` table has such a row. +- A command that a `vec0` table does not know, as in `insert into t(t) values ('compact')`, now fails with `unknown vec0 command: 'compact'` instead of `SQL logic error`, as upstream [asg017@6e2c4c6](https://github.com/asg017/sqlite-vec/commit/6e2c4c6) does. ### Fixed diff --git a/Makefile b/Makefile index 265c3cec..87b77bbb 100644 --- a/Makefile +++ b/Makefile @@ -211,7 +211,7 @@ test-loadable-watch: test-unit: | $(prefix) $(CC) tests/test-unit.c sqlite-vec.c vendor/sqlite3.c \ - -I./ -Ivendor -DSQLITE_CORE -o $(prefix)/test-unit -lm + -I./ -Ivendor -DSQLITE_CORE $(EXT_CFLAGS) -o $(prefix)/test-unit -lm $(prefix)/test-unit test-all: test test-loadable test-unit diff --git a/README.md b/README.md index ddc5076c..43a24423 100644 --- a/README.md +++ b/README.md @@ -295,8 +295,11 @@ VACUUM; `VACUUM` should not corrupt vec tables; a checkpoint first is recommended when using WAL so the rewrite starts from a clean state. -`optimize` runs through a hidden column named after the table, so a `vec0` -table cannot have another column with its name. `CREATE` rejects tables such as +`optimize`, like the `oversample=N` command of a rescore index, the +`search_list_size=N` commands of a DiskANN index and the commands of an IVF +index, runs through a hidden column +named after the table, so a `vec0` table cannot have another column with its +name. `CREATE` rejects tables such as `CREATE VIRTUAL TABLE emb USING vec0(emb float[4])`, and tables named `distance`, `k`, `mmr_lambda`, or (without a primary key column) `rowid`, which are names of columns `vec0` always declares. Older releases allowed these @@ -308,6 +311,218 @@ ALTER TABLE emb RENAME TO emb_vectors; INSERT INTO emb_vectors(emb_vectors) VALUES('optimize'); ``` +### Rescore Index + +A `float` vector column declared `indexed by rescore(quantizer=bit)` or +`indexed by rescore(quantizer=int8)` also keeps each vector quantized, to 1 bit +or 8 bits per dimension. A KNN query scans the quantized vectors for the +`k * oversample` nearest rows, then ranks those candidates by the distances of +their `float` vectors. The distances it returns are exact, and its rows are +those of an exact search whenever the nearest rows are among the candidates: + +```sql +create virtual table vec_rescore using vec0( + sample_embedding float[8] indexed by rescore(quantizer=bit) +); +insert into vec_rescore(rowid, sample_embedding) + select rowid, sample_embedding from vec_examples; + +select rowid, distance +from vec_rescore +where sample_embedding match '[0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]' + and k = 3; +/* +┌───────┬──────────────────┐ +│ rowid │ distance │ +├───────┼──────────────────┤ +│ 5 │ 1.16368770599365 │ +│ 13 │ 1.75137972831726 │ +│ 11 │ 1.83941268920898 │ +└───────┴──────────────────┘ +*/ +``` + +`oversample` is 8 unless the column sets it, as in +`indexed by rescore(quantizer=bit, oversample=16)`, from 1 to 128. The +`oversample=N` command sets it for every rescore column, for the queries of the +connection that runs it. With an oversample of 1, the query above keeps only the +3 rows nearest by bit distance, and row 11 is not among them: + +```sql +insert into vec_rescore(vec_rescore) values ('oversample=1'); + +select rowid, distance +from vec_rescore +where sample_embedding match '[0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]' + and k = 3; +/* +┌───────┬──────────────────┐ +│ rowid │ distance │ +├───────┼──────────────────┤ +│ 5 │ 1.16368770599365 │ +│ 13 │ 1.75137972831726 │ +│ 16 │ 2.36842274665833 │ +└───────┴──────────────────┘ +*/ +``` + +- `quantizer=bit` sets one bit for each value >= 0, and needs a number of + dimensions divisible by 8. `quantizer=int8` maps [-1, 1] onto the 256 values + of an int8, so it suits vectors whose values lie in that range, such as + normalized embeddings. +- Distance constraints apply to the exact distances of the candidates, so a + query with `distance > x`, as in cursor-based pagination, returns fewer than + `k` rows when most of the candidates are nearer than `x`. +- A table with a rescore column cannot have metadata or partition key columns. + Auxiliary columns work. +- `indexed by flat()` declares the exhaustive search that a column without the + clause gets. + +### DiskANN Index + +A `float` or `int8` vector column declared +`indexed by diskann(neighbor_quantizer=binary)` or +`indexed by diskann(neighbor_quantizer=int8)` keeps a graph of its rows, after +the LM-DiskANN paper: each row links to up to `n_neighbors` others and keeps +their vectors quantized. A KNN query walks the graph from one entry row, +keeping the `search_list_size` nearest rows it has seen, and ranks each row by +its exact distance once it reaches it. It reads a few hundred rows of a large +table where a flat column reads them all, and its distances are exact, but a +row it never reaches is missing from its results: + +```sql +create virtual table vec_diskann using vec0( + sample_embedding float[8] indexed by diskann(neighbor_quantizer=binary, n_neighbors=8) +); +insert into vec_diskann(rowid, sample_embedding) + select rowid, sample_embedding from vec_examples; + +select rowid, distance +from vec_diskann +where sample_embedding match '[0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]' + and k = 3; +/* +┌───────┬──────────────────┐ +│ rowid │ distance │ +├───────┼──────────────────┤ +│ 5 │ 1.16368770599365 │ +│ 13 │ 1.75137972831726 │ +│ 11 │ 1.83941268920898 │ +└───────┴──────────────────┘ +*/ +``` + +- `neighbor_quantizer=binary` keeps a bit for each value > 0, and needs a number + of dimensions divisible by 8. `neighbor_quantizer=int8` keeps a byte per + dimension: a `float` value in [-1, 1] mapped onto the 256 values of an int8, + and an `int8` column's own values. +- `n_neighbors` is 72 unless the column sets it, a multiple of 8 up to 256. +- `search_list_size` is 128 unless the column sets it, for inserts and queries + alike, or `search_list_size_search` and `search_list_size_insert` set the two + apart. A query keeps at least `k` rows. The commands `search_list_size=N`, + `search_list_size_search=N` and `search_list_size_insert=N` set them for every + DiskANN column, for the connection that runs them, as in + `insert into vec_diskann(vec_diskann) values ('search_list_size_search=256')`. +- With `buffer_threshold=N`, inserted rows wait in a buffer and join the graph + `N` at a time. A KNN query compares every row in the buffer by its exact + distance. +- With `rowid = ?` or `rowid in (...)`, a KNN query ranks the rows they name by + their exact distances, without the graph. +- Distance constraints apply to the exact distances of the rows the query + found, so a query with `distance > x` returns fewer than `k` rows when most of + them are nearer than `x`. MMR reranks them as it does any KNN query. +- A table with a DiskANN column cannot have metadata or partition key columns, + and a `bit` column cannot be indexed by DiskANN. Auxiliary columns work. +- `UPDATE` of a DiskANN column's vector fails, since its row's links were + chosen for the vector it has; delete the row and insert it again. +- A `DELETE` links the row's neighbors to each other and reads every node of + the graph to remove the links to the row. + +### IVF Index (experimental) + +A build compiled with `-DSQLITE_VEC_EXPERIMENTAL_IVF_ENABLE=1`, as by +`make loadable CPPFLAGS=-DSQLITE_VEC_EXPERIMENTAL_IVF_ENABLE=1`, has one more +index; the prebuilt binaries leave it out. A `float` vector column declared +`indexed by ivf()` clusters its rows around `nlist` centroids with k-means, and +a KNN query reads only the rows of the `nprobe` centroids nearest its vector. +Rows wait in cells of no centroid until `compute-centroids` clusters them, and +a query reads those cells in full, so until then it answers as a flat column +does: + +```sql +create virtual table vec_ivf using vec0( + sample_embedding float[8] indexed by ivf(nlist=4, nprobe=1) +); +insert into vec_ivf(rowid, sample_embedding) + select rowid, sample_embedding from vec_examples; +insert into vec_ivf(vec_ivf) values ('compute-centroids'); + +select rowid, distance +from vec_ivf +where sample_embedding match '[0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]' + and k = 3; +/* +┌───────┬──────────────────┐ +│ rowid │ distance │ +├───────┼──────────────────┤ +│ 5 │ 1.16368770599365 │ +│ 7 │ 1.89339029788971 │ +│ 4 │ 2.10507702827454 │ +└───────┴──────────────────┘ +*/ +``` + +The query read one of the four clusters, and rows 13 and 11, the second and +third nearest, are in others. The `nprobe=N` command sets `nprobe` for every +IVF column, for the connection that runs it: + +```sql +insert into vec_ivf(vec_ivf) values ('nprobe=2'); + +select rowid, distance +from vec_ivf +where sample_embedding match '[0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]' + and k = 3; +/* +┌───────┬──────────────────┐ +│ rowid │ distance │ +├───────┼──────────────────┤ +│ 5 │ 1.16368770599365 │ +│ 13 │ 1.75137972831726 │ +│ 11 │ 1.83941268920898 │ +└───────┴──────────────────┘ +*/ +``` + +- `nlist` is 128 unless the column sets it, up to 65536, and `nlist=0` leaves + the centroids to `set-centroid`. `nprobe` is 10, or `nlist` if that is less. +- `compute-centroids` clusters every row of each IVF column, with at most one + centroid per row, and moves each row to the cells of its nearest centroid. + `compute-centroids:{"nlist": 64, "max_iterations": 25, "seed": 1}` sets any + of the three as JSON integers; the same rows, read in the same order, and the + same seed give the same centroids. A row inserted later goes to the cells of + its nearest centroid, and the centroids stay as they are until the next + `compute-centroids`. +- `set-centroid:N` sets centroid `N` of each IVF column to the vector the same + `INSERT` gives it, as in + `insert into vec_ivf(vec_ivf, sample_embedding) values ('set-centroid:0', '[...]')`. + `assign-vectors` then moves the rows in cells of no centroid to their nearest + centroids, and `clear-centroids` removes the centroids and moves every row to + cells of no centroid. Every command applies to each IVF column of the table. +- With `quantizer=int8` or `quantizer=binary`, the cells and centroids hold the + vectors quantized and the `float` vectors are kept by rowid besides. A query + keeps the `k * oversample` rows nearest by quantized distance and ranks them + by their exact distances. `int8` maps [-1, 1] onto [-127, 127]; `binary` keeps + a bit for each value > 0 and needs a number of dimensions divisible by 8. + `oversample` is 1 unless the column sets it, and needs a quantizer. +- With `rowid = ?` or `rowid in (...)`, a KNN query ranks the rows they name by + their exact distances, without the clusters. Distance constraints apply to + the exact distances of the rows the query read, and MMR reranks them as it + does any KNN query. +- A table with an IVF column cannot have metadata or partition key columns, and + only `float` columns can be indexed by IVF. Auxiliary columns work. +- `UPDATE` of an IVF column's vector fails; delete the row and insert it again. + ## Sponsors > [!NOTE] diff --git a/sqlite-vec.c b/sqlite-vec.c index e7b25513..0dcd3334 100644 --- a/sqlite-vec.c +++ b/sqlite-vec.c @@ -73,6 +73,18 @@ typedef size_t usize; #define countof(x) (sizeof(x) / sizeof((x)[0])) #define min(a, b) (((a) <= (b)) ? (a) : (b)) +#ifndef SQLITE_VEC_ENABLE_RESCORE +#define SQLITE_VEC_ENABLE_RESCORE 1 +#endif + +#ifndef SQLITE_VEC_ENABLE_DISKANN +#define SQLITE_VEC_ENABLE_DISKANN 1 +#endif + +#ifndef SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE +#define SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE 0 +#endif + // VEC0_UNPREDICTABLE(cond) tells clang that cond is hard to predict, so it // emits a conditional move instead of a branch. Other compilers get cond as is. #if defined(__has_builtin) @@ -2287,6 +2299,9 @@ enum Vec0TokenType { TOKEN_TYPE_RBRACKET, TOKEN_TYPE_PLUS, TOKEN_TYPE_EQ, + TOKEN_TYPE_LPAREN, + TOKEN_TYPE_RPAREN, + TOKEN_TYPE_COMMA, }; struct Vec0Token { enum Vec0TokenType token_type; @@ -2337,6 +2352,24 @@ int vec0_token_next(char *start, char *end, struct Vec0Token *out) { out->end = ptr; out->token_type = TOKEN_TYPE_EQ; return VEC0_TOKEN_RESULT_SOME; + } else if (curr == '(') { + ptr++; + out->start = ptr; + out->end = ptr; + out->token_type = TOKEN_TYPE_LPAREN; + return VEC0_TOKEN_RESULT_SOME; + } else if (curr == ')') { + ptr++; + out->start = ptr; + out->end = ptr; + out->token_type = TOKEN_TYPE_RPAREN; + return VEC0_TOKEN_RESULT_SOME; + } else if (curr == ',') { + ptr++; + out->start = ptr; + out->end = ptr; + out->token_type = TOKEN_TYPE_COMMA; + return VEC0_TOKEN_RESULT_SOME; } else if (is_alpha(curr)) { char *start = ptr; while (ptr < end && (is_alpha(*ptr) || is_digit(*ptr) || *ptr == '_')) { @@ -2772,12 +2805,134 @@ enum Vec0DistanceMetrics { VEC0_DISTANCE_METRIC_L1 = 3, }; +// The index a vector column's `indexed by` clause names. A column without one +// is indexed by flat(), which scans every vector of the column. +// https://github.com/asg017/sqlite-vec/commit/0de765f +enum Vec0IndexType { + VEC0_INDEX_TYPE_FLAT = 1, +#if SQLITE_VEC_ENABLE_RESCORE + VEC0_INDEX_TYPE_RESCORE = 2, +#endif +#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE + VEC0_INDEX_TYPE_IVF = 3, +#endif +#if SQLITE_VEC_ENABLE_DISKANN + VEC0_INDEX_TYPE_DISKANN = 4, +#endif +}; + +#if SQLITE_VEC_ENABLE_RESCORE +// `indexed by rescore(quantizer=bit|int8, oversample=N)` keeps a quantized copy +// of each float32 vector in chunks for the KNN scan, which finds k * oversample +// candidates, and the vectors themselves by rowid to rank the candidates by +// their exact distances. +// https://github.com/asg017/sqlite-vec/commit/ba0db0b +enum Vec0RescoreQuantizerType { + VEC0_RESCORE_QUANTIZER_BIT = 1, + VEC0_RESCORE_QUANTIZER_INT8 = 2, +}; + +struct Vec0RescoreConfig { + enum Vec0RescoreQuantizerType quantizer_type; + // the oversample option of CREATE + int oversample; + // set by the 'oversample=N' command; 0 means the CREATE value + int oversample_search; +}; +#endif + +#if SQLITE_VEC_ENABLE_DISKANN +// `indexed by diskann(neighbor_quantizer=binary|int8, ...)` keeps a graph of +// the column's vectors, each node holding up to n_neighbors edges with a +// quantized copy of each neighbor's vector, and a KNN query walks the graph +// from its medoid, after the algorithms of the LM-DiskANN paper. +// https://github.com/asg017/sqlite-vec/commit/575371d +#define VEC0_DISKANN_DEFAULT_N_NEIGHBORS 72 +#define VEC0_DISKANN_MAX_N_NEIGHBORS 256 +#define VEC0_DISKANN_DEFAULT_SEARCH_LIST_SIZE 128 +#define VEC0_DISKANN_DEFAULT_ALPHA 1.2f + +enum Vec0DiskannQuantizerType { + // one bit per dimension + VEC0_DISKANN_QUANTIZER_BINARY = 1, + // one byte per dimension + VEC0_DISKANN_QUANTIZER_INT8 = 2, +}; + +struct Vec0DiskannConfig { + // how each node keeps its neighbors' vectors + enum Vec0DiskannQuantizerType quantizer_type; + // the most edges a node keeps (R in the paper), a multiple of 8 + int n_neighbors; + // the candidates a graph search keeps (L in the paper), for inserts and + // queries alike unless one of the two below is set + int search_list_size; + // the search list size of queries and of inserts; 0 means search_list_size + int search_list_size_search; + int search_list_size_insert; + // how far an edge must reach past the edges already kept for RobustPrune to + // keep it + f32 alpha; + // with N > 0, inserted vectors wait in _diskann_bufferNN and join the graph + // N at a time + int buffer_threshold; +}; +#endif + +#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE +// `indexed by ivf(nlist=N, nprobe=N, quantizer=none|int8|binary, +// oversample=N)` keeps the column's vectors in cells, each holding the rows +// nearest one centroid of a k-means clustering, and a KNN query reads only the +// cells of the nprobe centroids nearest the query, with the cells of the rows +// no clustering has placed yet. +// https://github.com/asg017/sqlite-vec/commit/e2c38f3 +#define VEC0_IVF_DEFAULT_NLIST 128 +#define VEC0_IVF_DEFAULT_NPROBE 10 +#define VEC0_IVF_MAX_NLIST 65536 +// the rows one cell holds +#define VEC0_IVF_CELL_MAX_VECTORS 64 +// the centroid id of the cells holding the rows no clustering has placed +#define VEC0_IVF_UNASSIGNED_CENTROID_ID (-1) +#define VEC0_IVF_KMEANS_MAX_ITER 25 +#define VEC0_IVF_KMEANS_DEFAULT_SEED 0 + +enum Vec0IvfQuantizer { + // a cell holds each float32 vector as is + VEC0_IVF_QUANTIZER_NONE = 0, + // a byte per dimension + VEC0_IVF_QUANTIZER_INT8 = 1, + // a bit per dimension + VEC0_IVF_QUANTIZER_BINARY = 2, +}; + +struct Vec0IvfConfig { + // the centroids compute-centroids makes; 0 leaves them to set-centroid + int nlist; + // the centroids whose cells a KNN query reads + int nprobe; + enum Vec0IvfQuantizer quantizer; + // with a quantizer, a KNN query ranks the k * oversample rows nearest by + // quantized distance by their exact distances + int oversample; +}; +#endif + struct VectorColumnDefinition { char *name; int name_length; size_t dimensions; enum VectorElementType element_type; enum Vec0DistanceMetrics distance_metric; + enum Vec0IndexType index_type; +#if SQLITE_VEC_ENABLE_RESCORE + struct Vec0RescoreConfig rescore; +#endif +#if SQLITE_VEC_ENABLE_DISKANN + struct Vec0DiskannConfig diskann; +#endif +#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE + struct Vec0IvfConfig ivf; +#endif }; struct Vec0PartitionColumnDefinition { @@ -2814,6 +2969,301 @@ size_t vector_column_byte_size(struct VectorColumnDefinition column) { return vector_byte_size(column.element_type, column.dimensions); } +#if SQLITE_VEC_ENABLE_RESCORE +/** + * @brief Parses the options of `indexed by rescore(...)`, from after its '(' + * through its ')': `quantizer=bit` or `quantizer=int8`, which is required, and + * `oversample=N` with N from 1 to 128, 8 when omitted, separated by commas. + * + * @return SQLITE_OK, or SQLITE_ERROR for any other text. + */ +static int vec0_parse_rescore_options(struct Vec0Scanner *scanner, + struct Vec0RescoreConfig *outConfig) { + struct Vec0Token token; + int hasQuantizer = 0; + outConfig->oversample = 8; + outConfig->oversample_search = 0; + + int rc = vec0_scanner_next(scanner, &token); + if (rc == VEC0_TOKEN_RESULT_SOME && token.token_type == TOKEN_TYPE_RPAREN) { + return SQLITE_ERROR; + } + while (1) { + if (rc != VEC0_TOKEN_RESULT_SOME || + token.token_type != TOKEN_TYPE_IDENTIFIER) { + return SQLITE_ERROR; + } + struct Vec0Token key = token; + rc = vec0_scanner_next(scanner, &token); + if (rc != VEC0_TOKEN_RESULT_SOME || token.token_type != TOKEN_TYPE_EQ) { + return SQLITE_ERROR; + } + rc = vec0_scanner_next(scanner, &token); + if (rc != VEC0_TOKEN_RESULT_SOME) { + return SQLITE_ERROR; + } + if (vec0_token_is_keyword(&key, "quantizer", true)) { + if (token.token_type != TOKEN_TYPE_IDENTIFIER) { + return SQLITE_ERROR; + } + if (vec0_token_is_keyword(&token, "bit", true)) { + outConfig->quantizer_type = VEC0_RESCORE_QUANTIZER_BIT; + } else if (vec0_token_is_keyword(&token, "int8", true)) { + outConfig->quantizer_type = VEC0_RESCORE_QUANTIZER_INT8; + } else { + return SQLITE_ERROR; + } + hasQuantizer = 1; + } else if (vec0_token_is_keyword(&key, "oversample", true)) { + if (token.token_type != TOKEN_TYPE_DIGIT) { + return SQLITE_ERROR; + } + errno = 0; + char *endptr; + long oversample = strtol(token.start, &endptr, 10); + if (errno == ERANGE || endptr != token.end || oversample < 1 || + oversample > 128) { + return SQLITE_ERROR; + } + outConfig->oversample = (int)oversample; + } else { + return SQLITE_ERROR; + } + + rc = vec0_scanner_next(scanner, &token); + if (rc == VEC0_TOKEN_RESULT_SOME && token.token_type == TOKEN_TYPE_RPAREN) { + break; + } + if (rc != VEC0_TOKEN_RESULT_SOME || token.token_type != TOKEN_TYPE_COMMA) { + return SQLITE_ERROR; + } + rc = vec0_scanner_next(scanner, &token); + } + return hasQuantizer ? SQLITE_OK : SQLITE_ERROR; +} +#endif + +#if SQLITE_VEC_ENABLE_DISKANN || SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE +/** + * @brief Reads the value of a numeric option of diskann() or ivf(): all + * digits, from min to max. + * + * @return SQLITE_OK, or SQLITE_ERROR for any other token. + */ +static int vec0_parse_option_int(const struct Vec0Token *token, long min, + long max, int *out) { + if (token->token_type != TOKEN_TYPE_DIGIT) { + return SQLITE_ERROR; + } + errno = 0; + char *endptr; + long value = strtol(token->start, &endptr, 10); + if (errno == ERANGE || endptr != token->end || value < min || value > max) { + return SQLITE_ERROR; + } + *out = (int)value; + return SQLITE_OK; +} +#endif + +#if SQLITE_VEC_ENABLE_DISKANN +/** + * @brief Parses the options of `indexed by diskann(...)`, from after its '(' + * through its ')', separated by commas: `neighbor_quantizer=binary` or + * `neighbor_quantizer=int8`, which is required; `n_neighbors=N`, a multiple + * of 8 up to 256, 72 when omitted; `search_list_size=N`, 128 when omitted, or + * in its place `search_list_size_search=N` and `search_list_size_insert=N`; + * and `buffer_threshold=N`, 0 when omitted. + * + * @return SQLITE_OK, or SQLITE_ERROR for any other text. + */ +static int vec0_parse_diskann_options(struct Vec0Scanner *scanner, + struct Vec0DiskannConfig *outConfig) { + struct Vec0Token token; + int hasQuantizer = 0; + int hasSearchListSize = 0; + int hasSplitSearchListSize = 0; + outConfig->n_neighbors = VEC0_DISKANN_DEFAULT_N_NEIGHBORS; + outConfig->search_list_size = VEC0_DISKANN_DEFAULT_SEARCH_LIST_SIZE; + outConfig->search_list_size_search = 0; + outConfig->search_list_size_insert = 0; + outConfig->alpha = VEC0_DISKANN_DEFAULT_ALPHA; + outConfig->buffer_threshold = 0; + + int rc = vec0_scanner_next(scanner, &token); + if (rc == VEC0_TOKEN_RESULT_SOME && token.token_type == TOKEN_TYPE_RPAREN) { + return SQLITE_ERROR; + } + while (1) { + if (rc != VEC0_TOKEN_RESULT_SOME || + token.token_type != TOKEN_TYPE_IDENTIFIER) { + return SQLITE_ERROR; + } + struct Vec0Token key = token; + rc = vec0_scanner_next(scanner, &token); + if (rc != VEC0_TOKEN_RESULT_SOME || token.token_type != TOKEN_TYPE_EQ) { + return SQLITE_ERROR; + } + rc = vec0_scanner_next(scanner, &token); + if (rc != VEC0_TOKEN_RESULT_SOME) { + return SQLITE_ERROR; + } + if (vec0_token_is_keyword(&key, "neighbor_quantizer", true)) { + if (token.token_type != TOKEN_TYPE_IDENTIFIER) { + return SQLITE_ERROR; + } + if (vec0_token_is_keyword(&token, "binary", true)) { + outConfig->quantizer_type = VEC0_DISKANN_QUANTIZER_BINARY; + } else if (vec0_token_is_keyword(&token, "int8", true)) { + outConfig->quantizer_type = VEC0_DISKANN_QUANTIZER_INT8; + } else { + return SQLITE_ERROR; + } + hasQuantizer = 1; + } else if (vec0_token_is_keyword(&key, "n_neighbors", true)) { + if (vec0_parse_option_int(&token, 8, VEC0_DISKANN_MAX_N_NEIGHBORS, + &outConfig->n_neighbors) != SQLITE_OK || + outConfig->n_neighbors % 8 != 0) { + return SQLITE_ERROR; + } + } else if (vec0_token_is_keyword(&key, "search_list_size", true)) { + if (vec0_parse_option_int(&token, 1, INT_MAX, + &outConfig->search_list_size) != SQLITE_OK) { + return SQLITE_ERROR; + } + hasSearchListSize = 1; + } else if (vec0_token_is_keyword(&key, "search_list_size_search", true)) { + if (vec0_parse_option_int(&token, 1, INT_MAX, + &outConfig->search_list_size_search) != + SQLITE_OK) { + return SQLITE_ERROR; + } + hasSplitSearchListSize = 1; + } else if (vec0_token_is_keyword(&key, "search_list_size_insert", true)) { + if (vec0_parse_option_int(&token, 1, INT_MAX, + &outConfig->search_list_size_insert) != + SQLITE_OK) { + return SQLITE_ERROR; + } + hasSplitSearchListSize = 1; + } else if (vec0_token_is_keyword(&key, "buffer_threshold", true)) { + if (vec0_parse_option_int(&token, 0, INT_MAX, + &outConfig->buffer_threshold) != SQLITE_OK) { + return SQLITE_ERROR; + } + } else { + return SQLITE_ERROR; + } + + rc = vec0_scanner_next(scanner, &token); + if (rc == VEC0_TOKEN_RESULT_SOME && token.token_type == TOKEN_TYPE_RPAREN) { + break; + } + if (rc != VEC0_TOKEN_RESULT_SOME || token.token_type != TOKEN_TYPE_COMMA) { + return SQLITE_ERROR; + } + rc = vec0_scanner_next(scanner, &token); + } + if (!hasQuantizer || (hasSearchListSize && hasSplitSearchListSize)) { + return SQLITE_ERROR; + } + return SQLITE_OK; +} +#endif + +#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE +/** + * @brief Parses the options of `indexed by ivf(...)`, from after its '(' + * through its ')', separated by commas, each optional: `nlist=N` from 0 to + * 65536, 128 when omitted; `nprobe=N` from 1 to nlist, 10 when omitted or + * nlist when that is less; `quantizer=none`, `quantizer=int8` or + * `quantizer=binary`, none when omitted; and `oversample=N`, 1 when omitted, + * more only with a quantizer. + * + * @return SQLITE_OK, or SQLITE_ERROR for any other text. + */ +static int vec0_parse_ivf_options(struct Vec0Scanner *scanner, + struct Vec0IvfConfig *outConfig) { + struct Vec0Token token; + int hasNprobe = 0; + outConfig->nlist = VEC0_IVF_DEFAULT_NLIST; + outConfig->nprobe = VEC0_IVF_DEFAULT_NPROBE; + outConfig->quantizer = VEC0_IVF_QUANTIZER_NONE; + outConfig->oversample = 1; + + int rc = vec0_scanner_next(scanner, &token); + int empty = + rc == VEC0_TOKEN_RESULT_SOME && token.token_type == TOKEN_TYPE_RPAREN; + while (!empty) { + if (rc != VEC0_TOKEN_RESULT_SOME || + token.token_type != TOKEN_TYPE_IDENTIFIER) { + return SQLITE_ERROR; + } + struct Vec0Token key = token; + rc = vec0_scanner_next(scanner, &token); + if (rc != VEC0_TOKEN_RESULT_SOME || token.token_type != TOKEN_TYPE_EQ) { + return SQLITE_ERROR; + } + rc = vec0_scanner_next(scanner, &token); + if (rc != VEC0_TOKEN_RESULT_SOME) { + return SQLITE_ERROR; + } + if (vec0_token_is_keyword(&key, "nlist", true)) { + if (vec0_parse_option_int(&token, 0, VEC0_IVF_MAX_NLIST, + &outConfig->nlist) != SQLITE_OK) { + return SQLITE_ERROR; + } + } else if (vec0_token_is_keyword(&key, "nprobe", true)) { + if (vec0_parse_option_int(&token, 1, VEC0_IVF_MAX_NLIST, + &outConfig->nprobe) != SQLITE_OK) { + return SQLITE_ERROR; + } + hasNprobe = 1; + } else if (vec0_token_is_keyword(&key, "quantizer", true)) { + if (token.token_type != TOKEN_TYPE_IDENTIFIER) { + return SQLITE_ERROR; + } + if (vec0_token_is_keyword(&token, "none", true)) { + outConfig->quantizer = VEC0_IVF_QUANTIZER_NONE; + } else if (vec0_token_is_keyword(&token, "int8", true)) { + outConfig->quantizer = VEC0_IVF_QUANTIZER_INT8; + } else if (vec0_token_is_keyword(&token, "binary", true)) { + outConfig->quantizer = VEC0_IVF_QUANTIZER_BINARY; + } else { + return SQLITE_ERROR; + } + } else if (vec0_token_is_keyword(&key, "oversample", true)) { + if (vec0_parse_option_int(&token, 1, INT_MAX, &outConfig->oversample) != + SQLITE_OK) { + return SQLITE_ERROR; + } + } else { + return SQLITE_ERROR; + } + + rc = vec0_scanner_next(scanner, &token); + if (rc == VEC0_TOKEN_RESULT_SOME && token.token_type == TOKEN_TYPE_RPAREN) { + break; + } + if (rc != VEC0_TOKEN_RESULT_SOME || token.token_type != TOKEN_TYPE_COMMA) { + return SQLITE_ERROR; + } + rc = vec0_scanner_next(scanner, &token); + } + if (outConfig->nlist > 0 && outConfig->nprobe > outConfig->nlist) { + if (hasNprobe) { + return SQLITE_ERROR; + } + outConfig->nprobe = outConfig->nlist; + } + if (outConfig->oversample > 1 && + outConfig->quantizer == VEC0_IVF_QUANTIZER_NONE) { + return SQLITE_ERROR; + } + return SQLITE_OK; +} +#endif + /** * @brief Parse an vec0 vtab argv[i] column definition and see if * it's a vector column defintion, ex `contents_embedding float[768]`. @@ -2842,6 +3292,19 @@ int vec0_parse_vector_column(const char *source, int source_length, int nameLength; enum VectorElementType elementType; enum Vec0DistanceMetrics distanceMetric = VEC0_DISTANCE_METRIC_L2; + enum Vec0IndexType indexType = VEC0_INDEX_TYPE_FLAT; +#if SQLITE_VEC_ENABLE_RESCORE + struct Vec0RescoreConfig rescoreConfig; + memset(&rescoreConfig, 0, sizeof(rescoreConfig)); +#endif +#if SQLITE_VEC_ENABLE_DISKANN + struct Vec0DiskannConfig diskannConfig; + memset(&diskannConfig, 0, sizeof(diskannConfig)); +#endif +#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE + struct Vec0IvfConfig ivfConfig; + memset(&ivfConfig, 0, sizeof(ivfConfig)); +#endif int dimensions; vec0_scanner_init(&scanner, source, source_length); @@ -2951,6 +3414,92 @@ int vec0_parse_vector_column(const char *source, int source_length, return SQLITE_ERROR; } } + // `indexed by flat()` or `indexed by rescore(...)`. No table predates the + // clause, so its keywords match exactly whether or not the parse is strict. + else if (vec0_token_is_keyword(&token, "indexed", true)) { + rc = vec0_scanner_next(&scanner, &token); + if (rc != VEC0_TOKEN_RESULT_SOME || + token.token_type != TOKEN_TYPE_IDENTIFIER || + !vec0_token_is_keyword(&token, "by", true)) { + return SQLITE_ERROR; + } + rc = vec0_scanner_next(&scanner, &token); + if (rc != VEC0_TOKEN_RESULT_SOME || + token.token_type != TOKEN_TYPE_IDENTIFIER) { + return SQLITE_ERROR; + } + if (vec0_token_is_keyword(&token, "flat", true)) { + indexType = VEC0_INDEX_TYPE_FLAT; + rc = vec0_scanner_next(&scanner, &token); + if (rc != VEC0_TOKEN_RESULT_SOME || + token.token_type != TOKEN_TYPE_LPAREN) { + return SQLITE_ERROR; + } + rc = vec0_scanner_next(&scanner, &token); + if (rc != VEC0_TOKEN_RESULT_SOME || + token.token_type != TOKEN_TYPE_RPAREN) { + return SQLITE_ERROR; + } + } +#if SQLITE_VEC_ENABLE_RESCORE + else if (vec0_token_is_keyword(&token, "rescore", true)) { + indexType = VEC0_INDEX_TYPE_RESCORE; + if (elementType != SQLITE_VEC_ELEMENT_TYPE_FLOAT32) { + return SQLITE_ERROR; + } + rc = vec0_scanner_next(&scanner, &token); + if (rc != VEC0_TOKEN_RESULT_SOME || + token.token_type != TOKEN_TYPE_LPAREN) { + return SQLITE_ERROR; + } + if (vec0_parse_rescore_options(&scanner, &rescoreConfig) != SQLITE_OK) { + return SQLITE_ERROR; + } + if (rescoreConfig.quantizer_type == VEC0_RESCORE_QUANTIZER_BIT && + (dimensions % CHAR_BIT) != 0) { + return SQLITE_ERROR; + } + } +#endif +#if SQLITE_VEC_ENABLE_DISKANN + // vec0_init() refuses a bit column and a binary quantizer on dimensions + // that are not a multiple of 8, with messages of their own + else if (vec0_token_is_keyword(&token, "diskann", true)) { + indexType = VEC0_INDEX_TYPE_DISKANN; + rc = vec0_scanner_next(&scanner, &token); + if (rc != VEC0_TOKEN_RESULT_SOME || + token.token_type != TOKEN_TYPE_LPAREN) { + return SQLITE_ERROR; + } + if (vec0_parse_diskann_options(&scanner, &diskannConfig) != SQLITE_OK) { + return SQLITE_ERROR; + } + } +#endif +#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE + else if (vec0_token_is_keyword(&token, "ivf", true)) { + indexType = VEC0_INDEX_TYPE_IVF; + if (elementType != SQLITE_VEC_ELEMENT_TYPE_FLOAT32) { + return SQLITE_ERROR; + } + rc = vec0_scanner_next(&scanner, &token); + if (rc != VEC0_TOKEN_RESULT_SOME || + token.token_type != TOKEN_TYPE_LPAREN) { + return SQLITE_ERROR; + } + if (vec0_parse_ivf_options(&scanner, &ivfConfig) != SQLITE_OK) { + return SQLITE_ERROR; + } + if (ivfConfig.quantizer == VEC0_IVF_QUANTIZER_BINARY && + (dimensions % CHAR_BIT) != 0) { + return SQLITE_ERROR; + } + } +#endif + else { + return SQLITE_ERROR; + } + } // unknown key else { return SQLITE_ERROR; @@ -2965,6 +3514,16 @@ int vec0_parse_vector_column(const char *source, int source_length, outColumn->distance_metric = distanceMetric; outColumn->element_type = elementType; outColumn->dimensions = dimensions; + outColumn->index_type = indexType; +#if SQLITE_VEC_ENABLE_RESCORE + outColumn->rescore = rescoreConfig; +#endif +#if SQLITE_VEC_ENABLE_DISKANN + outColumn->diskann = diskannConfig; +#endif +#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE + outColumn->ivf = ivfConfig; +#endif return SQLITE_OK; } @@ -4120,6 +4679,40 @@ struct vec0_vtab { // The first numMetadataColumns entries must be freed with sqlite3_free() char *shadowMetadataChunksNames[VEC0_MAX_METADATA_COLUMNS]; +#if SQLITE_VEC_ENABLE_RESCORE + // Name of the rescore chunk shadow table of each vector column indexed by + // rescore(), ie `_rescore_chunks00`, holding its quantized vectors in the + // _vector_chunks layout. NULL for other columns; freed with sqlite3_free(). + char *shadowRescoreChunksNames[VEC0_MAX_VECTOR_COLUMNS]; + + // Name of the rescore vector shadow table of each vector column indexed by + // rescore(), ie `_rescore_vectors00`, holding its float32 vectors by rowid. + // NULL for other columns; freed with sqlite3_free(). + char *shadowRescoreVectorsNames[VEC0_MAX_VECTOR_COLUMNS]; +#endif + +#if SQLITE_VEC_ENABLE_DISKANN + // Names of the shadow tables of each vector column indexed by diskann(), ie + // `_vectors00` with its vectors by rowid, `_diskann_nodes00` with the graph, + // one row per node, and `_diskann_buffer00` with the vectors waiting to join + // it. NULL for other columns; freed with sqlite3_free(). + char *shadowDiskannVectorsNames[VEC0_MAX_VECTOR_COLUMNS]; + char *shadowDiskannNodesNames[VEC0_MAX_VECTOR_COLUMNS]; + char *shadowDiskannBufferNames[VEC0_MAX_VECTOR_COLUMNS]; +#endif + +#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE + // Names of the shadow tables of each vector column indexed by ivf(), ie + // `_ivf_centroids00` with its centroids, `_ivf_cells00` with its vectors in + // cells, `_ivf_rowid_map00` with each row's cell and slot, and, with a + // quantizer, `_ivf_vectors00` with its float32 vectors by rowid. NULL for + // other columns; freed with sqlite3_free(). + char *shadowIvfCentroidsNames[VEC0_MAX_VECTOR_COLUMNS]; + char *shadowIvfCellsNames[VEC0_MAX_VECTOR_COLUMNS]; + char *shadowIvfRowidMapNames[VEC0_MAX_VECTOR_COLUMNS]; + char *shadowIvfVectorsNames[VEC0_MAX_VECTOR_COLUMNS]; +#endif + struct VectorColumnDefinition vector_columns[VEC0_MAX_VECTOR_COLUMNS]; struct Vec0PartitionColumnDefinition paritition_columns[VEC0_MAX_PARTITION_COLUMNS]; @@ -4185,6 +4778,42 @@ struct vec0_vtab { * Must be cleaned up with sqlite3_finalize(). */ sqlite3_stmt *stmtRowidsGetChunkPosition; + +#if SQLITE_VEC_ENABLE_DISKANN + // Statements a graph search runs once per node it visits, for each vector + // column indexed by diskann(): reading a node's row of _diskann_nodesNN, + // writing it, and reading and writing a vector's row of _vectorsNN. Each is + // prepared on first use and reset after every use. + sqlite3_stmt *stmtDiskannNodeRead[VEC0_MAX_VECTOR_COLUMNS]; + sqlite3_stmt *stmtDiskannNodeWrite[VEC0_MAX_VECTOR_COLUMNS]; + sqlite3_stmt *stmtDiskannVectorRead[VEC0_MAX_VECTOR_COLUMNS]; + sqlite3_stmt *stmtDiskannVectorWrite[VEC0_MAX_VECTOR_COLUMNS]; +#endif + +#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE + // Statements the inserts, deletes and queries of each vector column indexed + // by ivf() run, each prepared on first use and reset after every use. + struct { + // the first cell of a centroid with a free slot + sqlite3_stmt *cellFind; + // the cells of a centroid + sqlite3_stmt *cellScan; + // adds to a cell's count of rows + sqlite3_stmt *cellCount; + // a row's cell and slot, in _ivf_rowid_mapNN + sqlite3_stmt *mapRead; + sqlite3_stmt *mapWrite; + sqlite3_stmt *mapDelete; + // a row's float32 vector, in _ivf_vectorsNN + sqlite3_stmt *vectorRead; + sqlite3_stmt *vectorWrite; + sqlite3_stmt *vectorDelete; + // every centroid + sqlite3_stmt *centroids; + // the column's ivf_trained_N row of _info + sqlite3_stmt *trained; + } ivfStmts[VEC0_MAX_VECTOR_COLUMNS]; +#endif }; /** @@ -4203,6 +4832,34 @@ void vec0_free_resources(vec0_vtab *p) { p->stmtRowidsUpdatePosition = NULL; sqlite3_finalize(p->stmtRowidsGetChunkPosition); p->stmtRowidsGetChunkPosition = NULL; +#if SQLITE_VEC_ENABLE_DISKANN + for (int i = 0; i < VEC0_MAX_VECTOR_COLUMNS; i++) { + sqlite3_finalize(p->stmtDiskannNodeRead[i]); + p->stmtDiskannNodeRead[i] = NULL; + sqlite3_finalize(p->stmtDiskannNodeWrite[i]); + p->stmtDiskannNodeWrite[i] = NULL; + sqlite3_finalize(p->stmtDiskannVectorRead[i]); + p->stmtDiskannVectorRead[i] = NULL; + sqlite3_finalize(p->stmtDiskannVectorWrite[i]); + p->stmtDiskannVectorWrite[i] = NULL; + } +#endif +#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE + for (int i = 0; i < VEC0_MAX_VECTOR_COLUMNS; i++) { + sqlite3_stmt **stmts[] = { + &p->ivfStmts[i].cellFind, &p->ivfStmts[i].cellScan, + &p->ivfStmts[i].cellCount, &p->ivfStmts[i].mapRead, + &p->ivfStmts[i].mapWrite, &p->ivfStmts[i].mapDelete, + &p->ivfStmts[i].vectorRead, &p->ivfStmts[i].vectorWrite, + &p->ivfStmts[i].vectorDelete, &p->ivfStmts[i].centroids, + &p->ivfStmts[i].trained, + }; + for (size_t j = 0; j < countof(stmts); j++) { + sqlite3_finalize(*stmts[j]); + *stmts[j] = NULL; + } + } +#endif } /** @@ -4227,6 +4884,30 @@ void vec0_free(vec0_vtab *p) { for (int i = 0; i < p->numVectorColumns; i++) { sqlite3_free(p->shadowVectorChunksNames[i]); p->shadowVectorChunksNames[i] = NULL; +#if SQLITE_VEC_ENABLE_RESCORE + sqlite3_free(p->shadowRescoreChunksNames[i]); + p->shadowRescoreChunksNames[i] = NULL; + sqlite3_free(p->shadowRescoreVectorsNames[i]); + p->shadowRescoreVectorsNames[i] = NULL; +#endif +#if SQLITE_VEC_ENABLE_DISKANN + sqlite3_free(p->shadowDiskannVectorsNames[i]); + p->shadowDiskannVectorsNames[i] = NULL; + sqlite3_free(p->shadowDiskannNodesNames[i]); + p->shadowDiskannNodesNames[i] = NULL; + sqlite3_free(p->shadowDiskannBufferNames[i]); + p->shadowDiskannBufferNames[i] = NULL; +#endif +#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE + sqlite3_free(p->shadowIvfCentroidsNames[i]); + p->shadowIvfCentroidsNames[i] = NULL; + sqlite3_free(p->shadowIvfCellsNames[i]); + p->shadowIvfCellsNames[i] = NULL; + sqlite3_free(p->shadowIvfRowidMapNames[i]); + p->shadowIvfRowidMapNames[i] = NULL; + sqlite3_free(p->shadowIvfVectorsNames[i]); + p->shadowIvfVectorsNames[i] = NULL; +#endif sqlite3_free(p->vector_columns[i].name); p->vector_columns[i].name = NULL; } @@ -4253,10 +4934,24 @@ int vec0_num_defined_user_columns(vec0_vtab *p) { p->numMetadataColumns; } -/** - * @brief Returns the index of the distance hidden column for the given vec0 - * table. - * +#if SQLITE_VEC_ENABLE_RESCORE +static int rescore_new_chunk(vec0_vtab *p, i64 chunk_rowid); +#endif + +#if SQLITE_VEC_ENABLE_DISKANN +static int diskann_vector_read(vec0_vtab *p, int vec_col_idx, i64 rowid, + void **outVector, int *outVectorSize); +#endif + +#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE +static int ivf_vector_read(vec0_vtab *p, int i, i64 rowid, void **outVector, + int *outVectorSize); +#endif + +/** + * @brief Returns the index of the distance hidden column for the given vec0 + * table. + * * @param p vec0 table * @return int */ @@ -4585,9 +5280,55 @@ int vec0_get_vector_data(vec0_vtab *pVtab, i64 rowid, int vector_column_idx, goto cleanup; } - rc = sqlite3_blob_open(p->db, p->schemaName, - p->shadowVectorChunksNames[vector_column_idx], - "vectors", chunk_id, 0, &vectorBlob); +#if SQLITE_VEC_ENABLE_DISKANN + // a diskann() column keeps each vector in its row of _vectorsNN + if (p->vector_columns[vector_column_idx].index_type == + VEC0_INDEX_TYPE_DISKANN) { + rc = diskann_vector_read(p, vector_column_idx, rowid, outVector, + outVectorSize); + if (rc != SQLITE_OK) { + vtab_set_error(&pVtab->base, + "Could not fetch vector data for %lld from the DiskANN " + "vectors table", + rowid); + rc = rc == SQLITE_EMPTY ? SQLITE_ERROR : vec0_error_rc(rc); + } + goto cleanup; + } +#endif + +#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE + // an ivf() column keeps each float32 vector in a cell, or, with a + // quantizer, in its row of _ivf_vectorsNN + if (p->vector_columns[vector_column_idx].index_type == VEC0_INDEX_TYPE_IVF) { + rc = ivf_vector_read(p, vector_column_idx, rowid, outVector, outVectorSize); + if (rc != SQLITE_OK) { + vtab_set_error(&pVtab->base, + "Could not fetch vector data for %lld from the IVF index", + rowid); + rc = rc == SQLITE_EMPTY ? SQLITE_ERROR : vec0_error_rc(rc); + } + goto cleanup; + } +#endif + + size = vector_column_byte_size(pVtab->vector_columns[vector_column_idx]); +#if SQLITE_VEC_ENABLE_RESCORE + // a rescore() column keeps each float32 vector in a row of its own + if (p->vector_columns[vector_column_idx].index_type == + VEC0_INDEX_TYPE_RESCORE) { + rc = sqlite3_blob_open(p->db, p->schemaName, + p->shadowRescoreVectorsNames[vector_column_idx], + "vector", rowid, 0, &vectorBlob); + blobOffset = 0; + } else +#endif + { + rc = sqlite3_blob_open(p->db, p->schemaName, + p->shadowVectorChunksNames[vector_column_idx], + "vectors", chunk_id, 0, &vectorBlob); + blobOffset = chunk_offset * size; + } if (rc != SQLITE_OK) { vtab_set_error(&pVtab->base, @@ -4597,9 +5338,6 @@ int vec0_get_vector_data(vec0_vtab *pVtab, i64 rowid, int vector_column_idx, goto cleanup; } - size = vector_column_byte_size(pVtab->vector_columns[vector_column_idx]); - blobOffset = chunk_offset * size; - buf = sqlite3_malloc(size); if (!buf) { rc = SQLITE_NOMEM; @@ -5107,7 +5845,8 @@ int vec0_rowids_update_position(vec0_vtab *p, i64 rowid, i64 chunk_rowid, * chunks. * * Inserts a new row into the _chunks table, with blank data, and uses that new - * rowid to insert new blank rows into _vector_chunksXX tables. + * rowid to insert new blank rows into _vector_chunksXX tables, or into the + * _rescore_chunksXX tables of columns indexed by rescore(). * * @param p: vec0 table to add new chunk * @param paritionKeyValues: Array of partition key valeus for the new chunk, if @@ -5190,6 +5929,11 @@ int vec0_new_chunk(vec0_vtab *p, sqlite3_value **partitionKeyValues, continue; } int vector_column_idx = p->user_column_idxs[i]; + // a column indexed by anything but flat() keeps its vectors elsewhere + if (p->vector_columns[vector_column_idx].index_type != + VEC0_INDEX_TYPE_FLAT) { + continue; + } i64 vectorsSize = p->chunk_size * vector_column_byte_size( p->vector_columns[vector_column_idx]); @@ -5225,6 +5969,13 @@ int vec0_new_chunk(vec0_vtab *p, sqlite3_value **partitionKeyValues, } } +#if SQLITE_VEC_ENABLE_RESCORE + rc = rescore_new_chunk(p, rowid); + if (rc != SQLITE_OK) { + return rc; + } +#endif + // Step 3: Create new metadata chunks for each metadata column for (int i = 0; i < vec0_num_defined_user_columns(p); i++) { if (p->user_column_kinds[i] != SQLITE_VEC0_USER_COLUMN_KIND_METADATA) { @@ -5441,6 +6192,26 @@ static int vec0_init(sqlite3 *db, void *pAux, int argc, const char *const *argv, (i64)vecColumn.dimensions, SQLITE_VEC_VEC0_MAX_DIMENSIONS); goto error; } +#if SQLITE_VEC_ENABLE_DISKANN + if (vecColumn.index_type == VEC0_INDEX_TYPE_DISKANN) { + const char *zDiskannError = NULL; + if (vecColumn.element_type == SQLITE_VEC_ELEMENT_TYPE_BIT) { + zDiskannError = + "DiskANN index is not supported on bit vector columns"; + } else if (vecColumn.diskann.quantizer_type == + VEC0_DISKANN_QUANTIZER_BINARY && + (vecColumn.dimensions % CHAR_BIT) != 0) { + zDiskannError = + "DiskANN with binary quantizer requires dimensions divisible " + "by 8"; + } + if (zDiskannError) { + sqlite3_free(vecColumn.name); + *pzErr = sqlite3_mprintf(VEC_CONSTRUCTOR_ERROR "%s", zDiskannError); + goto error; + } + } +#endif pNew->user_column_kinds[user_column_idx] = SQLITE_VEC0_USER_COLUMN_KIND_VECTOR; pNew->user_column_idxs[user_column_idx] = numVectorColumns; @@ -5621,6 +6392,70 @@ static int vec0_init(sqlite3 *db, void *pAux, int argc, const char *const *argv, goto error; } +#if SQLITE_VEC_ENABLE_RESCORE + for (int i = 0; i < numVectorColumns; i++) { + if (pNew->vector_columns[i].index_type != VEC0_INDEX_TYPE_RESCORE) { + continue; + } + if (numMetadataColumns > 0) { + *pzErr = sqlite3_mprintf( + VEC_CONSTRUCTOR_ERROR + "Metadata columns are not supported with rescore indexes"); + goto error; + } + if (numPartitionColumns > 0) { + *pzErr = sqlite3_mprintf( + VEC_CONSTRUCTOR_ERROR + "Partition key columns are not supported with rescore indexes"); + goto error; + } + } +#endif + +#if SQLITE_VEC_ENABLE_DISKANN + // a graph search reaches the nearest vectors of the whole column, so no + // filter of a metadata column or partition key could apply before its k + for (int i = 0; i < numVectorColumns; i++) { + if (pNew->vector_columns[i].index_type != VEC0_INDEX_TYPE_DISKANN) { + continue; + } + if (numMetadataColumns > 0) { + *pzErr = sqlite3_mprintf(VEC_CONSTRUCTOR_ERROR + "Metadata columns are not supported with " + "DiskANN-indexed vector columns"); + goto error; + } + if (numPartitionColumns > 0) { + *pzErr = sqlite3_mprintf(VEC_CONSTRUCTOR_ERROR + "Partition key columns are not supported with " + "DiskANN-indexed vector columns"); + goto error; + } + } +#endif + +#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE + // a KNN query reads the cells of the centroids nearest its vector, so no + // filter of a metadata column or partition key could apply before its k + for (int i = 0; i < numVectorColumns; i++) { + if (pNew->vector_columns[i].index_type != VEC0_INDEX_TYPE_IVF) { + continue; + } + if (numMetadataColumns > 0) { + *pzErr = sqlite3_mprintf( + VEC_CONSTRUCTOR_ERROR + "metadata columns are not supported with IVF indexes"); + goto error; + } + if (numPartitionColumns > 0) { + *pzErr = sqlite3_mprintf( + VEC_CONSTRUCTOR_ERROR + "partition key columns are not supported with IVF indexes"); + goto error; + } + } +#endif + const char *schemaName = argv[1]; const char *tableName = argv[2]; @@ -5753,6 +6588,51 @@ static int vec0_init(sqlite3 *db, void *pAux, int argc, const char *const *argv, if (!pNew->shadowVectorChunksNames[i]) { goto error; } +#if SQLITE_VEC_ENABLE_RESCORE + if (pNew->vector_columns[i].index_type == VEC0_INDEX_TYPE_RESCORE) { + pNew->shadowRescoreChunksNames[i] = + sqlite3_mprintf("%s_rescore_chunks%02d", tableName, i); + if (!pNew->shadowRescoreChunksNames[i]) { + goto error; + } + pNew->shadowRescoreVectorsNames[i] = + sqlite3_mprintf("%s_rescore_vectors%02d", tableName, i); + if (!pNew->shadowRescoreVectorsNames[i]) { + goto error; + } + } +#endif +#if SQLITE_VEC_ENABLE_DISKANN + if (pNew->vector_columns[i].index_type == VEC0_INDEX_TYPE_DISKANN) { + pNew->shadowDiskannVectorsNames[i] = + sqlite3_mprintf("%s_vectors%02d", tableName, i); + pNew->shadowDiskannNodesNames[i] = + sqlite3_mprintf("%s_diskann_nodes%02d", tableName, i); + pNew->shadowDiskannBufferNames[i] = + sqlite3_mprintf("%s_diskann_buffer%02d", tableName, i); + if (!pNew->shadowDiskannVectorsNames[i] || + !pNew->shadowDiskannNodesNames[i] || + !pNew->shadowDiskannBufferNames[i]) { + goto error; + } + } +#endif +#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE + if (pNew->vector_columns[i].index_type == VEC0_INDEX_TYPE_IVF) { + pNew->shadowIvfCentroidsNames[i] = + sqlite3_mprintf("%s_ivf_centroids%02d", tableName, i); + pNew->shadowIvfCellsNames[i] = + sqlite3_mprintf("%s_ivf_cells%02d", tableName, i); + pNew->shadowIvfRowidMapNames[i] = + sqlite3_mprintf("%s_ivf_rowid_map%02d", tableName, i); + pNew->shadowIvfVectorsNames[i] = + sqlite3_mprintf("%s_ivf_vectors%02d", tableName, i); + if (!pNew->shadowIvfCentroidsNames[i] || !pNew->shadowIvfCellsNames[i] || + !pNew->shadowIvfRowidMapNames[i] || !pNew->shadowIvfVectorsNames[i]) { + goto error; + } + } +#endif } for (int i = 0; i < pNew->numMetadataColumns; i++) { pNew->shadowMetadataChunksNames[i] = @@ -5878,6 +6758,10 @@ static int vec0_init(sqlite3 *db, void *pAux, int argc, const char *const *argv, sqlite3_finalize(stmt); for (int i = 0; i < pNew->numVectorColumns; i++) { + // a column indexed by anything but flat() keeps its vectors elsewhere + if (pNew->vector_columns[i].index_type != VEC0_INDEX_TYPE_FLAT) { + continue; + } char *zSql = sqlite3_mprintf(VEC0_SHADOW_VECTOR_N_CREATE, pNew->schemaName, pNew->tableName, i); if (!zSql) { @@ -5896,6 +6780,179 @@ static int vec0_init(sqlite3 *db, void *pAux, int argc, const char *const *argv, sqlite3_finalize(stmt); } +#if SQLITE_VEC_ENABLE_RESCORE + for (int i = 0; i < pNew->numVectorColumns; i++) { + if (pNew->vector_columns[i].index_type != VEC0_INDEX_TYPE_RESCORE) { + continue; + } + const char *zCreate[] = { + "CREATE TABLE \"%w\".\"%w_rescore_chunks%02d\"" + "(rowid INTEGER PRIMARY KEY, vectors BLOB NOT NULL)", + "CREATE TABLE \"%w\".\"%w_rescore_vectors%02d\"" + "(rowid INTEGER PRIMARY KEY, vector BLOB NOT NULL)", + }; + const char *zShadow[] = {"_rescore_chunks", "_rescore_vectors"}; + for (size_t j = 0; j < countof(zCreate); j++) { + char *zSql = + sqlite3_mprintf(zCreate[j], pNew->schemaName, pNew->tableName, i); + if (!zSql) { + goto error; + } + rc = sqlite3_prepare_v2(db, zSql, -1, &stmt, 0); + sqlite3_free((void *)zSql); + if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { + sqlite3_finalize(stmt); + *pzErr = sqlite3_mprintf("Could not create '%s%02d' shadow table: %s", + zShadow[j], i, sqlite3_errmsg(db)); + goto error; + } + sqlite3_finalize(stmt); + } + } +#endif + +#if SQLITE_VEC_ENABLE_DISKANN + for (int i = 0; i < pNew->numVectorColumns; i++) { + if (pNew->vector_columns[i].index_type != VEC0_INDEX_TYPE_DISKANN) { + continue; + } + // the declarations asg017's DiskANN writes, so either opens the other's + // tables + const char *zCreate[] = { + "CREATE TABLE \"%w\".\"%w_vectors%02d\"" + " (rowid INTEGER PRIMARY KEY, vector BLOB NOT NULL)", + "CREATE TABLE \"%w\".\"%w_diskann_nodes%02d\"" + " (rowid INTEGER PRIMARY KEY, neighbors_validity BLOB NOT NULL, " + "neighbor_ids BLOB NOT NULL, neighbor_quantized_vectors BLOB NOT " + "NULL)", + "CREATE TABLE \"%w\".\"%w_diskann_buffer%02d\"" + " (rowid INTEGER PRIMARY KEY, vector BLOB NOT NULL)", + }; + const char *zShadow[] = {"_vectors", "_diskann_nodes", "_diskann_buffer"}; + for (size_t j = 0; j < countof(zCreate); j++) { + char *zSql = + sqlite3_mprintf(zCreate[j], pNew->schemaName, pNew->tableName, i); + if (!zSql) { + goto error; + } + rc = sqlite3_prepare_v2(db, zSql, -1, &stmt, 0); + sqlite3_free((void *)zSql); + if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { + sqlite3_finalize(stmt); + *pzErr = sqlite3_mprintf("Could not create '%s%02d' shadow table: %s", + zShadow[j], i, sqlite3_errmsg(db)); + goto error; + } + sqlite3_finalize(stmt); + } + + // the graph's entry point, NULL while the graph is empty + char *zSql = sqlite3_mprintf("INSERT INTO " VEC0_SHADOW_INFO_NAME + "(key, value) VALUES (?, NULL)", + pNew->schemaName, pNew->tableName); + char *zKey = sqlite3_mprintf("diskann_medoid_%02d", i); + if (!zSql || !zKey) { + sqlite3_free(zSql); + sqlite3_free(zKey); + goto error; + } + rc = sqlite3_prepare_v2(db, zSql, -1, &stmt, 0); + sqlite3_free(zSql); + if (rc == SQLITE_OK) { + sqlite3_bind_text(stmt, 1, zKey, -1, sqlite3_free); + } else { + sqlite3_free(zKey); + } + if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { + sqlite3_finalize(stmt); + *pzErr = sqlite3_mprintf("Could not seed '_info' shadow table: %s", + sqlite3_errmsg(db)); + goto error; + } + sqlite3_finalize(stmt); + } +#endif + +#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE + for (int i = 0; i < pNew->numVectorColumns; i++) { + if (pNew->vector_columns[i].index_type != VEC0_INDEX_TYPE_IVF) { + continue; + } + // asg017's declarations, except that the cells are found by centroid + // through the index of a UNIQUE constraint, which ALTER TABLE RENAME + // renames along with its table: an index from CREATE INDEX keeps its + // name through a rename, and nothing inside xRename can drop it, so a + // table created later under the old name could not create its own. + // Every statement on the cells names its columns and addresses a cell by + // rowid, so either implementation opens the other's tables. + const char *zCreate[] = { + "CREATE TABLE \"%w\".\"%w_ivf_centroids%02d\"" + " (centroid_id INTEGER PRIMARY KEY, centroid BLOB NOT NULL)", + "CREATE TABLE \"%w\".\"%w_ivf_cells%02d\"" + " (cell_id INTEGER PRIMARY KEY," + " centroid_id INTEGER NOT NULL," + " n_vectors INTEGER NOT NULL DEFAULT 0," + " validity BLOB NOT NULL," + " rowids BLOB NOT NULL," + " vectors BLOB NOT NULL," + " UNIQUE (centroid_id, cell_id))", + "CREATE TABLE \"%w\".\"%w_ivf_rowid_map%02d\"" + " (rowid INTEGER PRIMARY KEY, cell_id INTEGER NOT NULL," + " slot INTEGER NOT NULL)", + "CREATE TABLE \"%w\".\"%w_ivf_vectors%02d\"" + " (rowid INTEGER PRIMARY KEY, vector BLOB NOT NULL)", + }; + const char *zShadow[] = {"_ivf_centroids", "_ivf_cells", "_ivf_rowid_map", + "_ivf_vectors"}; + // without a quantizer, the cells hold the float32 vectors themselves + size_t nCreate = + pNew->vector_columns[i].ivf.quantizer == VEC0_IVF_QUANTIZER_NONE + ? countof(zCreate) - 1 + : countof(zCreate); + for (size_t j = 0; j < nCreate; j++) { + char *zSql = + sqlite3_mprintf(zCreate[j], pNew->schemaName, pNew->tableName, i); + if (!zSql) { + goto error; + } + rc = sqlite3_prepare_v2(db, zSql, -1, &stmt, 0); + sqlite3_free((void *)zSql); + if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { + sqlite3_finalize(stmt); + *pzErr = sqlite3_mprintf("Could not create '%s%02d' shadow table: %s", + zShadow[j], i, sqlite3_errmsg(db)); + goto error; + } + sqlite3_finalize(stmt); + } + + // set when the column has centroids + char *zSql = sqlite3_mprintf("INSERT INTO " VEC0_SHADOW_INFO_NAME + "(key, value) VALUES (?, '0')", + pNew->schemaName, pNew->tableName); + char *zKey = sqlite3_mprintf("ivf_trained_%d", i); + if (!zSql || !zKey) { + sqlite3_free(zSql); + sqlite3_free(zKey); + goto error; + } + rc = sqlite3_prepare_v2(db, zSql, -1, &stmt, 0); + sqlite3_free(zSql); + if (rc == SQLITE_OK) { + sqlite3_bind_text(stmt, 1, zKey, -1, sqlite3_free); + } else { + sqlite3_free(zKey); + } + if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { + sqlite3_finalize(stmt); + *pzErr = sqlite3_mprintf("Could not seed '_info' shadow table: %s", + sqlite3_errmsg(db)); + goto error; + } + sqlite3_finalize(stmt); + } +#endif + for (int i = 0; i < pNew->numMetadataColumns; i++) { char *zSql = sqlite3_mprintf("CREATE TABLE " VEC0_SHADOW_METADATA_N_NAME @@ -6038,15 +7095,44 @@ static int vec0Destroy(sqlite3_vtab *pVtab) { sqlite3_finalize(stmt); for (int i = 0; i < p->numVectorColumns; i++) { - zSql = sqlite3_mprintf("DROP TABLE \"%w\".\"%w\"", p->schemaName, - p->shadowVectorChunksNames[i]); - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, 0); - sqlite3_free((void *)zSql); - if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { - rc = SQLITE_ERROR; - goto done; + const char *shadowNames[] = { + p->vector_columns[i].index_type == VEC0_INDEX_TYPE_FLAT + ? p->shadowVectorChunksNames[i] + : NULL, +#if SQLITE_VEC_ENABLE_RESCORE + p->shadowRescoreChunksNames[i], + p->shadowRescoreVectorsNames[i], +#endif +#if SQLITE_VEC_ENABLE_DISKANN + p->shadowDiskannVectorsNames[i], + p->shadowDiskannNodesNames[i], + p->shadowDiskannBufferNames[i], +#endif +#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE + // dropping _ivf_cellsNN drops its index + p->shadowIvfCentroidsNames[i], + p->shadowIvfCellsNames[i], + p->shadowIvfRowidMapNames[i], + p->vector_columns[i].index_type == VEC0_INDEX_TYPE_IVF && + p->vector_columns[i].ivf.quantizer != VEC0_IVF_QUANTIZER_NONE + ? p->shadowIvfVectorsNames[i] + : NULL, +#endif + }; + for (size_t j = 0; j < countof(shadowNames); j++) { + if (!shadowNames[j]) { + continue; + } + zSql = sqlite3_mprintf("DROP TABLE \"%w\".\"%w\"", p->schemaName, + shadowNames[j]); + rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, 0); + sqlite3_free((void *)zSql); + if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { + rc = SQLITE_ERROR; + goto done; + } + sqlite3_finalize(stmt); } - sqlite3_finalize(stmt); } if (p->numAuxiliaryColumns > 0) { @@ -8924,56 +10010,4386 @@ static int vec0_knn_rowids_append(vec0_vtab *p, sqlite3_value *value, return array_append(rowids, &rowid); } -int vec0Filter_knn(vec0_cursor *pCur, vec0_vtab *p, int idxNum, - const char *idxStr, int argc, sqlite3_value **argv) { - assert(argc == (int)((strlen(idxStr) - 1) / 4)); - int rc; - struct vec0_query_knn_data *knn_data; - - int vectorColumnIdx = idxNum; - struct VectorColumnDefinition *vector_column = - &p->vector_columns[vectorColumnIdx]; - - struct Array *arrayRowidsIn = NULL; - sqlite3_stmt *stmtChunks = NULL; - void *queryVector; - size_t dimensions; - enum VectorElementType elementType; - vector_cleanup queryVectorCleanup = vector_cleanup_noop; - char *pzError; - knn_data = sqlite3_malloc(sizeof(*knn_data)); - if (!knn_data) { - return SQLITE_NOMEM; - } - memset(knn_data, 0, sizeof(*knn_data)); - // array of `struct Vec0MetadataIn`, IF there are any `xxx in (...)` metadata - // constraints - struct Array *aMetadataIn = NULL; - - int query_idx = -1; - int k_idx = -1; - int rowid_in_idx = -1; - int rowid_eq_idx = -1; - int mmr_lambda_idx = -1; - for (int i = 0; i < argc; i++) { - if (idxStr[1 + (i * 4)] == VEC0_IDXSTR_KIND_KNN_MATCH) { - query_idx = i; - } - if (idxStr[1 + (i * 4)] == VEC0_IDXSTR_KIND_KNN_K) { - k_idx = i; - } - if (idxStr[1 + (i * 4)] == VEC0_IDXSTR_KIND_KNN_ROWID_IN) { - rowid_in_idx = i; +#if SQLITE_VEC_ENABLE_RESCORE || SQLITE_VEC_ENABLE_DISKANN || \ + SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE +/** + * Keeps those of the first *n candidates, rowids and distances in step, whose + * distance meets every distance constraint of the KNN query, in their order, + * and sets *n to their number. + */ +static void vec0_knn_keep_within_distance(const char *idxStr, int argc, + sqlite3_value **argv, i64 *rowids, + f32 *distances, i64 *n) { + for (int c = 0; c < argc; c++) { + int idx = 1 + (c * 4); + if (idxStr[idx + 0] != VEC0_IDXSTR_KIND_KNN_DISTANCE_CONSTRAINT) { + continue; } - if (idxStr[1 + (i * 4)] == VEC0_IDXSTR_KIND_KNN_ROWID_EQ) { - rowid_eq_idx = i; + vec0_distance_constraint_operator op = idxStr[idx + 1]; + int type = sqlite3_value_type(argv[c]); + if (type != SQLITE_INTEGER && type != SQLITE_FLOAT) { + // No distance compares with NULL, and every distance is less than any + // TEXT or BLOB. + int keep = type != SQLITE_NULL && (op == VEC0_DISTANCE_CONSTRAINT_LT || + op == VEC0_DISTANCE_CONSTRAINT_LE); + if (!keep) { + *n = 0; + } + continue; } - if (idxStr[1 + (i * 4)] == VEC0_IDXSTR_KIND_KNN_MMR_LAMBDA) { - mmr_lambda_idx = i; + f32 target = (f32)sqlite3_value_double(argv[c]); + i64 kept = 0; + for (i64 j = 0; j < *n; j++) { + int pass; + switch (op) { + case VEC0_DISTANCE_CONSTRAINT_GE: + pass = distances[j] >= target; + break; + case VEC0_DISTANCE_CONSTRAINT_GT: + pass = distances[j] > target; + break; + case VEC0_DISTANCE_CONSTRAINT_LE: + pass = distances[j] <= target; + break; + case VEC0_DISTANCE_CONSTRAINT_LT: + pass = distances[j] < target; + break; + default: + pass = 1; + break; + } + if (pass) { + rowids[kept] = rowids[j]; + distances[kept] = distances[j]; + kept++; + } } + *n = kept; } - assert(query_idx >= 0); - assert(k_idx >= 0); +} +#endif + +#if SQLITE_VEC_ENABLE_DISKANN || SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE +/** + * Prepares *pStmt from zFormat, which names the schema and then zTable, unless + * a previous call prepared it. + */ +static int vec0_cached_stmt(vec0_vtab *p, sqlite3_stmt **pStmt, + const char *zFormat, const char *zTable) { + if (*pStmt) { + return SQLITE_OK; + } + char *zSql = sqlite3_mprintf(zFormat, p->schemaName, zTable); + if (!zSql) { + return SQLITE_NOMEM; + } + int rc = sqlite3_prepare_v2(p->db, zSql, -1, pStmt, NULL); + sqlite3_free(zSql); + return rc; +} + +/** + * Runs zFormat, a statement on table zTable of the schema whose one parameter + * is value, to its end. + */ +static int vec0_exec_i64(vec0_vtab *p, const char *zFormat, const char *zTable, + i64 value) { + sqlite3_stmt *stmt = NULL; + char *zSql = sqlite3_mprintf(zFormat, p->schemaName, zTable); + if (!zSql) { + return SQLITE_NOMEM; + } + int rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); + sqlite3_free(zSql); + if (rc != SQLITE_OK) { + return rc; + } + sqlite3_bind_int64(stmt, 1, value); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + return rc == SQLITE_DONE ? SQLITE_OK : vec0_error_rc(rc); +} +#endif + +#if SQLITE_VEC_ENABLE_RESCORE +// A vector column indexed by rescore() keeps two shadow tables: +// _rescore_chunksNN holds its vectors quantized, in the layout of +// _vector_chunksNN, for the KNN to scan, and _rescore_vectorsNN holds its +// float32 vectors by rowid, for the KNN to rank the candidates the scan finds. +// https://github.com/asg017/sqlite-vec/blob/04d28bd/sqlite-vec-rescore.c + +size_t rescore_quantized_byte_size(struct VectorColumnDefinition *col) { + switch (col->rescore.quantizer_type) { + case VEC0_RESCORE_QUANTIZER_BIT: + return col->dimensions / CHAR_BIT; + case VEC0_RESCORE_QUANTIZER_INT8: + return col->dimensions; + default: + return 0; + } +} + +// One bit per dimension, set when the value is >= 0. +void rescore_quantize_float_to_bit(const float *src, uint8_t *dst, + size_t dimensions) { + memset(dst, 0, dimensions / CHAR_BIT); + for (size_t i = 0; i < dimensions; i++) { + if (src[i] >= 0.0f) { + dst[i / CHAR_BIT] |= (1 << (i % CHAR_BIT)); + } + } +} + +// Maps [-1, 1] onto [-128, 127] in steps of 2/255, truncating toward zero, and +// clamps values outside it, NaN to 127. +void rescore_quantize_float_to_int8(const float *src, int8_t *dst, + size_t dimensions) { + float step = 2.0f / 255.0f; + for (size_t i = 0; i < dimensions; i++) { + float v = (src[i] - (-1.0f)) / step - 128.0f; + if (!(v <= 127.0f)) + v = 127.0f; + if (!(v >= -128.0f)) + v = -128.0f; + dst[i] = (int8_t)v; + } +} + +/** + * Inserts the row of chunk chunk_rowid, its quantized vectors zeroed, into the + * _rescore_chunksNN table of each column indexed by rescore(). + */ +static int rescore_new_chunk(vec0_vtab *p, i64 chunk_rowid) { + for (int i = 0; i < p->numVectorColumns; i++) { + if (p->vector_columns[i].index_type != VEC0_INDEX_TYPE_RESCORE) { + continue; + } + size_t quantized_size = rescore_quantized_byte_size(&p->vector_columns[i]); + i64 blob_size = (i64)p->chunk_size * (i64)quantized_size; + + char *zSql = sqlite3_mprintf( + "INSERT INTO \"%w\".\"%w\"(rowid, vectors) VALUES (?, ?)", + p->schemaName, p->shadowRescoreChunksNames[i]); + if (!zSql) { + return SQLITE_NOMEM; + } + sqlite3_stmt *stmt; + int rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); + sqlite3_free(zSql); + if (rc != SQLITE_OK) { + sqlite3_finalize(stmt); + return rc; + } + sqlite3_bind_int64(stmt, 1, chunk_rowid); + sqlite3_bind_zeroblob64(stmt, 2, blob_size); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + if (rc != SQLITE_DONE) { + return vec0_error_rc(rc); + } + } + return SQLITE_OK; +} + +/** + * Writes the quantized form of vector, a float32 vector of the column i + * indexed by rescore(), to the slot chunk_offset of chunk chunk_id in the + * column's _rescore_chunksNN table. + */ +static int rescore_write_quantized(vec0_vtab *p, int i, i64 chunk_id, + i64 chunk_offset, const void *vector) { + struct VectorColumnDefinition *col = &p->vector_columns[i]; + size_t qsize = rescore_quantized_byte_size(col); + void *qbuf = sqlite3_malloc(qsize); + if (!qbuf) { + return SQLITE_NOMEM; + } + switch (col->rescore.quantizer_type) { + case VEC0_RESCORE_QUANTIZER_BIT: + rescore_quantize_float_to_bit((const float *)vector, (uint8_t *)qbuf, + col->dimensions); + break; + case VEC0_RESCORE_QUANTIZER_INT8: + rescore_quantize_float_to_int8((const float *)vector, (int8_t *)qbuf, + col->dimensions); + break; + } + + sqlite3_blob *blob = NULL; + int rc = + sqlite3_blob_open(p->db, p->schemaName, p->shadowRescoreChunksNames[i], + "vectors", chunk_id, 1, &blob); + if (rc != SQLITE_OK) { + sqlite3_free(qbuf); + return vec0_error_rc(rc); + } + rc = sqlite3_blob_write(blob, qbuf, qsize, chunk_offset * qsize); + sqlite3_free(qbuf); + int brc = sqlite3_blob_close(blob); + if (rc != SQLITE_OK) { + return rc; + } + return brc; +} + +/** + * Writes the vectors of a new row to the columns indexed by rescore(): each + * quantized to the row's slot in _rescore_chunksNN, and as is to a new row of + * _rescore_vectorsNN. + */ +static int rescore_on_insert(vec0_vtab *p, i64 chunk_rowid, i64 chunk_offset, + i64 rowid, void *vectorDatas[]) { + for (int i = 0; i < p->numVectorColumns; i++) { + if (p->vector_columns[i].index_type != VEC0_INDEX_TYPE_RESCORE) { + continue; + } + int rc = rescore_write_quantized(p, i, chunk_rowid, chunk_offset, + vectorDatas[i]); + if (rc != SQLITE_OK) { + return rc; + } + + char *zSql = sqlite3_mprintf( + "INSERT INTO \"%w\".\"%w\"(rowid, vector) VALUES (?, ?)", p->schemaName, + p->shadowRescoreVectorsNames[i]); + if (!zSql) { + return SQLITE_NOMEM; + } + sqlite3_stmt *stmt; + rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); + sqlite3_free(zSql); + if (rc != SQLITE_OK) { + sqlite3_finalize(stmt); + return rc; + } + sqlite3_bind_int64(stmt, 1, rowid); + sqlite3_bind_blob(stmt, 2, vectorDatas[i], + vector_column_byte_size(p->vector_columns[i]), + SQLITE_TRANSIENT); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + if (rc != SQLITE_DONE) { + return vec0_error_rc(rc); + } + } + return SQLITE_OK; +} + +/** + * Zeroes a deleted row's quantized vectors in _rescore_chunksNN, and deletes + * its row of _rescore_vectorsNN, for each column indexed by rescore(). + */ +static int rescore_on_delete(vec0_vtab *p, i64 chunk_id, i64 chunk_offset, + i64 rowid) { + for (int i = 0; i < p->numVectorColumns; i++) { + if (p->vector_columns[i].index_type != VEC0_INDEX_TYPE_RESCORE) { + continue; + } + size_t qsize = rescore_quantized_byte_size(&p->vector_columns[i]); + void *zeroBuf = sqlite3_malloc(qsize); + if (!zeroBuf) { + return SQLITE_NOMEM; + } + memset(zeroBuf, 0, qsize); + + sqlite3_blob *blob = NULL; + int rc = + sqlite3_blob_open(p->db, p->schemaName, p->shadowRescoreChunksNames[i], + "vectors", chunk_id, 1, &blob); + if (rc != SQLITE_OK) { + sqlite3_free(zeroBuf); + return vec0_error_rc(rc); + } + rc = sqlite3_blob_write(blob, zeroBuf, qsize, chunk_offset * qsize); + sqlite3_free(zeroBuf); + int brc = sqlite3_blob_close(blob); + if (rc != SQLITE_OK) { + return rc; + } + if (brc != SQLITE_OK) { + return brc; + } + + char *zSql = + sqlite3_mprintf("DELETE FROM \"%w\".\"%w\" WHERE rowid = ?", + p->schemaName, p->shadowRescoreVectorsNames[i]); + if (!zSql) { + return SQLITE_NOMEM; + } + sqlite3_stmt *stmt; + rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); + sqlite3_free(zSql); + if (rc != SQLITE_OK) { + return rc; + } + sqlite3_bind_int64(stmt, 1, rowid); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + if (rc != SQLITE_DONE) { + return vec0_error_rc(rc); + } + } + return SQLITE_OK; +} + +/** + * Deletes chunk chunk_id from the _rescore_chunksNN table of each column + * indexed by rescore(). rescore_on_delete() already deleted the chunk's rows of + * _rescore_vectorsNN. + */ +static int rescore_delete_chunk(vec0_vtab *p, i64 chunk_id) { + for (int i = 0; i < p->numVectorColumns; i++) { + if (p->vector_columns[i].index_type != VEC0_INDEX_TYPE_RESCORE) { + continue; + } + char *zSql = sqlite3_mprintf("DELETE FROM \"%w\".\"%w\" WHERE rowid = ?", + p->schemaName, p->shadowRescoreChunksNames[i]); + if (!zSql) { + return SQLITE_NOMEM; + } + sqlite3_stmt *stmt; + int rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); + sqlite3_free(zSql); + if (rc != SQLITE_OK) { + return rc; + } + sqlite3_bind_int64(stmt, 1, chunk_id); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + if (rc != SQLITE_DONE) { + return vec0_error_rc(rc); + } + } + return SQLITE_OK; +} + +/** + * The KNN of a column indexed by rescore(). It scans the quantized vectors of + * the chunks stmtChunks returns for the k * oversample rows nearest the + * quantized query vector, ranks those candidates by the distance of their + * float32 vectors to the query vector, and returns the k nearest that meet the + * query's distance constraints. Takes and returns what + * vec0Filter_knn_chunks_iter() does. + * + * Distance constraints apply to the float32 distances of the candidates, as in + * https://github.com/asg017/sqlite-vec/pull/309 + */ +static int rescore_knn(vec0_vtab *p, sqlite3_stmt *stmtChunks, + struct VectorColumnDefinition *vector_column, + int vectorColumnIdx, struct Array *arrayRowidsIn, + const char *idxStr, int argc, sqlite3_value **argv, + void *queryVector, i64 k, i64 **out_topk_rowids, + f32 **out_topk_distances, i64 *out_used) { + int rc = SQLITE_OK; + int oversample = vector_column->rescore.oversample_search > 0 + ? vector_column->rescore.oversample_search + : vector_column->rescore.oversample; + i64 k_oversample = k * oversample; + if (k_oversample > 4096) { + k_oversample = 4096; + } + + size_t qdim = vector_column->dimensions; + size_t qsize = rescore_quantized_byte_size(vector_column); + size_t fsize = vector_column_byte_size(*vector_column); + + sqlite3_blob *blobQ = NULL; + sqlite3_blob *blobFloat = NULL; + i64 *cand_rowids = NULL; + f32 *cand_distances = NULL; + i64 *tmp_rowids = NULL; + f32 *tmp_distances = NULL; + f32 *chunk_distances = NULL; + i32 *chunk_topk_idxs = NULL; + u8 *b = NULL; + u8 *bmRowids = NULL; + void *baseVectors = NULL; + f32 *float_distances = NULL; + void *fBuf = NULL; + i64 *out_rowids = NULL; + f32 *out_distances = NULL; + + void *quantizedQuery = sqlite3_malloc(qsize); + if (!quantizedQuery) { + return SQLITE_NOMEM; + } + switch (vector_column->rescore.quantizer_type) { + case VEC0_RESCORE_QUANTIZER_BIT: + rescore_quantize_float_to_bit((const float *)queryVector, + (uint8_t *)quantizedQuery, qdim); + break; + case VEC0_RESCORE_QUANTIZER_INT8: + rescore_quantize_float_to_int8((const float *)queryVector, + (int8_t *)quantizedQuery, qdim); + break; + } + + // Phase 1: scan the quantized chunks for the k * oversample nearest rows + cand_rowids = sqlite3_malloc(k_oversample * sizeof(i64)); + cand_distances = sqlite3_malloc(k_oversample * sizeof(f32)); + tmp_rowids = sqlite3_malloc(k_oversample * sizeof(i64)); + tmp_distances = sqlite3_malloc(k_oversample * sizeof(f32)); + chunk_distances = sqlite3_malloc(p->chunk_size * sizeof(f32)); + chunk_topk_idxs = sqlite3_malloc(k_oversample * sizeof(i32)); + b = bitmap_new(p->chunk_size); + baseVectors = sqlite3_malloc((i64)p->chunk_size * (i64)qsize); + if (!cand_rowids || !cand_distances || !tmp_rowids || !tmp_distances || + !chunk_distances || !chunk_topk_idxs || !b || !baseVectors) { + rc = SQLITE_NOMEM; + goto cleanup; + } + memset(cand_rowids, 0, k_oversample * sizeof(i64)); + memset(cand_distances, 0, k_oversample * sizeof(f32)); + + if (arrayRowidsIn) { + bmRowids = bitmap_new(p->chunk_size); + if (!bmRowids) { + rc = SQLITE_NOMEM; + goto cleanup; + } + } + + i64 cand_used = 0; + while (1) { + rc = sqlite3_step(stmtChunks); + if (rc == SQLITE_DONE) { + break; + } + if (rc != SQLITE_ROW) { + vtab_set_error(&p->base, "chunks iter error"); + rc = vec0_error_rc(rc); + goto cleanup; + } + + i64 chunk_id = sqlite3_column_int64(stmtChunks, 0); + unsigned char *chunkValidity = + (unsigned char *)sqlite3_column_blob(stmtChunks, 1); + i64 validitySize = sqlite3_column_bytes(stmtChunks, 1); + if (validitySize != p->chunk_size / CHAR_BIT) { + vtab_set_error( + &p->base, + "chunk validity size doesn't match - expected %lld, found %lld", + p->chunk_size / CHAR_BIT, validitySize); + rc = SQLITE_ERROR; + goto cleanup; + } + i64 *chunkRowids = (i64 *)sqlite3_column_blob(stmtChunks, 2); + i64 rowidsSize = sqlite3_column_bytes(stmtChunks, 2); + if (rowidsSize != (i64)(p->chunk_size * sizeof(i64))) { + vtab_set_error(&p->base, + "chunk rowids size doesn't match - expected %lld, found " + "%lld", + p->chunk_size * sizeof(i64), rowidsSize); + rc = SQLITE_ERROR; + goto cleanup; + } + + memset(chunk_distances, 0, p->chunk_size * sizeof(f32)); + memset(chunk_topk_idxs, 0, k_oversample * sizeof(i32)); + bitmap_copy(b, chunkValidity, p->chunk_size); + + if (arrayRowidsIn) { + bitmap_clear(bmRowids, p->chunk_size); + for (int j = 0; j < p->chunk_size; j++) { + if (!bitmap_get(chunkValidity, j)) { + continue; + } + i64 rid = chunkRowids[j]; + void *found = bsearch(&rid, arrayRowidsIn->z, arrayRowidsIn->length, + sizeof(i64), _cmp); + bitmap_set(bmRowids, j, found ? 1 : 0); + } + bitmap_and_inplace(b, bmRowids, p->chunk_size); + } + + // A chunk whose rowid filter keeps no row offers no candidate, so its + // vectors are never read. + if (!bitmap_any(b, p->chunk_size)) { + continue; + } + + rc = sqlite3_blob_open(p->db, p->schemaName, + p->shadowRescoreChunksNames[vectorColumnIdx], + "vectors", chunk_id, 0, &blobQ); + if (rc != SQLITE_OK) { + vtab_set_error(&p->base, + "could not open rescore vectors blob for chunk %lld", + chunk_id); + rc = vec0_error_rc(rc); + goto cleanup; + } + i64 currentBaseVectorsSize = sqlite3_blob_bytes(blobQ); + i64 expectedBaseVectorsSize = (i64)p->chunk_size * (i64)qsize; + if (currentBaseVectorsSize != expectedBaseVectorsSize) { + vtab_set_error( + &p->base, + "rescore vectors blob size doesn't match - expected %lld, found %lld", + expectedBaseVectorsSize, currentBaseVectorsSize); + rc = SQLITE_ERROR; + goto cleanup; + } + rc = sqlite3_blob_read(blobQ, baseVectors, currentBaseVectorsSize, 0); + // blobQ is opened read-only, so closing it never fails + sqlite3_blob_close(blobQ); + blobQ = NULL; + if (rc != SQLITE_OK) { + vtab_set_error(&p->base, "rescore vectors blob read error for %lld", + chunk_id); + rc = SQLITE_ERROR; + goto cleanup; + } + + for (int j = 0; j < p->chunk_size; j++) { + if (!bitmap_get(b, j)) { + continue; + } + f32 dist = FLT_MAX; + switch (vector_column->rescore.quantizer_type) { + case VEC0_RESCORE_QUANTIZER_BIT: { + const u8 *base_j = ((u8 *)baseVectors) + (j * (qdim / CHAR_BIT)); + dist = distance_hamming(base_j, (u8 *)quantizedQuery, &qdim); + break; + } + case VEC0_RESCORE_QUANTIZER_INT8: { + const i8 *base_j = ((i8 *)baseVectors) + (j * qdim); + switch (vector_column->distance_metric) { + case VEC0_DISTANCE_METRIC_L2: + dist = distance_l2_sqr_int8(base_j, (i8 *)quantizedQuery, &qdim); + break; + case VEC0_DISTANCE_METRIC_COSINE: + dist = distance_cosine_int8(base_j, (i8 *)quantizedQuery, &qdim); + break; + case VEC0_DISTANCE_METRIC_L1: + dist = (f32)distance_l1_int8(base_j, (i8 *)quantizedQuery, &qdim); + break; + } + break; + } + } + chunk_distances[j] = dist; + } + + int used1; + min_idx(chunk_distances, p->chunk_size, b, chunk_topk_idxs, + min(k_oversample, p->chunk_size), &used1); + + i64 merged_used; + merge_sorted_lists(cand_distances, cand_rowids, cand_used, chunk_distances, + chunkRowids, chunk_topk_idxs, + min(min(k_oversample, p->chunk_size), used1), + tmp_distances, tmp_rowids, k_oversample, &merged_used); + + for (i64 j = 0; j < merged_used; j++) { + cand_rowids[j] = tmp_rowids[j]; + cand_distances[j] = tmp_distances[j]; + } + cand_used = merged_used; + } + rc = SQLITE_OK; + + // Phase 2: rank the candidates by the distances of their float32 vectors, + // which blob_reopen() reads from _rescore_vectorsNN by rowid directly + if (cand_used > 0) { + float_distances = sqlite3_malloc(cand_used * sizeof(f32)); + fBuf = sqlite3_malloc(fsize); + if (!float_distances || !fBuf) { + rc = SQLITE_NOMEM; + goto cleanup; + } + for (i64 j = 0; j < cand_used; j++) { + if (j == 0) { + rc = sqlite3_blob_open(p->db, p->schemaName, + p->shadowRescoreVectorsNames[vectorColumnIdx], + "vector", cand_rowids[0], 0, &blobFloat); + } else { + rc = sqlite3_blob_reopen(blobFloat, cand_rowids[j]); + } + if (rc != SQLITE_OK) { + vtab_set_error(&p->base, + "could not read the rescore vector of row %lld", + cand_rowids[j]); + rc = vec0_error_rc(rc); + goto cleanup; + } + rc = sqlite3_blob_read(blobFloat, fBuf, fsize, 0); + if (rc != SQLITE_OK) { + vtab_set_error(&p->base, + "could not read the rescore vector of row %lld", + cand_rowids[j]); + rc = SQLITE_ERROR; + goto cleanup; + } + float_distances[j] = + vec0_compute_distance(vector_column, fBuf, queryVector); + } + + vec0_knn_keep_within_distance(idxStr, argc, argv, cand_rowids, + float_distances, &cand_used); + + // sort by float32 distance + for (i64 a = 0; a + 1 < cand_used; a++) { + i64 minIdx = a; + for (i64 c = a + 1; c < cand_used; c++) { + if (float_distances[c] < float_distances[minIdx]) { + minIdx = c; + } + } + if (minIdx != a) { + f32 td = float_distances[a]; + float_distances[a] = float_distances[minIdx]; + float_distances[minIdx] = td; + i64 tr = cand_rowids[a]; + cand_rowids[a] = cand_rowids[minIdx]; + cand_rowids[minIdx] = tr; + } + } + } + + i64 result_k = min(k, cand_used); + if (result_k > 0) { + out_rowids = sqlite3_malloc(result_k * sizeof(i64)); + out_distances = sqlite3_malloc(result_k * sizeof(f32)); + if (!out_rowids || !out_distances) { + sqlite3_free(out_rowids); + sqlite3_free(out_distances); + rc = SQLITE_NOMEM; + goto cleanup; + } + for (i64 j = 0; j < result_k; j++) { + out_rowids[j] = cand_rowids[j]; + out_distances[j] = float_distances[j]; + } + } + *out_topk_rowids = out_rowids; + *out_topk_distances = out_distances; + *out_used = result_k; + +cleanup: + // both blobs are opened read-only, so closing them never fails + sqlite3_blob_close(blobQ); + sqlite3_blob_close(blobFloat); + sqlite3_free(quantizedQuery); + sqlite3_free(cand_rowids); + sqlite3_free(cand_distances); + sqlite3_free(tmp_rowids); + sqlite3_free(tmp_distances); + sqlite3_free(chunk_distances); + sqlite3_free(chunk_topk_idxs); + sqlite3_free(b); + sqlite3_free(bmRowids); + sqlite3_free(baseVectors); + sqlite3_free(float_distances); + sqlite3_free(fBuf); + return rc; +} + +/** + * The command `oversample=N` sets the oversample of every column indexed by + * rescore() for the KNN queries of this connection, in place of the one its + * CREATE gave. Returns SQLITE_EMPTY for any other command. + */ +static int rescore_handle_command(vec0_vtab *p, const char *command) { + static const char zKey[] = "oversample="; + if (strncmp(command, zKey, sizeof(zKey) - 1) != 0) { + return SQLITE_EMPTY; + } + const char *zValue = command + sizeof(zKey) - 1; + char *zEnd; + errno = 0; + long oversample = strtol(zValue, &zEnd, 10); + if (!(*zValue >= '0' && *zValue <= '9') || *zEnd != '\0' || errno == ERANGE || + oversample < 1 || oversample > INT_MAX) { + vtab_set_error(&p->base, "oversample must be >= 1"); + return SQLITE_ERROR; + } + for (int i = 0; i < p->numVectorColumns; i++) { + if (p->vector_columns[i].index_type == VEC0_INDEX_TYPE_RESCORE) { + p->vector_columns[i].rescore.oversample_search = (int)oversample; + } + } + return SQLITE_OK; +} +#endif + +#if SQLITE_VEC_ENABLE_DISKANN +// A vector column indexed by diskann() keeps three shadow tables: _vectorsNN +// holds each vector by rowid, _diskann_nodesNN holds a row for each node of +// the graph, and _diskann_bufferNN holds the vectors waiting to join the graph +// when buffer_threshold is set. A node's row holds a validity bitmap of its +// n_neighbors slots, the rowid of the neighbor in each slot, and each +// neighbor's vector quantized by the column's neighbor_quantizer, so a search +// ranks a node's neighbors without reading their vectors. Every search starts +// at the medoid, the `diskann_medoid_NN` row of _info. +// https://github.com/asg017/sqlite-vec/blob/04d28bd/sqlite-vec-diskann.c + +int diskann_validity_byte_size(int n_neighbors) { + return n_neighbors / CHAR_BIT; +} + +size_t diskann_neighbor_ids_byte_size(int n_neighbors) { + return (size_t)n_neighbors * sizeof(i64); +} + +size_t +diskann_quantized_vector_byte_size(enum Vec0DiskannQuantizerType quantizer_type, + size_t dimensions) { + switch (quantizer_type) { + case VEC0_DISKANN_QUANTIZER_BINARY: + return dimensions / CHAR_BIT; + case VEC0_DISKANN_QUANTIZER_INT8: + return dimensions; + } + return 0; +} + +size_t +diskann_neighbor_qvecs_byte_size(int n_neighbors, + enum Vec0DiskannQuantizerType quantizer_type, + size_t dimensions) { + return (size_t)n_neighbors * + diskann_quantized_vector_byte_size(quantizer_type, dimensions); +} + +/** + * Allocates the three blobs of a node with no neighbor, each to be freed with + * sqlite3_free(). + */ +int diskann_node_init(int n_neighbors, + enum Vec0DiskannQuantizerType quantizer_type, + size_t dimensions, u8 **outValidity, int *outValiditySize, + u8 **outNeighborIds, int *outNeighborIdsSize, + u8 **outNeighborQvecs, int *outNeighborQvecsSize) { + int validitySize = diskann_validity_byte_size(n_neighbors); + int idsSize = (int)diskann_neighbor_ids_byte_size(n_neighbors); + int qvecsSize = (int)diskann_neighbor_qvecs_byte_size( + n_neighbors, quantizer_type, dimensions); + u8 *validity = sqlite3_malloc(validitySize); + u8 *ids = sqlite3_malloc(idsSize); + u8 *qvecs = sqlite3_malloc(qvecsSize); + if (!validity || !ids || !qvecs) { + sqlite3_free(validity); + sqlite3_free(ids); + sqlite3_free(qvecs); + return SQLITE_NOMEM; + } + memset(validity, 0, validitySize); + memset(ids, 0, idsSize); + memset(qvecs, 0, qvecsSize); + *outValidity = validity; + *outValiditySize = validitySize; + *outNeighborIds = ids; + *outNeighborIdsSize = idsSize; + *outNeighborQvecs = qvecs; + *outNeighborQvecsSize = qvecsSize; + return SQLITE_OK; +} + +int diskann_validity_get(const u8 *validity, int i) { + return (validity[i / CHAR_BIT] >> (i % CHAR_BIT)) & 1; +} + +void diskann_validity_set(u8 *validity, int i, int value) { + if (value) { + validity[i / CHAR_BIT] |= (1 << (i % CHAR_BIT)); + } else { + validity[i / CHAR_BIT] &= ~(1 << (i % CHAR_BIT)); + } +} + +int diskann_validity_count(const u8 *validity, int n_neighbors) { + int count = 0; + for (int i = 0; i < n_neighbors; i++) { + count += diskann_validity_get(validity, i); + } + return count; +} + +i64 diskann_neighbor_id_get(const u8 *neighbor_ids, int i) { + i64 rowid; + memcpy(&rowid, neighbor_ids + i * sizeof(i64), sizeof(i64)); + return rowid; +} + +void diskann_neighbor_id_set(u8 *neighbor_ids, int i, i64 rowid) { + memcpy(neighbor_ids + i * sizeof(i64), &rowid, sizeof(i64)); +} + +const u8 * +diskann_neighbor_qvec_get(const u8 *qvecs, int i, + enum Vec0DiskannQuantizerType quantizer_type, + size_t dimensions) { + return qvecs + (size_t)i * diskann_quantized_vector_byte_size(quantizer_type, + dimensions); +} + +void diskann_neighbor_qvec_set(u8 *qvecs, int i, const u8 *src_qvec, + enum Vec0DiskannQuantizerType quantizer_type, + size_t dimensions) { + size_t size = diskann_quantized_vector_byte_size(quantizer_type, dimensions); + memcpy(qvecs + (size_t)i * size, src_qvec, size); +} + +void diskann_node_set_neighbor(u8 *validity, u8 *neighbor_ids, u8 *qvecs, int i, + i64 neighbor_rowid, const u8 *neighbor_qvec, + enum Vec0DiskannQuantizerType quantizer_type, + size_t dimensions) { + diskann_validity_set(validity, i, 1); + diskann_neighbor_id_set(neighbor_ids, i, neighbor_rowid); + diskann_neighbor_qvec_set(qvecs, i, neighbor_qvec, quantizer_type, + dimensions); +} + +void diskann_node_clear_neighbor(u8 *validity, u8 *neighbor_ids, u8 *qvecs, + int i, + enum Vec0DiskannQuantizerType quantizer_type, + size_t dimensions) { + size_t size = diskann_quantized_vector_byte_size(quantizer_type, dimensions); + diskann_validity_set(validity, i, 0); + diskann_neighbor_id_set(neighbor_ids, i, 0); + memset(qvecs + (size_t)i * size, 0, size); +} + +/** + * Writes the quantized form of a float32 vector to out, which holds + * diskann_quantized_vector_byte_size() bytes: under binary a bit per + * dimension, set when the value is > 0; under int8 [-1, 1] mapped onto + * [-128, 127] in steps of 2/255, truncating toward zero, with values outside + * it clamped and NaN as 127, as rescore's int8 quantizer does. + */ +int diskann_quantize_vector(const f32 *src, size_t dimensions, + enum Vec0DiskannQuantizerType quantizer_type, + u8 *out) { + switch (quantizer_type) { + case VEC0_DISKANN_QUANTIZER_BINARY: + memset(out, 0, dimensions / CHAR_BIT); + for (size_t i = 0; i < dimensions; i++) { + if (src[i] > 0.0f) { + out[i / CHAR_BIT] |= (1 << (i % CHAR_BIT)); + } + } + return SQLITE_OK; + case VEC0_DISKANN_QUANTIZER_INT8: { + f32 step = 2.0f / 255.0f; + for (size_t i = 0; i < dimensions; i++) { + f32 v = (src[i] - (-1.0f)) / step - 128.0f; + if (!(v <= 127.0f)) + v = 127.0f; + if (!(v >= -128.0f)) + v = -128.0f; + ((i8 *)out)[i] = (i8)v; + } + return SQLITE_OK; + } + } + return SQLITE_ERROR; +} + +/** + * Writes the quantized form of a vector of column col's element type to out: + * a float32 vector as diskann_quantize_vector() writes it, and an int8 vector + * as itself under the int8 quantizer and under binary as a bit per dimension, + * set when the value is > 0. + */ +static void diskann_quantize(struct VectorColumnDefinition *col, + const void *src, u8 *out) { + if (col->element_type == SQLITE_VEC_ELEMENT_TYPE_FLOAT32) { + diskann_quantize_vector((const f32 *)src, col->dimensions, + col->diskann.quantizer_type, out); + return; + } + const i8 *v = (const i8 *)src; + if (col->diskann.quantizer_type == VEC0_DISKANN_QUANTIZER_INT8) { + memcpy(out, v, col->dimensions); + return; + } + memset(out, 0, col->dimensions / CHAR_BIT); + for (size_t i = 0; i < col->dimensions; i++) { + if (v[i] > 0) { + out[i / CHAR_BIT] |= (1 << (i % CHAR_BIT)); + } + } +} + +/** + * The distance between two vectors quantized for column col, by which a + * search ranks the neighbors it has not visited: hamming under binary, and + * the column's metric between the int8 vectors under int8. + */ +static f32 diskann_distance_quantized(struct VectorColumnDefinition *col, + const u8 *a, const u8 *b) { + size_t dimensions = col->dimensions; + switch (col->diskann.quantizer_type) { + case VEC0_DISKANN_QUANTIZER_BINARY: + return distance_hamming(a, b, &dimensions); + case VEC0_DISKANN_QUANTIZER_INT8: + switch (col->distance_metric) { + case VEC0_DISTANCE_METRIC_L2: + return distance_l2_sqr_int8(a, b, &dimensions); + case VEC0_DISTANCE_METRIC_COSINE: + return distance_cosine_int8(a, b, &dimensions); + case VEC0_DISTANCE_METRIC_L1: + return (f32)distance_l1_int8(a, b, &dimensions); + } + } + return FLT_MAX; +} + +/** + * Reads the medoid of column i, the node every search starts at, into + * *outMedoid, or sets *outIsEmpty when the graph has no node. + */ +static int diskann_medoid_get(vec0_vtab *p, int i, i64 *outMedoid, + int *outIsEmpty) { + sqlite3_stmt *stmt = NULL; + char *zSql = sqlite3_mprintf("SELECT value FROM " VEC0_SHADOW_INFO_NAME + " WHERE key = ?", + p->schemaName, p->tableName); + char *zKey = sqlite3_mprintf("diskann_medoid_%02d", i); + if (!zSql || !zKey) { + sqlite3_free(zSql); + sqlite3_free(zKey); + return SQLITE_NOMEM; + } + int rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); + sqlite3_free(zSql); + if (rc != SQLITE_OK) { + sqlite3_free(zKey); + return rc; + } + sqlite3_bind_text(stmt, 1, zKey, -1, sqlite3_free); + rc = sqlite3_step(stmt); + if (rc == SQLITE_ROW) { + *outIsEmpty = sqlite3_column_type(stmt, 0) == SQLITE_NULL; + *outMedoid = sqlite3_column_int64(stmt, 0); + rc = SQLITE_OK; + } else if (rc == SQLITE_DONE) { + vtab_set_error(&p->base, + "the _info table has no DiskANN medoid for vector column %d", + i); + rc = SQLITE_ERROR; + } else { + rc = vec0_error_rc(rc); + } + sqlite3_finalize(stmt); + return rc; +} + +/** + * Sets the medoid of column i, or marks the graph empty when isEmpty is set. + */ +static int diskann_medoid_set(vec0_vtab *p, int i, i64 medoid, int isEmpty) { + sqlite3_stmt *stmt = NULL; + char *zSql = sqlite3_mprintf("UPDATE " VEC0_SHADOW_INFO_NAME + " SET value = ? WHERE key = ?", + p->schemaName, p->tableName); + char *zKey = sqlite3_mprintf("diskann_medoid_%02d", i); + if (!zSql || !zKey) { + sqlite3_free(zSql); + sqlite3_free(zKey); + return SQLITE_NOMEM; + } + int rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); + sqlite3_free(zSql); + if (rc != SQLITE_OK) { + sqlite3_free(zKey); + return rc; + } + if (isEmpty) { + sqlite3_bind_null(stmt, 1); + } else { + sqlite3_bind_int64(stmt, 1, medoid); + } + sqlite3_bind_text(stmt, 2, zKey, -1, sqlite3_free); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + return rc == SQLITE_DONE ? SQLITE_OK : vec0_error_rc(rc); +} + +/** + * Reads node rowid of column i: its validity bitmap, its neighbors' rowids + * and their quantized vectors, each to be freed with sqlite3_free(). Returns + * SQLITE_EMPTY when the graph has no such node, and SQLITE_ERROR when a blob + * is not the size the column's options give it. + */ +static int diskann_node_read(vec0_vtab *p, int i, i64 rowid, u8 **outValidity, + int *outValiditySize, u8 **outNeighborIds, + int *outNeighborIdsSize, u8 **outQvecs, + int *outQvecsSize) { + struct VectorColumnDefinition *col = &p->vector_columns[i]; + struct Vec0DiskannConfig *cfg = &col->diskann; + int rc = vec0_cached_stmt(p, &p->stmtDiskannNodeRead[i], + "SELECT neighbors_validity, neighbor_ids, " + "neighbor_quantized_vectors FROM \"%w\".\"%w\" " + "WHERE rowid = ?", + p->shadowDiskannNodesNames[i]); + if (rc != SQLITE_OK) { + return rc; + } + sqlite3_stmt *stmt = p->stmtDiskannNodeRead[i]; + sqlite3_bind_int64(stmt, 1, rowid); + rc = sqlite3_step(stmt); + if (rc != SQLITE_ROW) { + rc = rc == SQLITE_DONE ? SQLITE_EMPTY : vec0_error_rc(rc); + goto done; + } + + const void *blobs[3]; + int sizes[3]; + int expected[3] = { + diskann_validity_byte_size(cfg->n_neighbors), + (int)diskann_neighbor_ids_byte_size(cfg->n_neighbors), + (int)diskann_neighbor_qvecs_byte_size( + cfg->n_neighbors, cfg->quantizer_type, col->dimensions), + }; + for (int j = 0; j < 3; j++) { + blobs[j] = sqlite3_column_blob(stmt, j); + sizes[j] = sqlite3_column_bytes(stmt, j); + if (!blobs[j] || sizes[j] != expected[j]) { + vtab_set_error(&p->base, + "node %lld of the DiskANN graph of vector column %d is " + "corrupt", + rowid, i); + rc = SQLITE_ERROR; + goto done; + } + } + u8 *copies[3]; + for (int j = 0; j < 3; j++) { + copies[j] = sqlite3_malloc(sizes[j]); + if (!copies[j]) { + for (int c = 0; c < j; c++) { + sqlite3_free(copies[c]); + } + rc = SQLITE_NOMEM; + goto done; + } + memcpy(copies[j], blobs[j], sizes[j]); + } + *outValidity = copies[0]; + *outValiditySize = sizes[0]; + *outNeighborIds = copies[1]; + *outNeighborIdsSize = sizes[1]; + *outQvecs = copies[2]; + *outQvecsSize = sizes[2]; + rc = SQLITE_OK; + +done: + sqlite3_reset(stmt); + sqlite3_clear_bindings(stmt); + return rc; +} + +/** + * Writes node rowid of column i, replacing the row it had. + */ +static int diskann_node_write(vec0_vtab *p, int i, i64 rowid, + const u8 *validity, int validitySize, + const u8 *neighborIds, int neighborIdsSize, + const u8 *qvecs, int qvecsSize) { + int rc = vec0_cached_stmt(p, &p->stmtDiskannNodeWrite[i], + "INSERT OR REPLACE INTO \"%w\".\"%w\" (rowid, " + "neighbors_validity, neighbor_ids, " + "neighbor_quantized_vectors) VALUES (?, ?, ?, ?)", + p->shadowDiskannNodesNames[i]); + if (rc != SQLITE_OK) { + return rc; + } + sqlite3_stmt *stmt = p->stmtDiskannNodeWrite[i]; + sqlite3_bind_int64(stmt, 1, rowid); + sqlite3_bind_blob(stmt, 2, validity, validitySize, SQLITE_STATIC); + sqlite3_bind_blob(stmt, 3, neighborIds, neighborIdsSize, SQLITE_STATIC); + sqlite3_bind_blob(stmt, 4, qvecs, qvecsSize, SQLITE_STATIC); + rc = sqlite3_step(stmt); + sqlite3_reset(stmt); + sqlite3_clear_bindings(stmt); + return rc == SQLITE_DONE ? SQLITE_OK : vec0_error_rc(rc); +} + +/** + * Reads the vector of row rowid in column i into a buffer to be freed with + * sqlite3_free(), and its size into *outVectorSize unless that is NULL. + * Returns SQLITE_EMPTY when the column has no vector with that rowid. + */ +static int diskann_vector_read(vec0_vtab *p, int i, i64 rowid, void **outVector, + int *outVectorSize) { + int rc = vec0_cached_stmt(p, &p->stmtDiskannVectorRead[i], + "SELECT vector FROM \"%w\".\"%w\" WHERE rowid = ?", + p->shadowDiskannVectorsNames[i]); + if (rc != SQLITE_OK) { + return rc; + } + sqlite3_stmt *stmt = p->stmtDiskannVectorRead[i]; + sqlite3_bind_int64(stmt, 1, rowid); + rc = sqlite3_step(stmt); + if (rc != SQLITE_ROW) { + rc = rc == SQLITE_DONE ? SQLITE_EMPTY : vec0_error_rc(rc); + goto done; + } + const void *blob = sqlite3_column_blob(stmt, 0); + int size = sqlite3_column_bytes(stmt, 0); + if (!blob || size != (int)vector_column_byte_size(p->vector_columns[i])) { + vtab_set_error(&p->base, + "the DiskANN vector of row %lld in vector column %d is " + "corrupt", + rowid, i); + rc = SQLITE_ERROR; + goto done; + } + void *vector = sqlite3_malloc(size); + if (!vector) { + rc = SQLITE_NOMEM; + goto done; + } + memcpy(vector, blob, size); + *outVector = vector; + if (outVectorSize) { + *outVectorSize = size; + } + rc = SQLITE_OK; + +done: + sqlite3_reset(stmt); + sqlite3_clear_bindings(stmt); + return rc; +} + +/** + * Writes the vector of row rowid in column i, replacing the row it had. + */ +static int diskann_vector_write(vec0_vtab *p, int i, i64 rowid, + const void *vector) { + int rc = + vec0_cached_stmt(p, &p->stmtDiskannVectorWrite[i], + "INSERT OR REPLACE INTO \"%w\".\"%w\" (rowid, vector) " + "VALUES (?, ?)", + p->shadowDiskannVectorsNames[i]); + if (rc != SQLITE_OK) { + return rc; + } + sqlite3_stmt *stmt = p->stmtDiskannVectorWrite[i]; + sqlite3_bind_int64(stmt, 1, rowid); + sqlite3_bind_blob(stmt, 2, vector, + (int)vector_column_byte_size(p->vector_columns[i]), + SQLITE_STATIC); + rc = sqlite3_step(stmt); + sqlite3_reset(stmt); + sqlite3_clear_bindings(stmt); + return rc == SQLITE_DONE ? SQLITE_OK : vec0_error_rc(rc); +} + +struct Vec0DiskannCandidate { + i64 rowid; + f32 distance; + // set once the search has read the candidate's node + int visited; + // set once its exact distance replaced its quantized one + int confirmed; +}; + +/** + * The candidates of a search, nearest first, at most capacity of them. + */ +struct DiskannCandidateList { + struct Vec0DiskannCandidate *items; + int count; + int capacity; +}; + +int diskann_candidate_list_init(struct DiskannCandidateList *list, + int capacity) { + list->items = sqlite3_malloc64((sqlite3_uint64)capacity * + sizeof(struct Vec0DiskannCandidate)); + if (!list->items) { + return SQLITE_NOMEM; + } + list->count = 0; + list->capacity = capacity; + return SQLITE_OK; +} + +void diskann_candidate_list_free(struct DiskannCandidateList *list) { + sqlite3_free(list->items); + list->items = NULL; + list->count = 0; + list->capacity = 0; +} + +/** + * Moves the candidate at index i to its place by distance, and returns that + * place. + */ +static int diskann_candidate_list_sift(struct DiskannCandidateList *list, + int i) { + struct Vec0DiskannCandidate item = list->items[i]; + while (i > 0 && list->items[i - 1].distance > item.distance) { + list->items[i] = list->items[i - 1]; + i--; + } + while (i + 1 < list->count && list->items[i + 1].distance < item.distance) { + list->items[i] = list->items[i + 1]; + i++; + } + list->items[i] = item; + return i; +} + +/** + * Adds candidate rowid at distance, unvisited. A rowid the list holds keeps + * its place unless distance is nearer, and a list at capacity drops its + * farthest candidate for a nearer one and ignores a farther one. Returns 1 + * when the list holds the rowid afterward. + */ +int diskann_candidate_list_insert(struct DiskannCandidateList *list, i64 rowid, + f32 distance) { + for (int i = 0; i < list->count; i++) { + if (list->items[i].rowid == rowid) { + if (distance < list->items[i].distance) { + list->items[i].distance = distance; + diskann_candidate_list_sift(list, i); + } + return 1; + } + } + if (list->count >= list->capacity) { + if (list->count == 0 || distance >= list->items[list->count - 1].distance) { + return 0; + } + list->count--; + } + int lo = 0, hi = list->count; + while (lo < hi) { + int mid = (lo + hi) / 2; + if (list->items[mid].distance < distance) { + lo = mid + 1; + } else { + hi = mid; + } + } + memmove(&list->items[lo + 1], &list->items[lo], + (list->count - lo) * sizeof(struct Vec0DiskannCandidate)); + list->items[lo].rowid = rowid; + list->items[lo].distance = distance; + list->items[lo].visited = 0; + list->items[lo].confirmed = 0; + list->count++; + return 1; +} + +/** + * Gives the candidate at index i its exact distance, nearer or farther than + * the quantized one it was ranked by, and returns its new index. + */ +int diskann_candidate_list_rerank(struct DiskannCandidateList *list, int i, + f32 distance) { + list->items[i].distance = distance; + return diskann_candidate_list_sift(list, i); +} + +/** + * Returns the index of the nearest candidate not yet visited, or -1. + */ +int diskann_candidate_list_next_unvisited( + const struct DiskannCandidateList *list) { + for (int i = 0; i < list->count; i++) { + if (!list->items[i].visited) { + return i; + } + } + return -1; +} + +/** + * The rowids a search has visited: a hash set by open addressing that doubles + * once half full, with rowid 0, its empty slot, held apart. + */ +struct DiskannVisitedSet { + i64 *slots; + int capacity; + int count; + int hasZero; +}; + +int diskann_visited_set_init(struct DiskannVisitedSet *set, int capacity) { + int cap = 16; + while (cap < capacity && cap < (1 << 30)) { + cap *= 2; + } + set->slots = sqlite3_malloc64((sqlite3_uint64)cap * sizeof(i64)); + if (!set->slots) { + return SQLITE_NOMEM; + } + memset(set->slots, 0, (size_t)cap * sizeof(i64)); + set->capacity = cap; + set->count = 0; + set->hasZero = 0; + return SQLITE_OK; +} + +void diskann_visited_set_free(struct DiskannVisitedSet *set) { + sqlite3_free(set->slots); + set->slots = NULL; + set->capacity = 0; + set->count = 0; + set->hasZero = 0; +} + +static int diskann_visited_set_slot(i64 rowid, int capacity) { + return (int)(((u64)rowid * 0x9E3779B97F4A7C15ULL) >> 32) & (capacity - 1); +} + +int diskann_visited_set_contains(const struct DiskannVisitedSet *set, + i64 rowid) { + if (rowid == 0) { + return set->hasZero; + } + int mask = set->capacity - 1; + for (int slot = diskann_visited_set_slot(rowid, set->capacity);; + slot = (slot + 1) & mask) { + if (set->slots[slot] == 0) { + return 0; + } + if (set->slots[slot] == rowid) { + return 1; + } + } +} + +/** + * Adds rowid. Sets *outAdded to 1 when the set did not hold it. + */ +int diskann_visited_set_insert(struct DiskannVisitedSet *set, i64 rowid, + int *outAdded) { + *outAdded = 0; + if (rowid == 0) { + *outAdded = !set->hasZero; + set->hasZero = 1; + return SQLITE_OK; + } + if ((set->count + 1) * 2 > set->capacity) { + struct DiskannVisitedSet grown; + int rc = diskann_visited_set_init(&grown, set->capacity * 2); + if (rc != SQLITE_OK) { + return rc; + } + for (int i = 0; i < set->capacity; i++) { + if (set->slots[i] != 0) { + int added; + diskann_visited_set_insert(&grown, set->slots[i], &added); + } + } + grown.hasZero = set->hasZero; + sqlite3_free(set->slots); + *set = grown; + } + int mask = set->capacity - 1; + for (int slot = diskann_visited_set_slot(rowid, set->capacity);; + slot = (slot + 1) & mask) { + if (set->slots[slot] == rowid) { + return SQLITE_OK; + } + if (set->slots[slot] == 0) { + set->slots[slot] = rowid; + set->count++; + *outAdded = 1; + return SQLITE_OK; + } + } +} + +/** + * Finds the rows of column i nearest queryVector, a vector of the column's + * element type, by LM-Search in the LM-DiskANN paper: a greedy walk of the + * graph from the medoid that keeps the nearest searchListSize candidates (at + * least k), visits the nearest not yet visited until it has visited them all, + * and ranks a candidate by its quantized distance until it is visited and by + * its exact distance after. A candidate whose vector is gone, a deleted row + * an edge still names, is left out. Writes up to k of the nearest, nearest + * first, to outRowids and outDistances, and their number to *outCount. + */ +static int diskann_search(vec0_vtab *p, int i, const void *queryVector, int k, + int searchListSize, i64 *outRowids, f32 *outDistances, + int *outCount) { + struct VectorColumnDefinition *col = &p->vector_columns[i]; + struct Vec0DiskannConfig *cfg = &col->diskann; + struct DiskannCandidateList candidates = {0}; + struct DiskannVisitedSet visited = {0}; + u8 *queryQuantized = NULL; + void *medoidVector = NULL; + int rc; + + *outCount = 0; + if (searchListSize < k) { + searchListSize = k; + } + i64 medoid; + int isEmpty; + rc = diskann_medoid_get(p, i, &medoid, &isEmpty); + if (rc != SQLITE_OK || isEmpty) { + return rc; + } + + // the medoid starts the walk ranked by its exact distance + rc = diskann_vector_read(p, i, medoid, &medoidVector, NULL); + if (rc != SQLITE_OK) { + if (rc == SQLITE_EMPTY) { + vtab_set_error(&p->base, + "the DiskANN medoid %lld of vector column %d has no " + "vector", + medoid, i); + rc = SQLITE_ERROR; + } + return rc; + } + f32 medoidDistance = vec0_compute_distance(col, medoidVector, queryVector); + sqlite3_free(medoidVector); + + queryQuantized = sqlite3_malloc((int)diskann_quantized_vector_byte_size( + cfg->quantizer_type, col->dimensions)); + if (!queryQuantized) { + return SQLITE_NOMEM; + } + diskann_quantize(col, queryVector, queryQuantized); + rc = diskann_candidate_list_init(&candidates, searchListSize); + if (rc != SQLITE_OK) { + goto cleanup; + } + rc = diskann_visited_set_init(&visited, searchListSize); + if (rc != SQLITE_OK) { + goto cleanup; + } + diskann_candidate_list_insert(&candidates, medoid, medoidDistance); + candidates.items[0].confirmed = 1; + + for (int next; + (next = diskann_candidate_list_next_unvisited(&candidates)) >= 0;) { + candidates.items[next].visited = 1; + i64 rowid = candidates.items[next].rowid; + int added; + rc = diskann_visited_set_insert(&visited, rowid, &added); + if (rc != SQLITE_OK) { + goto cleanup; + } + + // paper line 13: rank the visited candidate by its exact distance + if (!candidates.items[next].confirmed) { + void *vector = NULL; + rc = diskann_vector_read(p, i, rowid, &vector, NULL); + if (rc == SQLITE_OK) { + f32 distance = vec0_compute_distance(col, vector, queryVector); + sqlite3_free(vector); + next = diskann_candidate_list_rerank(&candidates, next, distance); + candidates.items[next].confirmed = 1; + } else if (rc != SQLITE_EMPTY) { + goto cleanup; + } + } + + u8 *validity = NULL, *neighborIds = NULL, *qvecs = NULL; + int validitySize, neighborIdsSize, qvecsSize; + rc = diskann_node_read(p, i, rowid, &validity, &validitySize, &neighborIds, + &neighborIdsSize, &qvecs, &qvecsSize); + if (rc == SQLITE_EMPTY) { + continue; + } + if (rc != SQLITE_OK) { + goto cleanup; + } + for (int j = 0; j < cfg->n_neighbors; j++) { + if (!diskann_validity_get(validity, j)) { + continue; + } + i64 neighbor = diskann_neighbor_id_get(neighborIds, j); + if (diskann_visited_set_contains(&visited, neighbor)) { + continue; + } + f32 distance = diskann_distance_quantized( + col, queryQuantized, + diskann_neighbor_qvec_get(qvecs, j, cfg->quantizer_type, + col->dimensions)); + diskann_candidate_list_insert(&candidates, neighbor, distance); + } + sqlite3_free(validity); + sqlite3_free(neighborIds); + sqlite3_free(qvecs); + } + + for (int j = 0; j < candidates.count && *outCount < k; j++) { + if (candidates.items[j].confirmed) { + outRowids[*outCount] = candidates.items[j].rowid; + outDistances[*outCount] = candidates.items[j].distance; + (*outCount)++; + } + } + rc = SQLITE_OK; + +cleanup: + sqlite3_free(queryQuantized); + diskann_candidate_list_free(&candidates); + diskann_visited_set_free(&visited); + return rc; +} + +/** + * RobustPrune's selection, Algorithm 4 in the LM-DiskANN paper: of + * num_candidates candidates in order of p_distances, their distances to the + * node, keeps the nearest left, drops every candidate it covers, one whose + * distance to it times alpha is at most the candidate's distance to the node, + * and repeats until max_neighbors are kept or none is left. distance(ctx, a, + * b) is the distance between candidates a and b. Sets outSelected[i] to 1 for + * each kept candidate and 0 for the others, and *outCount to the number kept. + */ +int diskann_prune_select(const f32 *p_distances, int num_candidates, f32 alpha, + int max_neighbors, + f32 (*distance)(void *ctx, int a, int b), void *ctx, + int *outSelected, int *outCount) { + *outCount = 0; + if (num_candidates == 0) { + return SQLITE_OK; + } + u8 *active = sqlite3_malloc(num_candidates); + if (!active) { + return SQLITE_NOMEM; + } + memset(active, 1, num_candidates); + memset(outSelected, 0, num_candidates * sizeof(int)); + for (int best = 0; best < num_candidates && *outCount < max_neighbors; + best++) { + if (!active[best]) { + continue; + } + outSelected[best] = 1; + (*outCount)++; + active[best] = 0; + for (int j = best + 1; j < num_candidates; j++) { + if (active[j] && alpha * distance(ctx, best, j) <= p_distances[j]) { + active[j] = 0; + } + } + } + sqlite3_free(active); + return SQLITE_OK; +} + +struct DiskannPruneVectors { + struct VectorColumnDefinition *col; + void **vectors; +}; + +static f32 diskann_prune_distance(void *ctx, int a, int b) { + struct DiskannPruneVectors *v = ctx; + return vec0_compute_distance(v->col, v->vectors[a], v->vectors[b]); +} + +/** + * Chooses the neighbors of node p_rowid of column i from candidates, rows of + * the column at candidate_distances from it, by RobustPrune with the column's + * alpha, up to its n_neighbors, and writes their rowids, nearest first, to + * outNeighborRowids. Reorders candidates and candidate_distances. + */ +static int diskann_robust_prune(vec0_vtab *p, int i, i64 p_rowid, + i64 *candidates, f32 *candidate_distances, + int num_candidates, i64 *outNeighborRowids, + int *outNeighborCount) { + struct VectorColumnDefinition *col = &p->vector_columns[i]; + int rc = SQLITE_OK; + *outNeighborCount = 0; + + for (int j = 0; j < num_candidates; j++) { + if (candidates[j] == p_rowid) { + candidates[j] = candidates[num_candidates - 1]; + candidate_distances[j] = candidate_distances[num_candidates - 1]; + num_candidates--; + break; + } + } + if (num_candidates == 0) { + return SQLITE_OK; + } + for (int j = 1; j < num_candidates; j++) { + f32 d = candidate_distances[j]; + i64 r = candidates[j]; + int m = j - 1; + while (m >= 0 && candidate_distances[m] > d) { + candidate_distances[m + 1] = candidate_distances[m]; + candidates[m + 1] = candidates[m]; + m--; + } + candidate_distances[m + 1] = d; + candidates[m + 1] = r; + } + + void **vectors = sqlite3_malloc(num_candidates * sizeof(void *)); + int *selected = sqlite3_malloc(num_candidates * sizeof(int)); + if (!vectors || !selected) { + sqlite3_free(vectors); + sqlite3_free(selected); + return SQLITE_NOMEM; + } + memset(vectors, 0, num_candidates * sizeof(void *)); + // a candidate whose vector is gone is no candidate + int kept = 0; + for (int j = 0; j < num_candidates; j++) { + rc = diskann_vector_read(p, i, candidates[j], &vectors[kept], NULL); + if (rc == SQLITE_EMPTY) { + continue; + } + if (rc != SQLITE_OK) { + goto cleanup; + } + candidates[kept] = candidates[j]; + candidate_distances[kept] = candidate_distances[j]; + kept++; + } + struct DiskannPruneVectors ctx = {col, vectors}; + int count; + rc = diskann_prune_select(candidate_distances, kept, col->diskann.alpha, + col->diskann.n_neighbors, diskann_prune_distance, + &ctx, selected, &count); + if (rc != SQLITE_OK) { + goto cleanup; + } + for (int j = 0; j < kept; j++) { + if (selected[j]) { + outNeighborRowids[(*outNeighborCount)++] = candidates[j]; + } + } + +cleanup: + for (int j = 0; j < num_candidates; j++) { + sqlite3_free(vectors[j]); + } + sqlite3_free(vectors); + sqlite3_free(selected); + return rc; +} + +/** + * Writes node nodeRowid of column i with the neighbors neighborRowids, each + * with its vector quantized. + */ +static int diskann_write_pruned_neighbors(vec0_vtab *p, int i, i64 nodeRowid, + const i64 *neighborRowids, + int neighborCount) { + struct VectorColumnDefinition *col = &p->vector_columns[i]; + struct Vec0DiskannConfig *cfg = &col->diskann; + u8 *validity, *neighborIds, *qvecs; + int validitySize, neighborIdsSize, qvecsSize; + int rc = diskann_node_init( + cfg->n_neighbors, cfg->quantizer_type, col->dimensions, &validity, + &validitySize, &neighborIds, &neighborIdsSize, &qvecs, &qvecsSize); + if (rc != SQLITE_OK) { + return rc; + } + u8 *qvec = sqlite3_malloc((int)diskann_quantized_vector_byte_size( + cfg->quantizer_type, col->dimensions)); + if (!qvec) { + rc = SQLITE_NOMEM; + goto cleanup; + } + for (int j = 0; j < neighborCount && j < cfg->n_neighbors; j++) { + void *vector = NULL; + rc = diskann_vector_read(p, i, neighborRowids[j], &vector, NULL); + if (rc == SQLITE_EMPTY) { + continue; + } + if (rc != SQLITE_OK) { + goto cleanup; + } + diskann_quantize(col, vector, qvec); + sqlite3_free(vector); + diskann_node_set_neighbor(validity, neighborIds, qvecs, j, + neighborRowids[j], qvec, cfg->quantizer_type, + col->dimensions); + } + rc = diskann_node_write(p, i, nodeRowid, validity, validitySize, neighborIds, + neighborIdsSize, qvecs, qvecsSize); + +cleanup: + sqlite3_free(qvec); + sqlite3_free(validity); + sqlite3_free(neighborIds); + sqlite3_free(qvecs); + return rc; +} + +/** + * Makes target_rowid, whose vector is target_vector, a neighbor of node + * node_rowid of column i. A node with every slot taken keeps the nearer of + * the target and its farthest neighbor by quantized distance, which reads no + * neighbor's vector. + */ +static int diskann_add_reverse_edge(vec0_vtab *p, int i, i64 node_rowid, + i64 target_rowid, + const void *target_vector) { + struct VectorColumnDefinition *col = &p->vector_columns[i]; + struct Vec0DiskannConfig *cfg = &col->diskann; + u8 *validity = NULL, *neighborIds = NULL, *qvecs = NULL; + int validitySize, neighborIdsSize, qvecsSize; + u8 *targetQ = NULL, *nodeQ = NULL; + void *nodeVector = NULL; + int rc = + diskann_node_read(p, i, node_rowid, &validity, &validitySize, + &neighborIds, &neighborIdsSize, &qvecs, &qvecsSize); + if (rc == SQLITE_EMPTY) { + vtab_set_error(&p->base, + "node %lld of the DiskANN graph of vector column %d is " + "missing", + node_rowid, i); + return SQLITE_ERROR; + } + if (rc != SQLITE_OK) { + return rc; + } + for (int j = 0; j < cfg->n_neighbors; j++) { + if (diskann_validity_get(validity, j) && + diskann_neighbor_id_get(neighborIds, j) == target_rowid) { + goto cleanup; + } + } + + size_t qsize = + diskann_quantized_vector_byte_size(cfg->quantizer_type, col->dimensions); + targetQ = sqlite3_malloc((int)qsize); + nodeQ = sqlite3_malloc((int)qsize); + if (!targetQ || !nodeQ) { + rc = SQLITE_NOMEM; + goto cleanup; + } + diskann_quantize(col, target_vector, targetQ); + + int slot = -1; + for (int j = 0; j < cfg->n_neighbors; j++) { + if (!diskann_validity_get(validity, j)) { + slot = j; + break; + } + } + if (slot < 0) { + rc = diskann_vector_read(p, i, node_rowid, &nodeVector, NULL); + if (rc != SQLITE_OK) { + if (rc == SQLITE_EMPTY) { + vtab_set_error(&p->base, + "node %lld of the DiskANN graph of vector column %d " + "has no vector", + node_rowid, i); + rc = SQLITE_ERROR; + } + goto cleanup; + } + diskann_quantize(col, nodeVector, nodeQ); + f32 targetDistance = diskann_distance_quantized(col, nodeQ, targetQ); + f32 worstDistance = -1.0f; + for (int j = 0; j < cfg->n_neighbors; j++) { + f32 d = diskann_distance_quantized( + col, nodeQ, + diskann_neighbor_qvec_get(qvecs, j, cfg->quantizer_type, + col->dimensions)); + if (d > worstDistance) { + worstDistance = d; + slot = j; + } + } + if (!(targetDistance < worstDistance)) { + goto cleanup; + } + } + diskann_node_set_neighbor(validity, neighborIds, qvecs, slot, target_rowid, + targetQ, cfg->quantizer_type, col->dimensions); + rc = diskann_node_write(p, i, node_rowid, validity, validitySize, neighborIds, + neighborIdsSize, qvecs, qvecsSize); + +cleanup: + sqlite3_free(targetQ); + sqlite3_free(nodeQ); + sqlite3_free(nodeVector); + sqlite3_free(validity); + sqlite3_free(neighborIds); + sqlite3_free(qvecs); + return rc; +} + +/** + * Links row rowid of column i, whose vector is in _vectorsNN, into the graph + * by LM-Insert, Algorithm 2 in the LM-DiskANN paper: searches the graph for + * its nearest rows, chooses its neighbors among them by RobustPrune, and adds + * it to theirs. The first row of an empty graph becomes its medoid. + */ +static int diskann_insert_graph(vec0_vtab *p, int i, i64 rowid, + const void *vector) { + struct VectorColumnDefinition *col = &p->vector_columns[i]; + struct Vec0DiskannConfig *cfg = &col->diskann; + i64 medoid; + int isEmpty; + int rc = diskann_medoid_get(p, i, &medoid, &isEmpty); + if (rc != SQLITE_OK) { + return rc; + } + if (isEmpty) { + rc = diskann_write_pruned_neighbors(p, i, rowid, NULL, 0); + if (rc != SQLITE_OK) { + return rc; + } + return diskann_medoid_set(p, i, rowid, 0); + } + + int L = cfg->search_list_size_insert > 0 ? cfg->search_list_size_insert + : cfg->search_list_size; + i64 *searchRowids = sqlite3_malloc64((sqlite3_uint64)L * sizeof(i64)); + f32 *searchDistances = sqlite3_malloc64((sqlite3_uint64)L * sizeof(f32)); + i64 *neighbors = sqlite3_malloc(cfg->n_neighbors * sizeof(i64)); + if (!searchRowids || !searchDistances || !neighbors) { + rc = SQLITE_NOMEM; + goto cleanup; + } + int searchCount; + rc = diskann_search(p, i, vector, L, L, searchRowids, searchDistances, + &searchCount); + if (rc != SQLITE_OK) { + goto cleanup; + } + int neighborCount; + rc = diskann_robust_prune(p, i, rowid, searchRowids, searchDistances, + searchCount, neighbors, &neighborCount); + if (rc != SQLITE_OK) { + goto cleanup; + } + rc = diskann_write_pruned_neighbors(p, i, rowid, neighbors, neighborCount); + for (int j = 0; rc == SQLITE_OK && j < neighborCount; j++) { + rc = diskann_add_reverse_edge(p, i, neighbors[j], rowid, vector); + } + +cleanup: + sqlite3_free(searchRowids); + sqlite3_free(searchDistances); + sqlite3_free(neighbors); + return rc; +} + +/** + * Links the vectors waiting in _diskann_bufferNN of column i into the graph, + * in rowid order, and empties the buffer. + */ +static int diskann_flush_buffer(vec0_vtab *p, int i) { + sqlite3_stmt *stmt = NULL; + char *zSql = sqlite3_mprintf("SELECT rowid, vector FROM \"%w\".\"%w\" " + "ORDER BY rowid", + p->schemaName, p->shadowDiskannBufferNames[i]); + if (!zSql) { + return SQLITE_NOMEM; + } + int rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); + sqlite3_free(zSql); + if (rc != SQLITE_OK) { + return rc; + } + size_t size = vector_column_byte_size(p->vector_columns[i]); + int stepRc = SQLITE_DONE; + while (rc == SQLITE_OK && (stepRc = sqlite3_step(stmt)) == SQLITE_ROW) { + const void *vector = sqlite3_column_blob(stmt, 1); + if (!vector || sqlite3_column_bytes(stmt, 1) != (int)size) { + vtab_set_error(&p->base, + "the DiskANN buffer of vector column %d is corrupt", i); + rc = SQLITE_ERROR; + } else { + rc = diskann_insert_graph(p, i, sqlite3_column_int64(stmt, 0), vector); + } + } + sqlite3_finalize(stmt); + if (rc != SQLITE_OK) { + return rc; + } + if (stepRc != SQLITE_DONE) { + return vec0_error_rc(stepRc); + } + + zSql = sqlite3_mprintf("DELETE FROM \"%w\".\"%w\"", p->schemaName, + p->shadowDiskannBufferNames[i]); + if (!zSql) { + return SQLITE_NOMEM; + } + rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); + sqlite3_free(zSql); + if (rc != SQLITE_OK) { + return rc; + } + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + return rc == SQLITE_DONE ? SQLITE_OK : vec0_error_rc(rc); +} + +/** + * Adds row rowid with vector to column i: writes the vector to _vectorsNN, + * then links it into the graph, or with buffer_threshold set writes it to + * _diskann_bufferNN and links the buffer once it holds buffer_threshold + * vectors. + */ +static int diskann_insert(vec0_vtab *p, int i, i64 rowid, const void *vector) { + struct Vec0DiskannConfig *cfg = &p->vector_columns[i].diskann; + int rc = diskann_vector_write(p, i, rowid, vector); + if (rc != SQLITE_OK) { + return rc; + } + if (cfg->buffer_threshold == 0) { + return diskann_insert_graph(p, i, rowid, vector); + } + + sqlite3_stmt *stmt = NULL; + char *zSql = sqlite3_mprintf("INSERT INTO \"%w\".\"%w\" (rowid, vector) " + "VALUES (?, ?)", + p->schemaName, p->shadowDiskannBufferNames[i]); + if (!zSql) { + return SQLITE_NOMEM; + } + rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); + sqlite3_free(zSql); + if (rc != SQLITE_OK) { + return rc; + } + sqlite3_bind_int64(stmt, 1, rowid); + sqlite3_bind_blob(stmt, 2, vector, + (int)vector_column_byte_size(p->vector_columns[i]), + SQLITE_STATIC); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + if (rc != SQLITE_DONE) { + return vec0_error_rc(rc); + } + + zSql = sqlite3_mprintf("SELECT count(*) FROM \"%w\".\"%w\"", p->schemaName, + p->shadowDiskannBufferNames[i]); + if (!zSql) { + return SQLITE_NOMEM; + } + rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); + sqlite3_free(zSql); + if (rc != SQLITE_OK) { + return rc; + } + rc = sqlite3_step(stmt); + if (rc != SQLITE_ROW) { + sqlite3_finalize(stmt); + return vec0_error_rc(rc); + } + i64 buffered = sqlite3_column_int64(stmt, 0); + sqlite3_finalize(stmt); + return buffered >= cfg->buffer_threshold ? diskann_flush_buffer(p, i) + : SQLITE_OK; +} + +/** + * Repairs the graph of column i after node deleted_rowid goes, as LM-Delete, + * Algorithm 3 in the LM-DiskANN paper, does for each of its neighbors: drops + * the deleted node from the neighbor's slots and gives the freed slot the + * first of the deleted node's other neighbors the neighbor lacks. + */ +static int diskann_repair_reverse_edges(vec0_vtab *p, int i, i64 deleted_rowid, + const i64 *deleted_neighbors, + int deleted_neighbor_count) { + struct VectorColumnDefinition *col = &p->vector_columns[i]; + struct Vec0DiskannConfig *cfg = &col->diskann; + u8 *qvec = sqlite3_malloc((int)diskann_quantized_vector_byte_size( + cfg->quantizer_type, col->dimensions)); + if (!qvec) { + return SQLITE_NOMEM; + } + int rc = SQLITE_OK; + for (int dn = 0; rc == SQLITE_OK && dn < deleted_neighbor_count; dn++) { + i64 node = deleted_neighbors[dn]; + u8 *validity = NULL, *neighborIds = NULL, *qvecs = NULL; + int vs, nis, qs; + rc = diskann_node_read(p, i, node, &validity, &vs, &neighborIds, &nis, + &qvecs, &qs); + if (rc == SQLITE_EMPTY) { + rc = SQLITE_OK; + continue; + } + if (rc != SQLITE_OK) { + break; + } + int cleared = -1; + for (int j = 0; j < cfg->n_neighbors; j++) { + if (diskann_validity_get(validity, j) && + diskann_neighbor_id_get(neighborIds, j) == deleted_rowid) { + diskann_node_clear_neighbor(validity, neighborIds, qvecs, j, + cfg->quantizer_type, col->dimensions); + cleared = j; + break; + } + } + if (cleared >= 0) { + for (int di = 0; di < deleted_neighbor_count; di++) { + i64 candidate = deleted_neighbors[di]; + if (candidate == node || candidate == deleted_rowid) { + continue; + } + int already = 0; + for (int j = 0; j < cfg->n_neighbors; j++) { + if (diskann_validity_get(validity, j) && + diskann_neighbor_id_get(neighborIds, j) == candidate) { + already = 1; + break; + } + } + if (already) { + continue; + } + void *vector = NULL; + rc = diskann_vector_read(p, i, candidate, &vector, NULL); + if (rc == SQLITE_EMPTY) { + rc = SQLITE_OK; + continue; + } + if (rc != SQLITE_OK) { + break; + } + diskann_quantize(col, vector, qvec); + sqlite3_free(vector); + diskann_node_set_neighbor(validity, neighborIds, qvecs, cleared, + candidate, qvec, cfg->quantizer_type, + col->dimensions); + break; + } + if (rc == SQLITE_OK) { + rc = diskann_node_write(p, i, node, validity, vs, neighborIds, nis, + qvecs, qs); + } + } + sqlite3_free(validity); + sqlite3_free(neighborIds); + sqlite3_free(qvecs); + } + sqlite3_free(qvec); + return rc; +} + +/** + * Clears every slot of the graph of column i that still names deleted_rowid, + * so no node keeps its rowid or quantized vector. + */ +static int diskann_scrub_deleted_rowid(vec0_vtab *p, int i, i64 deleted_rowid) { + struct VectorColumnDefinition *col = &p->vector_columns[i]; + struct Vec0DiskannConfig *cfg = &col->diskann; + int validityBytes = diskann_validity_byte_size(cfg->n_neighbors); + int idsBytes = (int)diskann_neighbor_ids_byte_size(cfg->n_neighbors); + sqlite3_stmt *stmt = NULL; + struct Array dirty; + int rc = array_init(&dirty, sizeof(i64), 16); + if (rc != SQLITE_OK) { + return rc; + } + char *zSql = sqlite3_mprintf("SELECT rowid, neighbors_validity, " + "neighbor_ids FROM \"%w\".\"%w\"", + p->schemaName, p->shadowDiskannNodesNames[i]); + if (!zSql) { + rc = SQLITE_NOMEM; + goto cleanup; + } + rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); + sqlite3_free(zSql); + if (rc != SQLITE_OK) { + goto cleanup; + } + while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) { + const u8 *validity = sqlite3_column_blob(stmt, 1); + int vs = sqlite3_column_bytes(stmt, 1); + const u8 *ids = sqlite3_column_blob(stmt, 2); + int is = sqlite3_column_bytes(stmt, 2); + i64 node = sqlite3_column_int64(stmt, 0); + if (!validity || !ids || vs != validityBytes || is != idsBytes) { + vtab_set_error(&p->base, + "node %lld of the DiskANN graph of vector column %d is " + "corrupt", + node, i); + rc = SQLITE_ERROR; + goto cleanup; + } + for (int j = 0; j < cfg->n_neighbors; j++) { + if (diskann_validity_get(validity, j) && + diskann_neighbor_id_get(ids, j) == deleted_rowid) { + rc = array_append(&dirty, &node); + if (rc != SQLITE_OK) { + goto cleanup; + } + break; + } + } + } + if (rc != SQLITE_DONE) { + rc = vec0_error_rc(rc); + goto cleanup; + } + sqlite3_finalize(stmt); + stmt = NULL; + + rc = SQLITE_OK; + for (size_t d = 0; rc == SQLITE_OK && d < dirty.length; d++) { + i64 node = ((i64 *)dirty.z)[d]; + u8 *validity = NULL, *neighborIds = NULL, *qvecs = NULL; + int vs, nis, qs; + rc = diskann_node_read(p, i, node, &validity, &vs, &neighborIds, &nis, + &qvecs, &qs); + if (rc == SQLITE_EMPTY) { + rc = SQLITE_OK; + continue; + } + if (rc != SQLITE_OK) { + break; + } + for (int j = 0; j < cfg->n_neighbors; j++) { + if (diskann_validity_get(validity, j) && + diskann_neighbor_id_get(neighborIds, j) == deleted_rowid) { + diskann_node_clear_neighbor(validity, neighborIds, qvecs, j, + cfg->quantizer_type, col->dimensions); + } + } + rc = diskann_node_write(p, i, node, validity, vs, neighborIds, nis, qvecs, + qs); + sqlite3_free(validity); + sqlite3_free(neighborIds); + sqlite3_free(qvecs); + } + +cleanup: + sqlite3_finalize(stmt); + array_cleanup(&dirty); + return rc; +} + +/** + * Removes row rowid from column i: from the buffer when it waits there, and + * otherwise from the graph, whose neighbors of it are repaired, with a new + * medoid when it was the medoid, any node of the graph left, and from + * _vectorsNN. + */ +static int diskann_delete(vec0_vtab *p, int i, i64 rowid) { + struct VectorColumnDefinition *col = &p->vector_columns[i]; + struct Vec0DiskannConfig *cfg = &col->diskann; + int rc; + + if (cfg->buffer_threshold > 0) { + sqlite3_stmt *stmt = NULL; + char *zSql = sqlite3_mprintf("SELECT 1 FROM \"%w\".\"%w\" WHERE rowid = ?", + p->schemaName, p->shadowDiskannBufferNames[i]); + if (!zSql) { + return SQLITE_NOMEM; + } + rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); + sqlite3_free(zSql); + if (rc != SQLITE_OK) { + return rc; + } + sqlite3_bind_int64(stmt, 1, rowid); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + if (rc == SQLITE_ROW) { + rc = vec0_exec_i64(p, "DELETE FROM \"%w\".\"%w\" WHERE rowid = ?", + p->shadowDiskannBufferNames[i], rowid); + if (rc != SQLITE_OK) { + return rc; + } + return vec0_exec_i64(p, "DELETE FROM \"%w\".\"%w\" WHERE rowid = ?", + p->shadowDiskannVectorsNames[i], rowid); + } + if (rc != SQLITE_DONE) { + return vec0_error_rc(rc); + } + } + + u8 *validity = NULL, *neighborIds = NULL, *qvecs = NULL; + int vs, nis, qs; + rc = diskann_node_read(p, i, rowid, &validity, &vs, &neighborIds, &nis, + &qvecs, &qs); + if (rc == SQLITE_EMPTY) { + return SQLITE_OK; + } + if (rc != SQLITE_OK) { + return rc; + } + i64 *neighbors = sqlite3_malloc(cfg->n_neighbors * sizeof(i64)); + int neighborCount = 0; + if (!neighbors) { + sqlite3_free(validity); + sqlite3_free(neighborIds); + sqlite3_free(qvecs); + return SQLITE_NOMEM; + } + for (int j = 0; j < cfg->n_neighbors; j++) { + if (diskann_validity_get(validity, j)) { + neighbors[neighborCount++] = diskann_neighbor_id_get(neighborIds, j); + } + } + sqlite3_free(validity); + sqlite3_free(neighborIds); + sqlite3_free(qvecs); + + rc = diskann_repair_reverse_edges(p, i, rowid, neighbors, neighborCount); + sqlite3_free(neighbors); + if (rc == SQLITE_OK) { + rc = vec0_exec_i64(p, "DELETE FROM \"%w\".\"%w\" WHERE rowid = ?", + p->shadowDiskannNodesNames[i], rowid); + } + if (rc == SQLITE_OK) { + rc = vec0_exec_i64(p, "DELETE FROM \"%w\".\"%w\" WHERE rowid = ?", + p->shadowDiskannVectorsNames[i], rowid); + } + + // a new medoid comes from the graph, which a vector waiting in the buffer + // has not joined + i64 medoid = 0; + int isEmpty = 1; + if (rc == SQLITE_OK) { + rc = diskann_medoid_get(p, i, &medoid, &isEmpty); + } + if (rc == SQLITE_OK && !isEmpty && medoid == rowid) { + sqlite3_stmt *stmt = NULL; + char *zSql = sqlite3_mprintf("SELECT rowid FROM \"%w\".\"%w\" LIMIT 1", + p->schemaName, p->shadowDiskannNodesNames[i]); + if (!zSql) { + return SQLITE_NOMEM; + } + rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); + sqlite3_free(zSql); + if (rc != SQLITE_OK) { + return rc; + } + rc = sqlite3_step(stmt); + if (rc == SQLITE_ROW) { + i64 next = sqlite3_column_int64(stmt, 0); + sqlite3_finalize(stmt); + rc = diskann_medoid_set(p, i, next, 0); + } else { + sqlite3_finalize(stmt); + rc = rc == SQLITE_DONE ? diskann_medoid_set(p, i, 0, 1) + : vec0_error_rc(rc); + } + } + if (rc == SQLITE_OK) { + rc = diskann_scrub_deleted_rowid(p, i, rowid); + } + return rc; +} + +/** + * Returns 1 when every vector column of the table is indexed by diskann(), + * so the table keeps no chunks. + */ +static int vec0_all_columns_diskann(vec0_vtab *p) { + for (int i = 0; i < p->numVectorColumns; i++) { + if (p->vector_columns[i].index_type != VEC0_INDEX_TYPE_DISKANN) { + return 0; + } + } + return p->numVectorColumns > 0; +} + +/** + * The KNN query of column vectorColumnIdx, indexed by diskann(). With rowid = + * or rowid IN, the candidates are the rows they name; otherwise they are the + * rows a graph search finds (diskann_search), the larger of the search list + * size and k of them, with every vector still in _diskann_bufferNN. Returns + * the k nearest candidates within the query's distance constraints, nearest + * first, as vec0Filter_knn_chunks_iter() does. + */ +static int diskann_knn(vec0_vtab *p, + struct VectorColumnDefinition *vector_column, + int vectorColumnIdx, struct Array *arrayRowidsIn, + const char *idxStr, int argc, sqlite3_value **argv, + void *queryVector, i64 k, i64 **out_topk_rowids, + f32 **out_topk_distances, i64 *out_used) { + struct Vec0DiskannConfig *cfg = &vector_column->diskann; + sqlite3_stmt *stmt = NULL; + struct Array rowids, distances; + int rc = array_init(&rowids, sizeof(i64), 64); + if (rc != SQLITE_OK) { + return rc; + } + rc = array_init(&distances, sizeof(f32), 64); + if (rc != SQLITE_OK) { + array_cleanup(&rowids); + return rc; + } + + if (arrayRowidsIn) { + for (size_t j = 0; j < arrayRowidsIn->length; j++) { + i64 rowid = ((i64 *)arrayRowidsIn->z)[j]; + if (j > 0 && rowid == ((i64 *)arrayRowidsIn->z)[j - 1]) { + continue; + } + void *vector = NULL; + rc = diskann_vector_read(p, vectorColumnIdx, rowid, &vector, NULL); + if (rc == SQLITE_EMPTY) { + continue; + } + if (rc != SQLITE_OK) { + goto cleanup; + } + f32 distance = vec0_compute_distance(vector_column, vector, queryVector); + sqlite3_free(vector); + rc = array_append(&rowids, &rowid); + if (rc == SQLITE_OK) { + rc = array_append(&distances, &distance); + } + if (rc != SQLITE_OK) { + goto cleanup; + } + } + } else { + int L = cfg->search_list_size_search > 0 ? cfg->search_list_size_search + : cfg->search_list_size; + int n = L > k ? L : (int)k; + i64 *found = sqlite3_malloc64((sqlite3_uint64)n * sizeof(i64)); + f32 *foundDistances = sqlite3_malloc64((sqlite3_uint64)n * sizeof(f32)); + int foundCount = 0; + if (!found || !foundDistances) { + rc = SQLITE_NOMEM; + } else { + rc = diskann_search(p, vectorColumnIdx, queryVector, n, L, found, + foundDistances, &foundCount); + } + for (int j = 0; rc == SQLITE_OK && j < foundCount; j++) { + rc = array_append(&rowids, &found[j]); + if (rc == SQLITE_OK) { + rc = array_append(&distances, &foundDistances[j]); + } + } + sqlite3_free(found); + sqlite3_free(foundDistances); + if (rc != SQLITE_OK) { + goto cleanup; + } + + char *zSql = sqlite3_mprintf("SELECT rowid, vector FROM \"%w\".\"%w\"", + p->schemaName, + p->shadowDiskannBufferNames[vectorColumnIdx]); + if (!zSql) { + rc = SQLITE_NOMEM; + goto cleanup; + } + rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); + sqlite3_free(zSql); + if (rc != SQLITE_OK) { + goto cleanup; + } + size_t size = vector_column_byte_size(*vector_column); + while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) { + i64 rowid = sqlite3_column_int64(stmt, 0); + const void *vector = sqlite3_column_blob(stmt, 1); + if (!vector || sqlite3_column_bytes(stmt, 1) != (int)size) { + vtab_set_error(&p->base, + "the DiskANN buffer of vector column %d is corrupt", + vectorColumnIdx); + rc = SQLITE_ERROR; + goto cleanup; + } + f32 distance = vec0_compute_distance(vector_column, vector, queryVector); + rc = array_append(&rowids, &rowid); + if (rc == SQLITE_OK) { + rc = array_append(&distances, &distance); + } + if (rc != SQLITE_OK) { + goto cleanup; + } + } + if (rc != SQLITE_DONE) { + rc = vec0_error_rc(rc); + goto cleanup; + } + rc = SQLITE_OK; + } + + i64 used = (i64)rowids.length; + vec0_knn_keep_within_distance(idxStr, argc, argv, (i64 *)rowids.z, + (f32 *)distances.z, &used); + i64 *candRowids = (i64 *)rowids.z; + f32 *candDistances = (f32 *)distances.z; + i64 result_k = min(k, used); + // a selection of the result_k nearest, nearest first + for (i64 a = 0; a < result_k; a++) { + i64 m = a; + for (i64 c = a + 1; c < used; c++) { + if (candDistances[c] < candDistances[m]) { + m = c; + } + } + if (m != a) { + f32 td = candDistances[a]; + candDistances[a] = candDistances[m]; + candDistances[m] = td; + i64 tr = candRowids[a]; + candRowids[a] = candRowids[m]; + candRowids[m] = tr; + } + } + i64 *out_rowids = NULL; + f32 *out_distances = NULL; + if (result_k > 0) { + out_rowids = sqlite3_malloc64(result_k * sizeof(i64)); + out_distances = sqlite3_malloc64(result_k * sizeof(f32)); + if (!out_rowids || !out_distances) { + sqlite3_free(out_rowids); + sqlite3_free(out_distances); + rc = SQLITE_NOMEM; + goto cleanup; + } + memcpy(out_rowids, candRowids, result_k * sizeof(i64)); + memcpy(out_distances, candDistances, result_k * sizeof(f32)); + } + *out_topk_rowids = out_rowids; + *out_topk_distances = out_distances; + *out_used = result_k; + rc = SQLITE_OK; + +cleanup: + sqlite3_finalize(stmt); + array_cleanup(&rowids); + array_cleanup(&distances); + return rc; +} + +/** + * The commands `search_list_size=N`, `search_list_size_search=N` and + * `search_list_size_insert=N` set those options of every column indexed by + * diskann() for this connection, in place of the ones its CREATE gave. + * Returns SQLITE_EMPTY for any other command. + */ +static int diskann_handle_command(vec0_vtab *p, const char *command) { + static const char *azKey[] = {"search_list_size=", "search_list_size_search=", + "search_list_size_insert="}; + int key = -1; + for (int j = 0; j < (int)countof(azKey); j++) { + if (strncmp(command, azKey[j], strlen(azKey[j])) == 0) { + key = j; + } + } + if (key < 0) { + return SQLITE_EMPTY; + } + const char *zValue = command + strlen(azKey[key]); + char *zEnd; + errno = 0; + long value = strtol(zValue, &zEnd, 10); + if (!(*zValue >= '0' && *zValue <= '9') || *zEnd != '\0' || errno == ERANGE || + value < 1 || value > INT_MAX) { + vtab_set_error(&p->base, "%.*s must be >= 1", (int)strlen(azKey[key]) - 1, + azKey[key]); + return SQLITE_ERROR; + } + for (int i = 0; i < p->numVectorColumns; i++) { + if (p->vector_columns[i].index_type != VEC0_INDEX_TYPE_DISKANN) { + continue; + } + struct Vec0DiskannConfig *cfg = &p->vector_columns[i].diskann; + int *target[] = {&cfg->search_list_size, &cfg->search_list_size_search, + &cfg->search_list_size_insert}; + *target[key] = (int)value; + } + return SQLITE_OK; +} +#else +static int vec0_all_columns_diskann(vec0_vtab *p) { + UNUSED_PARAMETER(p); + return 0; +} +#endif + +#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE +// A vector column indexed by ivf() keeps its vectors in cells of up to 64 +// rows. Its k-means centroids are in _ivf_centroidsNN, held as the cells hold +// vectors. _ivf_cellsNN has one row per cell: the centroid whose rows it +// holds, or -1 for rows no clustering has placed, its count of rows, and in +// slots a validity bit, the rowid and the vector of each row. _ivf_rowid_mapNN +// gives each row's cell and slot. With a quantizer, the cells hold the vectors +// quantized and _ivf_vectorsNN holds each float32 vector by rowid. The +// ivf_trained_N row of _info is 1 once the column has centroids. +// https://github.com/asg017/sqlite-vec/blob/04d28bd/sqlite-vec-ivf.c +// https://github.com/asg017/sqlite-vec/blob/04d28bd/sqlite-vec-ivf-kmeans.c +// with the fixes of https://github.com/asg017/sqlite-vec/pull/313 + +/** + * The bytes of one vector in a cell of column col, and of one centroid. + */ +static int ivf_vector_size(struct VectorColumnDefinition *col) { + switch (col->ivf.quantizer) { + case VEC0_IVF_QUANTIZER_INT8: + return (int)col->dimensions; + case VEC0_IVF_QUANTIZER_BINARY: + return (int)(col->dimensions / CHAR_BIT); + case VEC0_IVF_QUANTIZER_NONE: + break; + } + return (int)(col->dimensions * sizeof(f32)); +} + +/** + * Writes the int8 form of a float32 vector: each value clamped to [-1, 1] and + * scaled to [-127, 127], truncating toward zero, with NaN as 127. + */ +void ivf_quantize_int8(const f32 *src, i8 *dst, size_t dimensions) { + for (size_t i = 0; i < dimensions; i++) { + f32 v = src[i]; + if (!(v <= 1.0f)) { + v = 1.0f; + } + if (!(v >= -1.0f)) { + v = -1.0f; + } + dst[i] = (i8)(v * 127.0f); + } +} + +/** + * Writes the binary form of a float32 vector: a bit per value, set when the + * value is > 0. dimensions is a multiple of 8. + */ +void ivf_quantize_binary(const f32 *src, u8 *dst, size_t dimensions) { + memset(dst, 0, dimensions / CHAR_BIT); + for (size_t i = 0; i < dimensions; i++) { + if (src[i] > 0.0f) { + dst[i / CHAR_BIT] |= (u8)(1 << (i % CHAR_BIT)); + } + } +} + +/** + * Writes the form of a float32 vector that the cells of column col hold. + */ +static void ivf_quantize(struct VectorColumnDefinition *col, const f32 *src, + void *dst) { + switch (col->ivf.quantizer) { + case VEC0_IVF_QUANTIZER_INT8: + ivf_quantize_int8(src, (i8 *)dst, col->dimensions); + return; + case VEC0_IVF_QUANTIZER_BINARY: + ivf_quantize_binary(src, (u8 *)dst, col->dimensions); + return; + case VEC0_IVF_QUANTIZER_NONE: + break; + } + memcpy(dst, src, col->dimensions * sizeof(f32)); +} + +/** + * The distance between two vectors as the cells of column col hold them: in + * the column's metric for float32 and int8 vectors, and hamming for binary. + */ +static f32 ivf_distance(struct VectorColumnDefinition *col, const void *a, + const void *b) { + size_t dimensions = col->dimensions; + switch (col->ivf.quantizer) { + case VEC0_IVF_QUANTIZER_INT8: + switch (col->distance_metric) { + case VEC0_DISTANCE_METRIC_L2: + return distance_l2_sqr_int8(a, b, &dimensions); + case VEC0_DISTANCE_METRIC_COSINE: + return distance_cosine_int8(a, b, &dimensions); + case VEC0_DISTANCE_METRIC_L1: + return (f32)distance_l1_int8(a, b, &dimensions); + } + break; + case VEC0_IVF_QUANTIZER_BINARY: + return distance_hamming(a, b, &dimensions); + case VEC0_IVF_QUANTIZER_NONE: + return vec0_compute_distance(col, a, b); + } + return FLT_MAX; +} + +static u32 ivf_xorshift32(u32 *state) { + u32 x = *state; + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + *state = x; + return x; +} + +static f32 ivf_l2_distance(const f32 *a, const f32 *b, size_t dimensions) { + f32 sum = 0.0f; + for (size_t d = 0; d < dimensions; d++) { + f32 diff = a[d] - b[d]; + sum += diff * diff; + } + return sum; +} + +/** + * The index of the nearest of the k centroids to vector by L2 distance. + */ +static int ivf_kmeans_nearest(const f32 *vector, const f32 *centroids, + size_t dimensions, int k) { + f32 minDistance = FLT_MAX; + int best = 0; + for (int c = 0; c < k; c++) { + f32 distance = + ivf_l2_distance(vector, ¢roids[(size_t)c * dimensions], dimensions); + if (distance < minDistance) { + minDistance = distance; + best = c; + } + } + return best; +} + +/** + * k-means++: picks the first of k centroids from the n vectors at random, and + * each next one with probability in proportion to its squared distance from + * the nearest centroid picked. + */ +static int ivf_kmeans_init_plusplus(const f32 *vectors, int n, + size_t dimensions, int k, u32 seed, + f32 *centroids) { + if (seed == 0) { + seed = 42; + } + size_t size = dimensions * sizeof(f32); + int first = (int)(ivf_xorshift32(&seed) % (u32)n); + memcpy(centroids, &vectors[(size_t)first * dimensions], size); + if (k == 1) { + return SQLITE_OK; + } + f32 *distances = sqlite3_malloc64((sqlite3_uint64)n * sizeof(f32)); + if (!distances) { + return SQLITE_NOMEM; + } + for (int c = 1; c < k; c++) { + // the distance of each vector from its nearest centroid so far + double total = 0.0; + for (int i = 0; i < n; i++) { + f32 distance = + ivf_l2_distance(&vectors[(size_t)i * dimensions], + ¢roids[(size_t)(c - 1) * dimensions], dimensions); + if (c == 1 || distance < distances[i]) { + distances[i] = distance; + } + total += distances[i]; + } + int pick = n - 1; + if (total <= 0.0) { + pick = (int)(ivf_xorshift32(&seed) % (u32)n); + } else { + double threshold = + ((double)ivf_xorshift32(&seed) / (double)0xFFFFFFFF) * total; + double cumulative = 0.0; + for (int i = 0; i < n; i++) { + cumulative += distances[i]; + if (cumulative >= threshold) { + pick = i; + break; + } + } + } + memcpy(¢roids[(size_t)c * dimensions], + &vectors[(size_t)pick * dimensions], size); + } + sqlite3_free(distances); + return SQLITE_OK; +} + +/** + * Lloyd's k-means of the n vectors of dimensions floats each, from a k-means++ + * start seeded with seed, for up to max_iter rounds or until no vector moves. + * A centroid that loses all its vectors moves to the vector farthest from its + * own centroid. Writes the k centroids to out_centroids, k <= n. + */ +int ivf_kmeans(const f32 *vectors, int n, size_t dimensions, int k, + int max_iter, u32 seed, f32 *out_centroids) { + if (n <= 0 || dimensions == 0 || k <= 0 || k > n) { + return SQLITE_MISUSE; + } + int *assignments = sqlite3_malloc64((sqlite3_uint64)n * sizeof(int)); + f32 *sums = sqlite3_malloc64((sqlite3_uint64)k * dimensions * sizeof(f32)); + int *counts = sqlite3_malloc64((sqlite3_uint64)k * sizeof(int)); + int rc = SQLITE_NOMEM; + if (!assignments || !sums || !counts) { + goto cleanup; + } + for (int i = 0; i < n; i++) { + assignments[i] = -1; + } + rc = ivf_kmeans_init_plusplus(vectors, n, dimensions, k, seed, out_centroids); + if (rc != SQLITE_OK) { + goto cleanup; + } + for (int iter = 0; iter < max_iter; iter++) { + int changed = 0; + for (int i = 0; i < n; i++) { + int nearest = ivf_kmeans_nearest(&vectors[(size_t)i * dimensions], + out_centroids, dimensions, k); + if (nearest != assignments[i]) { + assignments[i] = nearest; + changed++; + } + } + if (changed == 0) { + break; + } + memset(sums, 0, (size_t)k * dimensions * sizeof(f32)); + memset(counts, 0, (size_t)k * sizeof(int)); + for (int i = 0; i < n; i++) { + int c = assignments[i]; + counts[c]++; + for (size_t d = 0; d < dimensions; d++) { + sums[(size_t)c * dimensions + d] += vectors[(size_t)i * dimensions + d]; + } + } + for (int c = 0; c < k; c++) { + f32 *centroid = &out_centroids[(size_t)c * dimensions]; + if (counts[c] > 0) { + for (size_t d = 0; d < dimensions; d++) { + centroid[d] = sums[(size_t)c * dimensions + d] / (f32)counts[c]; + } + continue; + } + f32 maxDistance = -1.0f; + int farthest = 0; + for (int i = 0; i < n; i++) { + f32 distance = ivf_l2_distance( + &vectors[(size_t)i * dimensions], + &out_centroids[(size_t)assignments[i] * dimensions], dimensions); + if (distance > maxDistance) { + maxDistance = distance; + farthest = i; + } + } + memcpy(centroid, &vectors[(size_t)farthest * dimensions], + dimensions * sizeof(f32)); + } + } + rc = SQLITE_OK; + +cleanup: + sqlite3_free(assignments); + sqlite3_free(sums); + sqlite3_free(counts); + return rc; +} + +/** + * Reads whether column i has centroids into *outTrained, from its + * ivf_trained_N row of _info, which every connection's writes keep current. + */ +static int ivf_trained(vec0_vtab *p, int i, int *outTrained) { + int rc = vec0_cached_stmt( + p, &p->ivfStmts[i].trained, + "SELECT value FROM \"%w\".\"%w_info\" WHERE key = ?", p->tableName); + if (rc != SQLITE_OK) { + return rc; + } + char *zKey = sqlite3_mprintf("ivf_trained_%d", i); + if (!zKey) { + return SQLITE_NOMEM; + } + sqlite3_stmt *stmt = p->ivfStmts[i].trained; + sqlite3_bind_text(stmt, 1, zKey, -1, sqlite3_free); + rc = sqlite3_step(stmt); + if (rc == SQLITE_ROW) { + *outTrained = sqlite3_column_int(stmt, 0) == 1; + rc = SQLITE_OK; + } else if (rc == SQLITE_DONE) { + vtab_set_error(&p->base, + "the _info table has no ivf_trained_%d row for vector " + "column %d", + i, i); + rc = SQLITE_ERROR; + } else { + rc = vec0_error_rc(rc); + } + sqlite3_reset(stmt); + sqlite3_clear_bindings(stmt); + return rc; +} + +/** + * Sets the ivf_trained_N row of _info of column i. + */ +static int ivf_set_trained(vec0_vtab *p, int i, int trained) { + sqlite3_stmt *stmt = NULL; + char *zSql = sqlite3_mprintf("INSERT OR REPLACE INTO " VEC0_SHADOW_INFO_NAME + " (key, value) VALUES (?, ?)", + p->schemaName, p->tableName); + char *zKey = sqlite3_mprintf("ivf_trained_%d", i); + if (!zSql || !zKey) { + sqlite3_free(zSql); + sqlite3_free(zKey); + return SQLITE_NOMEM; + } + int rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); + sqlite3_free(zSql); + if (rc != SQLITE_OK) { + sqlite3_free(zKey); + return rc; + } + sqlite3_bind_text(stmt, 1, zKey, -1, sqlite3_free); + sqlite3_bind_text(stmt, 2, trained ? "1" : "0", -1, SQLITE_STATIC); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + return rc == SQLITE_DONE ? SQLITE_OK : vec0_error_rc(rc); +} + +/** + * Sets or clears the validity bit of slot in cell cellId of column i, and + * adds delta to the cell's count of rows. + */ +static int ivf_cell_mark(vec0_vtab *p, int i, i64 cellId, int slot, int valid, + int delta) { + sqlite3_blob *blob = NULL; + int rc = sqlite3_blob_open(p->db, p->schemaName, p->shadowIvfCellsNames[i], + "validity", cellId, 1, &blob); + if (rc != SQLITE_OK) { + return vec0_error_rc(rc); + } + u8 byte; + if (sqlite3_blob_bytes(blob) != VEC0_IVF_CELL_MAX_VECTORS / CHAR_BIT) { + sqlite3_blob_close(blob); + vtab_set_error(&p->base, + "cell %lld of the IVF index of vector column %d is corrupt", + cellId, i); + return SQLITE_ERROR; + } + rc = sqlite3_blob_read(blob, &byte, 1, slot / CHAR_BIT); + if (rc == SQLITE_OK) { + if (valid) { + byte |= (u8)(1 << (slot % CHAR_BIT)); + } else { + byte &= (u8) ~(1 << (slot % CHAR_BIT)); + } + rc = sqlite3_blob_write(blob, &byte, 1, slot / CHAR_BIT); + } + int brc = sqlite3_blob_close(blob); + if (rc == SQLITE_OK) { + rc = brc; + } + if (rc != SQLITE_OK) { + return vec0_error_rc(rc); + } + + rc = vec0_cached_stmt(p, &p->ivfStmts[i].cellCount, + "UPDATE \"%w\".\"%w\" SET n_vectors = n_vectors + ? " + "WHERE rowid = ?", + p->shadowIvfCellsNames[i]); + if (rc != SQLITE_OK) { + return rc; + } + sqlite3_stmt *stmt = p->ivfStmts[i].cellCount; + sqlite3_bind_int(stmt, 1, delta); + sqlite3_bind_int64(stmt, 2, cellId); + rc = sqlite3_step(stmt); + sqlite3_reset(stmt); + sqlite3_clear_bindings(stmt); + return rc == SQLITE_DONE ? SQLITE_OK : vec0_error_rc(rc); +} + +/** + * Writes n bytes of data at offset in column zColumn of row cellId of the + * cells of column i. + */ +static int ivf_cell_write(vec0_vtab *p, int i, i64 cellId, const char *zColumn, + const void *data, int n, int offset) { + sqlite3_blob *blob = NULL; + int rc = sqlite3_blob_open(p->db, p->schemaName, p->shadowIvfCellsNames[i], + zColumn, cellId, 1, &blob); + if (rc != SQLITE_OK) { + return vec0_error_rc(rc); + } + if (sqlite3_blob_bytes(blob) < offset + n) { + sqlite3_blob_close(blob); + vtab_set_error(&p->base, + "cell %lld of the IVF index of vector column %d is corrupt", + cellId, i); + return SQLITE_ERROR; + } + rc = sqlite3_blob_write(blob, data, n, offset); + int brc = sqlite3_blob_close(blob); + if (rc == SQLITE_OK) { + rc = brc; + } + return rc == SQLITE_OK ? SQLITE_OK : vec0_error_rc(rc); +} + +/** + * Places row rowid of column i, whose vector the cells hold as qvector, in the + * first free slot of a cell of centroid centroidId, making a cell when every + * cell of the centroid is full, and records the cell and slot in + * _ivf_rowid_mapNN. + */ +static int ivf_place(vec0_vtab *p, int i, i64 centroidId, i64 rowid, + const void *qvector) { + struct VectorColumnDefinition *col = &p->vector_columns[i]; + int vectorSize = ivf_vector_size(col); + int found = 0; + i64 cellId = 0; + int slot = -1; + + int rc = vec0_cached_stmt(p, &p->ivfStmts[i].cellFind, + "SELECT rowid, validity FROM \"%w\".\"%w\" WHERE " + "centroid_id = ? AND n_vectors < ? LIMIT 1", + p->shadowIvfCellsNames[i]); + if (rc != SQLITE_OK) { + return rc; + } + sqlite3_stmt *stmt = p->ivfStmts[i].cellFind; + sqlite3_bind_int64(stmt, 1, centroidId); + sqlite3_bind_int(stmt, 2, VEC0_IVF_CELL_MAX_VECTORS); + rc = sqlite3_step(stmt); + if (rc == SQLITE_ROW) { + found = 1; + cellId = sqlite3_column_int64(stmt, 0); + const u8 *validity = sqlite3_column_blob(stmt, 1); + if (validity && + sqlite3_column_bytes(stmt, 1) == VEC0_IVF_CELL_MAX_VECTORS / CHAR_BIT) { + for (int s = 0; s < VEC0_IVF_CELL_MAX_VECTORS; s++) { + if (!(validity[s / CHAR_BIT] & (1 << (s % CHAR_BIT)))) { + slot = s; + break; + } + } + } + rc = SQLITE_OK; + } else if (rc == SQLITE_DONE) { + rc = SQLITE_OK; + } else { + rc = vec0_error_rc(rc); + } + sqlite3_reset(stmt); + sqlite3_clear_bindings(stmt); + if (rc != SQLITE_OK) { + return rc; + } + if (found && slot < 0) { + // the count says the cell has room, but no slot is free + vtab_set_error(&p->base, + "cell %lld of the IVF index of vector column %d is corrupt", + cellId, i); + return SQLITE_ERROR; + } + + if (!found) { + char *zSql = sqlite3_mprintf( + "INSERT INTO \"%w\".\"%w\" (centroid_id, n_vectors, validity, rowids, " + "vectors) VALUES (?, 0, ?, ?, ?)", + p->schemaName, p->shadowIvfCellsNames[i]); + if (!zSql) { + return SQLITE_NOMEM; + } + stmt = NULL; + rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); + sqlite3_free(zSql); + if (rc != SQLITE_OK) { + return rc; + } + sqlite3_bind_int64(stmt, 1, centroidId); + sqlite3_bind_zeroblob(stmt, 2, VEC0_IVF_CELL_MAX_VECTORS / CHAR_BIT); + sqlite3_bind_zeroblob(stmt, 3, VEC0_IVF_CELL_MAX_VECTORS * sizeof(i64)); + sqlite3_bind_zeroblob(stmt, 4, VEC0_IVF_CELL_MAX_VECTORS * vectorSize); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + if (rc != SQLITE_DONE) { + return vec0_error_rc(rc); + } + cellId = sqlite3_last_insert_rowid(p->db); + slot = 0; + } + + rc = ivf_cell_write(p, i, cellId, "rowids", &rowid, sizeof(i64), + slot * (int)sizeof(i64)); + if (rc == SQLITE_OK) { + rc = ivf_cell_write(p, i, cellId, "vectors", qvector, vectorSize, + slot * vectorSize); + } + if (rc == SQLITE_OK) { + rc = ivf_cell_mark(p, i, cellId, slot, 1, 1); + } + if (rc != SQLITE_OK) { + return rc; + } + + rc = vec0_cached_stmt(p, &p->ivfStmts[i].mapWrite, + "INSERT INTO \"%w\".\"%w\" (rowid, cell_id, slot) " + "VALUES (?, ?, ?)", + p->shadowIvfRowidMapNames[i]); + if (rc != SQLITE_OK) { + return rc; + } + stmt = p->ivfStmts[i].mapWrite; + sqlite3_bind_int64(stmt, 1, rowid); + sqlite3_bind_int64(stmt, 2, cellId); + sqlite3_bind_int(stmt, 3, slot); + rc = sqlite3_step(stmt); + sqlite3_reset(stmt); + sqlite3_clear_bindings(stmt); + return rc == SQLITE_DONE ? SQLITE_OK : vec0_error_rc(rc); +} + +/** + * Reads the cell and slot of row rowid of column i from _ivf_rowid_mapNN. + * Returns SQLITE_EMPTY when the row has none. + */ +static int ivf_map_read(vec0_vtab *p, int i, i64 rowid, i64 *outCellId, + int *outSlot) { + int rc = vec0_cached_stmt( + p, &p->ivfStmts[i].mapRead, + "SELECT cell_id, slot FROM \"%w\".\"%w\" WHERE rowid = ?", + p->shadowIvfRowidMapNames[i]); + if (rc != SQLITE_OK) { + return rc; + } + sqlite3_stmt *stmt = p->ivfStmts[i].mapRead; + sqlite3_bind_int64(stmt, 1, rowid); + rc = sqlite3_step(stmt); + if (rc == SQLITE_ROW) { + *outCellId = sqlite3_column_int64(stmt, 0); + *outSlot = sqlite3_column_int(stmt, 1); + rc = SQLITE_OK; + if (*outSlot < 0 || *outSlot >= VEC0_IVF_CELL_MAX_VECTORS) { + vtab_set_error(&p->base, + "row %lld of the IVF index of vector column %d is " + "corrupt", + rowid, i); + rc = SQLITE_ERROR; + } + } else if (rc == SQLITE_DONE) { + rc = SQLITE_EMPTY; + } else { + rc = vec0_error_rc(rc); + } + sqlite3_reset(stmt); + sqlite3_clear_bindings(stmt); + return rc; +} + +/** + * Takes row rowid of column i out of its cell and out of _ivf_rowid_mapNN, + * if it is in one. + */ +static int ivf_unplace(vec0_vtab *p, int i, i64 rowid) { + i64 cellId; + int slot; + int rc = ivf_map_read(p, i, rowid, &cellId, &slot); + if (rc == SQLITE_EMPTY) { + return SQLITE_OK; + } + if (rc != SQLITE_OK) { + return rc; + } + rc = ivf_cell_mark(p, i, cellId, slot, 0, -1); + if (rc != SQLITE_OK) { + return rc; + } + rc = vec0_cached_stmt(p, &p->ivfStmts[i].mapDelete, + "DELETE FROM \"%w\".\"%w\" WHERE rowid = ?", + p->shadowIvfRowidMapNames[i]); + if (rc != SQLITE_OK) { + return rc; + } + sqlite3_stmt *stmt = p->ivfStmts[i].mapDelete; + sqlite3_bind_int64(stmt, 1, rowid); + rc = sqlite3_step(stmt); + sqlite3_reset(stmt); + sqlite3_clear_bindings(stmt); + return rc == SQLITE_DONE ? SQLITE_OK : vec0_error_rc(rc); +} + +/** + * Reads the centroid of column i nearest qvector, a vector as the cells hold + * it, into *outCentroidId. Fails when the column has no centroid. + */ +static int ivf_nearest_centroid(vec0_vtab *p, int i, const void *qvector, + i64 *outCentroidId) { + struct VectorColumnDefinition *col = &p->vector_columns[i]; + int vectorSize = ivf_vector_size(col); + int rc = vec0_cached_stmt(p, &p->ivfStmts[i].centroids, + "SELECT centroid_id, centroid FROM \"%w\".\"%w\"", + p->shadowIvfCentroidsNames[i]); + if (rc != SQLITE_OK) { + return rc; + } + sqlite3_stmt *stmt = p->ivfStmts[i].centroids; + f32 minDistance = FLT_MAX; + int found = 0; + while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) { + const void *centroid = sqlite3_column_blob(stmt, 1); + if (!centroid || sqlite3_column_bytes(stmt, 1) != vectorSize) { + vtab_set_error(&p->base, + "centroid %lld of the IVF index of vector column %d is " + "corrupt", + sqlite3_column_int64(stmt, 0), i); + rc = SQLITE_ERROR; + break; + } + f32 distance = ivf_distance(col, qvector, centroid); + if (!found || distance < minDistance) { + minDistance = distance; + *outCentroidId = sqlite3_column_int64(stmt, 0); + found = 1; + } + } + if (rc == SQLITE_DONE) { + rc = SQLITE_OK; + if (!found) { + vtab_set_error(&p->base, "vector column %d has no IVF centroids", i); + rc = SQLITE_ERROR; + } + } else if (rc != SQLITE_ERROR) { + rc = vec0_error_rc(rc); + } + sqlite3_reset(stmt); + return rc; +} + +/** + * Adds row rowid of column i, with float32 vector, to the index: in a cell of + * the centroid nearest its quantized vector, or of no centroid while the + * column has none, and with a quantizer to _ivf_vectorsNN. + */ +static int ivf_insert(vec0_vtab *p, int i, i64 rowid, const void *vector) { + struct VectorColumnDefinition *col = &p->vector_columns[i]; + void *qvector = sqlite3_malloc(ivf_vector_size(col)); + if (!qvector) { + return SQLITE_NOMEM; + } + ivf_quantize(col, vector, qvector); + int trained = 0; + i64 centroidId = VEC0_IVF_UNASSIGNED_CENTROID_ID; + int rc = ivf_trained(p, i, &trained); + if (rc == SQLITE_OK && trained) { + rc = ivf_nearest_centroid(p, i, qvector, ¢roidId); + } + if (rc == SQLITE_OK) { + rc = ivf_place(p, i, centroidId, rowid, qvector); + } + sqlite3_free(qvector); + if (rc != SQLITE_OK || col->ivf.quantizer == VEC0_IVF_QUANTIZER_NONE) { + return rc; + } + + rc = vec0_cached_stmt( + p, &p->ivfStmts[i].vectorWrite, + "INSERT INTO \"%w\".\"%w\" (rowid, vector) VALUES (?, ?)", + p->shadowIvfVectorsNames[i]); + if (rc != SQLITE_OK) { + return rc; + } + sqlite3_stmt *stmt = p->ivfStmts[i].vectorWrite; + sqlite3_bind_int64(stmt, 1, rowid); + sqlite3_bind_blob(stmt, 2, vector, (int)(col->dimensions * sizeof(f32)), + SQLITE_STATIC); + rc = sqlite3_step(stmt); + sqlite3_reset(stmt); + sqlite3_clear_bindings(stmt); + return rc == SQLITE_DONE ? SQLITE_OK : vec0_error_rc(rc); +} + +/** + * Removes row rowid of column i from the index. + */ +static int ivf_delete(vec0_vtab *p, int i, i64 rowid) { + int rc = ivf_unplace(p, i, rowid); + if (rc != SQLITE_OK || + p->vector_columns[i].ivf.quantizer == VEC0_IVF_QUANTIZER_NONE) { + return rc; + } + rc = vec0_cached_stmt(p, &p->ivfStmts[i].vectorDelete, + "DELETE FROM \"%w\".\"%w\" WHERE rowid = ?", + p->shadowIvfVectorsNames[i]); + if (rc != SQLITE_OK) { + return rc; + } + sqlite3_stmt *stmt = p->ivfStmts[i].vectorDelete; + sqlite3_bind_int64(stmt, 1, rowid); + rc = sqlite3_step(stmt); + sqlite3_reset(stmt); + sqlite3_clear_bindings(stmt); + return rc == SQLITE_DONE ? SQLITE_OK : vec0_error_rc(rc); +} + +/** + * Reads the float32 vector of row rowid of column i into *outVector, to be + * freed with sqlite3_free(), and its size into *outVectorSize unless that is + * NULL: from its cell, or with a quantizer from _ivf_vectorsNN. Returns + * SQLITE_EMPTY when the column has no vector with that rowid. + */ +static int ivf_vector_read(vec0_vtab *p, int i, i64 rowid, void **outVector, + int *outVectorSize) { + struct VectorColumnDefinition *col = &p->vector_columns[i]; + int size = (int)(col->dimensions * sizeof(f32)); + void *vector = NULL; + int rc; + if (col->ivf.quantizer == VEC0_IVF_QUANTIZER_NONE) { + i64 cellId; + int slot; + rc = ivf_map_read(p, i, rowid, &cellId, &slot); + if (rc != SQLITE_OK) { + return rc; + } + sqlite3_blob *blob = NULL; + rc = sqlite3_blob_open(p->db, p->schemaName, p->shadowIvfCellsNames[i], + "vectors", cellId, 0, &blob); + if (rc != SQLITE_OK) { + return vec0_error_rc(rc); + } + if (sqlite3_blob_bytes(blob) < (slot + 1) * size) { + sqlite3_blob_close(blob); + vtab_set_error(&p->base, + "cell %lld of the IVF index of vector column %d is " + "corrupt", + cellId, i); + return SQLITE_ERROR; + } + vector = sqlite3_malloc(size); + rc = vector ? sqlite3_blob_read(blob, vector, size, slot * size) + : SQLITE_NOMEM; + sqlite3_blob_close(blob); + if (rc != SQLITE_OK) { + sqlite3_free(vector); + return vec0_error_rc(rc); + } + } else { + rc = vec0_cached_stmt(p, &p->ivfStmts[i].vectorRead, + "SELECT vector FROM \"%w\".\"%w\" WHERE rowid = ?", + p->shadowIvfVectorsNames[i]); + if (rc != SQLITE_OK) { + return rc; + } + sqlite3_stmt *stmt = p->ivfStmts[i].vectorRead; + sqlite3_bind_int64(stmt, 1, rowid); + rc = sqlite3_step(stmt); + if (rc == SQLITE_ROW) { + const void *blob = sqlite3_column_blob(stmt, 0); + if (!blob || sqlite3_column_bytes(stmt, 0) != size) { + vtab_set_error(&p->base, + "the IVF vector of row %lld of vector column %d is " + "corrupt", + rowid, i); + rc = SQLITE_ERROR; + } else { + vector = sqlite3_malloc(size); + if (vector) { + memcpy(vector, blob, size); + rc = SQLITE_OK; + } else { + rc = SQLITE_NOMEM; + } + } + } else { + rc = rc == SQLITE_DONE ? SQLITE_EMPTY : vec0_error_rc(rc); + } + sqlite3_reset(stmt); + sqlite3_clear_bindings(stmt); + if (rc != SQLITE_OK) { + return rc; + } + } + *outVector = vector; + if (outVectorSize) { + *outVectorSize = size; + } + return SQLITE_OK; +} + +/** + * Runs zFormat, a statement without parameters on table zTable of the schema, + * to its end. + */ +static int ivf_exec(vec0_vtab *p, const char *zFormat, const char *zTable) { + sqlite3_stmt *stmt = NULL; + char *zSql = sqlite3_mprintf(zFormat, p->schemaName, zTable); + if (!zSql) { + return SQLITE_NOMEM; + } + int rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); + sqlite3_free(zSql); + if (rc != SQLITE_OK) { + return rc; + } + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + return rc == SQLITE_DONE ? SQLITE_OK : vec0_error_rc(rc); +} + +/** + * Appends to rowids and vectors the rowid and the vector, as the cells of + * column i hold it, of each row in a cell the statement stmt reads, whose + * columns are a cell's rowid, validity, rowids and vectors. Leaves stmt reset. + */ +static int ivf_read_cells(vec0_vtab *p, int i, sqlite3_stmt *stmt, + struct Array *rowids, struct Array *vectors) { + int vectorSize = ivf_vector_size(&p->vector_columns[i]); + int rc; + while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) { + rc = SQLITE_OK; + const u8 *validity = sqlite3_column_blob(stmt, 1); + const u8 *cellRowids = sqlite3_column_blob(stmt, 2); + const u8 *cellVectors = sqlite3_column_blob(stmt, 3); + if (!validity || !cellRowids || !cellVectors || + sqlite3_column_bytes(stmt, 1) != VEC0_IVF_CELL_MAX_VECTORS / CHAR_BIT || + sqlite3_column_bytes(stmt, 2) != + VEC0_IVF_CELL_MAX_VECTORS * (int)sizeof(i64) || + sqlite3_column_bytes(stmt, 3) != + VEC0_IVF_CELL_MAX_VECTORS * vectorSize) { + vtab_set_error(&p->base, + "cell %lld of the IVF index of vector column %d is " + "corrupt", + sqlite3_column_int64(stmt, 0), i); + rc = SQLITE_ERROR; + break; + } + for (int s = 0; s < VEC0_IVF_CELL_MAX_VECTORS; s++) { + if (!(validity[s / CHAR_BIT] & (1 << (s % CHAR_BIT)))) { + continue; + } + i64 rowid; + memcpy(&rowid, cellRowids + (size_t)s * sizeof(i64), sizeof(i64)); + rc = array_append(rowids, &rowid); + if (rc == SQLITE_OK) { + rc = array_append(vectors, cellVectors + (size_t)s * vectorSize); + } + if (rc != SQLITE_OK) { + break; + } + } + if (rc != SQLITE_OK) { + break; + } + } + if (rc == SQLITE_DONE) { + rc = SQLITE_OK; + } else if (rc != SQLITE_ERROR && rc != SQLITE_NOMEM) { + rc = vec0_error_rc(rc); + } + sqlite3_reset(stmt); + sqlite3_clear_bindings(stmt); + return rc; +} + +/** + * Appends to rowids and vectors the rowid and float32 vector of every row of + * column i: from its cells, or with a quantizer from _ivf_vectorsNN. + */ +static int ivf_load_vectors(vec0_vtab *p, int i, struct Array *rowids, + struct Array *vectors) { + struct VectorColumnDefinition *col = &p->vector_columns[i]; + sqlite3_stmt *stmt = NULL; + int rc; + if (col->ivf.quantizer == VEC0_IVF_QUANTIZER_NONE) { + char *zSql = sqlite3_mprintf( + "SELECT rowid, validity, rowids, vectors FROM \"%w\".\"%w\"", + p->schemaName, p->shadowIvfCellsNames[i]); + if (!zSql) { + return SQLITE_NOMEM; + } + rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); + sqlite3_free(zSql); + if (rc == SQLITE_OK) { + rc = ivf_read_cells(p, i, stmt, rowids, vectors); + } + sqlite3_finalize(stmt); + return rc; + } + + int size = (int)(col->dimensions * sizeof(f32)); + char *zSql = sqlite3_mprintf("SELECT rowid, vector FROM \"%w\".\"%w\"", + p->schemaName, p->shadowIvfVectorsNames[i]); + if (!zSql) { + return SQLITE_NOMEM; + } + rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); + sqlite3_free(zSql); + if (rc != SQLITE_OK) { + return rc; + } + while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) { + i64 rowid = sqlite3_column_int64(stmt, 0); + const void *vector = sqlite3_column_blob(stmt, 1); + if (!vector || sqlite3_column_bytes(stmt, 1) != size) { + vtab_set_error(&p->base, + "the IVF vector of row %lld of vector column %d is " + "corrupt", + rowid, i); + rc = SQLITE_ERROR; + break; + } + rc = array_append(rowids, &rowid); + if (rc == SQLITE_OK) { + rc = array_append(vectors, vector); + } + if (rc != SQLITE_OK) { + break; + } + } + if (rc == SQLITE_DONE) { + rc = SQLITE_OK; + } else if (rc != SQLITE_ERROR && rc != SQLITE_NOMEM) { + rc = vec0_error_rc(rc); + } + sqlite3_finalize(stmt); + return rc; +} + +struct IvfPlacement { + i64 centroidId; + int row; +}; + +static int ivf_placement_cmp(const void *a, const void *b) { + const struct IvfPlacement *x = a; + const struct IvfPlacement *y = b; + if (x->centroidId != y->centroidId) { + return x->centroidId < y->centroidId ? -1 : 1; + } + return x->row < y->row ? -1 : (x->row > y->row); +} + +/** + * Replaces the centroids, cells and row map of column i: nCentroids float32 + * centroids with ids from 0, and n rows, rowids and float32 vectors, each in + * cells of the centroid centroidOf gives it. + */ +static int ivf_rebuild(vec0_vtab *p, int i, int n, const i64 *rowids, + const f32 *vectors, const i64 *centroidOf, + int nCentroids, const f32 *centroids) { + struct VectorColumnDefinition *col = &p->vector_columns[i]; + int vectorSize = ivf_vector_size(col); + sqlite3_stmt *stmtCentroid = NULL; + sqlite3_stmt *stmtCell = NULL; + struct IvfPlacement *order = NULL; + u8 *validity = NULL; + i64 *cellRowids = NULL; + u8 *cellVectors = NULL; + void *qvector = NULL; + + int rc = + ivf_exec(p, "DELETE FROM \"%w\".\"%w\"", p->shadowIvfCentroidsNames[i]); + if (rc == SQLITE_OK) { + rc = ivf_exec(p, "DELETE FROM \"%w\".\"%w\"", p->shadowIvfCellsNames[i]); + } + if (rc == SQLITE_OK) { + rc = ivf_exec(p, "DELETE FROM \"%w\".\"%w\"", p->shadowIvfRowidMapNames[i]); + } + if (rc != SQLITE_OK) { + return rc; + } + + qvector = sqlite3_malloc(vectorSize); + validity = sqlite3_malloc(VEC0_IVF_CELL_MAX_VECTORS / CHAR_BIT); + cellRowids = sqlite3_malloc(VEC0_IVF_CELL_MAX_VECTORS * sizeof(i64)); + cellVectors = + sqlite3_malloc64((sqlite3_uint64)VEC0_IVF_CELL_MAX_VECTORS * vectorSize); + order = sqlite3_malloc64((sqlite3_uint64)(n > 0 ? n : 1) * sizeof(*order)); + if (!qvector || !validity || !cellRowids || !cellVectors || !order) { + rc = SQLITE_NOMEM; + goto cleanup; + } + + char *zSql = sqlite3_mprintf( + "INSERT INTO \"%w\".\"%w\" (centroid_id, centroid) VALUES (?, ?)", + p->schemaName, p->shadowIvfCentroidsNames[i]); + if (!zSql) { + rc = SQLITE_NOMEM; + goto cleanup; + } + rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmtCentroid, NULL); + sqlite3_free(zSql); + if (rc != SQLITE_OK) { + goto cleanup; + } + for (int c = 0; c < nCentroids; c++) { + ivf_quantize(col, ¢roids[(size_t)c * col->dimensions], qvector); + sqlite3_bind_int(stmtCentroid, 1, c); + sqlite3_bind_blob(stmtCentroid, 2, qvector, vectorSize, SQLITE_STATIC); + rc = sqlite3_step(stmtCentroid); + sqlite3_reset(stmtCentroid); + if (rc != SQLITE_DONE) { + rc = vec0_error_rc(rc); + goto cleanup; + } + } + + zSql = sqlite3_mprintf( + "INSERT INTO \"%w\".\"%w\" (centroid_id, n_vectors, validity, rowids, " + "vectors) VALUES (?, ?, ?, ?, ?)", + p->schemaName, p->shadowIvfCellsNames[i]); + if (!zSql) { + rc = SQLITE_NOMEM; + goto cleanup; + } + rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmtCell, NULL); + sqlite3_free(zSql); + if (rc != SQLITE_OK) { + goto cleanup; + } + rc = vec0_cached_stmt(p, &p->ivfStmts[i].mapWrite, + "INSERT INTO \"%w\".\"%w\" (rowid, cell_id, slot) " + "VALUES (?, ?, ?)", + p->shadowIvfRowidMapNames[i]); + if (rc != SQLITE_OK) { + goto cleanup; + } + + for (int r = 0; r < n; r++) { + order[r].centroidId = centroidOf[r]; + order[r].row = r; + } + qsort(order, n, sizeof(*order), ivf_placement_cmp); + + // fill cells of up to VEC0_IVF_CELL_MAX_VECTORS rows, one centroid at a time + for (int start = 0; start < n;) { + int count = 0; + memset(validity, 0, VEC0_IVF_CELL_MAX_VECTORS / CHAR_BIT); + memset(cellRowids, 0, VEC0_IVF_CELL_MAX_VECTORS * sizeof(i64)); + memset(cellVectors, 0, (size_t)VEC0_IVF_CELL_MAX_VECTORS * vectorSize); + while (start + count < n && count < VEC0_IVF_CELL_MAX_VECTORS && + order[start + count].centroidId == order[start].centroidId) { + int row = order[start + count].row; + validity[count / CHAR_BIT] |= (u8)(1 << (count % CHAR_BIT)); + cellRowids[count] = rowids[row]; + ivf_quantize(col, &vectors[(size_t)row * col->dimensions], + cellVectors + (size_t)count * vectorSize); + count++; + } + sqlite3_bind_int64(stmtCell, 1, order[start].centroidId); + sqlite3_bind_int(stmtCell, 2, count); + sqlite3_bind_blob(stmtCell, 3, validity, + VEC0_IVF_CELL_MAX_VECTORS / CHAR_BIT, SQLITE_STATIC); + sqlite3_bind_blob(stmtCell, 4, cellRowids, + VEC0_IVF_CELL_MAX_VECTORS * (int)sizeof(i64), + SQLITE_STATIC); + sqlite3_bind_blob(stmtCell, 5, cellVectors, + VEC0_IVF_CELL_MAX_VECTORS * vectorSize, SQLITE_STATIC); + rc = sqlite3_step(stmtCell); + sqlite3_reset(stmtCell); + if (rc != SQLITE_DONE) { + rc = vec0_error_rc(rc); + goto cleanup; + } + i64 cellId = sqlite3_last_insert_rowid(p->db); + sqlite3_stmt *stmtMap = p->ivfStmts[i].mapWrite; + for (int s = 0; s < count; s++) { + sqlite3_bind_int64(stmtMap, 1, cellRowids[s]); + sqlite3_bind_int64(stmtMap, 2, cellId); + sqlite3_bind_int(stmtMap, 3, s); + rc = sqlite3_step(stmtMap); + sqlite3_reset(stmtMap); + if (rc != SQLITE_DONE) { + rc = vec0_error_rc(rc); + goto cleanup; + } + } + sqlite3_clear_bindings(stmtMap); + start += count; + } + rc = SQLITE_OK; + +cleanup: + if (p->ivfStmts[i].mapWrite) { + sqlite3_reset(p->ivfStmts[i].mapWrite); + sqlite3_clear_bindings(p->ivfStmts[i].mapWrite); + } + sqlite3_finalize(stmtCentroid); + sqlite3_finalize(stmtCell); + sqlite3_free(order); + sqlite3_free(validity); + sqlite3_free(cellRowids); + sqlite3_free(cellVectors); + sqlite3_free(qvector); + return rc; +} + +/** + * compute-centroids for column i: clusters its rows with k-means into nlist + * centroids, the column's nlist when nlist is 0 and at most one per row, and + * moves each row to the cells of the centroid nearest it. + */ +static int ivf_compute_centroids(vec0_vtab *p, int i, int nlist, int maxIter, + u32 seed) { + struct VectorColumnDefinition *col = &p->vector_columns[i]; + size_t dimensions = col->dimensions; + struct Array rowids, vectors; + f32 *centroids = NULL; + i64 *centroidOf = NULL; + int rc = array_init(&rowids, sizeof(i64), 64); + if (rc != SQLITE_OK) { + return rc; + } + rc = array_init(&vectors, dimensions * sizeof(f32), 64); + if (rc != SQLITE_OK) { + array_cleanup(&rowids); + return rc; + } + rc = ivf_load_vectors(p, i, &rowids, &vectors); + if (rc != SQLITE_OK) { + goto cleanup; + } + int n = (int)rowids.length; + if (nlist <= 0) { + nlist = col->ivf.nlist; + } + if (nlist <= 0) { + vtab_set_error(&p->base, + "compute-centroids needs nlist for vector column %d, which " + "was created with nlist=0", + i); + rc = SQLITE_ERROR; + goto cleanup; + } + if (n == 0) { + vtab_set_error(&p->base, + "compute-centroids needs at least one row in vector " + "column %d", + i); + rc = SQLITE_ERROR; + goto cleanup; + } + if (nlist > n) { + nlist = n; + } + const f32 *data = (const f32 *)vectors.z; + centroids = + sqlite3_malloc64((sqlite3_uint64)nlist * dimensions * sizeof(f32)); + centroidOf = sqlite3_malloc64((sqlite3_uint64)n * sizeof(i64)); + if (!centroids || !centroidOf) { + rc = SQLITE_NOMEM; + goto cleanup; + } + rc = ivf_kmeans(data, n, dimensions, nlist, maxIter, seed, centroids); + if (rc != SQLITE_OK) { + goto cleanup; + } + for (int r = 0; r < n; r++) { + f32 minDistance = FLT_MAX; + centroidOf[r] = 0; + for (int c = 0; c < nlist; c++) { + f32 distance = vec0_compute_distance(col, &data[(size_t)r * dimensions], + ¢roids[(size_t)c * dimensions]); + if (distance < minDistance) { + minDistance = distance; + centroidOf[r] = c; + } + } + } + rc = ivf_rebuild(p, i, n, (const i64 *)rowids.z, data, centroidOf, nlist, + centroids); + if (rc == SQLITE_OK) { + rc = ivf_set_trained(p, i, 1); + } + +cleanup: + sqlite3_free(centroids); + sqlite3_free(centroidOf); + array_cleanup(&rowids); + array_cleanup(&vectors); + return rc; +} + +/** + * clear-centroids for column i: removes its centroids and moves every row to + * cells of no centroid. + */ +static int ivf_clear_centroids(vec0_vtab *p, int i) { + struct Array rowids, vectors; + i64 *centroidOf = NULL; + int rc = array_init(&rowids, sizeof(i64), 64); + if (rc != SQLITE_OK) { + return rc; + } + rc = array_init(&vectors, p->vector_columns[i].dimensions * sizeof(f32), 64); + if (rc != SQLITE_OK) { + array_cleanup(&rowids); + return rc; + } + rc = ivf_load_vectors(p, i, &rowids, &vectors); + if (rc != SQLITE_OK) { + goto cleanup; + } + int n = (int)rowids.length; + centroidOf = sqlite3_malloc64((sqlite3_uint64)(n > 0 ? n : 1) * sizeof(i64)); + if (!centroidOf) { + rc = SQLITE_NOMEM; + goto cleanup; + } + for (int r = 0; r < n; r++) { + centroidOf[r] = VEC0_IVF_UNASSIGNED_CENTROID_ID; + } + rc = ivf_rebuild(p, i, n, (const i64 *)rowids.z, (const f32 *)vectors.z, + centroidOf, 0, NULL); + if (rc == SQLITE_OK) { + rc = ivf_set_trained(p, i, 0); + } + +cleanup: + sqlite3_free(centroidOf); + array_cleanup(&rowids); + array_cleanup(&vectors); + return rc; +} + +/** + * set-centroid:N for column i: sets its centroid N to the float32 vector. + */ +static int ivf_set_centroid(vec0_vtab *p, int i, i64 centroidId, + const f32 *vector) { + struct VectorColumnDefinition *col = &p->vector_columns[i]; + int vectorSize = ivf_vector_size(col); + sqlite3_stmt *stmt = NULL; + void *qvector = sqlite3_malloc(vectorSize); + if (!qvector) { + return SQLITE_NOMEM; + } + ivf_quantize(col, vector, qvector); + char *zSql = sqlite3_mprintf("INSERT OR REPLACE INTO \"%w\".\"%w\" " + "(centroid_id, centroid) VALUES (?, ?)", + p->schemaName, p->shadowIvfCentroidsNames[i]); + int rc = + zSql ? sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL) : SQLITE_NOMEM; + sqlite3_free(zSql); + if (rc == SQLITE_OK) { + sqlite3_bind_int64(stmt, 1, centroidId); + sqlite3_bind_blob(stmt, 2, qvector, vectorSize, SQLITE_STATIC); + rc = sqlite3_step(stmt); + rc = rc == SQLITE_DONE ? SQLITE_OK : vec0_error_rc(rc); + } + sqlite3_finalize(stmt); + sqlite3_free(qvector); + if (rc == SQLITE_OK) { + rc = ivf_set_trained(p, i, 1); + } + return rc; +} + +/** + * assign-vectors for column i: moves each row in a cell of no centroid to the + * cells of the centroid nearest it. + */ +static int ivf_assign_vectors(vec0_vtab *p, int i) { + struct Array rowids, qvectors; + sqlite3_stmt *stmt = NULL; + int trained = 0; + int rc = ivf_trained(p, i, &trained); + if (rc != SQLITE_OK) { + return rc; + } + if (!trained) { + vtab_set_error(&p->base, "vector column %d has no IVF centroids", i); + return SQLITE_ERROR; + } + rc = array_init(&rowids, sizeof(i64), 64); + if (rc != SQLITE_OK) { + return rc; + } + rc = array_init(&qvectors, ivf_vector_size(&p->vector_columns[i]), 64); + if (rc != SQLITE_OK) { + array_cleanup(&rowids); + return rc; + } + char *zSql = sqlite3_mprintf( + "SELECT rowid, validity, rowids, vectors FROM \"%w\".\"%w\" WHERE " + "centroid_id = ?", + p->schemaName, p->shadowIvfCellsNames[i]); + if (!zSql) { + rc = SQLITE_NOMEM; + goto cleanup; + } + rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); + sqlite3_free(zSql); + if (rc != SQLITE_OK) { + goto cleanup; + } + sqlite3_bind_int64(stmt, 1, VEC0_IVF_UNASSIGNED_CENTROID_ID); + rc = ivf_read_cells(p, i, stmt, &rowids, &qvectors); + if (rc != SQLITE_OK) { + goto cleanup; + } + for (size_t r = 0; r < rowids.length; r++) { + i64 rowid = ((i64 *)rowids.z)[r]; + const void *qvector = (const u8 *)qvectors.z + r * qvectors.element_size; + i64 centroidId; + rc = ivf_nearest_centroid(p, i, qvector, ¢roidId); + if (rc == SQLITE_OK) { + rc = ivf_unplace(p, i, rowid); + } + if (rc == SQLITE_OK) { + rc = ivf_place(p, i, centroidId, rowid, qvector); + } + if (rc != SQLITE_OK) { + goto cleanup; + } + } + rc = + vec0_exec_i64(p, "DELETE FROM \"%w\".\"%w\" WHERE centroid_id = ?", + p->shadowIvfCellsNames[i], VEC0_IVF_UNASSIGNED_CENTROID_ID); + +cleanup: + sqlite3_finalize(stmt); + array_cleanup(&rowids); + array_cleanup(&qvectors); + return rc; +} + +// a row, or a centroid, and its distance from the query +struct IvfCandidate { + i64 id; + f32 distance; +}; + +// nearest first, a NaN distance after every number, and by id among equals +static int ivf_candidate_cmp(const void *a, const void *b) { + const struct IvfCandidate *x = a; + const struct IvfCandidate *y = b; + int xNan = isnan(x->distance); + int yNan = isnan(y->distance); + if (xNan != yNan) { + return xNan - yNan; + } + if (!xNan && x->distance != y->distance) { + return x->distance < y->distance ? -1 : 1; + } + return x->id < y->id ? -1 : (x->id > y->id); +} + +/** + * Appends to candidates each row in a cell of centroid centroidId of column i, + * at its distance from qquery, the query as the cells hold vectors. + */ +static int ivf_scan_cells(vec0_vtab *p, int i, i64 centroidId, + const void *qquery, struct Array *candidates) { + struct VectorColumnDefinition *col = &p->vector_columns[i]; + int vectorSize = ivf_vector_size(col); + int rc = vec0_cached_stmt(p, &p->ivfStmts[i].cellScan, + "SELECT rowid, validity, rowids, vectors FROM " + "\"%w\".\"%w\" WHERE centroid_id = ?", + p->shadowIvfCellsNames[i]); + if (rc != SQLITE_OK) { + return rc; + } + sqlite3_stmt *stmt = p->ivfStmts[i].cellScan; + sqlite3_bind_int64(stmt, 1, centroidId); + while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) { + rc = SQLITE_OK; + const u8 *validity = sqlite3_column_blob(stmt, 1); + const u8 *cellRowids = sqlite3_column_blob(stmt, 2); + const u8 *cellVectors = sqlite3_column_blob(stmt, 3); + if (!validity || !cellRowids || !cellVectors || + sqlite3_column_bytes(stmt, 1) != VEC0_IVF_CELL_MAX_VECTORS / CHAR_BIT || + sqlite3_column_bytes(stmt, 2) != + VEC0_IVF_CELL_MAX_VECTORS * (int)sizeof(i64) || + sqlite3_column_bytes(stmt, 3) != + VEC0_IVF_CELL_MAX_VECTORS * vectorSize) { + vtab_set_error(&p->base, + "cell %lld of the IVF index of vector column %d is " + "corrupt", + sqlite3_column_int64(stmt, 0), i); + rc = SQLITE_ERROR; + break; + } + for (int s = 0; s < VEC0_IVF_CELL_MAX_VECTORS; s++) { + if (!(validity[s / CHAR_BIT] & (1 << (s % CHAR_BIT)))) { + continue; + } + struct IvfCandidate candidate; + memcpy(&candidate.id, cellRowids + (size_t)s * sizeof(i64), sizeof(i64)); + candidate.distance = + ivf_distance(col, qquery, cellVectors + (size_t)s * vectorSize); + rc = array_append(candidates, &candidate); + if (rc != SQLITE_OK) { + break; + } + } + if (rc != SQLITE_OK) { + break; + } + } + if (rc == SQLITE_DONE) { + rc = SQLITE_OK; + } else if (rc != SQLITE_ERROR && rc != SQLITE_NOMEM) { + rc = vec0_error_rc(rc); + } + sqlite3_reset(stmt); + sqlite3_clear_bindings(stmt); + return rc; +} + +/** + * The KNN query of column i, indexed by ivf(). With rowid = or rowid IN, the + * candidates are the rows they name; otherwise they are the rows in the cells + * of the nprobe centroids nearest the query and in the cells of no centroid, + * and with a quantizer the k * oversample of them nearest by quantized + * distance. Returns the k nearest candidates by exact distance within the + * query's distance constraints, nearest first, as vec0Filter_knn_chunks_iter() + * does. + */ +static int ivf_knn(vec0_vtab *p, struct VectorColumnDefinition *col, int i, + struct Array *arrayRowidsIn, const char *idxStr, int argc, + sqlite3_value **argv, void *queryVector, i64 k, + i64 **out_topk_rowids, f32 **out_topk_distances, + i64 *out_used) { + struct Array candidates; + struct Array centroids; + void *qquery = NULL; + i64 *rowids = NULL; + f32 *distances = NULL; + int rc = array_init(&candidates, sizeof(struct IvfCandidate), 64); + if (rc != SQLITE_OK) { + return rc; + } + rc = array_init(¢roids, sizeof(struct IvfCandidate), 64); + if (rc != SQLITE_OK) { + array_cleanup(&candidates); + return rc; + } + + if (arrayRowidsIn) { + for (size_t j = 0; j < arrayRowidsIn->length; j++) { + i64 rowid = ((i64 *)arrayRowidsIn->z)[j]; + if (j > 0 && rowid == ((i64 *)arrayRowidsIn->z)[j - 1]) { + continue; + } + void *vector = NULL; + rc = ivf_vector_read(p, i, rowid, &vector, NULL); + if (rc == SQLITE_EMPTY) { + rc = SQLITE_OK; + continue; + } + if (rc != SQLITE_OK) { + goto cleanup; + } + struct IvfCandidate candidate = { + rowid, vec0_compute_distance(col, vector, queryVector)}; + sqlite3_free(vector); + rc = array_append(&candidates, &candidate); + if (rc != SQLITE_OK) { + goto cleanup; + } + } + } else { + int vectorSize = ivf_vector_size(col); + qquery = sqlite3_malloc(vectorSize); + if (!qquery) { + rc = SQLITE_NOMEM; + goto cleanup; + } + ivf_quantize(col, queryVector, qquery); + + rc = ivf_scan_cells(p, i, VEC0_IVF_UNASSIGNED_CENTROID_ID, qquery, + &candidates); + int trained = 0; + if (rc == SQLITE_OK) { + rc = ivf_trained(p, i, &trained); + } + if (rc != SQLITE_OK) { + goto cleanup; + } + if (trained) { + rc = vec0_cached_stmt(p, &p->ivfStmts[i].centroids, + "SELECT centroid_id, centroid FROM \"%w\".\"%w\"", + p->shadowIvfCentroidsNames[i]); + if (rc != SQLITE_OK) { + goto cleanup; + } + sqlite3_stmt *stmt = p->ivfStmts[i].centroids; + while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) { + struct IvfCandidate centroid; + centroid.id = sqlite3_column_int64(stmt, 0); + const void *blob = sqlite3_column_blob(stmt, 1); + if (!blob || sqlite3_column_bytes(stmt, 1) != vectorSize) { + vtab_set_error(&p->base, + "centroid %lld of the IVF index of vector column %d " + "is corrupt", + centroid.id, i); + rc = SQLITE_ERROR; + break; + } + centroid.distance = ivf_distance(col, qquery, blob); + rc = array_append(¢roids, ¢roid); + if (rc != SQLITE_OK) { + break; + } + } + if (rc == SQLITE_DONE) { + rc = SQLITE_OK; + } else if (rc != SQLITE_ERROR && rc != SQLITE_NOMEM) { + rc = vec0_error_rc(rc); + } + sqlite3_reset(stmt); + if (rc != SQLITE_OK) { + goto cleanup; + } + qsort(centroids.z, centroids.length, sizeof(struct IvfCandidate), + ivf_candidate_cmp); + size_t nprobe = (size_t)col->ivf.nprobe; + for (size_t c = 0; c < centroids.length && c < nprobe; c++) { + i64 centroidId = ((struct IvfCandidate *)centroids.z)[c].id; + if (centroidId == VEC0_IVF_UNASSIGNED_CENTROID_ID) { + continue; + } + rc = ivf_scan_cells(p, i, centroidId, qquery, &candidates); + if (rc != SQLITE_OK) { + goto cleanup; + } + } + } + + if (col->ivf.quantizer != VEC0_IVF_QUANTIZER_NONE) { + // rank the nearest by quantized distance by their exact distances + qsort(candidates.z, candidates.length, sizeof(struct IvfCandidate), + ivf_candidate_cmp); + i64 keep = k * col->ivf.oversample; + if ((i64)candidates.length > keep) { + candidates.length = (size_t)keep; + } + for (size_t j = 0; j < candidates.length; j++) { + struct IvfCandidate *candidate = + &((struct IvfCandidate *)candidates.z)[j]; + void *vector = NULL; + rc = ivf_vector_read(p, i, candidate->id, &vector, NULL); + if (rc == SQLITE_EMPTY) { + vtab_set_error(&p->base, + "row %lld of the IVF index of vector column %d has " + "no vector", + candidate->id, i); + rc = SQLITE_ERROR; + } + if (rc != SQLITE_OK) { + goto cleanup; + } + candidate->distance = vec0_compute_distance(col, vector, queryVector); + sqlite3_free(vector); + } + } + } + + qsort(candidates.z, candidates.length, sizeof(struct IvfCandidate), + ivf_candidate_cmp); + i64 used = (i64)candidates.length; + if (used > 0) { + rowids = sqlite3_malloc64(used * sizeof(i64)); + distances = sqlite3_malloc64(used * sizeof(f32)); + if (!rowids || !distances) { + rc = SQLITE_NOMEM; + goto cleanup; + } + for (i64 j = 0; j < used; j++) { + rowids[j] = ((struct IvfCandidate *)candidates.z)[j].id; + distances[j] = ((struct IvfCandidate *)candidates.z)[j].distance; + } + vec0_knn_keep_within_distance(idxStr, argc, argv, rowids, distances, &used); + } + if (used > k) { + used = k; + } + if (used == 0) { + sqlite3_free(rowids); + sqlite3_free(distances); + rowids = NULL; + distances = NULL; + } + *out_topk_rowids = rowids; + *out_topk_distances = distances; + *out_used = used; + rowids = NULL; + distances = NULL; + rc = SQLITE_OK; + +cleanup: + sqlite3_free(rowids); + sqlite3_free(distances); + sqlite3_free(qquery); + array_cleanup(&candidates); + array_cleanup(¢roids); + return rc; +} + +/** + * Reads the digits of zValue, from min to max, into *out. + * + * @return SQLITE_OK, or SQLITE_ERROR for any other text. + */ +static int ivf_command_int(const char *zValue, i64 min, i64 max, i64 *out) { + if (!(*zValue >= '0' && *zValue <= '9')) { + return SQLITE_ERROR; + } + char *zEnd; + errno = 0; + long long value = strtoll(zValue, &zEnd, 10); + if (*zEnd != '\0' || errno == ERANGE || value < min || value > max) { + return SQLITE_ERROR; + } + *out = value; + return SQLITE_OK; +} + +/** + * Reads the options of `compute-centroids:{...}`, a JSON object whose keys + * nlist, max_iterations and seed each take an integer. + */ +static int ivf_compute_options(vec0_vtab *p, const char *zJson, int *nlist, + int *maxIter, u32 *seed) { + sqlite3_stmt *stmt = NULL; + // the outer row makes a row of `{}` too, so its type is checked + int rc = sqlite3_prepare_v2(p->db, + "SELECT json_type(?1), key, type, value FROM " + "(SELECT 1) LEFT JOIN json_each(?1)", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + vtab_set_error(&p->base, "compute-centroids options: %s", + sqlite3_errmsg(p->db)); + return vec0_error_rc(rc); + } + sqlite3_bind_text(stmt, 1, zJson, -1, SQLITE_STATIC); + static const struct { + const char *zKey; + i64 min; + i64 max; + } aOption[] = { + {"nlist", 1, VEC0_IVF_MAX_NLIST}, + {"max_iterations", 0, INT_MAX}, + {"seed", 0, UINT32_MAX}, + }; + int invalid = 0; + while (!invalid && (rc = sqlite3_step(stmt)) == SQLITE_ROW) { + const char *zType = (const char *)sqlite3_column_text(stmt, 0); + if (!zType || strcmp(zType, "object") != 0) { + vtab_set_error(&p->base, + "compute-centroids options must be a JSON object"); + invalid = 1; + break; + } + if (sqlite3_column_type(stmt, 1) == SQLITE_NULL) { + continue; + } + const char *zKey = (const char *)sqlite3_column_text(stmt, 1); + const char *zValueType = (const char *)sqlite3_column_text(stmt, 2); + i64 value = sqlite3_column_int64(stmt, 3); + size_t o; + for (o = 0; o < countof(aOption); o++) { + if (zKey && strcmp(zKey, aOption[o].zKey) == 0) { + break; + } + } + if (o == countof(aOption)) { + vtab_set_error(&p->base, "unknown compute-centroids option: %s", + zKey ? zKey : ""); + invalid = 1; + } else if (!zValueType || strcmp(zValueType, "integer") != 0 || + value < aOption[o].min || value > aOption[o].max) { + vtab_set_error(&p->base, + "compute-centroids option %s must be an integer from " + "%lld to %lld", + aOption[o].zKey, aOption[o].min, aOption[o].max); + invalid = 1; + } else if (o == 0) { + *nlist = (int)value; + } else if (o == 1) { + *maxIter = (int)value; + } else { + *seed = (u32)value; + } + } + if (invalid) { + rc = SQLITE_ERROR; + } else if (rc == SQLITE_DONE) { + rc = SQLITE_OK; + } else { + vtab_set_error(&p->base, "compute-centroids options: %s", + sqlite3_errmsg(p->db)); + rc = vec0_error_rc(rc); + } + sqlite3_finalize(stmt); + return rc; +} + +/** + * The commands of columns indexed by ivf(), each applied to every such column + * of the table: `nprobe=N` sets the nprobe of this connection's queries; + * `compute-centroids` clusters the rows with k-means into nlist centroids and + * moves each row to the cells of the nearest, and `compute-centroids:{...}` + * does so with the nlist, max_iterations and seed of a JSON object; + * `set-centroid:N` sets centroid N to the vector the INSERT gives the column, + * of each column given one; `assign-vectors` moves the rows no clustering has + * placed to the cells of their nearest centroids; and `clear-centroids` + * removes the centroids and moves every row to cells of no centroid. Returns + * SQLITE_EMPTY for any other command. + */ +static int ivf_handle_command(vec0_vtab *p, const char *command, int argc, + sqlite3_value **argv) { + static const char zNprobe[] = "nprobe="; + static const char zCompute[] = "compute-centroids"; + static const char zSetCentroid[] = "set-centroid:"; + int rc; + int hasIvf = 0; + for (int i = 0; i < p->numVectorColumns; i++) { + hasIvf |= p->vector_columns[i].index_type == VEC0_INDEX_TYPE_IVF; + } + if (!hasIvf) { + return SQLITE_EMPTY; + } + + if (strncmp(command, zNprobe, sizeof(zNprobe) - 1) == 0) { + i64 nprobe; + if (ivf_command_int(command + sizeof(zNprobe) - 1, 1, INT_MAX, &nprobe) != + SQLITE_OK) { + vtab_set_error(&p->base, "nprobe must be >= 1"); + return SQLITE_ERROR; + } + for (int i = 0; i < p->numVectorColumns; i++) { + if (p->vector_columns[i].index_type == VEC0_INDEX_TYPE_IVF) { + p->vector_columns[i].ivf.nprobe = (int)nprobe; + } + } + return SQLITE_OK; + } + + size_t nCompute = sizeof(zCompute) - 1; + if (strncmp(command, zCompute, nCompute) == 0 && + (command[nCompute] == '\0' || command[nCompute] == ':')) { + int nlist = 0; + int maxIter = VEC0_IVF_KMEANS_MAX_ITER; + u32 seed = VEC0_IVF_KMEANS_DEFAULT_SEED; + if (command[nCompute] == ':') { + rc = ivf_compute_options(p, command + nCompute + 1, &nlist, &maxIter, + &seed); + if (rc != SQLITE_OK) { + return rc; + } + } + for (int i = 0; i < p->numVectorColumns; i++) { + if (p->vector_columns[i].index_type != VEC0_INDEX_TYPE_IVF) { + continue; + } + rc = ivf_compute_centroids(p, i, nlist, maxIter, seed); + if (rc != SQLITE_OK) { + return rc; + } + } + return SQLITE_OK; + } + + if (strncmp(command, zSetCentroid, sizeof(zSetCentroid) - 1) == 0) { + i64 centroidId; + if (ivf_command_int(command + sizeof(zSetCentroid) - 1, 0, INT_MAX, + ¢roidId) != SQLITE_OK) { + vtab_set_error(&p->base, "set-centroid needs a centroid id from 0 to %d", + INT_MAX); + return SQLITE_ERROR; + } + int set = 0; + for (int u = 0; u < vec0_num_defined_user_columns(p); u++) { + if (p->user_column_kinds[u] != SQLITE_VEC0_USER_COLUMN_KIND_VECTOR) { + continue; + } + int i = p->user_column_idxs[u]; + struct VectorColumnDefinition *col = &p->vector_columns[i]; + int argvIdx = 2 + VEC0_COLUMN_USERN_START + u; + if (col->index_type != VEC0_INDEX_TYPE_IVF || argvIdx >= argc || + sqlite3_value_type(argv[argvIdx]) == SQLITE_NULL) { + continue; + } + void *vector; + size_t dimensions; + enum VectorElementType elementType; + vector_cleanup cleanup; + char *zError; + rc = vector_from_value(argv[argvIdx], &vector, &dimensions, &elementType, + &cleanup, &zError); + if (rc != SQLITE_OK) { + vtab_set_error(&p->base, "set-centroid vector for \"%.*s\": %z", + col->name_length, col->name, zError); + return SQLITE_ERROR; + } + if (elementType != SQLITE_VEC_ELEMENT_TYPE_FLOAT32 || + dimensions != col->dimensions) { + cleanup(vector); + vtab_set_error(&p->base, + "set-centroid vector for \"%.*s\" must be a float32 " + "vector of %lld dimensions", + col->name_length, col->name, (i64)col->dimensions); + return SQLITE_ERROR; + } + rc = ivf_set_centroid(p, i, centroidId, vector); + cleanup(vector); + if (rc != SQLITE_OK) { + return rc; + } + set++; + } + if (!set) { + vtab_set_error(&p->base, + "set-centroid needs a vector for an IVF-indexed column"); + return SQLITE_ERROR; + } + return SQLITE_OK; + } + + int (*apply)(vec0_vtab *, int) = NULL; + if (strcmp(command, "assign-vectors") == 0) { + apply = ivf_assign_vectors; + } else if (strcmp(command, "clear-centroids") == 0) { + apply = ivf_clear_centroids; + } else { + return SQLITE_EMPTY; + } + for (int i = 0; i < p->numVectorColumns; i++) { + if (p->vector_columns[i].index_type != VEC0_INDEX_TYPE_IVF) { + continue; + } + rc = apply(p, i); + if (rc != SQLITE_OK) { + return rc; + } + } + return SQLITE_OK; +} + +#endif + +int vec0Filter_knn(vec0_cursor *pCur, vec0_vtab *p, int idxNum, + const char *idxStr, int argc, sqlite3_value **argv) { + assert(argc == (int)((strlen(idxStr) - 1) / 4)); + int rc; + struct vec0_query_knn_data *knn_data; + + int vectorColumnIdx = idxNum; + struct VectorColumnDefinition *vector_column = + &p->vector_columns[vectorColumnIdx]; + + struct Array *arrayRowidsIn = NULL; + sqlite3_stmt *stmtChunks = NULL; + void *queryVector; + size_t dimensions; + enum VectorElementType elementType; + vector_cleanup queryVectorCleanup = vector_cleanup_noop; + char *pzError; + knn_data = sqlite3_malloc(sizeof(*knn_data)); + if (!knn_data) { + return SQLITE_NOMEM; + } + memset(knn_data, 0, sizeof(*knn_data)); + // array of `struct Vec0MetadataIn`, IF there are any `xxx in (...)` metadata + // constraints + struct Array *aMetadataIn = NULL; + + int query_idx = -1; + int k_idx = -1; + int rowid_in_idx = -1; + int rowid_eq_idx = -1; + int mmr_lambda_idx = -1; + for (int i = 0; i < argc; i++) { + if (idxStr[1 + (i * 4)] == VEC0_IDXSTR_KIND_KNN_MATCH) { + query_idx = i; + } + if (idxStr[1 + (i * 4)] == VEC0_IDXSTR_KIND_KNN_K) { + k_idx = i; + } + if (idxStr[1 + (i * 4)] == VEC0_IDXSTR_KIND_KNN_ROWID_IN) { + rowid_in_idx = i; + } + if (idxStr[1 + (i * 4)] == VEC0_IDXSTR_KIND_KNN_ROWID_EQ) { + rowid_eq_idx = i; + } + if (idxStr[1 + (i * 4)] == VEC0_IDXSTR_KIND_KNN_MMR_LAMBDA) { + mmr_lambda_idx = i; + } + } + assert(query_idx >= 0); + assert(k_idx >= 0); // make sure the query vector matches the vector column (type dimensions etc.) rc = vector_from_value(argv[query_idx], &queryVector, &dimensions, @@ -9245,21 +14661,53 @@ int vec0Filter_knn(vec0_cursor *pCur, vec0_vtab *p, int idxNum, } #endif - rc = vec0_chunks_iter(p, idxStr, argc, argv, &stmtChunks); - if (rc != SQLITE_OK) { - // IMP: V06942_23781 - vtab_set_error(&p->base, "Error preparing stmtChunk: %s", - sqlite3_errmsg(p->db)); - goto cleanup; - } +#if SQLITE_VEC_ENABLE_DISKANN + // a diskann() column keeps no chunks to scan + if (vector_column->index_type != VEC0_INDEX_TYPE_DISKANN) +#endif +#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE + // an ivf() column's query reads its cells + if (vector_column->index_type != VEC0_INDEX_TYPE_IVF) +#endif + { + rc = vec0_chunks_iter(p, idxStr, argc, argv, &stmtChunks); + if (rc != SQLITE_OK) { + // IMP: V06942_23781 + vtab_set_error(&p->base, "Error preparing stmtChunk: %s", + sqlite3_errmsg(p->db)); + goto cleanup; + } + } i64 *topk_rowids = NULL; f32 *topk_distances = NULL; i64 k_used = 0; - rc = vec0Filter_knn_chunks_iter(p, stmtChunks, vector_column, vectorColumnIdx, - arrayRowidsIn, aMetadataIn, idxStr, argc, - argv, queryVector, k, &topk_rowids, - &topk_distances, &k_used); +#if SQLITE_VEC_ENABLE_DISKANN + if (vector_column->index_type == VEC0_INDEX_TYPE_DISKANN) { + rc = diskann_knn(p, vector_column, vectorColumnIdx, arrayRowidsIn, idxStr, + argc, argv, queryVector, k, &topk_rowids, &topk_distances, + &k_used); + } else +#endif +#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE + if (vector_column->index_type == VEC0_INDEX_TYPE_IVF) { + rc = ivf_knn(p, vector_column, vectorColumnIdx, arrayRowidsIn, idxStr, argc, + argv, queryVector, k, &topk_rowids, &topk_distances, &k_used); + } else +#endif +#if SQLITE_VEC_ENABLE_RESCORE + if (vector_column->index_type == VEC0_INDEX_TYPE_RESCORE) { + rc = rescore_knn(p, stmtChunks, vector_column, vectorColumnIdx, + arrayRowidsIn, idxStr, argc, argv, queryVector, k, + &topk_rowids, &topk_distances, &k_used); + } else +#endif + { + rc = vec0Filter_knn_chunks_iter(p, stmtChunks, vector_column, + vectorColumnIdx, arrayRowidsIn, aMetadataIn, + idxStr, argc, argv, queryVector, k, + &topk_rowids, &topk_distances, &k_used); + } if (rc != SQLITE_OK) { goto cleanup; } @@ -9652,8 +15100,13 @@ static int vec0Column_fullscan(vec0_vtab *pVtab, vec0_cursor *pCur, void *v; int sz; int vector_idx = vec0_column_idx_to_vector_idx(pVtab, i); - int rc = vec0_fullscan_read_vector(pVtab, pCur->fullscan_data, vector_idx, - &v, &sz); + // Only a flat column keeps its vectors in _vector_chunks; the other index + // types keep them in tables of their own, read per row. + int rc = + pVtab->vector_columns[vector_idx].index_type == VEC0_INDEX_TYPE_FLAT + ? vec0_fullscan_read_vector(pVtab, pCur->fullscan_data, vector_idx, + &v, &sz) + : vec0_get_vector_data(pVtab, rowid, vector_idx, &v, &sz); if (rc != SQLITE_OK) { return rc; } @@ -10206,6 +15659,10 @@ int vec0Update_InsertWriteFinalStep(vec0_vtab *p, i64 chunk_rowid, // Go insert the vector data into the vector chunk shadow tables for (int i = 0; i < p->numVectorColumns; i++) { + // a column indexed by anything but flat() keeps its vectors elsewhere + if (p->vector_columns[i].index_type != VEC0_INDEX_TYPE_FLAT) { + continue; + } sqlite3_blob *blobVectors; rc = sqlite3_blob_open(p->db, p->schemaName, p->shadowVectorChunksNames[i], "vectors", chunk_rowid, 1, &blobVectors); @@ -10727,23 +16184,58 @@ int vec0Update_Insert(sqlite3_vtab *pVTab, int argc, sqlite3_value **argv, goto cleanup; } - // Step #2: Find the next "available" position in the _chunks table for this - // row. - rc = vec0Update_InsertNextAvailableStep(p, partitionKeyValues, &chunk_rowid, - &chunk_offset, &blobChunksValidity, - &bufferChunksValidity); - if (rc != SQLITE_OK) { - goto cleanup; + // A table whose every vector column is indexed by diskann() keeps no chunks, + // and its rows take no chunk position. + if (!vec0_all_columns_diskann(p)) { + // Step #2: Find the next "available" position in the _chunks table for + // this row. + rc = vec0Update_InsertNextAvailableStep(p, partitionKeyValues, &chunk_rowid, + &chunk_offset, &blobChunksValidity, + &bufferChunksValidity); + if (rc != SQLITE_OK) { + goto cleanup; + } + + // Step #3: With the next available chunk position, write out all the + // vectors to their specified location. + rc = vec0Update_InsertWriteFinalStep(p, chunk_rowid, chunk_offset, rowid, + vectorDatas, blobChunksValidity, + bufferChunksValidity); + if (rc != SQLITE_OK) { + goto cleanup; + } + +#if SQLITE_VEC_ENABLE_RESCORE + rc = rescore_on_insert(p, chunk_rowid, chunk_offset, rowid, vectorDatas); + if (rc != SQLITE_OK) { + goto cleanup; + } +#endif } - // Step #3: With the next available chunk position, write out all the vectors - // to their specified location. - rc = vec0Update_InsertWriteFinalStep(p, chunk_rowid, chunk_offset, rowid, - vectorDatas, blobChunksValidity, - bufferChunksValidity); - if (rc != SQLITE_OK) { - goto cleanup; +#if SQLITE_VEC_ENABLE_DISKANN + for (int i = 0; i < p->numVectorColumns; i++) { + if (p->vector_columns[i].index_type != VEC0_INDEX_TYPE_DISKANN) { + continue; + } + rc = diskann_insert(p, i, rowid, vectorDatas[i]); + if (rc != SQLITE_OK) { + goto cleanup; + } + } +#endif + +#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE + for (int i = 0; i < p->numVectorColumns; i++) { + if (p->vector_columns[i].index_type != VEC0_INDEX_TYPE_IVF) { + continue; + } + rc = ivf_insert(p, i, rowid, vectorDatas[i]); + if (rc != SQLITE_OK) { + goto cleanup; + } } +#endif if (p->numAuxiliaryColumns > 0) { sqlite3_stmt *stmt; @@ -10982,6 +16474,10 @@ int vec0Update_Delete_ClearRowid(vec0_vtab *p, i64 chunk_id, i64 chunk_offset) { int vec0Update_Delete_ClearVectors(vec0_vtab *p, i64 chunk_id, i64 chunk_offset) { for (int i = 0; i < p->numVectorColumns; i++) { + // a column indexed by anything but flat() keeps its vectors elsewhere + if (p->vector_columns[i].index_type != VEC0_INDEX_TYPE_FLAT) { + continue; + } int rc; sqlite3_blob *blobVectors = NULL; @@ -11146,8 +16642,8 @@ int vec0Update_Delete_ClearMetadata(vec0_vtab *p, int metadata_idx, i64 rowid, } // Once a DELETE has cleared the last validity bit of its chunk, delete the -// chunk's rows from _chunks, _vector_chunksNN and _metadatachunksNN, so their -// pages are free without an optimize. +// chunk's rows from _chunks, _vector_chunksNN, _rescore_chunksNN and +// _metadatachunksNN, so their pages are free without an optimize. // https://github.com/asg017/sqlite-vec/commit/cb147c8 int vec0Update_Delete_DeleteChunk(vec0_vtab *p, i64 chunk_id) { int rc; @@ -11173,6 +16669,10 @@ int vec0Update_Delete_DeleteChunk(vec0_vtab *p, i64 chunk_id) { } for (int i = 0; i < p->numVectorColumns; i++) { + // a column indexed by anything but flat() keeps its vectors elsewhere + if (p->vector_columns[i].index_type != VEC0_INDEX_TYPE_FLAT) { + continue; + } zSql = sqlite3_mprintf("DELETE FROM " VEC0_SHADOW_VECTOR_N_NAME " WHERE rowid = ?", p->schemaName, p->tableName, i); @@ -11192,6 +16692,13 @@ int vec0Update_Delete_DeleteChunk(vec0_vtab *p, i64 chunk_id) { } } +#if SQLITE_VEC_ENABLE_RESCORE + rc = rescore_delete_chunk(p, chunk_id); + if (rc != SQLITE_OK) { + return rc; + } +#endif + for (int i = 0; i < p->numMetadataColumns; i++) { zSql = sqlite3_mprintf("DELETE FROM " VEC0_SHADOW_METADATA_N_NAME " WHERE rowid = ?", @@ -11243,24 +16750,64 @@ int vec0Update_Delete(sqlite3_vtab *pVTab, sqlite3_value *idValue) { return rc; } - int chunkEmpty; - rc = vec0Update_Delete_ClearValidity(p, chunk_id, chunk_offset, &chunkEmpty); - if (rc != SQLITE_OK) { - return rc; +#if SQLITE_VEC_ENABLE_DISKANN + // 1b. take the row out of the graph of each column indexed by diskann() + for (int i = 0; i < p->numVectorColumns; i++) { + if (p->vector_columns[i].index_type != VEC0_INDEX_TYPE_DISKANN) { + continue; + } + rc = diskann_delete(p, i, rowid); + if (rc != SQLITE_OK) { + return rc; + } } +#endif - // 3. zero out rowid in chunks.rowids - // https://github.com/asg017/sqlite-vec/issues/54 - rc = vec0Update_Delete_ClearRowid(p, chunk_id, chunk_offset); - if (rc != SQLITE_OK) { - return rc; +#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE + // 1c. take the row out of the cells of each column indexed by ivf() + for (int i = 0; i < p->numVectorColumns; i++) { + if (p->vector_columns[i].index_type != VEC0_INDEX_TYPE_IVF) { + continue; + } + rc = ivf_delete(p, i, rowid); + if (rc != SQLITE_OK) { + return rc; + } } +#endif - // 4. zero out any data in vector chunks tables - // https://github.com/asg017/sqlite-vec/issues/54 - rc = vec0Update_Delete_ClearVectors(p, chunk_id, chunk_offset); - if (rc != SQLITE_OK) { - return rc; + // a table whose every vector column is indexed by diskann() keeps no chunks + int hasChunks = !vec0_all_columns_diskann(p); + int chunkEmpty = 0; + if (hasChunks) { + rc = + vec0Update_Delete_ClearValidity(p, chunk_id, chunk_offset, &chunkEmpty); + if (rc != SQLITE_OK) { + return rc; + } + + // 3. zero out rowid in chunks.rowids + // https://github.com/asg017/sqlite-vec/issues/54 + rc = vec0Update_Delete_ClearRowid(p, chunk_id, chunk_offset); + if (rc != SQLITE_OK) { + return rc; + } + + // 4. zero out any data in vector chunks tables + // https://github.com/asg017/sqlite-vec/issues/54 + rc = vec0Update_Delete_ClearVectors(p, chunk_id, chunk_offset); + if (rc != SQLITE_OK) { + return rc; + } + +#if SQLITE_VEC_ENABLE_RESCORE + // 4b. zero out the quantized vectors in rescore chunks, and delete the + // row's rescore vectors + rc = rescore_on_delete(p, chunk_id, chunk_offset, rowid); + if (rc != SQLITE_OK) { + return rc; + } +#endif } // 5. delete from _rowids table @@ -11319,7 +16866,8 @@ int vec0Update_UpdateAuxColumn(vec0_vtab *p, int auxiliary_column_idx, } int vec0Update_UpdateVectorColumn(vec0_vtab *p, i64 chunk_id, i64 chunk_offset, - int i, sqlite3_value *valueVector) { + int i, sqlite3_value *valueVector, + i64 rowid) { int rc; sqlite3_blob *blobVectors = NULL; @@ -11364,6 +16912,40 @@ int vec0Update_UpdateVectorColumn(vec0_vtab *p, i64 chunk_id, i64 chunk_offset, goto cleanup; } +#if SQLITE_VEC_ENABLE_RESCORE + // a rescore() column keeps the vector quantized in the row's slot of + // _rescore_chunksNN, and as is in its row of _rescore_vectorsNN + if (p->vector_columns[i].index_type == VEC0_INDEX_TYPE_RESCORE) { + rc = rescore_write_quantized(p, i, chunk_id, chunk_offset, vector); + if (rc != SQLITE_OK) { + goto cleanup; + } + char *zSql = + sqlite3_mprintf("UPDATE \"%w\".\"%w\" SET vector = ? WHERE rowid = ?", + p->schemaName, p->shadowRescoreVectorsNames[i]); + if (!zSql) { + rc = SQLITE_NOMEM; + goto cleanup; + } + sqlite3_stmt *stmt; + rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); + sqlite3_free(zSql); + if (rc != SQLITE_OK) { + goto cleanup; + } + sqlite3_bind_blob(stmt, 1, vector, + vector_column_byte_size(p->vector_columns[i]), + SQLITE_TRANSIENT); + sqlite3_bind_int64(stmt, 2, rowid); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + rc = rc == SQLITE_DONE ? SQLITE_OK : vec0_error_rc(rc); + goto cleanup; + } +#else + UNUSED_PARAMETER(rowid); +#endif + rc = sqlite3_blob_open(p->db, p->schemaName, p->shadowVectorChunksNames[i], "vectors", chunk_id, 1, &blobVectors); if (rc != SQLITE_OK) { @@ -11514,8 +17096,33 @@ int vec0Update_Update(sqlite3_vtab *pVTab, int argc, sqlite3_value **argv) { continue; } +#if SQLITE_VEC_ENABLE_DISKANN + // the graph's edges were chosen for the vector the row has + // https://github.com/asg017/sqlite-vec/commit/9df59b4 + if (p->vector_columns[vector_idx].index_type == VEC0_INDEX_TYPE_DISKANN) { + vtab_set_error(&p->base, + "UPDATE on vector column \"%.*s\" is not supported for " + "DiskANN indexes.", + p->vector_columns[vector_idx].name_length, + p->vector_columns[vector_idx].name); + return SQLITE_ERROR; + } +#endif +#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE + // the row's cell belongs to the centroid nearest the vector it has + // https://github.com/asg017/sqlite-vec/commit/9df59b4 + if (p->vector_columns[vector_idx].index_type == VEC0_INDEX_TYPE_IVF) { + vtab_set_error(&p->base, + "UPDATE on vector column \"%.*s\" is not supported for " + "IVF indexes.", + p->vector_columns[vector_idx].name_length, + p->vector_columns[vector_idx].name); + return SQLITE_ERROR; + } +#endif + rc = vec0Update_UpdateVectorColumn(p, chunk_id, chunk_offset, vector_idx, - valueVector); + valueVector, rowid); if (rc != SQLITE_OK) { return vec0_error_rc(rc); } @@ -11666,6 +17273,17 @@ done: { } } +// flat() and rescore() keep a column's vectors in the chunks, at its row's +// slot, which the optimize moves; diskann() and ivf() keep them by rowid +static int vec0_index_in_chunks(enum Vec0IndexType index_type) { +#if SQLITE_VEC_ENABLE_RESCORE + if (index_type == VEC0_INDEX_TYPE_RESCORE) { + return 1; + } +#endif + return index_type == VEC0_INDEX_TYPE_FLAT; +} + int vec0Update_SpecialInsert_Optimize(vec0_vtab *p) { sqlite3_stmt *stmt = NULL, *partition_key_stmt = NULL; int rc; @@ -11789,6 +17407,9 @@ int vec0Update_SpecialInsert_Optimize(vec0_vtab *p) { } // get the vector data from all vector columns of a row for (int i = 0; i < p->numVectorColumns; i++) { + if (!vec0_index_in_chunks(p->vector_columns[i].index_type)) { + continue; + } rc = vec0_get_vector_data(p, rowid, i, &vectorDatas[i], NULL); if (rc != SQLITE_OK) { goto cleanup; @@ -11810,6 +17431,20 @@ int vec0Update_SpecialInsert_Optimize(vec0_vtab *p) { if (rc != SQLITE_OK) { goto cleanup; } +#if SQLITE_VEC_ENABLE_RESCORE + // the row keeps its rowid, so its rows of _rescore_vectorsNN stay, and + // only its quantized vectors move to the new slot + for (int i = 0; i < p->numVectorColumns; i++) { + if (p->vector_columns[i].index_type != VEC0_INDEX_TYPE_RESCORE) { + continue; + } + rc = rescore_write_quantized(p, i, new_chunk_id, new_chunk_offset, + vectorDatas[i]); + if (rc != SQLITE_OK) { + goto cleanup; + } + } +#endif sqlite3_free((void *)bufferChunksValidity); bufferChunksValidity = NULL; if (sqlite3_blob_close(blobChunksValidity) != SQLITE_OK) { @@ -11875,9 +17510,20 @@ int vec0Update_SpecialInsert_Optimize(vec0_vtab *p) { // 4) clean up old vector chunks for (int i = 0; i < p->numVectorColumns; i++) { - zSql = sqlite3_mprintf("DELETE FROM " VEC0_SHADOW_VECTOR_N_NAME - " WHERE rowid <= ?", - p->schemaName, p->tableName, i); + if (!vec0_index_in_chunks(p->vector_columns[i].index_type)) { + continue; + } +#if SQLITE_VEC_ENABLE_RESCORE + if (p->vector_columns[i].index_type == VEC0_INDEX_TYPE_RESCORE) { + zSql = sqlite3_mprintf("DELETE FROM \"%w\".\"%w\" WHERE rowid <= ?", + p->schemaName, p->shadowRescoreChunksNames[i]); + } else +#endif + { + zSql = sqlite3_mprintf("DELETE FROM " VEC0_SHADOW_VECTOR_N_NAME + " WHERE rowid <= ?", + p->schemaName, p->tableName, i); + } rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, 0); sqlite3_free((void *)zSql); if (rc != SQLITE_OK) { @@ -11929,7 +17575,8 @@ int vec0Update_SpecialInsert_Optimize(vec0_vtab *p) { return rc; } -int vec0Update_SpecialInsert(sqlite3_vtab *pVTab, sqlite3_value *pVal) { +int vec0Update_SpecialInsert(sqlite3_vtab *pVTab, sqlite3_value *pVal, int argc, + sqlite3_value **argv) { vec0_vtab *p = (vec0_vtab *)pVTab; const char *cmd = (const char *)sqlite3_value_text(pVal); @@ -11941,6 +17588,28 @@ int vec0Update_SpecialInsert(sqlite3_vtab *pVTab, sqlite3_value *pVal) { if (n_bytes == 8 && sqlite3_strnicmp(cmd, "optimize", 8) == 0) { return vec0Update_SpecialInsert_Optimize(p); } +#if SQLITE_VEC_ENABLE_RESCORE + int rc = rescore_handle_command(p, cmd); + if (rc != SQLITE_EMPTY) { + return rc; + } +#endif +#if SQLITE_VEC_ENABLE_DISKANN + int diskannRc = diskann_handle_command(p, cmd); + if (diskannRc != SQLITE_EMPTY) { + return diskannRc; + } +#endif +#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE + int ivfRc = ivf_handle_command(p, cmd, argc, argv); + if (ivfRc != SQLITE_EMPTY) { + return ivfRc; + } +#else + UNUSED_PARAMETER(argc); + UNUSED_PARAMETER(argv); +#endif + vtab_set_error(pVTab, "unknown vec0 command: '%s'", cmd); return SQLITE_ERROR; } @@ -11955,7 +17624,8 @@ static int vec0Update(sqlite3_vtab *pVTab, int argc, sqlite3_value **argv, return vec0_interrupt_result( pVTab, vec0Update_SpecialInsert( - pVTab, argv[2 + vec0_column_table_name_idx((vec0_vtab *)pVTab)])); + pVTab, argv[2 + vec0_column_table_name_idx((vec0_vtab *)pVTab)], + argc, argv)); } // DELETE operation if (argc == 1 && sqlite3_value_type(argv[0]) != SQLITE_NULL) { @@ -12025,13 +17695,27 @@ static int vec0ShadowName(const char *zName) { return 1; } - // "vector_chunksNN", one per vector column - static const char zVectorChunks[] = "vector_chunks"; - size_t n = sizeof(zVectorChunks) - 1; - if (sqlite3_strnicmp(zName, zVectorChunks, n) == 0 && zName[n] >= '0' && - zName[n] <= '9' && zName[n + 1] >= '0' && zName[n + 1] <= '9' && - zName[n + 2] == '\0') { - return 1; + // the tables of each vector column: one of these names followed by the + // column's two-digit number, ie "vector_chunks00" + static const char *azPerColumn[] = { + "vector_chunks", +#if SQLITE_VEC_ENABLE_RESCORE + "rescore_chunks", "rescore_vectors", +#endif +#if SQLITE_VEC_ENABLE_DISKANN + "vectors", "diskann_nodes", "diskann_buffer", +#endif +#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE + "ivf_centroids", "ivf_cells", "ivf_rowid_map", "ivf_vectors", +#endif + }; + for (size_t i = 0; i < countof(azPerColumn); i++) { + size_t n = strlen(azPerColumn[i]); + if (sqlite3_strnicmp(zName, azPerColumn[i], n) == 0 && zName[n] >= '0' && + zName[n] <= '9' && zName[n + 1] >= '0' && zName[n + 1] <= '9' && + zName[n + 2] == '\0') { + return 1; + } } return 0; } @@ -12040,29 +17724,11 @@ static int vec0Begin(sqlite3_vtab *pVTab) { UNUSED_PARAMETER(pVTab); return SQLITE_OK; } +// A statement left prepared past the commit keeps sqlite3_close() (not _v2) +// from closing the connection, so the commit finalizes every one the table +// keeps. https://github.com/asg017/sqlite-vec/commit/fe94171 static int vec0Sync(sqlite3_vtab *pVTab) { - UNUSED_PARAMETER(pVTab); - vec0_vtab *p = (vec0_vtab *)pVTab; - if (p->stmtLatestChunk) { - sqlite3_finalize(p->stmtLatestChunk); - p->stmtLatestChunk = NULL; - } - if (p->stmtRowidsInsertRowid) { - sqlite3_finalize(p->stmtRowidsInsertRowid); - p->stmtRowidsInsertRowid = NULL; - } - if (p->stmtRowidsInsertId) { - sqlite3_finalize(p->stmtRowidsInsertId); - p->stmtRowidsInsertId = NULL; - } - if (p->stmtRowidsUpdatePosition) { - sqlite3_finalize(p->stmtRowidsUpdatePosition); - p->stmtRowidsUpdatePosition = NULL; - } - if (p->stmtRowidsGetChunkPosition) { - sqlite3_finalize(p->stmtRowidsGetChunkPosition); - p->stmtRowidsGetChunkPosition = NULL; - } + vec0_free_resources((vec0_vtab *)pVTab); return SQLITE_OK; } static int vec0Commit(sqlite3_vtab *pVTab) { @@ -12119,23 +17785,53 @@ static int vec0Rename(sqlite3_vtab *pVTab, const char *zName) { sqlite3_finalize(stmt); for (int i = 0; i < p->numVectorColumns; i++) { - char *newShadowVectorChunksName = - sqlite3_mprintf("%s_vector_chunks%02d", zName, i); - if (!newShadowVectorChunksName) { - return SQLITE_NOMEM; - } - zSql = sqlite3_mprintf("ALTER TABLE \"%w\".\"%w\" RENAME TO \"%w\"", - p->schemaName, p->shadowVectorChunksNames[i], - newShadowVectorChunksName); - sqlite3_free(newShadowVectorChunksName); - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, 0); - sqlite3_free((void *)zSql); - if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { - rc = SQLITE_ERROR; - vtab_set_error(pVTab, "could not rename vector_chunks shadow table"); - goto done; + struct { + const char *zOld; + const char *zSuffix; + } aShadow[] = { + {p->vector_columns[i].index_type == VEC0_INDEX_TYPE_FLAT + ? p->shadowVectorChunksNames[i] + : NULL, + "vector_chunks"}, +#if SQLITE_VEC_ENABLE_RESCORE + {p->shadowRescoreChunksNames[i], "rescore_chunks"}, + {p->shadowRescoreVectorsNames[i], "rescore_vectors"}, +#endif +#if SQLITE_VEC_ENABLE_DISKANN + {p->shadowDiskannVectorsNames[i], "vectors"}, + {p->shadowDiskannNodesNames[i], "diskann_nodes"}, + {p->shadowDiskannBufferNames[i], "diskann_buffer"}, +#endif +#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE + {p->shadowIvfCentroidsNames[i], "ivf_centroids"}, + {p->shadowIvfCellsNames[i], "ivf_cells"}, + {p->shadowIvfRowidMapNames[i], "ivf_rowid_map"}, + {p->vector_columns[i].ivf.quantizer != VEC0_IVF_QUANTIZER_NONE + ? p->shadowIvfVectorsNames[i] + : NULL, + "ivf_vectors"}, +#endif + }; + for (size_t j = 0; j < countof(aShadow); j++) { + if (!aShadow[j].zOld) { + continue; + } + zSql = sqlite3_mprintf( + "ALTER TABLE \"%w\".\"%w\" RENAME TO \"%w_%s%02d\"", p->schemaName, + aShadow[j].zOld, zName, aShadow[j].zSuffix, i); + if (!zSql) { + return SQLITE_NOMEM; + } + rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, 0); + sqlite3_free((void *)zSql); + if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { + rc = SQLITE_ERROR; + vtab_set_error(pVTab, "could not rename %s shadow table", + aShadow[j].zSuffix); + goto done; + } + sqlite3_finalize(stmt); } - sqlite3_finalize(stmt); } if (p->numAuxiliaryColumns > 0) { @@ -12912,8 +18608,28 @@ static sqlite3_module vec_static_blob_entriesModule = { #define SQLITE_VEC_DEBUG_BUILD_NEON "" #endif +#if SQLITE_VEC_ENABLE_RESCORE +#define SQLITE_VEC_DEBUG_BUILD_RESCORE "rescore" +#else +#define SQLITE_VEC_DEBUG_BUILD_RESCORE "" +#endif + +#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE +#define SQLITE_VEC_DEBUG_BUILD_IVF "ivf" +#else +#define SQLITE_VEC_DEBUG_BUILD_IVF "" +#endif + +#if SQLITE_VEC_ENABLE_DISKANN +#define SQLITE_VEC_DEBUG_BUILD_DISKANN "diskann" +#else +#define SQLITE_VEC_DEBUG_BUILD_DISKANN "" +#endif + #define SQLITE_VEC_DEBUG_BUILD \ - SQLITE_VEC_DEBUG_BUILD_AVX " " SQLITE_VEC_DEBUG_BUILD_NEON + SQLITE_VEC_DEBUG_BUILD_AVX \ + " " SQLITE_VEC_DEBUG_BUILD_NEON " " SQLITE_VEC_DEBUG_BUILD_RESCORE \ + " " SQLITE_VEC_DEBUG_BUILD_IVF " " SQLITE_VEC_DEBUG_BUILD_DISKANN #define SQLITE_VEC_DEBUG_STRING \ "Version: " SQLITE_VEC_VERSION "\n" \ diff --git a/tests/__snapshots__/test-auxiliary.ambr b/tests/__snapshots__/test-auxiliary.ambr index 96fb1876..302cf4cd 100644 --- a/tests/__snapshots__/test-auxiliary.ambr +++ b/tests/__snapshots__/test-auxiliary.ambr @@ -169,6 +169,200 @@ }), }) # --- +# name: test_diskann_aux_insert_knn[diskann aux select all] + OrderedDict({ + 'sql': 'SELECT rowid, label FROM t ORDER BY rowid', + 'rows': list([ + OrderedDict({ + 'rowid': 1, + 'label': 'red', + }), + OrderedDict({ + 'rowid': 2, + 'label': 'green', + }), + OrderedDict({ + 'rowid': 3, + 'label': 'blue', + }), + ]), + }) +# --- +# name: test_diskann_aux_insert_knn[diskann aux shadow contents] + dict({ + 't_auxiliary': OrderedDict({ + 'sql': 'select * from t_auxiliary', + 'rows': list([ + OrderedDict({ + 'rowid': 1, + 'value00': 'red', + }), + OrderedDict({ + 'rowid': 2, + 'value00': 'green', + }), + OrderedDict({ + 'rowid': 3, + 'value00': 'blue', + }), + ]), + }), + 't_chunks': OrderedDict({ + 'sql': 'select * from t_chunks', + 'rows': list([ + ]), + }), + 't_diskann_buffer00': OrderedDict({ + 'sql': 'select * from t_diskann_buffer00', + 'rows': list([ + ]), + }), + 't_diskann_nodes00': OrderedDict({ + 'sql': 'select * from t_diskann_nodes00', + 'rows': list([ + OrderedDict({ + 'rowid': 1, + 'neighbors_validity': b'\x03', + 'neighbor_ids': b'\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + 'neighbor_quantized_vectors': b'\x02\x04\x00\x00\x00\x00\x00\x00', + }), + OrderedDict({ + 'rowid': 2, + 'neighbors_validity': b'\x03', + 'neighbor_ids': b'\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + 'neighbor_quantized_vectors': b'\x01\x04\x00\x00\x00\x00\x00\x00', + }), + OrderedDict({ + 'rowid': 3, + 'neighbors_validity': b'\x03', + 'neighbor_ids': b'\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + 'neighbor_quantized_vectors': b'\x01\x02\x00\x00\x00\x00\x00\x00', + }), + ]), + }), + 't_rowids': OrderedDict({ + 'sql': 'select * from t_rowids', + 'rows': list([ + OrderedDict({ + 'rowid': 1, + 'id': None, + 'chunk_id': None, + 'chunk_offset': None, + }), + OrderedDict({ + 'rowid': 2, + 'id': None, + 'chunk_id': None, + 'chunk_offset': None, + }), + OrderedDict({ + 'rowid': 3, + 'id': None, + 'chunk_id': None, + 'chunk_offset': None, + }), + ]), + }), + 't_vectors00': OrderedDict({ + 'sql': 'select * from t_vectors00', + 'rows': list([ + OrderedDict({ + 'rowid': 1, + 'vector': b'\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + }), + OrderedDict({ + 'rowid': 2, + 'vector': b'\x00\x00\x00\x00\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + }), + OrderedDict({ + 'rowid': 3, + 'vector': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + }), + ]), + }), + }) +# --- +# name: test_diskann_aux_shadow_tables[diskann aux shadow tables] + OrderedDict({ + 'sql': "SELECT name, sql FROM sqlite_master WHERE type='table' AND name LIKE 't_%' ORDER BY name", + 'rows': list([ + OrderedDict({ + 'name': 't_auxiliary', + 'sql': 'CREATE TABLE "t_auxiliary"( rowid integer PRIMARY KEY , value00, value01)', + }), + OrderedDict({ + 'name': 't_chunks', + 'sql': 'CREATE TABLE "t_chunks"(chunk_id INTEGER PRIMARY KEY AUTOINCREMENT,size INTEGER NOT NULL,validity BLOB NOT NULL,rowids BLOB NOT NULL)', + }), + OrderedDict({ + 'name': 't_diskann_buffer00', + 'sql': 'CREATE TABLE "t_diskann_buffer00" (rowid INTEGER PRIMARY KEY, vector BLOB NOT NULL)', + }), + OrderedDict({ + 'name': 't_diskann_nodes00', + 'sql': 'CREATE TABLE "t_diskann_nodes00" (rowid INTEGER PRIMARY KEY, neighbors_validity BLOB NOT NULL, neighbor_ids BLOB NOT NULL, neighbor_quantized_vectors BLOB NOT NULL)', + }), + OrderedDict({ + 'name': 't_info', + 'sql': 'CREATE TABLE "t_info" (key text primary key, value any)', + }), + OrderedDict({ + 'name': 't_rowids', + 'sql': 'CREATE TABLE "t_rowids"(rowid INTEGER PRIMARY KEY AUTOINCREMENT,id,chunk_id INTEGER,chunk_offset INTEGER)', + }), + OrderedDict({ + 'name': 't_vectors00', + 'sql': 'CREATE TABLE "t_vectors00" (rowid INTEGER PRIMARY KEY, vector BLOB NOT NULL)', + }), + ]), + }) +# --- +# name: test_diskann_aux_update_and_delete[diskann aux after update+delete] + OrderedDict({ + 'sql': 'SELECT rowid, label FROM t ORDER BY rowid', + 'rows': list([ + OrderedDict({ + 'rowid': 1, + 'label': 'item-1', + }), + OrderedDict({ + 'rowid': 2, + 'label': 'UPDATED', + }), + OrderedDict({ + 'rowid': 4, + 'label': 'item-4', + }), + OrderedDict({ + 'rowid': 5, + 'label': 'item-5', + }), + ]), + }) +# --- +# name: test_diskann_aux_update_and_delete[diskann aux shadow after update+delete] + OrderedDict({ + 'sql': 'SELECT rowid, value00 FROM t_auxiliary ORDER BY rowid', + 'rows': list([ + OrderedDict({ + 'rowid': 1, + 'value00': 'item-1', + }), + OrderedDict({ + 'rowid': 2, + 'value00': 'UPDATED', + }), + OrderedDict({ + 'rowid': 4, + 'value00': 'item-4', + }), + OrderedDict({ + 'rowid': 5, + 'value00': 'item-5', + }), + ]), + }) +# --- # name: test_knn OrderedDict({ 'sql': 'select * from v', @@ -471,6 +665,183 @@ }), }) # --- +# name: test_rescore_aux_delete[rescore aux after delete] + OrderedDict({ + 'sql': 'SELECT rowid, label FROM t ORDER BY rowid', + 'rows': list([ + OrderedDict({ + 'rowid': 1, + 'label': 'item-1', + }), + OrderedDict({ + 'rowid': 2, + 'label': 'item-2', + }), + OrderedDict({ + 'rowid': 4, + 'label': 'item-4', + }), + OrderedDict({ + 'rowid': 5, + 'label': 'item-5', + }), + ]), + }) +# --- +# name: test_rescore_aux_delete[rescore aux shadow after delete] + OrderedDict({ + 'sql': 'SELECT rowid, value00 FROM t_auxiliary ORDER BY rowid', + 'rows': list([ + OrderedDict({ + 'rowid': 1, + 'value00': 'item-1', + }), + OrderedDict({ + 'rowid': 2, + 'value00': 'item-2', + }), + OrderedDict({ + 'rowid': 4, + 'value00': 'item-4', + }), + OrderedDict({ + 'rowid': 5, + 'value00': 'item-5', + }), + ]), + }) +# --- +# name: test_rescore_aux_insert_knn[rescore aux select all] + OrderedDict({ + 'sql': 'SELECT rowid, label FROM t ORDER BY rowid', + 'rows': list([ + OrderedDict({ + 'rowid': 1, + 'label': 'alpha', + }), + OrderedDict({ + 'rowid': 2, + 'label': 'beta', + }), + OrderedDict({ + 'rowid': 3, + 'label': 'gamma', + }), + ]), + }) +# --- +# name: test_rescore_aux_insert_knn[rescore aux shadow contents] + dict({ + 't_auxiliary': OrderedDict({ + 'sql': 'select * from t_auxiliary', + 'rows': list([ + OrderedDict({ + 'rowid': 1, + 'value00': 'alpha', + }), + OrderedDict({ + 'rowid': 2, + 'value00': 'beta', + }), + OrderedDict({ + 'rowid': 3, + 'value00': 'gamma', + }), + ]), + }), + 't_chunks': OrderedDict({ + 'sql': 'select * from t_chunks', + 'rows': list([ + OrderedDict({ + 'chunk_id': 1, + 'size': 1024, + 'validity': b'\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + 'rowids': b'\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + }), + ]), + }), + 't_rescore_chunks00': OrderedDict({ + 'sql': 'select * from t_rescore_chunks00', + 'rows': list([ + OrderedDict({ + 'rowid': 1, + 'vectors': b'=\xf3<\xef\xf1H\x85\xa57\x16v\xe6/\x86\x7f\xace\x96\x11|1\x18a\xd8\x15\x1c&\x02z\x9e\xeb\x12\xa4\xd7\xd2i\x89\xc4\x18A>\xa2\x9bT\xcd=\xd9i\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + }), + ]), + }), + 't_rescore_vectors00': OrderedDict({ + 'sql': 'select * from t_rescore_vectors00', + 'rows': list([ + OrderedDict({ + 'rowid': 1, + 'vector': b']\x1c\x8a>\xc4\x9eX\xbf\xceY\xe3=w\x9b\xed?\nQZ?\xdc\x9f@?\x80u\xa4\xbf\x16q\xfa\xbeB\x9b\\?B\x13\x8f?\x80\xd1\xf9\xbf\x10\x0c\xf9\xbb\xf8\x1c\x1e@G\xbd\xe3>\xea\x03[?\xecc\xcf?\x12\x03\xf3\xbf\xa1\xd4\xb2\xbd\x8f\x99\xb8>\x92\xb5M?\xa1\xc9\xbd?/\x90g?\xe1u)\xbf\x94\xe8\x12\xbe\xd6\xba\x16>\'i\xc4>\xc3?\xca>\t\x0e\x9c?\xf0\xa8\xb8\xbf\x1b\x98\x18?\xa8<\xd9??\xdca?\x89\xad\xd0?\x88\xb8<\xbe<\xfa\xab\xbd#\xfc\xf8\xbf\x1e\x90k?\xa0\xec\x1f?a\x1a\xc4?\xc0yU?\xcc\x11\xec\xbe(\xad\xe5\xbd\xfbx\xd0\xbfA\xd3\x16?1\xc5\xf6\xbe\xdcn\'\xbe\x00\xe6\xa0>\t\xd4\x06\xbeD\xfb\xbe?\x1a,\x10\xc0\x80\x8a\x83<\xd2\x8f\x1a\xc0\xf0\xab0\xbf\xfaD\x0b\xbe\x18`\xce\xbf\xa43\x91>\xd0\x13\xea=\x1cpz\xbf\x9ai\x81>\x1d+\xb2\xbd\xb1:\x91\xbe\r\x9e\xf4;|"\xf2\xbfA\x0c\x16@+\x92\t@\x99\x9e\xfb?&\xb9\xa1?_v[\xbf\x98\xb7\x87?\xfe"\xc7>%]#\xbe\xa0\xf2\xd5\xbe\x9e"\x06\xbe\x1dz\xd6>\x84\xa2\x9d?\xd7\xb3\xec\xbf\xbbJ\xbd?\xbd\xebD\xbf\xdd.\xa3\xbe1\xcd\'\xbe\xa2\xf9\xd6\xbfL\xa7I>\xef\x17\x0f<(0n\xbe\xbe\xaf\xdb>\x7fo\xb5<\xcah\xdf>d\x00f\xbe\xb1\x85`\xbf\x95 9?\xd1\xeb\xcd>gk\xb8\xbe\x18\xd3\xfe\xbd1\x80\xdb?\x8b\x86\x03?\x1a\xb7\x9d>\xadM\x1f@\x04\xa0\xca>tc\xfc=\x186\x96?7.\x03\xc0V\xfa\xf8?\xf2\xf2\xa3\xbe1\xa1\xa8\xbf\x06\xb1I\xbfs\xcbT=\xda\xe5}>\xcd@\xca\xbe\x1ee\x1a\xbf\x02H\x14\xbf\x99\xef8\xbeG\xd9\x8a?&\xdc\xff>O\xf8\x9e?\xbd+4>\x9d\xa4\xab=PB\x8c>fs\xac>\x8b\xb4\\?q\xe2S\xbf^\x9a@\xbf\xe7\x7f\xc8\xbf\xbb\x9e\x9f?\xc2\xa0\x07@\xe2mT\xbf@\xf1v\x89n\xbf\xfb\xe2T\xbc\xd4\xd2\xff?,o\xaf?\x0c+\xb6\xbf#\x84|\xbf\x80\xc8\xfd?9\x97\xdb>oa\'\xc0\x8f\xa9\x00>[\xc3\x83\xc0d\xe2\xd2\xbf\xba\xeai>1\x14s>\xe3\x11\x99\xbf\xd9j\x81\xbf\xb3\x1e\xe1\xbcS;)?\x86\x987\xbf\t\xf4\xe4\xbc\xb8f\xa4\xbf\x1c\x83k\xbe|*T\xbf\x00\xd8\xa8?\xc4\x966?2\x14\x14?H\xfe>?\xbd\xbb\x7f=\xcb\x1c\x9f\xbe\xe5\xad\x90?U\x085\xbf\xde{\xb2\xbf\x1f\x03\x10\xbf\x19\xd6J>b\xb9\x97=\xc18z\xbe\xe76\xa7\xbf\xed\x80\x98\xbf\xf5\x12\xb7\xbf\x86(\x9f\xbdaY\x16?\x07j\xb1>\x9ea\x8c\xbd\x91(\xb2\xbf\xe1\xa1\x0c\xbe\xc4_\xd1>\x8c\xad\xf2\xbdc\xab\xf4\xbd\x81<\xc6\xbe}\xa7\xaa\xbfk\xe4\xcb>\xcd89>dk\x81\xbe%\xa4\xb0\xbbAU\x11\xbfG\n\x15\xc0\xb6m\xcb?\xafoq>0\x17\xa5\xbe?j\x81>\xbet\xee;\xc0\xc2\\=S\x81\x8c\xbe2T\xca> \xbe\xe2\xbf1\xd2w?\xed\xfd\x08\xc0\x01\x17\xa0\xbf\x99o\xb1\xbfRX\xb7\xbf\x06f\xca\xbeD\x9e\x92?\x86W\t?\x03G}?\xdd\xbfv\xbdd\xf0\x0c\xbe\xf8\x8a\x1c\xbf\xd8\xc9\x9e\xbfy/\x13>\x802\xdc= 5`\xbf\x00\xf3"\xbf\x99\x92\x01>7 \x06\xc0{\xd7\x8d\xbf\xa5/\x8e\xbf\\\x82u?\x17M\x1e\xbf\xcf\xbbk\xbe\xc3\x84i\xbe\xf1\xa4&\xbfD\xb4\x1a\xc0au\x06\xc0:\xbc\x04\xbf\x0cK\xb2?mdD\xbf\xfa\xa4\x9b?\x89w\xd9>\xde\xb7\x8c?!e\x1a>\xc3\x05-\xbf\x11\xce\xdf\xbe!\xf3\x10?\xab!P?\x96\xbc\x9f?]\x1c\x19?f\x97\x88\xbf\xddRM\xbf,)\x8e>7\x14$>}\x8d\x18@O%\xc0\xbf/\xa5C>`B\xe5\xbd\xb71\x1b?s\x11V?3\xa2F=\x13\xaf\x87\xbf\xe2X\x17?\xa7\xc8\x91\xbf\x19^\x83\xbf\xc6w\xa4?[H\xa1\xbf\x17M2\xbf\xfb\x7f\xd5\xbf', + }), + OrderedDict({ + 'rowid': 3, + 'vector': b'b\xd9\x8b\xbf9\x81\x96\xbe\x83h\xe9?\\\xa4\x89\xbf\x93\xff\xc1\xbf6\xfe\xa8?\xd8\x19\xc1\xbf*\xf06?\xb2\x0c\xaa?2\\P>\xd9\xf0\x81?K~\xb4\xbe\x05\x00\x85?\xcfg\x8c\xbf?\x93\xca>\xf82Q=\x00\x8e\xa5\xbf\xf3:\x15@\xbc\x9c>\xbf}\x13\x90\xbf5z\x17?w17\xbf\xaf\xde(?<\x00]?M\xff\x00?\n7\xa8\xbe\x83kU\xbf-R\x1a=\xa1\xbc\x8c\xbe\xf7.\xb0>\xf1W$?\xe3\xb1\xd8\xbeZ\x89\x19>\x08\x0b\x19\xbf8\xbfK\xbe\t\x12\xcb=P\xd5\x81\xbf8C!\xbf$\xaa\x1b\xbf1\x8f\x8e>\xbb\x1c2\xbe"\x88R\xbekR\x86?\xb3\xfa\xee\xbe\x1aAN\xbf|\xca,\xbf\x0c{z>\x97;\n=Q>4\xbey\x12\x92\xbf[\xa1]\xbe\t\x93\x9c?\xf5\xbcR?\x1cj]\xbf\xa5w\xa8\xbf\xf5\xc1\x1b\xbej\xb25>5\xf48\xbe\x87\xe4-\xbf x\x8f\xbfoC\x01\xbe\xe7:\x16\xbf\x1c\xf1W?\xf6\x1e\xc8\xbf\xe9&\xd5\xbec^\x19@\x19\n\x98?My\xd0>\xa3\xa4\r?\xfc#\xab>\xf7\x1a\x81\xbf\xbb\xe8\x98\xbf\xb0]>\xbff\x92\xc7=\xb3\x16\x86\xbf4\xdc\x9b\xbe\xe2\xd4C\xbfi\xbb/?r\x0fo\xbf\xb8\xd8g>$\x9cW?\xfd\xb0P?\x05\r\xc0\xbeC\x08\xde?Pz\xcb?\x88\xd5\xe9\xbe\xd4\x07\x0c\xbf\x16s\x7f?\xf9=K\xbd\x9378\xbfI\xd6\xbb=\xe7j\x92\xbe\xeb\xfa\x9f?\x9d\x9d\x83\xbe4\xbbK>\xcf\x82\xab\xbfv\x98\x1c?a"Z\xbf\xaf/\x12?\xfe4\xbc>\x84\xed\x91\xbd\xeb\x857\xc0\x90\x89">\x05t\x92?\x1b\x00(>F>V\xbf\x84\x12\x1e>\xcb\xae\xd8?\xc0S+?\x95Z\x1a?\xbe\x93x\xbf>\xfe\'\xbf`\xa4\x8b?\xca\x08\xba\xbe\x89\xc2\n\xbf\xec2\xb2>\x1c\xb3\x04>w\xc0\x95\xbe\x94\xf0r?\xb5\x08\xc4=\x15~\x84>:\xc78\xbfV-\xdb\xbe\xaf\xde\xb2=\xd8\xc8\xe1\xbe\x06\xf9\x14@^\x16\x92>bk\xb1\xbe', + }), + ]), + }), + 't_rowids': OrderedDict({ + 'sql': 'select * from t_rowids', + 'rows': list([ + OrderedDict({ + 'rowid': 1, + 'id': None, + 'chunk_id': 1, + 'chunk_offset': 0, + }), + OrderedDict({ + 'rowid': 2, + 'id': None, + 'chunk_id': 1, + 'chunk_offset': 1, + }), + OrderedDict({ + 'rowid': 3, + 'id': None, + 'chunk_id': 1, + 'chunk_offset': 2, + }), + ]), + }), + }) +# --- +# name: test_rescore_aux_shadow_tables[rescore aux shadow tables] + OrderedDict({ + 'sql': "SELECT name, sql FROM sqlite_master WHERE type='table' AND name LIKE 't_%' ORDER BY name", + 'rows': list([ + OrderedDict({ + 'name': 't_auxiliary', + 'sql': 'CREATE TABLE "t_auxiliary"( rowid integer PRIMARY KEY , value00, value01)', + }), + OrderedDict({ + 'name': 't_chunks', + 'sql': 'CREATE TABLE "t_chunks"(chunk_id INTEGER PRIMARY KEY AUTOINCREMENT,size INTEGER NOT NULL,validity BLOB NOT NULL,rowids BLOB NOT NULL)', + }), + OrderedDict({ + 'name': 't_info', + 'sql': 'CREATE TABLE "t_info" (key text primary key, value any)', + }), + OrderedDict({ + 'name': 't_rescore_chunks00', + 'sql': 'CREATE TABLE "t_rescore_chunks00"(rowid INTEGER PRIMARY KEY, vectors BLOB NOT NULL)', + }), + OrderedDict({ + 'name': 't_rescore_vectors00', + 'sql': 'CREATE TABLE "t_rescore_vectors00"(rowid INTEGER PRIMARY KEY, vector BLOB NOT NULL)', + }), + OrderedDict({ + 'name': 't_rowids', + 'sql': 'CREATE TABLE "t_rowids"(rowid INTEGER PRIMARY KEY AUTOINCREMENT,id,chunk_id INTEGER,chunk_offset INTEGER)', + }), + ]), + }) +# --- # name: test_types OrderedDict({ 'sql': 'select * from v', diff --git a/tests/conftest.py b/tests/conftest.py index db1c5656..c5eae2cf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,6 +17,26 @@ def get_extension_path(): return "dist/vec0" +def _has_build_flag(flag): + db = sqlite3.connect(":memory:") + db.enable_load_extension(True) + db.load_extension(get_extension_path()) + debug = db.execute("SELECT vec_debug()").fetchone()[0] + db.close() + return flag in debug.split("Build flags:")[-1].split() + + +def pytest_collection_modifyitems(config, items): + if _has_build_flag("ivf"): + return + skip_ivf = pytest.mark.skip( + reason="IVF not enabled (compile with -DSQLITE_VEC_EXPERIMENTAL_IVF_ENABLE=1)" + ) + for item in items: + if item.fspath.basename.startswith("test-ivf"): + item.add_marker(skip_ivf) + + @pytest.fixture() def db(): db = sqlite3.connect(":memory:") diff --git a/tests/fuzz/Makefile b/tests/fuzz/Makefile index 23c72491..2045b04c 100644 --- a/tests/fuzz/Makefile +++ b/tests/fuzz/Makefile @@ -70,10 +70,101 @@ $(TARGET_DIR)/vec_mismatch: vec-mismatch.c $(FUZZ_SRCS) | $(TARGET_DIR) $(TARGET_DIR)/vec0_delete_completeness: vec0-delete-completeness.c $(FUZZ_SRCS) | $(TARGET_DIR) $(FUZZ_CC) $(FUZZ_CFLAGS) $(FUZZ_SRCS) $< -o $@ +$(TARGET_DIR)/rescore_operations: rescore-operations.c $(FUZZ_SRCS) | $(TARGET_DIR) + $(FUZZ_CC) $(FUZZ_CFLAGS) $(FUZZ_SRCS) $< -o $@ + +$(TARGET_DIR)/rescore_create: rescore-create.c $(FUZZ_SRCS) | $(TARGET_DIR) + $(FUZZ_CC) $(FUZZ_CFLAGS) $(FUZZ_SRCS) $< -o $@ + +$(TARGET_DIR)/rescore_quantize: rescore-quantize.c $(FUZZ_SRCS) | $(TARGET_DIR) + $(FUZZ_CC) $(FUZZ_CFLAGS) $(FUZZ_SRCS) $< -o $@ + +$(TARGET_DIR)/rescore_shadow_corrupt: rescore-shadow-corrupt.c $(FUZZ_SRCS) | $(TARGET_DIR) + $(FUZZ_CC) $(FUZZ_CFLAGS) $(FUZZ_SRCS) $< -o $@ + +$(TARGET_DIR)/rescore_knn_deep: rescore-knn-deep.c $(FUZZ_SRCS) | $(TARGET_DIR) + $(FUZZ_CC) $(FUZZ_CFLAGS) $(FUZZ_SRCS) $< -o $@ + +$(TARGET_DIR)/rescore_quantize_edge: rescore-quantize-edge.c $(FUZZ_SRCS) | $(TARGET_DIR) + $(FUZZ_CC) $(FUZZ_CFLAGS) $(FUZZ_SRCS) $< -o $@ + +$(TARGET_DIR)/rescore_interleave: rescore-interleave.c $(FUZZ_SRCS) | $(TARGET_DIR) + $(FUZZ_CC) $(FUZZ_CFLAGS) $(FUZZ_SRCS) $< -o $@ + +$(TARGET_DIR)/rescore_optimize: rescore-optimize.c $(FUZZ_SRCS) | $(TARGET_DIR) + $(FUZZ_CC) $(FUZZ_CFLAGS) $(FUZZ_SRCS) $< -o $@ + +$(TARGET_DIR)/diskann_operations: diskann-operations.c $(FUZZ_SRCS) | $(TARGET_DIR) + $(FUZZ_CC) $(FUZZ_CFLAGS) $(FUZZ_SRCS) $< -o $@ + +$(TARGET_DIR)/diskann_create: diskann-create.c $(FUZZ_SRCS) | $(TARGET_DIR) + $(FUZZ_CC) $(FUZZ_CFLAGS) $(FUZZ_SRCS) $< -o $@ + +$(TARGET_DIR)/diskann_graph_corrupt: diskann-graph-corrupt.c $(FUZZ_SRCS) | $(TARGET_DIR) + $(FUZZ_CC) $(FUZZ_CFLAGS) $(FUZZ_SRCS) $< -o $@ + +$(TARGET_DIR)/diskann_deep_search: diskann-deep-search.c $(FUZZ_SRCS) | $(TARGET_DIR) + $(FUZZ_CC) $(FUZZ_CFLAGS) $(FUZZ_SRCS) $< -o $@ + +$(TARGET_DIR)/diskann_blob_truncate: diskann-blob-truncate.c $(FUZZ_SRCS) | $(TARGET_DIR) + $(FUZZ_CC) $(FUZZ_CFLAGS) $(FUZZ_SRCS) $< -o $@ + +$(TARGET_DIR)/diskann_delete_stress: diskann-delete-stress.c $(FUZZ_SRCS) | $(TARGET_DIR) + $(FUZZ_CC) $(FUZZ_CFLAGS) $(FUZZ_SRCS) $< -o $@ + +$(TARGET_DIR)/diskann_buffer_flush: diskann-buffer-flush.c $(FUZZ_SRCS) | $(TARGET_DIR) + $(FUZZ_CC) $(FUZZ_CFLAGS) $(FUZZ_SRCS) $< -o $@ + +$(TARGET_DIR)/diskann_int8_quant: diskann-int8-quant.c $(FUZZ_SRCS) | $(TARGET_DIR) + $(FUZZ_CC) $(FUZZ_CFLAGS) $(FUZZ_SRCS) $< -o $@ + +$(TARGET_DIR)/diskann_prune_direct: diskann-prune-direct.c $(FUZZ_SRCS) | $(TARGET_DIR) + $(FUZZ_CC) $(FUZZ_CFLAGS) $(FUZZ_SRCS) $< -o $@ + +$(TARGET_DIR)/diskann_command_inject: diskann-command-inject.c $(FUZZ_SRCS) | $(TARGET_DIR) + $(FUZZ_CC) $(FUZZ_CFLAGS) $(FUZZ_SRCS) $< -o $@ + +# the IVF index is compiled only on request +IVF_CFLAGS = -DSQLITE_VEC_EXPERIMENTAL_IVF_ENABLE=1 + +$(TARGET_DIR)/ivf_create: ivf-create.c $(FUZZ_SRCS) | $(TARGET_DIR) + $(FUZZ_CC) $(FUZZ_CFLAGS) $(IVF_CFLAGS) $(FUZZ_SRCS) $< -o $@ + +$(TARGET_DIR)/ivf_operations: ivf-operations.c $(FUZZ_SRCS) | $(TARGET_DIR) + $(FUZZ_CC) $(FUZZ_CFLAGS) $(IVF_CFLAGS) $(FUZZ_SRCS) $< -o $@ + +$(TARGET_DIR)/ivf_quantize: ivf-quantize.c $(FUZZ_SRCS) | $(TARGET_DIR) + $(FUZZ_CC) $(FUZZ_CFLAGS) $(IVF_CFLAGS) $(FUZZ_SRCS) $< -o $@ + +$(TARGET_DIR)/ivf_kmeans: ivf-kmeans.c $(FUZZ_SRCS) | $(TARGET_DIR) + $(FUZZ_CC) $(FUZZ_CFLAGS) $(IVF_CFLAGS) $(FUZZ_SRCS) $< -o $@ + +$(TARGET_DIR)/ivf_shadow_corrupt: ivf-shadow-corrupt.c $(FUZZ_SRCS) | $(TARGET_DIR) + $(FUZZ_CC) $(FUZZ_CFLAGS) $(IVF_CFLAGS) $(FUZZ_SRCS) $< -o $@ + +$(TARGET_DIR)/ivf_knn_deep: ivf-knn-deep.c $(FUZZ_SRCS) | $(TARGET_DIR) + $(FUZZ_CC) $(FUZZ_CFLAGS) $(IVF_CFLAGS) $(FUZZ_SRCS) $< -o $@ + +$(TARGET_DIR)/ivf_cell_overflow: ivf-cell-overflow.c $(FUZZ_SRCS) | $(TARGET_DIR) + $(FUZZ_CC) $(FUZZ_CFLAGS) $(IVF_CFLAGS) $(FUZZ_SRCS) $< -o $@ + +$(TARGET_DIR)/ivf_rescore: ivf-rescore.c $(FUZZ_SRCS) | $(TARGET_DIR) + $(FUZZ_CC) $(FUZZ_CFLAGS) $(IVF_CFLAGS) $(FUZZ_SRCS) $< -o $@ + FUZZ_TARGETS = vec0_create exec json numpy \ shadow_corrupt vec0_operations scalar_functions \ vec0_create_full metadata_columns vec_each vec_mismatch \ - vec0_delete_completeness + vec0_delete_completeness \ + rescore_operations rescore_create rescore_quantize \ + rescore_shadow_corrupt rescore_knn_deep \ + rescore_quantize_edge rescore_interleave rescore_optimize \ + diskann_operations diskann_create diskann_graph_corrupt \ + diskann_deep_search diskann_blob_truncate \ + diskann_delete_stress diskann_buffer_flush \ + diskann_int8_quant diskann_prune_direct \ + diskann_command_inject \ + ivf_create ivf_operations ivf_quantize ivf_kmeans \ + ivf_shadow_corrupt ivf_knn_deep ivf_cell_overflow ivf_rescore all: $(addprefix $(TARGET_DIR)/,$(FUZZ_TARGETS)) diff --git a/tests/fuzz/diskann-blob-truncate.c b/tests/fuzz/diskann-blob-truncate.c new file mode 100644 index 00000000..11e3bd84 --- /dev/null +++ b/tests/fuzz/diskann-blob-truncate.c @@ -0,0 +1,250 @@ +/** + * Fuzz target for DiskANN shadow table blob size mismatches. + * + * diskann_search/insert/delete index into a node's blobs using + * cfg->n_neighbors * sizeof(i64) etc., so diskann_node_read() must refuse a + * blob that is truncated, extended, or otherwise not the size the column's + * options give it, before any of them reads or writes out of bounds. + * + * This fuzzer: + * 1. Creates a valid DiskANN graph with several nodes + * 2. Uses fuzz data to directly write malformed blobs to shadow tables: + * - Truncated neighbor_ids (fewer bytes than n_neighbors * 8) + * - Truncated validity bitmaps + * - Oversized blobs with garbage trailing data + * - Zero-length blobs + * - Blobs with valid headers but corrupted neighbor rowids + * 3. Runs INSERT, DELETE, and KNN operations that traverse the corrupted graph + * + * Key code paths targeted: + * - diskann_node_read with mismatched blob sizes + * - diskann_validity_get / diskann_neighbor_id_get on truncated blobs + * - diskann_add_reverse_edge reading corrupted neighbor data + * - diskann_repair_reverse_edges traversing corrupted neighbor lists + * - diskann_search iterating neighbors from corrupted blobs + */ +#include +#include +#include +#include +#include +#include "sqlite-vec.h" +#include "sqlite3.h" +#include + +static uint8_t fuzz_byte(const uint8_t **data, size_t *size, uint8_t def) { + if (*size == 0) return def; + uint8_t b = **data; + (*data)++; + (*size)--; + return b; +} + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (size < 32) return 0; + + int rc; + sqlite3 *db; + + rc = sqlite3_open(":memory:", &db); + assert(rc == SQLITE_OK); + rc = sqlite3_vec_init(db, NULL, NULL); + assert(rc == SQLITE_OK); + + /* Use binary quantizer, float[16], n_neighbors=8 for predictable blob sizes: + * validity: 8/8 = 1 byte + * neighbor_ids: 8 * 8 = 64 bytes + * qvecs: 8 * (16/8) = 16 bytes (binary: 2 bytes per qvec) + */ + rc = sqlite3_exec(db, + "CREATE VIRTUAL TABLE v USING vec0(" + "emb float[16] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=8))", + NULL, NULL, NULL); + if (rc != SQLITE_OK) { sqlite3_close(db); return 0; } + + /* Insert 12 vectors to create a valid graph structure */ + { + sqlite3_stmt *stmt; + sqlite3_prepare_v2(db, + "INSERT INTO v(rowid, emb) VALUES (?, ?)", -1, &stmt, NULL); + for (int i = 1; i <= 12; i++) { + float vec[16]; + for (int j = 0; j < 16; j++) { + vec[j] = (float)i * 0.1f + (float)j * 0.01f; + } + sqlite3_reset(stmt); + sqlite3_bind_int64(stmt, 1, i); + sqlite3_bind_blob(stmt, 2, vec, sizeof(vec), SQLITE_TRANSIENT); + sqlite3_step(stmt); + } + sqlite3_finalize(stmt); + } + + /* Now corrupt shadow table blobs using fuzz data */ + const char *columns[] = { + "neighbors_validity", + "neighbor_ids", + "neighbor_quantized_vectors" + }; + + /* Expected sizes for n_neighbors=8, dims=16, binary quantizer */ + int expected_sizes[] = {1, 64, 16}; + + while (size >= 4) { + int target_row = (fuzz_byte(&data, &size, 0) % 12) + 1; + int col_idx = fuzz_byte(&data, &size, 0) % 3; + uint8_t corrupt_mode = fuzz_byte(&data, &size, 0) % 6; + uint8_t extra = fuzz_byte(&data, &size, 0); + + char sqlbuf[256]; + snprintf(sqlbuf, sizeof(sqlbuf), + "UPDATE v_diskann_nodes00 SET %s = ? WHERE rowid = ?", + columns[col_idx]); + + sqlite3_stmt *writeStmt; + rc = sqlite3_prepare_v2(db, sqlbuf, -1, &writeStmt, NULL); + if (rc != SQLITE_OK) continue; + + int expected = expected_sizes[col_idx]; + unsigned char *blob = NULL; + int blob_size = 0; + + switch (corrupt_mode) { + case 0: { + /* Truncated blob: 0 to expected-1 bytes */ + blob_size = extra % expected; + if (blob_size == 0) blob_size = 0; /* zero-length is interesting */ + blob = sqlite3_malloc(blob_size > 0 ? blob_size : 1); + if (!blob) { sqlite3_finalize(writeStmt); continue; } + for (int i = 0; i < blob_size; i++) { + blob[i] = fuzz_byte(&data, &size, 0); + } + break; + } + case 1: { + /* Oversized blob: expected + extra bytes */ + blob_size = expected + (extra % 64); + blob = sqlite3_malloc(blob_size); + if (!blob) { sqlite3_finalize(writeStmt); continue; } + for (int i = 0; i < blob_size; i++) { + blob[i] = fuzz_byte(&data, &size, 0xFF); + } + break; + } + case 2: { + /* Zero-length blob */ + blob_size = 0; + blob = NULL; + sqlite3_bind_zeroblob(writeStmt, 1, 0); + sqlite3_bind_int64(writeStmt, 2, target_row); + sqlite3_step(writeStmt); + sqlite3_finalize(writeStmt); + continue; + } + case 3: { + /* Correct size but all-ones validity (all slots "valid") with + * garbage neighbor IDs -- forces reading non-existent nodes */ + blob_size = expected; + blob = sqlite3_malloc(blob_size); + if (!blob) { sqlite3_finalize(writeStmt); continue; } + memset(blob, 0xFF, blob_size); + break; + } + case 4: { + /* neighbor_ids with very large rowid values (near INT64_MAX) */ + blob_size = expected; + blob = sqlite3_malloc(blob_size); + if (!blob) { sqlite3_finalize(writeStmt); continue; } + memset(blob, 0x7F, blob_size); /* fills with large positive values */ + break; + } + case 5: { + /* neighbor_ids with negative rowid values (rowid=0 is sentinel) */ + blob_size = expected; + blob = sqlite3_malloc(blob_size); + if (!blob) { sqlite3_finalize(writeStmt); continue; } + memset(blob, 0x80, blob_size); /* fills with large negative values */ + /* Flip some bytes from fuzz data */ + for (int i = 0; i < blob_size && size > 0; i++) { + blob[i] ^= fuzz_byte(&data, &size, 0); + } + break; + } + } + + if (blob) { + sqlite3_bind_blob(writeStmt, 1, blob, blob_size, SQLITE_TRANSIENT); + } else { + sqlite3_bind_blob(writeStmt, 1, "", 0, SQLITE_STATIC); + } + sqlite3_bind_int64(writeStmt, 2, target_row); + sqlite3_step(writeStmt); + sqlite3_finalize(writeStmt); + sqlite3_free(blob); + } + + /* Exercise the corrupted graph with various operations */ + + /* KNN query */ + { + float qvec[16]; + for (int j = 0; j < 16; j++) qvec[j] = (float)j * 0.1f; + sqlite3_stmt *knnStmt; + rc = sqlite3_prepare_v2(db, + "SELECT rowid, distance FROM v WHERE emb MATCH ? AND k = 5", + -1, &knnStmt, NULL); + if (rc == SQLITE_OK) { + sqlite3_bind_blob(knnStmt, 1, qvec, sizeof(qvec), SQLITE_STATIC); + while (sqlite3_step(knnStmt) == SQLITE_ROW) {} + sqlite3_finalize(knnStmt); + } + } + + /* Insert into corrupted graph (triggers add_reverse_edge on corrupted nodes) */ + { + float vec[16]; + for (int j = 0; j < 16; j++) vec[j] = 0.5f; + sqlite3_stmt *stmt; + sqlite3_prepare_v2(db, + "INSERT INTO v(rowid, emb) VALUES (?, ?)", -1, &stmt, NULL); + if (stmt) { + sqlite3_bind_int64(stmt, 1, 100); + sqlite3_bind_blob(stmt, 2, vec, sizeof(vec), SQLITE_TRANSIENT); + sqlite3_step(stmt); + sqlite3_finalize(stmt); + } + } + + /* Delete from corrupted graph (triggers repair_reverse_edges) */ + { + sqlite3_stmt *stmt; + sqlite3_prepare_v2(db, + "DELETE FROM v WHERE rowid = ?", -1, &stmt, NULL); + if (stmt) { + sqlite3_bind_int64(stmt, 1, 5); + sqlite3_step(stmt); + sqlite3_finalize(stmt); + } + } + + /* Another KNN to traverse the post-mutation graph */ + { + float qvec[16]; + for (int j = 0; j < 16; j++) qvec[j] = -0.5f + (float)j * 0.07f; + sqlite3_stmt *knnStmt; + rc = sqlite3_prepare_v2(db, + "SELECT rowid, distance FROM v WHERE emb MATCH ? AND k = 12", + -1, &knnStmt, NULL); + if (rc == SQLITE_OK) { + sqlite3_bind_blob(knnStmt, 1, qvec, sizeof(qvec), SQLITE_STATIC); + while (sqlite3_step(knnStmt) == SQLITE_ROW) {} + sqlite3_finalize(knnStmt); + } + } + + /* Full scan */ + sqlite3_exec(db, "SELECT * FROM v", NULL, NULL, NULL); + + sqlite3_close(db); + return 0; +} diff --git a/tests/fuzz/diskann-buffer-flush.c b/tests/fuzz/diskann-buffer-flush.c new file mode 100644 index 00000000..a5d23dd4 --- /dev/null +++ b/tests/fuzz/diskann-buffer-flush.c @@ -0,0 +1,164 @@ +/** + * Fuzz target for DiskANN buffered insert and flush paths. + * + * When buffer_threshold > 0, inserts go into a flat buffer table and + * are flushed into the graph in batch. This fuzzer exercises: + * + * - diskann_insert and diskann_delete on the buffer table + * - diskann_flush_buffer (batch graph insertion) + * - diskann_insert with buffer_threshold (batching logic) + * - Buffer-graph merge in diskann_knn (unflushed vectors + * must be scanned during KNN and merged with graph results) + * - Delete of a buffered (not yet flushed) vector + * - Delete of a graph vector while buffer has pending inserts + * - Interaction: insert to buffer, query (triggers buffer scan), flush, + * query again (now from graph) + * + * The buffer merge path in diskann_knn is particularly + * interesting because it does a brute-force scan of buffer vectors and + * merges with the top-k from graph search. + */ +#include +#include +#include +#include +#include +#include "sqlite-vec.h" +#include "sqlite3.h" +#include + +static uint8_t fuzz_byte(const uint8_t **data, size_t *size, uint8_t def) { + if (*size == 0) return def; + uint8_t b = **data; + (*data)++; + (*size)--; + return b; +} + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (size < 16) return 0; + + int rc; + sqlite3 *db; + rc = sqlite3_open(":memory:", &db); + assert(rc == SQLITE_OK); + rc = sqlite3_vec_init(db, NULL, NULL); + assert(rc == SQLITE_OK); + + /* buffer_threshold: small (3-8) to trigger frequent flushes */ + int buf_threshold = 3 + (fuzz_byte(&data, &size, 0) % 6); + int dims = 8; + + char sql[512]; + snprintf(sql, sizeof(sql), + "CREATE VIRTUAL TABLE v USING vec0(" + "emb float[%d] INDEXED BY diskann(" + "neighbor_quantizer=binary, n_neighbors=8, " + "search_list_size=16, buffer_threshold=%d" + "))", dims, buf_threshold); + + rc = sqlite3_exec(db, sql, NULL, NULL, NULL); + if (rc != SQLITE_OK) { sqlite3_close(db); return 0; } + + sqlite3_stmt *stmtInsert = NULL, *stmtDelete = NULL, *stmtKnn = NULL; + sqlite3_prepare_v2(db, + "INSERT INTO v(rowid, emb) VALUES (?, ?)", -1, &stmtInsert, NULL); + sqlite3_prepare_v2(db, + "DELETE FROM v WHERE rowid = ?", -1, &stmtDelete, NULL); + sqlite3_prepare_v2(db, + "SELECT rowid, distance FROM v WHERE emb MATCH ? AND k = ?", + -1, &stmtKnn, NULL); + + if (!stmtInsert || !stmtDelete || !stmtKnn) goto cleanup; + + float vec[8]; + int next_rowid = 1; + + while (size >= 2) { + uint8_t op = fuzz_byte(&data, &size, 0) % 6; + uint8_t param = fuzz_byte(&data, &size, 0); + + switch (op) { + case 0: { /* Insert: accumulates in buffer until threshold */ + int64_t rowid = next_rowid++; + if (next_rowid > 64) next_rowid = 1; /* wrap around for reuse */ + for (int j = 0; j < dims; j++) { + vec[j] = (float)((int8_t)fuzz_byte(&data, &size, 0)) / 10.0f; + } + sqlite3_reset(stmtInsert); + sqlite3_bind_int64(stmtInsert, 1, rowid); + sqlite3_bind_blob(stmtInsert, 2, vec, sizeof(vec), SQLITE_TRANSIENT); + sqlite3_step(stmtInsert); + break; + } + case 1: { /* KNN query while buffer may have unflushed vectors */ + for (int j = 0; j < dims; j++) { + vec[j] = (float)((int8_t)fuzz_byte(&data, &size, 0)) / 10.0f; + } + int k = (param % 10) + 1; + sqlite3_reset(stmtKnn); + sqlite3_bind_blob(stmtKnn, 1, vec, sizeof(vec), SQLITE_TRANSIENT); + sqlite3_bind_int(stmtKnn, 2, k); + while (sqlite3_step(stmtKnn) == SQLITE_ROW) {} + break; + } + case 2: { /* Delete a potentially-buffered vector */ + int64_t rowid = (int64_t)(param % 64) + 1; + sqlite3_reset(stmtDelete); + sqlite3_bind_int64(stmtDelete, 1, rowid); + sqlite3_step(stmtDelete); + break; + } + case 3: { /* Insert several at once to trigger flush mid-batch */ + for (int i = 0; i < buf_threshold + 1 && size >= 2; i++) { + int64_t rowid = (int64_t)(fuzz_byte(&data, &size, 0) % 64) + 1; + for (int j = 0; j < dims; j++) { + vec[j] = (float)((int8_t)fuzz_byte(&data, &size, 0)) / 10.0f; + } + sqlite3_reset(stmtInsert); + sqlite3_bind_int64(stmtInsert, 1, rowid); + sqlite3_bind_blob(stmtInsert, 2, vec, sizeof(vec), SQLITE_TRANSIENT); + sqlite3_step(stmtInsert); + } + break; + } + case 4: { /* Insert then immediately delete (still in buffer) */ + int64_t rowid = (int64_t)(param % 64) + 1; + for (int j = 0; j < dims; j++) vec[j] = 0.1f * param; + sqlite3_reset(stmtInsert); + sqlite3_bind_int64(stmtInsert, 1, rowid); + sqlite3_bind_blob(stmtInsert, 2, vec, sizeof(vec), SQLITE_TRANSIENT); + sqlite3_step(stmtInsert); + + sqlite3_reset(stmtDelete); + sqlite3_bind_int64(stmtDelete, 1, rowid); + sqlite3_step(stmtDelete); + break; + } + case 5: { /* Query with k=0 and k=1 (boundary) */ + for (int j = 0; j < dims; j++) vec[j] = 0.0f; + sqlite3_reset(stmtKnn); + sqlite3_bind_blob(stmtKnn, 1, vec, sizeof(vec), SQLITE_TRANSIENT); + sqlite3_bind_int(stmtKnn, 2, param % 2); /* k=0 or k=1 */ + while (sqlite3_step(stmtKnn) == SQLITE_ROW) {} + break; + } + } + } + + /* Final query to exercise post-operation state */ + { + float qvec[8] = {1.0f, -1.0f, 0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 0.0f}; + sqlite3_reset(stmtKnn); + sqlite3_bind_blob(stmtKnn, 1, qvec, sizeof(qvec), SQLITE_TRANSIENT); + sqlite3_bind_int(stmtKnn, 2, 20); + while (sqlite3_step(stmtKnn) == SQLITE_ROW) {} + } + +cleanup: + sqlite3_finalize(stmtInsert); + sqlite3_finalize(stmtDelete); + sqlite3_finalize(stmtKnn); + sqlite3_close(db); + return 0; +} diff --git a/tests/fuzz/diskann-command-inject.c b/tests/fuzz/diskann-command-inject.c new file mode 100644 index 00000000..dbd9111b --- /dev/null +++ b/tests/fuzz/diskann-command-inject.c @@ -0,0 +1,157 @@ +/** + * Fuzz target for DiskANN runtime command dispatch (diskann_handle_command). + * + * The command handler parses strings like "search_list_size_search=42" and + * modifies live DiskANN config. This fuzzer exercises: + * + * - value parsing of fuzz-controlled strings (integer overflow, negative, + * non-numeric) + * - strncmp boundary with fuzz data (near-matches to valid commands) + * - Changing search_list_size mid-operation (affects subsequent queries) + * - Setting search_list_size to 1 (minimum - single-candidate beam search) + * - Setting search_list_size very large (memory pressure) + * - Interleaving command changes with inserts and queries + */ +#include +#include +#include +#include +#include +#include "sqlite-vec.h" +#include "sqlite3.h" +#include + +static uint8_t fuzz_byte(const uint8_t **data, size_t *size, uint8_t def) { + if (*size == 0) return def; + uint8_t b = **data; + (*data)++; + (*size)--; + return b; +} + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (size < 20) return 0; + + int rc; + sqlite3 *db; + rc = sqlite3_open(":memory:", &db); + assert(rc == SQLITE_OK); + rc = sqlite3_vec_init(db, NULL, NULL); + assert(rc == SQLITE_OK); + + rc = sqlite3_exec(db, + "CREATE VIRTUAL TABLE v USING vec0(" + "emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=8))", + NULL, NULL, NULL); + if (rc != SQLITE_OK) { sqlite3_close(db); return 0; } + + /* Insert some vectors first */ + { + sqlite3_stmt *stmt; + sqlite3_prepare_v2(db, + "INSERT INTO v(v, emb) VALUES (?, ?)", -1, &stmt, NULL); + for (int i = 1; i <= 8; i++) { + float vec[8]; + for (int j = 0; j < 8; j++) vec[j] = (float)i * 0.1f + (float)j * 0.01f; + sqlite3_reset(stmt); + sqlite3_bind_int64(stmt, 1, i); + sqlite3_bind_blob(stmt, 2, vec, sizeof(vec), SQLITE_TRANSIENT); + sqlite3_step(stmt); + } + sqlite3_finalize(stmt); + } + + sqlite3_stmt *stmtCmd = NULL; + sqlite3_stmt *stmtInsert = NULL; + sqlite3_stmt *stmtKnn = NULL; + + /* Commands are dispatched via INSERT INTO t(t) VALUES ('cmd_string') */ + sqlite3_prepare_v2(db, + "INSERT INTO v(v) VALUES (?)", -1, &stmtCmd, NULL); + sqlite3_prepare_v2(db, + "INSERT INTO v(v, emb) VALUES (?, ?)", -1, &stmtInsert, NULL); + sqlite3_prepare_v2(db, + "SELECT rowid, distance FROM v WHERE emb MATCH ? AND k = ?", + -1, &stmtKnn, NULL); + + if (!stmtCmd || !stmtInsert || !stmtKnn) goto cleanup; + + /* Fuzz-driven command + operation interleaving */ + while (size >= 2) { + uint8_t op = fuzz_byte(&data, &size, 0) % 5; + + switch (op) { + case 0: { /* Send fuzz command string */ + int cmd_len = fuzz_byte(&data, &size, 0) % 64; + char cmd[65]; + for (int i = 0; i < cmd_len && size > 0; i++) { + cmd[i] = (char)fuzz_byte(&data, &size, 0); + } + cmd[cmd_len] = '\0'; + sqlite3_reset(stmtCmd); + sqlite3_bind_text(stmtCmd, 1, cmd, -1, SQLITE_TRANSIENT); + sqlite3_step(stmtCmd); /* May fail -- that's expected */ + break; + } + case 1: { /* Send valid-looking command with fuzz value */ + const char *prefixes[] = { + "search_list_size=", + "search_list_size_search=", + "search_list_size_insert=", + }; + int prefix_idx = fuzz_byte(&data, &size, 0) % 3; + int val = (int)(int8_t)fuzz_byte(&data, &size, 0); + + char cmd[128]; + snprintf(cmd, sizeof(cmd), "%s%d", prefixes[prefix_idx], val); + sqlite3_reset(stmtCmd); + sqlite3_bind_text(stmtCmd, 1, cmd, -1, SQLITE_TRANSIENT); + sqlite3_step(stmtCmd); + break; + } + case 2: { /* KNN query (uses whatever search_list_size is set) */ + float qvec[8] = {1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}; + qvec[0] = (float)((int8_t)fuzz_byte(&data, &size, 127)) / 10.0f; + int k = fuzz_byte(&data, &size, 3) % 10 + 1; + sqlite3_reset(stmtKnn); + sqlite3_bind_blob(stmtKnn, 1, qvec, sizeof(qvec), SQLITE_TRANSIENT); + sqlite3_bind_int(stmtKnn, 2, k); + while (sqlite3_step(stmtKnn) == SQLITE_ROW) {} + break; + } + case 3: { /* Insert (uses whatever search_list_size_insert is set) */ + int64_t rowid = (int64_t)(fuzz_byte(&data, &size, 0) % 32) + 1; + float vec[8]; + for (int j = 0; j < 8; j++) { + vec[j] = (float)((int8_t)fuzz_byte(&data, &size, 0)) / 10.0f; + } + sqlite3_reset(stmtInsert); + sqlite3_bind_int64(stmtInsert, 1, rowid); + sqlite3_bind_blob(stmtInsert, 2, vec, sizeof(vec), SQLITE_TRANSIENT); + sqlite3_step(stmtInsert); + break; + } + case 4: { /* Set search_list_size to extreme values */ + const char *extreme_cmds[] = { + "search_list_size=1", + "search_list_size=2", + "search_list_size=1000", + "search_list_size_search=1", + "search_list_size_insert=1", + }; + int idx = fuzz_byte(&data, &size, 0) % 5; + sqlite3_reset(stmtCmd); + sqlite3_bind_text(stmtCmd, 1, extreme_cmds[idx], -1, SQLITE_STATIC); + sqlite3_step(stmtCmd); + break; + } + } + } + +cleanup: + sqlite3_finalize(stmtCmd); + sqlite3_finalize(stmtInsert); + sqlite3_finalize(stmtKnn); + sqlite3_close(db); + return 0; +} diff --git a/tests/fuzz/diskann-create.c b/tests/fuzz/diskann-create.c new file mode 100644 index 00000000..1b40a84d --- /dev/null +++ b/tests/fuzz/diskann-create.c @@ -0,0 +1,44 @@ +/** + * Fuzz target for DiskANN CREATE TABLE config parsing. + * Feeds fuzz data as the INDEXED BY diskann(...) option string. + */ +#include +#include +#include +#include +#include +#include "sqlite-vec.h" +#include "sqlite3.h" +#include + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (size > 4096) return 0; /* Limit input size */ + + int rc; + sqlite3 *db; + sqlite3_stmt *stmt; + + rc = sqlite3_open(":memory:", &db); + assert(rc == SQLITE_OK); + rc = sqlite3_vec_init(db, NULL, NULL); + assert(rc == SQLITE_OK); + + sqlite3_str *s = sqlite3_str_new(NULL); + assert(s); + sqlite3_str_appendall(s, + "CREATE VIRTUAL TABLE v USING vec0(" + "emb float[64] INDEXED BY diskann("); + sqlite3_str_appendf(s, "%.*s", (int)size, data); + sqlite3_str_appendall(s, "))"); + const char *zSql = sqlite3_str_finish(s); + assert(zSql); + + rc = sqlite3_prepare_v2(db, zSql, -1, &stmt, NULL); + sqlite3_free((char *)zSql); + if (rc == SQLITE_OK) { + sqlite3_step(stmt); + } + sqlite3_finalize(stmt); + sqlite3_close(db); + return 0; +} diff --git a/tests/fuzz/diskann-create.dict b/tests/fuzz/diskann-create.dict new file mode 100644 index 00000000..7387ba81 --- /dev/null +++ b/tests/fuzz/diskann-create.dict @@ -0,0 +1,11 @@ +"neighbor_quantizer" +"binary" +"int8" +"n_neighbors" +"search_list_size" +"search_list_size_search" +"search_list_size_insert" +"buffer_threshold" +"alpha" +"=" +"," diff --git a/tests/fuzz/diskann-deep-search.c b/tests/fuzz/diskann-deep-search.c new file mode 100644 index 00000000..d5768d65 --- /dev/null +++ b/tests/fuzz/diskann-deep-search.c @@ -0,0 +1,187 @@ +/** + * Fuzz target for DiskANN greedy beam search deep paths. + * + * Builds a graph with enough nodes to force multi-hop traversal, then + * uses fuzz data to control: query vector values, k, search_list_size + * overrides, and interleaved insert/delete/query sequences that stress + * the candidate list growth, visited set hash collisions, and the + * re-ranking logic. + * + * Key code paths targeted: + * - diskann_candidate_list_insert (sorted insert, dedup, eviction) + * - diskann_visited_set (hash collisions, capacity) + * - diskann_search (full beam search loop, re-ranking with exact dist) + * - diskann_distance_quantized (both binary and int8) + * - Buffer merge in diskann_knn + */ +#include +#include +#include +#include +#include +#include +#include "sqlite-vec.h" +#include "sqlite3.h" +#include + +/* Consume one byte from fuzz input, or return default. */ +static uint8_t fuzz_byte(const uint8_t **data, size_t *size, uint8_t def) { + if (*size == 0) return def; + uint8_t b = **data; + (*data)++; + (*size)--; + return b; +} + +static uint16_t fuzz_u16(const uint8_t **data, size_t *size) { + uint8_t lo = fuzz_byte(data, size, 0); + uint8_t hi = fuzz_byte(data, size, 0); + return (uint16_t)hi << 8 | lo; +} + +static float fuzz_float(const uint8_t **data, size_t *size) { + return (float)((int8_t)fuzz_byte(data, size, 0)) / 10.0f; +} + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (size < 32) return 0; + + /* Use first bytes to pick quantizer type and dimensions */ + uint8_t quantizer_choice = fuzz_byte(&data, &size, 0) % 2; + const char *quantizer = quantizer_choice ? "int8" : "binary"; + + /* Dimensions must be divisible by 8. Pick from {8, 16, 32} */ + int dim_choices[] = {8, 16, 32}; + int dims = dim_choices[fuzz_byte(&data, &size, 0) % 3]; + + /* n_neighbors: 8 or 16 -- small to force full-neighbor scenarios quickly */ + int n_neighbors = (fuzz_byte(&data, &size, 0) % 2) ? 16 : 8; + + /* search_list_size: small so beam search terminates quickly but still exercises loops */ + int search_list_size = 8 + (fuzz_byte(&data, &size, 0) % 24); + + /* alpha: vary to test RobustPrune pruning logic */ + float alpha_choices[] = {1.0f, 1.2f, 1.5f, 2.0f}; + float alpha = alpha_choices[fuzz_byte(&data, &size, 0) % 4]; + + int rc; + sqlite3 *db; + rc = sqlite3_open(":memory:", &db); + assert(rc == SQLITE_OK); + rc = sqlite3_vec_init(db, NULL, NULL); + assert(rc == SQLITE_OK); + + char sql[512]; + snprintf(sql, sizeof(sql), + "CREATE VIRTUAL TABLE v USING vec0(" + "emb float[%d] INDEXED BY diskann(" + "neighbor_quantizer=%s, n_neighbors=%d, " + "search_list_size=%d" + "))", dims, quantizer, n_neighbors, search_list_size); + + rc = sqlite3_exec(db, sql, NULL, NULL, NULL); + if (rc != SQLITE_OK) { sqlite3_close(db); return 0; } + + sqlite3_stmt *stmtInsert = NULL, *stmtDelete = NULL, *stmtKnn = NULL; + sqlite3_prepare_v2(db, + "INSERT INTO v(rowid, emb) VALUES (?, ?)", -1, &stmtInsert, NULL); + sqlite3_prepare_v2(db, + "DELETE FROM v WHERE rowid = ?", -1, &stmtDelete, NULL); + + char knn_sql[256]; + snprintf(knn_sql, sizeof(knn_sql), + "SELECT rowid, distance FROM v WHERE emb MATCH ? AND k = ?"); + sqlite3_prepare_v2(db, knn_sql, -1, &stmtKnn, NULL); + + if (!stmtInsert || !stmtDelete || !stmtKnn) goto cleanup; + + /* Phase 1: Seed the graph with enough nodes to create multi-hop structure. + * Insert 2*n_neighbors nodes so the graph is dense enough for search + * to actually traverse multiple hops. */ + int seed_count = n_neighbors * 2; + if (seed_count > 64) seed_count = 64; /* Bound for performance */ + { + float *vec = malloc(dims * sizeof(float)); + if (!vec) goto cleanup; + for (int i = 1; i <= seed_count; i++) { + for (int j = 0; j < dims; j++) { + vec[j] = fuzz_float(&data, &size); + } + sqlite3_reset(stmtInsert); + sqlite3_bind_int64(stmtInsert, 1, i); + sqlite3_bind_blob(stmtInsert, 2, vec, dims * sizeof(float), SQLITE_TRANSIENT); + sqlite3_step(stmtInsert); + } + free(vec); + } + + /* Phase 2: Fuzz-driven operations on the seeded graph */ + float *vec = malloc(dims * sizeof(float)); + if (!vec) goto cleanup; + + while (size >= 2) { + uint8_t op = fuzz_byte(&data, &size, 0) % 5; + uint8_t param = fuzz_byte(&data, &size, 0); + + switch (op) { + case 0: { /* INSERT with fuzz-controlled vector and rowid */ + int64_t rowid = (int64_t)(param % 128) + 1; + for (int j = 0; j < dims; j++) { + vec[j] = fuzz_float(&data, &size); + } + sqlite3_reset(stmtInsert); + sqlite3_bind_int64(stmtInsert, 1, rowid); + sqlite3_bind_blob(stmtInsert, 2, vec, dims * sizeof(float), SQLITE_TRANSIENT); + sqlite3_step(stmtInsert); + break; + } + case 1: { /* DELETE */ + int64_t rowid = (int64_t)(param % 128) + 1; + sqlite3_reset(stmtDelete); + sqlite3_bind_int64(stmtDelete, 1, rowid); + sqlite3_step(stmtDelete); + break; + } + case 2: { /* KNN with fuzz query vector and variable k */ + for (int j = 0; j < dims; j++) { + vec[j] = fuzz_float(&data, &size); + } + int k = (param % 20) + 1; + sqlite3_reset(stmtKnn); + sqlite3_bind_blob(stmtKnn, 1, vec, dims * sizeof(float), SQLITE_TRANSIENT); + sqlite3_bind_int(stmtKnn, 2, k); + while (sqlite3_step(stmtKnn) == SQLITE_ROW) {} + break; + } + case 3: { /* KNN with k > number of nodes (boundary) */ + for (int j = 0; j < dims; j++) { + vec[j] = fuzz_float(&data, &size); + } + sqlite3_reset(stmtKnn); + sqlite3_bind_blob(stmtKnn, 1, vec, dims * sizeof(float), SQLITE_TRANSIENT); + sqlite3_bind_int(stmtKnn, 2, 1000); /* k >> graph size */ + while (sqlite3_step(stmtKnn) == SQLITE_ROW) {} + break; + } + case 4: { /* INSERT duplicate rowid (triggers OR REPLACE path) */ + int64_t rowid = (int64_t)(param % 32) + 1; + for (int j = 0; j < dims; j++) { + vec[j] = (float)(param + j) / 50.0f; + } + sqlite3_reset(stmtInsert); + sqlite3_bind_int64(stmtInsert, 1, rowid); + sqlite3_bind_blob(stmtInsert, 2, vec, dims * sizeof(float), SQLITE_TRANSIENT); + sqlite3_step(stmtInsert); + break; + } + } + } + free(vec); + +cleanup: + sqlite3_finalize(stmtInsert); + sqlite3_finalize(stmtDelete); + sqlite3_finalize(stmtKnn); + sqlite3_close(db); + return 0; +} diff --git a/tests/fuzz/diskann-delete-stress.c b/tests/fuzz/diskann-delete-stress.c new file mode 100644 index 00000000..00bf168e --- /dev/null +++ b/tests/fuzz/diskann-delete-stress.c @@ -0,0 +1,175 @@ +/** + * Fuzz target for DiskANN delete path and graph connectivity maintenance. + * + * The delete path is the most complex graph mutation: + * 1. Read deleted node's neighbor list + * 2. For each neighbor, remove deleted node from their list + * 3. Try to fill the gap with one of deleted node's other neighbors + * 4. Handle medoid deletion (pick new medoid) + * + * Edge cases this targets: + * - Delete the medoid (entry point) -- forces medoid reassignment + * - Delete all nodes except one -- graph degenerates + * - Delete nodes in a chain -- cascading dangling edges + * - Re-insert at deleted rowids -- stale graph edges to old data + * - Delete nonexistent rowids -- should be no-op + * - Insert-delete-insert same rowid rapidly + * - Delete when graph has exactly n_neighbors entries (full nodes) + * + * Key code paths: + * - diskann_delete -> diskann_repair_reverse_edges + * - diskann_delete's choice of a new medoid + * - diskann_node_clear_neighbor + * - Interaction between delete and concurrent search + */ +#include +#include +#include +#include +#include +#include "sqlite-vec.h" +#include "sqlite3.h" +#include + +static uint8_t fuzz_byte(const uint8_t **data, size_t *size, uint8_t def) { + if (*size == 0) return def; + uint8_t b = **data; + (*data)++; + (*size)--; + return b; +} + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (size < 20) return 0; + + int rc; + sqlite3 *db; + rc = sqlite3_open(":memory:", &db); + assert(rc == SQLITE_OK); + rc = sqlite3_vec_init(db, NULL, NULL); + assert(rc == SQLITE_OK); + + /* int8 quantizer to exercise that distance code path */ + uint8_t quant = fuzz_byte(&data, &size, 0) % 2; + const char *qname = quant ? "int8" : "binary"; + + char sql[256]; + snprintf(sql, sizeof(sql), + "CREATE VIRTUAL TABLE v USING vec0(" + "emb float[8] INDEXED BY diskann(neighbor_quantizer=%s, n_neighbors=8))", + qname); + rc = sqlite3_exec(db, sql, NULL, NULL, NULL); + if (rc != SQLITE_OK) { sqlite3_close(db); return 0; } + + sqlite3_stmt *stmtInsert = NULL, *stmtDelete = NULL, *stmtKnn = NULL; + sqlite3_prepare_v2(db, + "INSERT INTO v(rowid, emb) VALUES (?, ?)", -1, &stmtInsert, NULL); + sqlite3_prepare_v2(db, + "DELETE FROM v WHERE rowid = ?", -1, &stmtDelete, NULL); + sqlite3_prepare_v2(db, + "SELECT rowid, distance FROM v WHERE emb MATCH ? AND k = ?", + -1, &stmtKnn, NULL); + + if (!stmtInsert || !stmtDelete || !stmtKnn) goto cleanup; + + /* Phase 1: Build a graph of exactly n_neighbors+2 = 10 nodes. + * This makes every node nearly full, maximizing the chance that + * inserts trigger the "full node" path in add_reverse_edge. */ + for (int i = 1; i <= 10; i++) { + float vec[8]; + for (int j = 0; j < 8; j++) { + vec[j] = (float)((int8_t)fuzz_byte(&data, &size, (uint8_t)(i*13+j*7))) / 20.0f; + } + sqlite3_reset(stmtInsert); + sqlite3_bind_int64(stmtInsert, 1, i); + sqlite3_bind_blob(stmtInsert, 2, vec, sizeof(vec), SQLITE_TRANSIENT); + sqlite3_step(stmtInsert); + } + + /* Phase 2: Fuzz-driven delete-heavy workload */ + while (size >= 2) { + uint8_t op = fuzz_byte(&data, &size, 0); + uint8_t param = fuzz_byte(&data, &size, 0); + + switch (op % 6) { + case 0: /* Delete existing node */ + case 1: { /* (weighted toward deletes) */ + int64_t rowid = (int64_t)(param % 16) + 1; + sqlite3_reset(stmtDelete); + sqlite3_bind_int64(stmtDelete, 1, rowid); + sqlite3_step(stmtDelete); + break; + } + case 2: { /* Delete then immediately re-insert same rowid */ + int64_t rowid = (int64_t)(param % 10) + 1; + sqlite3_reset(stmtDelete); + sqlite3_bind_int64(stmtDelete, 1, rowid); + sqlite3_step(stmtDelete); + + float vec[8]; + for (int j = 0; j < 8; j++) { + vec[j] = (float)((int8_t)fuzz_byte(&data, &size, (uint8_t)(rowid+j))) / 15.0f; + } + sqlite3_reset(stmtInsert); + sqlite3_bind_int64(stmtInsert, 1, rowid); + sqlite3_bind_blob(stmtInsert, 2, vec, sizeof(vec), SQLITE_TRANSIENT); + sqlite3_step(stmtInsert); + break; + } + case 3: { /* KNN query on potentially sparse/empty graph */ + float qvec[8]; + for (int j = 0; j < 8; j++) { + qvec[j] = (float)((int8_t)fuzz_byte(&data, &size, 0)) / 10.0f; + } + int k = (param % 15) + 1; + sqlite3_reset(stmtKnn); + sqlite3_bind_blob(stmtKnn, 1, qvec, sizeof(qvec), SQLITE_TRANSIENT); + sqlite3_bind_int(stmtKnn, 2, k); + while (sqlite3_step(stmtKnn) == SQLITE_ROW) {} + break; + } + case 4: { /* Insert new node */ + int64_t rowid = (int64_t)(param % 32) + 1; + float vec[8]; + for (int j = 0; j < 8; j++) { + vec[j] = (float)((int8_t)fuzz_byte(&data, &size, 0)) / 10.0f; + } + sqlite3_reset(stmtInsert); + sqlite3_bind_int64(stmtInsert, 1, rowid); + sqlite3_bind_blob(stmtInsert, 2, vec, sizeof(vec), SQLITE_TRANSIENT); + sqlite3_step(stmtInsert); + break; + } + case 5: { /* Delete ALL remaining nodes, then insert fresh */ + for (int i = 1; i <= 32; i++) { + sqlite3_reset(stmtDelete); + sqlite3_bind_int64(stmtDelete, 1, i); + sqlite3_step(stmtDelete); + } + /* Now insert one node into empty graph */ + float vec[8] = {1.0f, 0, 0, 0, 0, 0, 0, 0}; + sqlite3_reset(stmtInsert); + sqlite3_bind_int64(stmtInsert, 1, 1); + sqlite3_bind_blob(stmtInsert, 2, vec, sizeof(vec), SQLITE_TRANSIENT); + sqlite3_step(stmtInsert); + break; + } + } + } + + /* Final KNN on whatever state the graph is in */ + { + float qvec[8] = {0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f}; + sqlite3_reset(stmtKnn); + sqlite3_bind_blob(stmtKnn, 1, qvec, sizeof(qvec), SQLITE_TRANSIENT); + sqlite3_bind_int(stmtKnn, 2, 10); + while (sqlite3_step(stmtKnn) == SQLITE_ROW) {} + } + +cleanup: + sqlite3_finalize(stmtInsert); + sqlite3_finalize(stmtDelete); + sqlite3_finalize(stmtKnn); + sqlite3_close(db); + return 0; +} diff --git a/tests/fuzz/diskann-graph-corrupt.c b/tests/fuzz/diskann-graph-corrupt.c new file mode 100644 index 00000000..a8dbc199 --- /dev/null +++ b/tests/fuzz/diskann-graph-corrupt.c @@ -0,0 +1,123 @@ +/** + * Fuzz target for DiskANN shadow table corruption resilience. + * Creates and populates a DiskANN table, then corrupts shadow table blobs + * using fuzz data and runs queries. + */ +#include +#include +#include +#include +#include +#include "sqlite-vec.h" +#include "sqlite3.h" +#include + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (size < 16) return 0; + + int rc; + sqlite3 *db; + + rc = sqlite3_open(":memory:", &db); + assert(rc == SQLITE_OK); + rc = sqlite3_vec_init(db, NULL, NULL); + assert(rc == SQLITE_OK); + + rc = sqlite3_exec(db, + "CREATE VIRTUAL TABLE v USING vec0(" + "emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=8))", + NULL, NULL, NULL); + if (rc != SQLITE_OK) { sqlite3_close(db); return 0; } + + /* Insert a few vectors to create graph structure */ + { + sqlite3_stmt *stmt; + sqlite3_prepare_v2(db, + "INSERT INTO v(rowid, emb) VALUES (?, ?)", -1, &stmt, NULL); + for (int i = 1; i <= 10; i++) { + float vec[8]; + for (int j = 0; j < 8; j++) { + vec[j] = (float)i * 0.1f + (float)j * 0.01f; + } + sqlite3_reset(stmt); + sqlite3_bind_int64(stmt, 1, i); + sqlite3_bind_blob(stmt, 2, vec, sizeof(vec), SQLITE_TRANSIENT); + sqlite3_step(stmt); + } + sqlite3_finalize(stmt); + } + + /* Corrupt shadow table data using fuzz bytes */ + size_t offset = 0; + + /* Determine which row and column to corrupt */ + int target_row = (data[offset++] % 10) + 1; + int corrupt_type = data[offset++] % 3; /* 0=validity, 1=neighbor_ids, 2=qvecs */ + + const char *column_name; + switch (corrupt_type) { + case 0: column_name = "neighbors_validity"; break; + case 1: column_name = "neighbor_ids"; break; + default: column_name = "neighbor_quantized_vectors"; break; + } + + /* Read the blob, corrupt it, write it back */ + { + sqlite3_stmt *readStmt; + char sqlbuf[256]; + snprintf(sqlbuf, sizeof(sqlbuf), + "SELECT %s FROM v_diskann_nodes00 WHERE rowid = ?", column_name); + rc = sqlite3_prepare_v2(db, sqlbuf, -1, &readStmt, NULL); + if (rc == SQLITE_OK) { + sqlite3_bind_int64(readStmt, 1, target_row); + if (sqlite3_step(readStmt) == SQLITE_ROW) { + const void *blob = sqlite3_column_blob(readStmt, 0); + int blobSize = sqlite3_column_bytes(readStmt, 0); + if (blob && blobSize > 0) { + unsigned char *corrupt = sqlite3_malloc(blobSize); + if (corrupt) { + memcpy(corrupt, blob, blobSize); + /* Apply fuzz bytes as XOR corruption */ + size_t remaining = size - offset; + for (size_t i = 0; i < remaining && i < (size_t)blobSize; i++) { + corrupt[i % blobSize] ^= data[offset + i]; + } + /* Write back */ + sqlite3_stmt *writeStmt; + snprintf(sqlbuf, sizeof(sqlbuf), + "UPDATE v_diskann_nodes00 SET %s = ? WHERE rowid = ?", column_name); + rc = sqlite3_prepare_v2(db, sqlbuf, -1, &writeStmt, NULL); + if (rc == SQLITE_OK) { + sqlite3_bind_blob(writeStmt, 1, corrupt, blobSize, SQLITE_TRANSIENT); + sqlite3_bind_int64(writeStmt, 2, target_row); + sqlite3_step(writeStmt); + sqlite3_finalize(writeStmt); + } + sqlite3_free(corrupt); + } + } + } + sqlite3_finalize(readStmt); + } + } + + /* Run queries on corrupted graph -- should not crash */ + { + float qvec[8] = {1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}; + sqlite3_stmt *knnStmt; + rc = sqlite3_prepare_v2(db, + "SELECT rowid, distance FROM v WHERE emb MATCH ? AND k = 5", + -1, &knnStmt, NULL); + if (rc == SQLITE_OK) { + sqlite3_bind_blob(knnStmt, 1, qvec, sizeof(qvec), SQLITE_STATIC); + while (sqlite3_step(knnStmt) == SQLITE_ROW) {} + sqlite3_finalize(knnStmt); + } + } + + /* Full scan */ + sqlite3_exec(db, "SELECT * FROM v", NULL, NULL, NULL); + + sqlite3_close(db); + return 0; +} diff --git a/tests/fuzz/diskann-int8-quant.c b/tests/fuzz/diskann-int8-quant.c new file mode 100644 index 00000000..b36918f0 --- /dev/null +++ b/tests/fuzz/diskann-int8-quant.c @@ -0,0 +1,162 @@ +/** + * Fuzz target for DiskANN int8 quantizer edge cases. + * + * The binary quantizer is simple (sign bit), but the int8 quantizer has + * interesting arithmetic: + * i8_val = (i8)(((src - (-1.0f)) / step) - 128.0f) + * where step = 2.0f / 255.0f, clamped to [-128, 127] before the cast + * + * Edge cases in this formula: + * - src values outside [-1, 1], which only the clamp keeps in i8 range + * - src very close to boundaries (-1.0, 1.0) -- rounding + * + * Also exercises int8 distance functions: + * - distance_l2_sqr_int8: accumulates squared differences, possible overflow + * - distance_cosine_int8: dot product with normalization + * - distance_l1_int8: absolute differences + * + * This fuzzer also tests the cosine distance metric path which the + * other fuzzers (using L2 default) don't cover. + */ +#include +#include +#include +#include +#include +#include +#include "sqlite-vec.h" +#include "sqlite3.h" +#include + +static uint8_t fuzz_byte(const uint8_t **data, size_t *size, uint8_t def) { + if (*size == 0) return def; + uint8_t b = **data; + (*data)++; + (*size)--; + return b; +} + +static float fuzz_extreme_float(const uint8_t **data, size_t *size) { + uint8_t mode = fuzz_byte(data, size, 0) % 8; + uint8_t raw = fuzz_byte(data, size, 0); + switch (mode) { + case 0: return (float)((int8_t)raw) / 10.0f; /* Normal range */ + case 1: return (float)((int8_t)raw) * 100.0f; /* Large values */ + case 2: return (float)((int8_t)raw) / 1000.0f; /* Tiny values near 0 */ + case 3: return -1.0f; /* Exact boundary */ + case 4: return 1.0f; /* Exact boundary */ + case 5: return 0.0f; /* Zero */ + case 6: return (float)raw / 255.0f; /* [0, 1] range */ + case 7: return -(float)raw / 255.0f; /* [-1, 0] range */ + } + return 0.0f; +} + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (size < 40) return 0; + + int rc; + sqlite3 *db; + rc = sqlite3_open(":memory:", &db); + assert(rc == SQLITE_OK); + rc = sqlite3_vec_init(db, NULL, NULL); + assert(rc == SQLITE_OK); + + /* Test both distance metrics with int8 quantizer */ + uint8_t metric_choice = fuzz_byte(&data, &size, 0) % 2; + const char *metric = metric_choice ? "cosine" : "L2"; + + int dims = 8 + (fuzz_byte(&data, &size, 0) % 3) * 8; /* 8, 16, or 24 */ + + char sql[512]; + snprintf(sql, sizeof(sql), + "CREATE VIRTUAL TABLE v USING vec0(" + "emb float[%d] distance_metric=%s " + "INDEXED BY diskann(neighbor_quantizer=int8, n_neighbors=8, search_list_size=16))", + dims, metric); + + rc = sqlite3_exec(db, sql, NULL, NULL, NULL); + if (rc != SQLITE_OK) { sqlite3_close(db); return 0; } + + sqlite3_stmt *stmtInsert = NULL, *stmtKnn = NULL, *stmtDelete = NULL; + sqlite3_prepare_v2(db, + "INSERT INTO v(rowid, emb) VALUES (?, ?)", -1, &stmtInsert, NULL); + sqlite3_prepare_v2(db, + "SELECT rowid, distance FROM v WHERE emb MATCH ? AND k = ?", + -1, &stmtKnn, NULL); + sqlite3_prepare_v2(db, + "DELETE FROM v WHERE rowid = ?", -1, &stmtDelete, NULL); + + if (!stmtInsert || !stmtKnn || !stmtDelete) goto cleanup; + + /* Insert vectors with extreme float values to stress quantization */ + float *vec = malloc(dims * sizeof(float)); + if (!vec) goto cleanup; + + for (int i = 1; i <= 16; i++) { + for (int j = 0; j < dims; j++) { + vec[j] = fuzz_extreme_float(&data, &size); + } + sqlite3_reset(stmtInsert); + sqlite3_bind_int64(stmtInsert, 1, i); + sqlite3_bind_blob(stmtInsert, 2, vec, dims * sizeof(float), SQLITE_TRANSIENT); + sqlite3_step(stmtInsert); + } + + /* Fuzz-driven operations */ + while (size >= 2) { + uint8_t op = fuzz_byte(&data, &size, 0) % 4; + uint8_t param = fuzz_byte(&data, &size, 0); + + switch (op) { + case 0: { /* KNN with extreme query values */ + for (int j = 0; j < dims; j++) { + vec[j] = fuzz_extreme_float(&data, &size); + } + int k = (param % 10) + 1; + sqlite3_reset(stmtKnn); + sqlite3_bind_blob(stmtKnn, 1, vec, dims * sizeof(float), SQLITE_TRANSIENT); + sqlite3_bind_int(stmtKnn, 2, k); + while (sqlite3_step(stmtKnn) == SQLITE_ROW) {} + break; + } + case 1: { /* Insert with extreme values */ + int64_t rowid = (int64_t)(param % 32) + 1; + for (int j = 0; j < dims; j++) { + vec[j] = fuzz_extreme_float(&data, &size); + } + sqlite3_reset(stmtInsert); + sqlite3_bind_int64(stmtInsert, 1, rowid); + sqlite3_bind_blob(stmtInsert, 2, vec, dims * sizeof(float), SQLITE_TRANSIENT); + sqlite3_step(stmtInsert); + break; + } + case 2: { /* Delete */ + int64_t rowid = (int64_t)(param % 32) + 1; + sqlite3_reset(stmtDelete); + sqlite3_bind_int64(stmtDelete, 1, rowid); + sqlite3_step(stmtDelete); + break; + } + case 3: { /* KNN with all-zero or all-same-value query */ + float val = (param % 3 == 0) ? 0.0f : + (param % 3 == 1) ? 1.0f : -1.0f; + for (int j = 0; j < dims; j++) vec[j] = val; + sqlite3_reset(stmtKnn); + sqlite3_bind_blob(stmtKnn, 1, vec, dims * sizeof(float), SQLITE_TRANSIENT); + sqlite3_bind_int(stmtKnn, 2, 5); + while (sqlite3_step(stmtKnn) == SQLITE_ROW) {} + break; + } + } + } + + free(vec); + +cleanup: + sqlite3_finalize(stmtInsert); + sqlite3_finalize(stmtKnn); + sqlite3_finalize(stmtDelete); + sqlite3_close(db); + return 0; +} diff --git a/tests/fuzz/diskann-operations.c b/tests/fuzz/diskann-operations.c new file mode 100644 index 00000000..b36620b8 --- /dev/null +++ b/tests/fuzz/diskann-operations.c @@ -0,0 +1,100 @@ +/** + * Fuzz target for DiskANN insert/delete/query operation sequences. + * Uses fuzz bytes to drive random operations on a DiskANN-indexed table. + */ +#include +#include +#include +#include +#include +#include "sqlite-vec.h" +#include "sqlite3.h" +#include + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (size < 6) return 0; + + int rc; + sqlite3 *db; + sqlite3_stmt *stmtInsert = NULL; + sqlite3_stmt *stmtDelete = NULL; + sqlite3_stmt *stmtKnn = NULL; + sqlite3_stmt *stmtScan = NULL; + + rc = sqlite3_open(":memory:", &db); + assert(rc == SQLITE_OK); + rc = sqlite3_vec_init(db, NULL, NULL); + assert(rc == SQLITE_OK); + + rc = sqlite3_exec(db, + "CREATE VIRTUAL TABLE v USING vec0(" + "emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=8))", + NULL, NULL, NULL); + if (rc != SQLITE_OK) { sqlite3_close(db); return 0; } + + sqlite3_prepare_v2(db, + "INSERT INTO v(rowid, emb) VALUES (?, ?)", -1, &stmtInsert, NULL); + sqlite3_prepare_v2(db, + "DELETE FROM v WHERE rowid = ?", -1, &stmtDelete, NULL); + sqlite3_prepare_v2(db, + "SELECT rowid, distance FROM v WHERE emb MATCH ? AND k = 3", + -1, &stmtKnn, NULL); + sqlite3_prepare_v2(db, + "SELECT rowid FROM v", -1, &stmtScan, NULL); + + if (!stmtInsert || !stmtDelete || !stmtKnn || !stmtScan) goto cleanup; + + size_t i = 0; + while (i + 2 <= size) { + uint8_t op = data[i++] % 4; + uint8_t rowid_byte = data[i++]; + int64_t rowid = (int64_t)(rowid_byte % 32) + 1; + + switch (op) { + case 0: { + /* INSERT: consume 32 bytes for 8 floats, or use what's left */ + float vec[8] = {0}; + for (int j = 0; j < 8 && i < size; j++, i++) { + vec[j] = (float)((int8_t)data[i]) / 10.0f; + } + sqlite3_reset(stmtInsert); + sqlite3_bind_int64(stmtInsert, 1, rowid); + sqlite3_bind_blob(stmtInsert, 2, vec, sizeof(vec), SQLITE_TRANSIENT); + sqlite3_step(stmtInsert); + break; + } + case 1: { + /* DELETE */ + sqlite3_reset(stmtDelete); + sqlite3_bind_int64(stmtDelete, 1, rowid); + sqlite3_step(stmtDelete); + break; + } + case 2: { + /* KNN query */ + float qvec[8] = {1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}; + sqlite3_reset(stmtKnn); + sqlite3_bind_blob(stmtKnn, 1, qvec, sizeof(qvec), SQLITE_STATIC); + while (sqlite3_step(stmtKnn) == SQLITE_ROW) {} + break; + } + case 3: { + /* Full scan */ + sqlite3_reset(stmtScan); + while (sqlite3_step(stmtScan) == SQLITE_ROW) {} + break; + } + } + } + + /* Final operations -- must not crash regardless of prior state */ + sqlite3_exec(db, "SELECT * FROM v", NULL, NULL, NULL); + +cleanup: + sqlite3_finalize(stmtInsert); + sqlite3_finalize(stmtDelete); + sqlite3_finalize(stmtKnn); + sqlite3_finalize(stmtScan); + sqlite3_close(db); + return 0; +} diff --git a/tests/fuzz/diskann-prune-direct.c b/tests/fuzz/diskann-prune-direct.c new file mode 100644 index 00000000..8fcd6f5e --- /dev/null +++ b/tests/fuzz/diskann-prune-direct.c @@ -0,0 +1,142 @@ +/** + * Fuzz target for DiskANN RobustPrune algorithm (diskann_prune_select). + * + * diskann_prune_select is exposed for testing and takes: + * - p_distances: N distances from node p to each candidate + * - num_candidates, alpha, max_neighbors + * - a function giving the distance between two candidates, here read from + * a flattened NxN matrix of inter-candidate distances + * + * This is a pure function that doesn't need a database, so we can + * call it directly with fuzz-controlled inputs. This gives the fuzzer + * maximum speed (no SQLite overhead) to explore: + * + * - alpha boundary: alpha=0 (prunes nothing), alpha=very large (prunes all) + * - max_neighbors = 0, 1, num_candidates, > num_candidates + * - num_candidates = 0, 1, large + * - Distance matrices with: all zeros, all same, negative values, NaN, Inf + * - Non-symmetric distance matrices (should still work) + * - Memory: large num_candidates to stress malloc + * + * Key code paths: + * - diskann_prune_select alpha-pruning loop + * - Boundary: selectedCount reaches max_neighbors exactly + * - All candidates pruned before max_neighbors reached + */ +#include +#include +#include +#include +#include +#include +#include "sqlite-vec.h" +#include "sqlite3.h" +#include + +/* Declare the test-exposed function. + * diskann_prune_select is not static -- it's a public symbol. */ +extern int diskann_prune_select(const float *p_distances, int num_candidates, + float alpha, int max_neighbors, + float (*distance)(void *ctx, int a, int b), + void *ctx, int *outSelected, int *outCount); + +struct matrix { + const float *distances; + int n; +}; + +static float matrix_distance(void *ctx, int a, int b) { + const struct matrix *m = ctx; + return m->distances[a * m->n + b]; +} + +static uint8_t fuzz_byte(const uint8_t **data, size_t *size, uint8_t def) { + if (*size == 0) return def; + uint8_t b = **data; + (*data)++; + (*size)--; + return b; +} + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (size < 8) return 0; + + /* Consume parameters from fuzz data */ + int num_candidates = fuzz_byte(&data, &size, 0) % 33; /* 0..32 */ + int max_neighbors = fuzz_byte(&data, &size, 0) % 17; /* 0..16 */ + + /* Alpha: pick from interesting values */ + uint8_t alpha_idx = fuzz_byte(&data, &size, 0) % 8; + float alpha_values[] = {0.0f, 0.5f, 1.0f, 1.2f, 1.5f, 2.0f, 10.0f, 100.0f}; + float alpha = alpha_values[alpha_idx]; + + if (num_candidates == 0) { + /* Test empty case */ + int outCount = -1; + int rc = diskann_prune_select(NULL, 0, alpha, max_neighbors, + matrix_distance, NULL, NULL, &outCount); + assert(rc == 0 /* SQLITE_OK */); + assert(outCount == 0); + return 0; + } + + /* Allocate arrays */ + int n = num_candidates; + float *inter_distances = malloc(n * n * sizeof(float)); + float *p_distances = malloc(n * sizeof(float)); + int *outSelected = malloc(n * sizeof(int)); + if (!inter_distances || !p_distances || !outSelected) { + free(inter_distances); + free(p_distances); + free(outSelected); + return 0; + } + + /* Fill p_distances from fuzz data (sorted ascending for correct input) */ + for (int i = 0; i < n; i++) { + uint8_t raw = fuzz_byte(&data, &size, (uint8_t)(i * 10)); + p_distances[i] = (float)raw / 10.0f; + } + /* Sort p_distances ascending (prune_select expects sorted input) */ + for (int i = 1; i < n; i++) { + float tmp = p_distances[i]; + int j = i - 1; + while (j >= 0 && p_distances[j] > tmp) { + p_distances[j + 1] = p_distances[j]; + j--; + } + p_distances[j + 1] = tmp; + } + + /* Fill inter-distance matrix from fuzz data */ + for (int i = 0; i < n * n; i++) { + uint8_t raw = fuzz_byte(&data, &size, (uint8_t)(i % 256)); + inter_distances[i] = (float)raw / 10.0f; + } + /* Make diagonal zero */ + for (int i = 0; i < n; i++) { + inter_distances[i * n + i] = 0.0f; + } + + int outCount = -1; + struct matrix m = {inter_distances, n}; + int rc = diskann_prune_select(p_distances, n, alpha, max_neighbors, + matrix_distance, &m, outSelected, &outCount); + /* Basic sanity: should not crash, count should be valid */ + assert(rc == 0); + assert(outCount >= 0); + assert(outCount <= max_neighbors || max_neighbors == 0); + assert(outCount <= n); + + /* Verify outSelected flags are consistent with outCount */ + int flagCount = 0; + for (int i = 0; i < n; i++) { + if (outSelected[i]) flagCount++; + } + assert(flagCount == outCount); + + free(inter_distances); + free(p_distances); + free(outSelected); + return 0; +} diff --git a/tests/fuzz/ivf-cell-overflow.c b/tests/fuzz/ivf-cell-overflow.c new file mode 100644 index 00000000..65ae6b2a --- /dev/null +++ b/tests/fuzz/ivf-cell-overflow.c @@ -0,0 +1,192 @@ +/** + * Fuzz target: IVF cell overflow and boundary conditions. + * + * Pushes cells past VEC0_IVF_CELL_MAX_VECTORS (64) to trigger cell + * splitting, then exercises blob I/O at slot boundaries. + * + * Targets: + * - Cell splitting when n_vectors reaches cap (64) + * - Blob offset arithmetic: slot * vecSize, slot / 8, slot % 8 + * - Validity bitmap at byte boundaries (slot 7->8, 15->16, etc.) + * - Insert into full cell -> create new cell path + * - Delete from various slot positions (first, last, middle) + * - Multiple cells per centroid + * - assign-vectors command with multi-cell centroids + */ +#include +#include +#include +#include +#include +#include "sqlite-vec.h" +#include "sqlite3.h" +#include + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (size < 8) return 0; + + int rc; + sqlite3 *db; + + rc = sqlite3_open(":memory:", &db); + assert(rc == SQLITE_OK); + rc = sqlite3_vec_init(db, NULL, NULL); + assert(rc == SQLITE_OK); + + // Use small dimensions for speed but enough vectors to overflow cells + int dim = (data[0] % 8) + 2; // 2..9 + int nlist = (data[1] % 4) + 1; // 1..4 + // We need >64 vectors to overflow a cell + int num_vecs = (data[2] % 64) + 65; // 65..128 + int delete_pattern = data[3]; // Controls which vectors to delete + + const uint8_t *payload = data + 4; + size_t payload_size = size - 4; + + char sql[256]; + snprintf(sql, sizeof(sql), + "CREATE VIRTUAL TABLE v USING vec0(" + "emb float[%d] indexed by ivf(nlist=%d, nprobe=%d))", + dim, nlist, nlist); + + rc = sqlite3_exec(db, sql, NULL, NULL, NULL); + if (rc != SQLITE_OK) { sqlite3_close(db); return 0; } + + // Insert enough vectors to overflow at least one cell + sqlite3_stmt *stmtInsert = NULL; + sqlite3_prepare_v2(db, + "INSERT INTO v(v, emb) VALUES (?, ?)", -1, &stmtInsert, NULL); + if (!stmtInsert) { sqlite3_close(db); return 0; } + + size_t offset = 0; + for (int i = 0; i < num_vecs; i++) { + float *vec = sqlite3_malloc(dim * sizeof(float)); + if (!vec) break; + for (int d = 0; d < dim; d++) { + if (offset < payload_size) { + vec[d] = ((float)(int8_t)payload[offset++]) / 50.0f; + } else { + // Cluster vectors near specific centroids to ensure some cells overflow + int cluster = i % nlist; + vec[d] = (float)cluster + (float)(i % 10) * 0.01f + d * 0.001f; + } + } + sqlite3_reset(stmtInsert); + sqlite3_bind_int64(stmtInsert, 1, (int64_t)(i + 1)); + sqlite3_bind_blob(stmtInsert, 2, vec, dim * sizeof(float), SQLITE_TRANSIENT); + sqlite3_step(stmtInsert); + sqlite3_free(vec); + } + sqlite3_finalize(stmtInsert); + + // Train to assign vectors to centroids (triggers cell building) + sqlite3_exec(db, + "INSERT INTO v(v) VALUES ('compute-centroids')", + NULL, NULL, NULL); + + // Delete vectors at boundary positions based on fuzz data + // This tests validity bitmap manipulation at different slot positions + for (int i = 0; i < num_vecs; i++) { + int byte_idx = i / 8; + if (byte_idx < (int)payload_size && (payload[byte_idx] & (1 << (i % 8)))) { + // Use delete_pattern to thin deletions + if ((delete_pattern + i) % 3 == 0) { + char delsql[64]; + snprintf(delsql, sizeof(delsql), "DELETE FROM v WHERE rowid = %d", i + 1); + sqlite3_exec(db, delsql, NULL, NULL, NULL); + } + } + } + + // Insert more vectors after deletions (into cells with holes) + { + sqlite3_stmt *si = NULL; + sqlite3_prepare_v2(db, + "INSERT INTO v(v, emb) VALUES (?, ?)", -1, &si, NULL); + if (si) { + for (int i = 0; i < 10; i++) { + float *vec = sqlite3_malloc(dim * sizeof(float)); + if (!vec) break; + for (int d = 0; d < dim; d++) + vec[d] = (float)(i + 200) * 0.01f; + sqlite3_reset(si); + sqlite3_bind_int64(si, 1, (int64_t)(num_vecs + i + 1)); + sqlite3_bind_blob(si, 2, vec, dim * sizeof(float), SQLITE_TRANSIENT); + sqlite3_step(si); + sqlite3_free(vec); + } + sqlite3_finalize(si); + } + } + + // KNN query that must scan multiple cells per centroid + { + float *qvec = sqlite3_malloc(dim * sizeof(float)); + if (qvec) { + for (int d = 0; d < dim; d++) qvec[d] = 0.0f; + sqlite3_stmt *sk = NULL; + snprintf(sql, sizeof(sql), + "SELECT rowid, distance FROM v WHERE emb MATCH ? LIMIT 20"); + sqlite3_prepare_v2(db, sql, -1, &sk, NULL); + if (sk) { + sqlite3_bind_blob(sk, 1, qvec, dim * sizeof(float), SQLITE_TRANSIENT); + while (sqlite3_step(sk) == SQLITE_ROW) {} + sqlite3_finalize(sk); + } + sqlite3_free(qvec); + } + } + + // Test assign-vectors with multi-cell state + // First clear centroids + sqlite3_exec(db, + "INSERT INTO v(v) VALUES ('clear-centroids')", + NULL, NULL, NULL); + + // Set centroids manually, then assign + for (int c = 0; c < nlist; c++) { + float *cvec = sqlite3_malloc(dim * sizeof(float)); + if (!cvec) break; + for (int d = 0; d < dim; d++) cvec[d] = (float)c + d * 0.1f; + + char cmd[128]; + snprintf(cmd, sizeof(cmd), + "INSERT INTO v(v, emb) VALUES ('set-centroid:%d', ?)", c); + sqlite3_stmt *sc = NULL; + sqlite3_prepare_v2(db, cmd, -1, &sc, NULL); + if (sc) { + sqlite3_bind_blob(sc, 1, cvec, dim * sizeof(float), SQLITE_TRANSIENT); + sqlite3_step(sc); + sqlite3_finalize(sc); + } + sqlite3_free(cvec); + } + + sqlite3_exec(db, + "INSERT INTO v(v) VALUES ('assign-vectors')", + NULL, NULL, NULL); + + // Final query after assign-vectors + { + float *qvec = sqlite3_malloc(dim * sizeof(float)); + if (qvec) { + for (int d = 0; d < dim; d++) qvec[d] = 1.0f; + sqlite3_stmt *sk = NULL; + sqlite3_prepare_v2(db, + "SELECT rowid, distance FROM v WHERE emb MATCH ? LIMIT 5", + -1, &sk, NULL); + if (sk) { + sqlite3_bind_blob(sk, 1, qvec, dim * sizeof(float), SQLITE_TRANSIENT); + while (sqlite3_step(sk) == SQLITE_ROW) {} + sqlite3_finalize(sk); + } + sqlite3_free(qvec); + } + } + + // Full scan + sqlite3_exec(db, "SELECT * FROM v", NULL, NULL, NULL); + + sqlite3_close(db); + return 0; +} diff --git a/tests/fuzz/ivf-create.c b/tests/fuzz/ivf-create.c new file mode 100644 index 00000000..222b67b2 --- /dev/null +++ b/tests/fuzz/ivf-create.c @@ -0,0 +1,36 @@ +#include +#include +#include +#include +#include +#include "sqlite-vec.h" +#include "sqlite3.h" +#include + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + int rc; + sqlite3 *db; + sqlite3_stmt *stmt; + + rc = sqlite3_open(":memory:", &db); + assert(rc == SQLITE_OK); + rc = sqlite3_vec_init(db, NULL, NULL); + assert(rc == SQLITE_OK); + + sqlite3_str *s = sqlite3_str_new(NULL); + assert(s); + sqlite3_str_appendall(s, "CREATE VIRTUAL TABLE v USING vec0(emb float[4] indexed by ivf("); + sqlite3_str_appendf(s, "%.*s", (int)size, data); + sqlite3_str_appendall(s, "))"); + const char *zSql = sqlite3_str_finish(s); + assert(zSql); + + rc = sqlite3_prepare_v2(db, zSql, -1, &stmt, NULL); + sqlite3_free((void *)zSql); + if (rc == SQLITE_OK) { + sqlite3_step(stmt); + } + sqlite3_finalize(stmt); + sqlite3_close(db); + return 0; +} diff --git a/tests/fuzz/ivf-create.dict b/tests/fuzz/ivf-create.dict new file mode 100644 index 00000000..9a014e7d --- /dev/null +++ b/tests/fuzz/ivf-create.dict @@ -0,0 +1,16 @@ +"nlist" +"nprobe" +"quantizer" +"oversample" +"binary" +"int8" +"none" +"=" +"," +"(" +")" +"0" +"1" +"128" +"65536" +"65537" diff --git a/tests/fuzz/ivf-kmeans.c b/tests/fuzz/ivf-kmeans.c new file mode 100644 index 00000000..1d37184e --- /dev/null +++ b/tests/fuzz/ivf-kmeans.c @@ -0,0 +1,180 @@ +/** + * Fuzz target: IVF k-means clustering. + * + * Builds a table, inserts fuzz-controlled vectors, then runs + * compute-centroids with fuzz-controlled parameters (nlist, max_iter, seed). + * Targets: + * - kmeans with N < k (clamping), N == 1, k == 1 + * - kmeans with duplicate/identical vectors (all distances zero) + * - kmeans with NaN/Inf vectors + * - Empty cluster reassignment path (farthest-point heuristic) + * - Large nlist relative to N + * - The compute-centroids:{json} command parsing + * - clear-centroids followed by compute-centroids (round-trip) + */ +#include +#include +#include +#include +#include +#include "sqlite-vec.h" +#include "sqlite3.h" +#include + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (size < 10) return 0; + + int rc; + sqlite3 *db; + + rc = sqlite3_open(":memory:", &db); + assert(rc == SQLITE_OK); + rc = sqlite3_vec_init(db, NULL, NULL); + assert(rc == SQLITE_OK); + + // Parse fuzz header + // Byte 0-1: dimension (1..128) + // Byte 2: nlist for CREATE (1..64) + // Byte 3: nlist override for compute-centroids (0 = use default) + // Byte 4: max_iter (1..50) + // Byte 5-8: seed + // Byte 9: num_vectors (1..64) + // Remaining: vector float data + + int dim = (data[0] | (data[1] << 8)) % 128 + 1; + int nlist_create = (data[2] % 64) + 1; + int nlist_override = data[3] % 65; // 0 means use table default + int max_iter = (data[4] % 50) + 1; + uint32_t seed = (uint32_t)data[5] | ((uint32_t)data[6] << 8) | + ((uint32_t)data[7] << 16) | ((uint32_t)data[8] << 24); + int num_vecs = (data[9] % 64) + 1; + + const uint8_t *payload = data + 10; + size_t payload_size = size - 10; + + char sql[256]; + snprintf(sql, sizeof(sql), + "CREATE VIRTUAL TABLE v USING vec0(" + "emb float[%d] indexed by ivf(nlist=%d, nprobe=%d))", + dim, nlist_create, nlist_create); + + rc = sqlite3_exec(db, sql, NULL, NULL, NULL); + if (rc != SQLITE_OK) { sqlite3_close(db); return 0; } + + // Insert vectors + sqlite3_stmt *stmtInsert = NULL; + sqlite3_prepare_v2(db, + "INSERT INTO v(v, emb) VALUES (?, ?)", -1, &stmtInsert, NULL); + if (!stmtInsert) { sqlite3_close(db); return 0; } + + size_t offset = 0; + for (int i = 0; i < num_vecs; i++) { + float *vec = sqlite3_malloc(dim * sizeof(float)); + if (!vec) break; + + for (int d = 0; d < dim; d++) { + if (offset + 4 <= payload_size) { + memcpy(&vec[d], payload + offset, sizeof(float)); + offset += 4; + } else if (offset < payload_size) { + // Scale to interesting range including values > 1, < -1 + vec[d] = ((float)(int8_t)payload[offset++]) / 5.0f; + } else { + // Reuse earlier bytes to fill remaining dimensions + vec[d] = (float)(i * dim + d) * 0.01f; + } + } + + sqlite3_reset(stmtInsert); + sqlite3_bind_int64(stmtInsert, 1, (int64_t)(i + 1)); + sqlite3_bind_blob(stmtInsert, 2, vec, dim * sizeof(float), SQLITE_TRANSIENT); + sqlite3_step(stmtInsert); + sqlite3_free(vec); + } + sqlite3_finalize(stmtInsert); + + // Exercise compute-centroids with JSON options + { + char cmd[256]; + snprintf(cmd, sizeof(cmd), + "INSERT INTO v(rowid) VALUES " + "('compute-centroids:{\"nlist\":%d,\"max_iterations\":%d,\"seed\":%u}')", + nlist_override, max_iter, seed); + sqlite3_exec(db, cmd, NULL, NULL, NULL); + } + + // KNN query after training + { + float *qvec = sqlite3_malloc(dim * sizeof(float)); + if (qvec) { + for (int d = 0; d < dim; d++) { + qvec[d] = (d < 3) ? 1.0f : 0.0f; + } + sqlite3_stmt *stmtKnn = NULL; + sqlite3_prepare_v2(db, + "SELECT rowid, distance FROM v WHERE emb MATCH ? LIMIT 5", + -1, &stmtKnn, NULL); + if (stmtKnn) { + sqlite3_bind_blob(stmtKnn, 1, qvec, dim * sizeof(float), SQLITE_TRANSIENT); + while (sqlite3_step(stmtKnn) == SQLITE_ROW) {} + sqlite3_finalize(stmtKnn); + } + sqlite3_free(qvec); + } + } + + // Clear centroids and re-compute to test round-trip + sqlite3_exec(db, + "INSERT INTO v(v) VALUES ('clear-centroids')", + NULL, NULL, NULL); + + // Insert a few more vectors in untrained state + { + sqlite3_stmt *si = NULL; + sqlite3_prepare_v2(db, + "INSERT INTO v(v, emb) VALUES (?, ?)", -1, &si, NULL); + if (si) { + for (int i = 0; i < 3; i++) { + float *vec = sqlite3_malloc(dim * sizeof(float)); + if (!vec) break; + for (int d = 0; d < dim; d++) vec[d] = (float)(i + 100) * 0.1f; + sqlite3_reset(si); + sqlite3_bind_int64(si, 1, (int64_t)(num_vecs + i + 1)); + sqlite3_bind_blob(si, 2, vec, dim * sizeof(float), SQLITE_TRANSIENT); + sqlite3_step(si); + sqlite3_free(vec); + } + sqlite3_finalize(si); + } + } + + // Re-train + sqlite3_exec(db, + "INSERT INTO v(v) VALUES ('compute-centroids')", + NULL, NULL, NULL); + + // Delete some rows after training, then query + sqlite3_exec(db, "DELETE FROM v WHERE rowid = 1", NULL, NULL, NULL); + sqlite3_exec(db, "DELETE FROM v WHERE rowid = 2", NULL, NULL, NULL); + + // Query after deletes + { + float *qvec = sqlite3_malloc(dim * sizeof(float)); + if (qvec) { + for (int d = 0; d < dim; d++) qvec[d] = 0.5f; + sqlite3_stmt *stmtKnn = NULL; + sqlite3_prepare_v2(db, + "SELECT rowid, distance FROM v WHERE emb MATCH ? LIMIT 10", + -1, &stmtKnn, NULL); + if (stmtKnn) { + sqlite3_bind_blob(stmtKnn, 1, qvec, dim * sizeof(float), SQLITE_TRANSIENT); + while (sqlite3_step(stmtKnn) == SQLITE_ROW) {} + sqlite3_finalize(stmtKnn); + } + sqlite3_free(qvec); + } + } + + sqlite3_close(db); + return 0; +} diff --git a/tests/fuzz/ivf-knn-deep.c b/tests/fuzz/ivf-knn-deep.c new file mode 100644 index 00000000..f5adb1e7 --- /dev/null +++ b/tests/fuzz/ivf-knn-deep.c @@ -0,0 +1,199 @@ +/** + * Fuzz target: IVF KNN search deep paths. + * + * Exercises the full KNN pipeline with fuzz-controlled: + * - nprobe values (including > nlist, =1, =nlist) + * - Query vectors (including adversarial floats) + * - Mix of trained/untrained state + * - Oversample + rescore path (quantizer=int8 with oversample>1) + * - Multiple interleaved KNN queries + * - Candidate array realloc path (many vectors in probed cells) + * + * Targets: + * - ivf_scan_cells_from_stmt: candidate realloc, distance computation + * - ivf_query_knn: centroid sorting, nprobe selection + * - Oversample rescore: re-ranking with full-precision vectors + * - qsort with NaN distances + */ +#include +#include +#include +#include +#include +#include "sqlite-vec.h" +#include "sqlite3.h" +#include + +static uint16_t read_u16(const uint8_t *p) { + return (uint16_t)(p[0] | (p[1] << 8)); +} + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (size < 16) return 0; + + int rc; + sqlite3 *db; + + rc = sqlite3_open(":memory:", &db); + assert(rc == SQLITE_OK); + rc = sqlite3_vec_init(db, NULL, NULL); + assert(rc == SQLITE_OK); + + // Header + int dim = (data[0] % 32) + 2; // 2..33 + int nlist = (data[1] % 16) + 1; // 1..16 + int nprobe_initial = (data[2] % 20) + 1; // 1..20 (can be > nlist) + int quantizer_type = data[3] % 3; // 0=none, 1=int8, 2=binary + int oversample = (data[4] % 4) + 1; // 1..4 + int num_vecs = (data[5] % 80) + 4; // 4..83 + int num_queries = (data[6] % 8) + 1; // 1..8 + int k_limit = (data[7] % 20) + 1; // 1..20 + + const uint8_t *payload = data + 8; + size_t payload_size = size - 8; + + // For binary quantizer, dimension must be multiple of 8 + if (quantizer_type == 2) { + dim = ((dim + 7) / 8) * 8; + if (dim == 0) dim = 8; + } + + const char *qname; + switch (quantizer_type) { + case 1: qname = "int8"; break; + case 2: qname = "binary"; break; + default: qname = "none"; break; + } + + // Oversample only valid with quantization + if (quantizer_type == 0) oversample = 1; + + // Cap nprobe to nlist for CREATE (parser rejects nprobe > nlist) + int nprobe_create = nprobe_initial <= nlist ? nprobe_initial : nlist; + + char sql[512]; + snprintf(sql, sizeof(sql), + "CREATE VIRTUAL TABLE v USING vec0(" + "emb float[%d] indexed by ivf(nlist=%d, nprobe=%d, quantizer=%s%s))", + dim, nlist, nprobe_create, qname, + oversample > 1 ? ", oversample=2" : ""); + + // If that fails (e.g. oversample with none), try without oversample + rc = sqlite3_exec(db, sql, NULL, NULL, NULL); + if (rc != SQLITE_OK) { + snprintf(sql, sizeof(sql), + "CREATE VIRTUAL TABLE v USING vec0(" + "emb float[%d] indexed by ivf(nlist=%d, nprobe=%d, quantizer=%s))", + dim, nlist, nprobe_create, qname); + rc = sqlite3_exec(db, sql, NULL, NULL, NULL); + if (rc != SQLITE_OK) { sqlite3_close(db); return 0; } + } + + // Insert vectors + sqlite3_stmt *stmtInsert = NULL; + sqlite3_prepare_v2(db, + "INSERT INTO v(v, emb) VALUES (?, ?)", -1, &stmtInsert, NULL); + if (!stmtInsert) { sqlite3_close(db); return 0; } + + size_t offset = 0; + for (int i = 0; i < num_vecs; i++) { + float *vec = sqlite3_malloc(dim * sizeof(float)); + if (!vec) break; + for (int d = 0; d < dim; d++) { + if (offset < payload_size) { + vec[d] = ((float)(int8_t)payload[offset++]) / 20.0f; + } else { + vec[d] = (float)((i * dim + d) % 256 - 128) / 128.0f; + } + } + sqlite3_reset(stmtInsert); + sqlite3_bind_int64(stmtInsert, 1, (int64_t)(i + 1)); + sqlite3_bind_blob(stmtInsert, 2, vec, dim * sizeof(float), SQLITE_TRANSIENT); + sqlite3_step(stmtInsert); + sqlite3_free(vec); + } + sqlite3_finalize(stmtInsert); + + // Query BEFORE training (flat scan path) + { + float *qvec = sqlite3_malloc(dim * sizeof(float)); + if (qvec) { + for (int d = 0; d < dim; d++) qvec[d] = 0.5f; + sqlite3_stmt *sk = NULL; + snprintf(sql, sizeof(sql), + "SELECT rowid, distance FROM v WHERE emb MATCH ? LIMIT %d", k_limit); + sqlite3_prepare_v2(db, sql, -1, &sk, NULL); + if (sk) { + sqlite3_bind_blob(sk, 1, qvec, dim * sizeof(float), SQLITE_TRANSIENT); + while (sqlite3_step(sk) == SQLITE_ROW) {} + sqlite3_finalize(sk); + } + sqlite3_free(qvec); + } + } + + // Train + sqlite3_exec(db, + "INSERT INTO v(v) VALUES ('compute-centroids')", + NULL, NULL, NULL); + + // Change nprobe at runtime (can exceed nlist -- tests clamping in query) + { + char cmd[64]; + snprintf(cmd, sizeof(cmd), + "INSERT INTO v(v) VALUES ('nprobe=%d')", nprobe_initial); + sqlite3_exec(db, cmd, NULL, NULL, NULL); + } + + // Multiple KNN queries with different fuzz-derived query vectors + for (int q = 0; q < num_queries; q++) { + float *qvec = sqlite3_malloc(dim * sizeof(float)); + if (!qvec) break; + for (int d = 0; d < dim; d++) { + if (offset < payload_size) { + qvec[d] = ((float)(int8_t)payload[offset++]) / 10.0f; + } else { + qvec[d] = (q == 0) ? 1.0f : 0.0f; + } + } + + sqlite3_stmt *sk = NULL; + snprintf(sql, sizeof(sql), + "SELECT rowid, distance FROM v WHERE emb MATCH ? LIMIT %d", k_limit); + sqlite3_prepare_v2(db, sql, -1, &sk, NULL); + if (sk) { + sqlite3_bind_blob(sk, 1, qvec, dim * sizeof(float), SQLITE_TRANSIENT); + while (sqlite3_step(sk) == SQLITE_ROW) {} + sqlite3_finalize(sk); + } + sqlite3_free(qvec); + } + + // Delete half the vectors then query again + for (int i = 1; i <= num_vecs / 2; i++) { + char delsql[64]; + snprintf(delsql, sizeof(delsql), "DELETE FROM v WHERE rowid = %d", i); + sqlite3_exec(db, delsql, NULL, NULL, NULL); + } + + // Query after mass deletion + { + float *qvec = sqlite3_malloc(dim * sizeof(float)); + if (qvec) { + for (int d = 0; d < dim; d++) qvec[d] = -0.5f; + sqlite3_stmt *sk = NULL; + snprintf(sql, sizeof(sql), + "SELECT rowid, distance FROM v WHERE emb MATCH ? LIMIT %d", k_limit); + sqlite3_prepare_v2(db, sql, -1, &sk, NULL); + if (sk) { + sqlite3_bind_blob(sk, 1, qvec, dim * sizeof(float), SQLITE_TRANSIENT); + while (sqlite3_step(sk) == SQLITE_ROW) {} + sqlite3_finalize(sk); + } + sqlite3_free(qvec); + } + } + + sqlite3_close(db); + return 0; +} diff --git a/tests/fuzz/ivf-operations.c b/tests/fuzz/ivf-operations.c new file mode 100644 index 00000000..c8d0c012 --- /dev/null +++ b/tests/fuzz/ivf-operations.c @@ -0,0 +1,121 @@ +#include +#include +#include +#include +#include +#include "sqlite-vec.h" +#include "sqlite3.h" +#include + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (size < 6) return 0; + + int rc; + sqlite3 *db; + sqlite3_stmt *stmtInsert = NULL; + sqlite3_stmt *stmtDelete = NULL; + sqlite3_stmt *stmtKnn = NULL; + sqlite3_stmt *stmtScan = NULL; + + rc = sqlite3_open(":memory:", &db); + assert(rc == SQLITE_OK); + rc = sqlite3_vec_init(db, NULL, NULL); + assert(rc == SQLITE_OK); + + rc = sqlite3_exec(db, + "CREATE VIRTUAL TABLE v USING vec0(emb float[4] indexed by ivf(nlist=4, nprobe=4))", + NULL, NULL, NULL); + if (rc != SQLITE_OK) { sqlite3_close(db); return 0; } + + sqlite3_prepare_v2(db, + "INSERT INTO v(v, emb) VALUES (?, ?)", -1, &stmtInsert, NULL); + sqlite3_prepare_v2(db, + "DELETE FROM v WHERE rowid = ?", -1, &stmtDelete, NULL); + sqlite3_prepare_v2(db, + "SELECT rowid, distance FROM v WHERE emb MATCH ? LIMIT 3", + -1, &stmtKnn, NULL); + sqlite3_prepare_v2(db, + "SELECT rowid FROM v", -1, &stmtScan, NULL); + + if (!stmtInsert || !stmtDelete || !stmtKnn || !stmtScan) goto cleanup; + + size_t i = 0; + while (i + 2 <= size) { + uint8_t op = data[i++] % 7; + uint8_t rowid_byte = data[i++]; + int64_t rowid = (int64_t)(rowid_byte % 32) + 1; + + switch (op) { + case 0: { + // INSERT: consume 16 bytes for 4 floats, or use what's left + float vec[4] = {0.0f, 0.0f, 0.0f, 0.0f}; + for (int j = 0; j < 4 && i < size; j++, i++) { + vec[j] = (float)((int8_t)data[i]) / 10.0f; + } + sqlite3_reset(stmtInsert); + sqlite3_bind_int64(stmtInsert, 1, rowid); + sqlite3_bind_blob(stmtInsert, 2, vec, sizeof(vec), SQLITE_TRANSIENT); + sqlite3_step(stmtInsert); + break; + } + case 1: { + // DELETE + sqlite3_reset(stmtDelete); + sqlite3_bind_int64(stmtDelete, 1, rowid); + sqlite3_step(stmtDelete); + break; + } + case 2: { + // KNN query with a fixed query vector + float qvec[4] = {1.0f, 0.0f, 0.0f, 0.0f}; + sqlite3_reset(stmtKnn); + sqlite3_bind_blob(stmtKnn, 1, qvec, sizeof(qvec), SQLITE_STATIC); + while (sqlite3_step(stmtKnn) == SQLITE_ROW) {} + break; + } + case 3: { + // Full scan + sqlite3_reset(stmtScan); + while (sqlite3_step(stmtScan) == SQLITE_ROW) {} + break; + } + case 4: { + // compute-centroids command + sqlite3_exec(db, + "INSERT INTO v(v) VALUES ('compute-centroids')", + NULL, NULL, NULL); + break; + } + case 5: { + // clear-centroids command + sqlite3_exec(db, + "INSERT INTO v(v) VALUES ('clear-centroids')", + NULL, NULL, NULL); + break; + } + case 6: { + // nprobe=N command + if (i < size) { + uint8_t n = data[i++]; + int nprobe = (n % 4) + 1; + char buf[64]; + snprintf(buf, sizeof(buf), + "INSERT INTO v(v) VALUES ('nprobe=%d')", nprobe); + sqlite3_exec(db, buf, NULL, NULL, NULL); + } + break; + } + } + } + + // Final operations — must not crash regardless of prior state + sqlite3_exec(db, "SELECT * FROM v", NULL, NULL, NULL); + +cleanup: + sqlite3_finalize(stmtInsert); + sqlite3_finalize(stmtDelete); + sqlite3_finalize(stmtKnn); + sqlite3_finalize(stmtScan); + sqlite3_close(db); + return 0; +} diff --git a/tests/fuzz/ivf-quantize.c b/tests/fuzz/ivf-quantize.c new file mode 100644 index 00000000..bc8800bf --- /dev/null +++ b/tests/fuzz/ivf-quantize.c @@ -0,0 +1,129 @@ +/** + * Fuzz target: IVF quantization functions. + * + * Directly exercises ivf_quantize_int8 and ivf_quantize_binary with + * fuzz-controlled dimensions and float data. Targets: + * - ivf_quantize_int8: clamping, int8 overflow boundary + * - ivf_quantize_binary: D not divisible by 8, memset(D/8) undercount + * - Round-trip through CREATE TABLE + INSERT with quantized IVF + */ +#include +#include +#include +#include +#include +#include +#include "sqlite-vec.h" +#include "sqlite3.h" +#include + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (size < 8) return 0; + + int rc; + sqlite3 *db; + + rc = sqlite3_open(":memory:", &db); + assert(rc == SQLITE_OK); + rc = sqlite3_vec_init(db, NULL, NULL); + assert(rc == SQLITE_OK); + + // Byte 0: quantizer type (0=int8, 1=binary) + // Byte 1: dimension (1..64, but we test edge cases) + // Byte 2: nlist (1..8) + // Byte 3: num_vectors to insert (1..32) + // Remaining: float data + int qtype = data[0] % 2; + int dim = (data[1] % 64) + 1; + int nlist = (data[2] % 8) + 1; + int num_vecs = (data[3] % 32) + 1; + const uint8_t *payload = data + 4; + size_t payload_size = size - 4; + + // For binary quantizer, D must be multiple of 8 to avoid the D/8 bug + // in production. But we explicitly want to test non-multiples too to + // find the bug. Use dim as-is. + const char *quantizer = qtype ? "binary" : "int8"; + + // Binary quantizer needs D multiple of 8 in current code, but let's + // test both valid and invalid dimensions to see what happens. + // For binary with non-multiple-of-8, the code does memset(dst, 0, D/8) + // which underallocates when D%8 != 0. + char sql[256]; + snprintf(sql, sizeof(sql), + "CREATE VIRTUAL TABLE v USING vec0(" + "emb float[%d] indexed by ivf(nlist=%d, nprobe=%d, quantizer=%s))", + dim, nlist, nlist, quantizer); + + rc = sqlite3_exec(db, sql, NULL, NULL, NULL); + if (rc != SQLITE_OK) { sqlite3_close(db); return 0; } + + // Insert vectors with fuzz-controlled float values + sqlite3_stmt *stmtInsert = NULL; + sqlite3_prepare_v2(db, + "INSERT INTO v(v, emb) VALUES (?, ?)", -1, &stmtInsert, NULL); + if (!stmtInsert) { sqlite3_close(db); return 0; } + + size_t offset = 0; + for (int i = 0; i < num_vecs && offset < payload_size; i++) { + // Build float vector from fuzz data + float *vec = sqlite3_malloc(dim * sizeof(float)); + if (!vec) break; + + for (int d = 0; d < dim; d++) { + if (offset + 4 <= payload_size) { + // Use raw bytes as float -- can produce NaN, Inf, denormals + memcpy(&vec[d], payload + offset, sizeof(float)); + offset += 4; + } else if (offset < payload_size) { + // Partial: use byte as scaled value + vec[d] = ((float)(int8_t)payload[offset++]) / 50.0f; + } else { + vec[d] = 0.0f; + } + } + + sqlite3_reset(stmtInsert); + sqlite3_bind_int64(stmtInsert, 1, (int64_t)(i + 1)); + sqlite3_bind_blob(stmtInsert, 2, vec, dim * sizeof(float), SQLITE_TRANSIENT); + sqlite3_step(stmtInsert); + sqlite3_free(vec); + } + sqlite3_finalize(stmtInsert); + + // Trigger compute-centroids to exercise kmeans + quantization together + sqlite3_exec(db, + "INSERT INTO v(v) VALUES ('compute-centroids')", + NULL, NULL, NULL); + + // KNN query with fuzz-derived query vector + { + float *qvec = sqlite3_malloc(dim * sizeof(float)); + if (qvec) { + for (int d = 0; d < dim; d++) { + if (offset < payload_size) { + qvec[d] = ((float)(int8_t)payload[offset++]) / 10.0f; + } else { + qvec[d] = 1.0f; + } + } + + sqlite3_stmt *stmtKnn = NULL; + sqlite3_prepare_v2(db, + "SELECT rowid, distance FROM v WHERE emb MATCH ? LIMIT 5", + -1, &stmtKnn, NULL); + if (stmtKnn) { + sqlite3_bind_blob(stmtKnn, 1, qvec, dim * sizeof(float), SQLITE_TRANSIENT); + while (sqlite3_step(stmtKnn) == SQLITE_ROW) {} + sqlite3_finalize(stmtKnn); + } + sqlite3_free(qvec); + } + } + + // Full scan + sqlite3_exec(db, "SELECT * FROM v", NULL, NULL, NULL); + + sqlite3_close(db); + return 0; +} diff --git a/tests/fuzz/ivf-rescore.c b/tests/fuzz/ivf-rescore.c new file mode 100644 index 00000000..3cddf88d --- /dev/null +++ b/tests/fuzz/ivf-rescore.c @@ -0,0 +1,182 @@ +/** + * Fuzz target: IVF oversample + rescore path. + * + * Specifically targets the code path where quantizer != none AND + * oversample > 1, which triggers: + * 1. Quantized KNN scan to collect oversample*k candidates + * 2. Full-precision vector lookup from _ivf_vectors table + * 3. Re-scoring with float32 distances + * 4. Re-sort and truncation + * + * This path has the most complex memory management in the KNN query: + * - Two separate distance computations (quantized + float) + * - Cross-table lookups (cells + vectors KV store) + * - Candidate array resizing + * - qsort over partially re-scored arrays + * + * Also tests the int8 + binary quantization round-trip fidelity + * under adversarial float inputs. + */ +#include +#include +#include +#include +#include +#include +#include "sqlite-vec.h" +#include "sqlite3.h" +#include + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (size < 12) return 0; + + int rc; + sqlite3 *db; + + rc = sqlite3_open(":memory:", &db); + assert(rc == SQLITE_OK); + rc = sqlite3_vec_init(db, NULL, NULL); + assert(rc == SQLITE_OK); + + // Header + int quantizer_type = (data[0] % 2) + 1; // 1=int8, 2=binary (never none) + int dim = (data[1] % 32) + 8; // 8..39 + int nlist = (data[2] % 8) + 1; // 1..8 + int oversample = (data[3] % 4) + 2; // 2..5 (always > 1) + int num_vecs = (data[4] % 60) + 8; // 8..67 + int k_limit = (data[5] % 15) + 1; // 1..15 + + const uint8_t *payload = data + 6; + size_t payload_size = size - 6; + + // Binary quantizer needs D multiple of 8 + if (quantizer_type == 2) { + dim = ((dim + 7) / 8) * 8; + } + + const char *qname = (quantizer_type == 1) ? "int8" : "binary"; + + char sql[512]; + snprintf(sql, sizeof(sql), + "CREATE VIRTUAL TABLE v USING vec0(" + "emb float[%d] indexed by ivf(nlist=%d, nprobe=%d, quantizer=%s, oversample=%d))", + dim, nlist, nlist, qname, oversample); + + rc = sqlite3_exec(db, sql, NULL, NULL, NULL); + if (rc != SQLITE_OK) { sqlite3_close(db); return 0; } + + // Insert vectors with diverse values + sqlite3_stmt *stmtInsert = NULL; + sqlite3_prepare_v2(db, + "INSERT INTO v(v, emb) VALUES (?, ?)", -1, &stmtInsert, NULL); + if (!stmtInsert) { sqlite3_close(db); return 0; } + + size_t offset = 0; + for (int i = 0; i < num_vecs; i++) { + float *vec = sqlite3_malloc(dim * sizeof(float)); + if (!vec) break; + for (int d = 0; d < dim; d++) { + if (offset + 4 <= payload_size) { + // Use raw bytes as float for adversarial values + memcpy(&vec[d], payload + offset, sizeof(float)); + offset += 4; + // Sanitize: replace NaN/Inf with bounded values to avoid + // poisoning the entire computation. We want edge values, + // not complete nonsense. + if (isnan(vec[d]) || isinf(vec[d])) { + vec[d] = (vec[d] > 0) ? 1e6f : -1e6f; + if (isnan(vec[d])) vec[d] = 0.0f; + } + } else if (offset < payload_size) { + vec[d] = ((float)(int8_t)payload[offset++]) / 30.0f; + } else { + vec[d] = (float)(i * dim + d) * 0.001f; + } + } + sqlite3_reset(stmtInsert); + sqlite3_bind_int64(stmtInsert, 1, (int64_t)(i + 1)); + sqlite3_bind_blob(stmtInsert, 2, vec, dim * sizeof(float), SQLITE_TRANSIENT); + sqlite3_step(stmtInsert); + sqlite3_free(vec); + } + sqlite3_finalize(stmtInsert); + + // Train + sqlite3_exec(db, + "INSERT INTO v(v) VALUES ('compute-centroids')", + NULL, NULL, NULL); + + // Multiple KNN queries to exercise rescore path + for (int q = 0; q < 4; q++) { + float *qvec = sqlite3_malloc(dim * sizeof(float)); + if (!qvec) break; + for (int d = 0; d < dim; d++) { + if (offset < payload_size) { + qvec[d] = ((float)(int8_t)payload[offset++]) / 10.0f; + } else { + qvec[d] = (q == 0) ? 1.0f : (q == 1) ? -1.0f : 0.0f; + } + } + + sqlite3_stmt *sk = NULL; + snprintf(sql, sizeof(sql), + "SELECT rowid, distance FROM v WHERE emb MATCH ? LIMIT %d", k_limit); + sqlite3_prepare_v2(db, sql, -1, &sk, NULL); + if (sk) { + sqlite3_bind_blob(sk, 1, qvec, dim * sizeof(float), SQLITE_TRANSIENT); + while (sqlite3_step(sk) == SQLITE_ROW) {} + sqlite3_finalize(sk); + } + sqlite3_free(qvec); + } + + // Delete some vectors, then query again (rescore with missing _ivf_vectors rows) + for (int i = 1; i <= num_vecs / 3; i++) { + char delsql[64]; + snprintf(delsql, sizeof(delsql), "DELETE FROM v WHERE rowid = %d", i); + sqlite3_exec(db, delsql, NULL, NULL, NULL); + } + + { + float *qvec = sqlite3_malloc(dim * sizeof(float)); + if (qvec) { + for (int d = 0; d < dim; d++) qvec[d] = 0.5f; + sqlite3_stmt *sk = NULL; + snprintf(sql, sizeof(sql), + "SELECT rowid, distance FROM v WHERE emb MATCH ? LIMIT %d", k_limit); + sqlite3_prepare_v2(db, sql, -1, &sk, NULL); + if (sk) { + sqlite3_bind_blob(sk, 1, qvec, dim * sizeof(float), SQLITE_TRANSIENT); + while (sqlite3_step(sk) == SQLITE_ROW) {} + sqlite3_finalize(sk); + } + sqlite3_free(qvec); + } + } + + // Retrain after deletions + sqlite3_exec(db, + "INSERT INTO v(v) VALUES ('compute-centroids')", + NULL, NULL, NULL); + + // Query after retrain + { + float *qvec = sqlite3_malloc(dim * sizeof(float)); + if (qvec) { + for (int d = 0; d < dim; d++) qvec[d] = -0.3f; + sqlite3_stmt *sk = NULL; + snprintf(sql, sizeof(sql), + "SELECT rowid, distance FROM v WHERE emb MATCH ? LIMIT %d", k_limit); + sqlite3_prepare_v2(db, sql, -1, &sk, NULL); + if (sk) { + sqlite3_bind_blob(sk, 1, qvec, dim * sizeof(float), SQLITE_TRANSIENT); + while (sqlite3_step(sk) == SQLITE_ROW) {} + sqlite3_finalize(sk); + } + sqlite3_free(qvec); + } + } + + sqlite3_close(db); + return 0; +} diff --git a/tests/fuzz/ivf-shadow-corrupt.c b/tests/fuzz/ivf-shadow-corrupt.c new file mode 100644 index 00000000..74d72c34 --- /dev/null +++ b/tests/fuzz/ivf-shadow-corrupt.c @@ -0,0 +1,228 @@ +/** + * Fuzz target: IVF shadow table corruption. + * + * Creates a trained IVF table, then corrupts IVF shadow table blobs + * (centroids, cells validity/rowids/vectors, rowid_map) with fuzz data. + * Then exercises all read/write paths. Must not crash. + * + * Targets: + * - Cell validity bitmap with wrong size + * - Cell rowids blob with wrong size/alignment + * - Cell vectors blob with wrong size + * - Centroid blob with wrong size + * - n_vectors inconsistent with validity bitmap + * - Missing rowid_map entries + * - KNN scan over corrupted cells + * - Insert/delete with corrupted rowid_map + */ +#include +#include +#include +#include +#include +#include "sqlite-vec.h" +#include "sqlite3.h" +#include + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (size < 4) return 0; + + int rc; + sqlite3 *db; + + rc = sqlite3_open(":memory:", &db); + assert(rc == SQLITE_OK); + rc = sqlite3_vec_init(db, NULL, NULL); + assert(rc == SQLITE_OK); + + // Create IVF table and insert enough vectors to train + rc = sqlite3_exec(db, + "CREATE VIRTUAL TABLE v USING vec0(" + "emb float[8] indexed by ivf(nlist=2, nprobe=2))", + NULL, NULL, NULL); + if (rc != SQLITE_OK) { sqlite3_close(db); return 0; } + + // Insert 10 vectors + { + sqlite3_stmt *si = NULL; + sqlite3_prepare_v2(db, + "INSERT INTO v(v, emb) VALUES (?, ?)", -1, &si, NULL); + if (!si) { sqlite3_close(db); return 0; } + for (int i = 0; i < 10; i++) { + float vec[8]; + for (int d = 0; d < 8; d++) { + vec[d] = (float)(i * 8 + d) * 0.1f; + } + sqlite3_reset(si); + sqlite3_bind_int64(si, 1, i + 1); + sqlite3_bind_blob(si, 2, vec, sizeof(vec), SQLITE_TRANSIENT); + sqlite3_step(si); + } + sqlite3_finalize(si); + } + + // Train + sqlite3_exec(db, + "INSERT INTO v(v) VALUES ('compute-centroids')", + NULL, NULL, NULL); + + // Now corrupt shadow tables based on fuzz input + uint8_t target = data[0] % 10; + const uint8_t *payload = data + 1; + int payload_size = (int)(size - 1); + + // Limit payload to avoid huge allocations + if (payload_size > 4096) payload_size = 4096; + + sqlite3_stmt *stmt = NULL; + + switch (target) { + case 0: { + // Corrupt cell validity blob + rc = sqlite3_prepare_v2(db, + "UPDATE v_ivf_cells00 SET validity = ? WHERE rowid = 1", + -1, &stmt, NULL); + if (rc == SQLITE_OK) { + sqlite3_bind_blob(stmt, 1, payload, payload_size, SQLITE_STATIC); + sqlite3_step(stmt); sqlite3_finalize(stmt); + } + break; + } + case 1: { + // Corrupt cell rowids blob + rc = sqlite3_prepare_v2(db, + "UPDATE v_ivf_cells00 SET rowids = ? WHERE rowid = 1", + -1, &stmt, NULL); + if (rc == SQLITE_OK) { + sqlite3_bind_blob(stmt, 1, payload, payload_size, SQLITE_STATIC); + sqlite3_step(stmt); sqlite3_finalize(stmt); + } + break; + } + case 2: { + // Corrupt cell vectors blob + rc = sqlite3_prepare_v2(db, + "UPDATE v_ivf_cells00 SET vectors = ? WHERE rowid = 1", + -1, &stmt, NULL); + if (rc == SQLITE_OK) { + sqlite3_bind_blob(stmt, 1, payload, payload_size, SQLITE_STATIC); + sqlite3_step(stmt); sqlite3_finalize(stmt); + } + break; + } + case 3: { + // Corrupt centroid blob + rc = sqlite3_prepare_v2(db, + "UPDATE v_ivf_centroids00 SET centroid = ? WHERE centroid_id = 0", + -1, &stmt, NULL); + if (rc == SQLITE_OK) { + sqlite3_bind_blob(stmt, 1, payload, payload_size, SQLITE_STATIC); + sqlite3_step(stmt); sqlite3_finalize(stmt); + } + break; + } + case 4: { + // Set n_vectors to a bogus value (larger than cell capacity) + int bogus_n = 99999; + if (payload_size >= 4) { + memcpy(&bogus_n, payload, 4); + bogus_n = abs(bogus_n) % 100000; + } + char sql[128]; + snprintf(sql, sizeof(sql), + "UPDATE v_ivf_cells00 SET n_vectors = %d WHERE rowid = 1", bogus_n); + sqlite3_exec(db, sql, NULL, NULL, NULL); + break; + } + case 5: { + // Delete rowid_map entries (orphan vectors) + sqlite3_exec(db, + "DELETE FROM v_ivf_rowid_map00 WHERE rowid IN (1, 2, 3)", + NULL, NULL, NULL); + break; + } + case 6: { + // Corrupt rowid_map slot values + char sql[128]; + int bogus_slot = payload_size > 0 ? (int)payload[0] * 1000 : 99999; + snprintf(sql, sizeof(sql), + "UPDATE v_ivf_rowid_map00 SET slot = %d WHERE rowid = 1", bogus_slot); + sqlite3_exec(db, sql, NULL, NULL, NULL); + break; + } + case 7: { + // Corrupt rowid_map cell_id values + sqlite3_exec(db, + "UPDATE v_ivf_rowid_map00 SET cell_id = 99999 WHERE rowid = 1", + NULL, NULL, NULL); + break; + } + case 8: { + // Delete all centroids (make trained but no centroids) + sqlite3_exec(db, + "DELETE FROM v_ivf_centroids00", + NULL, NULL, NULL); + break; + } + case 9: { + // Set validity to NULL + sqlite3_exec(db, + "UPDATE v_ivf_cells00 SET validity = NULL WHERE rowid = 1", + NULL, NULL, NULL); + break; + } + } + + // Exercise all read paths over corrupted state — must not crash + float qvec[8] = {1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}; + + // KNN query + { + sqlite3_stmt *sk = NULL; + sqlite3_prepare_v2(db, + "SELECT rowid, distance FROM v WHERE emb MATCH ? LIMIT 5", + -1, &sk, NULL); + if (sk) { + sqlite3_bind_blob(sk, 1, qvec, sizeof(qvec), SQLITE_STATIC); + while (sqlite3_step(sk) == SQLITE_ROW) {} + sqlite3_finalize(sk); + } + } + + // Full scan + sqlite3_exec(db, "SELECT * FROM v", NULL, NULL, NULL); + + // Point query + sqlite3_exec(db, "SELECT * FROM v WHERE rowid = 1", NULL, NULL, NULL); + sqlite3_exec(db, "SELECT * FROM v WHERE rowid = 5", NULL, NULL, NULL); + + // Delete + sqlite3_exec(db, "DELETE FROM v WHERE rowid = 3", NULL, NULL, NULL); + + // Insert after corruption + { + float newvec[8] = {0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f}; + sqlite3_stmt *si = NULL; + sqlite3_prepare_v2(db, + "INSERT INTO v(v, emb) VALUES (?, ?)", -1, &si, NULL); + if (si) { + sqlite3_bind_int64(si, 1, 100); + sqlite3_bind_blob(si, 2, newvec, sizeof(newvec), SQLITE_STATIC); + sqlite3_step(si); + sqlite3_finalize(si); + } + } + + // compute-centroids over corrupted state + sqlite3_exec(db, + "INSERT INTO v(v) VALUES ('compute-centroids')", + NULL, NULL, NULL); + + // clear-centroids + sqlite3_exec(db, + "INSERT INTO v(v) VALUES ('clear-centroids')", + NULL, NULL, NULL); + + sqlite3_close(db); + return 0; +} diff --git a/tests/fuzz/rescore-create.c b/tests/fuzz/rescore-create.c new file mode 100644 index 00000000..3e69d6db --- /dev/null +++ b/tests/fuzz/rescore-create.c @@ -0,0 +1,36 @@ +#include +#include +#include +#include +#include +#include "sqlite-vec.h" +#include "sqlite3.h" +#include + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + int rc = SQLITE_OK; + sqlite3 *db; + sqlite3_stmt *stmt; + + rc = sqlite3_open(":memory:", &db); + assert(rc == SQLITE_OK); + rc = sqlite3_vec_init(db, NULL, NULL); + assert(rc == SQLITE_OK); + + sqlite3_str *s = sqlite3_str_new(NULL); + assert(s); + sqlite3_str_appendall(s, "CREATE VIRTUAL TABLE v USING vec0(emb float[128] indexed by rescore("); + sqlite3_str_appendf(s, "%.*s", (int)size, data); + sqlite3_str_appendall(s, "))"); + const char *zSql = sqlite3_str_finish(s); + assert(zSql); + + rc = sqlite3_prepare_v2(db, zSql, -1, &stmt, NULL); + sqlite3_free((void *)zSql); + if (rc == SQLITE_OK) { + sqlite3_step(stmt); + } + sqlite3_finalize(stmt); + sqlite3_close(db); + return 0; +} diff --git a/tests/fuzz/rescore-create.dict b/tests/fuzz/rescore-create.dict new file mode 100644 index 00000000..a8adf71f --- /dev/null +++ b/tests/fuzz/rescore-create.dict @@ -0,0 +1,20 @@ +"rescore" +"quantizer" +"bit" +"int8" +"oversample" +"indexed" +"by" +"float" +"(" +")" +"," +"=" +"[" +"]" +"1" +"8" +"16" +"128" +"256" +"1024" diff --git a/tests/fuzz/rescore-interleave.c b/tests/fuzz/rescore-interleave.c new file mode 100644 index 00000000..74e8b8d7 --- /dev/null +++ b/tests/fuzz/rescore-interleave.c @@ -0,0 +1,151 @@ +#include +#include +#include +#include +#include +#include "sqlite-vec.h" +#include "sqlite3.h" +#include + +/** + * Fuzz target: interleaved insert/update/delete/KNN operations on rescore + * tables with BOTH quantizer types, exercising the int8 quantizer path + * and the update code path that the existing rescore-operations.c misses. + * + * Key differences from rescore-operations.c: + * - Tests BOTH bit and int8 quantizers (the existing target only tests bit) + * - Fuzz-controlled query vectors (not fixed [1,0,0,...]) + * - Exercises the UPDATE path (line 9080+ in sqlite-vec.c) + * - Tests with 16 dimensions (more realistic, exercises more of the + * quantization loop) + * - Interleaves KNN between mutations to stress the blob_reopen path + * when _rescore_vectors rows have been deleted/modified + */ +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (size < 8) return 0; + + int rc; + sqlite3 *db; + sqlite3_stmt *stmtInsert = NULL; + sqlite3_stmt *stmtUpdate = NULL; + sqlite3_stmt *stmtDelete = NULL; + sqlite3_stmt *stmtKnn = NULL; + + rc = sqlite3_open(":memory:", &db); + assert(rc == SQLITE_OK); + rc = sqlite3_vec_init(db, NULL, NULL); + assert(rc == SQLITE_OK); + + /* Use first byte to pick quantizer */ + int use_int8 = data[0] & 1; + data++; size--; + + const char *create_sql = use_int8 + ? "CREATE VIRTUAL TABLE v USING vec0(" + "emb float[16] indexed by rescore(quantizer=int8))" + : "CREATE VIRTUAL TABLE v USING vec0(" + "emb float[16] indexed by rescore(quantizer=bit))"; + + rc = sqlite3_exec(db, create_sql, NULL, NULL, NULL); + if (rc != SQLITE_OK) { sqlite3_close(db); return 0; } + + sqlite3_prepare_v2(db, + "INSERT INTO v(rowid, emb) VALUES (?, ?)", -1, &stmtInsert, NULL); + sqlite3_prepare_v2(db, + "UPDATE v SET emb = ? WHERE rowid = ?", -1, &stmtUpdate, NULL); + sqlite3_prepare_v2(db, + "DELETE FROM v WHERE rowid = ?", -1, &stmtDelete, NULL); + sqlite3_prepare_v2(db, + "SELECT rowid, distance FROM v WHERE emb MATCH ? " + "ORDER BY distance LIMIT 5", -1, &stmtKnn, NULL); + + if (!stmtInsert || !stmtUpdate || !stmtDelete || !stmtKnn) + goto cleanup; + + size_t i = 0; + while (i + 2 <= size) { + uint8_t op = data[i++] % 5; /* 5 operations now */ + uint8_t rowid_byte = data[i++]; + int64_t rowid = (int64_t)(rowid_byte % 24) + 1; + + switch (op) { + case 0: { + /* INSERT: consume bytes for 16 floats */ + float vec[16] = {0}; + for (int j = 0; j < 16 && i < size; j++, i++) { + vec[j] = (float)((int8_t)data[i]) / 8.0f; + } + sqlite3_reset(stmtInsert); + sqlite3_bind_int64(stmtInsert, 1, rowid); + sqlite3_bind_blob(stmtInsert, 2, vec, sizeof(vec), SQLITE_TRANSIENT); + sqlite3_step(stmtInsert); + break; + } + case 1: { + /* DELETE */ + sqlite3_reset(stmtDelete); + sqlite3_bind_int64(stmtDelete, 1, rowid); + sqlite3_step(stmtDelete); + break; + } + case 2: { + /* KNN with fuzz-controlled query vector */ + float qvec[16] = {0}; + for (int j = 0; j < 16 && i < size; j++, i++) { + qvec[j] = (float)((int8_t)data[i]) / 4.0f; + } + sqlite3_reset(stmtKnn); + sqlite3_bind_blob(stmtKnn, 1, qvec, sizeof(qvec), SQLITE_STATIC); + while (sqlite3_step(stmtKnn) == SQLITE_ROW) { + (void)sqlite3_column_int64(stmtKnn, 0); + (void)sqlite3_column_double(stmtKnn, 1); + } + break; + } + case 3: { + /* UPDATE: modify an existing vector (exercises rescore update path) */ + float vec[16] = {0}; + for (int j = 0; j < 16 && i < size; j++, i++) { + vec[j] = (float)((int8_t)data[i]) / 6.0f; + } + sqlite3_reset(stmtUpdate); + sqlite3_bind_blob(stmtUpdate, 1, vec, sizeof(vec), SQLITE_TRANSIENT); + sqlite3_bind_int64(stmtUpdate, 2, rowid); + sqlite3_step(stmtUpdate); + break; + } + case 4: { + /* INSERT then immediately UPDATE same row (stresses blob lifecycle) */ + float vec1[16] = {0}; + float vec2[16] = {0}; + for (int j = 0; j < 16 && i < size; j++, i++) { + vec1[j] = (float)((int8_t)data[i]) / 10.0f; + vec2[j] = -vec1[j]; /* opposite direction */ + } + /* Insert */ + sqlite3_reset(stmtInsert); + sqlite3_bind_int64(stmtInsert, 1, rowid); + sqlite3_bind_blob(stmtInsert, 2, vec1, sizeof(vec1), SQLITE_TRANSIENT); + if (sqlite3_step(stmtInsert) == SQLITE_DONE) { + /* Only update if insert succeeded (rowid might already exist) */ + sqlite3_reset(stmtUpdate); + sqlite3_bind_blob(stmtUpdate, 1, vec2, sizeof(vec2), SQLITE_TRANSIENT); + sqlite3_bind_int64(stmtUpdate, 2, rowid); + sqlite3_step(stmtUpdate); + } + break; + } + } + } + + /* Final consistency check: full scan must not crash */ + sqlite3_exec(db, "SELECT * FROM v", NULL, NULL, NULL); + +cleanup: + sqlite3_finalize(stmtInsert); + sqlite3_finalize(stmtUpdate); + sqlite3_finalize(stmtDelete); + sqlite3_finalize(stmtKnn); + sqlite3_close(db); + return 0; +} diff --git a/tests/fuzz/rescore-knn-deep.c b/tests/fuzz/rescore-knn-deep.c new file mode 100644 index 00000000..8ff3c378 --- /dev/null +++ b/tests/fuzz/rescore-knn-deep.c @@ -0,0 +1,178 @@ +#include +#include +#include +#include +#include +#include "sqlite-vec.h" +#include "sqlite3.h" +#include + +/** + * Fuzz target: deep exercise of rescore KNN with fuzz-controlled query vectors + * and both quantizer types (bit + int8), multiple distance metrics. + * + * The existing rescore-operations.c only tests bit quantizer with a fixed + * query vector. This target: + * - Tests both bit and int8 quantizers + * - Uses fuzz-controlled query vectors (hits NaN/Inf/denormal paths) + * - Tests all distance metrics with int8 (L2, cosine, L1) + * - Exercises large LIMIT values (oversample multiplication) + * - Tests KNN with rowid IN constraints + * - Exercises the insert->query->update->query->delete->query cycle + */ +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (size < 20) return 0; + + int rc; + sqlite3 *db; + + rc = sqlite3_open(":memory:", &db); + assert(rc == SQLITE_OK); + rc = sqlite3_vec_init(db, NULL, NULL); + assert(rc == SQLITE_OK); + + /* Use first 4 bytes for configuration */ + uint8_t config = data[0]; + uint8_t num_inserts = (data[1] % 20) + 3; /* 3..22 inserts */ + uint8_t limit_val = (data[2] % 50) + 1; /* 1..50 for LIMIT */ + uint8_t metric_choice = data[3] % 3; + data += 4; + size -= 4; + + int use_int8 = config & 1; + const char *metric_str; + switch (metric_choice) { + case 0: metric_str = ""; break; /* default L2 */ + case 1: metric_str = " distance_metric=cosine"; break; + case 2: metric_str = " distance_metric=l1"; break; + default: metric_str = ""; break; + } + + /* Build CREATE TABLE statement */ + char create_sql[256]; + if (use_int8) { + snprintf(create_sql, sizeof(create_sql), + "CREATE VIRTUAL TABLE v USING vec0(" + "emb float[16] indexed by rescore(quantizer=int8)%s)", metric_str); + } else { + /* bit quantizer ignores distance_metric for the coarse pass (always hamming), + but the float rescore phase uses the specified metric */ + snprintf(create_sql, sizeof(create_sql), + "CREATE VIRTUAL TABLE v USING vec0(" + "emb float[16] indexed by rescore(quantizer=bit)%s)", metric_str); + } + + rc = sqlite3_exec(db, create_sql, NULL, NULL, NULL); + if (rc != SQLITE_OK) { sqlite3_close(db); return 0; } + + /* Insert vectors using fuzz data */ + { + sqlite3_stmt *ins = NULL; + sqlite3_prepare_v2(db, + "INSERT INTO v(rowid, emb) VALUES (?, ?)", -1, &ins, NULL); + if (!ins) { sqlite3_close(db); return 0; } + + size_t cursor = 0; + for (int i = 0; i < num_inserts && cursor + 1 < size; i++) { + float vec[16]; + for (int j = 0; j < 16; j++) { + if (cursor < size) { + /* Map fuzz byte to float -- includes potential for + interesting float values via reinterpretation */ + int8_t sb = (int8_t)data[cursor++]; + vec[j] = (float)sb / 5.0f; + } else { + vec[j] = 0.0f; + } + } + sqlite3_reset(ins); + sqlite3_bind_int64(ins, 1, (sqlite3_int64)(i + 1)); + sqlite3_bind_blob(ins, 2, vec, sizeof(vec), SQLITE_TRANSIENT); + sqlite3_step(ins); + } + sqlite3_finalize(ins); + } + + /* Build a fuzz-controlled query vector from remaining data */ + float qvec[16] = {0}; + { + size_t cursor = 0; + for (int j = 0; j < 16 && cursor < size; j++) { + int8_t sb = (int8_t)data[cursor++]; + qvec[j] = (float)sb / 3.0f; + } + } + + /* KNN query with fuzz-controlled vector and LIMIT */ + { + char knn_sql[256]; + snprintf(knn_sql, sizeof(knn_sql), + "SELECT rowid, distance FROM v WHERE emb MATCH ? " + "ORDER BY distance LIMIT %d", (int)limit_val); + + sqlite3_stmt *knn = NULL; + sqlite3_prepare_v2(db, knn_sql, -1, &knn, NULL); + if (knn) { + sqlite3_bind_blob(knn, 1, qvec, sizeof(qvec), SQLITE_STATIC); + while (sqlite3_step(knn) == SQLITE_ROW) { + /* Read results to ensure distance computation didn't produce garbage + that crashes the cursor iteration */ + (void)sqlite3_column_int64(knn, 0); + (void)sqlite3_column_double(knn, 1); + } + sqlite3_finalize(knn); + } + } + + /* Update some vectors, then query again */ + { + float uvec[16]; + for (int j = 0; j < 16; j++) uvec[j] = qvec[15 - j]; /* reverse of query */ + sqlite3_stmt *upd = NULL; + sqlite3_prepare_v2(db, + "UPDATE v SET emb = ? WHERE rowid = 1", -1, &upd, NULL); + if (upd) { + sqlite3_bind_blob(upd, 1, uvec, sizeof(uvec), SQLITE_STATIC); + sqlite3_step(upd); + sqlite3_finalize(upd); + } + } + + /* Second KNN after update */ + { + sqlite3_stmt *knn = NULL; + sqlite3_prepare_v2(db, + "SELECT rowid, distance FROM v WHERE emb MATCH ? " + "ORDER BY distance LIMIT 10", -1, &knn, NULL); + if (knn) { + sqlite3_bind_blob(knn, 1, qvec, sizeof(qvec), SQLITE_STATIC); + while (sqlite3_step(knn) == SQLITE_ROW) {} + sqlite3_finalize(knn); + } + } + + /* Delete half the rows, then KNN again */ + for (int i = 1; i <= num_inserts; i += 2) { + char del_sql[64]; + snprintf(del_sql, sizeof(del_sql), + "DELETE FROM v WHERE rowid = %d", i); + sqlite3_exec(db, del_sql, NULL, NULL, NULL); + } + + /* Third KNN after deletes -- exercises distance computation over + zeroed-out slots in the quantized chunk */ + { + sqlite3_stmt *knn = NULL; + sqlite3_prepare_v2(db, + "SELECT rowid, distance FROM v WHERE emb MATCH ? " + "ORDER BY distance LIMIT 5", -1, &knn, NULL); + if (knn) { + sqlite3_bind_blob(knn, 1, qvec, sizeof(qvec), SQLITE_STATIC); + while (sqlite3_step(knn) == SQLITE_ROW) {} + sqlite3_finalize(knn); + } + } + + sqlite3_close(db); + return 0; +} diff --git a/tests/fuzz/rescore-operations.c b/tests/fuzz/rescore-operations.c new file mode 100644 index 00000000..4bb7ff18 --- /dev/null +++ b/tests/fuzz/rescore-operations.c @@ -0,0 +1,96 @@ +#include +#include +#include +#include +#include +#include "sqlite-vec.h" +#include "sqlite3.h" +#include + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (size < 6) return 0; + + int rc; + sqlite3 *db; + sqlite3_stmt *stmtInsert = NULL; + sqlite3_stmt *stmtDelete = NULL; + sqlite3_stmt *stmtKnn = NULL; + sqlite3_stmt *stmtScan = NULL; + + rc = sqlite3_open(":memory:", &db); + assert(rc == SQLITE_OK); + rc = sqlite3_vec_init(db, NULL, NULL); + assert(rc == SQLITE_OK); + + rc = sqlite3_exec(db, + "CREATE VIRTUAL TABLE v USING vec0(" + "emb float[8] indexed by rescore(quantizer=bit))", + NULL, NULL, NULL); + if (rc != SQLITE_OK) { sqlite3_close(db); return 0; } + + sqlite3_prepare_v2(db, + "INSERT INTO v(rowid, emb) VALUES (?, ?)", -1, &stmtInsert, NULL); + sqlite3_prepare_v2(db, + "DELETE FROM v WHERE rowid = ?", -1, &stmtDelete, NULL); + sqlite3_prepare_v2(db, + "SELECT rowid, distance FROM v WHERE emb MATCH ? ORDER BY distance LIMIT 3", + -1, &stmtKnn, NULL); + sqlite3_prepare_v2(db, + "SELECT rowid FROM v", -1, &stmtScan, NULL); + + if (!stmtInsert || !stmtDelete || !stmtKnn || !stmtScan) goto cleanup; + + size_t i = 0; + while (i + 2 <= size) { + uint8_t op = data[i++] % 4; + uint8_t rowid_byte = data[i++]; + int64_t rowid = (int64_t)(rowid_byte % 32) + 1; + + switch (op) { + case 0: { + // INSERT: consume 32 bytes for 8 floats, or use what's left + float vec[8] = {0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}; + for (int j = 0; j < 8 && i < size; j++, i++) { + vec[j] = (float)((int8_t)data[i]) / 10.0f; + } + sqlite3_reset(stmtInsert); + sqlite3_bind_int64(stmtInsert, 1, rowid); + sqlite3_bind_blob(stmtInsert, 2, vec, sizeof(vec), SQLITE_TRANSIENT); + sqlite3_step(stmtInsert); + break; + } + case 1: { + // DELETE + sqlite3_reset(stmtDelete); + sqlite3_bind_int64(stmtDelete, 1, rowid); + sqlite3_step(stmtDelete); + break; + } + case 2: { + // KNN query with a fixed query vector + float qvec[8] = {1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}; + sqlite3_reset(stmtKnn); + sqlite3_bind_blob(stmtKnn, 1, qvec, sizeof(qvec), SQLITE_STATIC); + while (sqlite3_step(stmtKnn) == SQLITE_ROW) {} + break; + } + case 3: { + // Full scan + sqlite3_reset(stmtScan); + while (sqlite3_step(stmtScan) == SQLITE_ROW) {} + break; + } + } + } + + // Final operations -- must not crash regardless of prior state + sqlite3_exec(db, "SELECT * FROM v", NULL, NULL, NULL); + +cleanup: + sqlite3_finalize(stmtInsert); + sqlite3_finalize(stmtDelete); + sqlite3_finalize(stmtKnn); + sqlite3_finalize(stmtScan); + sqlite3_close(db); + return 0; +} diff --git a/tests/fuzz/rescore-optimize.c b/tests/fuzz/rescore-optimize.c new file mode 100644 index 00000000..14b6e16d --- /dev/null +++ b/tests/fuzz/rescore-optimize.c @@ -0,0 +1,138 @@ +#include +#include +#include +#include +#include +#include "sqlite-vec.h" +#include "sqlite3.h" +#include + +static int64_t count_rows(sqlite3 *db, const char *zSql) { + sqlite3_stmt *stmt = NULL; + int64_t n = -1; + if (sqlite3_prepare_v2(db, zSql, -1, &stmt, NULL) == SQLITE_OK && + sqlite3_step(stmt) == SQLITE_ROW) { + n = sqlite3_column_int64(stmt, 0); + } + sqlite3_finalize(stmt); + return n; +} + +/** + * Fuzz target: optimize on rescore tables, interleaved with inserts, updates, + * deletes and KNN queries, with both quantizers and a chunk size of 8 so rows + * move across chunks. optimize moves each row's quantized vectors to its new + * slot and leaves its float32 vector where it is, so after every optimize the + * table and its _rescore_vectors00 must hold the same rows. + */ +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (size < 8) return 0; + + int rc; + sqlite3 *db; + sqlite3_stmt *stmtInsert = NULL; + sqlite3_stmt *stmtUpdate = NULL; + sqlite3_stmt *stmtDelete = NULL; + sqlite3_stmt *stmtKnn = NULL; + + rc = sqlite3_open(":memory:", &db); + assert(rc == SQLITE_OK); + rc = sqlite3_vec_init(db, NULL, NULL); + assert(rc == SQLITE_OK); + + int use_int8 = data[0] & 1; + data++; size--; + + const char *create_sql = use_int8 + ? "CREATE VIRTUAL TABLE v USING vec0(" + "emb float[16] indexed by rescore(quantizer=int8), chunk_size=8)" + : "CREATE VIRTUAL TABLE v USING vec0(" + "emb float[16] indexed by rescore(quantizer=bit), chunk_size=8)"; + + rc = sqlite3_exec(db, create_sql, NULL, NULL, NULL); + if (rc != SQLITE_OK) { sqlite3_close(db); return 0; } + + sqlite3_prepare_v2(db, + "INSERT INTO v(rowid, emb) VALUES (?, ?)", -1, &stmtInsert, NULL); + sqlite3_prepare_v2(db, + "UPDATE v SET emb = ? WHERE rowid = ?", -1, &stmtUpdate, NULL); + sqlite3_prepare_v2(db, + "DELETE FROM v WHERE rowid = ?", -1, &stmtDelete, NULL); + sqlite3_prepare_v2(db, + "SELECT rowid, distance FROM v WHERE emb MATCH ? AND k = 5", -1, + &stmtKnn, NULL); + + if (!stmtInsert || !stmtUpdate || !stmtDelete || !stmtKnn) + goto cleanup; + + size_t i = 0; + while (i + 2 <= size) { + uint8_t op = data[i++] % 5; + uint8_t rowid_byte = data[i++]; + int64_t rowid = (int64_t)(rowid_byte % 40) + 1; + + switch (op) { + case 0: { + float vec[16] = {0}; + for (int j = 0; j < 16 && i < size; j++, i++) { + vec[j] = (float)((int8_t)data[i]) / 64.0f; + } + sqlite3_reset(stmtInsert); + sqlite3_bind_int64(stmtInsert, 1, rowid); + sqlite3_bind_blob(stmtInsert, 2, vec, sizeof(vec), SQLITE_TRANSIENT); + sqlite3_step(stmtInsert); + break; + } + case 1: { + sqlite3_reset(stmtDelete); + sqlite3_bind_int64(stmtDelete, 1, rowid); + sqlite3_step(stmtDelete); + break; + } + case 2: { + float qvec[16] = {0}; + for (int j = 0; j < 16 && i < size; j++, i++) { + qvec[j] = (float)((int8_t)data[i]) / 64.0f; + } + sqlite3_reset(stmtKnn); + sqlite3_bind_blob(stmtKnn, 1, qvec, sizeof(qvec), SQLITE_STATIC); + while (sqlite3_step(stmtKnn) == SQLITE_ROW) { + (void)sqlite3_column_int64(stmtKnn, 0); + (void)sqlite3_column_double(stmtKnn, 1); + } + break; + } + case 3: { + float vec[16] = {0}; + for (int j = 0; j < 16 && i < size; j++, i++) { + vec[j] = (float)((int8_t)data[i]) / 32.0f; + } + sqlite3_reset(stmtUpdate); + sqlite3_bind_blob(stmtUpdate, 1, vec, sizeof(vec), SQLITE_TRANSIENT); + sqlite3_bind_int64(stmtUpdate, 2, rowid); + sqlite3_step(stmtUpdate); + break; + } + case 4: { + int64_t before = count_rows(db, "SELECT count(*) FROM v"); + rc = sqlite3_exec(db, "INSERT INTO v(v) VALUES ('optimize')", NULL, + NULL, NULL); + assert(rc == SQLITE_OK); + assert(count_rows(db, "SELECT count(*) FROM v") == before); + assert(count_rows(db, "SELECT count(*) FROM v_rescore_vectors00") == + before); + break; + } + } + } + + sqlite3_exec(db, "SELECT * FROM v", NULL, NULL, NULL); + +cleanup: + sqlite3_finalize(stmtInsert); + sqlite3_finalize(stmtUpdate); + sqlite3_finalize(stmtDelete); + sqlite3_finalize(stmtKnn); + sqlite3_close(db); + return 0; +} diff --git a/tests/fuzz/rescore-quantize-edge.c b/tests/fuzz/rescore-quantize-edge.c new file mode 100644 index 00000000..9eb34cde --- /dev/null +++ b/tests/fuzz/rescore-quantize-edge.c @@ -0,0 +1,187 @@ +#include +#include +#include +#include +#include +#include +#include "sqlite-vec.h" +#include "sqlite3.h" +#include "../sqlite-vec-internal.h" +#include + +static size_t quantized_byte_size(enum Vec0RescoreQuantizerType quantizer, + size_t dimensions) { + struct VectorColumnDefinition col; + memset(&col, 0, sizeof(col)); + col.dimensions = dimensions; + col.rescore.quantizer_type = quantizer; + return rescore_quantized_byte_size(&col); +} + +/** + * Fuzz target: edge cases in rescore quantization functions. + * + * The existing rescore-quantize.c only tests dimensions that are multiples of 8 + * and never passes special float values. This target: + * + * - Tests rescore_quantized_byte_size with arbitrary dimension values + * (including 0, 1, 7, MAX values -- looking for integer division issues) + * - Passes raw float reinterpretation of fuzz bytes (NaN, Inf, denormals, + * negative zero) + * - Tests the int8 quantizer with all-identical values, which must quantize + * to all-identical values + * - Tests the int8 quantizer with values far outside [-1, 1], which clamp + * - Tests bit quantizer with exact float threshold (0.0f boundary) + */ +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (size < 8) return 0; + + uint8_t mode = data[0] % 5; + data++; size--; + + switch (mode) { + case 0: { + /* Test rescore_quantized_byte_size with fuzz-controlled dimensions. + This function does dimensions / CHAR_BIT for bit, dimensions for int8. + We're checking it doesn't do anything weird with edge values. */ + if (size < sizeof(size_t)) return 0; + size_t dim; + memcpy(&dim, data, sizeof(dim)); + + /* These should never crash, just return values */ + size_t bit_size = quantized_byte_size(VEC0_RESCORE_QUANTIZER_BIT, dim); + size_t int8_size = quantized_byte_size(VEC0_RESCORE_QUANTIZER_INT8, dim); + + /* Verify basic invariants */ + (void)bit_size; + (void)int8_size; + break; + } + + case 1: { + /* Bit quantize with raw reinterpreted floats (NaN, Inf, denormal). + The key check: src[i] >= 0.0f -- NaN comparison is always false, + so NaN should produce 0-bits. But denormals and -0.0f are tricky. */ + size_t num_floats = size / sizeof(float); + if (num_floats == 0) return 0; + /* Round to multiple of 8 for bit quantizer */ + size_t dim = (num_floats / 8) * 8; + if (dim == 0) return 0; + + /* The fuzz bytes start one byte past the mode byte, so they are copied + out to be read as floats at float alignment. */ + float *src = (float *)malloc(dim * sizeof(float)); + size_t bit_bytes = dim / 8; + uint8_t *dst = (uint8_t *)malloc(bit_bytes); + if (!src || !dst) { free(src); free(dst); return 0; } + memcpy(src, data, dim * sizeof(float)); + + rescore_quantize_float_to_bit(src, dst, dim); + + /* Verify: for each bit, if src >= 0 then bit should be set */ + for (size_t i = 0; i < dim; i++) { + int bit_set = (dst[i / 8] >> (i % 8)) & 1; + if (src[i] >= 0.0f) { + assert(bit_set == 1); + } else if (src[i] < 0.0f) { + /* Definitely negative -- bit must be 0 */ + assert(bit_set == 0); + } + /* NaN: comparison is false, so bit_set should be 0 */ + } + + free(src); + free(dst); + break; + } + + case 2: { + /* Int8 quantize with raw reinterpreted floats. + The dangerous paths: + - NaN, which clamps to 127 + - Inf and values far outside [-1, 1], which clamp to -128 or 127 + - denormals near the clamping boundaries */ + size_t num_floats = size / sizeof(float); + if (num_floats == 0) return 0; + + /* Copied out to float alignment, as in mode 1 */ + float *src = (float *)malloc(num_floats * sizeof(float)); + int8_t *dst = (int8_t *)malloc(num_floats); + if (!src || !dst) { free(src); free(dst); return 0; } + memcpy(src, data, num_floats * sizeof(float)); + + rescore_quantize_float_to_int8(src, dst, num_floats); + + /* Output must always be in [-128, 127] (trivially true for int8_t, + but check the actual clamping logic worked) */ + for (size_t i = 0; i < num_floats; i++) { + assert(dst[i] >= -128 && dst[i] <= 127); + } + + free(src); + free(dst); + break; + } + + case 3: { + /* Int8 quantize stress: all-same values quantize to all-same values */ + size_t dim = (size < 64) ? size : 64; + if (dim == 0) return 0; + + float *src = (float *)malloc(dim * sizeof(float)); + int8_t *dst = (int8_t *)malloc(dim); + if (!src || !dst) { free(src); free(dst); return 0; } + + /* Fill with a single value derived from fuzz data */ + float val; + memcpy(&val, data, sizeof(float) < size ? sizeof(float) : size); + for (size_t i = 0; i < dim; i++) src[i] = val; + + rescore_quantize_float_to_int8(src, dst, dim); + + /* Each value quantizes on its own, so every output is the same */ + for (size_t i = 0; i < dim; i++) { + assert(dst[i] == dst[0]); + } + + free(src); + free(dst); + break; + } + + case 4: { + /* Int8 quantize with extreme values: one huge positive, one huge + negative, which clamp to [-128, 127]. */ + if (size < 2 * sizeof(float)) return 0; + + float extreme[2]; + memcpy(extreme, data, 2 * sizeof(float)); + + /* Only test if both are finite (NaN/Inf tested in case 2) */ + if (!isfinite(extreme[0]) || !isfinite(extreme[1])) return 0; + + /* Build a vector with these two extreme values plus some fuzz */ + size_t dim = 16; + float src[16]; + src[0] = extreme[0]; + src[1] = extreme[1]; + for (size_t i = 2; i < dim; i++) { + if (2 * sizeof(float) + (i - 2) < size) { + src[i] = (float)((int8_t)data[2 * sizeof(float) + (i - 2)]) * 1000.0f; + } else { + src[i] = 0.0f; + } + } + + int8_t dst[16]; + rescore_quantize_float_to_int8(src, dst, dim); + + for (size_t i = 0; i < dim; i++) { + assert(dst[i] >= -128 && dst[i] <= 127); + } + break; + } + } + + return 0; +} diff --git a/tests/fuzz/rescore-quantize.c b/tests/fuzz/rescore-quantize.c new file mode 100644 index 00000000..f68c9bd2 --- /dev/null +++ b/tests/fuzz/rescore-quantize.c @@ -0,0 +1,51 @@ +#include +#include +#include +#include +#include +#include "sqlite-vec.h" +#include "sqlite3.h" +#include "../sqlite-vec-internal.h" +#include + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + /* Need at least 4 bytes for one float */ + if (size < 4) return 0; + + /* Use the input as an array of floats. Dimensions must be a multiple of 8 + * for the bit quantizer. */ + size_t num_floats = size / sizeof(float); + if (num_floats == 0) return 0; + + /* Round down to multiple of 8 for bit quantizer compatibility */ + size_t dim = (num_floats / 8) * 8; + if (dim == 0) dim = 8; + if (dim > num_floats) return 0; + + const float *src = (const float *)data; + + /* Allocate output buffers */ + size_t bit_bytes = dim / 8; + uint8_t *bit_dst = (uint8_t *)malloc(bit_bytes); + int8_t *int8_dst = (int8_t *)malloc(dim); + if (!bit_dst || !int8_dst) { + free(bit_dst); + free(int8_dst); + return 0; + } + + /* Test bit quantization */ + rescore_quantize_float_to_bit(src, bit_dst, dim); + + /* Test int8 quantization */ + rescore_quantize_float_to_int8(src, int8_dst, dim); + + /* Verify int8 output is in range */ + for (size_t i = 0; i < dim; i++) { + assert(int8_dst[i] >= -128 && int8_dst[i] <= 127); + } + + free(bit_dst); + free(int8_dst); + return 0; +} diff --git a/tests/fuzz/rescore-shadow-corrupt.c b/tests/fuzz/rescore-shadow-corrupt.c new file mode 100644 index 00000000..edd87ef0 --- /dev/null +++ b/tests/fuzz/rescore-shadow-corrupt.c @@ -0,0 +1,230 @@ +#include +#include +#include +#include +#include +#include "sqlite-vec.h" +#include "sqlite3.h" +#include + +/** + * Fuzz target: corrupt rescore shadow tables then exercise KNN/read/write. + * + * This targets the dangerous code paths in rescore_knn (Phase 1 + 2): + * - sqlite3_blob_read into baseVectors with potentially wrong-sized blobs + * - distance computation on corrupted/partial quantized data + * - blob_reopen on _rescore_vectors with missing/corrupted rows + * - insert/delete after corruption (blob_write to wrong offsets) + * + * The existing shadow-corrupt.c only tests vec0 without rescore. + */ +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (size < 4) return 0; + + int rc; + sqlite3 *db; + + rc = sqlite3_open(":memory:", &db); + assert(rc == SQLITE_OK); + rc = sqlite3_vec_init(db, NULL, NULL); + assert(rc == SQLITE_OK); + + /* Pick quantizer type from first byte */ + int use_int8 = data[0] & 1; + int target = (data[1] % 8); + const uint8_t *payload = data + 2; + int payload_size = (int)(size - 2); + + const char *create_sql = use_int8 + ? "CREATE VIRTUAL TABLE v USING vec0(" + "emb float[16] indexed by rescore(quantizer=int8))" + : "CREATE VIRTUAL TABLE v USING vec0(" + "emb float[16] indexed by rescore(quantizer=bit))"; + + rc = sqlite3_exec(db, create_sql, NULL, NULL, NULL); + if (rc != SQLITE_OK) { sqlite3_close(db); return 0; } + + /* Insert several vectors so there's a full chunk to corrupt */ + { + sqlite3_stmt *ins = NULL; + sqlite3_prepare_v2(db, + "INSERT INTO v(rowid, emb) VALUES (?, ?)", -1, &ins, NULL); + if (!ins) { sqlite3_close(db); return 0; } + + for (int i = 1; i <= 8; i++) { + float vec[16]; + for (int j = 0; j < 16; j++) vec[j] = (float)(i * 10 + j) / 100.0f; + sqlite3_reset(ins); + sqlite3_bind_int64(ins, 1, i); + sqlite3_bind_blob(ins, 2, vec, sizeof(vec), SQLITE_TRANSIENT); + sqlite3_step(ins); + } + sqlite3_finalize(ins); + } + + /* Now corrupt rescore shadow tables based on fuzz input */ + sqlite3_stmt *stmt = NULL; + + switch (target) { + case 0: { + /* Corrupt _rescore_chunks00 vectors blob with fuzz data */ + rc = sqlite3_prepare_v2(db, + "UPDATE v_rescore_chunks00 SET vectors = ? WHERE rowid = 1", + -1, &stmt, NULL); + if (rc == SQLITE_OK) { + sqlite3_bind_blob(stmt, 1, payload, payload_size, SQLITE_STATIC); + sqlite3_step(stmt); + sqlite3_finalize(stmt); + stmt = NULL; + } + break; + } + case 1: { + /* Corrupt _rescore_vectors00 vector blob for a specific row */ + rc = sqlite3_prepare_v2(db, + "UPDATE v_rescore_vectors00 SET vector = ? WHERE rowid = 3", + -1, &stmt, NULL); + if (rc == SQLITE_OK) { + sqlite3_bind_blob(stmt, 1, payload, payload_size, SQLITE_STATIC); + sqlite3_step(stmt); + sqlite3_finalize(stmt); + stmt = NULL; + } + break; + } + case 2: { + /* Truncate the quantized chunk blob to wrong size */ + rc = sqlite3_prepare_v2(db, + "UPDATE v_rescore_chunks00 SET vectors = X'DEADBEEF' WHERE rowid = 1", + -1, &stmt, NULL); + if (rc == SQLITE_OK) { + sqlite3_step(stmt); + sqlite3_finalize(stmt); + stmt = NULL; + } + break; + } + case 3: { + /* Delete rows from _rescore_vectors (orphan the float vectors) */ + sqlite3_exec(db, + "DELETE FROM v_rescore_vectors00 WHERE rowid IN (2, 4, 6)", + NULL, NULL, NULL); + break; + } + case 4: { + /* Delete the chunk row entirely from _rescore_chunks */ + sqlite3_exec(db, + "DELETE FROM v_rescore_chunks00 WHERE rowid = 1", + NULL, NULL, NULL); + break; + } + case 5: { + /* Set vectors to NULL in _rescore_chunks */ + sqlite3_exec(db, + "UPDATE v_rescore_chunks00 SET vectors = NULL WHERE rowid = 1", + NULL, NULL, NULL); + break; + } + case 6: { + /* Set vector to NULL in _rescore_vectors */ + sqlite3_exec(db, + "UPDATE v_rescore_vectors00 SET vector = NULL WHERE rowid = 3", + NULL, NULL, NULL); + break; + } + case 7: { + /* Corrupt BOTH tables with fuzz data */ + int half = payload_size / 2; + rc = sqlite3_prepare_v2(db, + "UPDATE v_rescore_chunks00 SET vectors = ? WHERE rowid = 1", + -1, &stmt, NULL); + if (rc == SQLITE_OK) { + sqlite3_bind_blob(stmt, 1, payload, half, SQLITE_STATIC); + sqlite3_step(stmt); + sqlite3_finalize(stmt); + stmt = NULL; + } + rc = sqlite3_prepare_v2(db, + "UPDATE v_rescore_vectors00 SET vector = ? WHERE rowid = 1", + -1, &stmt, NULL); + if (rc == SQLITE_OK) { + sqlite3_bind_blob(stmt, 1, payload + half, + payload_size - half, SQLITE_STATIC); + sqlite3_step(stmt); + sqlite3_finalize(stmt); + stmt = NULL; + } + break; + } + } + + /* Exercise ALL read/write paths -- NONE should crash */ + + /* KNN query (triggers rescore_knn Phase 1 + Phase 2) */ + { + float qvec[16] = {1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1}; + sqlite3_stmt *knn = NULL; + sqlite3_prepare_v2(db, + "SELECT rowid, distance FROM v WHERE emb MATCH ? " + "ORDER BY distance LIMIT 5", -1, &knn, NULL); + if (knn) { + sqlite3_bind_blob(knn, 1, qvec, sizeof(qvec), SQLITE_STATIC); + while (sqlite3_step(knn) == SQLITE_ROW) {} + sqlite3_finalize(knn); + } + } + + /* Full scan (triggers reading from _rescore_vectors) */ + sqlite3_exec(db, "SELECT * FROM v", NULL, NULL, NULL); + + /* Point lookups */ + sqlite3_exec(db, "SELECT * FROM v WHERE rowid = 1", NULL, NULL, NULL); + sqlite3_exec(db, "SELECT * FROM v WHERE rowid = 3", NULL, NULL, NULL); + + /* Insert after corruption */ + { + float vec[16] = {0}; + sqlite3_stmt *ins = NULL; + sqlite3_prepare_v2(db, + "INSERT INTO v(rowid, emb) VALUES (99, ?)", -1, &ins, NULL); + if (ins) { + sqlite3_bind_blob(ins, 1, vec, sizeof(vec), SQLITE_STATIC); + sqlite3_step(ins); + sqlite3_finalize(ins); + } + } + + /* Delete after corruption */ + sqlite3_exec(db, "DELETE FROM v WHERE rowid = 5", NULL, NULL, NULL); + + /* Update after corruption */ + { + float vec[16] = {1,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1}; + sqlite3_stmt *upd = NULL; + sqlite3_prepare_v2(db, + "UPDATE v SET emb = ? WHERE rowid = 1", -1, &upd, NULL); + if (upd) { + sqlite3_bind_blob(upd, 1, vec, sizeof(vec), SQLITE_STATIC); + sqlite3_step(upd); + sqlite3_finalize(upd); + } + } + + /* KNN again after modifications to corrupted state */ + { + float qvec[16] = {0,0,0,0, 0,0,0,0, 1,1,1,1, 1,1,1,1}; + sqlite3_stmt *knn = NULL; + sqlite3_prepare_v2(db, + "SELECT rowid, distance FROM v WHERE emb MATCH ? " + "ORDER BY distance LIMIT 3", -1, &knn, NULL); + if (knn) { + sqlite3_bind_blob(knn, 1, qvec, sizeof(qvec), SQLITE_STATIC); + while (sqlite3_step(knn) == SQLITE_ROW) {} + sqlite3_finalize(knn); + } + } + + sqlite3_exec(db, "DROP TABLE v", NULL, NULL, NULL); + sqlite3_close(db); + return 0; +} diff --git a/tests/sqlite-vec-internal.h b/tests/sqlite-vec-internal.h index 20a788c5..91da52b4 100644 --- a/tests/sqlite-vec-internal.h +++ b/tests/sqlite-vec-internal.h @@ -24,6 +24,9 @@ enum Vec0TokenType { TOKEN_TYPE_RBRACKET = 3, TOKEN_TYPE_PLUS = 4, TOKEN_TYPE_EQ = 5, + TOKEN_TYPE_LPAREN = 6, + TOKEN_TYPE_RPAREN = 7, + TOKEN_TYPE_COMMA = 8, }; #define VEC0_TOKEN_RESULT_EOF 1 @@ -60,12 +63,92 @@ enum Vec0DistanceMetrics { VEC0_DISTANCE_METRIC_L1 = 3, }; +#ifndef SQLITE_VEC_ENABLE_RESCORE +#define SQLITE_VEC_ENABLE_RESCORE 1 +#endif + +#ifndef SQLITE_VEC_ENABLE_DISKANN +#define SQLITE_VEC_ENABLE_DISKANN 1 +#endif + +#ifndef SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE +#define SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE 0 +#endif + +enum Vec0IndexType { + VEC0_INDEX_TYPE_FLAT = 1, +#if SQLITE_VEC_ENABLE_RESCORE + VEC0_INDEX_TYPE_RESCORE = 2, +#endif +#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE + VEC0_INDEX_TYPE_IVF = 3, +#endif +#if SQLITE_VEC_ENABLE_DISKANN + VEC0_INDEX_TYPE_DISKANN = 4, +#endif +}; + +#if SQLITE_VEC_ENABLE_RESCORE +enum Vec0RescoreQuantizerType { + VEC0_RESCORE_QUANTIZER_BIT = 1, + VEC0_RESCORE_QUANTIZER_INT8 = 2, +}; + +struct Vec0RescoreConfig { + enum Vec0RescoreQuantizerType quantizer_type; + int oversample; + int oversample_search; +}; +#endif + +#if SQLITE_VEC_ENABLE_DISKANN +enum Vec0DiskannQuantizerType { + VEC0_DISKANN_QUANTIZER_BINARY = 1, + VEC0_DISKANN_QUANTIZER_INT8 = 2, +}; + +struct Vec0DiskannConfig { + enum Vec0DiskannQuantizerType quantizer_type; + int n_neighbors; + int search_list_size; + int search_list_size_search; + int search_list_size_insert; + float alpha; + int buffer_threshold; +}; +#endif + +#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE +enum Vec0IvfQuantizer { + VEC0_IVF_QUANTIZER_NONE = 0, + VEC0_IVF_QUANTIZER_INT8 = 1, + VEC0_IVF_QUANTIZER_BINARY = 2, +}; + +struct Vec0IvfConfig { + int nlist; + int nprobe; + enum Vec0IvfQuantizer quantizer; + int oversample; +}; +#endif + struct VectorColumnDefinition { char *name; int name_length; size_t dimensions; enum VectorElementType element_type; enum Vec0DistanceMetrics distance_metric; + enum Vec0IndexType index_type; +#if SQLITE_VEC_ENABLE_RESCORE + struct Vec0RescoreConfig rescore; +#endif +#if SQLITE_VEC_ENABLE_DISKANN + struct Vec0DiskannConfig diskann; +#endif +#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE + struct Vec0IvfConfig ivf; +#endif }; int vec0_parse_vector_column(const char *source, int source_length, @@ -77,4 +160,100 @@ int vec0_parse_partition_key_definition(const char *source, int source_length, int *out_column_name_length, int *out_column_type, bool isCreate); +#if SQLITE_VEC_ENABLE_RESCORE +size_t rescore_quantized_byte_size(struct VectorColumnDefinition *col); +void rescore_quantize_float_to_bit(const float *src, uint8_t *dst, + size_t dimensions); +void rescore_quantize_float_to_int8(const float *src, int8_t *dst, + size_t dimensions); +#endif + +#if SQLITE_VEC_ENABLE_DISKANN +int diskann_validity_byte_size(int n_neighbors); +size_t diskann_neighbor_ids_byte_size(int n_neighbors); +size_t +diskann_quantized_vector_byte_size(enum Vec0DiskannQuantizerType quantizer_type, + size_t dimensions); +size_t +diskann_neighbor_qvecs_byte_size(int n_neighbors, + enum Vec0DiskannQuantizerType quantizer_type, + size_t dimensions); +int diskann_node_init(int n_neighbors, + enum Vec0DiskannQuantizerType quantizer_type, + size_t dimensions, unsigned char **outValidity, + int *outValiditySize, unsigned char **outNeighborIds, + int *outNeighborIdsSize, unsigned char **outNeighborQvecs, + int *outNeighborQvecsSize); +int diskann_validity_get(const unsigned char *validity, int i); +void diskann_validity_set(unsigned char *validity, int i, int value); +int diskann_validity_count(const unsigned char *validity, int n_neighbors); +long long diskann_neighbor_id_get(const unsigned char *neighbor_ids, int i); +void diskann_neighbor_id_set(unsigned char *neighbor_ids, int i, + long long rowid); +const unsigned char * +diskann_neighbor_qvec_get(const unsigned char *qvecs, int i, + enum Vec0DiskannQuantizerType quantizer_type, + size_t dimensions); +void diskann_node_set_neighbor(unsigned char *validity, + unsigned char *neighbor_ids, + unsigned char *qvecs, int i, + long long neighbor_rowid, + const unsigned char *neighbor_qvec, + enum Vec0DiskannQuantizerType quantizer_type, + size_t dimensions); +void diskann_node_clear_neighbor(unsigned char *validity, + unsigned char *neighbor_ids, + unsigned char *qvecs, int i, + enum Vec0DiskannQuantizerType quantizer_type, + size_t dimensions); +int diskann_quantize_vector(const float *src, size_t dimensions, + enum Vec0DiskannQuantizerType quantizer_type, + unsigned char *out); +int diskann_prune_select(const float *p_distances, int num_candidates, + float alpha, int max_neighbors, + float (*distance)(void *ctx, int a, int b), void *ctx, + int *outSelected, int *outCount); + +struct Vec0DiskannCandidate { + long long rowid; + float distance; + int visited; + int confirmed; +}; +struct DiskannCandidateList { + struct Vec0DiskannCandidate *items; + int count; + int capacity; +}; +int diskann_candidate_list_init(struct DiskannCandidateList *list, + int capacity); +void diskann_candidate_list_free(struct DiskannCandidateList *list); +int diskann_candidate_list_insert(struct DiskannCandidateList *list, + long long rowid, float distance); +int diskann_candidate_list_rerank(struct DiskannCandidateList *list, int i, + float distance); +int diskann_candidate_list_next_unvisited( + const struct DiskannCandidateList *list); + +struct DiskannVisitedSet { + long long *slots; + int capacity; + int count; + int hasZero; +}; +int diskann_visited_set_init(struct DiskannVisitedSet *set, int capacity); +void diskann_visited_set_free(struct DiskannVisitedSet *set); +int diskann_visited_set_contains(const struct DiskannVisitedSet *set, + long long rowid); +int diskann_visited_set_insert(struct DiskannVisitedSet *set, long long rowid, + int *outAdded); +#endif + +#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE +void ivf_quantize_int8(const float *src, int8_t *dst, size_t dimensions); +void ivf_quantize_binary(const float *src, uint8_t *dst, size_t dimensions); +int ivf_kmeans(const float *vectors, int n, size_t dimensions, int k, + int max_iter, uint32_t seed, float *out_centroids); +#endif + #endif /* SQLITE_VEC_INTERNAL_H */ diff --git a/tests/test-auxiliary.py b/tests/test-auxiliary.py index af51e64e..0c73d5fc 100644 --- a/tests/test-auxiliary.py +++ b/tests/test-auxiliary.py @@ -1,7 +1,13 @@ +import random import sqlite3 +import struct from collections import OrderedDict +def _f32(values): + return struct.pack("%sf" % len(values), *values) + + def test_constructor_limit(db, snapshot): assert ( exec( @@ -161,6 +167,206 @@ def test_vacuum(db, snapshot): assert cur_page_count < prev_page_count +# ====================================================================== +# Auxiliary columns with non-flat indexes +# ====================================================================== + + +def test_rescore_aux_shadow_tables(db, snapshot): + """Rescore + aux column: verify shadow tables are created correctly.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " emb float[128] indexed by rescore(quantizer=bit)," + " +label text," + " +score float" + ")" + ) + assert exec( + db, + "SELECT name, sql FROM sqlite_master WHERE type='table' AND name LIKE 't_%' ORDER BY name", + ) == snapshot(name="rescore aux shadow tables") + + +def test_rescore_aux_insert_knn(db, snapshot): + """Insert with aux data, KNN should return aux column values.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " emb float[128] indexed by rescore(quantizer=bit)," + " +label text" + ")" + ) + random.seed(77) + data = [ + ("alpha", [random.gauss(0, 1) for _ in range(128)]), + ("beta", [random.gauss(0, 1) for _ in range(128)]), + ("gamma", [random.gauss(0, 1) for _ in range(128)]), + ] + for label, vec in data: + db.execute( + "INSERT INTO t(emb, label) VALUES (?, ?)", + [_f32(vec), label], + ) + + assert exec(db, "SELECT rowid, label FROM t ORDER BY rowid") == snapshot( + name="rescore aux select all" + ) + assert vec0_shadow_table_contents(db, "t") == snapshot( + name="rescore aux shadow contents" + ) + + # KNN should include aux column, "alpha" closest to its own vector + rows = db.execute( + "SELECT label, distance FROM t WHERE emb MATCH ? ORDER BY distance LIMIT 3", + [_f32(data[0][1])], + ).fetchall() + assert len(rows) == 3 + assert rows[0][0] == "alpha" + + +def test_rescore_aux_update(db): + """UPDATE aux column on rescore table should work without affecting vectors.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " emb float[128] indexed by rescore(quantizer=bit)," + " +label text" + ")" + ) + random.seed(88) + vec = [random.gauss(0, 1) for _ in range(128)] + db.execute( + "INSERT INTO t(rowid, emb, label) VALUES (1, ?, 'original')", [_f32(vec)] + ) + db.execute("UPDATE t SET label = 'updated' WHERE rowid = 1") + + assert db.execute("SELECT label FROM t WHERE rowid = 1").fetchone()[0] == "updated" + + # KNN still works with updated aux + rows = db.execute( + "SELECT rowid, label FROM t WHERE emb MATCH ? ORDER BY distance LIMIT 1", + [_f32(vec)], + ).fetchall() + assert rows[0][0] == 1 + assert rows[0][1] == "updated" + + +def test_rescore_aux_delete(db, snapshot): + """DELETE should remove aux data from shadow table.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " emb float[128] indexed by rescore(quantizer=bit)," + " +label text" + ")" + ) + random.seed(99) + for i in range(5): + db.execute( + "INSERT INTO t(rowid, emb, label) VALUES (?, ?, ?)", + [i + 1, _f32([random.gauss(0, 1) for _ in range(128)]), f"item-{i+1}"], + ) + + db.execute("DELETE FROM t WHERE rowid = 3") + + assert exec(db, "SELECT rowid, label FROM t ORDER BY rowid") == snapshot( + name="rescore aux after delete" + ) + assert exec( + db, "SELECT rowid, value00 FROM t_auxiliary ORDER BY rowid" + ) == snapshot(name="rescore aux shadow after delete") + + +def test_diskann_aux_shadow_tables(db, snapshot): + """DiskANN + aux column: verify shadow tables are created correctly.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=8), + +label text, + +score float + ) + """) + assert exec( + db, + "SELECT name, sql FROM sqlite_master WHERE type='table' AND name LIKE 't_%' ORDER BY name", + ) == snapshot(name="diskann aux shadow tables") + + +def test_diskann_aux_insert_knn(db, snapshot): + """DiskANN + aux: insert, KNN, verify aux values returned.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=8), + +label text + ) + """) + data = [ + ("red", [1, 0, 0, 0, 0, 0, 0, 0]), + ("green", [0, 1, 0, 0, 0, 0, 0, 0]), + ("blue", [0, 0, 1, 0, 0, 0, 0, 0]), + ] + for label, vec in data: + db.execute("INSERT INTO t(emb, label) VALUES (?, ?)", [_f32(vec), label]) + + assert exec(db, "SELECT rowid, label FROM t ORDER BY rowid") == snapshot( + name="diskann aux select all" + ) + assert vec0_shadow_table_contents(db, "t") == snapshot( + name="diskann aux shadow contents" + ) + + rows = db.execute( + "SELECT label, distance FROM t WHERE emb MATCH ? AND k = 3", + [_f32([1, 0, 0, 0, 0, 0, 0, 0])], + ).fetchall() + assert len(rows) >= 1 + assert rows[0][0] == "red" + + +def test_diskann_aux_update_and_delete(db, snapshot): + """DiskANN + aux: update aux column, delete row, verify cleanup.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=8), + +label text + ) + """) + for i in range(5): + vec = [0.0] * 8 + vec[i % 8] = 1.0 + db.execute( + "INSERT INTO t(rowid, emb, label) VALUES (?, ?, ?)", + [i + 1, _f32(vec), f"item-{i+1}"], + ) + + db.execute("UPDATE t SET label = 'UPDATED' WHERE rowid = 2") + db.execute("DELETE FROM t WHERE rowid = 3") + + assert exec(db, "SELECT rowid, label FROM t ORDER BY rowid") == snapshot( + name="diskann aux after update+delete" + ) + assert exec( + db, "SELECT rowid, value00 FROM t_auxiliary ORDER BY rowid" + ) == snapshot(name="diskann aux shadow after update+delete") + + +def test_diskann_aux_drop_cleans_all(db): + """DROP TABLE should remove aux shadow table too.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=8), + +label text + ) + """) + db.execute("INSERT INTO t(emb, label) VALUES (?, 'test')", [_f32([1] * 8)]) + db.execute("DROP TABLE t") + + tables = [ + r[0] + for r in db.execute( + "SELECT name FROM sqlite_master WHERE name LIKE 't_%'" + ).fetchall() + ] + assert "t_auxiliary" not in tables + + def exec(db, sql, parameters=[]): try: rows = db.execute(sql, parameters).fetchall() diff --git a/tests/test-cache-finalize.py b/tests/test-cache-finalize.py new file mode 100644 index 00000000..ae114997 --- /dev/null +++ b/tests/test-cache-finalize.py @@ -0,0 +1,47 @@ +"""vec0 must leave no cached prepared statement in progress, or VACUUM fails +with "SQL statements in progress": `vec0Sync` finalizes every cached statement +when a transaction commits, and a statement is reset as soon as it has run. + +https://github.com/asg017/sqlite-vec/issues/295 +""" + +import struct + + +def _f32(values): + return struct.pack("%sf" % len(values), *values) + + +def test_vacuum_after_diskann_inserts(db): + db.execute( + "create virtual table v using vec0(" + "a float[8] indexed by diskann(neighbor_quantizer=binary))" + ) + for i in range(1, 11): + db.execute("insert into v(rowid, a) values (?, ?)", (i, _f32([0.1 * i] * 8))) + db.commit() + db.execute("VACUUM") + + +def test_vacuum_after_diskann_knn(db): + # A KNN query commits nothing, so the statements it ran must not stay in + # progress until the next write. + db.execute( + "create virtual table v using vec0(" + "a float[8] indexed by diskann(neighbor_quantizer=binary))" + ) + for i in range(1, 11): + db.execute("insert into v(rowid, a) values (?, ?)", (i, _f32([0.1 * i] * 8))) + db.commit() + rows = db.execute( + "select rowid from v where a match ? and k = 3", (_f32([0.5] * 8),) + ).fetchall() + assert len(rows) == 3 + db.execute("VACUUM") + + +def test_vacuum_after_flat_inserts(db): + db.execute("create virtual table v using vec0(a float[2])") + db.execute("insert into v(rowid, a) values (1, ?)", (_f32([0.1, 0.2]),)) + db.commit() + db.execute("VACUUM") diff --git a/tests/test-diskann.py b/tests/test-diskann.py new file mode 100644 index 00000000..8d2daab8 --- /dev/null +++ b/tests/test-diskann.py @@ -0,0 +1,1708 @@ +import random +import sqlite3 +import struct +import pytest +from collections import OrderedDict + + +def _f32(values): + return struct.pack("%sf" % len(values), *values) + + +def exec(db, sql, parameters=[]): + try: + rows = db.execute(sql, parameters).fetchall() + except (sqlite3.OperationalError, sqlite3.DatabaseError) as e: + return { + "error": e.__class__.__name__, + "message": str(e), + } + a = [] + for row in rows: + o = OrderedDict() + for k in row.keys(): + o[k] = row[k] + a.append(o) + result = OrderedDict() + result["sql"] = sql + result["rows"] = a + return result + + +def test_diskann_create_basic(db): + """Basic DiskANN table creation with binary quantizer should succeed.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[128] INDEXED BY diskann(neighbor_quantizer=binary) + ) + """) + # Table should exist + tables = [ + row[0] + for row in db.execute( + "select name from sqlite_master where name like 't%' order by 1" + ).fetchall() + ] + assert "t" in tables + + +def test_diskann_create_int8_quantizer(db): + """DiskANN with int8 quantizer should succeed.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[64] INDEXED BY diskann(neighbor_quantizer=int8) + ) + """) + tables = [ + row[0] + for row in db.execute( + "select name from sqlite_master where name like 't%' order by 1" + ).fetchall() + ] + assert "t" in tables + + +def test_diskann_create_with_options(db): + """DiskANN with custom n_neighbors and search_list_size.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[128] INDEXED BY diskann( + neighbor_quantizer=binary, + n_neighbors=48, + search_list_size=256 + ) + ) + """) + tables = [ + row[0] + for row in db.execute( + "select name from sqlite_master where name like 't%' order by 1" + ).fetchall() + ] + assert "t" in tables + + +def test_diskann_create_with_distance_metric(db): + """DiskANN combined with distance_metric should work.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[128] distance_metric=cosine INDEXED BY diskann(neighbor_quantizer=binary) + ) + """) + tables = [ + row[0] + for row in db.execute( + "select name from sqlite_master where name like 't%' order by 1" + ).fetchall() + ] + assert "t" in tables + + +def test_diskann_create_error_missing_quantizer(db): + """Error when neighbor_quantizer is not specified.""" + result = exec( + db, + """ + CREATE VIRTUAL TABLE t USING vec0( + emb float[128] INDEXED BY diskann(n_neighbors=72) + ) + """, + ) + assert "error" in result + + +def test_diskann_create_error_empty_parens(db): + """Error on empty parens.""" + result = exec( + db, + """ + CREATE VIRTUAL TABLE t USING vec0( + emb float[128] INDEXED BY diskann() + ) + """, + ) + assert "error" in result + + +def test_diskann_create_error_unknown_quantizer(db): + """Error on unknown quantizer type.""" + result = exec( + db, + """ + CREATE VIRTUAL TABLE t USING vec0( + emb float[128] INDEXED BY diskann(neighbor_quantizer=unknown) + ) + """, + ) + assert "error" in result + + +def test_diskann_create_error_bit_column(db): + """Error: DiskANN not supported on bit vector columns.""" + result = exec( + db, + """ + CREATE VIRTUAL TABLE t USING vec0( + emb bit[128] INDEXED BY diskann(neighbor_quantizer=binary) + ) + """, + ) + assert "error" in result + assert "bit" in result["message"].lower() or "DiskANN" in result["message"] + + +def test_diskann_create_error_binary_quantizer_odd_dims(db): + """Error: binary quantizer requires dimensions divisible by 8.""" + result = exec( + db, + """ + CREATE VIRTUAL TABLE t USING vec0( + emb float[13] INDEXED BY diskann(neighbor_quantizer=binary) + ) + """, + ) + assert "error" in result + assert "divisible" in result["message"].lower() + + +def test_diskann_create_error_bad_n_neighbors(db): + """Error: n_neighbors must be divisible by 8.""" + result = exec( + db, + """ + CREATE VIRTUAL TABLE t USING vec0( + emb float[128] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=13) + ) + """, + ) + assert "error" in result + + +def test_diskann_shadow_tables_created(db): + """DiskANN table should create _vectors00 and _diskann_nodes00 shadow tables.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[64] INDEXED BY diskann(neighbor_quantizer=binary) + ) + """) + tables = sorted( + [ + row[0] + for row in db.execute( + "select name from sqlite_master where type='table' and name like 't_%' order by 1" + ).fetchall() + ] + ) + assert "t_vectors00" in tables + assert "t_diskann_nodes00" in tables + # DiskANN columns should NOT have _vector_chunks00 + assert "t_vector_chunks00" not in tables + + +def test_diskann_medoid_in_info(db): + """_info table should contain diskann_medoid_00 key with NULL value.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[64] INDEXED BY diskann(neighbor_quantizer=binary) + ) + """) + row = db.execute( + "SELECT key, value FROM t_info WHERE key = 'diskann_medoid_00'" + ).fetchone() + assert row is not None + assert row[0] == "diskann_medoid_00" + assert row[1] is None + + +def test_non_diskann_no_extra_tables(db): + """Non-DiskANN table must NOT create _vectors or _diskann_nodes tables.""" + db.execute("CREATE VIRTUAL TABLE t USING vec0(emb float[64])") + tables = [ + row[0] + for row in db.execute( + "select name from sqlite_master where type='table' and name like 't_%' order by 1" + ).fetchall() + ] + assert "t_vectors00" not in tables + assert "t_diskann_nodes00" not in tables + assert "t_vector_chunks00" in tables + + +def test_diskann_medoid_initial_null(db): + """Medoid should be NULL initially (empty graph).""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[64] INDEXED BY diskann(neighbor_quantizer=binary) + ) + """) + row = db.execute( + "SELECT value FROM t_info WHERE key = 'diskann_medoid_00'" + ).fetchone() + assert row[0] is None + + +def test_diskann_medoid_set_via_info(db): + """Setting medoid via _info table should be retrievable.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[64] INDEXED BY diskann(neighbor_quantizer=binary) + ) + """) + # Manually set medoid to simulate first insert + db.execute("UPDATE t_info SET value = 42 WHERE key = 'diskann_medoid_00'") + row = db.execute( + "SELECT value FROM t_info WHERE key = 'diskann_medoid_00'" + ).fetchone() + assert row[0] == 42 + + # Reset to NULL (empty graph) + db.execute("UPDATE t_info SET value = NULL WHERE key = 'diskann_medoid_00'") + row = db.execute( + "SELECT value FROM t_info WHERE key = 'diskann_medoid_00'" + ).fetchone() + assert row[0] is None + + +def test_diskann_single_insert(db): + """Insert 1 vector. Verify _vectors00, _diskann_nodes00, and medoid.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary) + ) + """) + db.execute( + "INSERT INTO t(rowid, emb) VALUES (1, ?)", + [_f32([1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])], + ) + # Verify _vectors00 has 1 row + count = db.execute("SELECT count(*) FROM t_vectors00").fetchone()[0] + assert count == 1 + + # Verify _diskann_nodes00 has 1 row + count = db.execute("SELECT count(*) FROM t_diskann_nodes00").fetchone()[0] + assert count == 1 + + # Verify medoid is set + medoid = db.execute( + "SELECT value FROM t_info WHERE key = 'diskann_medoid_00'" + ).fetchone()[0] + assert medoid == 1 + + +def test_diskann_multiple_inserts(db): + """Insert multiple vectors. Verify counts and that nodes have neighbors.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=8) + ) + """) + import random + + random.seed(42) + for i in range(1, 21): + vec = [random.gauss(0, 1) for _ in range(8)] + db.execute("INSERT INTO t(rowid, emb) VALUES (?, ?)", [i, _f32(vec)]) + + # Verify counts + assert db.execute("SELECT count(*) FROM t_vectors00").fetchone()[0] == 20 + assert db.execute("SELECT count(*) FROM t_diskann_nodes00").fetchone()[0] == 20 + + # Every node after the first should have at least 1 neighbor + rows = db.execute( + "SELECT rowid, neighbors_validity FROM t_diskann_nodes00" + ).fetchall() + nodes_with_neighbors = 0 + for row in rows: + validity = row[1] + has_neighbor = any(b != 0 for b in validity) + if has_neighbor: + nodes_with_neighbors += 1 + # At minimum, nodes 2-20 should have neighbors (node 1 gets neighbors via reverse edges) + assert nodes_with_neighbors >= 19 + + +def test_diskann_bidirectional_edges(db): + """Insert A then B. B should be in A's neighbors and A in B's.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=8) + ) + """) + db.execute( + "INSERT INTO t(rowid, emb) VALUES (1, ?)", + [_f32([1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])], + ) + db.execute( + "INSERT INTO t(rowid, emb) VALUES (2, ?)", + [_f32([0.9, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])], + ) + + # Check B(2) is in A(1)'s neighbor list + row_a = db.execute( + "SELECT neighbor_ids FROM t_diskann_nodes00 WHERE rowid = 1" + ).fetchone() + neighbor_ids_a = struct.unpack(f"{len(row_a[0])//8}q", row_a[0]) + assert 2 in neighbor_ids_a + + # Check A(1) is in B(2)'s neighbor list + row_b = db.execute( + "SELECT neighbor_ids FROM t_diskann_nodes00 WHERE rowid = 2" + ).fetchone() + neighbor_ids_b = struct.unpack(f"{len(row_b[0])//8}q", row_b[0]) + assert 1 in neighbor_ids_b + + +def test_diskann_delete_single(db): + """Insert 3, delete 1. Verify counts.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=8) + ) + """) + for i in range(1, 4): + db.execute( + "INSERT INTO t(rowid, emb) VALUES (?, ?)", + [i, _f32([float(i)] * 8)], + ) + db.execute("DELETE FROM t WHERE rowid = 2") + + assert db.execute("SELECT count(*) FROM t_vectors00").fetchone()[0] == 2 + assert db.execute("SELECT count(*) FROM t_diskann_nodes00").fetchone()[0] == 2 + + +def test_diskann_delete_no_stale_references(db): + """After delete, no node should reference the deleted rowid.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=8) + ) + """) + import random + + random.seed(123) + for i in range(1, 11): + vec = [random.gauss(0, 1) for _ in range(8)] + db.execute("INSERT INTO t(rowid, emb) VALUES (?, ?)", [i, _f32(vec)]) + + db.execute("DELETE FROM t WHERE rowid = 5") + + # Scan all remaining nodes and verify rowid 5 is not in any neighbor list + rows = db.execute( + "SELECT rowid, neighbors_validity, neighbor_ids FROM t_diskann_nodes00" + ).fetchall() + for row in rows: + validity = row[1] + neighbor_ids_blob = row[2] + n_neighbors = len(validity) * 8 + ids = struct.unpack(f"{n_neighbors}q", neighbor_ids_blob) + for i in range(n_neighbors): + byte_idx = i // 8 + bit_idx = i % 8 + if validity[byte_idx] & (1 << bit_idx): + assert ids[i] != 5, f"Node {row[0]} still references deleted rowid 5" + + +def test_diskann_delete_medoid(db): + """Delete the medoid. Verify a new non-NULL medoid is selected.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=8) + ) + """) + for i in range(1, 4): + db.execute( + "INSERT INTO t(rowid, emb) VALUES (?, ?)", + [i, _f32([float(i)] * 8)], + ) + + medoid_before = db.execute( + "SELECT value FROM t_info WHERE key = 'diskann_medoid_00'" + ).fetchone()[0] + assert medoid_before == 1 + + db.execute("DELETE FROM t WHERE rowid = 1") + + medoid_after = db.execute( + "SELECT value FROM t_info WHERE key = 'diskann_medoid_00'" + ).fetchone()[0] + assert medoid_after is not None + assert medoid_after != 1 + + +def test_diskann_delete_all(db): + """Delete all vectors. Medoid should be NULL.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=8) + ) + """) + for i in range(1, 4): + db.execute( + "INSERT INTO t(rowid, emb) VALUES (?, ?)", + [i, _f32([float(i)] * 8)], + ) + for i in range(1, 4): + db.execute("DELETE FROM t WHERE rowid = ?", [i]) + + assert db.execute("SELECT count(*) FROM t_vectors00").fetchone()[0] == 0 + assert db.execute("SELECT count(*) FROM t_diskann_nodes00").fetchone()[0] == 0 + + medoid = db.execute( + "SELECT value FROM t_info WHERE key = 'diskann_medoid_00'" + ).fetchone()[0] + assert medoid is None + + +def test_diskann_insert_delete_insert_cycle(db): + """Insert, delete, insert again. No crashes.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=8) + ) + """) + db.execute("INSERT INTO t(rowid, emb) VALUES (1, ?)", [_f32([1.0] * 8)]) + db.execute("INSERT INTO t(rowid, emb) VALUES (2, ?)", [_f32([2.0] * 8)]) + db.execute("DELETE FROM t WHERE rowid = 1") + db.execute("INSERT INTO t(rowid, emb) VALUES (3, ?)", [_f32([3.0] * 8)]) + + assert db.execute("SELECT count(*) FROM t_vectors00").fetchone()[0] == 2 + assert db.execute("SELECT count(*) FROM t_diskann_nodes00").fetchone()[0] == 2 + + +def test_diskann_knn_basic(db): + """Basic KNN query should return results.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=8) + ) + """) + db.execute( + "INSERT INTO t(rowid, emb) VALUES (1, ?)", [_f32([1, 0, 0, 0, 0, 0, 0, 0])] + ) + db.execute( + "INSERT INTO t(rowid, emb) VALUES (2, ?)", [_f32([0, 1, 0, 0, 0, 0, 0, 0])] + ) + db.execute( + "INSERT INTO t(rowid, emb) VALUES (3, ?)", [_f32([0.9, 0.1, 0, 0, 0, 0, 0, 0])] + ) + + rows = db.execute( + "SELECT rowid, distance FROM t WHERE emb MATCH ? AND k=2", + [_f32([1, 0, 0, 0, 0, 0, 0, 0])], + ).fetchall() + assert len(rows) == 2 + # Closest should be rowid 1 (exact match) + assert rows[0][0] == 1 + assert rows[0][1] < 0.01 # ~0 distance + + +def test_diskann_knn_distances_sorted(db): + """Returned distances should be in ascending order.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=16) + ) + """) + import random + + random.seed(42) + for i in range(1, 51): + vec = [random.gauss(0, 1) for _ in range(8)] + db.execute("INSERT INTO t(rowid, emb) VALUES (?, ?)", [i, _f32(vec)]) + + rows = db.execute( + "SELECT rowid, distance FROM t WHERE emb MATCH ? AND k=10", + [_f32([0.0] * 8)], + ).fetchall() + assert len(rows) == 10 + distances = [r[1] for r in rows] + for i in range(len(distances) - 1): + assert distances[i] <= distances[i + 1], f"Distances not sorted at index {i}" + + +def test_diskann_knn_empty_table(db): + """KNN on empty table should return 0 results.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=8) + ) + """) + rows = db.execute( + "SELECT rowid, distance FROM t WHERE emb MATCH ? AND k=5", + [_f32([1, 0, 0, 0, 0, 0, 0, 0])], + ).fetchall() + assert len(rows) == 0 + + +def test_diskann_knn_after_delete(db): + """KNN after delete should not return deleted rows.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=8) + ) + """) + db.execute( + "INSERT INTO t(rowid, emb) VALUES (1, ?)", [_f32([1, 0, 0, 0, 0, 0, 0, 0])] + ) + db.execute( + "INSERT INTO t(rowid, emb) VALUES (2, ?)", [_f32([0, 1, 0, 0, 0, 0, 0, 0])] + ) + db.execute( + "INSERT INTO t(rowid, emb) VALUES (3, ?)", [_f32([0.5, 0.5, 0, 0, 0, 0, 0, 0])] + ) + db.execute("DELETE FROM t WHERE rowid = 1") + + rows = db.execute( + "SELECT rowid, distance FROM t WHERE emb MATCH ? AND k=3", + [_f32([1, 0, 0, 0, 0, 0, 0, 0])], + ).fetchall() + rowids = [r[0] for r in rows] + assert 1 not in rowids + assert len(rows) == 2 + + +def test_diskann_no_index_still_works(db): + """Tables without INDEXED BY should still work identically.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[4] + ) + """) + db.execute("INSERT INTO t(rowid, emb) VALUES (1, ?)", [_f32([1, 2, 3, 4])]) + rows = db.execute( + "SELECT rowid, distance FROM t WHERE emb MATCH ? AND k=1", + [_f32([1, 2, 3, 4])], + ).fetchall() + assert len(rows) == 1 + assert rows[0][0] == 1 + + +def test_diskann_drop_table(db): + """DROP TABLE should clean up all shadow tables.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[128] INDEXED BY diskann(neighbor_quantizer=binary) + ) + """) + db.execute("DROP TABLE t") + tables = [ + row[0] + for row in db.execute( + "select name from sqlite_master where name like 't%'" + ).fetchall() + ] + assert len(tables) == 0 + + +def test_diskann_create_split_search_list_size(db): + """DiskANN with separate search_list_size_search and search_list_size_insert.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[128] INDEXED BY diskann( + neighbor_quantizer=binary, + search_list_size_search=256, + search_list_size_insert=64 + ) + ) + """) + tables = [ + row[0] + for row in db.execute( + "select name from sqlite_master where name like 't%' order by 1" + ).fetchall() + ] + assert "t" in tables + + +def test_diskann_create_error_mixed_search_list_size(db): + """Error when mixing search_list_size with search_list_size_search.""" + result = exec( + db, + """ + CREATE VIRTUAL TABLE t USING vec0( + emb float[128] INDEXED BY diskann( + neighbor_quantizer=binary, + search_list_size=128, + search_list_size_search=256 + ) + ) + """, + ) + assert "error" in result + + +def test_diskann_command_search_list_size(db): + """Runtime search_list_size override via command insert.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[64] INDEXED BY diskann(neighbor_quantizer=binary) + ) + """) + import struct, random + + random.seed(42) + for i in range(20): + vec = struct.pack("64f", *[random.random() for _ in range(64)]) + db.execute("INSERT INTO t(emb) VALUES (?)", [vec]) + + # Query with default search_list_size + query = struct.pack("64f", *[random.random() for _ in range(64)]) + results_before = db.execute( + "SELECT rowid, distance FROM t WHERE emb MATCH ? AND k = 5", [query] + ).fetchall() + assert len(results_before) == 5 + + # Override search_list_size_search at runtime + db.execute("INSERT INTO t(t) VALUES ('search_list_size_search=256')") + + # Query should still work + results_after = db.execute( + "SELECT rowid, distance FROM t WHERE emb MATCH ? AND k = 5", [query] + ).fetchall() + assert len(results_after) == 5 + + # Override search_list_size_insert at runtime + db.execute("INSERT INTO t(t) VALUES ('search_list_size_insert=32')") + + # Inserts should still work + vec = struct.pack("64f", *[random.random() for _ in range(64)]) + db.execute("INSERT INTO t(emb) VALUES (?)", [vec]) + + # Override unified search_list_size + db.execute("INSERT INTO t(t) VALUES ('search_list_size=64')") + + results_final = db.execute( + "SELECT rowid, distance FROM t WHERE emb MATCH ? AND k = 5", [query] + ).fetchall() + assert len(results_final) == 5 + + +def test_diskann_command_search_list_size_error(db): + """Error on invalid search_list_size command value.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[64] INDEXED BY diskann(neighbor_quantizer=binary) + ) + """) + result = exec(db, "INSERT INTO t(t) VALUES ('search_list_size=0')") + assert "error" in result + result = exec(db, "INSERT INTO t(t) VALUES ('search_list_size=-1')") + assert "error" in result + + +# ====================================================================== +# Error cases: DiskANN + auxiliary/metadata/partition columns +# ====================================================================== + + +def test_diskann_create_with_auxiliary_column(db): + """DiskANN tables should support auxiliary columns.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[64] INDEXED BY diskann(neighbor_quantizer=binary), + +extra text + ) + """) + # Auxiliary shadow table should exist + tables = [ + r[0] + for r in db.execute( + "SELECT name FROM sqlite_master WHERE name LIKE 't_%' ORDER BY 1" + ).fetchall() + ] + assert "t_auxiliary" in tables + + +def test_diskann_create_error_with_metadata_column(db): + """DiskANN tables should not support metadata columns.""" + result = exec( + db, + """ + CREATE VIRTUAL TABLE t USING vec0( + emb float[64] INDEXED BY diskann(neighbor_quantizer=binary), + metadata_col integer metadata + ) + """, + ) + assert "error" in result + assert "metadata" in result["message"].lower() or "Metadata" in result["message"] + + +def test_diskann_create_error_with_partition_key(db): + """DiskANN tables should not support partition key columns.""" + result = exec( + db, + """ + CREATE VIRTUAL TABLE t USING vec0( + emb float[64] INDEXED BY diskann(neighbor_quantizer=binary), + user_id text partition key + ) + """, + ) + assert "error" in result + assert "partition" in result["message"].lower() or "Partition" in result["message"] + + +# ====================================================================== +# Insert edge cases +# ====================================================================== + + +def test_diskann_insert_no_rowid(db): + """INSERT without explicit rowid (auto-generated) should work.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary) + ) + """) + db.execute("INSERT INTO t(emb) VALUES (?)", [_f32([1.0] * 8)]) + db.execute("INSERT INTO t(emb) VALUES (?)", [_f32([2.0] * 8)]) + assert db.execute("SELECT count(*) FROM t_vectors00").fetchone()[0] == 2 + assert db.execute("SELECT count(*) FROM t_diskann_nodes00").fetchone()[0] == 2 + + +def test_diskann_insert_large_batch(db): + """INSERT 500+ vectors, verify all are queryable via KNN.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[16] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=16) + ) + """) + import random + + random.seed(99) + N = 500 + for i in range(1, N + 1): + vec = [random.gauss(0, 1) for _ in range(16)] + db.execute("INSERT INTO t(rowid, emb) VALUES (?, ?)", [i, _f32(vec)]) + + assert db.execute("SELECT count(*) FROM t_vectors00").fetchone()[0] == N + assert db.execute("SELECT count(*) FROM t_diskann_nodes00").fetchone()[0] == N + + # KNN should return results + query = [random.gauss(0, 1) for _ in range(16)] + rows = db.execute( + "SELECT rowid, distance FROM t WHERE emb MATCH ? AND k=10", + [_f32(query)], + ).fetchall() + assert len(rows) == 10 + # Distances should be sorted + distances = [r[1] for r in rows] + for i in range(len(distances) - 1): + assert distances[i] <= distances[i + 1] + + +def test_diskann_insert_zero_vector(db): + """Insert an all-zero vector (edge case for binary quantizer).""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary) + ) + """) + db.execute("INSERT INTO t(rowid, emb) VALUES (1, ?)", [_f32([0.0] * 8)]) + db.execute("INSERT INTO t(rowid, emb) VALUES (2, ?)", [_f32([1.0] * 8)]) + count = db.execute("SELECT count(*) FROM t_vectors00").fetchone()[0] + assert count == 2 + + # Query with zero vector should find rowid 1 as closest + rows = db.execute( + "SELECT rowid, distance FROM t WHERE emb MATCH ? AND k=2", + [_f32([0.0] * 8)], + ).fetchall() + assert len(rows) == 2 + assert rows[0][0] == 1 + + +def test_diskann_insert_large_values(db): + """Insert vectors with very large float values.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary) + ) + """) + import sys + + large = sys.float_info.max / 1e300 # Large but not overflowing + db.execute("INSERT INTO t(rowid, emb) VALUES (1, ?)", [_f32([large] * 8)]) + db.execute("INSERT INTO t(rowid, emb) VALUES (2, ?)", [_f32([-large] * 8)]) + db.execute("INSERT INTO t(rowid, emb) VALUES (3, ?)", [_f32([0.0] * 8)]) + assert db.execute("SELECT count(*) FROM t_vectors00").fetchone()[0] == 3 + + +def test_diskann_insert_int8_quantizer_knn(db): + """Full insert + query cycle with int8 quantizer.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[16] INDEXED BY diskann(neighbor_quantizer=int8, n_neighbors=8) + ) + """) + import random + + random.seed(77) + for i in range(1, 31): + vec = [random.gauss(0, 1) for _ in range(16)] + db.execute("INSERT INTO t(rowid, emb) VALUES (?, ?)", [i, _f32(vec)]) + + assert db.execute("SELECT count(*) FROM t_vectors00").fetchone()[0] == 30 + + # KNN should work + query = [random.gauss(0, 1) for _ in range(16)] + rows = db.execute( + "SELECT rowid, distance FROM t WHERE emb MATCH ? AND k=5", + [_f32(query)], + ).fetchall() + assert len(rows) == 5 + distances = [r[1] for r in rows] + for i in range(len(distances) - 1): + assert distances[i] <= distances[i + 1] + + +# ====================================================================== +# Delete edge cases +# ====================================================================== + + +def test_diskann_delete_nonexistent(db): + """DELETE of a nonexistent rowid should either be a no-op or return an error, not crash.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=8) + ) + """) + db.execute("INSERT INTO t(rowid, emb) VALUES (1, ?)", [_f32([1.0] * 8)]) + # Deleting a nonexistent rowid may error but should not crash + result = exec(db, "DELETE FROM t WHERE rowid = 999") + # Whether it succeeds or errors, the existing row should still be there + assert db.execute("SELECT count(*) FROM t_vectors00").fetchone()[0] == 1 + + +def test_diskann_delete_then_reinsert_same_rowid(db): + """Delete rowid 5, then reinsert rowid 5 with a new vector.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=8) + ) + """) + for i in range(1, 6): + db.execute("INSERT INTO t(rowid, emb) VALUES (?, ?)", [i, _f32([float(i)] * 8)]) + + db.execute("DELETE FROM t WHERE rowid = 5") + assert db.execute("SELECT count(*) FROM t_vectors00").fetchone()[0] == 4 + + # Reinsert with new vector + db.execute("INSERT INTO t(rowid, emb) VALUES (5, ?)", [_f32([99.0] * 8)]) + assert db.execute("SELECT count(*) FROM t_vectors00").fetchone()[0] == 5 + assert db.execute("SELECT count(*) FROM t_diskann_nodes00").fetchone()[0] == 5 + + +def test_diskann_delete_all_then_insert(db): + """Delete everything, then insert new vectors. Graph should rebuild.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=8) + ) + """) + for i in range(1, 6): + db.execute("INSERT INTO t(rowid, emb) VALUES (?, ?)", [i, _f32([float(i)] * 8)]) + + # Delete all + for i in range(1, 6): + db.execute("DELETE FROM t WHERE rowid = ?", [i]) + assert db.execute("SELECT count(*) FROM t_vectors00").fetchone()[0] == 0 + + medoid = db.execute( + "SELECT value FROM t_info WHERE key = 'diskann_medoid_00'" + ).fetchone()[0] + assert medoid is None + + # Insert new vectors + for i in range(10, 15): + db.execute("INSERT INTO t(rowid, emb) VALUES (?, ?)", [i, _f32([float(i)] * 8)]) + + assert db.execute("SELECT count(*) FROM t_vectors00").fetchone()[0] == 5 + assert db.execute("SELECT count(*) FROM t_diskann_nodes00").fetchone()[0] == 5 + + medoid = db.execute( + "SELECT value FROM t_info WHERE key = 'diskann_medoid_00'" + ).fetchone()[0] + assert medoid is not None + + # KNN should work + rows = db.execute( + "SELECT rowid FROM t WHERE emb MATCH ? AND k=3", + [_f32([12.0] * 8)], + ).fetchall() + assert len(rows) == 3 + + +def test_diskann_delete_preserves_graph_connectivity(db): + """After deleting a node, remaining nodes should still be reachable via KNN.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=8) + ) + """) + import random + + random.seed(456) + for i in range(1, 21): + vec = [random.gauss(0, 1) for _ in range(8)] + db.execute("INSERT INTO t(rowid, emb) VALUES (?, ?)", [i, _f32(vec)]) + + # Delete 5 nodes + for i in [3, 7, 11, 15, 19]: + db.execute("DELETE FROM t WHERE rowid = ?", [i]) + + remaining = db.execute("SELECT count(*) FROM t_vectors00").fetchone()[0] + assert remaining == 15 + + # Every remaining node should be reachable via KNN (appears somewhere in top-k) + all_rowids = [r[0] for r in db.execute("SELECT rowid FROM t_vectors00").fetchall()] + reachable = set() + for rid in all_rowids: + vec_blob = db.execute( + "SELECT vector FROM t_vectors00 WHERE rowid = ?", [rid] + ).fetchone()[0] + rows = db.execute( + "SELECT rowid FROM t WHERE emb MATCH ? AND k=5", + [vec_blob], + ).fetchall() + assert len(rows) >= 1 # At least some results + for r in rows: + reachable.add(r[0]) + # Most nodes should be reachable through the graph + assert ( + len(reachable) >= len(all_rowids) * 0.8 + ), f"Only {len(reachable)}/{len(all_rowids)} nodes reachable" + + +# ====================================================================== +# Update scenarios +# ====================================================================== + + +def test_diskann_update_vector(db): + """UPDATE a vector on DiskANN table should error (will be implemented soon).""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=8) + ) + """) + db.execute( + "INSERT INTO t(rowid, emb) VALUES (1, ?)", [_f32([1, 0, 0, 0, 0, 0, 0, 0])] + ) + db.execute( + "INSERT INTO t(rowid, emb) VALUES (2, ?)", [_f32([0, 1, 0, 0, 0, 0, 0, 0])] + ) + db.execute( + "INSERT INTO t(rowid, emb) VALUES (3, ?)", [_f32([0, 0, 1, 0, 0, 0, 0, 0])] + ) + + with pytest.raises( + sqlite3.OperationalError, + match="UPDATE on vector column.*not supported for DiskANN", + ): + db.execute( + "UPDATE t SET emb = ? WHERE rowid = 1", [_f32([0, 0.9, 0.1, 0, 0, 0, 0, 0])] + ) + + +# ====================================================================== +# KNN correctness after mutations +# ====================================================================== + + +def test_diskann_knn_recall_after_inserts(db): + """Insert N vectors, verify top-1 recall is 100% for exact matches.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=16) + ) + """) + import random + + random.seed(200) + vectors = {} + for i in range(1, 51): + vec = [random.gauss(0, 1) for _ in range(8)] + vectors[i] = vec + db.execute("INSERT INTO t(rowid, emb) VALUES (?, ?)", [i, _f32(vec)]) + + # Top-1 for each vector should return itself + correct = 0 + for rid, vec in vectors.items(): + rows = db.execute( + "SELECT rowid FROM t WHERE emb MATCH ? AND k=1", + [_f32(vec)], + ).fetchall() + if rows and rows[0][0] == rid: + correct += 1 + + # With binary quantizer, approximate search may not always return exact match + # but should have high recall (at least 80%) + assert ( + correct >= len(vectors) * 0.8 + ), f"Top-1 recall too low: {correct}/{len(vectors)}" + + +def test_diskann_knn_k_larger_than_table(db): + """k=100 on table with 5 rows should return 5.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=8) + ) + """) + for i in range(1, 6): + db.execute("INSERT INTO t(rowid, emb) VALUES (?, ?)", [i, _f32([float(i)] * 8)]) + + rows = db.execute( + "SELECT rowid, distance FROM t WHERE emb MATCH ? AND k=100", + [_f32([3.0] * 8)], + ).fetchall() + assert len(rows) == 5 + + +def test_diskann_knn_cosine_metric(db): + """KNN with cosine distance metric.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] distance_metric=cosine INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=8) + ) + """) + # Insert orthogonal-ish vectors + db.execute( + "INSERT INTO t(rowid, emb) VALUES (1, ?)", [_f32([1, 0, 0, 0, 0, 0, 0, 0])] + ) + db.execute( + "INSERT INTO t(rowid, emb) VALUES (2, ?)", [_f32([0, 1, 0, 0, 0, 0, 0, 0])] + ) + db.execute( + "INSERT INTO t(rowid, emb) VALUES (3, ?)", [_f32([0.7, 0.7, 0, 0, 0, 0, 0, 0])] + ) + + rows = db.execute( + "SELECT rowid, distance FROM t WHERE emb MATCH ? AND k=3", + [_f32([1, 0, 0, 0, 0, 0, 0, 0])], + ).fetchall() + assert len(rows) == 3 + # rowid 1 should be closest (exact match in direction) + assert rows[0][0] == 1 + # Distances should be sorted + distances = [r[1] for r in rows] + for i in range(len(distances) - 1): + assert distances[i] <= distances[i + 1] + + +def test_diskann_knn_after_heavy_churn(db): + """Interleave many inserts and deletes, then query.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=16) + ) + """) + import random + + random.seed(321) + + # Insert 50 vectors + for i in range(1, 51): + vec = [random.gauss(0, 1) for _ in range(8)] + db.execute("INSERT INTO t(rowid, emb) VALUES (?, ?)", [i, _f32(vec)]) + + # Delete even-numbered rows + for i in range(2, 51, 2): + db.execute("DELETE FROM t WHERE rowid = ?", [i]) + + # Insert more vectors with higher rowids + for i in range(51, 76): + vec = [random.gauss(0, 1) for _ in range(8)] + db.execute("INSERT INTO t(rowid, emb) VALUES (?, ?)", [i, _f32(vec)]) + + remaining = db.execute("SELECT count(*) FROM t_vectors00").fetchone()[0] + assert remaining == 50 # 25 odd + 25 new + + # KNN should still work and return results + query = [random.gauss(0, 1) for _ in range(8)] + rows = db.execute( + "SELECT rowid, distance FROM t WHERE emb MATCH ? AND k=10", + [_f32(query)], + ).fetchall() + assert len(rows) == 10 + # Distances should be sorted + distances = [r[1] for r in rows] + for i in range(len(distances) - 1): + assert distances[i] <= distances[i + 1] + + +def test_diskann_knn_batch_recall(db): + """Insert 100+ vectors and verify reasonable recall.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[16] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=16) + ) + """) + import random + + random.seed(42) + N = 150 + vectors = {} + for i in range(1, N + 1): + vec = [random.gauss(0, 1) for _ in range(16)] + vectors[i] = vec + db.execute("INSERT INTO t(rowid, emb) VALUES (?, ?)", [i, _f32(vec)]) + + # Brute-force top-5 for a query and compare with DiskANN + query = [random.gauss(0, 1) for _ in range(16)] + + # Compute true distances + true_dists = [] + for rid, vec in vectors.items(): + d = sum((a - b) ** 2 for a, b in zip(query, vec)) + true_dists.append((d, rid)) + true_dists.sort() + true_top5 = set(r for _, r in true_dists[:5]) + + rows = db.execute( + "SELECT rowid, distance FROM t WHERE emb MATCH ? AND k=5", + [_f32(query)], + ).fetchall() + result_top5 = set(r[0] for r in rows) + assert len(rows) == 5 + + # At least 3 of top-5 should match (reasonable recall for approximate search) + overlap = len(true_top5 & result_top5) + assert overlap >= 3, f"Recall too low: only {overlap}/5 overlap" + + +# ====================================================================== +# Additional edge cases +# ====================================================================== + + +def test_diskann_insert_wrong_dimensions(db): + """INSERT with wrong dimension vector should error.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=8) + ) + """) + result = exec(db, "INSERT INTO t(rowid, emb) VALUES (1, ?)", [_f32([1.0] * 4)]) + assert "error" in result + + +def test_diskann_knn_wrong_query_dimensions(db): + """KNN MATCH with wrong dimension query should error.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=8) + ) + """) + db.execute("INSERT INTO t(rowid, emb) VALUES (1, ?)", [_f32([1.0] * 8)]) + + result = exec( + db, "SELECT rowid FROM t WHERE emb MATCH ? AND k=1", [_f32([1.0] * 4)] + ) + assert "error" in result + + +def test_diskann_graph_connectivity_after_many_deletes(db): + """After many deletes, the graph should still be connected enough for search.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=16) + ) + """) + import random + + random.seed(789) + N = 40 + for i in range(1, N + 1): + vec = [random.gauss(0, 1) for _ in range(8)] + db.execute("INSERT INTO t(rowid, emb) VALUES (?, ?)", [i, _f32(vec)]) + + # Delete 30 of 40 nodes + to_delete = list(range(1, 31)) + for i in to_delete: + db.execute("DELETE FROM t WHERE rowid = ?", [i]) + + remaining = db.execute("SELECT count(*) FROM t_vectors00").fetchone()[0] + assert remaining == 10 + + # Search should still work and return results + query = [random.gauss(0, 1) for _ in range(8)] + rows = db.execute( + "SELECT rowid, distance FROM t WHERE emb MATCH ? AND k=10", + [_f32(query)], + ).fetchall() + # Should return some results (graph may be fragmented after heavy deletion) + assert len(rows) >= 1 + # Distances should be sorted + distances = [r[1] for r in rows] + for i in range(len(distances) - 1): + assert distances[i] <= distances[i + 1] + + +def test_diskann_large_batch_insert_500(db): + """Insert 500+ vectors and verify counts and KNN.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=16) + ) + """) + import random + + random.seed(555) + N = 500 + for i in range(1, N + 1): + vec = [random.gauss(0, 1) for _ in range(8)] + db.execute("INSERT INTO t(rowid, emb) VALUES (?, ?)", [i, _f32(vec)]) + + assert db.execute("SELECT count(*) FROM t_vectors00").fetchone()[0] == N + + query = [random.gauss(0, 1) for _ in range(8)] + rows = db.execute( + "SELECT rowid, distance FROM t WHERE emb MATCH ? AND k=20", + [_f32(query)], + ).fetchall() + assert len(rows) == 20 + distances = [r[1] for r in rows] + for i in range(len(distances) - 1): + assert distances[i] <= distances[i + 1] + + +def test_corrupt_truncated_node_blob(db): + """KNN should error (not crash) when DiskANN node blob is truncated.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=8) + ) + """) + for i in range(5): + vec = [0.0] * 8 + vec[i % 8] = 1.0 + db.execute("INSERT INTO t(rowid, emb) VALUES (?, ?)", [i + 1, _f32(vec)]) + + # Corrupt a DiskANN node: truncate neighbor_ids to 1 byte (wrong size) + db.execute("UPDATE t_diskann_nodes00 SET neighbor_ids = x'00' WHERE rowid = 1") + + # Should not crash — may return wrong results or error + try: + db.execute( + "SELECT rowid FROM t WHERE emb MATCH ? AND k=3", + [_f32([1, 0, 0, 0, 0, 0, 0, 0])], + ).fetchall() + except sqlite3.OperationalError: + pass # Error is acceptable — crash is not + + +def test_diskann_delete_reinsert_cycle_knn(db): + """Repeatedly delete and reinsert rows, verify KNN stays correct.""" + import random + + random.seed(101) + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=8) + ) + """) + N = 30 + vecs = {} + for i in range(1, N + 1): + v = [random.gauss(0, 1) for _ in range(8)] + vecs[i] = v + db.execute("INSERT INTO t(rowid, emb) VALUES (?, ?)", [i, _f32(v)]) + + # 3 cycles: delete half, reinsert with new vectors, verify KNN + for cycle in range(3): + to_delete = random.sample(sorted(vecs.keys()), len(vecs) // 2) + for r in to_delete: + db.execute("DELETE FROM t WHERE rowid = ?", [r]) + del vecs[r] + + # Reinsert with new rowids + new_start = 100 + cycle * 50 + for i in range(len(to_delete)): + rid = new_start + i + v = [random.gauss(0, 1) for _ in range(8)] + vecs[rid] = v + db.execute("INSERT INTO t(rowid, emb) VALUES (?, ?)", [rid, _f32(v)]) + + # KNN should return only alive rows + query = [0.0] * 8 + rows = db.execute( + "SELECT rowid FROM t WHERE emb MATCH ? AND k=10", + [_f32(query)], + ).fetchall() + returned = {r["rowid"] for r in rows} + assert returned.issubset( + set(vecs.keys()) + ), f"Cycle {cycle}: deleted rowid in KNN results" + assert len(rows) >= 1 + + +def test_diskann_delete_interleaved_with_knn(db): + """Delete one row at a time, querying KNN after each delete.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=8) + ) + """) + N = 20 + for i in range(1, N + 1): + vec = [0.0] * 8 + vec[i % 8] = float(i) + db.execute("INSERT INTO t(rowid, emb) VALUES (?, ?)", [i, _f32(vec)]) + + alive = set(range(1, N + 1)) + for to_del in [1, 5, 10, 15, 20]: + db.execute("DELETE FROM t WHERE rowid = ?", [to_del]) + alive.discard(to_del) + + rows = db.execute( + "SELECT rowid FROM t WHERE emb MATCH ? AND k=5", + [_f32([1, 0, 0, 0, 0, 0, 0, 0])], + ).fetchall() + returned = {r["rowid"] for r in rows} + assert returned.issubset(alive), f"Deleted rowid {to_del} found in KNN results" + + +# ====================================================================== +# Text primary key + DiskANN +# ====================================================================== + + +def test_diskann_text_pk_insert_knn_delete(db): + """DiskANN with text primary key: insert, KNN, delete, KNN again.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + id text primary key, + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=8) + ) + """) + + vecs = { + "alpha": [1, 0, 0, 0, 0, 0, 0, 0], + "beta": [0, 1, 0, 0, 0, 0, 0, 0], + "gamma": [0, 0, 1, 0, 0, 0, 0, 0], + "delta": [0, 0, 0, 1, 0, 0, 0, 0], + "epsilon": [0, 0, 0, 0, 1, 0, 0, 0], + } + for name, vec in vecs.items(): + db.execute("INSERT INTO t(id, emb) VALUES (?, ?)", [name, _f32(vec)]) + + # KNN should return text IDs + rows = db.execute( + "SELECT id, distance FROM t WHERE emb MATCH ? AND k=3", + [_f32([1, 0, 0, 0, 0, 0, 0, 0])], + ).fetchall() + assert len(rows) >= 1 + ids = [r["id"] for r in rows] + assert "alpha" in ids # closest to query + + # Delete and verify + db.execute("DELETE FROM t WHERE id = 'alpha'") + rows = db.execute( + "SELECT id FROM t WHERE emb MATCH ? AND k=3", + [_f32([1, 0, 0, 0, 0, 0, 0, 0])], + ).fetchall() + ids = [r["id"] for r in rows] + assert "alpha" not in ids + + +def test_diskann_delete_scrubs_all_references(db): + """After DELETE, no shadow table should contain the deleted rowid or its data.""" + import struct + + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=8) + ) + """) + for i in range(20): + vec = struct.pack("8f", *[float(i + d) for d in range(8)]) + db.execute("INSERT INTO t(rowid, emb) VALUES (?, ?)", [i, vec]) + + target = 5 + db.execute("DELETE FROM t WHERE rowid = ?", [target]) + + # Node row itself should be gone + assert ( + db.execute( + "SELECT count(*) FROM t_diskann_nodes00 WHERE rowid=?", [target] + ).fetchone()[0] + == 0 + ) + + # Vector should be gone + assert ( + db.execute( + "SELECT count(*) FROM t_vectors00 WHERE rowid=?", [target] + ).fetchone()[0] + == 0 + ) + + # No other node should reference the deleted rowid in neighbor_ids + for row in db.execute("SELECT rowid, neighbor_ids FROM t_diskann_nodes00"): + node_rowid = row[0] + ids_blob = row[1] + for j in range(0, len(ids_blob), 8): + nid = struct.unpack(" 0.4", []), + ("k = 10 AND distance <= 0.8", []), + ("k = 10 AND distance >= 0.2 AND distance < 0.9", []), + ("k = 5 AND rowid IN (3, 7, 11, 19, 23, 31, 999)", []), + ("k = 3 AND rowid = 17", []), + ("k = 6 AND mmr_lambda = 0.5", []), + ("k = 6 AND mmr_lambda = 0.2 AND distance > 0.3", []), + ], +) +def test_diskann_knn_answers_as_flat_when_the_search_covers_the_table( + db, where, params +): + db.execute(""" + CREATE VIRTUAL TABLE d USING vec0( + emb float[16] distance_metric=cosine + INDEXED BY diskann(neighbor_quantizer=int8, n_neighbors=8, + search_list_size=128) + ) + """) + db.execute( + "CREATE VIRTUAL TABLE f USING vec0(emb float[16] distance_metric=cosine)" + ) + rows = _random_rows(11, 60, 16) + for rowid, vec in rows.items(): + for table in ("d", "f"): + db.execute( + f"INSERT INTO {table}(rowid, emb) VALUES (?, ?)", [rowid, _f32(vec)] + ) + rng = random.Random(12) + for _ in range(10): + query = _f32([rng.uniform(-1, 1) for _ in range(16)]) + sql = "SELECT rowid, distance FROM {} WHERE emb MATCH ? AND " + where + got = db.execute(sql.format("d"), [query, *params]).fetchall() + want = db.execute(sql.format("f"), [query, *params]).fetchall() + assert [tuple(r) for r in got] == [tuple(r) for r in want] + + +@pytest.mark.parametrize("quantizer", ["int8", "binary"]) +def test_diskann_int8_column(db, quantizer): + """An int8 column's vectors are quantized as int8 values, query included.""" + db.execute(f""" + CREATE VIRTUAL TABLE t USING vec0( + emb int8[64] INDEXED BY diskann(neighbor_quantizer={quantizer}, + n_neighbors=16) + ) + """) + rng = random.Random(3) + for i in range(1, 101): + vec = struct.pack("64b", *[rng.randint(-100, 100) for _ in range(64)]) + db.execute("INSERT INTO t(rowid, emb) VALUES (?, vec_int8(?))", [i, vec]) + for _ in range(20): + query = struct.pack("64b", *[rng.randint(-100, 100) for _ in range(64)]) + got = db.execute( + "SELECT rowid, distance FROM t WHERE emb MATCH vec_int8(?) AND k = 10", + [query], + ).fetchall() + want = db.execute( + "SELECT rowid, vec_distance_l2(emb, vec_int8(?)) AS d FROM t " + "ORDER BY d LIMIT 10", + [query], + ).fetchall() + assert [tuple(r) for r in got] == [tuple(r) for r in want] + + +def test_diskann_knn_distances_are_exact(db): + """A result's distance is its exact one, never its quantized one.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[32] distance_metric=cosine + INDEXED BY diskann(neighbor_quantizer=int8, n_neighbors=16) + ) + """) + for rowid, vec in _random_rows(5, 300, 32).items(): + db.execute("INSERT INTO t(rowid, emb) VALUES (?, ?)", [rowid, _f32(vec)]) + rng = random.Random(6) + for _ in range(30): + query = _f32([rng.uniform(-1, 1) for _ in range(32)]) + got = db.execute( + "SELECT rowid, distance FROM t WHERE emb MATCH ? AND k = 10", [query] + ).fetchall() + exact = dict( + db.execute( + "SELECT rowid, vec_distance_cosine(emb, ?) FROM t", [query] + ).fetchall() + ) + assert [d for _, d in got] == [exact[r] for r, _ in got] + assert [d for _, d in got] == sorted(d for _, d in got) + + +def test_diskann_deleted_medoid_is_replaced_from_the_graph(db): + """A medoid deleted while vectors wait in the buffer is replaced by a node.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[16] INDEXED BY diskann(neighbor_quantizer=int8, + n_neighbors=8, buffer_threshold=10) + ) + """) + rng = random.Random(9) + vecs = {} + # ten rows flush into the graph, then three wait in the buffer with rowids + # below every node's + for rowid in [*range(100, 110), 1, 2, 3]: + vecs[rowid] = [rng.uniform(-1, 1) for _ in range(16)] + db.execute( + "INSERT INTO t(rowid, emb) VALUES (?, ?)", [rowid, _f32(vecs[rowid])] + ) + medoid_sql = "SELECT value FROM t_info WHERE key = 'diskann_medoid_00'" + db.execute("DELETE FROM t WHERE rowid = ?", [db.execute(medoid_sql).fetchone()[0]]) + + def nodes(): + return {r for (r,) in db.execute("SELECT rowid FROM t_diskann_nodes00")} + + def finds_itself(rowid): + return ( + db.execute( + "SELECT rowid FROM t WHERE emb MATCH ? AND k = 1", [_f32(vecs[rowid])] + ).fetchone()[0] + == rowid + ) + + assert db.execute(medoid_sql).fetchone()[0] in nodes() + assert all(finds_itself(r) for r in nodes()) + for rowid in range(4, 11): + vecs[rowid] = [rng.uniform(-1, 1) for _ in range(16)] + db.execute( + "INSERT INTO t(rowid, emb) VALUES (?, ?)", [rowid, _f32(vecs[rowid])] + ) + assert len(nodes()) == 19 + assert all(finds_itself(r) for r in nodes()) + + +def test_diskann_point_query_for_a_missing_rowid(db): + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary) + ) + """) + db.execute("INSERT INTO t(rowid, emb) VALUES (1, ?)", [_f32([1] * 8)]) + assert db.execute("SELECT rowid, emb FROM t WHERE rowid = 2").fetchall() == [] + assert [tuple(r) for r in db.execute("SELECT rowid FROM t WHERE rowid = 1")] == [ + (1,) + ] + + +def test_diskann_corrupt_node_blob_errors(db): + """A node whose blobs have the wrong size fails the query that reads it.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=8) + ) + """) + for i in range(5): + vec = [0.0] * 8 + vec[i % 8] = 1.0 + db.execute("INSERT INTO t(rowid, emb) VALUES (?, ?)", [i + 1, _f32(vec)]) + medoid = db.execute( + "SELECT value FROM t_info WHERE key = 'diskann_medoid_00'" + ).fetchone()[0] + db.execute( + "UPDATE t_diskann_nodes00 SET neighbor_ids = x'00' WHERE rowid = ?", [medoid] + ) + with pytest.raises(sqlite3.OperationalError, match="corrupt"): + db.execute( + "SELECT rowid FROM t WHERE emb MATCH ? AND k = 3", [_f32([1] + [0] * 7)] + ).fetchall() + + +@pytest.mark.parametrize( + "option", + [ + "neighbor_quantizer=bin", + "neighbor_quantizer=binary, n_neigh=16", + "neighbor_quantizer=binary, search_list_size=99999999999", + "neighbor_quantizer=binary, n_neighbors=0", + "neighbor_quantizer=binary, n_neighbors=264", + "neighbor_quantizer=binary, search_list_size=0", + "neighbor_quantizer=binary,", + ], +) +def test_diskann_create_option_must_be_whole(db, option): + result = exec( + db, + f"CREATE VIRTUAL TABLE t USING vec0(emb float[8] INDEXED BY diskann({option}))", + ) + assert "error" in result + + +@pytest.mark.parametrize( + "command, message", + [ + ("search_list_size=12abc", "search_list_size must be >= 1"), + ("search_list_size=", "search_list_size must be >= 1"), + ("search_list_size_search= 64", "search_list_size_search must be >= 1"), + ("search_list_size_insert=0", "search_list_size_insert must be >= 1"), + ("search_list_sizes=64", "unknown vec0 command"), + ], +) +def test_diskann_command_value_must_be_digits(db, command, message): + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann(neighbor_quantizer=binary) + ) + """) + with pytest.raises(sqlite3.OperationalError, match=message): + db.execute("INSERT INTO t(t) VALUES (?)", [command]) + + +def test_diskann_optimize_beside_a_flat_column(db): + """optimize moves a flat column's vectors and leaves a DiskANN column's.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + f float[8], + d float[8] INDEXED BY diskann(neighbor_quantizer=binary), + chunk_size=8 + ) + """) + rows = _random_rows(13, 40, 8) + for rowid, vec in rows.items(): + db.execute( + "INSERT INTO t(rowid, f, d) VALUES (?, ?, ?)", + [rowid, _f32(vec), _f32([-x for x in vec])], + ) + for rowid in range(1, 41, 3): + db.execute("DELETE FROM t WHERE rowid = ?", [rowid]) + del rows[rowid] + db.execute("INSERT INTO t(t) VALUES ('optimize')") + for rowid, f, d in db.execute("SELECT rowid, f, d FROM t").fetchall(): + assert f == _f32(rows[rowid]) + assert d == _f32([-x for x in rows[rowid]]) + for rowid, vec in rows.items(): + assert ( + db.execute( + "SELECT rowid FROM t WHERE d MATCH ? AND k = 1", + [_f32([-x for x in vec])], + ).fetchone()[0] + == rowid + ) diff --git a/tests/test-error-paths.py b/tests/test-error-paths.py index bbf0eb20..ca693d8e 100644 --- a/tests/test-error-paths.py +++ b/tests/test-error-paths.py @@ -14,7 +14,13 @@ import subprocess import sys -from conftest import get_extension_path +from conftest import _has_build_flag, get_extension_path + +HAS_IVF = _has_build_flag("ivf") +ivf_only = pytest.mark.skipif( + not HAS_IVF, + reason="IVF not enabled (compile with -DSQLITE_VEC_EXPERIMENTAL_IVF_ENABLE=1)", +) def _raises(message, error=sqlite3.OperationalError): @@ -417,6 +423,74 @@ def interrupt_template(): "INSERT INTO vt(id, vector) VALUES (?, ?)", [(f"id{i}", f"[{i}, {i % 3}]") for i in range(1, 9)], ) + # vr is indexed by rescore(). Its first chunk has a deleted row for optimize + # to skip, and rowid 9 is the only row of its second chunk. + db.execute( + "CREATE VIRTUAL TABLE vr USING vec0(vector float[2] indexed by " + "rescore(quantizer=int8), +a text, chunk_size=8)" + ) + db.executemany( + "INSERT INTO vr(rowid, vector, a) VALUES (?, ?, ?)", + [(i, f"[{i / 10}, {i % 3 / 10}]", f"aux {i}") for i in range(1, 10)], + ) + db.execute("DELETE FROM vr WHERE rowid = 2") + # vd and vb are indexed by diskann(), so they have no chunks, and rowid 1, + # inserted first, is the medoid of each graph. vb adds vectors to its graph + # four at a time: rowids 1 to 4 are nodes, 5 to 7 wait in its buffer, and + # one more insert adds them. + diskann_vectors = [ + struct.pack("8f", *[(i * 7 + j * 3) % 11 / 10 - 0.5 for j in range(8)]) + for i in range(10) + ] + db.execute( + "CREATE VIRTUAL TABLE vd USING vec0(vector float[8] indexed by " + "diskann(neighbor_quantizer=binary, n_neighbors=8), +a text)" + ) + db.executemany( + "INSERT INTO vd(rowid, vector, a) VALUES (?, ?, ?)", + [(i, diskann_vectors[i], f"aux {i}") for i in range(1, 10)], + ) + db.execute( + "CREATE VIRTUAL TABLE vb USING vec0(vector float[8] indexed by " + "diskann(neighbor_quantizer=binary, n_neighbors=8, buffer_threshold=4))" + ) + db.executemany( + "INSERT INTO vb(rowid, vector) VALUES (?, ?)", + [(i, diskann_vectors[i]) for i in range(1, 8)], + ) + if HAS_IVF: + # vi and viq are indexed by ivf(), with centroids: rows 1 to 9 are in + # the cells of their centroids, and rowid 3 left a free slot. viq + # quantizes, so it keeps its float32 vectors in _ivf_vectors00. viu has + # two centroids set by hand and its rows in cells of no centroid, for + # assign-vectors. + for table, options in ( + ("vi", "nlist=2, nprobe=2"), + ("viq", "nlist=2, quantizer=int8, oversample=4"), + ): + db.execute( + f"CREATE VIRTUAL TABLE {table} USING vec0(vector float[8] " + f"indexed by ivf({options}), +a text)" + ) + db.executemany( + f"INSERT INTO {table}(rowid, vector, a) VALUES (?, ?, ?)", + [(i, diskann_vectors[i], f"aux {i}") for i in range(1, 10)], + ) + db.execute(f"INSERT INTO {table}({table}) VALUES ('compute-centroids')") + db.execute(f"DELETE FROM {table} WHERE rowid = 3") + db.execute( + "CREATE VIRTUAL TABLE viu USING vec0(vector float[8] " + "indexed by ivf(nlist=0))" + ) + db.executemany( + "INSERT INTO viu(rowid, vector) VALUES (?, ?)", + [(i, diskann_vectors[i]) for i in range(1, 7)], + ) + for centroid in (0, 1): + db.execute( + f"INSERT INTO viu(viu, vector) VALUES ('set-centroid:{centroid}', ?)", + [diskann_vectors[centroid + 7]], + ) db.commit() return db.serialize() @@ -504,6 +578,149 @@ def handler(): pytest.param("DELETE FROM vt WHERE id = 'id4'", id="delete-text-pk"), pytest.param("DELETE FROM vp WHERE rowid = 4", id="delete-emptying-chunk"), pytest.param("INSERT INTO vp(vp) VALUES ('optimize')", id="optimize"), + pytest.param( + "SELECT rowid, distance, a FROM vr WHERE vector MATCH '[0.3, 0.4]' " + "AND k = 3 AND distance < 10", + id="knn-rescore", + ), + pytest.param("SELECT rowid, vector, a FROM vr", id="fullscan-rescore"), + pytest.param( + "SELECT rowid, vector, a FROM vr WHERE rowid = 4", id="point-rescore" + ), + pytest.param( + "INSERT INTO vr(rowid, vector, a) VALUES (100, '[0.1, 0.1]', 'x')", + id="insert-rescore", + ), + pytest.param( + "UPDATE vr SET vector = '[0.9, 0.9]' WHERE rowid = 4", id="update-rescore" + ), + pytest.param("DELETE FROM vr WHERE rowid = 4", id="delete-rescore"), + pytest.param( + "DELETE FROM vr WHERE rowid = 9", id="delete-emptying-rescore-chunk" + ), + pytest.param("INSERT INTO vr(vr) VALUES ('optimize')", id="optimize-rescore"), + pytest.param( + "SELECT rowid, distance, a FROM vd WHERE vector MATCH " + "'[0.1, 0.2, -0.3, 0.4, -0.1, 0.2, 0.3, -0.4]' AND k = 3 " + "AND distance < 10", + id="knn-diskann", + ), + pytest.param( + "SELECT rowid, distance FROM vd WHERE vector MATCH " + "'[0.1, 0.2, -0.3, 0.4, -0.1, 0.2, 0.3, -0.4]' AND k = 2 " + "AND rowid IN (1, 3, 5)", + id="knn-rowid-in-diskann", + ), + pytest.param( + "SELECT rowid, distance FROM vb WHERE vector MATCH " + "'[0.1, 0.2, -0.3, 0.4, -0.1, 0.2, 0.3, -0.4]' AND k = 3", + id="knn-buffered-diskann", + ), + pytest.param("SELECT rowid, vector, a FROM vd", id="fullscan-diskann"), + pytest.param( + "SELECT rowid, vector, a FROM vd WHERE rowid = 4", id="point-diskann" + ), + pytest.param( + "INSERT INTO vd(rowid, vector, a) VALUES " + "(100, '[0.1, 0.1, 0.1, 0.1, -0.1, -0.1, -0.1, -0.1]', 'x')", + id="insert-diskann", + ), + pytest.param( + "INSERT INTO vb(rowid, vector) VALUES " + "(100, '[0.1, 0.1, 0.1, 0.1, -0.1, -0.1, -0.1, -0.1]')", + id="insert-flushing-diskann-buffer", + ), + pytest.param("UPDATE vd SET a = 'y' WHERE rowid = 4", id="update-diskann"), + pytest.param("DELETE FROM vd WHERE rowid = 4", id="delete-diskann"), + pytest.param("DELETE FROM vd WHERE rowid = 1", id="delete-diskann-medoid"), + pytest.param("DELETE FROM vb WHERE rowid = 6", id="delete-diskann-buffered"), + pytest.param("INSERT INTO vd(vd) VALUES ('optimize')", id="optimize-diskann"), + pytest.param( + "SELECT rowid, distance, a FROM vi WHERE vector MATCH " + "'[0.1, 0.2, -0.3, 0.4, -0.1, 0.2, 0.3, -0.4]' AND k = 3 " + "AND distance < 10", + id="knn-ivf", + marks=ivf_only, + ), + pytest.param( + "SELECT rowid, distance FROM vi WHERE vector MATCH " + "'[0.1, 0.2, -0.3, 0.4, -0.1, 0.2, 0.3, -0.4]' AND k = 2 " + "AND rowid IN (1, 3, 5)", + id="knn-rowid-in-ivf", + marks=ivf_only, + ), + pytest.param( + "SELECT rowid, distance FROM viq WHERE vector MATCH " + "'[0.1, 0.2, -0.3, 0.4, -0.1, 0.2, 0.3, -0.4]' AND k = 3", + id="knn-quantized-ivf", + marks=ivf_only, + ), + pytest.param( + "SELECT rowid, vector, a FROM vi", id="fullscan-ivf", marks=ivf_only + ), + pytest.param( + "SELECT rowid, vector FROM viq", id="fullscan-quantized-ivf", marks=ivf_only + ), + pytest.param( + "SELECT rowid, vector, a FROM vi WHERE rowid = 4", + id="point-ivf", + marks=ivf_only, + ), + pytest.param( + "INSERT INTO vi(rowid, vector, a) VALUES " + "(100, '[0.1, 0.1, 0.1, 0.1, -0.1, -0.1, -0.1, -0.1]', 'x')", + id="insert-ivf", + marks=ivf_only, + ), + pytest.param( + "INSERT INTO viq(rowid, vector, a) VALUES " + "(100, '[0.1, 0.1, 0.1, 0.1, -0.1, -0.1, -0.1, -0.1]', 'x')", + id="insert-quantized-ivf", + marks=ivf_only, + ), + pytest.param( + "INSERT INTO viu(rowid, vector) VALUES " + "(100, '[0.1, 0.1, 0.1, 0.1, -0.1, -0.1, -0.1, -0.1]')", + id="insert-unassigned-ivf", + marks=ivf_only, + ), + pytest.param( + "UPDATE vi SET a = 'y' WHERE rowid = 4", id="update-ivf", marks=ivf_only + ), + pytest.param("DELETE FROM vi WHERE rowid = 4", id="delete-ivf", marks=ivf_only), + pytest.param( + "DELETE FROM viq WHERE rowid = 4", id="delete-quantized-ivf", marks=ivf_only + ), + pytest.param( + "INSERT INTO vi(vi) VALUES ('optimize')", id="optimize-ivf", marks=ivf_only + ), + pytest.param( + "INSERT INTO vi(vi) VALUES ('compute-centroids')", + id="compute-centroids-ivf", + marks=ivf_only, + ), + pytest.param( + "INSERT INTO viq(viq) VALUES " + """('compute-centroids:{"nlist": 3, "seed": 2}')""", + id="compute-centroids-quantized-ivf", + marks=ivf_only, + ), + pytest.param( + "INSERT INTO viq(viq) VALUES ('clear-centroids')", + id="clear-centroids-ivf", + marks=ivf_only, + ), + pytest.param( + "INSERT INTO viu(viu, vector) VALUES " + "('set-centroid:5', '[0.1, 0.1, 0.1, 0.1, -0.1, -0.1, -0.1, -0.1]')", + id="set-centroid-ivf", + marks=ivf_only, + ), + pytest.param( + "INSERT INTO viu(viu) VALUES ('assign-vectors')", + id="assign-vectors-ivf", + marks=ivf_only, + ), ], ) def test_interrupt_is_reported_as_interrupt(interrupt_template, sql, mode): diff --git a/tests/test-fullscan.py b/tests/test-fullscan.py index d87194c0..f62f7e8a 100644 --- a/tests/test-fullscan.py +++ b/tests/test-fullscan.py @@ -8,6 +8,29 @@ def _f32(values): return struct.pack(f"{len(values)}f", *values) +def test_fullscan_reads_indexed_columns(db): + # Rescore and DiskANN columns keep their vectors outside _vector_chunks, so + # a scan reads them row by row, beside a flat column read by chunk. + db.execute( + "create virtual table v using vec0(" + "flat float[8], " + "r float[8] indexed by rescore(quantizer=bit), " + "d float[8] indexed by diskann(neighbor_quantizer=binary), " + "chunk_size=8)" + ) + for i in range(1, 21): + db.execute( + "insert into v(rowid, flat, r, d) values (?, ?, ?, ?)", + [i, _f32([i] * 8), _f32([-i] * 8), _f32([i, -i] * 4)], + ) + rows = db.execute("select rowid, flat, r, d from v").fetchall() + assert len(rows) == 20 + for rowid, flat, r, d in rows: + assert flat == _f32([rowid] * 8) + assert r == _f32([-rowid] * 8) + assert d == _f32([rowid, -rowid] * 4) + + def test_fullscan_reads_every_vector_across_chunks(db): # Two vector columns, so each keeps its own handle, over chunks with gaps. db.execute( @@ -54,10 +77,10 @@ def test_fullscan_reads_a_chunk_rewritten_during_the_scan(db): assert rows[rowid] == (rowid, 0) -def _scan_around(db, write): +def _scan_around(db, column, write): # Fetches one row of a full scan, runs `write` on the same connection, then # fetches the rest. - db.execute("create virtual table v using vec0(a float[2], chunk_size=8)") + db.execute(f"create virtual table v using vec0({column}, chunk_size=8)") for i in range(1, 17): db.execute("insert into v(rowid, a) values (?, ?)", [i, _f32([i, 0])]) db.execute("delete from v where rowid between 2 and 7") @@ -67,18 +90,31 @@ def _scan_around(db, write): return {r[0]: struct.unpack("2f", r[1]) for r in head + scan.fetchall()} -def test_fullscan_errors_on_a_row_deleted_during_the_scan(db): +# A flat column is read by chunk, a rescore or DiskANN column row by row, and a +# table whose only vector column is DiskANN keeps no chunks at all. +SCANNED_COLUMNS = [ + "a float[2]", + "a float[2] indexed by rescore(quantizer=int8)", + "a float[2] indexed by diskann(neighbor_quantizer=int8)", +] + + +@pytest.mark.parametrize("column", SCANNED_COLUMNS) +def test_fullscan_errors_on_a_row_deleted_during_the_scan(db, column): def write(): db.execute("delete from v where rowid = 12") db.execute("insert into v(rowid, a) values (99, ?)", [_f32([99, 99])]) # Row 99 takes row 12's slot; the scan must not return its vector as row 12. with pytest.raises(sqlite3.OperationalError, match="rowid 12"): - _scan_around(db, write) + _scan_around(db, column, write) -def test_fullscan_reads_rows_moved_by_optimize_during_the_scan(db): - rows = _scan_around(db, lambda: db.execute("insert into v(v) values ('optimize')")) +@pytest.mark.parametrize("column", SCANNED_COLUMNS) +def test_fullscan_reads_rows_moved_by_optimize_during_the_scan(db, column): + rows = _scan_around( + db, column, lambda: db.execute("insert into v(v) values ('optimize')") + ) assert rows == {i: (i, 0) for i in [1, *range(8, 17)]} diff --git a/tests/test-ivf-mutations.py b/tests/test-ivf-mutations.py new file mode 100644 index 00000000..9bb199fe --- /dev/null +++ b/tests/test-ivf-mutations.py @@ -0,0 +1,736 @@ +""" +Thorough IVF mutation tests: insert, delete, update, KNN correctness, +error cases, edge cases, and cell overflow scenarios. +""" + +import pytest +import sqlite3 +import struct +from collections import OrderedDict + + +def _f32(values): + return struct.pack("%sf" % len(values), *values) + + +def exec(db, sql, parameters=[]): + try: + rows = db.execute(sql, parameters).fetchall() + except (sqlite3.OperationalError, sqlite3.DatabaseError) as e: + return { + "error": e.__class__.__name__, + "message": str(e), + } + a = [] + for row in rows: + o = OrderedDict() + for k in row.keys(): + o[k] = row[k] + a.append(o) + result = OrderedDict() + result["sql"] = sql + result["rows"] = a + return result + + +def ivf_total_vectors(db, table="t", col=0): + """Count total vectors across all IVF cells.""" + return db.execute( + f"SELECT COALESCE(SUM(n_vectors), 0) FROM {table}_ivf_cells{col:02d}" + ).fetchone()[0] + + +def ivf_unassigned_count(db, table="t", col=0): + return db.execute( + f"SELECT COALESCE(SUM(n_vectors), 0) FROM {table}_ivf_cells{col:02d} WHERE centroid_id = -1" + ).fetchone()[0] + + +def ivf_assigned_count(db, table="t", col=0): + return db.execute( + f"SELECT COALESCE(SUM(n_vectors), 0) FROM {table}_ivf_cells{col:02d} WHERE centroid_id >= 0" + ).fetchone()[0] + + +def knn(db, query, k, table="t", col="v"): + """Run a KNN query and return list of (rowid, distance) tuples.""" + rows = db.execute( + f"SELECT rowid, distance FROM {table} WHERE {col} MATCH ? AND k = ?", + [_f32(query), k], + ).fetchall() + return [(r[0], r[1]) for r in rows] + + +# ============================================================================ +# Single row insert + KNN +# ============================================================================ + + +def test_insert_single_row_knn(db): + db.execute("CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf())") + db.execute("INSERT INTO t(rowid, v) VALUES (1, ?)", [_f32([1, 0, 0, 0])]) + results = knn(db, [1, 0, 0, 0], 5) + assert len(results) == 1 + assert results[0][0] == 1 + assert results[0][1] < 0.001 + + +# ============================================================================ +# Batch insert + KNN recall +# ============================================================================ + + +def test_batch_insert_knn_recall(db): + """Insert 200 vectors, train, verify KNN recall with nprobe=nlist.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=8, nprobe=8))" + ) + for i in range(200): + db.execute( + "INSERT INTO t(rowid, v) VALUES (?, ?)", + [i, _f32([float(i), 0, 0, 0])], + ) + assert ivf_total_vectors(db) == 200 + + db.execute("INSERT INTO t(t) VALUES ('compute-centroids')") + assert ivf_assigned_count(db) == 200 + + # Query near 100 -- closest should be rowid 100 + results = knn(db, [100.0, 0, 0, 0], 10) + assert len(results) == 10 + assert results[0][0] == 100 + assert results[0][1] < 0.01 + + # All results should be near 100 + rowids = {r[0] for r in results} + assert all(95 <= r <= 105 for r in rowids) + + +# ============================================================================ +# Delete rows, verify they're gone from KNN +# ============================================================================ + + +def test_delete_rows_gone_from_knn(db): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=2, nprobe=2))" + ) + for i in range(20): + db.execute( + "INSERT INTO t(rowid, v) VALUES (?, ?)", + [i, _f32([float(i), 0, 0, 0])], + ) + + db.execute("INSERT INTO t(t) VALUES ('compute-centroids')") + + # Delete rowid 10 + db.execute("DELETE FROM t WHERE rowid = 10") + + results = knn(db, [10.0, 0, 0, 0], 20) + rowids = {r[0] for r in results} + assert 10 not in rowids + + +def test_delete_all_rows_empty_results(db): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=2, nprobe=2))" + ) + for i in range(10): + db.execute( + "INSERT INTO t(rowid, v) VALUES (?, ?)", + [i, _f32([float(i), 0, 0, 0])], + ) + + db.execute("INSERT INTO t(t) VALUES ('compute-centroids')") + + for i in range(10): + db.execute("DELETE FROM t WHERE rowid = ?", [i]) + + assert ivf_total_vectors(db) == 0 + results = knn(db, [5.0, 0, 0, 0], 10) + assert len(results) == 0 + + +# ============================================================================ +# Insert after delete (reuse rowids) +# ============================================================================ + + +def test_insert_after_delete_reuse_rowid(db): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=2, nprobe=2))" + ) + for i in range(10): + db.execute( + "INSERT INTO t(rowid, v) VALUES (?, ?)", + [i, _f32([float(i), 0, 0, 0])], + ) + + db.execute("INSERT INTO t(t) VALUES ('compute-centroids')") + + # Delete rowid 5 + db.execute("DELETE FROM t WHERE rowid = 5") + + # Re-insert rowid 5 with a very different vector + db.execute("INSERT INTO t(rowid, v) VALUES (5, ?)", [_f32([999.0, 0, 0, 0])]) + + # KNN near 999 should find rowid 5 + results = knn(db, [999.0, 0, 0, 0], 1) + assert len(results) >= 1 + assert results[0][0] == 5 + + +# ============================================================================ +# Update vectors (INSERT OR REPLACE), verify KNN reflects new values +# ============================================================================ + + +def test_update_vector_via_delete_insert(db): + """vec0 IVF update: delete then re-insert with new vector.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=2, nprobe=2))" + ) + for i in range(10): + db.execute( + "INSERT INTO t(rowid, v) VALUES (?, ?)", + [i, _f32([float(i), 0, 0, 0])], + ) + + db.execute("INSERT INTO t(t) VALUES ('compute-centroids')") + + # "Update" rowid 3: delete and re-insert with new vector + db.execute("DELETE FROM t WHERE rowid = 3") + db.execute( + "INSERT INTO t(rowid, v) VALUES (3, ?)", + [_f32([100.0, 0, 0, 0])], + ) + + # KNN near 100 should find rowid 3 + results = knn(db, [100.0, 0, 0, 0], 1) + assert results[0][0] == 3 + + +# ============================================================================ +# Error cases: IVF + auxiliary/metadata/partition key columns +# ============================================================================ + + +def test_ivf_with_auxiliary_column(db): + """IVF should support auxiliary columns.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(), +extra text)" + ) + tables = [ + r[0] + for r in db.execute( + "SELECT name FROM sqlite_master WHERE name LIKE 't_%' ORDER BY 1" + ).fetchall() + ] + assert "t_auxiliary" in tables + + +def test_error_ivf_with_metadata_column(db): + result = exec( + db, + "CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(), genre text)", + ) + assert "error" in result + assert "metadata" in result.get("message", "").lower() + + +def test_error_ivf_with_partition_key(db): + result = exec( + db, + "CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(), user_id integer partition key)", + ) + assert "error" in result + assert "partition" in result.get("message", "").lower() + + +def test_flat_with_auxiliary_still_works(db): + """Regression guard: flat-indexed tables with aux columns should still work.""" + db.execute("CREATE VIRTUAL TABLE t USING vec0(v float[4], +extra text)") + db.execute( + "INSERT INTO t(rowid, v, extra) VALUES (1, ?, 'hello')", + [_f32([1, 0, 0, 0])], + ) + row = db.execute("SELECT extra FROM t WHERE rowid = 1").fetchone() + assert row[0] == "hello" + + +def test_flat_with_metadata_still_works(db): + """Regression guard: flat-indexed tables with metadata columns should still work.""" + db.execute("CREATE VIRTUAL TABLE t USING vec0(v float[4], genre text)") + db.execute( + "INSERT INTO t(rowid, v, genre) VALUES (1, ?, 'rock')", + [_f32([1, 0, 0, 0])], + ) + row = db.execute("SELECT genre FROM t WHERE rowid = 1").fetchone() + assert row[0] == "rock" + + +def test_flat_with_partition_key_still_works(db): + """Regression guard: flat-indexed tables with partition key should still work.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(v float[4], user_id integer partition key)" + ) + db.execute( + "INSERT INTO t(rowid, v, user_id) VALUES (1, ?, 42)", + [_f32([1, 0, 0, 0])], + ) + row = db.execute("SELECT user_id FROM t WHERE rowid = 1").fetchone() + assert row[0] == 42 + + +# ============================================================================ +# Edge cases +# ============================================================================ + + +def test_zero_vectors(db): + """Insert zero vectors, verify KNN still works.""" + db.execute("CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf())") + for i in range(5): + db.execute( + "INSERT INTO t(rowid, v) VALUES (?, ?)", + [i, _f32([0, 0, 0, 0])], + ) + results = knn(db, [0, 0, 0, 0], 5) + assert len(results) == 5 + # All distances should be 0 + for _, dist in results: + assert dist < 0.001 + + +def test_large_values(db): + """Insert vectors with very large and small values.""" + db.execute("CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf())") + db.execute("INSERT INTO t(rowid, v) VALUES (1, ?)", [_f32([1e6, 1e6, 1e6, 1e6])]) + db.execute( + "INSERT INTO t(rowid, v) VALUES (2, ?)", [_f32([1e-6, 1e-6, 1e-6, 1e-6])] + ) + db.execute( + "INSERT INTO t(rowid, v) VALUES (3, ?)", [_f32([-1e6, -1e6, -1e6, -1e6])] + ) + + results = knn(db, [1e6, 1e6, 1e6, 1e6], 3) + assert results[0][0] == 1 + + +def test_single_row_compute_centroids(db): + """Single row table, compute-centroids should still work.""" + db.execute("CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=1))") + db.execute("INSERT INTO t(rowid, v) VALUES (1, ?)", [_f32([1, 2, 3, 4])]) + db.execute("INSERT INTO t(t) VALUES ('compute-centroids')") + assert ivf_assigned_count(db) == 1 + + results = knn(db, [1, 2, 3, 4], 1) + assert len(results) == 1 + assert results[0][0] == 1 + + +# ============================================================================ +# Cell overflow (many vectors in one cell) +# ============================================================================ + + +def test_cell_overflow_many_vectors(db): + """Insert >64 vectors that all go to same centroid. Should create multiple cells.""" + db.execute("CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=0))") + # Insert 100 very similar vectors + for i in range(100): + db.execute( + "INSERT INTO t(rowid, v) VALUES (?, ?)", + [i, _f32([1.0 + i * 0.001, 0, 0, 0])], + ) + + # Set a single centroid so all vectors go there + db.execute( + "INSERT INTO t(t, v) VALUES ('set-centroid:0', ?)", + [_f32([1.0, 0, 0, 0])], + ) + db.execute("INSERT INTO t(t) VALUES ('assign-vectors')") + + assert ivf_assigned_count(db) == 100 + + # Should have more than 1 cell (64 max per cell) + cell_count = db.execute( + "SELECT count(*) FROM t_ivf_cells00 WHERE centroid_id = 0" + ).fetchone()[0] + assert cell_count >= 2 # 100 / 64 = 2 cells needed + + # All vectors should be queryable + results = knn(db, [1.0, 0, 0, 0], 100) + assert len(results) == 100 + + +# ============================================================================ +# Large batch with training +# ============================================================================ + + +def test_large_batch_with_training(db): + """Insert 500, train, insert 500 more, verify total is 1000.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=16, nprobe=16))" + ) + for i in range(500): + db.execute( + "INSERT INTO t(rowid, v) VALUES (?, ?)", + [i, _f32([float(i), 0, 0, 0])], + ) + + db.execute("INSERT INTO t(t) VALUES ('compute-centroids')") + + for i in range(500, 1000): + db.execute( + "INSERT INTO t(rowid, v) VALUES (?, ?)", + [i, _f32([float(i), 0, 0, 0])], + ) + + assert ivf_total_vectors(db) == 1000 + + # KNN should still work + results = knn(db, [750.0, 0, 0, 0], 5) + assert len(results) == 5 + assert results[0][0] == 750 + + +# ============================================================================ +# KNN after interleaved insert/delete +# ============================================================================ + + +def test_knn_after_interleaved_insert_delete(db): + """Insert 20, train, delete 10 closest to query, verify remaining.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=4, nprobe=4))" + ) + for i in range(20): + db.execute( + "INSERT INTO t(rowid, v) VALUES (?, ?)", + [i, _f32([float(i), 0, 0, 0])], + ) + + db.execute("INSERT INTO t(t) VALUES ('compute-centroids')") + + # Delete rowids 0-9 (closest to query at 5.0) + for i in range(10): + db.execute("DELETE FROM t WHERE rowid = ?", [i]) + + results = knn(db, [5.0, 0, 0, 0], 10) + rowids = {r[0] for r in results} + # None of the deleted rowids should appear + assert all(r >= 10 for r in rowids) + assert len(results) == 10 + + +def test_knn_empty_centroids_after_deletes(db): + """Some centroids may become empty after deletes. Should not crash.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=4, nprobe=2))" + ) + # Insert vectors clustered near 4 points + for i in range(40): + db.execute( + "INSERT INTO t(rowid, v) VALUES (?, ?)", + [i, _f32([float(i % 10) * 10, 0, 0, 0])], + ) + + db.execute("INSERT INTO t(t) VALUES ('compute-centroids')") + + # Delete a bunch, potentially emptying some centroids + for i in range(30): + db.execute("DELETE FROM t WHERE rowid = ?", [i]) + + # Should not crash even with empty centroids + results = knn(db, [50.0, 0, 0, 0], 20) + assert len(results) <= 10 # only 10 left + + +# ============================================================================ +# KNN returns correct distances +# ============================================================================ + + +def test_knn_correct_distances(db): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=2, nprobe=2))" + ) + db.execute("INSERT INTO t(rowid, v) VALUES (1, ?)", [_f32([0, 0, 0, 0])]) + db.execute("INSERT INTO t(rowid, v) VALUES (2, ?)", [_f32([3, 0, 0, 0])]) + db.execute("INSERT INTO t(rowid, v) VALUES (3, ?)", [_f32([0, 4, 0, 0])]) + + db.execute("INSERT INTO t(t) VALUES ('compute-centroids')") + + results = knn(db, [0, 0, 0, 0], 3) + result_map = {r[0]: r[1] for r in results} + + # L2 distances (sqrt of sum of squared differences) + assert abs(result_map[1] - 0.0) < 0.01 + assert abs(result_map[2] - 3.0) < 0.01 # sqrt(3^2) = 3 + assert abs(result_map[3] - 4.0) < 0.01 # sqrt(4^2) = 4 + + +# ============================================================================ +# Delete in flat mode leaves no orphan rowid_map entries +# ============================================================================ + + +def test_delete_flat_mode_rowid_map_count(db): + db.execute("CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=4))") + for i in range(5): + db.execute( + "INSERT INTO t(rowid, v) VALUES (?, ?)", + [i, _f32([float(i), 0, 0, 0])], + ) + + db.execute("DELETE FROM t WHERE rowid = 0") + db.execute("DELETE FROM t WHERE rowid = 2") + db.execute("DELETE FROM t WHERE rowid = 4") + + assert db.execute("SELECT count(*) FROM t_ivf_rowid_map00").fetchone()[0] == 2 + assert ivf_unassigned_count(db) == 2 + + +# ============================================================================ +# Duplicate rowid insert +# ============================================================================ + + +def test_delete_reinsert_as_update(db): + """Simulate update via delete + insert on same rowid.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=2, nprobe=2))" + ) + db.execute("INSERT INTO t(rowid, v) VALUES (1, ?)", [_f32([1, 0, 0, 0])]) + + # Delete then re-insert as "update" + db.execute("DELETE FROM t WHERE rowid = 1") + db.execute("INSERT INTO t(rowid, v) VALUES (1, ?)", [_f32([99, 0, 0, 0])]) + + results = knn(db, [99, 0, 0, 0], 1) + assert len(results) == 1 + assert results[0][0] == 1 + assert results[0][1] < 0.01 + + +def test_duplicate_rowid_insert_fails(db): + """Inserting a duplicate rowid should fail with a constraint error.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=2, nprobe=2))" + ) + db.execute("INSERT INTO t(rowid, v) VALUES (1, ?)", [_f32([1, 0, 0, 0])]) + + result = exec( + db, + "INSERT INTO t(rowid, v) VALUES (1, ?)", + [_f32([99, 0, 0, 0])], + ) + assert "error" in result + + +# ============================================================================ +# Interleaved insert/delete with KNN correctness +# ============================================================================ + + +def test_interleaved_ops_correctness(db): + """Complex sequence of inserts and deletes, verify KNN is always correct.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=4, nprobe=4))" + ) + + # Phase 1: Insert 50 vectors + for i in range(50): + db.execute( + "INSERT INTO t(rowid, v) VALUES (?, ?)", + [i, _f32([float(i), 0, 0, 0])], + ) + + db.execute("INSERT INTO t(t) VALUES ('compute-centroids')") + + # Phase 2: Delete even-numbered rowids + for i in range(0, 50, 2): + db.execute("DELETE FROM t WHERE rowid = ?", [i]) + + # Phase 3: Insert new vectors with higher rowids + for i in range(50, 75): + db.execute( + "INSERT INTO t(rowid, v) VALUES (?, ?)", + [i, _f32([float(i), 0, 0, 0])], + ) + + # Phase 4: Delete some of the new ones + for i in range(60, 70): + db.execute("DELETE FROM t WHERE rowid = ?", [i]) + + # KNN query: should only find existing vectors + results = knn(db, [25.0, 0, 0, 0], 50) + rowids = {r[0] for r in results} + + # Verify no deleted rowids appear + deleted = set(range(0, 50, 2)) | set(range(60, 70)) + assert len(rowids & deleted) == 0 + + # Verify we get the right count (25 odd + 15 new - 10 deleted new = 30) + expected_alive = set(range(1, 50, 2)) | set(range(50, 60)) | set(range(70, 75)) + assert rowids.issubset(expected_alive) + + +def test_ivf_update_vector_blocked(db): + """UPDATE on a vector column with IVF index should error (index would become stale).""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(emb float[4] indexed by ivf(nlist=2))" + ) + db.execute("INSERT INTO t(rowid, emb) VALUES (1, ?)", [_f32([1, 0, 0, 0])]) + db.execute("INSERT INTO t(rowid, emb) VALUES (2, ?)", [_f32([0, 1, 0, 0])]) + + with pytest.raises( + sqlite3.OperationalError, match="UPDATE on vector column.*not supported for IVF" + ): + db.execute("UPDATE t SET emb = ? WHERE rowid = 1", [_f32([0, 0, 1, 0])]) + + +# ============================================================================ +# Regression: slot allocation must reuse freed slots, not overwrite live ones +# ============================================================================ + + +def test_insert_after_delete_no_overwrite(db): + """Inserts after deletes must go into freed slots. Previously the insert + appended at slot=n_vectors, overwriting live vectors once a cell had + interior holes (data corruption + rows vanishing from KNN).""" + db.execute("CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=1))") + + # Fill exactly one cell to capacity (64) in untrained mode + for i in range(64): + db.execute( + "INSERT INTO t(rowid, v) VALUES (?, ?)", [i, _f32([float(i), 0, 0, 0])] + ) + # Free slots 0..9 + for i in range(10): + db.execute("DELETE FROM t WHERE rowid = ?", [i]) + # These must land in the freed slots + for i in range(100, 110): + db.execute( + "INSERT INTO t(rowid, v) VALUES (?, ?)", [i, _f32([float(i), 0, 0, 0])] + ) + + expected = set(range(10, 64)) | set(range(100, 110)) + rows = db.execute("SELECT rowid, vec_to_json(v) AS vj FROM t").fetchall() + assert set(r["rowid"] for r in rows) == expected + import json + + for r in rows: + vec = json.loads(r["vj"]) + assert vec[0] == pytest.approx( + float(r["rowid"]) + ), f"rowid {r['rowid']} vector corrupted: {vec}" + + # Every live row must be reachable via KNN + found = set(rowid for rowid, _ in knn(db, [50.0, 0, 0, 0], 100)) + assert found == expected + + # Bookkeeping: n_vectors must equal number of live rows + assert ivf_total_vectors(db) == len(expected) + + +def test_insert_delete_churn_consistency(db): + """Randomized insert/delete churn across multiple cells stays consistent.""" + import random + + rng = random.Random(42) + db.execute("CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=2))") + + alive = {} + next_rowid = 0 + for _ in range(600): + if alive and rng.random() < 0.4: + rid = rng.choice(list(alive)) + db.execute("DELETE FROM t WHERE rowid = ?", [rid]) + del alive[rid] + else: + vec = [rng.uniform(-1, 1) for _ in range(4)] + db.execute("INSERT INTO t(rowid, v) VALUES (?, ?)", [next_rowid, _f32(vec)]) + alive[next_rowid] = vec + next_rowid += 1 + + found = set(rowid for rowid, _ in knn(db, [0.0, 0, 0, 0], len(alive) + 10)) + assert found == set(alive) + assert ivf_total_vectors(db) == len(alive) + import json + + for rid, vec in alive.items(): + stored = json.loads( + db.execute( + "SELECT vec_to_json(v) FROM t WHERE rowid = ?", [rid] + ).fetchone()[0] + ) + assert stored == pytest.approx(vec, abs=1e-6) + + +# ============================================================================ +# Regression: assign-vectors centroid byte stride + non-contiguous ids +# ============================================================================ + + +def test_assign_vectors_correct_assignments(db): + """assign-vectors must place each vector in its nearest centroid's cell. + Previously the centroid array was indexed with a stride of D bytes instead + of D*sizeof(float), so all but centroid 0 were garbage.""" + db.execute("CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=4))") + + truth = {} + rowid = 0 + for c in range(4): + for i in range(5): + truth[rowid] = c + db.execute( + "INSERT INTO t(rowid, v) VALUES (?, ?)", + [rowid, _f32([c * 100.0 + i * 0.1] * 4)], + ) + rowid += 1 + + for c in range(4): + db.execute( + "INSERT INTO t(t, v) VALUES (?, ?)", + [f"set-centroid:{c}", _f32([c * 100.0] * 4)], + ) + db.execute("INSERT INTO t(t) VALUES ('assign-vectors')") + + for rid, expected_c in truth.items(): + cell_id, _ = db.execute( + "SELECT cell_id, slot FROM t_ivf_rowid_map00 WHERE rowid = ?", [rid] + ).fetchone() + (centroid_id,) = db.execute( + "SELECT centroid_id FROM t_ivf_cells00 WHERE rowid = ?", [cell_id] + ).fetchone() + assert centroid_id == expected_c, f"rowid {rid} assigned to {centroid_id}" + assert ivf_unassigned_count(db) == 0 + + +def test_assign_vectors_non_contiguous_centroid_ids(db): + """set-centroid allows arbitrary centroid ids; assign-vectors must map + to the actual ids, not array indexes.""" + db.execute("CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=2))") + + db.execute("INSERT INTO t(rowid, v) VALUES (1, ?)", [_f32([0.0, 0, 0, 0])]) + db.execute("INSERT INTO t(rowid, v) VALUES (2, ?)", [_f32([100.0] * 4)]) + + db.execute( + "INSERT INTO t(t, v) VALUES ('set-centroid:10', ?)", [_f32([0.0, 0, 0, 0])] + ) + db.execute("INSERT INTO t(t, v) VALUES ('set-centroid:20', ?)", [_f32([100.0] * 4)]) + db.execute("INSERT INTO t(t) VALUES ('assign-vectors')") + + used = set( + r[0] + for r in db.execute( + "SELECT DISTINCT centroid_id FROM t_ivf_cells00 WHERE n_vectors > 0" + ).fetchall() + ) + assert used == {10, 20} + # Both rows must remain findable + found = set(rowid for rowid, _ in knn(db, [0.0, 0, 0, 0], 10)) + assert found == {1, 2} diff --git a/tests/test-ivf-quantization.py b/tests/test-ivf-quantization.py new file mode 100644 index 00000000..e51f194e --- /dev/null +++ b/tests/test-ivf-quantization.py @@ -0,0 +1,385 @@ +import pytest +import sqlite3 +import struct +from collections import OrderedDict + + +def _f32(values): + return struct.pack("%sf" % len(values), *values) + + +def exec(db, sql, parameters=[]): + try: + rows = db.execute(sql, parameters).fetchall() + except (sqlite3.OperationalError, sqlite3.DatabaseError) as e: + return { + "error": e.__class__.__name__, + "message": str(e), + } + a = [] + for row in rows: + o = OrderedDict() + for k in row.keys(): + o[k] = row[k] + a.append(o) + result = OrderedDict() + result["sql"] = sql + result["rows"] = a + return result + + +# ============================================================================ +# Parser tests — quantizer and oversample options +# ============================================================================ + + +def test_ivf_quantizer_binary(db): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + "v float[768] indexed by ivf(nlist=64, quantizer=binary, oversample=10))" + ) + tables = [ + r[0] + for r in db.execute( + "SELECT name FROM sqlite_master WHERE type='table' ORDER BY 1" + ).fetchall() + ] + assert "t_ivf_centroids00" in tables + assert "t_ivf_cells00" in tables + + +def test_ivf_quantizer_int8(db): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + "v float[768] indexed by ivf(nlist=64, quantizer=int8))" + ) + tables = [ + r[0] + for r in db.execute( + "SELECT name FROM sqlite_master WHERE type='table' ORDER BY 1" + ).fetchall() + ] + assert "t_ivf_centroids00" in tables + + +def test_ivf_quantizer_none_explicit(db): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + "v float[768] indexed by ivf(quantizer=none))" + ) + # Should work — same as no quantizer + tables = [ + r[0] + for r in db.execute( + "SELECT name FROM sqlite_master WHERE type='table' ORDER BY 1" + ).fetchall() + ] + assert "t_ivf_centroids00" in tables + + +def test_ivf_quantizer_all_params(db): + """All params together: nlist, nprobe, quantizer, oversample.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + "v float[768] distance_metric=cosine " + "indexed by ivf(nlist=128, nprobe=16, quantizer=int8, oversample=4))" + ) + tables = [ + r[0] + for r in db.execute( + "SELECT name FROM sqlite_master WHERE type='table' ORDER BY 1" + ).fetchall() + ] + assert "t_ivf_centroids00" in tables + + +def test_ivf_error_oversample_without_quantizer(db): + """oversample > 1 without quantizer should error.""" + result = exec( + db, + "CREATE VIRTUAL TABLE t USING vec0(" + "v float[768] indexed by ivf(oversample=10))", + ) + assert "error" in result + + +def test_ivf_error_unknown_quantizer(db): + result = exec( + db, + "CREATE VIRTUAL TABLE t USING vec0(" + "v float[768] indexed by ivf(quantizer=pq))", + ) + assert "error" in result + + +def test_ivf_oversample_1_without_quantizer_ok(db): + """oversample=1 (default) is fine without quantizer.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" "v float[768] indexed by ivf(nlist=64))" + ) + # Should succeed — oversample defaults to 1 + + +# ============================================================================ +# Functional tests — insert, train, query with quantized IVF +# ============================================================================ + + +def test_ivf_int8_insert_and_query(db): + """int8 quantized IVF: insert, train, query.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + "v float[4] indexed by ivf(nlist=2, quantizer=int8, oversample=4))" + ) + for i in range(20): + db.execute("INSERT INTO t(rowid, v) VALUES (?, ?)", [i, _f32([i, 0, 0, 0])]) + + db.execute("INSERT INTO t(t) VALUES ('compute-centroids')") + + # Should be able to query + rows = db.execute( + "SELECT rowid, distance FROM t WHERE v MATCH ? AND k = 5", + [_f32([10.0, 0, 0, 0])], + ).fetchall() + assert len(rows) == 5 + # Top result should be close to 10 + assert rows[0][0] in range(8, 13) + + # Full vectors should be in _ivf_vectors table + fv_count = db.execute("SELECT count(*) FROM t_ivf_vectors00").fetchone()[0] + assert fv_count == 20 + + +def test_ivf_binary_insert_and_query(db): + """Binary quantized IVF: insert, train, query.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + "v float[8] indexed by ivf(nlist=2, quantizer=binary, oversample=4))" + ) + for i in range(20): + # Vectors with varying sign patterns + v = [(i * 0.1 - 1.0) + j * 0.3 for j in range(8)] + db.execute("INSERT INTO t(rowid, v) VALUES (?, ?)", [i, _f32(v)]) + + db.execute("INSERT INTO t(t) VALUES ('compute-centroids')") + + rows = db.execute( + "SELECT rowid FROM t WHERE v MATCH ? AND k = 5", + [_f32([0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5])], + ).fetchall() + assert len(rows) == 5 + + # Full vectors stored + fv_count = db.execute("SELECT count(*) FROM t_ivf_vectors00").fetchone()[0] + assert fv_count == 20 + + +def test_ivf_int8_cell_sizes_smaller(db): + """Cell blobs should be smaller with int8 quantization.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + "v float[768] indexed by ivf(nlist=2, quantizer=int8, oversample=1))" + ) + for i in range(10): + db.execute( + "INSERT INTO t(rowid, v) VALUES (?, ?)", + [i, _f32([float(x) / 768 for x in range(768)])], + ) + + # Cell vectors blob: 10 vectors at int8 = 10 * 768 = 7680 bytes + # vs float32 = 10 * 768 * 4 = 30720 bytes + # But cells have capacity 64, so blob = 64 * 768 = 49152 (int8) vs 64*768*4=196608 (float32) + blob_size = db.execute( + "SELECT length(vectors) FROM t_ivf_cells00 LIMIT 1" + ).fetchone()[0] + # int8: 64 slots * 768 bytes = 49152 + assert blob_size == 64 * 768 + + +def test_ivf_binary_cell_sizes_smaller(db): + """Cell blobs should be much smaller with binary quantization.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + "v float[768] indexed by ivf(nlist=2, quantizer=binary, oversample=1))" + ) + for i in range(10): + db.execute( + "INSERT INTO t(rowid, v) VALUES (?, ?)", + [i, _f32([float(x) / 768 for x in range(768)])], + ) + + blob_size = db.execute( + "SELECT length(vectors) FROM t_ivf_cells00 LIMIT 1" + ).fetchone()[0] + # binary: 64 slots * 768/8 bytes = 6144 + assert blob_size == 64 * (768 // 8) + + +def test_ivf_int8_oversample_improves_recall(db): + """Oversample re-ranking should improve recall over oversample=1.""" + # Create two tables: one with oversample=1, one with oversample=10 + db.execute( + "CREATE VIRTUAL TABLE t1 USING vec0(" + "v float[4] indexed by ivf(nlist=4, quantizer=int8, oversample=1))" + ) + db.execute( + "CREATE VIRTUAL TABLE t2 USING vec0(" + "v float[4] indexed by ivf(nlist=4, quantizer=int8, oversample=10))" + ) + for i in range(100): + v = _f32([i * 0.1, (i * 0.1) % 3, (i * 0.3) % 5, i * 0.01]) + db.execute("INSERT INTO t1(rowid, v) VALUES (?, ?)", [i, v]) + db.execute("INSERT INTO t2(rowid, v) VALUES (?, ?)", [i, v]) + + db.execute("INSERT INTO t1(t1) VALUES ('compute-centroids')") + db.execute("INSERT INTO t2(t2) VALUES ('compute-centroids')") + db.execute("INSERT INTO t1(t1) VALUES ('nprobe=4')") + db.execute("INSERT INTO t2(t2) VALUES ('nprobe=4')") + + query = _f32([5.0, 1.5, 2.5, 0.5]) + r1 = db.execute("SELECT rowid FROM t1 WHERE v MATCH ? AND k=10", [query]).fetchall() + r2 = db.execute("SELECT rowid FROM t2 WHERE v MATCH ? AND k=10", [query]).fetchall() + + # Both should return 10 results + assert len(r1) == 10 + assert len(r2) == 10 + # oversample=10 should have at least as good recall (same or better ordering) + + +def test_ivf_quantized_delete(db): + """Delete should remove from both cells and _ivf_vectors.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + "v float[4] indexed by ivf(nlist=2, quantizer=int8, oversample=1))" + ) + for i in range(10): + db.execute("INSERT INTO t(rowid, v) VALUES (?, ?)", [i, _f32([i, 0, 0, 0])]) + + db.execute("INSERT INTO t(t) VALUES ('compute-centroids')") + assert db.execute("SELECT count(*) FROM t_ivf_vectors00").fetchone()[0] == 10 + + db.execute("DELETE FROM t WHERE rowid = 5") + # _ivf_vectors should have 9 rows + assert db.execute("SELECT count(*) FROM t_ivf_vectors00").fetchone()[0] == 9 + + +def test_ivf_binary_rejects_non_multiple_of_8_dims(db): + """Binary quantizer requires dimensions divisible by 8.""" + with pytest.raises(sqlite3.OperationalError): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " v float[12] indexed by ivf(quantizer=binary)" + ")" + ) + + # Dimensions divisible by 8 should work + db.execute( + "CREATE VIRTUAL TABLE t2 USING vec0(" + " v float[16] indexed by ivf(quantizer=binary)" + ")" + ) + + +# ============================================================================ +# Regression: commands on quantized indexes must keep the quantized +# representation consistent (cells + centroids both store quantized bytes) +# ============================================================================ + + +def _self_match_count(db, vecs): + ok = 0 + for rid, v in vecs.items(): + got = db.execute( + "SELECT rowid FROM t WHERE v MATCH ? AND k = 1", [_f32(v)] + ).fetchone()[0] + ok += got == rid + return ok + + +def _random_unit_vecs(n, d, seed=1): + import random + + rng = random.Random(seed) + return {rid: [rng.uniform(-1, 1) for _ in range(d)] for rid in range(n)} + + +def test_ivf_int8_clear_centroids_preserves_vectors(db): + """clear-centroids must re-quantize full-precision vectors when moving + them back to unassigned cells. Previously it wrote raw float32 bytes into + int8/binary cells, corrupting every vector.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(v float[8] indexed by ivf(nlist=2, quantizer=int8))" + ) + vecs = _random_unit_vecs(20, 8) + for rid, v in vecs.items(): + db.execute("INSERT INTO t(rowid, v) VALUES (?, ?)", [rid, _f32(v)]) + db.execute("INSERT INTO t(t) VALUES ('compute-centroids')") + assert _self_match_count(db, vecs) == 20 + + db.execute("INSERT INTO t(t) VALUES ('clear-centroids')") + assert _self_match_count(db, vecs) == 20 + + +def test_ivf_binary_clear_centroids_preserves_vectors(db): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(v float[16] indexed by ivf(nlist=2, quantizer=binary))" + ) + vecs = _random_unit_vecs(20, 16, seed=7) + for rid, v in vecs.items(): + db.execute("INSERT INTO t(rowid, v) VALUES (?, ?)", [rid, _f32(v)]) + db.execute("INSERT INTO t(t) VALUES ('compute-centroids')") + + db.execute("INSERT INTO t(t) VALUES ('clear-centroids')") + # All rows must still be present and findable + found = set( + r[0] + for r in db.execute( + "SELECT rowid FROM t WHERE v MATCH ? AND k = 50", [_f32(vecs[0])] + ).fetchall() + ) + assert found == set(vecs) + + +def test_ivf_int8_set_centroid_and_insert(db): + """set-centroid on a quantized index must store the quantized + representation; otherwise subsequent inserts find no size-matching + centroid and fail.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(v float[8] indexed by ivf(nlist=2, quantizer=int8))" + ) + db.execute("INSERT INTO t(t, v) VALUES ('set-centroid:0', ?)", [_f32([0.5] * 8)]) + db.execute("INSERT INTO t(t, v) VALUES ('set-centroid:1', ?)", [_f32([-0.5] * 8)]) + + # Inserts after manual centroids must succeed and be assigned + db.execute("INSERT INTO t(rowid, v) VALUES (1, ?)", [_f32([0.4] * 8)]) + db.execute("INSERT INTO t(rowid, v) VALUES (2, ?)", [_f32([-0.4] * 8)]) + + found = set( + r[0] + for r in db.execute( + "SELECT rowid FROM t WHERE v MATCH ? AND k = 10", [_f32([0.4] * 8)] + ).fetchall() + ) + assert found == {1, 2} + + +def test_ivf_int8_assign_vectors(db): + """assign-vectors on a quantized index must interpret cell blobs with the + quantized element size, not float32.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(v float[8] indexed by ivf(nlist=2, quantizer=int8))" + ) + vecs = _random_unit_vecs(30, 8, seed=3) + for rid, v in vecs.items(): + db.execute("INSERT INTO t(rowid, v) VALUES (?, ?)", [rid, _f32(v)]) + + db.execute("INSERT INTO t(t, v) VALUES ('set-centroid:0', ?)", [_f32([0.5] * 8)]) + db.execute("INSERT INTO t(t, v) VALUES ('set-centroid:1', ?)", [_f32([-0.5] * 8)]) + db.execute("INSERT INTO t(t) VALUES ('assign-vectors')") + + # No vectors left unassigned, all findable, none corrupted + unassigned = db.execute( + "SELECT COALESCE(SUM(n_vectors),0) FROM t_ivf_cells00 WHERE centroid_id = -1" + ).fetchone()[0] + assert unassigned == 0 + assert _self_match_count(db, vecs) == 30 diff --git a/tests/test-ivf-rename.py b/tests/test-ivf-rename.py new file mode 100644 index 00000000..d8775294 --- /dev/null +++ b/tests/test-ivf-rename.py @@ -0,0 +1,105 @@ +import sqlite3 +import struct +import pytest + + +def _f32(values): + return struct.pack("%sf" % len(values), *values) + + +def _shadow_tables(db, prefix): + """Return sorted list of shadow table names for a given prefix.""" + return sorted( + [ + row[0] + for row in db.execute( + r"select name from sqlite_master where name like ? escape '\' and type='table' order by 1", + [f"{prefix}\\__%"], + ).fetchall() + ] + ) + + +def test_rename_ivf_no_quantizer(db): + """Rename should rename all IVF shadow tables (_ivf_centroids, _ivf_cells, + _ivf_rowid_map). quantizer=none — no _ivf_vectors table.""" + db.execute(""" + CREATE VIRTUAL TABLE v USING vec0( + a float[4] indexed by ivf(nlist=2, quantizer=none) + ) + """) + db.execute("insert into v(rowid, a) values (1, ?)", [_f32([0.1] * 4)]) + db.execute("insert into v(rowid, a) values (2, ?)", [_f32([0.9] * 4)]) + + before = _shadow_tables(db, "v") + assert "v_ivf_centroids00" in before + assert "v_ivf_cells00" in before + assert "v_ivf_rowid_map00" in before + assert "v_ivf_vectors00" not in before # quantizer=none -> no _ivf_vectors + assert "v_vector_chunks00" not in before + + db.execute("ALTER TABLE v RENAME TO v2") + + # Querying the renamed table should still work — it hits _ivf_cells, + # _ivf_centroids (when trained), and _ivf_rowid_map. + rows = db.execute( + "select rowid from v2 where a match ? and k=10", + [_f32([0.1] * 4)], + ).fetchall() + assert any(r[0] == 1 for r in rows) + + after = _shadow_tables(db, "v2") + assert "v2_ivf_centroids00" in after + assert "v2_ivf_cells00" in after + assert "v2_ivf_rowid_map00" in after + + # No old shadow tables should remain + assert _shadow_tables(db, "v") == [] + + +def test_rename_ivf_quantizer_binary(db): + """Rename should also rename _ivf_vectors when quantizer != none.""" + db.execute(""" + CREATE VIRTUAL TABLE v USING vec0( + a float[8] indexed by ivf(nlist=2, quantizer=binary) + ) + """) + db.execute("insert into v(rowid, a) values (1, ?)", [_f32([0.1] * 8)]) + + before = _shadow_tables(db, "v") + assert "v_ivf_centroids00" in before + assert "v_ivf_cells00" in before + assert "v_ivf_rowid_map00" in before + assert "v_ivf_vectors00" in before # quantizer=binary creates _ivf_vectors + + db.execute("ALTER TABLE v RENAME TO v2") + + rows = db.execute( + "select rowid from v2 where a match ? and k=10", + [_f32([0.1] * 8)], + ).fetchall() + assert rows[0][0] == 1 + + after = _shadow_tables(db, "v2") + assert "v2_ivf_centroids00" in after + assert "v2_ivf_cells00" in after + assert "v2_ivf_rowid_map00" in after + assert "v2_ivf_vectors00" in after + + assert _shadow_tables(db, "v") == [] + + +def test_rename_ivf_drop_after(db): + """DROP TABLE on a renamed IVF table must drop every shadow table — leftover + shadows from a half-renamed IVF index would orphan tables in the schema.""" + db.execute(""" + CREATE VIRTUAL TABLE v USING vec0( + a float[8] indexed by ivf(nlist=2, quantizer=binary) + ) + """) + db.execute("insert into v(rowid, a) values (1, ?)", [_f32([0.1] * 8)]) + db.execute("ALTER TABLE v RENAME TO v2") + db.execute("DROP TABLE v2") + + assert _shadow_tables(db, "v") == [] + assert _shadow_tables(db, "v2") == [] diff --git a/tests/test-ivf.py b/tests/test-ivf.py new file mode 100644 index 00000000..1926a02e --- /dev/null +++ b/tests/test-ivf.py @@ -0,0 +1,957 @@ +import pytest +import random +import sqlite3 +import struct +from collections import OrderedDict +from conftest import get_extension_path + + +def _f32(values): + return struct.pack("%sf" % len(values), *values) + + +def exec(db, sql, parameters=[]): + try: + rows = db.execute(sql, parameters).fetchall() + except (sqlite3.OperationalError, sqlite3.DatabaseError) as e: + return { + "error": e.__class__.__name__, + "message": str(e), + } + a = [] + for row in rows: + o = OrderedDict() + for k in row.keys(): + o[k] = row[k] + a.append(o) + result = OrderedDict() + result["sql"] = sql + result["rows"] = a + return result + + +def ivf_total_vectors(db, table="t", col=0): + """Count total vectors across all IVF cells.""" + return db.execute( + f"SELECT COALESCE(SUM(n_vectors), 0) FROM {table}_ivf_cells{col:02d}" + ).fetchone()[0] + + +def ivf_unassigned_count(db, table="t", col=0): + """Count vectors in unassigned cells (centroid_id=-1).""" + return db.execute( + f"SELECT COALESCE(SUM(n_vectors), 0) FROM {table}_ivf_cells{col:02d} WHERE centroid_id = -1" + ).fetchone()[0] + + +def ivf_assigned_count(db, table="t", col=0): + """Count vectors in trained cells (centroid_id >= 0).""" + return db.execute( + f"SELECT COALESCE(SUM(n_vectors), 0) FROM {table}_ivf_cells{col:02d} WHERE centroid_id >= 0" + ).fetchone()[0] + + +# ============================================================================ +# Parser tests +# ============================================================================ + + +def test_ivf_create_defaults(db): + """ivf() with no args uses defaults.""" + db.execute("CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf())") + tables = [ + r[0] + for r in db.execute( + "SELECT name FROM sqlite_master WHERE type='table' ORDER BY 1" + ).fetchall() + ] + assert "t_ivf_centroids00" in tables + assert "t_ivf_cells00" in tables + assert "t_ivf_rowid_map00" in tables + + +def test_ivf_create_custom_params(db): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=64, nprobe=8))" + ) + tables = [ + r[0] + for r in db.execute( + "SELECT name FROM sqlite_master WHERE type='table' ORDER BY 1" + ).fetchall() + ] + assert "t_ivf_centroids00" in tables + assert "t_ivf_cells00" in tables + + +def test_ivf_create_with_distance_metric(db): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(v float[4] distance_metric=cosine indexed by ivf(nlist=16))" + ) + tables = [ + r[0] + for r in db.execute( + "SELECT name FROM sqlite_master WHERE type='table' ORDER BY 1" + ).fetchall() + ] + assert "t_ivf_centroids00" in tables + + +def test_ivf_create_error_unknown_key(db): + result = exec( + db, + "CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(bogus=1))", + ) + assert "error" in result + + +def test_ivf_create_error_nprobe_gt_nlist(db): + result = exec( + db, + "CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=4, nprobe=10))", + ) + assert "error" in result + + +# ============================================================================ +# Shadow table tests +# ============================================================================ + + +def test_ivf_shadow_tables_created(db): + db.execute("CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=8))") + trained = db.execute( + "SELECT value FROM t_info WHERE key = 'ivf_trained_0'" + ).fetchone()[0] + assert str(trained) == "0" + + # No cells yet (created lazily on first insert) + count = db.execute("SELECT count(*) FROM t_ivf_cells00").fetchone()[0] + assert count == 0 + + +def test_ivf_drop_cleans_up(db): + db.execute("CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=4))") + db.execute("DROP TABLE t") + tables = [ + r[0] + for r in db.execute( + "SELECT name FROM sqlite_master WHERE type='table'" + ).fetchall() + ] + assert not any("ivf" in t for t in tables) + + +# ============================================================================ +# Insert tests (flat mode) +# ============================================================================ + + +def test_ivf_insert_flat_mode(db): + """Before training, vectors go to unassigned cell.""" + db.execute("CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=4))") + db.execute("INSERT INTO t(rowid, v) VALUES (1, ?)", [_f32([1, 2, 3, 4])]) + db.execute("INSERT INTO t(rowid, v) VALUES (2, ?)", [_f32([5, 6, 7, 8])]) + + assert ivf_unassigned_count(db) == 2 + assert ivf_assigned_count(db) == 0 + + # rowid_map should have 2 entries + assert db.execute("SELECT count(*) FROM t_ivf_rowid_map00").fetchone()[0] == 2 + + +def test_ivf_delete_flat_mode(db): + db.execute("CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=4))") + db.execute("INSERT INTO t(rowid, v) VALUES (1, ?)", [_f32([1, 2, 3, 4])]) + db.execute("INSERT INTO t(rowid, v) VALUES (2, ?)", [_f32([5, 6, 7, 8])]) + db.execute("DELETE FROM t WHERE rowid = 1") + + assert ivf_unassigned_count(db) == 1 + assert db.execute("SELECT count(*) FROM t_ivf_rowid_map00").fetchone()[0] == 1 + + +# ============================================================================ +# KNN flat mode tests +# ============================================================================ + + +def test_ivf_knn_flat_mode(db): + db.execute("CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=4))") + db.execute("INSERT INTO t(rowid, v) VALUES (1, ?)", [_f32([1, 0, 0, 0])]) + db.execute("INSERT INTO t(rowid, v) VALUES (2, ?)", [_f32([2, 0, 0, 0])]) + db.execute("INSERT INTO t(rowid, v) VALUES (3, ?)", [_f32([9, 0, 0, 0])]) + + rows = db.execute( + "SELECT rowid, distance FROM t WHERE v MATCH ? AND k = 2", + [_f32([1.5, 0, 0, 0])], + ).fetchall() + assert len(rows) == 2 + rowids = {r[0] for r in rows} + assert rowids == {1, 2} + + +def test_ivf_knn_flat_empty(db): + db.execute("CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=4))") + rows = db.execute( + "SELECT rowid FROM t WHERE v MATCH ? AND k = 5", + [_f32([1, 0, 0, 0])], + ).fetchall() + assert len(rows) == 0 + + +# ============================================================================ +# compute-centroids tests +# ============================================================================ + + +def test_compute_centroids(db): + db.execute("CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=4))") + for i in range(40): + db.execute( + "INSERT INTO t(rowid, v) VALUES (?, ?)", + [i, _f32([i % 10, i // 10, 0, 0])], + ) + + assert ivf_unassigned_count(db) == 40 + + db.execute("INSERT INTO t(t) VALUES ('compute-centroids')") + + # After training: unassigned cell should be gone (or empty), vectors in trained cells + assert ivf_unassigned_count(db) == 0 + assert ivf_assigned_count(db) == 40 + assert db.execute("SELECT count(*) FROM t_ivf_centroids00").fetchone()[0] == 4 + trained = db.execute( + "SELECT value FROM t_info WHERE key='ivf_trained_0'" + ).fetchone()[0] + assert str(trained) == "1" + + +def test_compute_centroids_recompute(db): + db.execute("CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=2))") + for i in range(20): + db.execute("INSERT INTO t(rowid, v) VALUES (?, ?)", [i, _f32([i, 0, 0, 0])]) + + db.execute("INSERT INTO t(t) VALUES ('compute-centroids')") + assert db.execute("SELECT count(*) FROM t_ivf_centroids00").fetchone()[0] == 2 + + db.execute("INSERT INTO t(t) VALUES ('compute-centroids')") + assert db.execute("SELECT count(*) FROM t_ivf_centroids00").fetchone()[0] == 2 + assert ivf_assigned_count(db) == 20 + + +# ============================================================================ +# Insert after training (assigned mode) +# ============================================================================ + + +def test_ivf_insert_after_training(db): + db.execute("CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=2))") + for i in range(20): + db.execute("INSERT INTO t(rowid, v) VALUES (?, ?)", [i, _f32([i, 0, 0, 0])]) + + db.execute("INSERT INTO t(t) VALUES ('compute-centroids')") + + db.execute("INSERT INTO t(rowid, v) VALUES (100, ?)", [_f32([5, 0, 0, 0])]) + + # Should be in a trained cell, not unassigned + row = db.execute( + "SELECT m.cell_id, c.centroid_id FROM t_ivf_rowid_map00 m " + "JOIN t_ivf_cells00 c ON c.rowid = m.cell_id " + "WHERE m.rowid = 100" + ).fetchone() + assert row is not None + assert row[1] >= 0 # centroid_id >= 0 means trained cell + + +# ============================================================================ +# KNN after training (IVF probe mode) +# ============================================================================ + + +def test_ivf_knn_after_training(db): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=4, nprobe=4))" + ) + for i in range(100): + db.execute("INSERT INTO t(rowid, v) VALUES (?, ?)", [i, _f32([i, 0, 0, 0])]) + + db.execute("INSERT INTO t(t) VALUES ('compute-centroids')") + + rows = db.execute( + "SELECT rowid, distance FROM t WHERE v MATCH ? AND k = 5", + [_f32([50.0, 0, 0, 0])], + ).fetchall() + assert len(rows) == 5 + assert rows[0][0] == 50 + assert rows[0][1] < 0.01 + + +def test_ivf_knn_k_larger_than_n(db): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=2, nprobe=2))" + ) + for i in range(5): + db.execute("INSERT INTO t(rowid, v) VALUES (?, ?)", [i, _f32([i, 0, 0, 0])]) + + db.execute("INSERT INTO t(t) VALUES ('compute-centroids')") + + rows = db.execute( + "SELECT rowid FROM t WHERE v MATCH ? AND k = 100", + [_f32([0, 0, 0, 0])], + ).fetchall() + assert len(rows) == 5 + + +# ============================================================================ +# Manual centroid import (set-centroid, assign-vectors) +# ============================================================================ + + +def test_set_centroid_and_assign(db): + db.execute("CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=0))") + for i in range(20): + db.execute("INSERT INTO t(rowid, v) VALUES (?, ?)", [i, _f32([i, 0, 0, 0])]) + + db.execute( + "INSERT INTO t(t, v) VALUES ('set-centroid:0', ?)", + [_f32([5, 0, 0, 0])], + ) + db.execute( + "INSERT INTO t(t, v) VALUES ('set-centroid:1', ?)", + [_f32([15, 0, 0, 0])], + ) + + assert db.execute("SELECT count(*) FROM t_ivf_centroids00").fetchone()[0] == 2 + + db.execute("INSERT INTO t(t) VALUES ('assign-vectors')") + + assert ivf_unassigned_count(db) == 0 + assert ivf_assigned_count(db) == 20 + + +# ============================================================================ +# clear-centroids +# ============================================================================ + + +def test_clear_centroids(db): + db.execute("CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=2))") + for i in range(20): + db.execute("INSERT INTO t(rowid, v) VALUES (?, ?)", [i, _f32([i, 0, 0, 0])]) + + db.execute("INSERT INTO t(t) VALUES ('compute-centroids')") + assert db.execute("SELECT count(*) FROM t_ivf_centroids00").fetchone()[0] == 2 + + db.execute("INSERT INTO t(t) VALUES ('clear-centroids')") + assert db.execute("SELECT count(*) FROM t_ivf_centroids00").fetchone()[0] == 0 + assert ivf_unassigned_count(db) == 20 + trained = db.execute( + "SELECT value FROM t_info WHERE key='ivf_trained_0'" + ).fetchone()[0] + assert str(trained) == "0" + + +# ============================================================================ +# Delete after training +# ============================================================================ + + +def test_ivf_delete_after_training(db): + db.execute("CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=2))") + for i in range(10): + db.execute("INSERT INTO t(rowid, v) VALUES (?, ?)", [i, _f32([i, 0, 0, 0])]) + + db.execute("INSERT INTO t(t) VALUES ('compute-centroids')") + assert ivf_assigned_count(db) == 10 + + db.execute("DELETE FROM t WHERE rowid = 5") + assert ivf_assigned_count(db) == 9 + assert db.execute("SELECT count(*) FROM t_ivf_rowid_map00").fetchone()[0] == 9 + + +# ============================================================================ +# Recall test +# ============================================================================ + + +def test_ivf_recall_nprobe_equals_nlist(db): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(v float[4] indexed by ivf(nlist=8, nprobe=8))" + ) + for i in range(100): + db.execute("INSERT INTO t(rowid, v) VALUES (?, ?)", [i, _f32([i, 0, 0, 0])]) + + db.execute("INSERT INTO t(t) VALUES ('compute-centroids')") + + rows = db.execute( + "SELECT rowid FROM t WHERE v MATCH ? AND k = 10", + [_f32([50.0, 0, 0, 0])], + ).fetchall() + rowids = {r[0] for r in rows} + + # 45 and 55 are equidistant from 50, so either may appear in top 10 + assert 50 in rowids + assert len(rowids) == 10 + assert all(45 <= r <= 55 for r in rowids) + + +# ============================================================================ +# Behavior this port defines where upstream's IVF differs +# ============================================================================ + + +def _random_rows(seed, n, dims): + rng = random.Random(seed) + return {i: [rng.uniform(-1, 1) for _ in range(dims)] for i in range(1, n + 1)} + + +def _connect(path): + db = sqlite3.connect(path) + db.enable_load_extension(True) + db.load_extension(get_extension_path()) + db.enable_load_extension(False) + return db + + +def _contents(db, table="t"): + """Every row of every table of the vec0 table, by table name.""" + names = [ + r[0] + for r in db.execute( + "SELECT name FROM sqlite_master WHERE type = 'table'" + " AND name LIKE ? ESCAPE '\\' ORDER BY name", + [f"{table}\\_%"], + ) + ] + return { + name: db.execute(f'SELECT * FROM "{name}" ORDER BY rowid').fetchall() + for name in names + } + + +# With nprobe = nlist every cell is read, and an oversample as large as the +# table ranks every row by its exact distance, so the KNN of an IVF column must +# answer as a flat column does, constraint for constraint, before and after +# compute-centroids. +@pytest.mark.parametrize( + "options", + [ + "nlist=4, nprobe=4", + "nlist=4, nprobe=4, quantizer=int8, oversample=100", + "nlist=4, nprobe=4, quantizer=binary, oversample=100", + ], +) +@pytest.mark.parametrize( + "where", + [ + "k = 10", + "k = 10 AND distance > 0.4", + "k = 10 AND distance <= 0.8", + "k = 10 AND distance >= 0.2 AND distance < 0.9", + "k = 5 AND rowid IN (3, 7, 11, 19, 23, 31, 999)", + "k = 3 AND rowid = 17", + "k = 6 AND mmr_lambda = 0.5", + "k = 6 AND mmr_lambda = 0.2 AND distance > 0.3", + ], +) +def test_ivf_knn_answers_as_flat_when_every_cell_is_read(db, options, where): + db.execute( + "CREATE VIRTUAL TABLE i USING vec0(" + f"emb float[16] distance_metric=cosine INDEXED BY ivf({options}))" + ) + db.execute( + "CREATE VIRTUAL TABLE f USING vec0(emb float[16] distance_metric=cosine)" + ) + rows = _random_rows(11, 60, 16) + for rowid, vec in rows.items(): + for table in ("i", "f"): + db.execute( + f"INSERT INTO {table}(rowid, emb) VALUES (?, ?)", [rowid, _f32(vec)] + ) + + def compare(): + rng = random.Random(12) + for _ in range(10): + query = _f32([rng.uniform(-1, 1) for _ in range(16)]) + sql = "SELECT rowid, distance FROM {} WHERE emb MATCH ? AND " + where + got = db.execute(sql.format("i"), [query]).fetchall() + want = db.execute(sql.format("f"), [query]).fetchall() + assert [tuple(r) for r in got] == [tuple(r) for r in want] + + compare() + db.execute("INSERT INTO i(i) VALUES ('compute-centroids')") + compare() + + +@pytest.mark.parametrize("quantizer", ["int8", "binary"]) +def test_ivf_quantized_column_returns_its_float_vectors(db, quantizer): + """The cells hold the vectors quantized; the column reads as float32.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + f"v float[8] INDEXED BY ivf(nlist=2, quantizer={quantizer}, oversample=2))" + ) + rows = _random_rows(5, 20, 8) + for rowid, vec in rows.items(): + db.execute("INSERT INTO t(rowid, v) VALUES (?, ?)", [rowid, _f32(vec)]) + for command in (None, "compute-centroids", "clear-centroids"): + if command: + db.execute("INSERT INTO t(t) VALUES (?)", [command]) + assert db.execute("SELECT v FROM t WHERE rowid = 7").fetchone()[0] == _f32( + rows[7] + ) + assert {r: v for r, v in db.execute("SELECT rowid, v FROM t")} == { + r: _f32(v) for r, v in rows.items() + } + + +def test_ivf_point_query_for_a_missing_rowid(db): + db.execute("CREATE VIRTUAL TABLE t USING vec0(v float[8] INDEXED BY ivf())") + db.execute("INSERT INTO t(rowid, v) VALUES (1, ?)", [_f32([1] * 8)]) + assert db.execute("SELECT rowid, v FROM t WHERE rowid = 2").fetchall() == [] + assert [tuple(r) for r in db.execute("SELECT rowid FROM t WHERE rowid = 1")] == [ + (1,) + ] + + +def test_ivf_another_connection_reads_the_centroids(tmp_path): + """Whether a column has centroids is read from the database, not kept by + the connection, so a connection sees the centroids another one makes.""" + path = str(tmp_path / "ivf.db") + a = _connect(path) + b = _connect(path) + a.execute( + "CREATE VIRTUAL TABLE t USING vec0(v float[8] INDEXED BY ivf(nlist=4, nprobe=1))" + ) + rows = _random_rows(9, 40, 8) + for rowid, vec in rows.items(): + a.execute("INSERT INTO t(rowid, v) VALUES (?, ?)", [rowid, _f32(vec)]) + a.commit() + sql = "SELECT rowid, distance FROM t WHERE v MATCH ? AND k = 5" + query = [_f32(rows[3])] + assert b.execute(sql, query).fetchall() == a.execute(sql, query).fetchall() + for command in ("compute-centroids", "clear-centroids", "compute-centroids"): + a.execute("INSERT INTO t(t) VALUES (?)", [command]) + a.commit() + got = b.execute(sql, query).fetchall() + assert got == a.execute(sql, query).fetchall() + assert got[0] == (3, 0.0) + + +def test_ivf_int8_quantizer_ranks_by_the_column_metric(db): + """By cosine, the first two rows are both at distance 0 from the query.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + "v float[8] distance_metric=cosine INDEXED BY ivf(nlist=2, quantizer=int8))" + ) + for rowid, vec in ( + (1, [0.9, 0.9, 0, 0, 0, 0, 0, 0]), + (2, [0.1, 0.1, 0, 0, 0, 0, 0, 0]), + (3, [0.5, -0.5, 0, 0, 0, 0, 0, 0]), + ): + db.execute("INSERT INTO t(rowid, v) VALUES (?, ?)", [rowid, _f32(vec)]) + rows = db.execute( + "SELECT rowid, distance FROM t WHERE v MATCH ? AND k = 2", + [_f32([0.2, 0.2, 0, 0, 0, 0, 0, 0])], + ).fetchall() + assert {r[0] for r in rows} == {1, 2} + assert all(r[1] == pytest.approx(0.0, abs=1e-6) for r in rows) + + +def test_ivf_rename_then_create_under_the_old_name(db): + db.execute("CREATE VIRTUAL TABLE t USING vec0(v float[8] INDEXED BY ivf(nlist=2))") + rows = _random_rows(4, 20, 8) + for rowid, vec in rows.items(): + db.execute("INSERT INTO t(rowid, v) VALUES (?, ?)", [rowid, _f32(vec)]) + db.execute("INSERT INTO t(t) VALUES ('compute-centroids')") + db.execute("ALTER TABLE t RENAME TO u") + db.execute("CREATE VIRTUAL TABLE t USING vec0(v float[8] INDEXED BY ivf(nlist=2))") + db.execute("INSERT INTO t(rowid, v) VALUES (1, ?)", [_f32(rows[2])]) + knn = "SELECT rowid FROM {} WHERE v MATCH ? AND k = 1" + assert db.execute(knn.format("u"), [_f32(rows[5])]).fetchone()[0] == 5 + assert db.execute(knn.format("t"), [_f32(rows[2])]).fetchone()[0] == 1 + indexed = sorted( + r[0] + for r in db.execute("SELECT tbl_name FROM sqlite_master WHERE type = 'index'") + if "ivf" in r[0] + ) + assert indexed == ["t_ivf_cells00", "u_ivf_cells00"] + + +@pytest.mark.parametrize( + "columns, message", + [ + ("v int8[8] INDEXED BY ivf()", "could not parse vector column"), + ("v bit[8] INDEXED BY ivf()", "could not parse vector column"), + ( + "v float[8] INDEXED BY ivf(), m integer", + "metadata columns are not supported with IVF indexes", + ), + ( + "p integer partition key, v float[8] INDEXED BY ivf()", + "partition key columns are not supported with IVF indexes", + ), + ], +) +def test_ivf_create_refuses(db, columns, message): + with pytest.raises(sqlite3.OperationalError, match=message): + db.execute(f"CREATE VIRTUAL TABLE t USING vec0({columns})") + + +@pytest.mark.parametrize( + "option", + [ + "quantizer=bin", + "nli=8", + "nlist=8x", + "nlist=99999999999", + "nlist=65537", + "nprobe=0", + "quantizer=int8, oversample=0", + "nlist=8,", + ], +) +def test_ivf_create_option_must_be_whole(db, option): + result = exec( + db, f"CREATE VIRTUAL TABLE t USING vec0(v float[8] INDEXED BY ivf({option}))" + ) + assert "error" in result + + +def test_ivf_with_auxiliary_and_text_primary_key(db): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + "id text primary key, v float[8] INDEXED BY ivf(nlist=2), +label text)" + ) + rows = _random_rows(6, 20, 8) + for rowid, vec in rows.items(): + db.execute( + "INSERT INTO t(id, v, label) VALUES (?, ?, ?)", + [f"id{rowid}", _f32(vec), f"label{rowid}"], + ) + db.execute("INSERT INTO t(t) VALUES ('compute-centroids')") + db.execute("DELETE FROM t WHERE id = 'id4'") + for rowid, vec in rows.items(): + got = db.execute( + "SELECT id, label, distance FROM t WHERE v MATCH ? AND k = 1", + [_f32(vec)], + ).fetchone() + if rowid == 4: + assert got[0] != "id4" + else: + assert tuple(got) == (f"id{rowid}", f"label{rowid}", 0.0) + + +@pytest.mark.parametrize( + "command", + ["compute-centroids", "clear-centroids", "assign-vectors"], +) +def test_ivf_interrupted_command_changes_nothing(db, command): + """A command stopped part way leaves every table as it was, and the + connection goes on answering queries.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + "v float[8] INDEXED BY ivf(nlist=4, quantizer=int8, oversample=4))" + ) + rows = _random_rows(8, 100, 8) + for rowid, vec in rows.items(): + db.execute("INSERT INTO t(rowid, v) VALUES (?, ?)", [rowid, _f32(vec)]) + if command == "clear-centroids": + db.execute("INSERT INTO t(t) VALUES ('compute-centroids')") + if command == "assign-vectors": + db.execute("INSERT INTO t(t, v) VALUES ('set-centroid:3', ?)", [_f32(rows[1])]) + db.execute("INSERT INTO t(t, v) VALUES ('set-centroid:8', ?)", [_f32(rows[2])]) + db.commit() + image = db.serialize() + + def fresh(): + c = sqlite3.connect(":memory:") + c.deserialize(image) + c.enable_load_extension(True) + c.load_extension(get_extension_path()) + c.enable_load_extension(False) + return c + + counted = fresh() + steps = 0 + + def count(): + nonlocal steps + steps += 1 + return 0 + + counted.set_progress_handler(count, 1) + counted.execute("INSERT INTO t(t) VALUES (?)", [command]) + counted.set_progress_handler(None, 1) + after = _contents(counted) + + before = _contents(fresh()) + assert after != before + # A progress handler also runs once the statement has halted, when SQLite + # reports the interrupt but has nothing left to roll back, so the points + # stop short of the last steps. + for n in [1 + steps * tenth // 10 for tenth in range(10)]: + c = fresh() + seen = 0 + + def stop(): + nonlocal seen + seen += 1 + return seen >= n + + c.set_progress_handler(stop, 1) + with pytest.raises(sqlite3.OperationalError, match="interrupted"): + c.execute("INSERT INTO t(t) VALUES (?)", [command]) + c.set_progress_handler(None, 1) + assert _contents(c) == before + got = c.execute( + "SELECT rowid, distance FROM t WHERE v MATCH ? AND k = 3", + [_f32(rows[50])], + ).fetchall() + assert got[0] == (50, 0.0) + + +def test_ivf_commands_apply_to_every_ivf_column(db): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + "a float[8] INDEXED BY ivf(nlist=2), " + "f float[8], " + "b float[8] INDEXED BY ivf(nlist=3, quantizer=int8))" + ) + rows = _random_rows(7, 30, 8) + for rowid, vec in rows.items(): + db.execute( + "INSERT INTO t(rowid, a, f, b) VALUES (?, ?, ?, ?)", + [rowid, _f32(vec), _f32(vec), _f32([-x for x in vec])], + ) + + def trained(): + return [ + (key, str(value)) + for key, value in db.execute( + "SELECT key, value FROM t_info WHERE key LIKE 'ivf_trained_%' " + "ORDER BY key" + ) + ] + + def centroids(col): + return db.execute(f"SELECT count(*) FROM t_ivf_centroids{col:02d}").fetchone()[ + 0 + ] + + def unassigned(col): + return ivf_unassigned_count(db, col=col) + + db.execute("INSERT INTO t(t) VALUES ('compute-centroids')") + assert trained() == [("ivf_trained_0", "1"), ("ivf_trained_2", "1")] + assert (centroids(0), centroids(2)) == (2, 3) + assert (unassigned(0), unassigned(2)) == (0, 0) + + db.execute("INSERT INTO t(t) VALUES ('clear-centroids')") + assert trained() == [("ivf_trained_0", "0"), ("ivf_trained_2", "0")] + assert (centroids(0), centroids(2)) == (0, 0) + assert (unassigned(0), unassigned(2)) == (30, 30) + + db.execute( + "INSERT INTO t(t, a, b) VALUES ('set-centroid:4', ?, ?)", + [_f32([0.5] * 8), _f32([-0.5] * 8)], + ) + assert trained() == [("ivf_trained_0", "1"), ("ivf_trained_2", "1")] + db.execute("INSERT INTO t(t) VALUES ('assign-vectors')") + assert (unassigned(0), unassigned(2)) == (0, 0) + for rowid, vec in rows.items(): + for column, value in (("a", vec), ("b", [-x for x in vec])): + got = db.execute( + f"SELECT rowid, distance FROM t WHERE {column} MATCH ? AND k = 1", + [_f32(value)], + ).fetchone() + assert tuple(got) == (rowid, 0.0) + + +@pytest.mark.parametrize( + "command, message", + [ + ("nprobe=0", "nprobe must be >= 1"), + ("nprobe=5x", "nprobe must be >= 1"), + ("nprobe= 5", "nprobe must be >= 1"), + ("nprobe=", "nprobe must be >= 1"), + ( + 'compute-centroids:{"nlist": 0}', + "option nlist must be an integer from 1 to 65536", + ), + ( + 'compute-centroids:{"nlist": 2.5}', + "option nlist must be an integer from 1 to 65536", + ), + ( + 'compute-centroids:{"nlist": "2"}', + "option nlist must be an integer from 1 to 65536", + ), + ( + 'compute-centroids:{"seed": -1}', + "option seed must be an integer from 0 to 4294967295", + ), + ('compute-centroids:{"nlists": 2}', "unknown compute-centroids option: nlists"), + ("compute-centroids:[2]", "options must be a JSON object"), + ("compute-centroids:[]", "options must be a JSON object"), + ("compute-centroids:2", "options must be a JSON object"), + ("compute-centroids:{nlist: 2", "compute-centroids options: malformed JSON"), + ("compute-centroids:", "compute-centroids options: malformed JSON"), + ("set-centroid:-1", "set-centroid needs a centroid id from 0 to 2147483647"), + ("set-centroid:x", "set-centroid needs a centroid id from 0 to 2147483647"), + ("set-centroid:1", "set-centroid needs a vector for an IVF-indexed column"), + ("assign-vectors", "vector column 0 has no IVF centroids"), + ("compute-centroid", "unknown vec0 command"), + ("compute-centroids2", "unknown vec0 command"), + ], +) +def test_ivf_command_errors(db, command, message): + db.execute("CREATE VIRTUAL TABLE t USING vec0(v float[8] INDEXED BY ivf(nlist=2))") + db.execute("INSERT INTO t(rowid, v) VALUES (1, ?)", [_f32([0.5] * 8)]) + with pytest.raises(sqlite3.OperationalError, match=message): + db.execute("INSERT INTO t(t) VALUES (?)", [command]) + + +def test_ivf_compute_centroids_options(db): + db.execute("CREATE VIRTUAL TABLE t USING vec0(v float[8] INDEXED BY ivf(nlist=0))") + with pytest.raises( + sqlite3.OperationalError, + match="compute-centroids needs at least one row in vector column 0", + ): + db.execute("""INSERT INTO t(t) VALUES ('compute-centroids:{"nlist": 2}')""") + rows = _random_rows(10, 30, 8) + for rowid, vec in rows.items(): + db.execute("INSERT INTO t(rowid, v) VALUES (?, ?)", [rowid, _f32(vec)]) + with pytest.raises( + sqlite3.OperationalError, + match="compute-centroids needs nlist for vector column 0, which was " + "created with nlist=0", + ): + db.execute("INSERT INTO t(t) VALUES ('compute-centroids')") + + def centroids(table="t"): + return [ + tuple(r) + for r in db.execute( + f"SELECT centroid_id, centroid FROM {table}_ivf_centroids00 ORDER BY 1" + ) + ] + + # the same rows, read in the same order, and the same seed + db.execute("CREATE VIRTUAL TABLE u USING vec0(v float[8] INDEXED BY ivf(nlist=0))") + for rowid, vec in rows.items(): + db.execute("INSERT INTO u(rowid, v) VALUES (?, ?)", [rowid, _f32(vec)]) + for table in ("t", "u"): + db.execute( + f"INSERT INTO {table}({table}) VALUES " + """('compute-centroids:{"nlist": 3, "seed": 5}')""" + ) + assert len(centroids("t")) == 3 + assert centroids("t") == centroids("u") + + # without iterations, the centroids are the rows k-means++ picks + db.execute( + "INSERT INTO t(t) VALUES " + """('compute-centroids:{"nlist": 4, "max_iterations": 0, "seed": 9}')""" + ) + vectors = {_f32(vec) for vec in rows.values()} + assert len(centroids()) == 4 + assert all(c in vectors for _, c in centroids()) + + # at most one centroid per row + db.execute("""INSERT INTO t(t) VALUES ('compute-centroids:{"nlist": 64}')""") + assert len(centroids()) == 30 + assert ivf_assigned_count(db) == 30 + + +def test_ivf_corrupt_cell_errors(db): + db.execute("CREATE VIRTUAL TABLE t USING vec0(v float[8] INDEXED BY ivf(nlist=2))") + rows = _random_rows(2, 10, 8) + for rowid, vec in rows.items(): + db.execute("INSERT INTO t(rowid, v) VALUES (?, ?)", [rowid, _f32(vec)]) + db.execute("UPDATE t_ivf_rowid_map00 SET slot = 99 WHERE rowid = 3") + with pytest.raises( + sqlite3.OperationalError, + match="Could not fetch vector data for 3 from the IVF index", + ): + db.execute("SELECT v FROM t WHERE rowid = 3").fetchall() + db.execute("UPDATE t_ivf_cells00 SET vectors = x'00'") + with pytest.raises(sqlite3.OperationalError, match="cell 1 .* is corrupt"): + db.execute( + "SELECT rowid FROM t WHERE v MATCH ? AND k = 3", [_f32(rows[1])] + ).fetchall() + with pytest.raises(sqlite3.OperationalError, match="cell 1 .* is corrupt"): + db.execute("INSERT INTO t(t) VALUES ('compute-centroids')") + + +@pytest.mark.parametrize("options", ["nlist=2", "nlist=2, quantizer=int8"]) +def test_ivf_insert_or_replace(db, options): + db.execute( + f"CREATE VIRTUAL TABLE t USING vec0(v float[4] INDEXED BY ivf({options}), " + "+a text)" + ) + db.execute("INSERT INTO t(rowid, v, a) VALUES (1, ?, 'x')", [_f32([1, 0, 0, 0])]) + db.execute("INSERT INTO t(rowid, v, a) VALUES (2, ?, 'y')", [_f32([0, 1, 0, 0])]) + db.execute("INSERT INTO t(t) VALUES ('compute-centroids')") + db.execute( + "INSERT OR REPLACE INTO t(rowid, v, a) VALUES (1, ?, 'z')", + [_f32([0, 0, 1, 0])], + ) + rows = db.execute("SELECT rowid, v, a FROM t ORDER BY rowid") + assert [tuple(r) for r in rows] == [ + (1, _f32([0, 0, 1, 0]), "z"), + (2, _f32([0, 1, 0, 0]), "y"), + ] + got = db.execute( + "SELECT rowid, distance FROM t WHERE v MATCH ? AND k = 2", [_f32([0, 0, 1, 0])] + ).fetchall() + assert [tuple(r) for r in got] == [(1, 0.0), (2, pytest.approx(2**0.5))] + assert ivf_total_vectors(db) == 2 + + +def test_ivf_vacuum_after_knn(db): + """A KNN query leaves no statement in progress, so VACUUM works after it. + https://github.com/asg017/sqlite-vec/issues/295""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + "v float[8] INDEXED BY ivf(nlist=2, quantizer=binary, oversample=2))" + ) + for i in range(1, 11): + db.execute("INSERT INTO t(rowid, v) VALUES (?, ?)", [i, _f32([0.1 * i] * 8)]) + db.execute("INSERT INTO t(t) VALUES ('compute-centroids')") + db.commit() + rows = db.execute( + "SELECT rowid FROM t WHERE v MATCH ? AND k = 3", [_f32([0.5] * 8)] + ).fetchall() + assert len(rows) == 3 + db.execute("VACUUM") + + +def test_ivf_optimize_beside_a_flat_column(db): + """optimize moves a flat column's vectors and leaves an IVF column's.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + f float[8], + i float[8] INDEXED BY ivf(nlist=3, quantizer=int8, oversample=8), + chunk_size=8 + ) + """) + rows = _random_rows(13, 40, 8) + for rowid, vec in rows.items(): + db.execute( + "INSERT INTO t(rowid, f, i) VALUES (?, ?, ?)", + [rowid, _f32(vec), _f32([-x for x in vec])], + ) + db.execute("INSERT INTO t(t) VALUES ('compute-centroids')") + for rowid in range(1, 41, 3): + db.execute("DELETE FROM t WHERE rowid = ?", [rowid]) + del rows[rowid] + db.execute("INSERT INTO t(t) VALUES ('optimize')") + for rowid, f, i in db.execute("SELECT rowid, f, i FROM t").fetchall(): + assert f == _f32(rows[rowid]) + assert i == _f32([-x for x in rows[rowid]]) + for rowid, vec in rows.items(): + for column, value in (("f", vec), ("i", [-x for x in vec])): + got = db.execute( + f"SELECT rowid FROM t WHERE {column} MATCH ? AND k = 1", [_f32(value)] + ).fetchone()[0] + assert got == rowid diff --git a/tests/test-loadable.py b/tests/test-loadable.py index 1e351bea..ab1e2075 100644 --- a/tests/test-loadable.py +++ b/tests/test-loadable.py @@ -2984,6 +2984,58 @@ def test_vec0_constructor(): db.execute("create virtual table v using vec0(4)") +def test_vec0_indexed_by_flat(): + db.execute("drop table if exists t_ibf") + db.execute("drop table if exists t_ibf2") + db.execute("drop table if exists t_ibf3") + db.execute("drop table if exists t_ibf4") + + # indexed by flat() should succeed and behave identically to no index clause + db.execute("create virtual table t_ibf using vec0(emb float[4] indexed by flat())") + db.execute( + "insert into t_ibf(rowid, emb) values (1, X'00000000000000000000000000000000')" + ) + rows = db.execute( + "select rowid from t_ibf where emb match X'00000000000000000000000000000000' and k = 1" + ).fetchall() + assert len(rows) == 1 + assert rows[0][0] == 1 + db.execute("drop table t_ibf") + + # indexed by flat() with distance_metric + db.execute( + "create virtual table t_ibf2 using vec0(emb float[4] distance_metric=cosine indexed by flat())" + ) + db.execute("drop table t_ibf2") + + # indexed by flat() on int8 + db.execute("create virtual table t_ibf3 using vec0(emb int8[4] indexed by flat())") + db.execute("drop table t_ibf3") + + # indexed by flat() on bit + db.execute("create virtual table t_ibf4 using vec0(emb bit[8] indexed by flat())") + db.execute("drop table t_ibf4") + + # Error: unknown index type + with _raises( + "vec0 constructor error: could not parse vector column 'emb float[4] indexed by unknown()'", + sqlite3.DatabaseError, + ): + db.execute( + "create virtual table v using vec0(emb float[4] indexed by unknown())" + ) + + # Error: indexed by (missing type) + with _raises( + "vec0 constructor error: could not parse vector column 'emb float[4] indexed by'", + sqlite3.DatabaseError, + ): + db.execute("create virtual table v using vec0(emb float[4] indexed by)") + + if db.in_transaction: + db.rollback() + + def test_vec0_create_errors(): # EVIDENCE-OF: V17740_01811 vec0 create _chunks error handling db.set_authorizer(authorizer_deny_on(sqlite3.SQLITE_CREATE_TABLE, "t1_chunks")) diff --git a/tests/test-rename.py b/tests/test-rename.py index ab6732e3..8b86ee69 100644 --- a/tests/test-rename.py +++ b/tests/test-rename.py @@ -1,7 +1,7 @@ """ALTER TABLE ... RENAME on vec0 tables. Ported from asg017/sqlite-vec@c560710 and @8105eee, without the tests for -upstream's ANN indexes, which this fork does not have. +upstream's DiskANN and IVF indexes, which this fork does not have. """ import sqlite3 @@ -174,6 +174,81 @@ def test_rename_with_metadata(db): assert _shadow_tables(db, "v") == [] +def test_rename_diskann(db): + """Rename should work on DiskANN-indexed tables (no _vector_chunks shadow).""" + db.execute(""" + CREATE VIRTUAL TABLE v USING vec0( + a float[8] INDEXED BY diskann(neighbor_quantizer=binary) + ) + """) + db.execute("insert into v(rowid, a) values (1, ?)", [_f32([0.1] * 8)]) + + # DiskANN columns use _vectors / _diskann_nodes / _diskann_buffer instead + # of _vector_chunks; the rename must skip the missing _vector_chunks ALTER + # and rename all three DiskANN shadow tables. + before = _shadow_tables(db, "v") + assert "v_vectors00" in before + assert "v_diskann_nodes00" in before + assert "v_diskann_buffer00" in before + assert "v_vector_chunks00" not in before + + db.execute("ALTER TABLE v RENAME TO v2") + + rows = db.execute( + "select rowid from v2 where a match ? and k=10", + [_f32([0.1] * 8)], + ).fetchall() + assert rows[0][0] == 1 + + # the graph takes a new node under the new name + db.execute("insert into v2(rowid, a) values (2, ?)", [_f32([0.9] * 8)]) + rows = db.execute( + "select rowid from v2 where a match ? and k=1", + [_f32([0.9] * 8)], + ).fetchall() + assert rows[0][0] == 2 + + after = _shadow_tables(db, "v2") + assert "v2_vectors00" in after + assert "v2_diskann_nodes00" in after + assert "v2_diskann_buffer00" in after + assert "v2_vector_chunks00" not in after + assert _shadow_tables(db, "v") == [] + + +def test_rename_rescore(db): + """Rename should work on rescore-indexed tables (no _vector_chunks shadow).""" + db.execute(""" + CREATE VIRTUAL TABLE v USING vec0( + a float[8] indexed by rescore(quantizer=bit) + ) + """) + db.execute("insert into v(rowid, a) values (1, ?)", [_f32([0.1] * 8)]) + db.execute("insert into v(rowid, a) values (2, ?)", [_f32([0.9] * 8)]) + + # Rescore columns use _rescore_chunks / _rescore_vectors instead of + # _vector_chunks; the rename must skip the missing _vector_chunks ALTER + # and rename both rescore shadow tables. + before = _shadow_tables(db, "v") + assert "v_rescore_chunks00" in before + assert "v_rescore_vectors00" in before + assert "v_vector_chunks00" not in before + + db.execute("ALTER TABLE v RENAME TO v2") + + rows = db.execute( + "select rowid from v2 where a match ? and k=10", + [_f32([0.1] * 8)], + ).fetchall() + assert rows[0][0] == 1 + + after = _shadow_tables(db, "v2") + assert "v2_rescore_chunks00" in after + assert "v2_rescore_vectors00" in after + assert "v2_vector_chunks00" not in after + assert _shadow_tables(db, "v") == [] + + def test_rename_drop_after(db): """DROP TABLE should work on a renamed table.""" db.execute("create virtual table v using vec0(a float[2], chunk_size=8)") diff --git a/tests/test-rescore-mutations.py b/tests/test-rescore-mutations.py new file mode 100644 index 00000000..349a9b21 --- /dev/null +++ b/tests/test-rescore-mutations.py @@ -0,0 +1,563 @@ +"""Mutation and edge-case tests for the rescore index feature.""" + +import struct +import sqlite3 +import pytest +import math +import random + + +def float_vec(values): + """Pack a list of floats into a blob for sqlite-vec.""" + return struct.pack(f"{len(values)}f", *values) + + +def unpack_float_vec(blob): + """Unpack a float vector blob.""" + n = len(blob) // 4 + return list(struct.unpack(f"{n}f", blob)) + + +# ============================================================================ +# Error cases: rescore + aux/metadata/partition +# ============================================================================ + + +def test_create_with_aux_column(db): + """Rescore should support auxiliary columns.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[128] indexed by rescore(quantizer=bit)," + " +extra text" + ")" + ) + tables = [ + r[0] + for r in db.execute( + "SELECT name FROM sqlite_master WHERE name LIKE 't_%' ORDER BY 1" + ).fetchall() + ] + assert "t_auxiliary" in tables + + +def test_create_error_with_metadata_column(db): + """Rescore should reject metadata columns.""" + with pytest.raises(sqlite3.OperationalError, match="Metadata columns"): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] indexed by rescore(quantizer=bit)," + " genre text" + ")" + ) + + +def test_create_error_with_partition_key(db): + """Rescore should reject partition key columns.""" + with pytest.raises(sqlite3.OperationalError, match="Partition key"): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] indexed by rescore(quantizer=bit)," + " user_id integer partition key" + ")" + ) + + +# ============================================================================ +# Insert / batch / delete / update mutations +# ============================================================================ + + +def test_insert_single_verify_knn(db): + """Insert a single row and verify KNN returns it.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] indexed by rescore(quantizer=bit)" + ")" + ) + db.execute("INSERT INTO t(rowid, embedding) VALUES (1, ?)", [float_vec([1.0] * 8)]) + rows = db.execute( + "SELECT rowid, distance FROM t WHERE embedding MATCH ? ORDER BY distance LIMIT 1", + [float_vec([1.0] * 8)], + ).fetchall() + assert len(rows) == 1 + assert rows[0]["rowid"] == 1 + assert rows[0]["distance"] < 0.01 + + +def test_insert_large_batch(db): + """Insert 200+ rows (multiple chunks with default chunk_size=1024) and verify count and KNN.""" + dim = 16 + n = 200 + random.seed(99) + db.execute( + f"CREATE VIRTUAL TABLE t USING vec0(" + f" embedding float[{dim}] indexed by rescore(quantizer=int8)" + f")" + ) + for i in range(n): + v = [random.gauss(0, 1) for _ in range(dim)] + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (?, ?)", + [i + 1, float_vec(v)], + ) + row = db.execute("SELECT count(*) as cnt FROM t").fetchone() + assert row["cnt"] == n + + # KNN should return results + query = float_vec([random.gauss(0, 1) for _ in range(dim)]) + rows = db.execute( + "SELECT rowid FROM t WHERE embedding MATCH ? ORDER BY distance LIMIT 10", + [query], + ).fetchall() + assert len(rows) == 10 + + +def test_delete_all_rows(db): + """Delete every row, verify count=0, KNN returns empty.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] indexed by rescore(quantizer=bit)" + ")" + ) + for i in range(20): + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (?, ?)", + [i + 1, float_vec([float(i)] * 8)], + ) + assert db.execute("SELECT count(*) as cnt FROM t").fetchone()["cnt"] == 20 + + for i in range(20): + db.execute("DELETE FROM t WHERE rowid = ?", [i + 1]) + + assert db.execute("SELECT count(*) as cnt FROM t").fetchone()["cnt"] == 0 + + rows = db.execute( + "SELECT rowid FROM t WHERE embedding MATCH ? ORDER BY distance LIMIT 5", + [float_vec([0.0] * 8)], + ).fetchall() + assert len(rows) == 0 + + +def test_delete_then_reinsert_same_rowid(db): + """Delete rowid=1, re-insert rowid=1 with different vector, verify KNN uses new vector.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] indexed by rescore(quantizer=int8)" + ")" + ) + # Insert rowid=1 near origin, rowid=2 far from origin + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (1, ?)", + [float_vec([0.1] * 8)], + ) + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (2, ?)", + [float_vec([100.0] * 8)], + ) + + # KNN to [0]*8 -> rowid 1 is closer + rows = db.execute( + "SELECT rowid FROM t WHERE embedding MATCH ? ORDER BY distance LIMIT 1", + [float_vec([0.0] * 8)], + ).fetchall() + assert rows[0]["rowid"] == 1 + + # Delete rowid=1, re-insert with vector far from origin + db.execute("DELETE FROM t WHERE rowid = 1") + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (1, ?)", + [float_vec([200.0] * 8)], + ) + + # Now KNN to [0]*8 -> rowid 2 should be closer + rows = db.execute( + "SELECT rowid FROM t WHERE embedding MATCH ? ORDER BY distance LIMIT 1", + [float_vec([0.0] * 8)], + ).fetchall() + assert rows[0]["rowid"] == 2 + + +def test_update_vector(db): + """UPDATE the vector column and verify KNN reflects new value.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] indexed by rescore(quantizer=int8)" + ")" + ) + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (1, ?)", + [float_vec([0.0] * 8)], + ) + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (2, ?)", + [float_vec([10.0] * 8)], + ) + + # Update rowid=1 to be far away + db.execute( + "UPDATE t SET embedding = ? WHERE rowid = 1", + [float_vec([100.0] * 8)], + ) + + # Now KNN to [0]*8 -> rowid 2 should be closest + rows = db.execute( + "SELECT rowid FROM t WHERE embedding MATCH ? ORDER BY distance LIMIT 1", + [float_vec([0.0] * 8)], + ).fetchall() + assert rows[0]["rowid"] == 2 + + +def test_knn_after_delete_all_but_one(db): + """Insert 50 rows, delete 49, KNN should only return the survivor.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] indexed by rescore(quantizer=bit)" + ")" + ) + for i in range(50): + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (?, ?)", + [i + 1, float_vec([float(i)] * 8)], + ) + # Delete all except rowid=25 + for i in range(50): + if i + 1 != 25: + db.execute("DELETE FROM t WHERE rowid = ?", [i + 1]) + + assert db.execute("SELECT count(*) as cnt FROM t").fetchone()["cnt"] == 1 + + rows = db.execute( + "SELECT rowid FROM t WHERE embedding MATCH ? ORDER BY distance LIMIT 10", + [float_vec([0.0] * 8)], + ).fetchall() + assert len(rows) == 1 + assert rows[0]["rowid"] == 25 + + +# ============================================================================ +# Edge cases +# ============================================================================ + + +def test_single_row_knn(db): + """Table with exactly 1 row. LIMIT 1 returns it; LIMIT 5 returns 1.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] indexed by rescore(quantizer=bit)" + ")" + ) + db.execute("INSERT INTO t(rowid, embedding) VALUES (1, ?)", [float_vec([1.0] * 8)]) + + rows = db.execute( + "SELECT rowid FROM t WHERE embedding MATCH ? ORDER BY distance LIMIT 1", + [float_vec([1.0] * 8)], + ).fetchall() + assert len(rows) == 1 + + rows = db.execute( + "SELECT rowid FROM t WHERE embedding MATCH ? ORDER BY distance LIMIT 5", + [float_vec([1.0] * 8)], + ).fetchall() + assert len(rows) == 1 + + +def test_knn_with_all_identical_vectors(db): + """All vectors are the same. All distances should be equal.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] indexed by rescore(quantizer=int8)" + ")" + ) + vec = [3.0, 1.0, 4.0, 1.0, 5.0, 9.0, 2.0, 6.0] + for i in range(10): + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (?, ?)", + [i + 1, float_vec(vec)], + ) + + rows = db.execute( + "SELECT rowid, distance FROM t WHERE embedding MATCH ? ORDER BY distance LIMIT 10", + [float_vec(vec)], + ).fetchall() + assert len(rows) == 10 + # All distances should be ~0 (exact match) + for r in rows: + assert r["distance"] < 0.01 + + +def test_zero_vector_insert(db): + """Insert the zero vector [0,0,...,0]. Should not crash quantization.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] indexed by rescore(quantizer=bit)" + ")" + ) + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (1, ?)", + [float_vec([0.0] * 8)], + ) + row = db.execute("SELECT count(*) as cnt FROM t").fetchone() + assert row["cnt"] == 1 + + # Also test int8 quantizer with zero vector + db.execute( + "CREATE VIRTUAL TABLE t2 USING vec0(" + " embedding float[8] indexed by rescore(quantizer=int8)" + ")" + ) + db.execute( + "INSERT INTO t2(rowid, embedding) VALUES (1, ?)", + [float_vec([0.0] * 8)], + ) + row = db.execute("SELECT count(*) as cnt FROM t2").fetchone() + assert row["cnt"] == 1 + + +def test_very_large_values(db): + """Insert vectors with very large float values. Quantization should not crash.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] indexed by rescore(quantizer=int8)" + ")" + ) + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (1, ?)", + [float_vec([1e30] * 8)], + ) + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (2, ?)", + [float_vec([1e30, -1e30, 1e30, -1e30, 1e30, -1e30, 1e30, -1e30])], + ) + row = db.execute("SELECT count(*) as cnt FROM t").fetchone() + assert row["cnt"] == 2 + + +def test_negative_values(db): + """Insert vectors with all negative values. Bit quantization maps all to 0.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] indexed by rescore(quantizer=bit)" + ")" + ) + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (1, ?)", + [float_vec([-1.0, -2.0, -3.0, -4.0, -5.0, -6.0, -7.0, -8.0])], + ) + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (2, ?)", + [float_vec([-0.1, -0.2, -0.3, -0.4, -0.5, -0.6, -0.7, -0.8])], + ) + row = db.execute("SELECT count(*) as cnt FROM t").fetchone() + assert row["cnt"] == 2 + + # KNN should still work + rows = db.execute( + "SELECT rowid FROM t WHERE embedding MATCH ? ORDER BY distance LIMIT 2", + [float_vec([-0.1, -0.2, -0.3, -0.4, -0.5, -0.6, -0.7, -0.8])], + ).fetchall() + assert len(rows) == 2 + assert rows[0]["rowid"] == 2 + + +def test_single_dimension(db): + """Single-dimension vector (edge case for quantization).""" + # int8 quantizer (bit needs dim divisible by 8) + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] indexed by rescore(quantizer=int8)" + ")" + ) + db.execute("INSERT INTO t(rowid, embedding) VALUES (1, ?)", [float_vec([1.0] * 8)]) + db.execute("INSERT INTO t(rowid, embedding) VALUES (2, ?)", [float_vec([5.0] * 8)]) + rows = db.execute( + "SELECT rowid FROM t WHERE embedding MATCH ? ORDER BY distance LIMIT 1", + [float_vec([1.0] * 8)], + ).fetchall() + assert rows[0]["rowid"] == 1 + + +# ============================================================================ +# vec_debug() verification +# ============================================================================ + + +def test_vec_debug_contains_rescore(db): + """vec_debug() should contain 'rescore' in build flags when compiled with SQLITE_VEC_ENABLE_RESCORE.""" + row = db.execute("SELECT vec_debug() as d").fetchone() + assert "rescore" in row["d"] + + +# ============================================================================ +# Insert batch recall test +# ============================================================================ + + +def test_insert_batch_recall(db): + """Insert 150 rows and verify KNN recall is reasonable (>0.6).""" + dim = 16 + n = 150 + k = 10 + random.seed(77) + + db.execute( + f"CREATE VIRTUAL TABLE t_rescore USING vec0(" + f" embedding float[{dim}] indexed by rescore(quantizer=int8, oversample=16)" + f")" + ) + db.execute(f"CREATE VIRTUAL TABLE t_flat USING vec0(embedding float[{dim}])") + + vectors = [[random.gauss(0, 1) for _ in range(dim)] for _ in range(n)] + for i, v in enumerate(vectors): + blob = float_vec(v) + db.execute( + "INSERT INTO t_rescore(rowid, embedding) VALUES (?, ?)", [i + 1, blob] + ) + db.execute("INSERT INTO t_flat(rowid, embedding) VALUES (?, ?)", [i + 1, blob]) + + query = float_vec([random.gauss(0, 1) for _ in range(dim)]) + + rescore_rows = db.execute( + "SELECT rowid FROM t_rescore WHERE embedding MATCH ? ORDER BY distance LIMIT ?", + [query, k], + ).fetchall() + flat_rows = db.execute( + "SELECT rowid FROM t_flat WHERE embedding MATCH ? ORDER BY distance LIMIT ?", + [query, k], + ).fetchall() + + rescore_ids = {r["rowid"] for r in rescore_rows} + flat_ids = {r["rowid"] for r in flat_rows} + recall = len(rescore_ids & flat_ids) / k + assert recall >= 0.6, f"Recall too low: {recall}" + + +# ============================================================================ +# Distance metric variants +# ============================================================================ + + +def test_delete_interleaved_with_knn(db): + """Delete rows one at a time, running KNN after each delete to verify correctness.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] indexed by rescore(quantizer=bit)" + ")" + ) + N = 30 + random.seed(42) + vecs = {i: [random.gauss(0, 1) for _ in range(8)] for i in range(1, N + 1)} + for rowid, vec in vecs.items(): + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (?, ?)", + [rowid, float_vec(vec)], + ) + + alive = set(vecs.keys()) + query = [0.0] * 8 + + for to_del in [5, 10, 15, 20, 25]: + db.execute("DELETE FROM t WHERE rowid = ?", [to_del]) + alive.discard(to_del) + + rows = db.execute( + "SELECT rowid FROM t WHERE embedding MATCH ? ORDER BY distance LIMIT 10", + [float_vec(query)], + ).fetchall() + returned = {r["rowid"] for r in rows} + # All returned rows must be alive (not deleted) + assert returned.issubset( + alive + ), f"Deleted rowid found in KNN after deleting {to_del}" + # Count should match alive set (up to k) + assert len(rows) == min(10, len(alive)) + + +def test_delete_with_rowid_in_constraint(db): + """Delete rows and verify KNN with rowid_in filter excludes deleted rows.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] indexed by rescore(quantizer=int8)" + ")" + ) + for i in range(1, 11): + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (?, ?)", + [i, float_vec([float(i)] * 8)], + ) + + # Delete rows 3, 5, 7 + for r in [3, 5, 7]: + db.execute("DELETE FROM t WHERE rowid = ?", [r]) + + # KNN with rowid IN (1,2,3,4,5) — should only return 1, 2, 4 (3 and 5 deleted) + rows = db.execute( + "SELECT rowid FROM t WHERE embedding MATCH ? AND k = 5 AND rowid IN (1, 2, 3, 4, 5)", + [float_vec([1.0] * 8)], + ).fetchall() + returned = {r["rowid"] for r in rows} + assert 3 not in returned + assert 5 not in returned + assert returned.issubset({1, 2, 4}) + + +def test_delete_all_then_reinsert_batch(db): + """Delete all rows, reinsert a new batch, verify KNN only returns new rows.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] indexed by rescore(quantizer=bit)" + ")" + ) + # First batch + for i in range(1, 21): + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (?, ?)", + [i, float_vec([float(i)] * 8)], + ) + + # Delete all + for i in range(1, 21): + db.execute("DELETE FROM t WHERE rowid = ?", [i]) + + assert db.execute("SELECT count(*) FROM t").fetchone()[0] == 0 + + # Second batch with different rowids and vectors + for i in range(100, 110): + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (?, ?)", + [i, float_vec([float(i - 100)] * 8)], + ) + + rows = db.execute( + "SELECT rowid FROM t WHERE embedding MATCH ? ORDER BY distance LIMIT 5", + [float_vec([0.0] * 8)], + ).fetchall() + returned = {r["rowid"] for r in rows} + # All returned rowids should be from the second batch + assert returned.issubset(set(range(100, 110))) + + +def test_knn_int8_cosine(db): + """Rescore with quantizer=int8 and distance_metric=cosine.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] distance_metric=cosine indexed by rescore(quantizer=int8)" + ")" + ) + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (1, ?)", + [float_vec([1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])], + ) + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (2, ?)", + [float_vec([0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])], + ) + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (3, ?)", + [float_vec([1.0, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])], + ) + rows = db.execute( + "SELECT rowid, distance FROM t WHERE embedding MATCH ? ORDER BY distance LIMIT 2", + [float_vec([1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])], + ).fetchall() + assert rows[0]["rowid"] == 1 + assert rows[0]["distance"] < 0.01 diff --git a/tests/test-rescore.py b/tests/test-rescore.py new file mode 100644 index 00000000..0632d864 --- /dev/null +++ b/tests/test-rescore.py @@ -0,0 +1,1004 @@ +"""Tests for the rescore index feature in sqlite-vec.""" + +import struct +import sqlite3 +import pytest +import math +import random + + +def float_vec(values): + """Pack a list of floats into a blob for sqlite-vec.""" + return struct.pack(f"{len(values)}f", *values) + + +def unpack_float_vec(blob): + """Unpack a float vector blob.""" + n = len(blob) // 4 + return list(struct.unpack(f"{n}f", blob)) + + +# ============================================================================ +# Creation tests +# ============================================================================ + + +def test_create_bit(db): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[128] indexed by rescore(quantizer=bit)" + ")" + ) + # Table exists and has the right structure + row = db.execute( + "SELECT count(*) as cnt FROM sqlite_master WHERE name LIKE 't_%'" + ).fetchone() + assert row["cnt"] > 0 + + +def test_create_int8(db): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[128] indexed by rescore(quantizer=int8)" + ")" + ) + row = db.execute( + "SELECT count(*) as cnt FROM sqlite_master WHERE name LIKE 't_%'" + ).fetchone() + assert row["cnt"] > 0 + + +def test_create_with_oversample(db): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[128] indexed by rescore(quantizer=bit, oversample=16)" + ")" + ) + row = db.execute( + "SELECT count(*) as cnt FROM sqlite_master WHERE name LIKE 't_%'" + ).fetchone() + assert row["cnt"] > 0 + + +def test_create_with_distance_metric(db): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[128] distance_metric=cosine indexed by rescore(quantizer=bit)" + ")" + ) + row = db.execute( + "SELECT count(*) as cnt FROM sqlite_master WHERE name LIKE 't_%'" + ).fetchone() + assert row["cnt"] > 0 + + +def test_create_error_missing_quantizer(db): + with pytest.raises(sqlite3.OperationalError): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[128] indexed by rescore(oversample=8)" + ")" + ) + + +def test_create_error_invalid_quantizer(db): + with pytest.raises(sqlite3.OperationalError): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[128] indexed by rescore(quantizer=float)" + ")" + ) + + +def test_create_error_on_bit_column(db): + with pytest.raises(sqlite3.OperationalError): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding bit[1024] indexed by rescore(quantizer=bit)" + ")" + ) + + +def test_create_error_on_int8_column(db): + with pytest.raises(sqlite3.OperationalError): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding int8[128] indexed by rescore(quantizer=bit)" + ")" + ) + + +def test_create_error_bad_oversample_zero(db): + with pytest.raises(sqlite3.OperationalError): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[128] indexed by rescore(quantizer=bit, oversample=0)" + ")" + ) + + +def test_create_error_bad_oversample_too_large(db): + with pytest.raises(sqlite3.OperationalError): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[128] indexed by rescore(quantizer=bit, oversample=999)" + ")" + ) + + +def test_create_error_bit_dim_not_divisible_by_8(db): + with pytest.raises(sqlite3.OperationalError): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[100] indexed by rescore(quantizer=bit)" + ")" + ) + + +# ============================================================================ +# Shadow table tests +# ============================================================================ + + +def test_shadow_tables_exist(db): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[128] indexed by rescore(quantizer=bit)" + ")" + ) + tables = [ + r[0] + for r in db.execute( + "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 't_%' ORDER BY name" + ).fetchall() + ] + assert "t_rescore_chunks00" in tables + assert "t_rescore_vectors00" in tables + # Rescore columns don't create _vector_chunks + assert "t_vector_chunks00" not in tables + + +def test_drop_cleans_up(db): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[128] indexed by rescore(quantizer=bit)" + ")" + ) + db.execute("DROP TABLE t") + tables = [ + r[0] + for r in db.execute( + "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 't_%'" + ).fetchall() + ] + assert len(tables) == 0 + + +# ============================================================================ +# Insert tests +# ============================================================================ + + +def test_insert_single(db): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] indexed by rescore(quantizer=bit)" + ")" + ) + db.execute("INSERT INTO t(rowid, embedding) VALUES (1, ?)", [float_vec([1.0] * 8)]) + row = db.execute("SELECT count(*) as cnt FROM t").fetchone() + assert row["cnt"] == 1 + + +def test_insert_multiple(db): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] indexed by rescore(quantizer=int8)" + ")" + ) + for i in range(10): + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (?, ?)", + [i + 1, float_vec([float(i)] * 8)], + ) + row = db.execute("SELECT count(*) as cnt FROM t").fetchone() + assert row["cnt"] == 10 + + +# ============================================================================ +# Delete tests +# ============================================================================ + + +def test_delete_single(db): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] indexed by rescore(quantizer=bit)" + ")" + ) + db.execute("INSERT INTO t(rowid, embedding) VALUES (1, ?)", [float_vec([1.0] * 8)]) + db.execute("DELETE FROM t WHERE rowid = 1") + row = db.execute("SELECT count(*) as cnt FROM t").fetchone() + assert row["cnt"] == 0 + + +def test_delete_and_reinsert(db): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] indexed by rescore(quantizer=bit)" + ")" + ) + db.execute("INSERT INTO t(rowid, embedding) VALUES (1, ?)", [float_vec([1.0] * 8)]) + db.execute("DELETE FROM t WHERE rowid = 1") + db.execute("INSERT INTO t(rowid, embedding) VALUES (2, ?)", [float_vec([2.0] * 8)]) + row = db.execute("SELECT count(*) as cnt FROM t").fetchone() + assert row["cnt"] == 1 + + +def test_point_query_returns_float(db): + """SELECT by rowid should return the original float vector, not quantized.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] indexed by rescore(quantizer=bit)" + ")" + ) + vals = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8] + db.execute("INSERT INTO t(rowid, embedding) VALUES (1, ?)", [float_vec(vals)]) + row = db.execute("SELECT embedding FROM t WHERE rowid = 1").fetchone() + result = unpack_float_vec(row["embedding"]) + for a, b in zip(result, vals): + assert abs(a - b) < 1e-6 + + +# ============================================================================ +# KNN tests +# ============================================================================ + + +def test_knn_basic_bit(db): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] indexed by rescore(quantizer=bit)" + ")" + ) + # Insert vectors where [1,0,0,...] is closest to query [1,0,0,...] + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (1, ?)", + [float_vec([1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])], + ) + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (2, ?)", + [float_vec([0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])], + ) + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (3, ?)", + [float_vec([0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0])], + ) + rows = db.execute( + "SELECT rowid, distance FROM t WHERE embedding MATCH ? ORDER BY distance LIMIT 1", + [float_vec([1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])], + ).fetchall() + assert len(rows) == 1 + assert rows[0]["rowid"] == 1 + + +def test_knn_basic_int8(db): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] indexed by rescore(quantizer=int8)" + ")" + ) + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (1, ?)", + [float_vec([1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])], + ) + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (2, ?)", + [float_vec([0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])], + ) + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (3, ?)", + [float_vec([0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0])], + ) + rows = db.execute( + "SELECT rowid, distance FROM t WHERE embedding MATCH ? ORDER BY distance LIMIT 1", + [float_vec([1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])], + ).fetchall() + assert len(rows) == 1 + assert rows[0]["rowid"] == 1 + + +def test_knn_returns_float_distances(db): + """KNN should return float-precision distances, not quantized distances.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] indexed by rescore(quantizer=bit)" + ")" + ) + v1 = [1.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + v2 = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0] + db.execute("INSERT INTO t(rowid, embedding) VALUES (1, ?)", [float_vec(v1)]) + db.execute("INSERT INTO t(rowid, embedding) VALUES (2, ?)", [float_vec(v2)]) + + query = [1.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + rows = db.execute( + "SELECT rowid, distance FROM t WHERE embedding MATCH ? ORDER BY distance LIMIT 2", + [float_vec(query)], + ).fetchall() + + # First result should be exact match with distance ~0 + assert rows[0]["rowid"] == 1 + assert rows[0]["distance"] < 0.01 + + # Second result should have a float distance + # sqrt((1-0)^2 + (0.5-0)^2 + (0-1)^2) = sqrt(2.25) = 1.5 + assert abs(rows[1]["distance"] - 1.5) < 0.01 + + +def test_knn_recall(db): + """With enough vectors, rescore should achieve good recall (>0.9).""" + dim = 32 + n = 1000 + k = 10 + random.seed(42) + + db.execute( + "CREATE VIRTUAL TABLE t_rescore USING vec0(" + f" embedding float[{dim}] indexed by rescore(quantizer=bit, oversample=16)" + ")" + ) + db.execute(f"CREATE VIRTUAL TABLE t_flat USING vec0(embedding float[{dim}])") + + vectors = [[random.gauss(0, 1) for _ in range(dim)] for _ in range(n)] + for i, v in enumerate(vectors): + blob = float_vec(v) + db.execute( + "INSERT INTO t_rescore(rowid, embedding) VALUES (?, ?)", [i + 1, blob] + ) + db.execute("INSERT INTO t_flat(rowid, embedding) VALUES (?, ?)", [i + 1, blob]) + + query = float_vec([random.gauss(0, 1) for _ in range(dim)]) + + rescore_rows = db.execute( + "SELECT rowid FROM t_rescore WHERE embedding MATCH ? ORDER BY distance LIMIT ?", + [query, k], + ).fetchall() + flat_rows = db.execute( + "SELECT rowid FROM t_flat WHERE embedding MATCH ? ORDER BY distance LIMIT ?", + [query, k], + ).fetchall() + + rescore_ids = {r["rowid"] for r in rescore_rows} + flat_ids = {r["rowid"] for r in flat_rows} + recall = len(rescore_ids & flat_ids) / k + assert recall >= 0.7, f"Recall too low: {recall}" + + +def test_knn_cosine(db): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] distance_metric=cosine indexed by rescore(quantizer=bit)" + ")" + ) + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (1, ?)", + [float_vec([1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])], + ) + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (2, ?)", + [float_vec([0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])], + ) + rows = db.execute( + "SELECT rowid, distance FROM t WHERE embedding MATCH ? ORDER BY distance LIMIT 1", + [float_vec([1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])], + ).fetchall() + assert rows[0]["rowid"] == 1 + # cosine distance of identical vectors should be ~0 + assert rows[0]["distance"] < 0.01 + + +def test_knn_empty_table(db): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] indexed by rescore(quantizer=bit)" + ")" + ) + rows = db.execute( + "SELECT rowid FROM t WHERE embedding MATCH ? ORDER BY distance LIMIT 5", + [float_vec([1.0] * 8)], + ).fetchall() + assert len(rows) == 0 + + +def test_knn_k_larger_than_n(db): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] indexed by rescore(quantizer=bit)" + ")" + ) + db.execute("INSERT INTO t(rowid, embedding) VALUES (1, ?)", [float_vec([1.0] * 8)]) + db.execute("INSERT INTO t(rowid, embedding) VALUES (2, ?)", [float_vec([2.0] * 8)]) + rows = db.execute( + "SELECT rowid FROM t WHERE embedding MATCH ? ORDER BY distance LIMIT 10", + [float_vec([1.0] * 8)], + ).fetchall() + assert len(rows) == 2 + + +# ============================================================================ +# Integration / edge case tests +# ============================================================================ + + +def test_knn_with_rowid_in(db): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] indexed by rescore(quantizer=bit)" + ")" + ) + for i in range(5): + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (?, ?)", + [i + 1, float_vec([float(i)] * 8)], + ) + # Only search within rowids 1, 3, 5 + rows = db.execute( + "SELECT rowid FROM t WHERE embedding MATCH ? AND rowid IN (1, 3, 5) ORDER BY distance LIMIT 3", + [float_vec([0.0] * 8)], + ).fetchall() + result_ids = {r["rowid"] for r in rows} + assert result_ids <= {1, 3, 5} + + +def test_knn_after_deletes(db): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] indexed by rescore(quantizer=int8)" + ")" + ) + for i in range(10): + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (?, ?)", + [i + 1, float_vec([float(i)] * 8)], + ) + # Delete the closest match (rowid 1 = [0,0,...]) + db.execute("DELETE FROM t WHERE rowid = 1") + rows = db.execute( + "SELECT rowid, distance FROM t WHERE embedding MATCH ? ORDER BY distance LIMIT 5", + [float_vec([0.0] * 8)], + ).fetchall() + # Verify ordering: rowid 2 ([1]*8) should be closest, then 3 ([2]*8), etc. + assert len(rows) >= 2 + assert rows[0]["distance"] <= rows[1]["distance"] + # rowid 2 = [1,1,...] → L2 = sqrt(8) ≈ 2.83, rowid 3 = [2,2,...] → L2 = sqrt(32) ≈ 5.66 + assert ( + rows[0]["rowid"] == 2 + ), f"Expected rowid 2, got {rows[0]['rowid']} with dist={rows[0]['distance']}" + + +def test_oversample_effect(db): + """Higher oversample should give equal or better recall.""" + dim = 32 + n = 500 + k = 10 + random.seed(123) + + vectors = [[random.gauss(0, 1) for _ in range(dim)] for _ in range(n)] + query = float_vec([random.gauss(0, 1) for _ in range(dim)]) + + recalls = [] + for oversample in [2, 16]: + tname = f"t_os{oversample}" + db.execute( + f"CREATE VIRTUAL TABLE {tname} USING vec0(" + f" embedding float[{dim}] indexed by rescore(quantizer=bit, oversample={oversample})" + ")" + ) + for i, v in enumerate(vectors): + db.execute( + f"INSERT INTO {tname}(rowid, embedding) VALUES (?, ?)", + [i + 1, float_vec(v)], + ) + rows = db.execute( + f"SELECT rowid FROM {tname} WHERE embedding MATCH ? ORDER BY distance LIMIT ?", + [query, k], + ).fetchall() + recalls.append({r["rowid"] for r in rows}) + + # Also get ground truth + db.execute(f"CREATE VIRTUAL TABLE t_flat USING vec0(embedding float[{dim}])") + for i, v in enumerate(vectors): + db.execute( + "INSERT INTO t_flat(rowid, embedding) VALUES (?, ?)", + [i + 1, float_vec(v)], + ) + gt_rows = db.execute( + "SELECT rowid FROM t_flat WHERE embedding MATCH ? ORDER BY distance LIMIT ?", + [query, k], + ).fetchall() + gt_ids = {r["rowid"] for r in gt_rows} + + recall_low = len(recalls[0] & gt_ids) / k + recall_high = len(recalls[1] & gt_ids) / k + assert recall_high >= recall_low + + +def test_multiple_vector_columns(db): + """One column with rescore, one without.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " v1 float[8] indexed by rescore(quantizer=bit)," + " v2 float[8]" + ")" + ) + db.execute( + "INSERT INTO t(rowid, v1, v2) VALUES (1, ?, ?)", + [float_vec([1.0] * 8), float_vec([0.0] * 8)], + ) + db.execute( + "INSERT INTO t(rowid, v1, v2) VALUES (2, ?, ?)", + [float_vec([0.0] * 8), float_vec([1.0] * 8)], + ) + + # KNN on v1 (rescore path) + rows = db.execute( + "SELECT rowid FROM t WHERE v1 MATCH ? ORDER BY distance LIMIT 1", + [float_vec([1.0] * 8)], + ).fetchall() + assert rows[0]["rowid"] == 1 + + # KNN on v2 (normal path) + rows = db.execute( + "SELECT rowid FROM t WHERE v2 MATCH ? ORDER BY distance LIMIT 1", + [float_vec([1.0] * 8)], + ).fetchall() + assert rows[0]["rowid"] == 2 + + +def test_corrupt_zeroblob_validity(db): + """KNN should error (not crash) when rescore chunk rowids blob is zeroed out.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] indexed by rescore(quantizer=bit)" + ")" + ) + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (1, ?)", + [float_vec([1, 0, 0, 0, 0, 0, 0, 0])], + ) + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (2, ?)", + [float_vec([0, 1, 0, 0, 0, 0, 0, 0])], + ) + + # Corrupt: replace rowids with a truncated blob (wrong size) + db.execute("UPDATE t_chunks SET rowids = x'00'") + + # Should error, not crash — blob size validation catches the mismatch + with pytest.raises(sqlite3.OperationalError): + db.execute( + "SELECT rowid FROM t WHERE embedding MATCH ? ORDER BY distance LIMIT 1", + [float_vec([1, 0, 0, 0, 0, 0, 0, 0])], + ).fetchall() + + +def test_corrupt_truncated_validity_blob(db): + """KNN should error when rescore chunk validity blob is truncated.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[128] indexed by rescore(quantizer=bit)" + ")" + ) + for i in range(5): + random.seed(i) + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (?, ?)", + [i + 1, float_vec([random.gauss(0, 1) for _ in range(128)])], + ) + + # Corrupt: truncate validity blob to 1 byte (should be chunk_size/8 = 128 bytes) + db.execute("UPDATE t_chunks SET validity = x'FF'") + + with pytest.raises(sqlite3.OperationalError): + db.execute( + "SELECT rowid FROM t WHERE embedding MATCH ? ORDER BY distance LIMIT 1", + [float_vec([1.0] * 128)], + ).fetchall() + + +def test_rescore_text_pk_insert_knn_delete(db): + """Rescore with text primary key: insert, KNN, delete, KNN again.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " id text primary key," + " embedding float[128] indexed by rescore(quantizer=bit)" + ")" + ) + + random.seed(99) + vecs = {} + for name in ["alpha", "beta", "gamma", "delta", "epsilon"]: + v = [random.gauss(0, 1) for _ in range(128)] + vecs[name] = v + db.execute("INSERT INTO t(id, embedding) VALUES (?, ?)", [name, float_vec(v)]) + + # KNN should return text IDs + rows = db.execute( + "SELECT id, distance FROM t WHERE embedding MATCH ? ORDER BY distance LIMIT 3", + [float_vec(vecs["alpha"])], + ).fetchall() + assert len(rows) >= 1 + ids = [r["id"] for r in rows] + assert "alpha" in ids + + # Delete and verify + db.execute("DELETE FROM t WHERE id = 'alpha'") + rows = db.execute( + "SELECT id FROM t WHERE embedding MATCH ? ORDER BY distance LIMIT 3", + [float_vec(vecs["alpha"])], + ).fetchall() + ids = [r["id"] for r in rows] + assert "alpha" not in ids + assert len(rows) >= 1 # other results still returned + + +def test_runtime_oversample(db): + """oversample can be changed at query time via FTS5-style command.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[128] indexed by rescore(quantizer=bit, oversample=2)" + ")" + ) + random.seed(200) + for i in range(200): + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (?, ?)", + [i + 1, float_vec([random.gauss(0, 1) for _ in range(128)])], + ) + + query = float_vec([random.gauss(0, 1) for _ in range(128)]) + + # KNN with default oversample=2 (low) + rows_low = db.execute( + "SELECT rowid FROM t WHERE embedding MATCH ? ORDER BY distance LIMIT 10", + [query], + ).fetchall() + assert len(rows_low) == 10 + + # Change oversample at runtime to high value + db.execute("INSERT INTO t(t) VALUES ('oversample=32')") + + # KNN with oversample=32 (high) — same or better recall + rows_high = db.execute( + "SELECT rowid FROM t WHERE embedding MATCH ? ORDER BY distance LIMIT 10", + [query], + ).fetchall() + assert len(rows_high) == 10 + + # Reset to original + db.execute("INSERT INTO t(t) VALUES ('oversample=2')") + + rows_reset = db.execute( + "SELECT rowid FROM t WHERE embedding MATCH ? ORDER BY distance LIMIT 10", + [query], + ).fetchall() + assert len(rows_reset) == 10 + # After reset, should match the original low-oversample results + assert [r["rowid"] for r in rows_reset] == [r["rowid"] for r in rows_low] + + +def test_runtime_oversample_error(db): + """Invalid oversample values should error.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[128] indexed by rescore(quantizer=bit)" + ")" + ) + with pytest.raises(sqlite3.OperationalError, match="oversample must be >= 1"): + db.execute("INSERT INTO t(t) VALUES ('oversample=0')") + + with pytest.raises(sqlite3.OperationalError, match="oversample must be >= 1"): + db.execute("INSERT INTO t(t) VALUES ('oversample=-5')") + + +def test_unknown_command_errors(db): + """Unknown command strings should produce a clear error.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[128] indexed by rescore(quantizer=bit)" + ")" + ) + with pytest.raises(sqlite3.OperationalError, match="unknown vec0 command"): + db.execute("INSERT INTO t(t) VALUES ('not_a_real_command')") + + +# ============================================================================ +# Distance constraint tests (asg017/sqlite-vec#308) +# ============================================================================ + + +def test_knn_distance_constraint_le(db): + """A `distance <= threshold` constraint must be honored in rescore KNN.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] indexed by rescore(quantizer=bit)" + ")" + ) + # rowid 1 is the exact match (distance 0); rowid 2 is far away. + v1 = [1.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + v2 = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0] + db.execute("INSERT INTO t(rowid, embedding) VALUES (1, ?)", [float_vec(v1)]) + db.execute("INSERT INTO t(rowid, embedding) VALUES (2, ?)", [float_vec(v2)]) + + query = [1.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + rows = db.execute( + "SELECT rowid, distance FROM t " + "WHERE embedding MATCH ? AND k = 10 AND distance <= 0.5 " + "ORDER BY distance", + [float_vec(query)], + ).fetchall() + + # Only the exact match (distance ~0) is within the 0.5 threshold; + # rowid 2 (distance 1.5) must be filtered out. + assert [r["rowid"] for r in rows] == [1] + assert all(r["distance"] <= 0.5 for r in rows) + + +def test_knn_distance_constraint_lt_gt(db): + """`distance < x` and `distance > x` constraints must be honored.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] indexed by rescore(quantizer=bit)" + ")" + ) + v1 = [1.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + v2 = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0] + db.execute("INSERT INTO t(rowid, embedding) VALUES (1, ?)", [float_vec(v1)]) + db.execute("INSERT INTO t(rowid, embedding) VALUES (2, ?)", [float_vec(v2)]) + + query = [1.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + # Exclude the exact match, keep only the far vector. + rows = db.execute( + "SELECT rowid, distance FROM t " + "WHERE embedding MATCH ? AND k = 10 AND distance > 0.5 " + "ORDER BY distance", + [float_vec(query)], + ).fetchall() + assert [r["rowid"] for r in rows] == [2] + + +# ============================================================================ +# Behavior this fork adds or keeps for rescore columns +# ============================================================================ + + +def _rescore_table(db, quantizer, n=40, dim=16, chunk_size=8, seed=11): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + f" embedding float[{dim}] indexed by rescore(quantizer={quantizer})," + f" chunk_size={chunk_size}" + ")" + ) + rng = random.Random(seed) + vecs = {} + for rowid in range(1, n + 1): + vecs[rowid] = [rng.uniform(-1.5, 1.5) for _ in range(dim)] + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (?, ?)", + [rowid, float_vec(vecs[rowid])], + ) + return vecs + + +def _f32(x): + return struct.unpack("f", struct.pack("f", x))[0] + + +def _quantize(quantizer, vec): + if quantizer == "bit": + out = bytearray(len(vec) // 8) + for i, x in enumerate(vec): + if _f32(x) >= 0: + out[i // 8] |= 1 << (i % 8) + return bytes(out) + # the C quantizer's float32 arithmetic, truncated toward zero + step = _f32(2.0 / 255.0) + out = [] + for x in vec: + v = _f32(_f32(_f32(x) + 1.0) / step) + v = _f32(v - 128.0) + if not v <= 127.0: + v = 127.0 + if not v >= -128.0: + v = -128.0 + out.append(int(v)) + return struct.pack(f"{len(vec)}b", *out) + + +def _assert_quantized_slots(db, quantizer): + """Each row's slot in _rescore_chunks00 holds the quantized form of its + vector in _rescore_vectors00, each free slot is zero, and every chunk has + its _rescore_chunks00 row.""" + qsize = None + for rowid, chunk_id, offset in db.execute( + "select rowid, chunk_id, chunk_offset from t_rowids" + ).fetchall(): + vector = unpack_float_vec( + db.execute( + "select vector from t_rescore_vectors00 where rowid = ?", [rowid] + ).fetchone()[0] + ) + expected = _quantize(quantizer, vector) + qsize = len(expected) + chunk = db.execute( + "select vectors from t_rescore_chunks00 where rowid = ?", [chunk_id] + ).fetchone()[0] + assert chunk[offset * qsize : (offset + 1) * qsize] == expected + for chunk_id, validity, vectors in db.execute( + "select c.chunk_id, c.validity, r.vectors from t_chunks c " + "join t_rescore_chunks00 r on r.rowid = c.chunk_id" + ).fetchall(): + size = len(vectors) // (len(validity) * 8) + for j in range(len(validity) * 8): + if not (validity[j // 8] >> (j % 8)) & 1: + assert not any(vectors[j * size : (j + 1) * size]) + assert ( + db.execute( + "select count(*) from t_chunks " + "where chunk_id not in (select rowid from t_rescore_chunks00)" + ).fetchone()[0] + == 0 + ) + assert ( + db.execute( + "select count(*) from t_rescore_chunks00 " + "where rowid not in (select chunk_id from t_chunks)" + ).fetchone()[0] + == 0 + ) + assert ( + db.execute( + "select count(*) from t_rescore_vectors00 " + "where rowid not in (select rowid from t_rowids)" + ).fetchone()[0] + == 0 + ) + + +def test_point_query_missing_rowid(db): + """A point query for a rowid not in the table returns no row.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] indexed by rescore(quantizer=bit)" + ")" + ) + db.execute("INSERT INTO t(rowid, embedding) VALUES (1, ?)", [float_vec([1.0] * 8)]) + assert db.execute("SELECT rowid FROM t WHERE rowid = 999").fetchall() == [] + assert [r[0] for r in db.execute("SELECT rowid FROM t WHERE rowid = 1")] == [1] + + +@pytest.mark.parametrize("quantizer", ["bit", "int8"]) +def test_quantized_slots_follow_writes(db, quantizer): + """Inserts, updates, deletes that empty chunks, and optimize keep each row's + quantized vector in its own slot.""" + rng = random.Random(5) + _rescore_table(db, quantizer) + _assert_quantized_slots(db, quantizer) + + db.execute("DELETE FROM t WHERE rowid % 3 = 0") + _assert_quantized_slots(db, quantizer) + + for rowid in range(1, 41, 7): + db.execute( + "UPDATE t SET embedding = ? WHERE rowid = ?", + [float_vec([rng.uniform(-1.5, 1.5) for _ in range(16)]), rowid], + ) + _assert_quantized_slots(db, quantizer) + + # rows 1 to 16 fill the first two chunks, so their deletion empties them + db.execute("DELETE FROM t WHERE rowid <= 16") + _assert_quantized_slots(db, quantizer) + + db.execute("INSERT INTO t(t) VALUES ('optimize')") + _assert_quantized_slots(db, quantizer) + + for rowid in range(100, 120): + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (?, ?)", + [rowid, float_vec([rng.uniform(-1.5, 1.5) for _ in range(16)])], + ) + _assert_quantized_slots(db, quantizer) + + +def test_optimize_keeps_knn_results(db): + """optimize moves rows to new chunks without changing what a KNN query + returns, here with an oversample small enough that the quantized vectors + decide the candidates.""" + vecs = _rescore_table(db, "int8", n=200) + db.execute("INSERT INTO t(t) VALUES ('oversample=1')") + db.execute("DELETE FROM t WHERE rowid % 2 = 0") + query = float_vec([0.3] * 16) + knn = "SELECT rowid, distance FROM t WHERE embedding MATCH ? AND k = 5" + before = db.execute(knn, [query]).fetchall() + db.execute("INSERT INTO t(t) VALUES ('optimize')") + after = db.execute(knn, [query]).fetchall() + assert [tuple(r) for r in after] == [tuple(r) for r in before] + assert db.execute("SELECT count(*) FROM t_rescore_vectors00").fetchone()[0] == 100 + + +def test_delete_last_row_deletes_rescore_chunk(db): + """A DELETE that empties a chunk deletes its _rescore_chunks00 row too.""" + _rescore_table(db, "bit", n=16) + assert db.execute("SELECT count(*) FROM t_rescore_chunks00").fetchone()[0] == 2 + db.execute("DELETE FROM t WHERE rowid <= 8") + assert db.execute("SELECT count(*) FROM t_rescore_chunks00").fetchone()[0] == 1 + assert db.execute("SELECT count(*) FROM t_rescore_vectors00").fetchone()[0] == 8 + db.execute("DELETE FROM t") + assert db.execute("SELECT count(*) FROM t_rescore_chunks00").fetchone()[0] == 0 + assert db.execute("SELECT count(*) FROM t_rescore_vectors00").fetchone()[0] == 0 + + +def test_knn_distance_constraint_non_numeric(db): + """No distance compares with NULL, and every distance is less than any TEXT + or BLOB, as for a flat column.""" + _rescore_table(db, "bit", n=10) + query = float_vec([0.0] * 16) + knn = "SELECT rowid FROM t WHERE embedding MATCH ? AND k = 5 AND distance {} ?" + for op, value, expected in [ + ("<", None, 0), + (">", None, 0), + ("<", "a", 5), + ("<=", b"\x00", 5), + (">", "a", 0), + (">=", b"\x00", 0), + ]: + rows = db.execute(knn.format(op), [query, value]).fetchall() + assert len(rows) == expected, (op, value) + + +def test_knn_mmr(db): + """mmr_lambda reranks a rescore column's candidates by their float32 + vectors.""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] indexed by rescore(quantizer=int8)" + ")" + ) + vectors = { + 1: [1.0, 0, 0, 0, 0, 0, 0, 0], + 2: [0.99, 0.1, 0, 0, 0, 0, 0, 0], + 3: [0.98, 0.2, 0, 0, 0, 0, 0, 0], + 4: [0, 1.0, 0, 0, 0, 0, 0, 0], + 5: [0, 0, 1.0, 0, 0, 0, 0, 0], + } + for rowid, v in vectors.items(): + db.execute( + "INSERT INTO t(rowid, embedding) VALUES (?, ?)", [rowid, float_vec(v)] + ) + query = float_vec([1.0, 0, 0, 0, 0, 0, 0, 0]) + plain = db.execute( + "SELECT rowid FROM t WHERE embedding MATCH ? AND k = 3", [query] + ).fetchall() + assert [r[0] for r in plain] == [1, 2, 3] + diverse = db.execute( + "SELECT rowid FROM t WHERE embedding MATCH ? AND k = 3 AND mmr_lambda = 0.1", + [query], + ).fetchall() + assert [r[0] for r in diverse][0] == 1 + assert {r[0] for r in diverse} != {1, 2, 3} + + +def test_runtime_oversample_rejects_trailing_text(db): + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(" + " embedding float[8] indexed by rescore(quantizer=bit)" + ")" + ) + for command in ["oversample=8x", "oversample=", "oversample= 8", "oversample=+8"]: + with pytest.raises(sqlite3.OperationalError, match="oversample must be >= 1"): + db.execute("INSERT INTO t(t) VALUES (?)", [command]) + db.execute("INSERT INTO t(t) VALUES ('oversample=200')") + + +def test_vec_debug_build_flags(db): + assert "rescore" in db.execute("SELECT vec_debug()").fetchone()[0] diff --git a/tests/test-unit.c b/tests/test-unit.c index 21d1fd40..4aba2423 100644 --- a/tests/test-unit.c +++ b/tests/test-unit.c @@ -3,6 +3,7 @@ #include #include #include +#include #define countof(x) (sizeof(x) / sizeof((x)[0])) @@ -107,6 +108,24 @@ void test_vec0_token_next() { assert(token.token_type == TOKEN_TYPE_DIGIT); assert(token.end - token.start == 2); + // Left paren + input = "("; + rc = vec0_token_next(input, input + 1, &token); + assert(rc == VEC0_TOKEN_RESULT_SOME); + assert(token.token_type == TOKEN_TYPE_LPAREN); + + // Right paren + input = ")"; + rc = vec0_token_next(input, input + 1, &token); + assert(rc == VEC0_TOKEN_RESULT_SOME); + assert(token.token_type == TOKEN_TYPE_RPAREN); + + // Comma + input = ","; + rc = vec0_token_next(input, input + 1, &token); + assert(rc == VEC0_TOKEN_RESULT_SOME); + assert(token.token_type == TOKEN_TYPE_COMMA); + printf(" All vec0_token_next tests passed.\n"); } @@ -385,6 +404,91 @@ void test_vec0_parse_vector_column() { assert(rc != SQLITE_OK); } + // indexed by flat() + { + const char *input = "emb float[768] indexed by flat()"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_OK); + assert(col.index_type == VEC0_INDEX_TYPE_FLAT); + assert(col.dimensions == 768); + sqlite3_free(col.name); + } + + // indexed by flat() with distance_metric + { + const char *input = + "emb float[768] distance_metric=cosine indexed by flat()"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_OK); + assert(col.index_type == VEC0_INDEX_TYPE_FLAT); + assert(col.distance_metric == VEC0_DISTANCE_METRIC_COSINE); + sqlite3_free(col.name); + } + + // indexed by flat() on int8 + { + const char *input = "emb int8[256] indexed by flat()"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_OK); + assert(col.index_type == VEC0_INDEX_TYPE_FLAT); + assert(col.element_type == SQLITE_VEC_ELEMENT_TYPE_INT8); + sqlite3_free(col.name); + } + + // indexed by flat() on bit + { + const char *input = "emb bit[64] indexed by flat()"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_OK); + assert(col.index_type == VEC0_INDEX_TYPE_FLAT); + assert(col.element_type == SQLITE_VEC_ELEMENT_TYPE_BIT); + sqlite3_free(col.name); + } + + // default index_type is FLAT + { + const char *input = "emb float[768]"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_OK); + assert(col.index_type == VEC0_INDEX_TYPE_FLAT); + sqlite3_free(col.name); + } + + // Error: indexed by (missing type name) + { + const char *input = "emb float[768] indexed by"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_ERROR); + } + + // Error: indexed by unknown() + { + const char *input = "emb float[768] indexed by unknown()"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_ERROR); + } + + // Error: indexed by flat (missing parens) + { + const char *input = "emb float[768] indexed by flat"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_ERROR); + } + + // Error: indexed flat() (missing "by") + { + const char *input = "emb float[768] indexed flat()"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_ERROR); + } + + // Error: a prefix of a keyword, even when not strict + { + const char *input = "emb float[768] index by flat()"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, false); + assert(rc == SQLITE_ERROR); + } + printf(" All vec0_parse_vector_column tests passed.\n"); } @@ -495,6 +599,1106 @@ void test_vec0_fullscan_schema_rollback() { } } +#if SQLITE_VEC_ENABLE_RESCORE + +void test_rescore_quantize_float_to_bit() { + printf("Starting %s...\n", __func__); + uint8_t dst[16]; + + // All positive -> all bits 1 + { + float src[8] = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f}; + memset(dst, 0, sizeof(dst)); + rescore_quantize_float_to_bit(src, dst, 8); + assert(dst[0] == 0xFF); + } + + // All negative -> all bits 0 + { + float src[8] = {-1.0f, -2.0f, -3.0f, -4.0f, -5.0f, -6.0f, -7.0f, -8.0f}; + memset(dst, 0xFF, sizeof(dst)); + rescore_quantize_float_to_bit(src, dst, 8); + assert(dst[0] == 0x00); + } + + // Alternating positive/negative + { + float src[8] = {1.0f, -1.0f, 1.0f, -1.0f, 1.0f, -1.0f, 1.0f, -1.0f}; + rescore_quantize_float_to_bit(src, dst, 8); + // bits 0,2,4,6 set => 0b01010101 = 0x55 + assert(dst[0] == 0x55); + } + + // Zero values -> bit is set (>= 0.0f) + { + float src[8] = {0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}; + rescore_quantize_float_to_bit(src, dst, 8); + assert(dst[0] == 0xFF); + } + + // 128 dimensions -> 16 bytes output + { + float src[128]; + for (int i = 0; i < 128; i++) + src[i] = (i % 2 == 0) ? 1.0f : -1.0f; + memset(dst, 0, 16); + rescore_quantize_float_to_bit(src, dst, 128); + // Even indices set: bits 0,2,4,6 in each byte => 0x55 + for (int i = 0; i < 16; i++) { + assert(dst[i] == 0x55); + } + } + + printf(" All rescore_quantize_float_to_bit tests passed.\n"); +} + +void test_rescore_quantize_float_to_int8() { + printf("Starting %s...\n", __func__); + int8_t dst[16]; + + // [-1, 1] maps onto [-128, 127] in steps of 2/255, truncating toward zero. + // 1.0 lands just below 127 in float arithmetic, so it truncates to 126. + { + float src[6] = {-1.0f, -0.5f, 0.0f, 0.5f, 1.0f, 1.004f}; + rescore_quantize_float_to_int8(src, dst, 6); + assert(dst[0] == -128); + assert(dst[1] == -64); + assert(dst[2] == 0); + assert(dst[3] == 63); + assert(dst[4] == 126); + assert(dst[5] == 127); + } + + // Values outside [-1, 1] clamp, and NaN clamps to 127 + { + float src[5] = {2.0f, -2.0f, INFINITY, -INFINITY, NAN}; + rescore_quantize_float_to_int8(src, dst, 5); + assert(dst[0] == 127); + assert(dst[1] == -128); + assert(dst[2] == 127); + assert(dst[3] == -128); + assert(dst[4] == 127); + } + + // The mapping never decreases + { + int8_t prev = -128; + for (int i = 0; i <= 2200; i++) { + float src = -1.1f + (float)i * 0.001f; + int8_t out; + rescore_quantize_float_to_int8(&src, &out, 1); + assert(out >= prev); + prev = out; + } + } + + printf(" All rescore_quantize_float_to_int8 tests passed.\n"); +} + +void test_rescore_quantized_byte_size() { + printf("Starting %s...\n", __func__); + struct VectorColumnDefinition col; + memset(&col, 0, sizeof(col)); + + // Bit quantizer: dims/8 + col.rescore.quantizer_type = VEC0_RESCORE_QUANTIZER_BIT; + col.dimensions = 128; + assert(rescore_quantized_byte_size(&col) == 16); + col.dimensions = 8; + assert(rescore_quantized_byte_size(&col) == 1); + col.dimensions = 1024; + assert(rescore_quantized_byte_size(&col) == 128); + + // Int8 quantizer: dims + col.rescore.quantizer_type = VEC0_RESCORE_QUANTIZER_INT8; + col.dimensions = 128; + assert(rescore_quantized_byte_size(&col) == 128); + col.dimensions = 8; + assert(rescore_quantized_byte_size(&col) == 8); + col.dimensions = 1024; + assert(rescore_quantized_byte_size(&col) == 1024); + + printf(" All rescore_quantized_byte_size tests passed.\n"); +} + +void test_vec0_parse_vector_column_rescore() { + printf("Starting %s...\n", __func__); + struct VectorColumnDefinition col; + int rc; + + // Basic bit quantizer + { + const char *input = "emb float[128] indexed by rescore(quantizer=bit)"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_OK); + assert(col.index_type == VEC0_INDEX_TYPE_RESCORE); + assert(col.rescore.quantizer_type == VEC0_RESCORE_QUANTIZER_BIT); + assert(col.rescore.oversample == 8); // default + assert(col.rescore.oversample_search == 0); + sqlite3_free(col.name); + } + + // Int8 quantizer + { + const char *input = "emb float[128] indexed by rescore(quantizer=int8)"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_OK); + assert(col.index_type == VEC0_INDEX_TYPE_RESCORE); + assert(col.rescore.quantizer_type == VEC0_RESCORE_QUANTIZER_INT8); + sqlite3_free(col.name); + } + + // Bit quantizer with oversample, in either order + { + const char *input = + "emb float[128] indexed by rescore(quantizer=bit, oversample=16)"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_OK); + assert(col.index_type == VEC0_INDEX_TYPE_RESCORE); + assert(col.rescore.quantizer_type == VEC0_RESCORE_QUANTIZER_BIT); + assert(col.rescore.oversample == 16); + sqlite3_free(col.name); + } + { + const char *input = + "emb float[128] indexed by rescore(oversample=128, quantizer=int8)"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_OK); + assert(col.rescore.quantizer_type == VEC0_RESCORE_QUANTIZER_INT8); + assert(col.rescore.oversample == 128); + sqlite3_free(col.name); + } + + // With distance_metric=cosine + { + const char *input = "emb float[128] distance_metric=cosine indexed by " + "rescore(quantizer=int8)"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_OK); + assert(col.index_type == VEC0_INDEX_TYPE_RESCORE); + assert(col.distance_metric == VEC0_DISTANCE_METRIC_COSINE); + assert(col.rescore.quantizer_type == VEC0_RESCORE_QUANTIZER_INT8); + sqlite3_free(col.name); + } + + const char *errors[] = { + // a non-float element type + "emb int8[128] indexed by rescore(quantizer=bit)", + "emb bit[128] indexed by rescore(quantizer=bit)", + // dimensions not divisible by 8 for the bit quantizer + "emb float[100] indexed by rescore(quantizer=bit)", + // no quantizer + "emb float[128] indexed by rescore(oversample=8)", + "emb float[128] indexed by rescore()", + "emb float[128] indexed by rescore", + // an unknown quantizer or option + "emb float[128] indexed by rescore(quantizer=float)", + "emb float[128] indexed by rescore(quantizer=bit, k=8)", + // oversample outside 1 to 128, or not an integer + "emb float[128] indexed by rescore(quantizer=bit, oversample=0)", + "emb float[128] indexed by rescore(quantizer=bit, oversample=129)", + "emb float[128] indexed by rescore(quantizer=bit, oversample=bit)", + // malformed options + "emb float[128] indexed by rescore(quantizer=bit", + "emb float[128] indexed by rescore(quantizer=bit,)", + "emb float[128] indexed by rescore(quantizer bit)", + "emb float[128] indexed by rescore(quantizer=bit oversample=8)", + }; + for (size_t i = 0; i < countof(errors); i++) { + rc = vec0_parse_vector_column(errors[i], (int)strlen(errors[i]), &col, + true); + assert(rc == SQLITE_ERROR); + } + + // Error: a prefix of a keyword, even when not strict + { + const char *input = "emb float[128] indexed by rescor(quantizer=bit)"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, false); + assert(rc == SQLITE_ERROR); + } + { + const char *input = "emb float[128] indexed by rescore(quant=bit)"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, false); + assert(rc == SQLITE_ERROR); + } + + printf(" All vec0_parse_vector_column_rescore tests passed.\n"); +} + +#endif /* SQLITE_VEC_ENABLE_RESCORE */ + +#if SQLITE_VEC_ENABLE_DISKANN + +void test_vec0_parse_vector_column_diskann() { + printf("Starting %s...\n", __func__); + struct VectorColumnDefinition col; + int rc; + + // No INDEXED BY: not DiskANN + { + const char *input = "emb float[128]"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_OK); + assert(col.index_type != VEC0_INDEX_TYPE_DISKANN); + sqlite3_free(col.name); + } + + // With distance_metric but no INDEXED BY + { + const char *input = "emb float[128] distance_metric=cosine"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_OK); + assert(col.index_type != VEC0_INDEX_TYPE_DISKANN); + assert(col.distance_metric == VEC0_DISTANCE_METRIC_COSINE); + sqlite3_free(col.name); + } + + // Binary quantizer, with every default + { + const char *input = + "emb float[128] INDEXED BY diskann(neighbor_quantizer=binary)"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_OK); + assert(col.index_type == VEC0_INDEX_TYPE_DISKANN); + assert(col.diskann.quantizer_type == VEC0_DISKANN_QUANTIZER_BINARY); + assert(col.diskann.n_neighbors == 72); + assert(col.diskann.search_list_size == 128); + assert(col.diskann.search_list_size_search == 0); + assert(col.diskann.search_list_size_insert == 0); + assert(col.diskann.buffer_threshold == 0); + assert(col.dimensions == 128); + sqlite3_free(col.name); + } + + // Int8 quantizer + { + const char *input = + "v float[64] INDEXED BY diskann(neighbor_quantizer=int8)"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_OK); + assert(col.index_type == VEC0_INDEX_TYPE_DISKANN); + assert(col.diskann.quantizer_type == VEC0_DISKANN_QUANTIZER_INT8); + sqlite3_free(col.name); + } + + // Custom n_neighbors, search_list_size and buffer_threshold + { + const char *input = "emb float[128] INDEXED BY " + "diskann(neighbor_quantizer=binary, n_neighbors=48, " + "search_list_size=256, buffer_threshold=100)"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_OK); + assert(col.diskann.n_neighbors == 48); + assert(col.diskann.search_list_size == 256); + assert(col.diskann.buffer_threshold == 100); + sqlite3_free(col.name); + } + + // Combined with distance_metric (distance_metric first) + { + const char *input = "emb float[128] distance_metric=cosine INDEXED BY " + "diskann(neighbor_quantizer=int8)"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_OK); + assert(col.distance_metric == VEC0_DISTANCE_METRIC_COSINE); + assert(col.index_type == VEC0_INDEX_TYPE_DISKANN); + assert(col.diskann.quantizer_type == VEC0_DISKANN_QUANTIZER_INT8); + sqlite3_free(col.name); + } + + // Keywords in any case + { + const char *input = + "emb float[128] indexed by DISKANN(NEIGHBOR_QUANTIZER=BINARY)"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_OK); + assert(col.index_type == VEC0_INDEX_TYPE_DISKANN); + assert(col.diskann.quantizer_type == VEC0_DISKANN_QUANTIZER_BINARY); + sqlite3_free(col.name); + } + + // Split search_list_size: search and insert + { + const char *input = + "emb float[128] INDEXED BY diskann(neighbor_quantizer=binary, " + "search_list_size_search=256, search_list_size_insert=64)"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_OK); + assert(col.diskann.search_list_size == 128); + assert(col.diskann.search_list_size_search == 256); + assert(col.diskann.search_list_size_insert == 64); + sqlite3_free(col.name); + } + + // Split search_list_size: only search + { + const char *input = "emb float[128] INDEXED BY " + "diskann(neighbor_quantizer=binary, " + "search_list_size_search=200)"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_OK); + assert(col.diskann.search_list_size_search == 200); + assert(col.diskann.search_list_size_insert == 0); + sqlite3_free(col.name); + } + + const char *errors[] = { + // neighbor_quantizer is required + "emb float[128] INDEXED BY diskann(n_neighbors=72)", + "emb float[128] INDEXED BY diskann()", + "emb float[128] INDEXED BY diskann(neighbor_quantizer=unknown)", + // n_neighbors: a multiple of 8 from 8 to 256 + "emb float[128] INDEXED BY diskann(neighbor_quantizer=binary, " + "n_neighbors=13)", + "emb float[128] INDEXED BY diskann(neighbor_quantizer=binary, " + "n_neighbors=512)", + "emb float[128] INDEXED BY diskann(neighbor_quantizer=binary, " + "n_neighbors=0)", + "emb float[128] INDEXED diskann(neighbor_quantizer=binary)", + "emb float[128] INDEXED BY hnsw(neighbor_quantizer=binary)", + "emb float[128] INDEXED BY diskann(neighbor_quantizer=binary, " + "foobar=baz)", + // search_list_size beside either of the split sizes + "emb float[128] INDEXED BY diskann(neighbor_quantizer=binary, " + "search_list_size=128, search_list_size_search=256)", + "emb float[128] INDEXED BY diskann(neighbor_quantizer=binary, " + "search_list_size=128, search_list_size_insert=64)", + // whole keywords and values, all digits, no trailing comma + "emb float[128] INDEXED BY diskann(neighbor_quantizer=bin)", + "emb float[128] INDEXED BY diskann(neighbor_quant=binary)", + "emb float[128] INDEXED BY diskann(neighbor_quantizer=binary, n=16)", + "emb float[128] INDEXED BY diskann(neighbor_quantizer=binary, " + "search_list_size=12x)", + "emb float[128] INDEXED BY diskann(neighbor_quantizer=binary,)", + "emb float[128] INDEXED BY diskann(neighbor_quantizer=binary, " + "buffer_threshold=99999999999)", + }; + for (size_t i = 0; i < countof(errors); i++) { + rc = vec0_parse_vector_column(errors[i], (int)strlen(errors[i]), &col, + true); + assert(rc == SQLITE_ERROR); + } + + printf(" All vec0_parse_vector_column_diskann tests passed.\n"); +} + +void test_diskann_validity_bitmap() { + printf("Starting %s...\n", __func__); + + unsigned char validity[3]; // 24 bits + memset(validity, 0, sizeof(validity)); + + for (int i = 0; i < 24; i++) { + assert(diskann_validity_get(validity, i) == 0); + } + assert(diskann_validity_count(validity, 24) == 0); + + diskann_validity_set(validity, 0, 1); + assert(diskann_validity_get(validity, 0) == 1); + assert(diskann_validity_count(validity, 24) == 1); + + // the last bit of the first byte and the first of the second + diskann_validity_set(validity, 7, 1); + diskann_validity_set(validity, 8, 1); + assert(diskann_validity_get(validity, 7) == 1); + assert(diskann_validity_get(validity, 8) == 1); + assert(diskann_validity_count(validity, 24) == 3); + + diskann_validity_set(validity, 23, 1); + assert(diskann_validity_get(validity, 23) == 1); + assert(diskann_validity_count(validity, 24) == 4); + + diskann_validity_set(validity, 0, 0); + assert(diskann_validity_get(validity, 0) == 0); + assert(diskann_validity_count(validity, 24) == 3); + assert(diskann_validity_get(validity, 7) == 1); + assert(diskann_validity_get(validity, 8) == 1); + + printf(" All diskann_validity_bitmap tests passed.\n"); +} + +void test_diskann_neighbor_ids() { + printf("Starting %s...\n", __func__); + + unsigned char ids[8 * 8]; // 8 slots of 8 bytes + memset(ids, 0, sizeof(ids)); + + diskann_neighbor_id_set(ids, 0, 42); + diskann_neighbor_id_set(ids, 3, 12345); + diskann_neighbor_id_set(ids, 7, 99999); + diskann_neighbor_id_set(ids, 1, INT64_MAX); + diskann_neighbor_id_set(ids, 2, -7); + assert(diskann_neighbor_id_get(ids, 0) == 42); + assert(diskann_neighbor_id_get(ids, 3) == 12345); + assert(diskann_neighbor_id_get(ids, 7) == 99999); + assert(diskann_neighbor_id_get(ids, 1) == INT64_MAX); + assert(diskann_neighbor_id_get(ids, 2) == -7); + + printf(" All diskann_neighbor_ids tests passed.\n"); +} + +void test_diskann_quantize_binary() { + printf("Starting %s...\n", __func__); + + // a bit per dimension, LSB first, set when the value is > 0 + { + float src[8] = {1.0f, -1.0f, 0.5f, 0.0f, -0.5f, 0.1f, -0.1f, 100.0f}; + unsigned char out[1]; + assert(diskann_quantize_vector(src, 8, VEC0_DISKANN_QUANTIZER_BINARY, + out) == SQLITE_OK); + assert(out[0] == 0xA5); + } + { + float src[16] = {1.0f, -1.0f, 0.5f, -0.5f, 0.1f, -0.1f, 0.0f, 100.0f, + -1.0f, 1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, -1.0f}; + unsigned char out[2]; + assert(diskann_quantize_vector(src, 16, VEC0_DISKANN_QUANTIZER_BINARY, + out) == SQLITE_OK); + assert(out[0] == 0x95); + assert(out[1] == 0x4E); + } + { + float src[8] = {1.0f, 2.0f, 0.1f, 0.001f, 100.0f, 42.0f, 0.5f, 3.14f}; + unsigned char out[1]; + diskann_quantize_vector(src, 8, VEC0_DISKANN_QUANTIZER_BINARY, out); + assert(out[0] == 0xFF); + } + { + float src[8] = {-1.0f, -2.0f, -0.1f, -0.001f, -100.0f, -42.0f, -0.5f, 0.0f}; + unsigned char out[1]; + diskann_quantize_vector(src, 8, VEC0_DISKANN_QUANTIZER_BINARY, out); + assert(out[0] == 0x00); + } + + printf(" All diskann_quantize_binary tests passed.\n"); +} + +void test_diskann_quantize_int8() { + printf("Starting %s...\n", __func__); + + // [-1, 1] maps onto [-128, 127] in steps of 2/255, truncating toward zero. + // 1.0 lands just below 127 in float arithmetic, so it truncates to 126. + { + float src[6] = {-1.0f, -0.5f, 0.0f, 0.5f, 1.0f, 1.004f}; + unsigned char out[6]; + assert(diskann_quantize_vector(src, 6, VEC0_DISKANN_QUANTIZER_INT8, out) == + SQLITE_OK); + int8_t *q = (int8_t *)out; + assert(q[0] == -128); + assert(q[1] == -64); + assert(q[2] == 0); + assert(q[3] == 63); + assert(q[4] == 126); + assert(q[5] == 127); + } + // values outside [-1, 1] clamp, and NaN clamps to 127 + { + float src[5] = {2.0f, -2.0f, INFINITY, -INFINITY, NAN}; + unsigned char out[5]; + diskann_quantize_vector(src, 5, VEC0_DISKANN_QUANTIZER_INT8, out); + int8_t *q = (int8_t *)out; + assert(q[0] == 127); + assert(q[1] == -128); + assert(q[2] == 127); + assert(q[3] == -128); + assert(q[4] == 127); + } + // the mapping never decreases + { + int8_t prev = -128; + for (int i = 0; i <= 2200; i++) { + float src = -1.1f + (float)i * 0.001f; + unsigned char out; + diskann_quantize_vector(&src, 1, VEC0_DISKANN_QUANTIZER_INT8, &out); + assert((int8_t)out >= prev); + prev = (int8_t)out; + } + } + + printf(" All diskann_quantize_int8 tests passed.\n"); +} + +void test_diskann_node_init_sizes() { + printf("Starting %s...\n", __func__); + + unsigned char *validity, *ids, *qvecs; + int validitySize, idsSize, qvecsSize; + + int rc = diskann_node_init(72, VEC0_DISKANN_QUANTIZER_BINARY, 1024, + &validity, &validitySize, &ids, &idsSize, &qvecs, + &qvecsSize); + assert(rc == SQLITE_OK); + assert(validitySize == 9); // 72 / 8 + assert(idsSize == 576); // 72 * 8 + assert(qvecsSize == 9216); // 72 * 1024 / 8 + assert(diskann_validity_count(validity, 72) == 0); + sqlite3_free(validity); + sqlite3_free(ids); + sqlite3_free(qvecs); + + rc = diskann_node_init(8, VEC0_DISKANN_QUANTIZER_INT8, 32, &validity, + &validitySize, &ids, &idsSize, &qvecs, &qvecsSize); + assert(rc == SQLITE_OK); + assert(validitySize == 1); + assert(idsSize == 64); + assert(qvecsSize == 256); + sqlite3_free(validity); + sqlite3_free(ids); + sqlite3_free(qvecs); + + assert(diskann_quantized_vector_byte_size(VEC0_DISKANN_QUANTIZER_BINARY, + 128) == 16); + assert(diskann_quantized_vector_byte_size(VEC0_DISKANN_QUANTIZER_BINARY, + 8) == 1); + assert(diskann_quantized_vector_byte_size(VEC0_DISKANN_QUANTIZER_INT8, + 768) == 768); + assert(diskann_neighbor_ids_byte_size(72) == 576); + assert(diskann_neighbor_qvecs_byte_size(8, VEC0_DISKANN_QUANTIZER_INT8, + 32) == 256); + + printf(" All diskann_node_init_sizes tests passed.\n"); +} + +void test_diskann_node_set_clear_neighbor() { + printf("Starting %s...\n", __func__); + + unsigned char *validity, *ids, *qvecs; + int validitySize, idsSize, qvecsSize; + int rc = diskann_node_init(8, VEC0_DISKANN_QUANTIZER_BINARY, 16, &validity, + &validitySize, &ids, &idsSize, &qvecs, + &qvecsSize); + assert(rc == SQLITE_OK); + + unsigned char test_qvec[2] = {0xAB, 0xCD}; + diskann_node_set_neighbor(validity, ids, qvecs, 3, 42, test_qvec, + VEC0_DISKANN_QUANTIZER_BINARY, 16); + assert(diskann_validity_get(validity, 3) == 1); + assert(diskann_validity_count(validity, 8) == 1); + assert(diskann_neighbor_id_get(ids, 3) == 42); + const unsigned char *read_qvec = + diskann_neighbor_qvec_get(qvecs, 3, VEC0_DISKANN_QUANTIZER_BINARY, 16); + assert(read_qvec[0] == 0xAB); + assert(read_qvec[1] == 0xCD); + + diskann_node_clear_neighbor(validity, ids, qvecs, 3, + VEC0_DISKANN_QUANTIZER_BINARY, 16); + assert(diskann_validity_get(validity, 3) == 0); + assert(diskann_neighbor_id_get(ids, 3) == 0); + assert(read_qvec[0] == 0 && read_qvec[1] == 0); + assert(diskann_validity_count(validity, 8) == 0); + + sqlite3_free(validity); + sqlite3_free(ids); + sqlite3_free(qvecs); + + printf(" All diskann_node_set_clear_neighbor tests passed.\n"); +} + +static float test_prune_matrix_distance(void *ctx, int a, int b) { + return ((const float *)ctx)[a * 5 + b]; +} + +void test_diskann_prune_select() { + printf("Starting %s...\n", __func__); + + // Five candidates in order of their distances to the node, A=1 to E=5, and + // the distances between them: + // A B C D E + // A 0.0 1.5 3.0 4.0 5.0 + // B 1.5 0.0 1.5 3.0 4.0 + // C 3.0 1.5 0.0 1.5 3.0 + // D 4.0 3.0 1.5 0.0 1.5 + // E 5.0 4.0 3.0 1.5 0.0 + float p_distances[5] = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f}; + float inter[25] = { + 0.0f, 1.5f, 3.0f, 4.0f, 5.0f, 1.5f, 0.0f, 1.5f, 3.0f, + 4.0f, 3.0f, 1.5f, 0.0f, 1.5f, 3.0f, 4.0f, 3.0f, 1.5f, + 0.0f, 1.5f, 5.0f, 4.0f, 3.0f, 1.5f, 0.0f, + }; + int selected[5]; + int count; + + // alpha=1: A covers every other candidate + { + int rc = diskann_prune_select(p_distances, 5, 1.0f, 3, + test_prune_matrix_distance, inter, selected, + &count); + assert(rc == SQLITE_OK); + assert(count == 1); + assert(selected[0] == 1); + } + + // alpha=1.5: A covers none; B covers C; D is the third + { + int rc = diskann_prune_select(p_distances, 5, 1.5f, 3, + test_prune_matrix_distance, inter, selected, + &count); + assert(rc == SQLITE_OK); + assert(count == 3); + assert(selected[0] == 1); + assert(selected[1] == 1); + assert(selected[2] == 0); + assert(selected[3] == 1); + assert(selected[4] == 0); + } + + // a large alpha covers nothing, and every candidate fits + { + int rc = diskann_prune_select(p_distances, 5, 100.0f, 10, + test_prune_matrix_distance, inter, selected, + &count); + assert(rc == SQLITE_OK); + assert(count == 5); + } + + // no candidate + { + int rc = diskann_prune_select(NULL, 0, 1.2f, 3, test_prune_matrix_distance, + inter, selected, &count); + assert(rc == SQLITE_OK); + assert(count == 0); + } + + printf(" All diskann_prune_select tests passed.\n"); +} + +void test_diskann_config_defaults() { + printf("Starting %s...\n", __func__); + + struct VectorColumnDefinition col; + memset(&col, 0, sizeof(col)); + assert(col.index_type != VEC0_INDEX_TYPE_DISKANN); + assert(col.diskann.n_neighbors == 0); + assert(col.diskann.search_list_size == 0); + + const char *input = "embedding float[768]"; + int rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_OK); + assert(col.index_type != VEC0_INDEX_TYPE_DISKANN); + sqlite3_free(col.name); + + printf(" All diskann_config_defaults tests passed.\n"); +} + +void test_diskann_candidate_list_operations() { + printf("Starting %s...\n", __func__); + + struct DiskannCandidateList list; + int rc = diskann_candidate_list_init(&list, 5); + assert(rc == SQLITE_OK); + + diskann_candidate_list_insert(&list, 10, 3.0f); + diskann_candidate_list_insert(&list, 20, 1.0f); + diskann_candidate_list_insert(&list, 30, 2.0f); + assert(list.count == 3); + assert(list.items[0].rowid == 20 && list.items[0].distance == 1.0f); + assert(list.items[1].rowid == 30 && list.items[1].distance == 2.0f); + assert(list.items[2].rowid == 10 && list.items[2].distance == 3.0f); + + // a rowid already held moves up to a nearer distance + diskann_candidate_list_insert(&list, 10, 0.5f); + assert(list.count == 3); + assert(list.items[0].rowid == 10 && list.items[0].distance == 0.5f); + // and keeps its place for a farther one + diskann_candidate_list_insert(&list, 10, 9.0f); + assert(list.items[0].rowid == 10 && list.items[0].distance == 0.5f); + + assert(diskann_candidate_list_next_unvisited(&list) == 0); + list.items[0].visited = 1; + assert(diskann_candidate_list_next_unvisited(&list) == 1); + + // at capacity a farther candidate is ignored, a nearer one drops the + // farthest + diskann_candidate_list_insert(&list, 40, 4.0f); + diskann_candidate_list_insert(&list, 50, 5.0f); + assert(list.count == 5); + assert(diskann_candidate_list_insert(&list, 60, 10.0f) == 0); + assert(list.count == 5); + assert(diskann_candidate_list_insert(&list, 60, 3.5f) == 1); + assert(list.count == 5); + assert(list.items[4].rowid == 40); + + // re-ranking by an exact distance moves a candidate either way + // (list: 10 0.5, 20 1.0, 30 2.0, 60 3.5, 40 4.0) + int at = diskann_candidate_list_rerank(&list, 0, 3.0f); + assert(at == 2); + assert(list.items[2].rowid == 10 && list.items[2].visited == 1); + assert(list.items[0].rowid == 20 && list.items[1].rowid == 30); + at = diskann_candidate_list_rerank(&list, 4, 0.1f); + assert(at == 0); + assert(list.items[0].rowid == 40); + for (int i = 1; i < list.count; i++) { + assert(list.items[i - 1].distance <= list.items[i].distance); + } + + diskann_candidate_list_free(&list); + + printf(" All diskann_candidate_list_operations tests passed.\n"); +} + +void test_diskann_visited_set_operations() { + printf("Starting %s...\n", __func__); + + struct DiskannVisitedSet set; + int rc = diskann_visited_set_init(&set, 32); + assert(rc == SQLITE_OK); + int added; + + assert(diskann_visited_set_contains(&set, 1) == 0); + assert(diskann_visited_set_insert(&set, 42, &added) == SQLITE_OK); + assert(added == 1); + assert(diskann_visited_set_contains(&set, 42) == 1); + assert(diskann_visited_set_contains(&set, 43) == 0); + diskann_visited_set_insert(&set, 42, &added); + assert(added == 0); + + // rowid 0 is a rowid like any other + assert(diskann_visited_set_contains(&set, 0) == 0); + diskann_visited_set_insert(&set, 0, &added); + assert(added == 1); + assert(diskann_visited_set_contains(&set, 0) == 1); + diskann_visited_set_insert(&set, 0, &added); + assert(added == 0); + + // far past its starting capacity it holds every rowid, negative ones too + for (long long r = -500; r <= 500; r++) { + assert(diskann_visited_set_insert(&set, r * 7919, &added) == SQLITE_OK); + } + assert(set.capacity > 1001 * 2); + for (long long r = -500; r <= 500; r++) { + assert(diskann_visited_set_contains(&set, r * 7919) == 1); + assert(diskann_visited_set_contains(&set, r * 7919 + 1) == (r * 7919 + 1 == 42)); + } + + diskann_visited_set_free(&set); + + printf(" All diskann_visited_set_operations tests passed.\n"); +} + +#endif /* SQLITE_VEC_ENABLE_DISKANN */ + +#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE + +void test_vec0_parse_vector_column_ivf() { + printf("Starting %s...\n", __func__); + struct VectorColumnDefinition col; + int rc; + + // Every default + { + const char *input = "v float[4] indexed by ivf()"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_OK); + assert(col.index_type == VEC0_INDEX_TYPE_IVF); + assert(col.dimensions == 4); + assert(col.ivf.nlist == 128); + assert(col.ivf.nprobe == 10); + assert(col.ivf.quantizer == VEC0_IVF_QUANTIZER_NONE); + assert(col.ivf.oversample == 1); + sqlite3_free(col.name); + } + + // nprobe defaults to at most nlist + { + const char *input = "v float[4] indexed by ivf(nlist=8)"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_OK); + assert(col.ivf.nlist == 8); + assert(col.ivf.nprobe == 8); + sqlite3_free(col.name); + } + + { + const char *input = "v float[4] indexed by ivf(nlist=64, nprobe=8)"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_OK); + assert(col.ivf.nlist == 64); + assert(col.ivf.nprobe == 8); + sqlite3_free(col.name); + } + + // distance_metric before indexed by + { + const char *input = + "v float[4] distance_metric=cosine indexed by ivf(nlist=16)"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_OK); + assert(col.index_type == VEC0_INDEX_TYPE_IVF); + assert(col.distance_metric == VEC0_DISTANCE_METRIC_COSINE); + assert(col.ivf.nlist == 16); + sqlite3_free(col.name); + } + + // nlist=0 leaves the centroids to set-centroid + { + const char *input = "v float[4] indexed by ivf(nlist=0)"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_OK); + assert(col.ivf.nlist == 0); + sqlite3_free(col.name); + } + + // nlist at its maximum + { + const char *input = "v float[4] indexed by ivf(nlist=65536)"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_OK); + assert(col.ivf.nlist == 65536); + sqlite3_free(col.name); + } + + // Quantizers + { + const char *input = + "v float[768] indexed by ivf(nlist=128, quantizer=binary)"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_OK); + assert(col.ivf.nlist == 128); + assert(col.ivf.quantizer == VEC0_IVF_QUANTIZER_BINARY); + assert(col.ivf.oversample == 1); + sqlite3_free(col.name); + } + + { + const char *input = "v float[768] indexed by ivf(nlist=64, quantizer=int8)"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_OK); + assert(col.ivf.quantizer == VEC0_IVF_QUANTIZER_INT8); + sqlite3_free(col.name); + } + + { + const char *input = "v float[768] indexed by ivf(quantizer=none)"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_OK); + assert(col.ivf.quantizer == VEC0_IVF_QUANTIZER_NONE); + sqlite3_free(col.name); + } + + // oversample with a quantizer + { + const char *input = "v float[768] indexed by ivf(nlist=128, " + "quantizer=binary, oversample=10)"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_OK); + assert(col.ivf.quantizer == VEC0_IVF_QUANTIZER_BINARY); + assert(col.ivf.oversample == 10); + assert(col.ivf.nlist == 128); + sqlite3_free(col.name); + } + + // Every option, keywords in any case + { + const char *input = "v float[768] distance_metric=cosine INDEXED BY " + "IVF(NLIST=256, NPROBE=16, QUANTIZER=INT8, " + "OVERSAMPLE=4)"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_OK); + assert(col.distance_metric == VEC0_DISTANCE_METRIC_COSINE); + assert(col.ivf.nlist == 256); + assert(col.ivf.nprobe == 16); + assert(col.ivf.quantizer == VEC0_IVF_QUANTIZER_INT8); + assert(col.ivf.oversample == 4); + sqlite3_free(col.name); + } + + { + const char *input = + "v float[768] indexed by ivf(quantizer=binary, oversample=5)"; + rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_OK); + assert(col.ivf.nlist == 128); + assert(col.ivf.nprobe == 10); + assert(col.ivf.quantizer == VEC0_IVF_QUANTIZER_BINARY); + assert(col.ivf.oversample == 5); + sqlite3_free(col.name); + } + + const char *errors[] = { + // an explicit nprobe past nlist + "v float[4] indexed by ivf(nlist=4, nprobe=10)", + "v float[4] indexed by ivf(bogus=1)", + "v float[4] indexed by hnsw()", + "v float[4] indexed by ivf(nlist=65537)", + "v float[4] indexed by ivf(nlist=-1)", + "v float[4] indexed by ivf(nprobe=0)", + // oversample only ranks a quantizer's candidates + "v float[768] indexed by ivf(oversample=10)", + "v float[768] indexed by ivf(quantizer=binary, oversample=0)", + "v float[768] indexed by ivf(quantizer=pq)", + // the binary quantizer packs 8 dimensions to a byte + "v float[12] indexed by ivf(quantizer=binary)", + // only float32 columns + "v int8[8] indexed by ivf()", + "v bit[8] indexed by ivf()", + // whole keywords and values, all digits, no trailing comma + "v float[8] indexed by ivf(quantizer=bin)", + "v float[8] indexed by ivf(nli=8)", + "v float[8] indexed by ivf(nlist=8x)", + "v float[8] indexed by ivf(nlist=8,)", + "v float[8] indexed by ivf(quantizer=int8, oversample=99999999999)", + "v float[8] indexed by ivf", + "v float[8] indexed by ivf(", + }; + for (size_t i = 0; i < countof(errors); i++) { + rc = vec0_parse_vector_column(errors[i], (int)strlen(errors[i]), &col, + true); + assert(rc == SQLITE_ERROR); + } + + printf(" All vec0_parse_vector_column_ivf tests passed.\n"); +} + +void test_ivf_quantize_int8() { + printf("Starting %s...\n", __func__); + + // Values in [-1, 1] scale to [-127, 127], truncating toward zero + { + float src[] = {0.0f, 1.0f, -1.0f, 0.5f}; + int8_t dst[4]; + ivf_quantize_int8(src, dst, 4); + assert(dst[0] == 0); + assert(dst[1] == 127); + assert(dst[2] == -127); + assert(dst[3] == 63); + } + + // Values past [-1, 1] clamp, and NaN is 127 + { + float src[] = {2.0f, -3.0f, 100.0f, -0.01f, NAN, INFINITY, -INFINITY}; + int8_t dst[7]; + ivf_quantize_int8(src, dst, 7); + assert(dst[0] == 127); + assert(dst[1] == -127); + assert(dst[2] == 127); + assert(dst[3] == (int8_t)(-0.01f * 127.0f)); + assert(dst[4] == 127); + assert(dst[5] == 127); + assert(dst[6] == -127); + } + + { + float src[] = {0.0f, -0.0f, 0.75f}; + int8_t dst[3]; + ivf_quantize_int8(src, dst, 3); + assert(dst[0] == 0); + assert(dst[1] == 0); + assert(dst[2] == (int8_t)(0.75f * 127.0f)); + } + + printf(" All ivf_quantize_int8 tests passed.\n"); +} + +void test_ivf_quantize_binary() { + printf("Starting %s...\n", __func__); + + // A value above 0 sets its bit, the first value in the lowest bit + { + float src[] = {1.0f, -1.0f, 0.5f, -0.5f, 0.0f, 0.1f, -0.1f, 2.0f}; + uint8_t dst[1]; + ivf_quantize_binary(src, dst, 8); + assert(dst[0] == 0xA5); + } + + { + float src[] = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f}; + uint8_t dst[1]; + ivf_quantize_binary(src, dst, 8); + assert(dst[0] == 0xFF); + } + + { + float src[] = {-1.0f, -2.0f, 0.0f, -0.0f, -5.0f, -6.0f, -7.0f, NAN}; + uint8_t dst[1] = {0xFF}; + ivf_quantize_binary(src, dst, 8); + assert(dst[0] == 0x00); + } + + { + float src[16]; + for (int i = 0; i < 16; i++) { + src[i] = (i % 2 == 0) ? 1.0f : -1.0f; + } + uint8_t dst[2]; + ivf_quantize_binary(src, dst, 16); + assert(dst[0] == 0x55); + assert(dst[1] == 0x55); + } + + printf(" All ivf_quantize_binary tests passed.\n"); +} + +void test_ivf_kmeans() { + printf("Starting %s...\n", __func__); + + // Three clusters of 2-dimensional points around (0, 0), (10, 0), (0, 10) + float vectors[30 * 2]; + for (int i = 0; i < 30; i++) { + float offset = (float)(i % 10) / 10.0f; + vectors[i * 2] = (i / 10 == 1 ? 10.0f : 0.0f) + offset; + vectors[i * 2 + 1] = (i / 10 == 2 ? 10.0f : 0.0f) - offset; + } + + float centroids[3 * 2]; + assert(ivf_kmeans(vectors, 30, 2, 3, 25, 0, centroids) == SQLITE_OK); + int found[3] = {0, 0, 0}; + for (int c = 0; c < 3; c++) { + float x = centroids[c * 2], y = centroids[c * 2 + 1]; + // each cluster's mean is its corner moved by (0.45, -0.45) + if (fabsf(x - 0.45f) < 1e-4 && fabsf(y + 0.45f) < 1e-4) { + found[0]++; + } else if (fabsf(x - 10.45f) < 1e-4 && fabsf(y + 0.45f) < 1e-4) { + found[1]++; + } else if (fabsf(x - 0.45f) < 1e-4 && fabsf(y - 9.55f) < 1e-4) { + found[2]++; + } + } + assert(found[0] == 1 && found[1] == 1 && found[2] == 1); + + // The same seed gives the same centroids + { + float a[3 * 2], b[3 * 2]; + assert(ivf_kmeans(vectors, 30, 2, 3, 25, 7, a) == SQLITE_OK); + assert(ivf_kmeans(vectors, 30, 2, 3, 25, 7, b) == SQLITE_OK); + assert(memcmp(a, b, sizeof(a)) == 0); + } + + // As many centroids as vectors: each vector is one + { + float few[3 * 2] = {1.0f, 1.0f, 5.0f, 5.0f, 9.0f, 9.0f}; + float out[3 * 2]; + assert(ivf_kmeans(few, 3, 2, 3, 25, 0, out) == SQLITE_OK); + for (int v = 0; v < 3; v++) { + int matches = 0; + for (int c = 0; c < 3; c++) { + matches += out[c * 2] == few[v * 2] && out[c * 2 + 1] == few[v * 2 + 1]; + } + assert(matches == 1); + } + } + + // More centroids than vectors, or none + assert(ivf_kmeans(vectors, 2, 2, 3, 25, 0, centroids) == SQLITE_MISUSE); + assert(ivf_kmeans(vectors, 30, 2, 0, 25, 0, centroids) == SQLITE_MISUSE); + assert(ivf_kmeans(vectors, 0, 2, 1, 25, 0, centroids) == SQLITE_MISUSE); + + printf(" All ivf_kmeans tests passed.\n"); +} + +#else + +void test_vec0_parse_vector_column_ivf() { + printf("Starting %s...\n", __func__); + struct VectorColumnDefinition col; + // a build without the IVF index has no ivf() to parse + const char *input = "v float[4] indexed by ivf()"; + int rc = vec0_parse_vector_column(input, (int)strlen(input), &col, true); + assert(rc == SQLITE_ERROR); + printf(" All vec0_parse_vector_column_ivf tests passed.\n"); +} + +#endif /* SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE */ + int main() { printf("Starting unit tests...\n"); test_vec0_token_next(); @@ -502,5 +1706,30 @@ int main() { test_vec0_parse_vector_column(); test_vec0_parse_partition_key_definition(); test_vec0_fullscan_schema_rollback(); +#if SQLITE_VEC_ENABLE_RESCORE + test_rescore_quantize_float_to_bit(); + test_rescore_quantize_float_to_int8(); + test_rescore_quantized_byte_size(); + test_vec0_parse_vector_column_rescore(); +#endif +#if SQLITE_VEC_ENABLE_DISKANN + test_vec0_parse_vector_column_diskann(); + test_diskann_validity_bitmap(); + test_diskann_neighbor_ids(); + test_diskann_quantize_binary(); + test_diskann_quantize_int8(); + test_diskann_node_init_sizes(); + test_diskann_node_set_clear_neighbor(); + test_diskann_prune_select(); + test_diskann_config_defaults(); + test_diskann_candidate_list_operations(); + test_diskann_visited_set_operations(); +#endif + test_vec0_parse_vector_column_ivf(); +#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE + test_ivf_quantize_int8(); + test_ivf_quantize_binary(); + test_ivf_kmeans(); +#endif printf("All unit tests passed.\n"); }