From 6d2c50f0b983a12440dbc2526196264026dc9938 Mon Sep 17 00:00:00 2001 From: Pontoporeia Date: Mon, 23 Mar 2026 11:00:21 +0100 Subject: [PATCH] docs: add M2M tags refactor task proposal to TODO --- TODO.md | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/TODO.md b/TODO.md index ceec361..27ce8b6 100644 --- a/TODO.md +++ b/TODO.md @@ -33,6 +33,84 @@ - [x] Guard `deploy-db` against overwriting existing remote database - [x] Update README.md and docs/SERVER_SETUP.md to reflect current structure +## Refactor: M2M tags via `tags` + `thesis_tags` junction table + +The current schema stores keywords in a `keywords` table joined via `thesis_keywords`. +The field column is named `keyword` (not `name`), breaking the naming convention used by +every other lookup table (`orientations.name`, `format_types.name`, etc.). +More critically, `buildSearchConditions` and the view `v_theses_full` filter keywords +through `GROUP_CONCAT` strings with `LIKE`, bypassing the junction table entirely. + +Goal: rename the tables and column to the canonical M2M pattern (`tags`, `thesis_tags`, +`tags.name`), add the missing index, and rewrite all tag queries to use a proper JOIN. + +### 1 — Schema migration (`storage/schema.sql` + live DB) + +- [ ] Rename table `keywords` → `tags`; rename column `keywords.keyword` → `tags.name` +- [ ] Rename junction table `thesis_keywords` → `thesis_tags`; rename FK column + `thesis_keywords.keyword_id` → `thesis_tags.tag_id` +- [ ] Composite PK on `thesis_tags(tag_id, thesis_id)` (tag first — matches the lookup + pattern `WHERE t.name = ?`) +- [ ] Add index `idx_tags_name ON tags(name)` (supports exact-match lookup on insert/find) +- [ ] Update `idx_thesis_keywords_*` index names → `idx_thesis_tags_thesis`, + `idx_thesis_tags_tag` +- [ ] Update view `v_theses_full` / `v_theses_public`: replace + `LEFT JOIN keywords k ON tk.keyword_id = k.id … GROUP_CONCAT(DISTINCT k.keyword)` + with `LEFT JOIN tags t ON tt.tag_id = t.id … GROUP_CONCAT(DISTINCT t.name)` +- [ ] Write and test a SQLite migration script + (`storage/migrations/001_rename_keywords_to_tags.sql`) + +### 2 — `src/Database.php` + +- [ ] `findOrCreateKeyword()` → `findOrCreateTag()`: query `tags` table, column `name` +- [ ] `getUsedKeywords()` → `getUsedTags()`: rewrite to use proper M2M JOIN instead of + querying the view: + ```sql + SELECT DISTINCT t.* FROM tags t + JOIN thesis_tags tt ON t.id = tt.tag_id + JOIN theses th ON tt.thesis_id = th.id + WHERE th.is_published = 1 ORDER BY t.name + ``` +- [ ] `buildSearchConditions`: replace the `keywords LIKE :keyword` view-string hack with + a subquery using the junction table: + ```sql + EXISTS ( + SELECT 1 FROM thesis_tags tt + JOIN tags t ON t.id = tt.tag_id + WHERE tt.thesis_id = theses.id AND t.name LIKE :keyword ESCAPE '\' + ) + ``` + (search still runs on `v_theses_public`; the subquery references the base table) +- [ ] `validateSearchParams`: rename key `'keyword'` → `'tag'` (or keep alias for + backwards-compat during transition) +- [ ] Add backwards-compat alias `findOrCreateKeyword` → `findOrCreateTag` and + `getUsedKeywords` → `getUsedTags` (remove after all callers updated) + +### 3 — Admin write paths + +- [ ] `public/admin/actions/formulaire.php`: replace `findOrCreateKeyword` + + `INSERT INTO thesis_keywords` with `findOrCreateTag` + `INSERT INTO thesis_tags` +- [ ] `public/admin/edit.php`: same replacement in keyword update block + (`DELETE FROM thesis_keywords` → `DELETE FROM thesis_tags`, insert loop) + +### 4 — Public read paths + +- [ ] `public/search.php`: rename `$keywords` → `$tags`; update `getUsedKeywords()` call + → `getUsedTags()`; rename GET param `keyword` → `tag` (keep old param as alias) +- [ ] `public/tfe.php`: `$data['keywords']` → `$data['tags']` (view column rename) +- [ ] `templates/search-bar.php` (if applicable): update any hardcoded `keyword` param refs + +### 5 — Tests + +- [ ] `tests/Unit/DatabaseTest.php`: add test for `findOrCreateTag` round-trip +- [ ] `tests/Integration/SearchTest.php`: add test for tag-filter search using the new + subquery path (not the LIKE-on-GROUP_CONCAT path) + +### 6 — Fixtures / seed data + +- [ ] `storage/fixtures/CreateTestDatabase.php`: update to use `tags` / `thesis_tags` + table names and `findOrCreateTag()` + ## Pending - [x] Add flake.nix for Nix-based PHP dev environment