Repository navigation
Port the rest of vlasky#12: collated text partition keys and UTF-16 text ranges - #4
Conversation
ec07dc1 to
71a5221
Compare
|
About #2, #3, #5 and #6 showing as closed rather than merged: main requires linear history and verified signatures, and your commits weren't signed, so I re-committed them onto main with my signature. You're still the author of each (5fb0b3d, 050bb59, 01c827e, b02677e, 8ffbdf1). If you sign your commits with a key registered on your GitHub account and rebase this PR onto main, it can land as your own commits and show as merged. |
|
Thanks. Reviewing 1c9ebd8 and 71a5221 against main turned up five things, each reproduced against main's build and a plain table:
vlasky#12 has the same |
…he query's collation
vec0 tells SQLite not to check again the KNN constraints it claims
(aConstraintUsage[].omit), so each has to keep the rows SQLite would
keep. An =, <, <=, > or >= on a text partition key compared bytewise
whatever collation the query named, so `p = 'alice' collate nocase`
missed the partition keyed 'Alice'.
The _chunks query now compares under the BINARY, NOCASE or RTRIM
collation sqlite3_vtab_collation() reports, carried in the fourth
character of the idxStr block as metadata constraints carry theirs, and
any other collation fails the query as it does for a metadata column.
SQLite reports BINARY for != whatever collation the query names, so a
!= on a text key is checked again by SQLite, as IS NOT already is.
An IN that names NOCASE or RTRIM, which vec0 runs as one = per value,
compares each value under it too, so `p collate nocase in ('alice',
'bob')` also searches the partition keyed 'Alice'. The range conditions
SQLite passes for a LIKE ... ESCAPE pattern that begins with literal
text carry NOCASE unless case_sensitive_like is on, so they no longer
drop keys that differ from the pattern only in case.
test_knn_partition_comparisons_with_collation,
test_knn_partition_comparison_with_other_collation_fails,
test_knn_partition_in_with_collation_compares_each_value_under_it and
test_knn_partition_like_escape_with_literal_prefix in
tests/test-partition-keys.py fail on main and pass with this change.
…te does vec0 tells SQLite not to check again the KNN constraints it claims, so each has to keep the rows SQLite would keep. vec0 keeps text metadata as UTF-8 and compared it as UTF-8, but SQLite orders text under BINARY by the bytes of the database's encoding, and UTF-16 orders some characters differently (U+FFFD before U+1F600 in UTF-8, after it in UTF-16; and in UTF-16LE 'Ā' before 'a'), so in a UTF-16 database a KNN <, <=, > or >= on a text column, and the range conditions SQLite derives from a literal GLOB or LIKE prefix, kept rows a plain scan drops and dropped rows it keeps. vec0_init() reads the database's encoding with PRAGMA encoding, and in a UTF-16 database vec0_text_cmp() compares under BINARY by the bytes SQLite stores for each value, converting its UTF-8 with SQLite's own READ_UTF8, WRITE_UTF16LE and WRITE_UTF16BE (https://sqlite.org/src/file/src/utf.c). SQLite registers NOCASE and RTRIM for UTF-8 only (openDatabase() in https://sqlite.org/src/file/src/main.c), so filters under those two compare UTF-8 in every encoding. A PRAGMA encoding that cannot run, as under an authorizer that denies SQLITE_PRAGMA, fails the constructor with its error, and an interrupted one reports as an interrupt; one that returns no row, as in a build with SQLITE_OMIT_UTF16, means UTF-8. test_knn_text_filters_in_utf16_database in tests/test-metadata.py, which compares every filter with a plain table's at each k and with LIMIT in UTF-16LE and UTF-16BE databases, and test_knn_fails_to_open_when_encoding_is_unreadable fail on main and pass with this change.
71a5221 to
30ec839
Compare
|
Both commits are revised, rebased onto main at f6a3244, and signed:
vlasky#12 has the same two fixes; there a long text in a UTF-16 database is also ordered by its full text rather than by the 12-byte prefix its chunk keeps, since that prefix can end inside a character. #8, #11 and #12 are rebased onto f6a3244 and signed as well. |
… on SQLite 3.41+ #4 read the encoding only with PRAGMA encoding, so a connection whose authorizer denies PRAGMAs could no longer open a vec0 table, and one that ignores them made a UTF-16 database look like UTF-8.
|
Merged, thanks. Two follow-ups: 8156329 reads the encoding from |
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.
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. - Text comparisons ignored the query's collation: `t = 'abc' COLLATE NOCASE` missed 'ABC', and `COLLATE RTRIM` missed 'abc '. - 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 (the TODO beside that cast) 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 !=, so vec0 keeps the rows unequal under BINARY and leaves != 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. - 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, 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 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 full suite passes (247 passed, 66 skipped, 204 snapshots), also with vec0 built with AddressSanitizer and UBSan. make test-unit does not build on main (tests/sqlite-vec-internal.h redeclares the rescore types), and this change adds no error to it.
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. - Text comparisons ignored the query's collation: `t = 'abc' COLLATE NOCASE` missed 'ABC', and `COLLATE RTRIM` missed 'abc '. - 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 (the TODO beside that cast) 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 !=, so vec0 keeps the rows unequal under BINARY and leaves != 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. - 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, 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 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 full suite passes (247 passed, 66 skipped, 204 snapshots), also with vec0 built with AddressSanitizer and UBSan. make test-unit does not build on main (tests/sqlite-vec-internal.h redeclares the rescore types), and this change adds no error to it.
|
8156329 is now in both twins, vlasky#12 and asg017#329, with its two tests; each fails on the previous head of its PR and passes now, and the bodies credit your commit. 08a881c corrects my reply above: I had checked the literal-prefix |
2.1.0 ported most of vlasky#12 (asg017#329). Two of its cases are still missing on main. Both are KNN constraints that
vec0claims (aConstraintUsage[].omit), so SQLite never checks them again, whilevec0keeps rows SQLite would not. One commit each.Collated text partition keys. An
=,<,<=,>or>=on atextpartition key compares bytewise whatever collation the query names:The
_chunksquery now compares under theBINARY,NOCASEorRTRIMcollationsqlite3_vtab_collation()reports. The collation travels in the fourth character of the idxStr block, as metadata constraints carry theirs. Any other collation fails the query, as it does on a metadata column. SQLite reportsBINARYfor!=whatever collation the query names, so on a text keyvec0leavesomitunset for!=and SQLite checks it again, as forIS NOT. AnINthat namesNOCASEorRTRIM, whichvec0runs as one=per value, compares each value under it too, soname collate nocase in ('alice', 'bob')also searches the partition keyed'Alice'; and the range conditions SQLite passes for aLIKE ... ESCAPEpattern that begins with literal text compare underNOCASEunlesscase_sensitive_likeis on, so such a query keeps keys that differ from the pattern only in case.UTF-16 text ranges.
vec0keeps text metadata as UTF-8, but SQLite orders text underBINARYby the bytes of the database's encoding. UTF-16 orders some characters differently: U+FFFD sorts before U+1F600 in UTF-8 and after it in UTF-16, and in UTF-16LE'Ā'sorts before'a'.vec0_init()reads the encoding withPRAGMA encoding, and in a UTF-16 databasevec0_text_cmp()compares underBINARYby the bytes SQLite stores for each value, converting the UTF-8vec0keeps with SQLite's ownREAD_UTF8,WRITE_UTF16LEandWRITE_UTF16BEfrom utf.c. So these filters, and the range conditions SQLite derives from a literalGLOBorLIKEprefix, stay withvec0and work withLIMIT. APRAGMA encodingthat cannot run, as under an authorizer that deniesSQLITE_PRAGMA, fails the table's constructor with its own error, and an interrupted one reports as an interrupt; a pragma that returns no row, as in a build withSQLITE_OMIT_UTF16, means UTF-8. SQLite registersBINARYin each encoding butNOCASEandRTRIMfor UTF-8 only (openDatabase()in main.c), so filters under those two order text as UTF-8 in every encoding.test_knn_partition_comparisons_with_collation,test_knn_partition_comparison_with_other_collation_fails,test_knn_partition_in_with_collation_compares_each_value_under_itandtest_knn_partition_like_escape_with_literal_prefixintests/test-partition-keys.py, andtest_knn_text_filters_in_utf16_databaseandtest_knn_fails_to_open_when_encoding_is_unreadableintests/test-metadata.py, fail on main and pass with these commits. The UTF-16 test compares every filter with a plain table's at eachkand withLIMITin UTF-16LE and UTF-16BE databases, with literalGLOBandLIKEpatterns under bothcase_sensitive_likesettings. ARCHITECTURE.md and site/features/vec0.md describe both, and each commit adds a### Changedentry under[Unreleased]in CHANGELOG.md, at the head of the list.It merges cleanly with main and with #8, #11 and #12.