Summary
search --fts sends the user's query directly into SQLite FTS5's MATCH clause. When FTS5 rejects the query (syntax error, non-existent column, unterminated string, etc.), the resulting SqliteException is wrapped by cdidx's CLI error handler as Error: database error: ... and exits with code 3 = DatabaseError. Neither of those is correct — the failure is a query-syntax error, not a database problem. AI agents and CI scripts that branch on exit 3 treat this as "DB is corrupt, rebuild" and take the wrong recovery path.
Worst offender: cdidx search "tokio::spawn" --fts returns no such column: tokio because FTS5 treats the first : as a column qualifier. Users pasting real C++/Rust/Scala/PHP identifiers with :: hit this by accident, read "database error," and go debug the DB instead of the query.
Repro
curl -fsSL https://raw.githubusercontent.com/Widthdom/CodeIndex/main/install.sh | bash
CDIDX=/root/.local/bin/cdidx
mkdir -p /tmp/srcs && cd /tmp/srcs
curl -fsSL -o tokio.tar.gz https://codeload.github.com/tokio-rs/tokio/tar.gz/refs/heads/master
tar xzf tokio.tar.gz
"$CDIDX" /tmp/srcs/tokio-master --db /tmp/tokio.db
# The most confusing one: a plausible Rust/C++ identifier query
"$CDIDX" search "tokio::spawn" --db /tmp/tokio.db --fts
# Error: database error: SQLite Error 1: 'no such column: tokio'.
# exit=3
# FTS5 syntax error
"$CDIDX" search "AND OR" --db /tmp/tokio.db --fts
# Error: database error: SQLite Error 1: 'fts5: syntax error near "AND"'.
# exit=3
# Unterminated quote
"$CDIDX" search 'foo"bar' --db /tmp/tokio.db --fts
# Error: database error: SQLite Error 1: 'unterminated string'.
# exit=3
# Intentional column qualifier to a non-existent column
"$CDIDX" search "not_a_col:foo" --db /tmp/tokio.db --fts
# Error: database error: SQLite Error 1: 'no such column: not_a_col'.
# exit=3
All four are query-authoring mistakes (bad FTS5 input). None are database problems.
The default (literal-safe) path is healthy
"$CDIDX" search "tokio::spawn" --db /tmp/tokio.db --count # → 198, exit 0
"$CDIDX" search 'foo"bar' --db /tmp/tokio.db --count # → 9, exit 0
"$CDIDX" search 'データ' --db /tmp/tokio.db --count # → 0, exit 0
The issue is specifically in the --fts path's error mapping.
Suspected root cause (from the source)
Likely in src/CodeIndex/Database/DbSearchReader.cs: when an FTS5 MATCH throws SqliteException, it propagates to the top-level CLI error handler, which prefixes every SqliteException with "Error: database error: " and exits with CommandExitCodes.DatabaseError = 3. There's no distinction between "SQLite had a schema/IO problem" and "SQLite's FTS5 parser rejected the user's query."
Why it matters
- Misleading user-facing text. "database error" suggests running
--rebuild or checking DB file permissions. Neither fixes the real problem (change the query or drop --fts).
- Wrong exit code for scripts.
CommandExitCodes.DbError = 3 is documented as "database error — run --rebuild". AI agents following that contract take a wasteful recovery path.
- No hint about the
: column-qualifier semantics. A Rust/C++/Scala/PHP user who pastes foo::bar with --fts has no way to know why they got "no such column: foo" — they never wrote a column qualifier.
Suggested direction
-
Catch the SQLite exception specifically inside the FTS5 MATCH path and reclassify:
try {
// FTS5 MATCH query
} catch (SqliteException ex) when (IsFtsSyntaxError(ex)) {
throw new FtsQuerySyntaxException(ex.Message, ex);
}
where IsFtsSyntaxError checks for the known marker strings: fts5: syntax error, unterminated string, no such column.
-
CLI error handler: when the exception is an FtsQuerySyntaxException, print Error: FTS5 query syntax: <message> instead of the DB-error prefix, and exit with CommandExitCodes.UsageError = 1.
-
Add a targeted hint for the no such column case:
Error: FTS5 query syntax: no such column: tokio
Hint: '--fts' interprets the first ':' in a token as an FTS5 column qualifier.
If ':' is part of the identifier, drop '--fts' to use default literal-safe search.
-
Document in --help and README: "--fts passes raw FTS5 syntax; the query must follow FTS5 rules (column qualifiers with :, AND/OR/NEAR/NOT operators, quoted phrases). Use the default search (without --fts) for literal text."
Scope
src/CodeIndex/Database/DbSearchReader.cs — classify FTS5 MATCH exceptions.
src/CodeIndex/Cli/QueryCommandRunner.cs (or the central error handler) — dedicated exit code + message for FTS syntax errors.
src/CodeIndex/Cli/CommandExitCodes.cs — possibly add a dedicated FtsSyntaxError or reuse UsageError = 1.
tests/CodeIndex.Tests/QueryCommandRunnerTests.cs — regression:
search "X::Y" --fts → exit != 3, message mentions FTS5 column qualifier
search "AND OR" --fts → exit != 3, message mentions FTS5 syntax
search "foo" --fts on a valid token → exit 0, results returned
Related
Environment
- cdidx: v1.10.0 (installed via
install.sh).
- Any indexed project reproduces.
tokio-rs/tokio@master used above.
- Platform: linux-x64 container.
- Filed from a cloud Claude Code session per
CLOUD_BOOTSTRAP_PROMPT.md.
Summary
search --ftssends the user's query directly into SQLite FTS5'sMATCHclause. When FTS5 rejects the query (syntax error, non-existent column, unterminated string, etc.), the resultingSqliteExceptionis wrapped by cdidx's CLI error handler asError: database error: ...and exits with code3 = DatabaseError. Neither of those is correct — the failure is a query-syntax error, not a database problem. AI agents and CI scripts that branch on exit 3 treat this as "DB is corrupt, rebuild" and take the wrong recovery path.Worst offender:
cdidx search "tokio::spawn" --ftsreturnsno such column: tokiobecause FTS5 treats the first:as a column qualifier. Users pasting real C++/Rust/Scala/PHP identifiers with::hit this by accident, read "database error," and go debug the DB instead of the query.Repro
All four are query-authoring mistakes (bad FTS5 input). None are database problems.
The default (literal-safe) path is healthy
The issue is specifically in the
--ftspath's error mapping.Suspected root cause (from the source)
Likely in
src/CodeIndex/Database/DbSearchReader.cs: when an FTS5 MATCH throwsSqliteException, it propagates to the top-level CLI error handler, which prefixes everySqliteExceptionwith"Error: database error: "and exits withCommandExitCodes.DatabaseError = 3. There's no distinction between "SQLite had a schema/IO problem" and "SQLite's FTS5 parser rejected the user's query."Why it matters
--rebuildor checking DB file permissions. Neither fixes the real problem (change the query or drop--fts).CommandExitCodes.DbError = 3is documented as "database error — run--rebuild". AI agents following that contract take a wasteful recovery path.:column-qualifier semantics. A Rust/C++/Scala/PHP user who pastesfoo::barwith--ftshas no way to know why they got "no such column: foo" — they never wrote a column qualifier.Suggested direction
Catch the SQLite exception specifically inside the FTS5 MATCH path and reclassify:
where
IsFtsSyntaxErrorchecks for the known marker strings:fts5: syntax error,unterminated string,no such column.CLI error handler: when the exception is an
FtsQuerySyntaxException, printError: FTS5 query syntax: <message>instead of the DB-error prefix, and exit withCommandExitCodes.UsageError = 1.Add a targeted hint for the
no such columncase:Document in
--helpand README: "--ftspasses raw FTS5 syntax; the query must follow FTS5 rules (column qualifiers with:, AND/OR/NEAR/NOT operators, quoted phrases). Use the default search (without--fts) for literal text."Scope
src/CodeIndex/Database/DbSearchReader.cs— classify FTS5 MATCH exceptions.src/CodeIndex/Cli/QueryCommandRunner.cs(or the central error handler) — dedicated exit code + message for FTS syntax errors.src/CodeIndex/Cli/CommandExitCodes.cs— possibly add a dedicatedFtsSyntaxErroror reuseUsageError = 1.tests/CodeIndex.Tests/QueryCommandRunnerTests.cs— regression:search "X::Y" --fts→ exit != 3, message mentions FTS5 column qualifiersearch "AND OR" --fts→ exit != 3, message mentions FTS5 syntaxsearch "foo" --ftson a valid token → exit 0, results returnedRelated
--jsonCLI crashes with misleading "Error: database error:" prefix and exit code 3 on trimmed build #147 — CLI--jsonmis-labels trimmed-build serialization failures with the same "database error:" prefix. Same family of exception-classification bug, different trigger.symbols --count/search --countreturnmin(total, limit)instead of the true row total, breaking AI preflight #159 —--countsilently capped at--limit. Same family of "signal means something different from what the user infers."--limit/--snippet-linesvalidation errors are non-fatal — "Error: ..." prints but command continues with default, exit=0 (and--db=value/ unknown flags have similar silent-fail shapes) #184 (just filed) — CLI numeric-arg validation is non-fatal. Adjacent family: CLI error paths don't behave consistently.Environment
install.sh).tokio-rs/tokio@masterused above.CLOUD_BOOTSTRAP_PROMPT.md.