refactor: rename keywords→tags M2M (migration 001)

- migration 001_rename_keywords_to_tags.sql: CREATE tags/thesis_tags from keywords/thesis_keywords,
  copy data, drop old tables, rebuild indexes and views
- schema.sql: tags table, thesis_tags junction, updated indexes and v_theses_full/v_theses_public
- Database.php: findOrCreateTag(), getUsedTags() with proper JOIN; backwards-compat aliases;
  buildSearchConditions uses EXISTS subquery on thesis_tags+tags with vp. alias throughout
- admin/actions/formulaire.php: INSERT OR IGNORE INTO thesis_tags
- admin/edit.php: DELETE FROM thesis_tags + findOrCreateTag
- search.php: $kw['name'] (was $kw['keyword'])
- fixtures/CreateTestDatabase.php: tags/thesis_tags table names
This commit is contained in:
Pontoporeia
2026-03-24 13:30:53 +01:00
parent cefceb046c
commit 0933137540
9 changed files with 226 additions and 130 deletions

View File

@@ -288,58 +288,66 @@ class Database {
* @return array{0: string[], 1: array<string,mixed>} [$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);
}
/**