Daslang documentation Logo
0.6
  • Daslang 0.6.2 Reference Manual
    • 1. Introduction
    • 2. Design Philosophy
    • 3. The language
    • 4. The Runtime
    • 5. Embedding and Integration
    • 6. Utils
    • 7. Tutorials
      • 7.1. Language Tutorials
      • 7.2. Building from the Installed SDK
      • 7.3. C Integration Tutorials
      • 7.4. C++ Integration Tutorials
      • 7.5. Macro Tutorials
      • 7.6. dasHV (HTTP / WebSocket) Tutorials
      • 7.7. dasPUGIXML (XML) Tutorials
      • 7.8. dasStbImage (Image I/O) Tutorials
      • 7.9. dasSQLITE (SQL) Tutorials
        • 7.9.1. SQL-01 — Hello dasSQLITE
        • 7.9.2. SQL-02 — Declare a Table, Insert
        • 7.9.3. SQL-03 — Auto-Increment Primary Key
        • 7.9.4. SQL-04 — Reading Rows with _sql
        • 7.9.5. SQL-05 — Parameter Binding
        • 7.9.6. SQL-06 — Error Handling
        • 7.9.7. SQL-07 — Anatomy of _sql
        • 7.9.8. SQL-08 — _where Predicates: the Full Surface
        • 7.9.9. SQL-09 — _select Projections
        • 7.9.10. SQL-10 — _order_by and _order_by_descending
        • 7.9.11. SQL-11 — take and skip: Paging
        • 7.9.12. SQL-12 — distinct
        • 7.9.13. SQL-12b — Set operations
        • 7.9.14. SQL-13 — Aggregates: sum/avg/…
        • 7.9.15. SQL-14 — _group_by and _having
        • 7.9.16. SQL-15 — _join (inner equi-join)
        • 7.9.17. SQL-16 — _left_join with Option<TB>
        • 7.9.18. SQL-17 — Subqueries
        • 7.9.19. SQL-18 — NULL Handling: Option<T> Everywhere
        • 7.9.20. SQL-19 — UPDATE
        • 7.9.21. SQL-20 — DELETE
        • 7.9.22. SQL-21 — UPSERT
        • 7.9.23. SQL-22 — Transactions
        • 7.9.24. SQL-23 — Foreign keys
        • 7.9.25. SQL-24 — Indexes
        • 7.9.26. SQL-25 — Defaults + computed columns
        • 7.9.27. SQL-26 — Custom type adapters
        • 7.9.28. SQL-27 — BLOB round-trip
        • 7.9.29. SQL-28 — JSON and BLOB columns
        • 7.9.30. SQL-29 — Column metadata
        • 7.9.31. SQL-30 — Listing tables
        • 7.9.32. SQL-31 — Views
        • 7.9.33. SQL-32 — User-defined SQL scalar functions
        • 7.9.34. SQL-33 — PRAGMA tuning
        • 7.9.35. SQL-34 — Backup, VACUUM, integrity check
        • 7.9.36. SQL-35 — Streaming results with _each_sql
        • 7.9.37. SQL-36 — ATTACH DATABASE: cross-DB queries
        • 7.9.38. SQL-37 — Bulk operations: making writes fast
        • 7.9.39. SQL-38 — Concurrency: threads, contention
        • 7.9.40. SQL-40 — FTS5 full-text search
        • 7.9.41. SQL-41 — Triggers: DB-level callbacks
      • 7.10. dasAudio (Audio) Tutorials
      • 7.11. daStrudel (Live-Coding) Tutorials
      • 7.12. dasPEG (Parser Generator) Tutorials
    • 8. daslang strudel vs strudel.cc — Feature Comparison
  • Daslang Standard Library 0.6.2
Daslang documentation
  • Daslang 0.6.2 Reference Manual
  • 7. Tutorials
  • 7.9.40. SQL-40 — FTS5 full-text search
  • View page source

7.9.40. SQL-40 — FTS5 full-text search

SQLite-only. The portable string predicates (starts_with, ends_with, contains) emit LIKE ? ESCAPE '\' patterns and work on any backend. They’re fine for short columns, but LIKE '%x%' can’t use a B-tree index — full scan at scale is painful. FTS5 is the specialized tool: a virtual table that builds an inverted index over text columns and exposes relevance ranking + Boolean / phrase / prefix / NEAR query operators.

7.9.40.1. API surface added in this chunk

  • [sql_fts5(name = "...")] — struct annotation: the type is materialized as a SQLite FTS5 virtual table.

  • @sql_fts_rank — read-only float column annotation: maps to FTS5’s hidden rank column on SELECT, skipped on INSERT.

  • text_match(col, query) — usable inside _where; emits col MATCH ?. Compile error on a non-FTS5 column, with a fix-it pointing at contains or [sql_fts5].

The same text_match is also a Phase 0.4 daslib/fts5_query predicate (full FTS5-grammar parser, in-memory matcher); user code gets in-memory and SQL-side matching from one call site.

7.9.40.2. Declaring the virtual table

