Skip to content

Port the rest of vlasky#12: collated text partition keys and UTF-16 text ranges - #4

Merged
mceachen merged 2 commits into
photostructure:mainfrom
MayCXC:constraint-affinity-photostructure
Oct 24, 2026
Merged

mceachen merged 2 commits into
photostructure:mainfrom
MayCXC:constraint-affinity-photostructure

Conversation

@MayCXC

@MayCXC MayCXC commented Oct 22, 2026 •

Copy link
Copy Markdown

2.1.0 ported most of vlasky#12 (asg017#329). Two of its cases are still missing on main. Both are KNN constraints that vec0 claims (aConstraintUsage[].omit), so SQLite never checks them again, while vec0 keeps rows SQLite would not. One commit each.

Collated text partition keys. An =, <, <=, > or >= on a text partition key compares bytewise whatever collation the query names:

create virtual table v using vec0(name text partition key, e float[1]);
insert into v(rowid, name, e) values (1, 'Alice', '[1]');
select rowid from v where e match '[1]' and k = 1 and name = 'alice' collate nocase;  -- main: no row
select rowid from v where name = 'alice' collate nocase;                            -- 1

The _chunks query now compares under the BINARY, NOCASE or RTRIM collation sqlite3_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 reports BINARY for != whatever collation the query names, so on a text key vec0 leaves omit unset for != and SQLite checks it again, as for IS NOT. An IN that names NOCASE or RTRIM, which vec0 runs as one = per value, compares each value under it too, so name collate nocase in ('alice', 'bob') also searches the partition keyed 'Alice'; and the range conditions SQLite passes for a LIKE ... ESCAPE pattern that begins with literal text compare under NOCASE unless case_sensitive_like is on, so such a query keeps keys that differ from the pattern only in case.

UTF-16 text ranges. vec0 keeps text metadata as UTF-8, but SQLite orders text under BINARY by 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'.

pragma encoding = 'UTF-16le';
create virtual table v using vec0(e float[1], t text);
insert into v(rowid, e, t) values (1, '[1]', char(0xfffd)), (2, '[1]', char(0x1f600));
select rowid from v where e match '[1]' and k = 2 and t < char(0x1f600);  -- main: 1; a plain table: no row

vec0_init() reads the 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 the UTF-8 vec0 keeps with SQLite's own READ_UTF8, WRITE_UTF16LE and WRITE_UTF16BE from utf.c. So these filters, and the range conditions SQLite derives from a literal GLOB or LIKE prefix, stay with vec0 and work with LIMIT. A PRAGMA encoding that cannot run, as under an authorizer that denies SQLITE_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 with SQLITE_OMIT_UTF16, means UTF-8. SQLite registers BINARY in each encoding but NOCASE and RTRIM for 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_it and test_knn_partition_like_escape_with_literal_prefix in tests/test-partition-keys.py, and test_knn_text_filters_in_utf16_database and test_knn_fails_to_open_when_encoding_is_unreadable in tests/test-metadata.py, fail on main and pass with these commits. The UTF-16 test compares every filter with a plain table's at each k and with LIMIT in UTF-16LE and UTF-16BE databases, with literal GLOB and LIKE patterns under both case_sensitive_like settings. ARCHITECTURE.md and site/features/vec0.md describe both, and each commit adds a ### Changed entry under [Unreleased] in CHANGELOG.md, at the head of the list.

It merges cleanly with main and with #8, #11 and #12.

@mceachen

Copy link
Copy Markdown
Member

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.

@mceachen

Copy link
Copy Markdown
Member

