fix(db): rewrite ILIKE for SQLite, and stop tracking database sidecars - #543
Conversation
SQLite has no ILIKE operator, and unlike a missing function it cannot be polyfilled: ILIKE is a parser keyword, so SQLite rejects the statement before a user-defined function could ever run. The operator appears in roughly fifty places, but almost none can be fixed where they are written. buncolgen and querybuilder precompute their SQL fragments into package-level values at init, long before configuration is read, so a dialect-aware accessor would come too late, and most of buncolgen is generated anyway. That leaves 28 hand-written call sites which a helper could cover and 21 which it could not. Rewriting the statement as it reaches the driver is the one point that covers all of them, including generated code and anything added later. The SQLite handle now goes through a driver wrapper that substitutes the operator on the prepare path. Only the prepare path is implemented on purpose: database/sql falls back to Prepare when a connection does not advertise QueryerContext or ExecerContext, so no statement can slip past on a fast path. The substitution is sound because SQLite's LIKE is already case-insensitive for ASCII, which is what ILIKE is used for here. It is not equivalent for non-ASCII text, which is recorded in the docs as another reason SQLite is development-only. The rewrite skips string literals and quoted identifiers, so a stored value or column name containing the word is left alone; both cases are covered by tests, along with executing ILIKE against a real SQLite database. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FXYnxBszfwkNUeeAcVSSNf
trenova.db-shm and trenova.db-wal were committed alongside the SQLite work. They are per-machine transient state that SQLite rewrites constantly, so they produce spurious diffs and merge conflicts for anyone running the stack locally. .gitignore already carried *.db, but that pattern does not match the shared memory and write-ahead log sidecars SQLite writes next to the database, since their names extend past the extension. Both are now listed explicitly, along with *.db-journal for non-WAL journal modes. The files are removed from the index only; the working copies stay in place so nobody's local database is disturbed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FXYnxBszfwkNUeeAcVSSNf
📝 WalkthroughWalkthroughThe SQLite integration now rewrites ChangesSQLite ILIKE compatibility
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant SQLiteCaller
participant openSQLiteDB
participant wrappedConn
participant SQLiteDriver
SQLiteCaller->>openSQLiteDB: Open SQLite database
openSQLiteDB->>SQLiteDriver: Open underlying driver
SQLiteCaller->>wrappedConn: Prepare SQL with ILIKE
wrappedConn->>wrappedConn: Rewrite ILIKE to LIKE
wrappedConn->>SQLiteDriver: Prepare rewritten SQL
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ❌ Deployment failed View logs |
trenova | c37cdde | Aug 12 2026, 06:20 PM |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
services/tms/internal/infrastructure/postgres/sqlitedriver.go (1)
101-102: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winAvoid copying every SQL statement during the fast-path check.
strings.ToLower(haystack)allocates a full lowercase copy for statements that contain uppercase SQL keywords. This path runs before every prepared statement, including statements withoutILIKE. Scan ASCII bytes case-insensitively so unchanged statements return without an allocation.As per coding guidelines, “Write efficient, allocation-conscious Go code” and “avoid unnecessary copies.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@services/tms/internal/infrastructure/postgres/sqlitedriver.go` around lines 101 - 102, Update containsFold to scan the haystack and needle byte-by-byte using ASCII case-insensitive comparison instead of calling strings.ToLower, so matching remains correct while unchanged statements avoid allocating a lowercase copy. Preserve the existing substring semantics and case-insensitive behavior.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@services/tms/internal/infrastructure/postgres/sqlitedriver_test.go`:
- Around line 66-120: Move TestSQLiteDriverExecutesILike and
TestSQLiteDriverPreservesLiteralContainingILike into a separate
*_integration_test.go file guarded by the integration build tag, preserving
their existing database setup and assertions. Leave TestRewriteILike in the
current untagged unit-test file.
- Around line 7-8: Replace Testify usage in the tests within
sqlitedriver_test.go with Go's standard testing methods, removing the assert and
require imports and converting their calls to equivalent t.Error, t.Errorf, or
t.Fatal handling while preserving the existing test behavior.
- Line 72: Update both cleanup callbacks in
services/tms/internal/infrastructure/postgres/sqlitedriver_test.go at lines
72-72 and 101-101 to capture the error returned by db.Close() and report it with
t.Errorf instead of discarding it.
In `@services/tms/internal/infrastructure/postgres/sqlitedriver.go`:
- Around line 12-24: Remove the explanatory Go comments describing SQLite ILIKE
behavior and substitution rationale, including the corresponding comment blocks
at the referenced locations; retain only comments required by linter directives.
- Around line 48-64: Update rewriteILike to track SQLite grave-accent and
square-bracket identifier quoting, preventing rewrites inside those regions
while preserving existing single- and double-quote handling. Extend
isIdentifierByte to include '$' so tokens such as foo$ILIKE and $ILIKE are not
rewritten. Add regression cases covering each quoted form and
dollar-prefixed/containing identifiers.
---
Nitpick comments:
In `@services/tms/internal/infrastructure/postgres/sqlitedriver.go`:
- Around line 101-102: Update containsFold to scan the haystack and needle
byte-by-byte using ASCII case-insensitive comparison instead of calling
strings.ToLower, so matching remains correct while unchanged statements avoid
allocating a lowercase copy. Preserve the existing substring semantics and
case-insensitive behavior.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: c9f4c1e4-6e6d-41e4-a426-d13723c48496
📒 Files selected for processing (7)
docs/databases.mdservices/tms/.gitignoreservices/tms/internal/infrastructure/postgres/connection.goservices/tms/internal/infrastructure/postgres/sqlitedriver.goservices/tms/internal/infrastructure/postgres/sqlitedriver_test.goservices/tms/trenova.db-shmservices/tms/trenova.db-wal
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Use the Go standard testing package.
Replace Testify assertions with t.Error, t.Errorf, and t.Fatal paths. The applicable test rule requires the standard package.
As per coding guidelines, “Use Go's standard testing package for Go tests.”
Also applies to: 61-62, 71-72, 81-92, 100-101, 110-119
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@services/tms/internal/infrastructure/postgres/sqlitedriver_test.go` around
lines 7 - 8, Replace Testify usage in the tests within sqlitedriver_test.go with
Go's standard testing methods, removing the assert and require imports and
converting their calls to equivalent t.Error, t.Errorf, or t.Fatal handling
while preserving the existing test behavior.
Source: Coding guidelines
| func TestSQLiteDriverExecutesILike(t *testing.T) { | ||
| ctx := t.Context() | ||
| path := filepath.Join(t.TempDir(), "ilike.db") | ||
|
|
||
| db, err := openSQLiteDB("file:" + path) | ||
| require.NoError(t, err) | ||
| t.Cleanup(func() { _ = db.Close() }) | ||
|
|
||
| _, err = db.ExecContext(ctx, `CREATE TABLE people (name TEXT)`) | ||
| require.NoError(t, err) | ||
|
|
||
| _, err = db.ExecContext(ctx, `INSERT INTO people (name) VALUES ('Alice'), ('bob')`) | ||
| require.NoError(t, err) | ||
|
|
||
| var name string | ||
| require.NoError( | ||
| t, | ||
| db.QueryRowContext(ctx, `SELECT name FROM people WHERE name ILIKE ?`, "alice").Scan(&name), | ||
| "ILIKE must survive the rewrite and execute on SQLite", | ||
| ) | ||
| assert.Equal(t, "Alice", name, "ILIKE must stay case-insensitive after the rewrite") | ||
|
|
||
| require.NoError( | ||
| t, | ||
| db.QueryRowContext(ctx, `SELECT name FROM people WHERE name ILIKE ?`, "BOB").Scan(&name), | ||
| ) | ||
| assert.Equal(t, "bob", name) | ||
| } | ||
|
|
||
| func TestSQLiteDriverPreservesLiteralContainingILike(t *testing.T) { | ||
| ctx := t.Context() | ||
| path := filepath.Join(t.TempDir(), "literal.db") | ||
|
|
||
| db, err := openSQLiteDB("file:" + path) | ||
| require.NoError(t, err) | ||
| t.Cleanup(func() { _ = db.Close() }) | ||
|
|
||
| _, err = db.ExecContext(ctx, `CREATE TABLE notes (body TEXT)`) | ||
| require.NoError(t, err) | ||
|
|
||
| _, err = db.ExecContext(ctx, `INSERT INTO notes (body) VALUES ('uses ILIKE here')`) | ||
| require.NoError(t, err) | ||
|
|
||
| var body string | ||
| require.NoError( | ||
| t, | ||
| db.QueryRowContext(ctx, `SELECT body FROM notes WHERE body ILIKE ?`, "%ilike%").Scan(&body), | ||
| ) | ||
| assert.Equal( | ||
| t, | ||
| "uses ILIKE here", | ||
| body, | ||
| "a stored value containing ILIKE must not be rewritten", | ||
| ) | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Tag-gate the SQLite driver integration tests.
These tests create a SQLite database and execute through the wrapped driver. Move them to a separate *_integration_test.go file with the integration build tag. Keep TestRewriteILike as an untagged unit test.
As per coding guidelines, “Integration tests must be tag-gated with the integration build tag.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@services/tms/internal/infrastructure/postgres/sqlitedriver_test.go` around
lines 66 - 120, Move TestSQLiteDriverExecutesILike and
TestSQLiteDriverPreservesLiteralContainingILike into a separate
*_integration_test.go file guarded by the integration build tag, preserving
their existing database setup and assertions. Leave TestRewriteILike in the
current untagged unit-test file.
Source: Coding guidelines
|
|
||
| db, err := openSQLiteDB("file:" + path) | ||
| require.NoError(t, err) | ||
| t.Cleanup(func() { _ = db.Close() }) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Handle db.Close errors in both cleanup callbacks.
Both callbacks discard a possible close error. Report the error with t.Errorf from each cleanup callback.
services/tms/internal/infrastructure/postgres/sqlitedriver_test.go#L72-L72: report thedb.Close()error.services/tms/internal/infrastructure/postgres/sqlitedriver_test.go#L101-L101: report thedb.Close()error.
As per coding guidelines, “Handle every error path explicitly; never swallow errors.”
📍 Affects 1 file
services/tms/internal/infrastructure/postgres/sqlitedriver_test.go#L72-L72(this comment)services/tms/internal/infrastructure/postgres/sqlitedriver_test.go#L101-L101
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@services/tms/internal/infrastructure/postgres/sqlitedriver_test.go` at line
72, Update both cleanup callbacks in
services/tms/internal/infrastructure/postgres/sqlitedriver_test.go at lines
72-72 and 101-101 to capture the error returned by db.Close() and report it with
t.Errorf instead of discarding it.
Source: Coding guidelines
| // SQLite has no ILIKE operator, and unlike a missing function it cannot be | ||
| // polyfilled: ILIKE is a parser keyword, so SQLite rejects the statement before | ||
| // any user-defined function could run. | ||
| // | ||
| // The operator appears in roughly fifty places, but almost none of them can be | ||
| // fixed at the call site. buncolgen and querybuilder precompute their SQL | ||
| // fragments into package-level values at init, long before configuration is | ||
| // read, and most of buncolgen is generated. Rewriting the statement as it | ||
| // reaches the driver is therefore the only single point that covers all of them. | ||
| // | ||
| // The substitution is sound because SQLite's LIKE is already case-insensitive | ||
| // for ASCII, which is what ILIKE is used for here. It is not equivalent for | ||
| // non-ASCII text, and that is one more reason SQLite is development-only. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Remove the added explanatory Go comments.
The applicable rule prohibits added Go comments. Keep only required linter directives.
As per coding guidelines, “Do not add comments to Go code.”
Also applies to: 29-30, 105-106, 156-159
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@services/tms/internal/infrastructure/postgres/sqlitedriver.go` around lines
12 - 24, Remove the explanatory Go comments describing SQLite ILIKE behavior and
substitution rationale, including the corresponding comment blocks at the
referenced locations; retain only comments required by linter directives.
Source: Coding guidelines
| switch { | ||
| case inSingle: | ||
| if ch == '\'' { | ||
| inSingle = false | ||
| } | ||
| case inDouble: | ||
| if ch == '"' { | ||
| inDouble = false | ||
| } | ||
| case ch == '\'': | ||
| inSingle = true | ||
| case ch == '"': | ||
| inDouble = true | ||
| case (ch == 'i' || ch == 'I') && isILikeAt(query, i): | ||
| out.WriteString("LIKE") | ||
| i += len("ILIKE") | ||
| continue |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
According to SQLite SQL syntax, which quoted identifier forms are accepted, and is $ permitted inside an unquoted identifier?
💡 Result:
In SQLite, there are four ways to quote an identifier [1][2]: - Double quotes ("identifier"): This is the standard SQL way to quote identifiers [3]. - Square brackets ([identifier]): Included for compatibility with MS Access and SQL Server [1][2]. - Grave accents (identifier): Included for compatibility with MySQL [1][2]. - Single quotes ('identifier'): While normally used for string literals, SQLite will treat a single-quoted token as an identifier if it appears in a context where an identifier is required but a string literal is not [1][4]. Regarding the use of the dollar sign ($) in unquoted identifiers, SQLite permits the dollar-sign character within an unquoted identifier [3]. According to the SQLite tokenizer requirements, an ID token may begin with an alphabetic character and continue with any sequence of alphanumeric characters and/or dollar signs ($) [5][3]. Note that while $ is permitted within an unquoted identifier, it is also used as a prefix for named parameters (e.g., $parameter_name) [6][7]. Additionally, developers are generally encouraged to quote identifiers that are English language words to prevent conflicts with current or future SQL keywords [1][2].
Citations:
- 1: https://sqlite.org/lang_keywords.html
- 2: https://www.sqlite.org/lang_keywords.html
- 3: https://www.sqlite.org/draft/tokenreq.html
- 4: https://sqlite.org/quirks.html
- 5: https://www2.sqlite.org/hlr40000.html
- 6: https://sqlite.org/lang_expr.html
- 7: https://www.sqlite.org/lang_expr.html
🏁 Script executed:
#!/bin/bash
set -eu
file="services/tms/internal/infrastructure/postgres/sqlitedriver.go"
printf '%s\n' '--- file outline ---'
ast-grep outline "$file" || true
printf '%s\n' '--- relevant source ---'
sed -n '1,180p' "$file"
printf '%s\n' '--- related tests and usages ---'
rg -n -C 3 'rewriteILike|isILikeAt|containsFold|ILIKE|sqlitedriver' services/tms
printf '%s\n' '--- repository status ---'
git diff --statRepository: emoss08/Trenova
Length of output: 50371
🏁 Script executed:
#!/bin/bash
set -eu
python3 - <<'PY'
import sqlite3
def is_identifier_byte(b):
return b == "_" or 97 <= b <= 122 or 65 <= b <= 90 or 48 <= b <= 57
def is_ilike_at(query, i):
keyword = "ilike"
if i + len(keyword) > len(query):
return False
if query[i:i+len(keyword)].lower() != keyword:
return False
if i > 0 and is_identifier_byte(ord(query[i-1])):
return False
end = i + len(keyword)
return end == len(query) or not is_identifier_byte(ord(query[end]))
def rewrite_ilike(query):
if "ilike" not in query.lower():
return query
out = []
in_single = False
in_double = False
i = 0
while i < len(query):
ch = query[i]
if in_single:
if ch == "'":
in_single = False
elif in_double:
if ch == '"':
in_double = False
elif ch == "'":
in_single = True
elif ch == '"':
in_double = True
elif ch in "iI" and is_ilike_at(query, i):
out.append("LIKE")
i += len("ILIKE")
continue
out.append(ch)
i += 1
return "".join(out)
cases = [
"SELECT `ILIKE` FROM t",
"SELECT [ILIKE] FROM t",
"SELECT \"ILIKE\" FROM t",
"SELECT foo$ILIKE FROM t",
"SELECT $ILIKE FROM t",
"SELECT foo_ILIKE FROM t",
"SELECT name ILIKE ?",
]
for query in cases:
print(f"{query!r} -> {rewrite_ilike(query)!r}")
print("--- SQLite token/parse probes ---")
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE t (`ILIKE` TEXT, [ILIKE] TEXT, foo$ILIKE TEXT)")
for query, params in [
("SELECT `ILIKE` FROM t", ()),
("SELECT [ILIKE] FROM t", ()),
("SELECT foo$ILIKE FROM t", ()),
("SELECT $ILIKE", {"ILIKE": "value"}),
]:
try:
db.execute(query, params)
print(f"accepted: {query!r}")
except Exception as exc:
print(f"rejected: {query!r}: {exc}")
PYRepository: emoss08/Trenova
Length of output: 669
🏁 Script executed:
#!/bin/bash
set -eu
python3 - <<'PY'
import sqlite3
def is_identifier_byte(b):
return b == "_" or 97 <= b <= 122 or 65 <= b <= 90 or 48 <= b <= 57
def is_ilike_at(query, i):
keyword = "ilike"
if i + len(keyword) > len(query):
return False
if query[i:i+len(keyword)].lower() != keyword:
return False
if i > 0 and is_identifier_byte(ord(query[i-1])):
return False
end = i + len(keyword)
return end == len(query) or not is_identifier_byte(ord(query[end]))
def rewrite_ilike(query):
if "ilike" not in query.lower():
return query
out = []
in_single = False
in_double = False
i = 0
while i < len(query):
ch = query[i]
if in_single:
if ch == "'":
in_single = False
elif in_double:
if ch == '"':
in_double = False
elif ch == "'":
in_single = True
elif ch == '"':
in_double = True
elif ch in "iI" and is_ilike_at(query, i):
out.append("LIKE")
i += len("ILIKE")
continue
out.append(ch)
i += 1
return "".join(out)
cases = [
"SELECT `ILIKE` FROM t",
"SELECT [ILIKE] FROM t",
'SELECT "ILIKE" FROM t',
"SELECT foo$ILIKE FROM t",
"SELECT $ILIKE FROM t",
"SELECT foo_ILIKE FROM t",
"SELECT name ILIKE ?",
]
for query in cases:
print(f"{query!r} -> {rewrite_ilike(query)!r}")
print("--- SQLite token/parse probes ---")
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE t (`ILIKE` TEXT, [ILIKE] TEXT, foo$ILIKE TEXT)")
for query, params in [
("SELECT `ILIKE` FROM t", ()),
("SELECT [ILIKE] FROM t", ()),
("SELECT foo$ILIKE FROM t", ()),
("SELECT $ILIKE", {"ILIKE": "value"}),
]:
try:
db.execute(query, params)
print(f"accepted: {query!r}")
except Exception as exc:
print(f"rejected: {query!r}: {exc}")
PYRepository: emoss08/Trenova
Length of output: 669
🏁 Script executed:
#!/bin/bash
set -eu
python3 - <<'PY'
import sqlite3
def probe(schema, query, params=()):
db = sqlite3.connect(":memory:")
try:
db.execute(schema)
db.execute(query, params)
print(f"accepted: {query!r}")
except Exception as exc:
print(f"rejected: {query!r}: {exc}")
probe("CREATE TABLE t (`ILIKE` TEXT)", "SELECT `ILIKE` FROM t")
probe("CREATE TABLE t ([ILIKE] TEXT)", "SELECT [ILIKE] FROM t")
probe("CREATE TABLE t (foo$ILIKE TEXT)", "SELECT foo$ILIKE FROM t")
probe("SELECT 1", "SELECT $ILIKE", {"ILIKE": "value"})
probe("SELECT 1", "SELECT $ILIKE", {"ILIKE": 1})
PYRepository: emoss08/Trenova
Length of output: 309
Protect all SQLite identifier forms before rewriting.
rewriteILike tracks only single- and double-quoted text. SQLite also accepts grave-accent and square-bracket quoted identifiers, so it rewrites `ILIKE` and [ILIKE]. isIdentifierByte excludes $, so it also rewrites valid tokens such as foo$ILIKE and $ILIKE. Track these delimiters and treat $ as an identifier byte. Add regression cases.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@services/tms/internal/infrastructure/postgres/sqlitedriver.go` around lines
48 - 64, Update rewriteILike to track SQLite grave-accent and square-bracket
identifier quoting, preventing rewrites inside those regions while preserving
existing single- and double-quote handling. Extend isIdentifierByte to include
'$' so tokens such as foo$ILIKE and $ILIKE are not rewritten. Add regression
cases covering each quoted form and dollar-prefixed/containing identifiers.
Description
Two independent fixes on top of
125ff73.ILIKEon SQLite. SQLite has noILIKEoperator, and unlike a missing function it cannot be polyfilled:ILIKEis a parser keyword, so SQLite rejects the statement before any user-defined function could run. Every query using it fails.Tracked database sidecars.
trenova.db-shmandtrenova.db-walwere committed alongside the SQLite work. They are per-machine transient state that SQLite rewrites constantly.Related Issue or Discussion
Continues the SQLite development-support work from #536, #540 and #542.
Type of Change
Scope
ILIKE
The operator appears in roughly fifty places, but almost none can be fixed where they are written.
buncolgenandquerybuilderprecompute their SQL fragments into package-level values at init, long before configuration is read, so a dialect-aware accessor arrives too late — and most ofbuncolgenis generated, so edits there are lost on the next regeneration. That split is 28 hand-written call sites a helper could cover against 21 it could not.Rewriting the statement as it reaches the driver is the one point that covers all of them, including generated code and anything added later.
internal/infrastructure/postgres/sqlitedriver.gowraps the SQLite handle and substitutes the operator on the prepare path.Only the prepare path is implemented, deliberately:
database/sqlfalls back toPreparewhen a connection does not advertiseQueryerContextorExecerContext, so no statement can slip past on a fast path. Implementing those too would mean rewriting in three places rather than one.The substitution is sound because SQLite's
LIKEis already case-insensitive for ASCII, which is whatILIKEis used for here. It is not equivalent for non-ASCII text, recorded indocs/databases.mdas one more reason SQLite stays development-only.The rewrite skips string literals and quoted identifiers, so a stored value containing the word, or a column named
ilike_count, is left alone.Database sidecars
.gitignorealready carried*.db, which is whytrenova.dbitself was never tracked. SQLite's sidecars are namedtrenova.db-shmandtrenova.db-wal— those names extend past the extension, so the pattern misses them. Both are now listed explicitly, along with*.db-journalfor non-WAL journal modes.Removed from the index only, with
git rm --cached; working copies stay in place so nobody's local database is disturbed.Validation
cd services/tms && task test— passes, including the full SQLite seed run and the migrator suitecd services/tms && task lint— not run. golangci-lint does not start here: built against Go 1.25 while the module targets 1.26. Changed files were formatted withgithub.com/golangci/golinesv0.15.0, the fork golangci-lint v2.12.2 embeds.cd client && pnpm build— no client changescd client && pnpm lint— no client changesgo build ./...andgo vet ./internal/infrastructure/postgres/— cleanNOT ILIKE, multiple occurrences, string literals, quoted identifiers,ilike_count-style column names) and execute a realILIKEquery against SQLite, asserting it stays case-insensitiveDeployment Notes
PostgreSQL is entirely unaffected: the wrapper is installed only on the SQLite handle, and
ILIKEreaches PostgreSQL unchanged.One thing to expect on merge: anyone who already pulled
125ff73will have those two sidecar files removed from their working tree on checkout, because they leave the index. Harmless, since SQLite regenerates both on next open, but it can look alarming if the app is running at the time.Checklist
AGENTS.md,CLAUDE.md, and existing repository patterns.Generated by Claude Code
Summary by CodeRabbit
ILIKEqueries by translating them toLIKE.ILIKEsupport, behavior, and limitations.ILIKE,NOT ILIKE, multiple operators, and values containing the operator text.