diff --git a/TODO.md b/TODO.md index b7c4934..30d001d 100644 --- a/TODO.md +++ b/TODO.md @@ -237,59 +237,30 @@ Goal: rename the tables and column to the canonical M2M pattern (`tags`, `thesis ### 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`) +- [x] Rename table `keywords` → `tags`; column `keyword` → `name` +- [x] Rename junction `thesis_keywords` → `thesis_tags`; FK `keyword_id` → `tag_id` +- [x] PK on `thesis_tags(tag_id, thesis_id)`; `idx_tags_name`; updated index names +- [x] Views `v_theses_full`/`v_theses_public` use `thesis_tags`/`tags.name` +- [x] Migration `storage/migrations/001_rename_keywords_to_tags.sql` written and applied ### 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) +- [x] `findOrCreateTag()` added; `findOrCreateKeyword()` is a backwards-compat alias +- [x] `getUsedTags()` rewritten with proper M2M JOIN; `getUsedKeywords()` alias kept +- [x] `buildSearchConditions`: keyword/query use `EXISTS` subquery on `thesis_tags`/`tags` +- [x] All conditions prefixed with `vp.` to match view alias; `vp` alias added to search queries ### 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) +- [x] `public/admin/actions/formulaire.php`: uses `findOrCreateTag` + `thesis_tags` +- [x] `public/admin/edit.php`: `DELETE FROM thesis_tags` + `findOrCreateTag` + `thesis_tags` ### 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 +- [x] `public/search.php`: fixed `$kw['keyword']` → `$kw['name']` (tag column rename) +- [x] `getUsedKeywords()` alias delegates to `getUsedTags()` — no functional change needed +- [ ] `public/tfe.php`: `$data['keywords']` still works (view column name unchanged) +- [ ] `templates/search-bar.php`: no keyword param refs ### 5 — Admin tag management UI (`/admin/tags.php`) @@ -327,8 +298,7 @@ results). ### 6 — Fixtures / seed data -- [ ] `storage/fixtures/CreateTestDatabase.php`: update to use `tags` / `thesis_tags` - table names and `findOrCreateTag()` +- [x] `storage/fixtures/CreateTestDatabase.php`: updated to `tags`/`thesis_tags`/`findOrCreateTag()` --- diff --git a/public/admin/actions/formulaire.php b/public/admin/actions/formulaire.php index f0b02fb..2c01bac 100644 --- a/public/admin/actions/formulaire.php +++ b/public/admin/actions/formulaire.php @@ -193,13 +193,13 @@ try { $stmt->execute([$thesisId, $formatId]); } - // ===== LINK KEYWORDS TO THESIS ===== + // ===== LINK TAGS TO THESIS ===== foreach ($keywords as $keyword) { if (!empty($keyword)) { - $keywordId = $db->findOrCreateKeyword($keyword); - if ($keywordId) { - $stmt = $pdo->prepare("INSERT INTO thesis_keywords (thesis_id, keyword_id) VALUES (?, ?)"); - $stmt->execute([$thesisId, $keywordId]); + $tagId = $db->findOrCreateTag($keyword); + if ($tagId) { + $stmt = $pdo->prepare("INSERT OR IGNORE INTO thesis_tags (tag_id, thesis_id) VALUES (?, ?)"); + $stmt->execute([$tagId, $thesisId]); } } } diff --git a/public/admin/edit.php b/public/admin/edit.php index 94e50d3..eb20718 100644 --- a/public/admin/edit.php +++ b/public/admin/edit.php @@ -120,18 +120,18 @@ try { } } - // Update keywords - $pdo->prepare("DELETE FROM thesis_keywords WHERE thesis_id = ?")->execute([$thesisId]); + // Update tags + $pdo->prepare("DELETE FROM thesis_tags WHERE thesis_id = ?")->execute([$thesisId]); $keywordsRaw = trim($_POST['tag'] ?? ''); if (!empty($keywordsRaw)) { $keywords = array_map('trim', explode(',', $keywordsRaw)); $keywords = array_slice($keywords, 0, 10); // Max 10 foreach ($keywords as $keyword) { if (!empty($keyword)) { - $keywordId = $db->findOrCreateKeyword($keyword); - if ($keywordId) { - $stmt = $pdo->prepare("INSERT INTO thesis_keywords (thesis_id, keyword_id) VALUES (?, ?)"); - $stmt->execute([$thesisId, $keywordId]); + $tagId = $db->findOrCreateTag($keyword); + if ($tagId) { + $stmt = $pdo->prepare("INSERT OR IGNORE INTO thesis_tags (tag_id, thesis_id) VALUES (?, ?)"); + $stmt->execute([$tagId, $thesisId]); } } } diff --git a/public/search.php b/public/search.php index b932c3d..4468644 100644 --- a/public/search.php +++ b/public/search.php @@ -240,9 +240,9 @@ $searchBarValue = $_GET['query'] ?? '';

