Skip to content

Commit 8b355e6

Browse files
committed
fix(database): add columns + renamed all columns (schema.org + snake_case)
1 parent bbb8c36 commit 8b355e6

6 files changed

Lines changed: 495 additions & 198 deletions

File tree

sandbox/postgres/README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ l'interface [schema.org](https://schema.org) par-dessus les composants fragment
4141
.
4242
├── master_schema_ddl.pgsql # Blueprint immuable — DDL pur
4343
├── master_schema_dml.pgsql # Seed data — dev / CI uniquement
44-
├── architecture_decision_records.md # 23 arbitrages architecturaux
44+
├── architecture_decision_records.md # 25 arbitrages architecturaux
4545
├── README.md
4646
└── tests/
4747
├── 01_schema_and_security.sql # Types physiques, BRIN, RBAC, SECURITY DEFINER
@@ -81,7 +81,7 @@ d'écriture que la production.
8181

8282
### `architecture_decision_records.md`
8383

84-
23 arbitrages architecturaux, ordonnés par importance décroissante. Chaque
84+
25 arbitrages architecturaux, ordonnés par importance décroissante. Chaque
8585
entrée documente ce qui **n'est pas déductible de la lecture du code** : le
8686
raisonnement derrière la décision, les alternatives écartées et leurs coûts.
8787

@@ -168,7 +168,7 @@ WHERE table_schema IN ('identity','geo','org','commerce','content')
168168
GROUP BY table_schema ORDER BY table_schema;
169169

170170
-- Tester une vue sémantique
171-
SELECT "identifier", "headline", "datePublished"
171+
SELECT identifier, headline, published_at
172172
FROM content.v_article_list
173173
LIMIT 5;
174174

@@ -227,11 +227,11 @@ alias de vues :
227227

228228
```sql
229229
-- Lecture du prix d'un produit
230-
SELECT "identifier", "priceCents" FROM commerce.v_product WHERE "identifier" = 1;
230+
SELECT identifier, price_cents FROM commerce.v_product WHERE identifier = 1;
231231
-- priceCents = 1999 → 19,99 € (conversion déléguée à la couche applicative)
232232

233233
-- Total d'une commande
234-
SELECT "totalPriceCents" FROM commerce.v_transaction WHERE "identifier" = 42;
234+
SELECT total_cents FROM commerce.v_transaction WHERE identifier = 42;
235235
```
236236

237237
`INT8` est pass-by-value sur toutes les architectures 64 bits : arithmétique ALU
@@ -406,7 +406,7 @@ SELECT "identifier", headline, slug FROM content.v_article_list LIMIT 20;
406406