Thanks. Reviewing 1c9ebd8 and 71a5221 against main turned up five things, each reproduced against main's build and a plain table:

  1. vec0_database_is_utf8() treats a failed or empty PRAGMA encoding as UTF-16. With an authorizer that denies SQLITE_PRAGMA, rows (1, '[1]', 'a') and (2, '[2]', 'z'), and select rowid from v where e match '[0]' and k = 1 and t > 'm', this branch returns nothing where main returns row 2, and the limit 1 form fails with the missing-k error. SQLite built with SQLITE_OMIT_UTF16 (SQLite's WASM glue defines it) returns no row from the pragma, so every database there is treated as UTF-16. No row can only mean UTF-8; a failed prepare or step should fail xConnect with its own error instead of choosing an encoding.
  2. In a UTF-16 database, the range terms SQLite adds for t glob 'abc*', and for t like 'abc%' under case_sensitive_like = on, are now left unconsumed, so … and t glob 'abc*' limit 3 fails with the missing-k error where main returned the plain table's rows. Either compare BINARY text in the database's UTF-16 order and keep consuming the ranges, or document that these need k = ?.
  3. Both CHANGELOG entries add errors (p = 'alice' collate reverse on a partition key; UTF-16 range queries with LIMIT), so they belong under ### Changed. The partition entry should also say a collated IN now compares each value under the collation.
  4. site/features/vec0.md still says name collate nocase in ('alice', 'Alice') searches only one partition (it now returns both), and that partition LIKE … ESCAPE ranges compare bytewise (they now compare under NOCASE).
  5. In tests/test-partition-keys.py, the opening comment of test_knn_partition_filters_with_collation, and the name and comment of test_knn_partition_in_with_collation_keeps_bytewise_matches, still describe bytewise comparison, and no test pins a collated IN matching a case variant such as 'ACME'.

vlasky#12 has the same vec0_database_is_utf8() and the same unconsumed ranges. Please sign the revised commits; then this can land as your own commits.

…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.
@MayCXC
MayCXC force-pushed the constraint-affinity-photostructure branch from 71a5221 to 30ec839 Compare October 24, 2026 02:19
@MayCXC

MayCXC commented Oct 24, 2026

Copy link
Copy Markdown
Author

Both commits are revised, rebased onto main at f6a3244, and signed:

  1. vec0_read_text_encoding() takes no row as UTF-8 and returns the pragma's own result code otherwise, and vec0_init() reads it before anything else. Under your authorizer the table fails to open with vec0 constructor error: could not read the database's text encoding: not authorized, and an interrupted read reports SQLITE_INTERRUPT with SQLite's interrupted; test_interrupt_is_reported_as_interrupt[knn-progress] caught that case, since a fresh connection opens the table under the progress handler. test_knn_fails_to_open_when_encoding_is_unreadable pins the authorizer case.
  2. I took the first option. In a UTF-16 database vec0_text_cmp() compares under BINARY by the bytes SQLite stores for each value, converting the UTF-8 vec0 keeps with SQLite's own READ_UTF8, WRITE_UTF16LE and WRITE_UTF16BE, so the range terms stay consumed and ... and t glob 'abc*' limit 3 returns the plain table's rows in both byte orders. test_knn_text_filters_in_utf16_database now gives each row its own vector and compares every filter with the plain table at each k and with LIMIT, and literal GLOB and LIKE patterns under both case_sensitive_like settings.
  3. Both entries are under ### Changed, at its head. The partition entry says a collated IN compares each value under the collation, and that the ranges of a literal-prefix LIKE ... ESCAPE compare under NOCASE.
  4. site/features/vec0.md says name collate nocase in ('alice', 'Alice') runs one = that searches both partitions, and that a literal-prefix LIKE ... ESCAPE on a partition key passes ranges that select the same rows and work with LIMIT, both checked against a plain table. The metadata section's UTF-16 item is gone, since vec0 keeps those filters.
  5. The opening comment of test_knn_partition_filters_with_collation no longer says bytewise. The IN test is now test_knn_partition_in_with_collation_compares_each_value_under_it, which pins 'ACME' under NOCASE and 'acme ' under RTRIM, and test_knn_partition_like_escape_with_literal_prefix pins the ESCAPE ranges with k and LIMIT.

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.

mceachen added a commit that referenced this pull request Oct 24, 2026
… 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.
@mceachen
mceachen merged commit 30ec839 into photostructure:main Oct 24, 2026
@mceachen

Copy link
Copy Markdown
Member

Merged, thanks. Two follow-ups: 8156329 reads the encoding from sqlite3_value_encoding() of a string literal on SQLite 3.41+, so a connection whose authorizer denies PRAGMAs still opens its vec0 tables as it did before; below 3.41 it keeps the PRAGMA, and takes no row as UTF-8 only when sqlite3_compileoption_used("OMIT_UTF16") confirms it, since an authorizer returning SQLITE_IGNORE also gives no row. 08a881c notes that a literal-prefix LIKE ... ESCAPE still needs k = ? in a UTF-16le database, where the LIMIT form fails as it did before.

MayCXC added a commit to MayCXC/sqlite-vec that referenced this pull request Oct 24, 2026
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.
MayCXC added a commit to MayCXC/sqlite-vec that referenced this pull request Oct 24, 2026
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.
MayCXC added a commit to MayCXC/sqlite-vec that referenced this pull request Oct 24, 2026
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.
@MayCXC

MayCXC commented Oct 24, 2026

Copy link
Copy Markdown
Author

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 LIKE ... ESCAPE ranges with LIMIT in a UTF-8 database only.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants