fix: switch articles_fts to external-content FTS5 (closes #48)#50
Merged
fix: switch articles_fts to external-content FTS5 (closes #48)#50
Conversation
The contentless FTS5 table from migration 0002 required the
delete-by-insert tuple to **exactly match** what's stored:
INSERT INTO articles_fts(articles_fts, rowid, title, excerpt, body, locale, article_id)
VALUES('delete', old.rowid, old.title, ...);
If anything drifted — different normalization, trailing newline,
prior failed REPLACE — the delete trigger threw SQLITE_ERROR. UPDATE
inherited the fault (delete-then-reinsert). End result: editors
couldn't update or delete an article_localizations row once any
drift had occurred.
Migration 0011 fixes it by switching to the **external-content**
FTS5 pattern:
CREATE VIRTUAL TABLE articles_fts USING fts5(
...
content = 'article_localizations',
content_rowid = 'rowid',
...
);
This means:
- Triggers reference rowid alone — no column values to match against,
so drift can't break delete/update:
INSERT INTO articles_fts(articles_fts, rowid) VALUES('delete', old.rowid);
- The source-of-truth values come from article_localizations itself
via the `content =` link.
- `INSERT INTO articles_fts(articles_fts) VALUES('rebuild')` recovers
from any inconsistency without a full DROP/CREATE cycle.
Migration 0011 already applied to live D1. Reproduced + verified the
fix end-to-end:
INSERT → UPDATE → DELETE on article_localizations now all succeed
where they previously threw SQLITE_ERROR; FTS search still returns
ranked snippets correctly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
thunpisit
added a commit
to codustry/khaopad-example
that referenced
this pull request
May 3, 2026
codustry#49 + PR codustry#50) (#15) Cherry-picks both upstream fixes: - PR codustry#49 (closes codustry#47) — 7 ESLint errors blocking CI on fresh install - PR codustry#50 (closes codustry#48) — switch articles_fts from contentless to external-content FTS5; UPDATE/DELETE on article_localizations no longer throws SQLITE_ERROR Migration 0011 already applied to live D1 (same database serves both repos). Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #48. Migrates `articles_fts` from a contentless to an external-content FTS5 virtual table. The contentless pattern from migration 0002 required `('delete', old.rowid, …all column values…)` tuples to match exactly — any drift broke UPDATE and DELETE on `article_localizations` with `SQLITE_ERROR`.
What changed
Migration 0011 (`drizzle/0011_fts5_external_content.sql`):
```sql
INSERT INTO articles_fts(articles_fts, rowid) VALUES('delete', old.rowid);
```
Why external-content is the right fix
Live verification
Migration applied to khaopad-example-db. Reproduced the original bug pattern:
```sql
INSERT INTO article_localizations (...) VALUES (...); -- ok before fix
UPDATE article_localizations SET ... WHERE id='...'; -- threw SQLITE_ERROR before
DELETE FROM article_localizations WHERE id='...'; -- threw SQLITE_ERROR before
```
After the fix: all three succeed. `SELECT … FROM articles_fts WHERE articles_fts MATCH 'khao'` still returns ranked snippets.
Migration notes
Test plan
🤖 Generated with Claude Code