407407
EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)
408408
SELECT "identifier", headline, "articleBody" FROM content.v_article
409-
WHERE "identifier" = 1;
409+
WHERE identifier = 1;
410410
```
411411

412412
`shared_blks_hit` doit être significativement plus élevé pour la seconde requête.

sandbox/postgres/architecture_decision_records.md

Lines changed: 171 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,176 @@ descriptives (`mime_type`, `folder_url`, `file_name`, `width`, `height`).
132132

133133
---
134134

135+
## ADR-025 — Interface sémantique snake_case : schema.org sans guillemets SQL
136+
137+
**Statut** : Adopté
138+
139+
### Contexte
140+
141+
Les vues sémantiques exposaient des alias camelCase entre guillemets doubles
142+
(`"givenName"`, `"datePublished"`, `"@type"`). Cette convention crée trois
143+
frictions opérationnelles :
144+
145+
1. **Guillemets obligatoires dans toute requête SQL** : `SELECT "givenName" FROM
146+
identity.v_person` — l'oubli d'un guillemet provoque une erreur silencieuse
147+
(colonne non trouvée, ou pire : résolution vers une colonne système).
148+
2. **Caractère `@` illégal** comme identifiant SQL nu : `"@type"` ne peut jamais
149+
être utilisé sans guillemets.
150+
3. **Friction avec les ORM et drivers** : la majorité des drivers (psycopg3, JDBC,
151+
node-postgres) retournent les colonnes en minuscules par défaut, ce qui force
152+
soit des mappings explicites, soit des guillemets systématiques.
153+
154+
### Décision
155+
156+
Toutes les vues sémantiques utilisent désormais le **snake_case PostgreSQL natif**,
157+
sans guillemets. Le vocabulaire schema.org est préservé dans les noms de colonnes
158+
par translittération directe : `givenName → given_name`, `datePublished →
159+
published_at`, `articleBody → article_body`.
160+
161+
### Règles de translittération
162+
163+
| Règle | Exemple schema.org | Alias vue |
164+
| ----- | ------------------ | --------- |
165+
| camelCase → snake_case | `givenName` | `given_name` |
166+
| Suffixe `_at` pour TIMESTAMPTZ | `datePublished` | `published_at` |
167+
| Suffixe `_cents` pour INT8 monétaire | `price` | `price_cents` |
168+
| Suffixe `_id` pour FK | `authorId` | `author_id` |
169+
| Suffixe `_code` pour codes numériques | `currencyCode` | `currency_code` |
170+
| Miroir du nom physique quand identique | `is_readable` | `is_readable` (pas `is_accessible_for_free`) |
171+
| `@type``doc_type` / `org_type` | `@type` | `doc_type`, `org_type` |
172+
173+
### Exceptions documentées — refus de `address_country` pour `country_code`
174+
175+
La suggestion d'aliaser `country_code` en `address_country` est refusée.
176+
`address_country` évoque une valeur textuelle ("France"), alors que le type
177+
physique est `SMALLINT` contenant un code ISO 3166-1 numérique (250).
178+
Un alias trompeur sur le type crée des erreurs de comparaison applicatives
179+
(`WHERE address_country = 'FR'` ne fonctionnerait pas sur un SMALLINT).
180+
Le nom `country_code` est conservé dans la table et dans la vue — il est
181+
auto-documenté : "c'est un code, pas un nom de pays".
182+
183+
### Colonne `content.identity.name``headline`
184+
185+
Renommage du nom physique, pas seulement de l'alias. `name` est ambigu dans
186+
le contexte d'un article (est-ce le titre, le nom de fichier, le nom d'auteur ?).
187+
`headline` est le terme exact schema.org/Article. Le renommage s'étend à
188+
`content.revision.snapshot_name → snapshot_headline` pour la cohérence du
189+
composant de versioning.
190+
191+
### Colonnes `org.org_legal`
192+
193+
`vat_number → vat_id` : le suffixe `_id` signale un identifiant externe (non
194+
une valeur calculée). Cohérent avec `duns` et `siret`. Ajout de `legal_name
195+
VARCHAR(128)` : raison sociale officielle, distincte du nom commercial dans
196+
`org.org_identity.name`.
197+
198+
### Impact applicatif
199+
200+
Les consommateurs existants de l'API doivent adapter leurs sélections :
201+
`"givenName"``given_name`, `"headline"``headline` (idem), etc. Ce
202+
changement est une rupture de contrat intentionnelle, effectuée en phase R&D
203+
avant toute mise en production.
204+
205+
---
206+
207+
## ADR-024 — Fragmentation geo, soft delete RGPD et compliance de production
208+
209+
**Statut** : Adopté
210+
211+
### Contexte
212+
213+
Quatre vecteurs de risque identifiés avant mise en production européenne :
214+
215+
1. `geo.place_core` mélange spine spatial (coordonnées KNN) et adresse postale (logistique).
216+
2. `org.org_legal.vat_number VARCHAR(15)` trop court pour les identifiants fiscaux internationaux.
217+
3. Absence de mécanisme de droit à l'oubli (RGPD art. 17) et de traçabilité du consentement.
218+
4. Absence de mention de droits dans les métadonnées médias (risque légal d'exploitation d'images).
219+
220+
---
221+
222+
### 1. Séparation `geo.place_core` / `geo.postal_address` (ECS spatial vs logistique)
223+
224+
**Problème** : `geo.place_core` mixait coordonnées GPS et adresse postale dans le même tuple.
225+
Les requêtes géospatiales (KNN `<->`, `ST_DWithin`) ne nécessitent que `id + coordinates`,
226+
mais chargeaient systématiquement `street`, `locality`, `region`, `country`, `postal_code`.
227+
228+
**Décision** : fragmentation en deux composants 1:1 :
229+
230+
| Composant | Sémantique schema.org | Contenu | Fréquence |
231+
| ------------------- | ---------------------- | ------------------------------------------- | --------- |
232+
| `geo.place_core` | `Place` | id, name, elevation, type_id, coordinates | Hot |
233+
| `geo.postal_address`| `PostalAddress` | country_code, locality, region, street, postal_code | Warm |
234+
235+
**Gain de densité** :
236+
237+
| État | Bytes/tuple | Tuples/page |
238+
| ------------ | ----------- | ----------- |
239+
| Avant (mixte)| ~211 B | ~38 |
240+
| Après (spine)| ~26–46 B | ~179–317 |
241+
242+
Les requêtes KNN sur 500 000 lieux ne chargent plus aucun octet postal.
243+
244+
**`country_code SMALLINT` (ISO 3166-1 numérique)** : cohérent avec `currency_code`
245+
d'ADR-022. 2 bytes pass-by-value vs `CHAR(2)` ou `VARCHAR(2)` varlena avec en-tête
246+
de 4 bytes. Le mapping vers le code alphabétique (`250 → "FR"`) est délégué à
247+
l'applicatif — table de lookup statique, zéro accès base de données.
248+
249+
---
250+
251+
### 2. `vat_number VARCHAR(32)` (compliance internationale)
252+
253+
`VARCHAR(15)` couvrait les numéros TVA européens (max 15 chars incluant le préfixe
254+
pays). Les identifiants fiscaux hors UE (GSTIN indien 15 chars avec format strict,
255+
CNPJ brésilien 18 chars avec ponctuation, CFE mexicain 13 chars) nécessitent
256+
davantage de marge. `VARCHAR(32)` absorbe tous les formats connus sans coût physique
257+
(varlena : seul le contenu réel est stocké).
258+
259+
---
260+
261+
### 3. Soft delete RGPD : `anonymized_at` + procédure `anonymize_person`
262+
263+
**Problème** : un `DELETE` physique d'une entité casserait les FK vers
264+
`commerce.transaction_core` — les commandes passéesdeviendraient incohérentes.
265+
Une suppression logicielle par colonne booléenne (`is_deleted`) serait ambiguë
266+
et difficile à auditer.
267+
268+
**Décision** : colonne `anonymized_at TIMESTAMPTZ NULL` dans `identity.entity` (spine).
269+
270+
- `NULL` = entité active.
271+
- Non-NULL = anonymisation exécutée, timestamp d'audit RGPD irréversible.
272+
273+
La procédure `identity.anonymize_person(p_entity_id)` (SECURITY DEFINER) efface
274+
en une transaction atomique :
275+
276+
| Composant | Action |
277+
| -------------------------- | -------------------------------------------------- |
278+
| `identity.entity` | `anonymized_at = now()` |
279+
| `identity.person_identity` | Tous les champs nominatifs → NULL |
280+
| `identity.person_contact` | email, phone, fax, url → NULL |
281+
| `identity.person_biography`| Dates et lieux → NULL |
282+
| `identity.person_content` | Textes personnels → NULL, media_id → NULL |
283+
| `identity.account_core` | username/slug → `user_<id>` (non nominatif) |
284+
| `identity.auth` | password_hash → `'ANONYMIZED'`, is_banned → true |
285+
| `identity.group_to_account`| Suppression (appartenance = donnée de traçabilité) |
286+
287+
L'entité physique subsiste dans `identity.entity` avec son `id` — toutes les FK
288+
`commerce.transaction_core.client_entity_id` restent valides.
289+
290+
**`tos_accepted_at TIMESTAMPTZ NULL`** dans `identity.account_core` : timestamp
291+
d'acceptation des CGU. NULL = non encore accepté. Placé en première colonne
292+
(TIMESTAMPTZ 8 bytes, ADR-004) pour zéro padding.
293+
294+
---
295+
296+
### 4. `copyright_notice VARCHAR(255)` dans `content.media_content`
297+
298+
Placé dans `media_content` (cold path, BASSE fréquence) et non dans `media_core`
299+
(hot path, HAUTE fréquence). Un `SELECT` sur les listings d'articles ne charge
300+
jamais la mention de droits — elle n'est projetée que lors de l'affichage complet
301+
d'un média ou de la génération d'une page légale.
302+
303+
---
304+
135305
## ADR-023 — Éclatement de l'agrégat de commande en composants ECS 1:1
136306

137307
**Statut** : Adopté
@@ -884,4 +1054,4 @@ au même titre que les `REVOKE` qui les suivent.
8841054

8851055
---
8861056

887-
*Architecture ECS/DOD · PostgreSQL 18 · Projet Marius · 23 décisions*
1057+
*Architecture ECS/DOD · PostgreSQL 18 · Projet Marius · 25 décisions*

0 commit comments

Comments
 (0)