Mots-clés

- - + +
diff --git a/src/Database.php b/src/Database.php index 1980fcc..384a466 100644 --- a/src/Database.php +++ b/src/Database.php @@ -288,58 +288,66 @@ class Database { * @return array{0: string[], 1: array} [$conditions, $bindings] */ private function buildSearchConditions(array $params): array { - $conditions = ["is_published = 1"]; + $conditions = ["vp.is_published = 1"]; $bindings = []; if (!empty($params['query'])) { $conditions[] = "( - title LIKE :query ESCAPE '\\' OR - subtitle LIKE :query ESCAPE '\\' OR - synopsis LIKE :query ESCAPE '\\' OR - authors LIKE :query ESCAPE '\\' OR - supervisors LIKE :query ESCAPE '\\' OR - keywords LIKE :query ESCAPE '\\' + vp.title LIKE :query ESCAPE '\\' OR + vp.subtitle LIKE :query ESCAPE '\\' OR + vp.synopsis LIKE :query ESCAPE '\\' OR + vp.authors LIKE :query ESCAPE '\\' OR + vp.supervisors LIKE :query ESCAPE '\\' OR + EXISTS ( + SELECT 1 FROM thesis_tags tt2 + JOIN tags tg2 ON tg2.id = tt2.tag_id + WHERE tt2.thesis_id = vp.id AND tg2.name LIKE :query ESCAPE '\\' + ) )"; $bindings[':query'] = '%' . $params['query'] . '%'; } if (!empty($params['year'])) { - $conditions[] = "year = :year"; + $conditions[] = "vp.year = :year"; $bindings[':year'] = $params['year']; } if (!empty($params['orientation'])) { - $conditions[] = "orientation LIKE :orientation ESCAPE '\\'"; + $conditions[] = "vp.orientation LIKE :orientation ESCAPE '\\'"; $bindings[':orientation'] = '%' . $params['orientation'] . '%'; } if (!empty($params['ap_program'])) { - $conditions[] = "ap_program LIKE :ap_program ESCAPE '\\'"; + $conditions[] = "vp.ap_program LIKE :ap_program ESCAPE '\\'"; $bindings[':ap_program'] = '%' . $params['ap_program'] . '%'; } if (!empty($params['finality'])) { - $conditions[] = "finality_type LIKE :finality ESCAPE '\\'"; + $conditions[] = "vp.finality_type LIKE :finality ESCAPE '\\'"; $bindings[':finality'] = '%' . $params['finality'] . '%'; } if (!empty($params['keyword'])) { - $conditions[] = "keywords LIKE :keyword ESCAPE '\\'"; + $conditions[] = "EXISTS ( + SELECT 1 FROM thesis_tags tt_kw + JOIN tags tg_kw ON tg_kw.id = tt_kw.tag_id + WHERE tt_kw.thesis_id = vp.id AND tg_kw.name LIKE :keyword ESCAPE '\\' + )"; $bindings[':keyword'] = '%' . $params['keyword'] . '%'; } if (!empty($params['format'])) { - $conditions[] = "formats LIKE :format ESCAPE '\\'"; + $conditions[] = "vp.formats LIKE :format ESCAPE '\\'"; $bindings[':format'] = '%' . $params['format'] . '%'; } if (!empty($params['language'])) { - $conditions[] = "languages LIKE :language ESCAPE '\\'"; + $conditions[] = "vp.languages LIKE :language ESCAPE '\\'"; $bindings[':language'] = '%' . $params['language'] . '%'; } if (isset($params['is_doctoral'])) { - $conditions[] = "is_doctoral = :is_doctoral"; + $conditions[] = "vp.is_doctoral = :is_doctoral"; $bindings[':is_doctoral'] = $params['is_doctoral'] ? 1 : 0; } @@ -357,7 +365,7 @@ class Database { [$conditions, $bindings] = $this->buildSearchConditions($params); $whereClause = implode(' AND ', $conditions); - $sql = "SELECT * FROM v_theses_public WHERE $whereClause ORDER BY year DESC, title ASC LIMIT :limit OFFSET :offset"; + $sql = "SELECT vp.* FROM v_theses_public vp WHERE $whereClause ORDER BY vp.year DESC, vp.title ASC LIMIT :limit OFFSET :offset"; $stmt = $this->pdo->prepare($sql); foreach ($bindings as $key => $value) { @@ -379,7 +387,7 @@ class Database { [$conditions, $bindings] = $this->buildSearchConditions($params); $whereClause = implode(' AND ', $conditions); - $sql = "SELECT COUNT(*) as count FROM v_theses_public WHERE $whereClause"; + $sql = "SELECT COUNT(*) as count FROM v_theses_public vp WHERE $whereClause"; $stmt = $this->pdo->prepare($sql); foreach ($bindings as $key => $value) { @@ -451,16 +459,21 @@ class Database { /** * Get all keywords used in published theses */ - public function getUsedKeywords() { - $sql = "SELECT DISTINCT k.* FROM keywords k - INNER JOIN thesis_keywords tk ON k.id = tk.keyword_id - INNER JOIN theses t ON tk.thesis_id = t.id - WHERE t.is_published = 1 - ORDER BY k.keyword"; + public function getUsedTags(): array { + $sql = "SELECT DISTINCT tg.id, tg.name FROM tags tg + JOIN thesis_tags tt ON tg.id = tt.tag_id + JOIN theses th ON tt.thesis_id = th.id + WHERE th.is_published = 1 + ORDER BY tg.name"; $stmt = $this->pdo->query($sql); return $stmt->fetchAll(); } + /** Backwards-compat alias */ + public function getUsedKeywords(): array { + return $this->getUsedTags(); + } + /** * Get all format types */ @@ -601,25 +614,30 @@ class Database { } /** - * Find or create a keyword + * Find or create a tag (formerly keyword) */ - public function findOrCreateKeyword($keyword) { - $keyword = trim($keyword); - if (empty($keyword)) { + public function findOrCreateTag(string $name): ?int { + $name = trim($name); + if ($name === '') { return null; } - $stmt = $this->pdo->prepare("SELECT id FROM keywords WHERE keyword = ?"); - $stmt->execute([$keyword]); - $kw = $stmt->fetch(); + $stmt = $this->pdo->prepare("SELECT id FROM tags WHERE name = ?"); + $stmt->execute([$name]); + $row = $stmt->fetch(); - if ($kw) { - return $kw['id']; + if ($row) { + return (int)$row['id']; } - $stmt = $this->pdo->prepare("INSERT INTO keywords (keyword) VALUES (?)"); - $stmt->execute([$keyword]); - return $this->pdo->lastInsertId(); + $stmt = $this->pdo->prepare("INSERT INTO tags (name) VALUES (?)"); + $stmt->execute([$name]); + return (int)$this->pdo->lastInsertId(); + } + + /** Backwards-compat alias */ + public function findOrCreateKeyword($keyword): ?int { + return $this->findOrCreateTag((string)$keyword); } /** diff --git a/storage/fixtures/CreateTestDatabase.php b/storage/fixtures/CreateTestDatabase.php index d09a31b..81aa64c 100644 --- a/storage/fixtures/CreateTestDatabase.php +++ b/storage/fixtures/CreateTestDatabase.php @@ -56,7 +56,7 @@ try { } echo "Inserted " . count($supervisors) . " sample supervisors\n"; - // Insert sample keywords + // Insert sample tags (formerly keywords) $sampleKeywords = [ 'spéculation', 'narration', 'urbanisme', 'patrimoine', 'intime', 'collectivité', 'film', 'cinéma', 'sociologie', 'anthropologie', @@ -65,10 +65,10 @@ try { ]; foreach ($sampleKeywords as $keyword) { - $stmt = $pdo->prepare("INSERT INTO keywords (keyword) VALUES (:keyword)"); - $stmt->execute(['keyword' => $keyword]); + $stmt = $pdo->prepare("INSERT INTO tags (name) VALUES (:name)"); + $stmt->execute(['name' => $keyword]); } - echo "Inserted " . count($sampleKeywords) . " sample keywords\n"; + echo "Inserted " . count($sampleKeywords) . " sample tags\n"; // Insert sample theses $theses = [ @@ -192,30 +192,30 @@ try { } echo "Linked supervisors to theses\n"; - // Link keywords to theses + // Link tags to theses (thesis_tags junction) $thesisKeywords = [ - ['thesis_id' => 1, 'keyword_id' => 3], // urbanisme - ['thesis_id' => 1, 'keyword_id' => 2], // narration - ['thesis_id' => 1, 'keyword_id' => 6], // collectivité - ['thesis_id' => 2, 'keyword_id' => 16], // technologies - ['thesis_id' => 2, 'keyword_id' => 18], // performance - ['thesis_id' => 2, 'keyword_id' => 20], // art numérique - ['thesis_id' => 3, 'keyword_id' => 14], // poésie - ['thesis_id' => 3, 'keyword_id' => 11], // éphémérité - ['thesis_id' => 3, 'keyword_id' => 5], // intime - ['thesis_id' => 4, 'keyword_id' => 15], // écologie - ['thesis_id' => 4, 'keyword_id' => 17], // design - ['thesis_id' => 5, 'keyword_id' => 1], // spéculation - ['thesis_id' => 5, 'keyword_id' => 4], // patrimoine - ['thesis_id' => 6, 'keyword_id' => 18], // performance - ['thesis_id' => 6, 'keyword_id' => 9], // sociologie + ['thesis_id' => 1, 'tag_id' => 3], // urbanisme + ['thesis_id' => 1, 'tag_id' => 2], // narration + ['thesis_id' => 1, 'tag_id' => 6], // collectivité + ['thesis_id' => 2, 'tag_id' => 16], // technologies + ['thesis_id' => 2, 'tag_id' => 18], // performance + ['thesis_id' => 2, 'tag_id' => 20], // art numérique + ['thesis_id' => 3, 'tag_id' => 14], // poésie + ['thesis_id' => 3, 'tag_id' => 11], // éphémérité + ['thesis_id' => 3, 'tag_id' => 5], // intime + ['thesis_id' => 4, 'tag_id' => 15], // écologie + ['thesis_id' => 4, 'tag_id' => 17], // design + ['thesis_id' => 5, 'tag_id' => 1], // spéculation + ['thesis_id' => 5, 'tag_id' => 4], // patrimoine + ['thesis_id' => 6, 'tag_id' => 18], // performance + ['thesis_id' => 6, 'tag_id' => 9], // sociologie ]; foreach ($thesisKeywords as $link) { - $stmt = $pdo->prepare("INSERT INTO thesis_keywords (thesis_id, keyword_id) VALUES (:thesis_id, :keyword_id)"); + $stmt = $pdo->prepare("INSERT OR IGNORE INTO thesis_tags (tag_id, thesis_id) VALUES (:tag_id, :thesis_id)"); $stmt->execute($link); } - echo "Linked keywords to theses\n"; + echo "Linked tags to theses\n"; // Link languages to theses (all in French) for ($i = 1; $i <= 6; $i++) { diff --git a/storage/migrations/001_rename_keywords_to_tags.sql b/storage/migrations/001_rename_keywords_to_tags.sql new file mode 100644 index 0000000..b8c29a5 --- /dev/null +++ b/storage/migrations/001_rename_keywords_to_tags.sql @@ -0,0 +1,106 @@ +-- Migration 001: Rename keywords→tags, thesis_keywords→thesis_tags, keyword column→name +-- SQLite does not support ALTER TABLE RENAME COLUMN before 3.25, so we recreate tables. +-- This migration is safe to run after 004 and 005 (no dependency ordering required +-- since SQLite processes this in one transaction). + +PRAGMA foreign_keys = OFF; + +BEGIN; + +-- 1. Create new tags table +CREATE TABLE tags ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +-- 2. Copy data from keywords → tags (keyword column → name) +INSERT INTO tags (id, name, created_at) +SELECT id, keyword, created_at FROM keywords; + +-- 3. Create new junction table thesis_tags +CREATE TABLE thesis_tags ( + tag_id INTEGER NOT NULL, + thesis_id INTEGER NOT NULL, + PRIMARY KEY (tag_id, thesis_id), + FOREIGN KEY (thesis_id) REFERENCES theses(id) ON DELETE CASCADE, + FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE +); + +-- 4. Copy junction data (keyword_id → tag_id) +INSERT INTO thesis_tags (tag_id, thesis_id) +SELECT keyword_id, thesis_id FROM thesis_keywords; + +-- 5. Drop old tables +DROP TABLE thesis_keywords; +DROP TABLE keywords; + +-- 6. Recreate indexes with canonical names +CREATE INDEX idx_tags_name ON tags(name); +CREATE INDEX idx_thesis_tags_thesis ON thesis_tags(thesis_id); +CREATE INDEX idx_thesis_tags_tag ON thesis_tags(tag_id); + +-- 7. Rebuild views to reference new tables +DROP VIEW IF EXISTS v_theses_full; + +CREATE VIEW v_theses_full AS +SELECT + t.id, + t.identifier, + t.title, + t.subtitle, + t.year, + t.is_doctoral, + o.name as orientation, + ap.name as ap_program, + ft.name as finality_type, + t.synopsis, + t.context_note, + t.duration_minutes, + t.duration_pages, + t.file_size_info, + at.name as access_type, + lt.name as license_type, + t.license_id, + t.jury_points, + t.submitted_at, + t.defense_date, + t.published_at, + t.is_published, + t.baiu_link, + t.banner_path, + GROUP_CONCAT(DISTINCT a.name) as authors, + GROUP_CONCAT(DISTINCT s.name) as supervisors, + GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'president' THEN s.name END) as jury_president, + GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'promoteur' THEN s.name END) as jury_promoteurs, + GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'lecteur' THEN s.name END) as jury_lecteurs, + GROUP_CONCAT(DISTINCT l.name) as languages, + GROUP_CONCAT(DISTINCT fmt.name) as formats, + GROUP_CONCAT(DISTINCT tg.name) as keywords +FROM theses t +LEFT JOIN orientations o ON t.orientation_id = o.id +LEFT JOIN ap_programs ap ON t.ap_program_id = ap.id +LEFT JOIN finality_types ft ON t.finality_id = ft.id +LEFT JOIN access_types at ON t.access_type_id = at.id +LEFT JOIN license_types lt ON t.license_id = lt.id +LEFT JOIN thesis_authors ta ON t.id = ta.thesis_id +LEFT JOIN authors a ON ta.author_id = a.id +LEFT JOIN thesis_supervisors ts ON t.id = ts.thesis_id +LEFT JOIN supervisors s ON ts.supervisor_id = s.id +LEFT JOIN thesis_languages tl ON t.id = tl.thesis_id +LEFT JOIN languages l ON tl.language_id = l.id +LEFT JOIN thesis_formats tf ON t.id = tf.thesis_id +LEFT JOIN format_types fmt ON tf.format_id = fmt.id +LEFT JOIN thesis_tags tt ON t.id = tt.thesis_id +LEFT JOIN tags tg ON tt.tag_id = tg.id +GROUP BY t.id; + +DROP VIEW IF EXISTS v_theses_public; + +CREATE VIEW v_theses_public AS +SELECT * FROM v_theses_full +WHERE is_published = 1; + +COMMIT; + +PRAGMA foreign_keys = ON; diff --git a/storage/schema.sql b/storage/schema.sql index b5eb6de..f94b81a 100644 --- a/storage/schema.sql +++ b/storage/schema.sql @@ -105,13 +105,15 @@ INSERT OR IGNORE INTO format_types (name) VALUES ('Installation'), ('Autre'); --- Keywords (expandable list) -CREATE TABLE IF NOT EXISTS keywords ( +-- Tags (keywords — canonical M2M table; formerly 'keywords'/'keyword' column) +CREATE TABLE IF NOT EXISTS tags ( id INTEGER PRIMARY KEY AUTOINCREMENT, - keyword TEXT NOT NULL UNIQUE, + name TEXT NOT NULL UNIQUE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); +CREATE INDEX IF NOT EXISTS idx_tags_name ON tags(name); + -- Access authorization types CREATE TABLE IF NOT EXISTS access_types ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -250,13 +252,13 @@ CREATE TABLE IF NOT EXISTS thesis_formats ( FOREIGN KEY (format_id) REFERENCES format_types(id) ON DELETE CASCADE ); --- Keywords per thesis (max 10 as per specs) -CREATE TABLE IF NOT EXISTS thesis_keywords ( +-- Tags per thesis (max 10 as per specs; formerly thesis_keywords) +CREATE TABLE IF NOT EXISTS thesis_tags ( + tag_id INTEGER NOT NULL, thesis_id INTEGER NOT NULL, - keyword_id INTEGER NOT NULL, - PRIMARY KEY (thesis_id, keyword_id), + PRIMARY KEY (tag_id, thesis_id), FOREIGN KEY (thesis_id) REFERENCES theses(id) ON DELETE CASCADE, - FOREIGN KEY (keyword_id) REFERENCES keywords(id) ON DELETE CASCADE + FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE ); -- ============================================================================ @@ -311,8 +313,8 @@ CREATE INDEX IF NOT EXISTS idx_theses_access_type ON theses(access_type_id); CREATE INDEX IF NOT EXISTS idx_authors_email ON authors(email); CREATE INDEX IF NOT EXISTS idx_thesis_authors_thesis ON thesis_authors(thesis_id); CREATE INDEX IF NOT EXISTS idx_thesis_authors_author ON thesis_authors(author_id); -CREATE INDEX IF NOT EXISTS idx_thesis_keywords_thesis ON thesis_keywords(thesis_id); -CREATE INDEX IF NOT EXISTS idx_thesis_keywords_keyword ON thesis_keywords(keyword_id); +CREATE INDEX IF NOT EXISTS idx_thesis_tags_thesis ON thesis_tags(thesis_id); +CREATE INDEX IF NOT EXISTS idx_thesis_tags_tag ON thesis_tags(tag_id); -- ============================================================================ -- TRIGGERS for automatic timestamp updates @@ -380,7 +382,7 @@ SELECT GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'lecteur' THEN s.name END) as jury_lecteurs, GROUP_CONCAT(DISTINCT l.name) as languages, GROUP_CONCAT(DISTINCT fmt.name) as formats, - GROUP_CONCAT(DISTINCT k.keyword) as keywords + GROUP_CONCAT(DISTINCT tg.name) as keywords FROM theses t LEFT JOIN orientations o ON t.orientation_id = o.id LEFT JOIN ap_programs ap ON t.ap_program_id = ap.id @@ -395,8 +397,8 @@ LEFT JOIN thesis_languages tl ON t.id = tl.thesis_id LEFT JOIN languages l ON tl.language_id = l.id LEFT JOIN thesis_formats tf ON t.id = tf.thesis_id LEFT JOIN format_types fmt ON tf.format_id = fmt.id -LEFT JOIN thesis_keywords tk ON t.id = tk.thesis_id -LEFT JOIN keywords k ON tk.keyword_id = k.id +LEFT JOIN thesis_tags tt ON t.id = tt.thesis_id +LEFT JOIN tags tg ON tt.tag_id = tg.id GROUP BY t.id; -- Published theses only (for public view) diff --git a/storage/test.db b/storage/test.db index 707201c..4c55718 100644 Binary files a/storage/test.db and b/storage/test.db differ