Skip to content

fix(db): rewrite ILIKE for SQLite, and stop tracking database sidecars - #543

Merged
emoss08 merged 2 commits into
masterfrom
claude/trenova-sqlite-support-4oc8h8
Aug 12, 2026
Merged

fix(db): rewrite ILIKE for SQLite, and stop tracking database sidecars#543
emoss08 merged 2 commits into
masterfrom
claude/trenova-sqlite-support-4oc8h8

Conversation

@emoss08

@emoss08 emoss08 commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Description

Two independent fixes on top of 125ff73.

ILIKE on SQLite. 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. Every query using it fails.

Tracked database sidecars. trenova.db-shm and trenova.db-wal were 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

  • Bug fix
  • Feature
  • Documentation
  • Refactor
  • Tests
  • Build, CI, or infrastructure

Scope

ILIKE

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 arrives too late — and most of buncolgen is 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.go wraps the SQLite handle and substitutes the operator on the prepare path.

Only the prepare path is implemented, deliberately: 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. Implementing those too would mean rewriting in three places rather than one.

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, recorded in docs/databases.md as 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

.gitignore already carried *.db, which is why trenova.db itself was never tracked. SQLite's sidecars are named trenova.db-shm and trenova.db-wal — those names extend past the extension, so the pattern misses them. Both are now listed explicitly, along with *.db-journal for 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 suite
  • cd services/tms && task lintnot run. golangci-lint does not start here: built against Go 1.25 while the module targets 1.26. Changed files were formatted with github.com/golangci/golines v0.15.0, the fork golangci-lint v2.12.2 embeds.
  • cd client && pnpm build — no client changes
  • cd client && pnpm lint — no client changes
  • Other: go build ./... and go vet ./internal/infrastructure/postgres/ — clean
  • Other: new tests cover the rewrite in isolation (case variants, NOT ILIKE, multiple occurrences, string literals, quoted identifiers, ilike_count-style column names) and execute a real ILIKE query against SQLite, asserting it stays case-insensitive

Deployment Notes

PostgreSQL is entirely unaffected: the wrapper is installed only on the SQLite handle, and ILIKE reaches PostgreSQL unchanged.

One thing to expect on merge: anyone who already pulled 125ff73 will 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

  • I kept the change focused and reviewable.
  • I followed AGENTS.md, CLAUDE.md, and existing repository patterns.
  • I added or updated tests for behavior changes, or explained why tests are not applicable.
  • I updated relevant documentation, examples, migrations, or configuration.
  • I did not include secrets, credentials, private customer data, unrelated refactors, or placeholder code.

Generated by Claude Code

Summary by CodeRabbit

  • New Features
    • SQLite now supports case-insensitive ILIKE queries by translating them to LIKE.
    • Query rewriting preserves string literals and quoted identifiers.
  • Documentation
    • Updated SQLite documentation to describe ILIKE support, behavior, and limitations.
  • Bug Fixes
    • Added coverage for ILIKE, NOT ILIKE, multiple operators, and values containing the operator text.
  • Chores
    • SQLite temporary, journal, and write-ahead-log files are now ignored.

claude added 2 commits August 12, 2026 18:16
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
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The SQLite integration now rewrites ILIKE operators to LIKE during statement preparation. The wrapper preserves literals and quoted identifiers, delegates connection operations, adds integration tests, documents the behavior, and ignores SQLite sidecar files.

Changes

SQLite ILIKE compatibility

Layer / File(s) Summary
ILIKE rewrite rules and unit coverage
services/tms/internal/infrastructure/postgres/sqlitedriver.go, services/tms/internal/infrastructure/postgres/sqlitedriver_test.go
The rewrite logic handles ILIKE and NOT ILIKE, repeated operators, keyword boundaries, and case-insensitive matching. It preserves string literals and quoted identifiers.
Wrapped driver integration and validation
services/tms/internal/infrastructure/postgres/sqlitedriver.go, services/tms/internal/infrastructure/postgres/connection.go, services/tms/internal/infrastructure/postgres/sqlitedriver_test.go
SQLite opening uses the wrapped driver. Regular and context-aware preparation rewrite SQL. Connection lifecycle operations delegate to the underlying driver. Integration tests verify case-insensitive queries and stored literals.
SQLite compatibility documentation and file rules
docs/databases.md, services/tms/.gitignore
The database documentation describes driver-level ILIKE rewriting and its ASCII-only behavior. SQLite shared-memory, WAL, and journal files are ignored.

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
Loading

Possibly related PRs

  • emoss08/Trenova#542: Both changes modify SQLite compatibility handling in the database connection code.

Suggested reviewers: emoss-rin

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes both primary changes: SQLite ILIKE rewriting and ignoring database sidecar files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/trenova-sqlite-support-4oc8h8

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cloudflare-workers-and-pages

Copy link
Copy Markdown
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
❌ Deployment failed
View logs
trenova c37cdde Aug 12 2026, 06:20 PM

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (1)
services/tms/internal/infrastructure/postgres/sqlitedriver.go (1)

101-102: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid 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 without ILIKE. 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

📥 Commits

Reviewing files that changed from the base of the PR and between 125ff73 and c37cdde.

📒 Files selected for processing (7)
  • docs/databases.md
  • services/tms/.gitignore
  • services/tms/internal/infrastructure/postgres/connection.go
  • services/tms/internal/infrastructure/postgres/sqlitedriver.go
  • services/tms/internal/infrastructure/postgres/sqlitedriver_test.go
  • services/tms/trenova.db-shm
  • services/tms/trenova.db-wal

Comment on lines +7 to +8
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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

Comment on lines +66 to +120
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",
)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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() })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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 the db.Close() error.
  • services/tms/internal/infrastructure/postgres/sqlitedriver_test.go#L101-L101: report the db.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

Comment on lines +12 to +24
// 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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

Comment on lines +48 to +64
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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:


🏁 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 --stat

Repository: 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}")
PY

Repository: 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}")
PY

Repository: 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})
PY

Repository: 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.

@emoss08
emoss08 merged commit 20a6729 into master Aug 12, 2026
18 of 21 checks passed
@emoss08
emoss08 deleted the claude/trenova-sqlite-support-4oc8h8 branch August 12, 2026 18:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants