Skip to content

docs: say what the raw-SQL rule means instead of forbidding what the code does - #109

Merged
tae2089 merged 3 commits into
mainfrom
ticket-108-rule
Aug 11, 2026
Merged

docs: say what the raw-SQL rule means instead of forbidding what the code does#109
tae2089 merged 3 commits into
mainfrom
ticket-108-rule

Conversation

@tae2089

@tae2089 tae2089 commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Closes #108. Picks option 1 — write the exception down — and records why nothing is converted.

The decision

The rule now states a criterion instead of a prohibition: use GORM's model layer wherever it has a form for the statement, and raw SQL only where it has none. Five categories have none:

  • Full-text operators and index maintenance — FTS5 MATCH and its rank column; to_tsvector / to_tsquery / ts_rank / @@.
  • DDL on the FTS5 virtual tablesCREATE VIRTUAL TABLE … USING fts5, and the DROP / ALTER TABLE … RENAME TO pairs the legacy upgrade needs. AutoMigrate does not model a virtual table.
  • Writes into those tables — the namespace-scoped deletes and the bulk inserts. These tables have no GORM model; Table("search_fts") would name the table in a string either way, and the bulk insert is one statement on purpose.
  • Schema introspection the migrator cannot doPRAGMA table_info, sqlite_master, information_schema.columns, pg_indexes, pg_trigger.
  • Connection pragmasPRAGMA journal_mode, PRAGMA busy_timeout.

Confined to internal/adapters/outbound/searchsql and internal/db; a raw statement anywhere else is a review stop. Inside an exempt statement, identifiers come from package constants and every value is a bound parameter — no exemption makes concatenating a value into the string correct.

Why nothing is converted

The issue's option 1 ends with "then convert the statements that fall outside the exemption". After checking each one, there are none — and the category that looks most convertible is the one I checked hardest, because GORM does ship Migrator().HasTable, HasColumn and ColumnTypes.

searchsql/migrator_limits_test.go runs all three against a real FTS5 table:

HasTable      answers correctly, present and absent
ColumnTypes   fails: "invalid DDL" — it parses the stored CREATE statement,
              and CREATE VIRTUAL TABLE is not a shape it parses
HasColumn     matches patterns against that DDL text rather than asking the
              schema: false for `content` on a table that has a `content`
              column (PRAGMA table_info sees it in the same test, so the
              fixture is not what is wrong)

HasTable is the one call that works, and sqliteTableExists still keeps its raw statement: HasTable returns a bool with no error, while all four callers propagate one. Swallowing it would read a transient failure as "the table is absent" — in the legacy-upgrade path that is the difference between stopping and rebuilding.

The FTS5 writes are the other case that looks convertible. db.Table(sqliteFTSTable).Where("namespace = ?", ns).Delete(nil) names the table in a string and keeps the raw predicate, so it trades one string for two and changes no safety property. The bulk insert is a single hand-built statement so a rebuild does not pay a round trip per row.

If a GORM upgrade makes one of those tests fail, that is the test working: the exemption gets reconsidered, not the test relaxed. The doc and the test both say so.

What changed

  • CLAUDE.md, AGENTS.md — the compressed rule, pointing at the guide
  • guide/development.md — new ### Raw SQL section with the categories, the two in-statement constraints, and the migrator evidence
  • guide/ko/development.md — the same section in Korean
  • internal/adapters/outbound/searchsql/backend.go — package comment naming the categories and pointing at both
  • internal/adapters/outbound/searchsql/migrator_limits_test.go — the evidence, three tests

Three commits: the evidence test, then the rule, then the in-code pointer.

Verification

  • CGO_ENABLED=1 go test -tags "fts5" ./... -count=1 — all packages pass
  • go vet -tags fts5 ./internal/adapters/outbound/searchsql/ — clean
  • gofmt -l internal/ — only the two pre-existing parse fixtures under testdata/binding_gap/go/

🤖 Generated with Claude Code

tae2089 and others added 3 commits August 11, 2026 16:23
…table

The raw-SQL rule is about to name schema introspection as exempt, and that is
the one exemption worth checking rather than trusting: GORM does ship
`Migrator().HasTable`, `HasColumn` and `ColumnTypes`, so "the migrator cannot do
this" needs evidence, not an assertion.

Three tests run all three calls against a real FTS5 table:

  HasTable      answers correctly, both present and absent
  ColumnTypes   fails with "invalid DDL" — it parses the stored CREATE
                statement, and CREATE VIRTUAL TABLE is not a shape it parses
  HasColumn     matches patterns against that DDL text rather than asking the
                schema, so it reports false for `content` on a table that has
                a `content` column

The HasColumn case is checked both ways: PRAGMA table_info sees the column in
the same test, so the fixture cannot be what is wrong.

HasTable is the one call that works, and sqliteTableExists still keeps its raw
statement — HasTable returns a bool with no error while every caller propagates
one, and swallowing it would read a transient failure as "the table is absent"
in the legacy-upgrade path. The test says so where a reader will find it.

If a GORM upgrade makes one of these fail, that is the test working: the
exemption gets reconsidered instead of the test relaxed.

Refs #108

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…code does

CLAUDE.md, AGENTS.md and guide/development.md all said "use GORM queries only
(no raw SQL)" with no exception written down anywhere, while
internal/adapters/outbound/searchsql holds 25 raw statements in non-test code.
As written, the rule declared that package a wholesale violation and gave a
reader no way to tell a deliberate raw statement from one nobody got round to.

The rule now states the criterion — GORM's model layer wherever it has a form
for the statement — and names the five categories where it has none:

  full-text operators and index maintenance (FTS5 MATCH and its rank column,
    to_tsvector / to_tsquery / ts_rank / @@)
  DDL on the FTS5 virtual tables, which AutoMigrate does not model
  writes into those tables, which have no GORM model at all
  schema introspection the migrator cannot do (PRAGMA table_info,
    sqlite_master, information_schema, pg_indexes, pg_trigger)
  connection pragmas

Exempt statements are confined to internal/adapters/outbound/searchsql and
internal/db; a raw statement anywhere else is a review stop. Inside an exempt
statement two constraints hold regardless: identifiers come from package
constants, never caller input, and every value is a bound parameter. No
exemption makes concatenating a value into the string correct.

Nothing is converted, and the reason is recorded rather than claimed. The
introspection exemption is the one that looks convertible, since GORM ships
HasTable, HasColumn and ColumnTypes — searchsql/migrator_limits_test.go runs all
three against a real FTS5 table and shows ColumnTypes failing with "invalid DDL"
and HasColumn reporting false for a column that exists. HasTable does work, but
it returns no error where every caller of sqliteTableExists propagates one.

guide/development.md carries the full section; the two rule files carry the
compressed form and point at it. guide/ko/development.md mirrors it.

Closes #108

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A reader who opens sqlite.go finds raw statements and no local sign that they
are sanctioned. The package comment now names the four categories they fall
into, points at guide/development.md §Raw SQL for the rule and at
migrator_limits_test.go for the introspection evidence, and says plainly that
anything GORM's model layer can express belongs there instead — here as much as
anywhere else.

Refs #108

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tae2089
tae2089 merged commit 9601e4e into main Aug 11, 2026
2 checks passed
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.

Decide the raw-SQL rule for searchsql: write down the exception, or convert what is convertible

1 participant