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'] ?? '';
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