CREATE VIRTUAL TABLE t USING fts5(...) is built into the core (RecursiveAI.OneLite.Extensions.Fts5). Its shadow tables
(t_data, t_idx, t_content, t_docsize, t_config) have the layout the reference writes, so a database is read and
modified by both engines. Tokenizers (unicode61, ascii, porter, trigram) are internal code in
RecursiveAI.OneLite.Internal.Text, free of engine dependencies.
What is covered
- Table options:
content,content_rowid,contentless,columnsize,detail(full,column,none),prefix,tokenize,UNINDEXEDcolumns, hiddenrank. - Queries: phrases,
+,AND/OR/NOT, parentheses, column filters,^, prefix*,NEAR(...), table-valued formt('query'),col MATCH 'query'. - Auxiliary functions:
bm25(k1 1.2, b 0.75),highlight,snippet, also on a scan with noMATCH(bm25 is 0). - Special commands:
rebuild,optimize,merge,automerge,usermerge,crisismerge,pgsz,hashsize,secure-delete,deletemerge,insttoken,rank,integrity-check,delete,delete-all. Invalid values fail with the bareSQL logic error. fts5vocab(row,col,instance).- Index format: structure record, segments, doclists, position lists,
t_idxkeys,t_docsizevarints,t_config— written and read both ways.
Behaviour settled by running the pinned shell
- Unicode tables are Unicode 6.1 (the pinned build): unassigned code points are token characters except U+FFFE/U+FFFF; the marks in the
diacritic set (U+0300–0304, 0306–030C, 030F, 0311, 031B, 0323–0328, 032D–032E, 0330–0331) are token characters even with
remove_diacritics 0. - Porter works on UTF-8 bytes: digits and every byte of a multi-byte character are consonants, and nothing is skipped.
- detail=column/none:
phrase queries are not supported (detail!=full)for a multi-token phrase or^, `NEAR queries are not supported (detail!=full)forNEARwith two or more phrases,column queries are not supported (detail=none)` for a column filter (raised after the filtered primary parsed) and forcol MATCH(raised after the whole query parsed). Instances forbm25/highlight/snippetcome from tokenizing the row text, for every column including unindexed ones, and only for phrases that took part in the match.fts5vocabcounts are NULL without positions. - Empty phrases (a one-letter word under
trigram) are dropped from an implicit AND, fromOR,NEARand the right ofNOT; an explicitANDwith one matches nothing. - Syntax errors report the token the reference reports:
x (y)namesx,{a breports the unknown column before the missing brace, an unterminated string isunterminated string,* aisunknown special query: a,NEARis a word unless a(follows. - Contentless tables: the
'delete'command also drops thet_docsizerow; withoutt_docsize(columnsize=0) a scan ist: table does not support scanningandbm25runs with a column size of -1. - snippet (window of
ntokens,nclamped to 1..64): the best anchor is the instance whose window holds the most instances; the window starts atanchor - (n - span)/2clamped to the text, except that it starts at the latest sentence start before the first match that still reaches it, and at the text start when the first match lies in the firstntokens. A sentence start is a token whose preceding gap ends in.or:followed by white space (.,:,.; not.alone,?,!,;). A phrase that began before the window is not marked. A snippet that reaches the end of the text keeps the text after its last token; one starting at token 0 starts at the text start. Unindexed columns are returned whole. Forsnippet(t, -1, ...)the column is the one whose best anchor scores most, where an instance with no more thanntokens after it to the end of its text scores 0; ties keep the first column.
Known differences (open)
snippetwhen several matches lie near sentence starts and the span of the first match equals the window (a few percent of random differential cases), and the window around trigram phrases of different lengths.snippet()of an UNINDEXED column in adetail=columnordetail=nonetable: for some rows the reference shell fails with "database disk image is malformed" (SQLITE_CORRUPT) on files that pass its own integrity check; we return the snippet text. Seen twice in 1,450 random cases (seed 21).bm25/highlight/snippetquirks that are matched on purpose: when the left side of a NOT matches no row at all, the right side keeps its first row's instances (they show in that row if it matches another way); a segment recorded with first and last page 0 and no page 0 is empty; a combining mark continues a token but never starts one; a contentless table without size records reads each indexed column's size as -1.- The NOT quirk below is matched when the left side is empty because its terms are absent, or because the phrase never occurs; in one of 373 random cases (seed 41, a prefix on the right of the NOT, a two-token phrase on the left) the reference does not keep the right side's instances, and
highlightandbm25differ by one marked token or 1e-6. - Where the NOT quirk above applies,
highlightandsnippetshow the kept instances butbm25does not count them (two of 647 random cases, seed 61); in one of 1,450 (seed 21, a phrase that also occurs in anANDbranch) the reference does count them. - Query nesting: the reference parser stack holds 100 entries and reports "fts5: parser stack overflow". Ours counts one entry for a bare parenthesis and three for one that follows an operator or a column filter, and agrees on plain,
AND/OR/NOTand column-filter chains; the exact limit differs by a level or so in chains that mix several pending operators. contentless_delete,tokendata,localeandfts5_locale()are not implemented. UTF-8 and UTF-16 databases both work.bm25(t)inside an aggregate such assum(bm25(t))reports "unable to use function bm25 in the requested context".
Method
tools/fts/fts_diff.py generates seeded scripts (random options, corpus rows, updates, deletes, special commands, every query form,
auxiliary functions, vocab tables) and compares rows and error text per statement with the shell, then runs the shell's integrity-check and
PRAGMA integrity_check on the file OneLite wrote and OneLite's integrity-check on the shell's file.
tests/OneLite.Tests/Engine/Fts5Tests.cs runs the same comparisons over a fixed corpus.