require daslib/sql
require sqlite/sqlite_boost
require sqlite/sqlite_linq

[sql_fts5(name = "docs_idx")]
struct Doc {
    Body : string
    @sql_fts_rank Rank : float
}

db |> create_table(type<Doc>)
// emits:
//   CREATE VIRTUAL TABLE IF NOT EXISTS "docs_idx"
//       USING fts5("Body", tokenize='unicode61')

INSERT works like any [sql_table]; the Rank column is skipped automatically (FTS5 has no user-writable rank).

db |> insert(Doc(Body = "The quick brown fox jumps over the lazy dog"))
db |> insert(Doc(Body = "A swift red fox escaped at dawn"))

7.9.40.3. Querying

Use text_match inside _where. _order_by(_.Rank) gives ascending BM25 score (lower = more relevant first):

let foxes <- _sql(db |> select_from(type<Doc>)
                    |> _where(_.Body |> text_match("quick fox"))
                    |> _order_by(_.Rank))
// emits:
//   SELECT "Body", rank FROM "docs_idx"
//   WHERE "Body" MATCH ? ORDER BY rank

7.9.40.4. FTS5 query syntax (passed through to MATCH)

Pattern

Meaning

quick fox

whitespace-AND — both tokens (any order, any distance)

quick OR sleepy

Boolean OR

quick AND NOT fox

Boolean AND NOT

quick*

prefix match

"quick brown"

phrase match (adjacent tokens, in order)

NEAR(quick fox, 3)

proximity — both within 3 tokens

7.9.40.5. Compile-time rejection on non-FTS5 columns

The translator refuses to silently emit MATCH against a non-FTS5 column — it has no way to give useful behavior. The error message points at contains (LIKE-based substring) or adding [sql_fts5]:

[sql_table(name = "Articles")]
struct Article { Title : string }

_sql(db |> select_from(type<Article>)
    |> _where(_.Title |> text_match("foo")))
// ^^^ compile error:
//     _sql: text_match() requires an [sql_fts5] struct, got 'Article'.
//     Options: use contains(_.Col, q) for LIKE-based substring match,
//     or annotate the struct with [sql_fts5] for indexed full-text
//     search.

7.9.40.6. In-memory text_match (outside SQL)

The same query syntax works in-memory via daslib/fts5_query. Useful for log scanning, in-memory filtering before bulk insert, custom highlighters:

require daslib/fts5_query

let lines = ["The quick brown fox", "the lazy dog", "quicksand boot"]
var hits : array<string>
for (line in lines) {
    if (line |> text_match("quick OR fox")) {
        hits |> push_clone(line)
    }
}

The in-memory matcher supports the full FTS5 grammar (phrases, AND/OR/NOT, NEAR, prefix, parens). For repeated matching against the same query, compile once with compile_text_match(query) and reuse the Fts5Query value.

7.9.40.7. Choosing the right predicate

Need

Predicate

SQL emission

Exact equality

==

col = ?

Starts with prefix

starts_with

col LIKE 'x%' ESCAPE '\'

Ends with suffix

ends_with

col LIKE '%x' ESCAPE '\'

Contains substring

contains

col LIKE '%x%' ESCAPE '\'

Full-text + relevance rank

text_match

col MATCH ? (+ [sql_fts5])

Rule of thumb:

  • Short-column prefix lookup (names, SKUs, tags): starts_with + a B-tree index on the column (tut 24). LIKE with a literal leading prefix uses the index.

  • Substring search on small/medium tables: contains is fine; full scan at small scale is free.

  • Big document bodies, relevance ranking, query operators: text_match + [sql_fts5].

7.9.40.8. v1 limitations

  • Self-contained mode only. The FTS5 table holds both content and index. External-content mode (FTS5 indexing rows of a separate [sql_table]) is deferred.

  • Default tokenizer is unicode61 (case-fold + Unicode word boundaries). Custom tokenizers (porter, ascii, unicode61 remove_diacritics 2) need raw DDL.

  • No typed UPDATE / DELETE. Drop and re-INSERT, or use raw db |> exec("DELETE FROM docs_idx WHERE rowid = ?", id).

  • BM25 weighting, snippet/highlight helpers, per-column query filters (``Body:fox``) all work via the FTS5 query string but don’t have typed wrappers in v1.

7.9.40.9. LIKE escaping (portable trio)

The portable trio — starts_with / ends_with / contains — automatically escapes %, _, \ in the bound value via SQL ESCAPE '\'. A user typing "50%" matches literal "50%" (not “50” + anything). User code never sees escape sequences:

let prefix = "50%"
let hits <- _sql(db |> select_from(type<Item>)
                   |> _where(_.Code |> starts_with(prefix)))
// emits LIKE ? ESCAPE '\' with the bound value pre-escaped
// and pre-wildcarded ("50\%%").

See also

Full source: tutorials/sql/40-fts5.das

Previous tutorial: SQL-38 — Concurrency: threads, contention

Previous Next

© Copyright 2018-2026, Gaijin Entertainment.

Built with Sphinx using a theme provided by Read the Docs.