CREATE VIRTUAL TABLE t USING fts3(...), fts4(...) and fts4aux(t) are built into the core
(RecursiveAI.OneLite.Extensions.Fts3). The shadow tables (t_content, t_segments, t_segdir, t_docsize, t_stat) have the layout
the reference writes (sqlite.org/fts3.html, "Data Structures"): segment b-trees, doclist and position-list varints, prefix indexes, language
id levels. A database is read, written and integrity-checked by both engines.
Tokenizers are shared code in RecursiveAI.OneLite.Internal.Text (engine-independent): simple is the ASCII tokenizer FTS5 also uses,
unicode61 (remove_diacritics, tokenchars, separators) is the FTS5 one, porter is Fts3Tokenizers.cs (FTS3's stemmer differs from
FTS5's in which characters make a token). icu is not built into the pinned reference and is not offered.
What is covered
- Options:
content=(external table, or''for contentless),compress=/uncompress=(SQL functions, called asf(?)),languageid=,matchinfo=fts3,notindexed=,order=desc,prefix=,tokenize=. Errors for bad options carry the reference's text. - Queries: the standard syntax of the pinned build (no parentheses;
OR, implicitAND,-for NOT,NEAR,NEAR/n,"phrase", prefix*,col:term, FTS4^anchor),docid/rowid,col MATCH,lid = n. - Auxiliary functions:
offsets,snippet(all arities),matchinfo(p c n a l s x y b),optimize(t). - Special commands:
optimize,rebuild,integrity-check,merge=X,Y,automerge=N. - Writes: insert, update (including
docid), delete; the oracle'sintegrity-checkandPRAGMA integrity_checkpass on our files. fts4aux(t)andfts4aux(db, t)(the two-argument form only for a table created intemp, as in the reference).
Behaviour settled by running the pinned shell
- Operators
ORandNEARare recognised only right after white space that follows the previous word or phrase, and only when followed by white space,(,), a quote or the end. Soalpha, OR beta,(a) OR b,a OR,bandnaive NEAR xunderseparators=eare all words. - Expression size: at most 4095 phrases in a query and 13 operands in a
NEARchain; beyond that `FTS expression tree is too large (maximum depth 12)`. Query text is lexed in linear time. ^anchors the word directly after it (^ worddoes not); before a quote it does nothing; a^right before the first word inside the quotes anchors the phrase ("^one two"). A-before a^is malformed.col:"phrase"with a space after the colon ignores the column (as the predecessor test showed); see the open differences for the form without a space.- matchinfo
scounts runs of consecutive phrases (a phrase that did not match breaks the run). - snippet (token window
N, default 15 though the page says -15;Mfragments ofceil(N/M)tokens,M= 1..4): window start isfloor((first + last + 1 - size) / 2)clamped to the column; fragments print in the order they were chosen (most new phrases, then most hits, first), not in text order; text after the last token is kept when the window reaches the end. fts4aux: rows are one*row per term and one per column with hits; alanguageidconstraint is taken by the reference as an absolute segment-directory index, so with prefix indexes or other languages aroundlanguageid = 1reads the first prefix index (reproduced); a term that occurs in a column after the second makes the scan fail withdatabase disk image is malformedafter the rows before it (reproduced, including that term bounds narrow the scan and a loneORDER BY termstreams).- UPDATE with
docidset to its current value succeeds (an engine fix made with this module).
Differences from the reference (documented, not planned)
- Not resumable. The module reads and writes its shadow tables through nested statements on the sync driver. On a backend that is
async-only a cold read fails with
SQLITE_MISUSE("backend requires the async driver") instead of suspending; it never hangs or corrupts. OverBlockingOkbackends and the memory and file backends it works under both drivers. - Segment layout. Each statement that changes rows writes one level-0 segment (the reference writes one per transaction); a level is merged
when it holds 16 segments.
merge=X,Yandautomerge=Nmerge fully instead of a little at a time, so segment counts and whatoptimize()merges differ. File validity is unaffected:integrity-checkpasses in both directions. col:"phrase"written without a space returns no rows in the reference (the form with a space ignores the column); we ignore the column in both.matchinfo(t, 'x')totals (hits and documents over all rows) of a query with several phrases: the reference does not read a later phrase's doclist to its end, so the totals depend on how the file's segments are laid out and, for aNEARgroup ANDed with other terms, on the query. Ours are always the full totals. They agree in most cases (no difference in 400 random cases, one in 301 queries over a 1,500-document table) and the differential and interop runs comparexfor one-phrase queries only.fts4auxoverorder=desctables counts entries per segment without merging a document's older versions (the reference's counts rise after a delete until the next merge); stale entries of an external-content table whose content changed before the delete are counted by the reference and not by us.GROUP BY termover a scan that fails does not stream the rows before the failure.- Contentless FTS4: the reference fails
integrity-checkandPRAGMA integrity_checkon its own contentless tables; ours answer ok. - pragma
table_xinfoon a virtual table is a separate engine gap.
Method
tools/fts/fts3_diff.py generates seeded scripts (options, rows, updates, deletes, special commands, every query form, fts4aux) and compares
rows and error text per statement with the shell, then runs the shell's integrity-check and PRAGMA integrity_check on our file and ours on
the shell's file. tools/fts/fts3_interop.py builds the same tables in both engines (eight option combinations, four page sizes, UTF-8 and
UTF-16, thousands of documents), checks all four engine/file pairings and then lets each engine modify the other's file.
tests/OneLite.Tests/Engine/Fts3Tests.cs runs the comparisons over a fixed corpus. fuzz/OneLite.Fuzz/Fts3Target.cs (fts3-query,
fts3-data) fuzzes the query parser and hostile t_segments/t_segdir/t_content/t_docsize/t_stat contents. The kill campaign runs
the workload with WRITER_EXTRA="fts3=1".