ADR
ADR-0002 — Taxonomie des schémas de base de données
Sur cette page
- Statut :
Accepté(2026-08-30 — validation mécanique du mapping :planning/audits/database-schema-taxonomy-validation.md, VERDICT PASS). GO migration conditionné à la Vague 0. - Date : 2026-08-30
- Portée : toute la base PostgreSQL / Supabase, toutes les couches applicatives qui y accèdent
- Références :
planning/audits/b1-database-schema-audit.md,docs/architecture/domain-layer.md,docs/scientific/knowledge-model.md,planning/epics/b0-persistence-mapping-decision.md, ADR-0001,supabase/migrations/20260829150000_add_labs_hadithrank.sql
1. Contexte#
La base compte 61 tables métier (58 public + 3 labs), 28 enums, 6 vues, 65 fonctions et 43 triggers — tout dans public, sauf le schéma labs introduit le 2026-08-29 (inventaire vérifié : planning/audits/database-schema-taxonomy-validation.md §3). La séparation en couches décrite par docs/architecture/domain-layer.md s'arrête à l'application : côté base, aucune frontière de bounded context. Les préfixes de noms (narrator_*, commentary_*, transliteration_*) font le travail d'un schéma sans en donner les bénéfices (grants par domaine, SET SCHEMA, isolation d'exposition PostgREST, direction de dépendance visible).
L'audit planning/audits/b1-database-schema-audit.md conclut : restructuration faisable sans refonte applicative massive (client Supabase non typé → pas de régénération de types ; RPC conservables comme façade ; coût réel concentré sur ~54 appels supabase.from() directs et ~52 corps de fonctions référençant public.*). Le moment est optimal : pré-lancement, un seul précédent (labs) déjà conforme.
2. Décision#
2.1 Dix schémas par bounded context#
public cesse d'héberger des tables métier. Il ne conserve que la façade API (fonctions RPC exposées, vues-façade stables) et les utilitaires transverses (triggers génériques, normalisation). Le métier est réparti dans dix schémas :
| Code | Domaine | Couche domain-layer.md | Dépend de | Exposé PostgREST |
|---|---|---|---|---|
cor | Corpus canonique (hadith, isnād, embeddings) | Domain | — | oui (lecture) |
nar | Narrators / rijāl (prosopographie) | Domain | cor | oui (lecture) |
knw | Knowledge — acquisition savante + assertions/evidence | Domain | nar, cor | oui (lecture) |
rec | Reconciliation — entity resolution, crosswalk, feedback | Engine | nar, knw | partiel |
com | Commentary — sharḥ / Fatḥ al-Bārī | Domain | cor | oui (lecture) |
trl | Transliteration — moteur RUWAT FR | Engine | nar (lecture) | partiel |
ing | Ingestion — provenance, batches, runs | Infrastructure | écrit partout | non |
qua | Quality Engine — snapshots, décisions, métriques | Engine | lit partout | non |
iam | Comptes applicatifs | Infrastructure | — | non |
lab | Labs — modèles exploratoires (renommage de labs) | hors architecture | rien ne doit en dépendre | via ponts public.* |
Direction de dépendance (flèche pleine = dépendance structurelle déclarée ; flèche pointillée = lecture transverse par un schéma de plomberie, non exposé) :
2.2 Convention de nommage (figée)#
- Code de 3 lettres, anglais,
[a-z]{3}strict. Pas de_, pas de chiffre, pas de préfixe projet.iamtoléré (acronyme établi). - Racines arabes écartées (
rij,rwt,srh) — préférence explicite pour l'anglais. - Un schéma = un bounded context, jamais une couche technique abstraite (
data,app). - La direction de dépendance est déclarée dans
comment on schemaet ne doit jamais s'inverser (corne référence jamaisnar, etc.). - Toute évolution de cette liste = amendement de cette ADR +
b1-database-schema-audit.md.
2.3 Règles d'appartenance#
corporte l'agrégat hadith entier :hadith_chains,hadith_chain_nodes,hadith_embeddingsy restent. Pas de schémarel— la chaîne est l'isnād d'un hadith, l'écriture est atomique, l'agrégat = la frontière. Les futures tables calculées du graphe (poids d'arêtes, caches de chemins, réseaux agrégés) iront dansquaou un schéma dédié, jamais les tables sources.knwn'est pas scindé :knowledge_sources → source_editions → knowledge_entries → narrator_assertions → assertion_evidenceforment une seule chaîne d'un seul contexte (docs/scientific/knowledge-model.md). Pas desrc/evd.public= façade uniquement : RPC exposées + vues-façade stables + helpers transverses. Zéro table métier.- Les tables projetées vers l'utilisateur mais alimentées par un moteur (ex.
narrator_quality_profiles) vivent dans le schéma lu (nar), pas dans le schéma écrivain (qua). Le sens d'écriture est documenté en commentaire de table.
2.4 Accès applicatif#
- La UI n'appelle plus
supabase.from()directement. Les lectures passent par des adapterssrc/infrastructure/supabase/<Domain>RepositorySupabase.tsqui portent le.schema()(DOMAIN_LAYER_V1.md§4). - Les RPC restent dans
publicet constituent la couche API officielle ; seuls leurs corps sont re-qualifiés vers les schémas métier. - Les vues-façade
public.*sont réservées à de vraies API stables partagées, pas au masquage d'un couplage applicatif. - Fonctions
SECURITY DEFINER:search_pathminimal + qualification explicite totale des références dans le corps (from nar.narrators join cor.hadith_chain_nodes). Aucune résolution implicite.
3. Mapping exhaustif (D4)#
3.1 Tables#
| Schéma | Tables |
|---|---|
cor | collections, books, chapters, hadiths, hadith_chains, hadith_chain_nodes, hadith_embeddings |
nar | narrators, narrator_name_forms, narrator_appellations, narrator_nisbas, narrator_kinships, narrator_locations, narrator_collections, narrator_transmissions, narrator_reliability, narrator_identities, narrator_identity_variants, narrator_quality_profiles (écrite par qua), external_narrators |
knw | knowledge_sources, source_editions, knowledge_entries, narrator_assertions, assertion_evidence |
rec | narrator_crosswalk, narrator_crosswalk_candidates, narrator_data_conflicts, narrator_reconciliation_decisions, reconciliation_decision_assertions, narrator_resolution_feedback, resolution_feedback_audit |
com | commentary_sources, commentary_editions, commentary_documents, commentary_passages, commentary_passage_labels, commentary_targets, commentary_target_hadiths, commentary_alignments, commentary_alignment_evidence, commentary_translations, commentary_validated_rules, commentary_fath_manifest |
trl | transliteration_standards, transliteration_lexicon, transliteration_aliases, transliteration_stopwords, transliteration_dry_run_runs, transliteration_narrator_proposals |
ing | data_sources, datasets, import_batches, processing_runs |
qua | data_quality_snapshots, quality_engine_state, quality_decisions |
iam | admins |
lab | hadithrank_grade_fidelity, hadithrank_narrator_override, hadithrank_run |
3.2 Vues#
| Cible | Vue | Note |
|---|---|---|
nar | narrator_canonical_display | projection d'affichage |
com | commentary_current_validated_rules, commentary_hadith_passages | |
cor | hadith_index_rows | |
public | public_narrator_assertions, public_transliteration_lexicon | vues-façade : à conserver comme API stable (lecture knw / trl filtrée) ou à convertir en RPC lors de la Vague 4 |
3.3 Enums#
| Schéma | Enums |
|---|---|
knw | knowledge_source_work_type, knowledge_entry_resolution_status, narrator_assertion_field_type, assertion_basis, assertion_canonical_status, assertion_evidence_type, assertion_relation_type |
rec | reconciliation_decision_type, reconciliation_assertion_role, data_conflict_status |
qua | quality_decision_kind, narrator_quality_status |
ing | processing_run_type, processing_run_status |
com | commentary_work_type, commentary_edition_role, commentary_manifestation_kind, commentary_offset_unit, commentary_passage_kind, commentary_provenance_status, commentary_rights_status, commentary_target_type, commentary_translation_status, commentary_translator_type, commentary_verification_status, commentary_alignment_status, commentary_alignment_evidence_type, commentary_rule_validated_status |
3.4 Fonctions#
| Destination | Fonctions |
|---|---|
public (façade API, corps re-qualifié) | browse_hadith_index, browse_narrator_dictionary, search_narrators, search_narrators_translit, search_narrator_candidates, search_narrator_candidates_translit, search_hadiths_arabic, find_chain_edges, get_narrator_hadith_occurrences, dashboard_totals, dashboard_grade_distribution, dashboard_region_distribution, dashboard_generation_distribution, dashboard_collection_distribution, dashboard_top_connected, get_public_quality_statistics, get_public_transliteration_stats, get_admin_data_quality, get_admin_transliteration_quality, get_admin_transliteration_impact_queue, is_admin, review_resolution_feedback, review_narrator_assertion, apply_crosswalk_decision, set_transliteration_lexicon_review_status, batch_approve_transliteration_lexicon, start_transliteration_dry_run, record_transliteration_dry_run, append_transliteration_dry_run_proposals, labs_hadithrank_hadith_chains, labs_hadithrank_grade_fidelity, labs_hadithrank_narrator_overrides |
public (helpers transverses) | set_updated_at, deny_update_delete, immutable_unaccent, arabic_initial, normalize_ar_matn, normalize_ar_query, normalize_translit, extract_hadith_index_matn |
qua | compute_data_quality_metrics, create_data_quality_snapshot, compute_night_shift_provenance_metrics, validate_quality_run |
trl | compute_transliteration_quality_metrics |
ing | protect_terminal_processing_run |
knw (triggers de validation) | validate_assertion_supersession, validate_narrator_assertion_value, protect_narrator_assertion_observation, validate_name_structure |
rec (triggers de validation) | protect_crosswalk_projection, validate_data_conflict, validate_decision_assertion, validate_decision_cardinality, validate_decision_supersession, validate_effective_canonical_cardinality |
com (triggers de validation) | validate_commentary_alignment_supersession, validate_commentary_document_supersession, validate_commentary_multiple_target_cardinality, validate_commentary_passage_group, validate_commentary_passage_supersession, validate_commentary_target_hadith_type, validate_commentary_translation_supersession, validate_commentary_validated_rule_evidence, validate_commentary_validated_rule_supersession |
Un trigger de validation vit dans le schéma de la table qu'il protège. Les helpers réellement génériques restent dans
public.
4. Séquencement#
| Vague | Contenu | Effort |
|---|---|---|
| 0 | Snapshot complet + test de restauration ; geler l'inventaire §3 (diff avec la base réelle, PGlite jetable) ; geler la liste dans cette ADR. Aucun alter. | faible |
| 1 | ing, qua, iam ; renommer labs → lab. Feuilles techniques, ~0 accès app direct. Valide le playbook (create schema → set schema → re-qualifier → grants → golden test). | faible |
| 2 | cor, nar. + repositories src/infrastructure/supabase/ (corpus, narrators) remplaçant les .from() directs. | élevé (SQL) / moyen (TS) |
| 3 | knw, com. | moyen |
| 4 | rec, trl ; conversion des vues public_* ; réduction finale de public à la façade ; suppression des compat devenues inutiles. | moyen |
Prérequis avant Vague 1 : cette ADR passée en Accepté, §3 validé contre la base réelle, Vague 0 exécutée.
Cible : avant S1.5, sans bloquer L1.4-A.
5. Playbook de migration (par schéma)#
begin;
create schema <ctx>;
comment on schema <ctx> is '<domaine> — dépend de <…>. Aucune dépendance entrante depuis <…>.';
grant usage on schema <ctx> to anon, authenticated;
alter table public.<t> set schema <ctx>; -- opération catalogue, pas de réécriture
alter type public.<e> set schema <ctx>;
alter view public.<v> set schema <ctx>;
-- fonctions : recréer avec corps qualifié + search_path explicite
create or replace function <ctx>.<f>(...) ...
set search_path = <ctx>, cor, nar, public, pg_temp
as $$ ... from <ctx>.<t> ... $$;
grant select on all tables in schema <ctx> to anon, authenticated; -- si exposé
alter default privileges in schema <ctx> grant select on tables to anon, authenticated;
commit;
Puis, hors migration : ajouter <ctx> aux Exposed schemas du projet Supabase si lecture directe voulue ; sinon la façade RPC public suffit. Golden test SQL dédié (supabase/tests/).
6. Conséquences#
Positives
- Frontières de bounded context matérialisées et non contournables ; direction de dépendance lisible.
- Surface PostgREST réduite de 58 tables
publicà une façade RPC choisie + quelques vues. - Grants par domaine (
grant … on all tables in schema) au lieu de table par table. ingetqua(plomberie) deviennent non exposés → moindre privilège.- La migration force enfin la séparation Domain / Infrastructure côté app (~54
.from()→ ~5 repositories).
Négatives / coûts
- ~52 fonctions à recréer avec corps qualifié (SQL, une fois, couvert par golden tests).
- ~54 sites d'appel
.from()à router via repositories (Vagues 2–3). - Chaque nouvelle migration doit qualifier
cor./nar./ … ; les fixtures de test créent les schémas. search_pathdes fonctionsSECURITY DEFINERà auditer un par un.
Neutres
- FK inter-schémas : supportées nativement par PostgreSQL, aucun changement.
- RLS :
alter table set schemaconserve les policies ;auth.uid()inchangé. - Types TS : client non typé, rien à régénérer.
7. Alternatives écartées#
| Alternative | Raison du rejet |
|---|---|
Statu quo (tout public) | Dette croissante ; surface API = toutes les tables ; grants ingérables. |
Schémas par couche technique (domain, engine, infra) | Ne dit rien du quoi ; un schéma domain à 35 tables reproduit le problème. |
Schéma rel séparé pour chaînes/nœuds | Frontière artificielle ; casse l'atomicité de l'agrégat hadith ; le Reasoning Engine lit à travers les schémas de toute façon. |
knw scindé en src / evd | Sur-fragmentation d'une chaîne à contexte unique. |
Tout re-exposer via vues public.* | Masque indéfiniment le couplage applicatif que la migration doit résorber. |
Codes longs (corpus, narrators) | Verbeux en SQL qualifié ; l'équipe a tranché pour 3 lettres. |
8. Suivi#
- §3 diffé contre la base réelle —
planning/audits/database-schema-taxonomy-validation.md(2026-08-30, PASS : 61/61 tables, 6/6 vues, 28/28 enums, 65/65 fonctions, 43/43 triggers, 0 duplicate, 0 contradiction D1–D3) - ADR passée en
Accepté(2026-08-30) - Vague 0 : snapshot + restore test + manifeste d'ordre du socle
supabase/*.sql(blocage B1 du rapport de validation) - Vague 1 exécutée + golden test
-
planning/audits/b1-database-schema-audit.md§7 D4 coché