Repository navigation
Conversation
3c6d1a5 to
f7501fb
Compare
… SQLite does A KNN query converted each filter value to the column's type, so the same WHERE clause kept different rows with and without a `match` (`n = 'abc'` matched n = 0, and `t < NULL` on a long text value crashed), and `rowid = ?` bound to NULL or 'abc' selected, updated, or deleted rowid 0. Ported from the unmerged vlasky#12 (asg017#329).
7c0c94b to
43cf71c
Compare
vec0 checks a KNN query's constraints itself and sets aConstraintUsage[].omit, which tells SQLite a constraint "is assumed to be fully handled by the virtual table and might not be checked again" (sqlite3.h). Each constraint vec0 claims therefore has to keep exactly the rows SQLite would keep for the same WHERE clause. They did not: - `n = ?` bound to NULL or 'abc' kept n = 0, `n = 5.5` kept n = 5, `n < 5.5` dropped n = 5, and `n in (5.5, 6)` kept n = 5. - `f > 'abc'` kept every positive f, though every number is less than any text. - `b = 2` kept the true rows. - `t in (null)` kept t = '', and `t = x'78'` kept t = 'x'. - `t < ?` bound to NULL crashed (a NULL pointer read) once a chunk held a text longer than 12 bytes, and <, <=, > and >= failed with "Could not filter metadata fields" once a chunk held a text of exactly 12 bytes. The prefix bounds SQLite derives from `t LIKE 'abc%'` and `t GLOB 'abc*'` are such comparisons, so those failed too. - Text comparisons ignored the query's collation: `t = 'abc' COLLATE NOCASE` missed 'ABC', and `COLLATE RTRIM` missed 'abc '. - LIKE matched with SQLite's default case folding and claimed the result, so under PRAGMA case_sensitive_like `t LIKE '%c'` kept 'ABC'. GLOB read a prefix pattern with a character class literally (`[aA]*` matched nothing), and both read past the end of a 12-byte text. - A partition key compared as stored: `p = '5'` missed p = 5, `s = 5` missed s = '5', and collations were ignored. - `rowid in (5.5, 'abc', null)` matched rowids 5 and 0, and a point lookup `rowid = ?` matched rowid 0 for NULL, 'abc' and a blob, and rowid 5 for 5.5. With a text primary key, an id in `id in (...)` that names no row emptied the whole result. - `distance > ?` bound to NULL kept every row and `distance < 'abc'` none, and distances compared in f32 kept the row at distance 2 for `distance <= 1.999999999999`, which SQLite drops. vec0 now follows SQLite's R-Tree, the extension that checks its own constraints: - It declares its columns with their types, as rtreeInit() does: metadata columns boolean, integer, float or text, partition keys and a text primary key with theirs, rowid integer and distance real. So SQLite's own checks give a value the affinity vec0 gives it. - Each filter gives the constraint's value its column's affinity and compares by https://www.sqlite.org/datatype3.html#comparison_expressions: NULL compares with nothing, INTEGER and REAL compare exactly, as sqlite3IntFloatCompare() does, every number sorts before any text, and every text before any blob. A rowid lookup takes the value as rtreeFilter() does: numeric affinity, then an integer or a real equal to one. - Text compares under the constraint's collation, from sqlite3_vtab_collation(): BINARY, NOCASE and RTRIM as binCollFunc, nocaseCollatingFunc and rtrimCollFunc do, and any other collation is refused with an error. SQLite names no collation for != or IS NOT, so vec0 keeps the rows unequal under BINARY and leaves those two for SQLite to check again. SQLite orders text under BINARY by the bytes of the database's encoding, so in a UTF-16 database vec0 compares the bytes SQLite stores for each value, converting its UTF-8 with SQLite's own READ_UTF8, WRITE_UTF16LE and WRITE_UTF16BE (src/utf.c), and orders a text longer than its 12-byte prefix by its full text. vec0 reads the encoding when it opens a table, from sqlite3_value_encoding() of a string literal on SQLite 3.41 and later, which needs no PRAGMA an authorizer might deny, and from PRAGMA encoding before that, where no row means UTF-8 only in a build with SQLITE_OMIT_UTF16. A read that fails fails the constructor with its error, and an interrupted one reports as an interrupt. This read follows photostructure/sqlite-vec@8156329 by Matthew McEachen, which corrected it after this change merged there as photostructure#4. - LIKE and GLOB match as SQLite's built-in functions do and are left for SQLite to check again, as FTS5 leaves them, so the rows returned satisfy the connection's own like() and glob(). - A distance compares as the double SQLite receives. The tests compare each filter with a plain table declaring the same types and with SQLite's own scan of the vtab: every operator over NULL, integers, reals, numeric and other text and blobs, texts around the 12 bytes a chunk keeps inline, each collation, LIKE and GLOB patterns (also under case_sensitive_like), IN lists, partition keys, rowid and text id lookups, distances a hair either side of a row's, and UTF-16 databases, where every filter and literal LIKE and GLOB pattern is compared at each k and with LIMIT. A KNN query whose k is the number of matching rows must return all of them, so a filter that keeps any other row fails. A connection whose authorizer denies PRAGMAs opens the table and keeps its text range filters, as on main, and creating a table fails when the encoding cannot be read. The tests fail on main and pass here. The two LIKE and GLOB boundary snapshots recorded the 12-byte error and now record the rows. The full suite passes (151 passed, 4 skipped, 259 snapshots), also with vec0 built with AddressSanitizer and UBSan, and make test-unit passes.
43cf71c to
5308584
Compare
|
Thanks for the update, and for the UTF-16 comparator: it is the right fix (I had found that SQLite's BINARY in a UTF-16LE database is a memcmp over little-endian bytes, so
Full suite: 152 passed, 4 skipped, 259 snapshots; Two things I left alone for your judgement: |
vec0 checks a KNN query's constraints itself and sets
aConstraintUsage[].omit, which tells SQLite a constraint "is assumed to be fully handled by the virtual table and might not be checked again" (sqlite3.h). Each constraint vec0 claims therefore has to keep exactly the rows SQLite would keep for the same WHERE clause, and today the same WHERE clause keeps different rows with and without amatch:On main:
n = ?bound to NULL or'abc'keeps n = 0,n = 5.5keeps n = 5,n < 5.5drops n = 5, andn in (5.5, 6)keeps n = 5.f > 'abc'keeps every positive f, though every number is less than any text.b = 2keeps the true rows.t in (null)keeps t = '', andt = x'78'keeps t = 'x'.t < ?bound to NULL crashes (a NULL pointer read) once a chunk holds a text longer than 12 bytes, and<,<=,>and>=fail with "Could not filter metadata fields" once a chunk holds a text of exactly 12 bytes. The prefix bounds SQLite derives fromt LIKE 'abc%'andt GLOB 'abc*'are such comparisons, so those fail too; the LIKE and GLOB boundary snapshots recorded that error.t = 'abc' COLLATE NOCASEmisses 'ABC', andCOLLATE RTRIMmisses 'abc '.PRAGMA case_sensitive_liket LIKE '%c'keeps 'ABC'. GLOB reads a prefix pattern with a character class literally ([aA]*matches nothing), and both read past the end of a 12-byte text.p = '5'misses p = 5,s = 5misses s = '5', and collations are ignored.rowid in (5.5, 'abc', null)matches rowids 5 and 0, and a point lookuprowid = ?matches rowid 0 for NULL,'abc'and a blob, and rowid 5 for 5.5. With a text primary key, an id inid in (...)that names no row empties the whole result.distance > ?bound to NULL keeps every row anddistance < 'abc'none, and distances compared in f32 keep the row at distance 2 fordistance <= 1.999999999999, which SQLite drops.This follows SQLite's R-Tree, the extension that checks its own constraints:
rtreeInit()does: metadata columnsboolean,integer,floatortext, partition keys and a text primary key with theirs,rowid integeranddistance real. SQLite's own checks then give a value the affinity vec0 gives it.sqlite3IntFloatCompare()does, every number sorts before any text, and every text before any blob. A rowid lookup takes the value asrtreeFilter()does: numeric affinity, then an integer or a real equal to one.sqlite3_vtab_collation(): BINARY, NOCASE and RTRIM asbinCollFunc,nocaseCollatingFuncandrtrimCollFuncdo, and any other collation is refused with an error. SQLite names no collation for!=orIS NOT(it hands them over without their left operand), so vec0 keeps the rows unequal under BINARY and leaves those two for SQLite to check again. SQLite orders text under BINARY by the bytes of the database's encoding, so in a UTF-16 database vec0 compares the bytes SQLite stores for each value, converting its UTF-8 with SQLite's ownREAD_UTF8,WRITE_UTF16LEandWRITE_UTF16BE(utf.c), and orders a text longer than its 12-byte prefix by its full text, since a prefix can end inside a character. vec0 reads the encoding when it opens a table, fromsqlite3_value_encoding()of a string literal on SQLite 3.41 and later, which needs no PRAGMA an authorizer might deny, and fromPRAGMA encodingbefore that, where no row means UTF-8 only in a build withSQLITE_OMIT_UTF16. A read that fails fails the constructor with its error, and an interrupted one reports as an interrupt. This read follows photostructure/sqlite-vec@8156329 by Matthew McEachen, which corrected it after this change merged there as Port the rest of vlasky#12: collated text partition keys and UTF-16 text ranges photostructure/sqlite-vec#4.fts5BestIndexMethod()), so the rows returned satisfy the connection's ownlike()andglob().The tests compare each filter with a plain table declaring the same types and with SQLite's own scan of the vtab: every operator over NULL, integers, reals, numeric and other text and blobs, texts around the 12 bytes a chunk keeps inline, each collation, LIKE and GLOB patterns (also under
case_sensitive_like),in (...)lists, partition keys, rowid and text id lookups, distances a hair either side of a row's, and UTF-16 databases, where every filter and literalLIKEandGLOBpattern is compared at eachkand withLIMIT. A KNN query whosekis the number of matching rows must return all of them, so a filter that keeps any other row fails. A connection whose authorizer denies PRAGMAs opens the table and keeps its text range filters, as on main, and creating a table fails when the encoding cannot be read. The tests fail on main and pass here. The full suite passes (151 passed, 4 skipped, 259 snapshots), also with vec0 built with AddressSanitizer and UBSan, andmake test-unitpasses.The same change is open upstream as asg017#329.