Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion docs/databases.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ tables.**
| `numeric(p,s)` | `REAL` (NUMERIC affinity demotes integers and breaks float64 scans) |
| `bytea` | `BLOB` |
| `EXTRACT(EPOCH FROM CURRENT_TIMESTAMP)::bigint` | `(unixepoch())` |
| `ILIKE` | `LIKE`, rewritten in the driver (see below) |
| `now()` | `CURRENT_TIMESTAMP` |
| `TRIM(BOTH FROM x)` | `TRIM(x)` |
| `char_length` | `length` |
Expand Down Expand Up @@ -148,13 +149,27 @@ only emits those for zero values, and `BeforeAppendModel` sets the fields first.
A file that is deliberately Postgres-only opts out with a `dialect:postgres-only`
marker near its package clause, and must gate itself on a capability instead.

### ILIKE

`ILIKE` is rewritten to `LIKE` in the SQLite driver rather than at the call site.
It appears in roughly fifty places, and almost none of them can be fixed where
they are written: `buncolgen` and `querybuilder` precompute their SQL fragments
into package-level values at init, before configuration is read, and most of
`buncolgen` is generated. The driver is 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 — one more reason SQLite is development-only. The rewrite skips
string literals and quoted identifiers, so a stored value containing the word is
left alone.

### Known gaps not yet addressed

These still contain Postgres-only SQL and will fail at runtime on SQLite:

| Construct | Extent | Note |
|---|---|---|
| `ILIKE` | 17 files | SQLite `LIKE` is already case-insensitive for ASCII, so this is a mechanical swap |
| `::int`, `::float`, `::bigint`, `::numeric`, `::text`, `::jsonb` casts | ~109 occurrences | mostly in analytics and reporting aggregates |
| `date_trunc` bucketing | reporting compiler, A/R analytics | marked `dialect:postgres-only`; needs a `strftime` equivalent |
| `pg_stat_activity`, `pg_blocking_pids` | database session diagnostics | gated on `CapSessionDiagnostic`, returns 501 on SQLite |
Expand Down
5 changes: 4 additions & 1 deletion services/tms/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ build
.idea
config/config.yaml/cli
config/*
*.db
*.db
*.db-shm
*.db-wal
*.db-journal
4 changes: 2 additions & 2 deletions services/tms/internal/infrastructure/postgres/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,9 +205,9 @@ func (c *Connection) openDB(
dsn string,
) (*sql.DB, schema.Dialect, error) {
if dialect.IsSQLite() {
sqldb, err := sql.Open(sqliteDriverName, dsn)
sqldb, err := openSQLiteDB(dsn)
if err != nil {
return nil, nil, fmt.Errorf("failed to open sqlite database: %w", err)
return nil, nil, err
}

return sqldb, sqlitedialect.New(), nil
Expand Down
215 changes: 215 additions & 0 deletions services/tms/internal/infrastructure/postgres/sqlitedriver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
package postgres

import (
"context"
"database/sql"
"database/sql/driver"
"fmt"
"strings"
"sync"
)

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

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

const sqliteRewriteDriverName = "sqlite-trenova"

var registerSQLiteRewriteDriver sync.Once

// rewriteILike replaces the ILIKE operator with LIKE outside string literals and
// quoted identifiers, so a value or column name containing the word is untouched.
func rewriteILike(query string) string {
if !containsFold(query, "ilike") {
return query
}

var (
out strings.Builder
inSingle bool
inDouble bool
i int
)

out.Grow(len(query))

for i < len(query) {
ch := query[i]

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

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.

}

out.WriteByte(ch)
i++
}

return out.String()
}

func isILikeAt(query string, i int) bool {
const keyword = "ilike"

if i+len(keyword) > len(query) {
return false
}

if !strings.EqualFold(query[i:i+len(keyword)], keyword) {
return false
}

if i > 0 && isIdentifierByte(query[i-1]) {
return false
}

end := i + len(keyword)

return end == len(query) || !isIdentifierByte(query[end])
}

func isIdentifierByte(b byte) bool {
return b == '_' ||
(b >= 'a' && b <= 'z') ||
(b >= 'A' && b <= 'Z') ||
(b >= '0' && b <= '9')
}

func containsFold(haystack, needle string) bool {
return strings.Contains(strings.ToLower(haystack), needle)
}

// openSQLiteDB returns a database handle whose statements pass through
// rewriteILike on the way to modernc.org/sqlite.
func openSQLiteDB(dsn string) (*sql.DB, error) {
base, err := sql.Open(sqliteDriverName, dsn)
if err != nil {
return nil, fmt.Errorf("failed to open sqlite database: %w", err)
}

inner := base.Driver()
if closeErr := base.Close(); closeErr != nil {
return nil, fmt.Errorf("failed to close probe sqlite handle: %w", closeErr)
}

registerSQLiteRewriteDriver.Do(func() {
sql.Register(sqliteRewriteDriverName, &rewriteDriver{inner: inner})
})

return sql.OpenDB(&rewriteConnector{dsn: dsn, inner: inner}), nil
}

type rewriteDriver struct {
inner driver.Driver
}

func (d *rewriteDriver) Open(name string) (driver.Conn, error) {
conn, err := d.inner.Open(name)
if err != nil {
return nil, err
}

return &rewriteConn{inner: conn}, nil
}

type rewriteConnector struct {
dsn string
inner driver.Driver
}

func (c *rewriteConnector) Connect(_ context.Context) (driver.Conn, error) {
conn, err := c.inner.Open(c.dsn)
if err != nil {
return nil, err
}

return &rewriteConn{inner: conn}, nil
}

func (c *rewriteConnector) Driver() driver.Driver {
return &rewriteDriver{inner: c.inner}
}

// rewriteConn deliberately implements only the prepare path. database/sql falls
// back to Prepare when a connection does not advertise QueryerContext or
// ExecerContext, which keeps every statement going through the rewrite rather
// than slipping past it on a fast path.
type rewriteConn struct {
inner driver.Conn
}

func (c *rewriteConn) Prepare(query string) (driver.Stmt, error) {
return c.inner.Prepare(rewriteILike(query))
}

func (c *rewriteConn) PrepareContext(
ctx context.Context,
query string,
) (driver.Stmt, error) {
if preparer, ok := c.inner.(driver.ConnPrepareContext); ok {
return preparer.PrepareContext(ctx, rewriteILike(query))
}

return c.inner.Prepare(rewriteILike(query))
}

func (c *rewriteConn) Close() error { return c.inner.Close() }

func (c *rewriteConn) Begin() (driver.Tx, error) { //nolint:staticcheck // required by driver.Conn

Check failure on line 181 in services/tms/internal/infrastructure/postgres/sqlitedriver.go

View workflow job for this annotation

GitHub Actions / Lint

directive `//nolint:staticcheck // required by driver.Conn` is unused for linter "staticcheck" (nolintlint)
return c.inner.Begin() //nolint:staticcheck // delegating to the wrapped driver
}

func (c *rewriteConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
if beginner, ok := c.inner.(driver.ConnBeginTx); ok {
return beginner.BeginTx(ctx, opts)
}

return c.inner.Begin() //nolint:staticcheck // fallback for drivers without BeginTx
}

func (c *rewriteConn) Ping(ctx context.Context) error {
if pinger, ok := c.inner.(driver.Pinger); ok {
return pinger.Ping(ctx)
}

return nil
}

func (c *rewriteConn) ResetSession(ctx context.Context) error {
if resetter, ok := c.inner.(driver.SessionResetter); ok {
return resetter.ResetSession(ctx)
}

return nil
}

func (c *rewriteConn) IsValid() bool {
if validator, ok := c.inner.(driver.Validator); ok {
return validator.IsValid()
}

return true
}
120 changes: 120 additions & 0 deletions services/tms/internal/infrastructure/postgres/sqlitedriver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package postgres

import (
"path/filepath"
"testing"

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

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

)

func TestRewriteILike(t *testing.T) {
tests := []struct {
name string
query string
want string
}{
{
name: "leaves a query without the operator alone",
query: `SELECT * FROM t WHERE a = ?`,
want: `SELECT * FROM t WHERE a = ?`,
},
{
name: "rewrites the operator",
query: `SELECT * FROM t WHERE name ILIKE ?`,
want: `SELECT * FROM t WHERE name LIKE ?`,
},
{
name: "rewrites regardless of case",
query: `SELECT * FROM t WHERE name iLiKe ?`,
want: `SELECT * FROM t WHERE name LIKE ?`,
},
{
name: "rewrites NOT ILIKE",
query: `SELECT * FROM t WHERE name NOT ILIKE ?`,
want: `SELECT * FROM t WHERE name NOT LIKE ?`,
},
{
name: "rewrites every occurrence",
query: `SELECT * FROM t WHERE a ILIKE ? OR b ILIKE ?`,
want: `SELECT * FROM t WHERE a LIKE ? OR b LIKE ?`,
},
{
name: "leaves string literals untouched",
query: `SELECT * FROM t WHERE note = ' ILIKE ' AND name ILIKE ?`,
want: `SELECT * FROM t WHERE note = ' ILIKE ' AND name LIKE ?`,
},
{
name: "leaves quoted identifiers untouched",
query: `SELECT "ilike" FROM t WHERE name ILIKE ?`,
want: `SELECT "ilike" FROM t WHERE name LIKE ?`,
},
{
name: "does not touch a column whose name merely contains the word",
query: `SELECT * FROM t WHERE ilike_count > ? AND unilike > ?`,
want: `SELECT * FROM t WHERE ilike_count > ? AND unilike > ?`,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, rewriteILike(tt.query))
})
}
}

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

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


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

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

Binary file removed services/tms/trenova.db-shm
Binary file not shown.
Binary file removed services/tms/trenova.db-wal
Binary file not shown.
Loading