Skip to content

Commit 6309758

Browse files
committed
feat: add tools for postgres
1 parent 1b7d6de commit 6309758

5 files changed

Lines changed: 611 additions & 0 deletions

File tree

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
# Outillage `meta` — Mode d'emploi
2+
**Projet Marius · Architecture ECS/DOD · PostgreSQL 18**
3+
4+
---
5+
6+
## Contexte
7+
8+
Le schéma `meta` héberge trois outils d'ingénierie complémentaires. Ils s'appuient sur `meta.containment_intent` (registre AOT des composants) et sur `pg_catalog`. Aucun n'est destiné au runtime applicatif — accès réservé à `marius_admin` et `postgres`.
9+
10+
---
11+
12+
## 1. `meta.f_generate_dod_template` — Générateur de DDL aligné CPU
13+
14+
### Raison d'être
15+
16+
Dans une architecture DOD, l'ordre des colonnes dans une table n'est pas cosmétique : il détermine le padding d'alignement CPU entre chaque champ. Un ordre naïf (tel que dicté par la logique métier) peut insérer plusieurs octets de padding par tuple, ce qui réduit la densité et augmente la pression cache.
17+
18+
Cette fonction prend une liste de colonnes brutes et produit trois livrables en un seul appel :
19+
20+
- le `CREATE TABLE` avec les colonnes triées dans l'ordre optimal (8 B → 4 B → 2 B → 1 B → varlena),
21+
- un bloc de commentaires SQL détaillant le layout mémoire colonne par colonne (offset, taille, padding),
22+
- l'`INSERT INTO meta.containment_intent` pré-calculé pour armer le registre d'audit.
23+
24+
### Utilisation
25+
26+
```sql
27+
SELECT meta.f_generate_dod_template(
28+
'commerce.my_table',
29+
ARRAY[
30+
'id int8 GENERATED ALWAYS AS IDENTITY',
31+
'created_at timestamptz NOT NULL',
32+
'status smallint NOT NULL DEFAULT 0',
33+
'is_active boolean NOT NULL DEFAULT true',
34+
'label varchar(64)'
35+
]
36+
);
37+
```
38+
39+
La fonction retourne un bloc TEXT à copier-coller directement dans le DDL de migration. Exemple de sortie :
40+
41+
```sql
42+
-- ============================================================
43+
-- DOD TEMPLATE : commerce.my_table
44+
-- ------------------------------------------------------------
45+
-- Fixed-Length Header : 24 B
46+
-- (23 B base + 1 B null bitmap [ceil(5 cols / 8)] -> MAXALIGN 8)
47+
-- ------------------------------------------------------------
48+
-- Layout memoire (Ordre DOD) :
49+
-- id (int8 ) : offset 24 B, size 8 B, padding_before 0 B
50+
-- created_at (timestamptz ) : offset 32 B, size 8 B, padding_before 0 B
51+
-- status (smallint ) : offset 40 B, size 2 B, padding_before 0 B
52+
-- is_active (boolean ) : offset 42 B, size 1 B, padding_before 0 B
53+
-- label (varchar(64) ) : offset 44 B, size 4 B, padding_before 1 B [varlena -- 4 B pre-ANALYZE]
54+
-- ------------------------------------------------------------
55+
-- Ordre naif (input) : 56 B / tuple
56+
-- Ordre DOD optimise : 48 B / tuple
57+
-- Padding economise : 8 B / tuple
58+
-- ============================================================
59+
CREATE TABLE commerce.my_table (
60+
id int8 GENERATED ALWAYS AS IDENTITY,
61+
created_at timestamptz NOT NULL,
62+
status smallint NOT NULL DEFAULT 0,
63+
is_active boolean NOT NULL DEFAULT true,
64+
label varchar(64)
65+
);
66+
67+
-- Armement du registre AOT
68+
INSERT INTO meta.containment_intent
69+
(component_id, intent_density_bytes)
70+
VALUES
71+
('commerce.my_table', 48)
72+
ON CONFLICT (component_id)
73+
DO UPDATE SET intent_density_bytes = EXCLUDED.intent_density_bytes;
74+
```
75+
76+
### Points d'attention
77+
78+
- Les types custom (PostGIS `geometry`, `ltree`, etc.) sont supportés à condition que l'extension soit installée.
79+
- Les colonnes varlena (`text`, `varchar`, `jsonb`...) sont toujours reléguées en fin de table (Fixed-Length Prefixing) et leur taille est fixée à 4 B dans la simulation. La valeur `intent_density_bytes` doit être réévaluée après `ANALYZE` via `meta.v_extended_containment_security_matrix`.
80+
- Le format `p_table_name` doit respecter `schema.table` en snake_case, lettre initiale obligatoire — conforme au `CHECK` constraint de `meta.containment_intent`.
81+
82+
---
83+
84+
## 2. `meta.f_compile_entity_profile` — Compilateur AOT de la vue de profil ECS
85+
86+
### Raison d'être
87+
88+
Dans une architecture ECS, une entité est un identifiant entier pur. Ses données sont fragmentées dans des tables de composants indépendantes (`identity.auth`, `content.core`, etc.). Il n'existe aucune table centrale listant quels composants sont actifs pour un identifiant donné.
89+
90+
Cette fonction génère et exécute dynamiquement la vue `meta.v_entity_profile`, qui permet de répondre à la question de debugging : *"Quels composants sont peuplés pour l'entité #N ?"*
91+
92+
Le principe AOT (Ahead-Of-Time) est central : l'introspection du catalogue (`pg_catalog`, `meta.containment_intent`) n'a lieu qu'une seule fois — à la compilation. La vue résultante est un `UNION ALL` pur, sans aucun accès catalogue au runtime.
93+
94+
### Utilisation
95+
96+
**Compiler (ou recompiler après ajout d'un composant) :**
97+
98+
```sql
99+
SELECT meta.f_compile_entity_profile();
100+
```
101+
102+
La fonction retourne le DDL compilé pour inspection, puis l'exécute. Exemple de sortie :
103+
104+
```sql
105+
CREATE OR REPLACE VIEW meta.v_entity_profile AS
106+
SELECT
107+
spine_id,
108+
spine_type,
109+
array_agg(component_name ORDER BY component_name) AS active_components
110+
FROM (
111+
SELECT entity_id AS spine_id, 'entity_id' AS spine_type, 'identity.auth' AS component_name FROM identity.auth
112+
UNION ALL
113+
SELECT document_id AS spine_id, 'document_id' AS spine_type, 'content.core' AS component_name FROM content.core
114+
) AS raw_components
115+
GROUP BY spine_id, spine_type
116+
```
117+
118+
**Interroger la vue une fois compilée :**
119+
120+
```sql
121+
-- Tous les composants actifs pour l'entité #42
122+
SELECT spine_id, spine_type, active_components
123+
FROM meta.v_entity_profile
124+
WHERE spine_id = 42;
125+
```
126+
127+
```
128+
spine_id | spine_type | active_components
129+
----------+------------+--------------------------------
130+
42 | entity_id | {identity.auth}
131+
42 | document_id| {content.core}
132+
```
133+
134+
### Points d'attention
135+
136+
- **Recompiler après chaque ajout de composant** dans `meta.containment_intent`. La vue n'est pas auto-rafraîchie.
137+
- La colonne `spine_type` est obligatoire pour lever l'ambiguïté : `identity.entity` et `content.document` ont des séquences d'identifiants indépendantes. Un `spine_id = 7` peut désigner simultanément une entité et un document — le `spine_type` les distingue.
138+
- Seuls les composants possédant une colonne de liaison directe vers une spine (`entity_id`, `document_id`, ou `id` avec FK vérifiée via `pg_constraint`) sont inclus. Les composants sans liaison spine directe (`commerce.transaction_item`, `content.tag_hierarchy`, etc.) sont silencieusement exclus — ils ne portent pas d'identifiant d'entité au sens ECS.
139+
140+
---
141+
142+
## 3. `meta.v_performance_sentinel` — Sentinel de performance DOD
143+
144+
### Raison d'être
145+
146+
Les invariants de performance du projet (densité DOD, HOT-eligibility, corrélation BRIN) peuvent être dégradés silencieusement par une modification de schéma : ajout d'un index sur une colonne fréquemment mise à jour, réorganisation physique des lignes, ou dérive de densité après une vague d'insertions imprévues.
147+
148+
Cette vue croise les statistiques runtime (`pg_stat_user_tables`, `pg_stats`) avec le registre d'intention (`meta.containment_intent`) pour lever trois types d'alertes booléennes par composant enregistré.
149+
150+
### Les trois alertes
151+
152+
| Alerte | Condition | Cause typique |
153+
|---|---|---|
154+
| `hot_blocker_alert` | `n_tup_upd > 0` ET colonne indexée non-immutable | Ajout d'un index sur une colonne mise à jour fréquemment |
155+
| `brin_drift_alert` | `abs(brin_correlation) < 0.90` | Insertions non séquentielles sur une table avec index BRIN |
156+
| `bloat_alert` | `pg_relation_size / n_live_tup > (intent_density / fillfactor) * 1.20` | Padding structurel, varlena plus large que prévu, dead tuples |
157+
158+
### Utilisation
159+
160+
```sql
161+
-- Vue d'ensemble : tous les composants avec au moins une alerte active
162+
SELECT component_id,
163+
hot_blocker_alert, hot_blocker_cols,
164+
brin_drift_alert, brin_worst_col, brin_correlation,
165+
bloat_alert, observed_bytes_per_tuple, bloat_threshold_bytes,
166+
fillfactor_pct, n_live_tup
167+
FROM meta.v_performance_sentinel
168+
WHERE hot_blocker_alert
169+
OR brin_drift_alert
170+
OR bloat_alert = TRUE;
171+
172+
-- Détail d'un composant spécifique
173+
SELECT * FROM meta.v_performance_sentinel
174+
WHERE component_id = 'content.core';
175+
```
176+
177+
### Colonnes de diagnostic
178+
179+
| Colonne | Usage |
180+
|---|---|
181+
| `hot_blocker_cols` | Noms des colonnes qui bloquent HOT (à dé-indexer ou déclarer immutable) |
182+
| `brin_worst_col` | Colonne BRIN la plus dégradée (candidat à un `CLUSTER` ou `VACUUM FULL`) |
183+
| `brin_correlation` | Valeur abs de corrélation (1.0 = parfait, < 0.9 = drift) |
184+
| `observed_bytes_per_tuple` | Densité observée brute (pg_relation_size / n_live_tup) |
185+
| `bloat_threshold_bytes` | Seuil effectif après correction fillfactor (pour comprendre le calcul) |
186+
| `fillfactor_pct` | Fillfactor lu depuis `pg_class.reloptions` (100 si absent) |
187+
| `n_live_tup` | Tuples vivants selon `pg_stat_user_tables` — NULL ou 0 = ANALYZE requis |
188+
189+
### Points d'attention
190+
191+
- **Prérequis : `ANALYZE`** sur les tables auditées. Sans statistiques, `brin_correlation` est NULL (pas d'alerte BRIN) et `n_live_tup` peut être sous-estimé (alertes bloat silencieuses ou erronées).
192+
- `bloat_alert = NULL` (et non FALSE) indique une table vide ou sans statistiques — à distinguer d'une absence de dérive.
193+
- La correction fillfactor est automatique : les tables `identity.auth` (ff=70) et `commerce.product_core` (ff=80) n'afficheront pas de faux positif bloat sur un layout sain.
194+
- La vue est en lecture seule, sans effet de bord. Elle peut être interrogée à tout moment en session `marius_admin`.
195+
196+
---
197+
198+
## Workflow recommandé
199+
200+
```
201+
Nouveau composant
202+
203+
204+
1. meta.f_generate_dod_template(...)
205+
└─ Obtenir le DDL trié + intent_density_bytes
206+
207+
208+
2. Appliquer le DDL (CREATE TABLE) + INSERT meta.containment_intent
209+
210+
211+
3. meta.f_compile_entity_profile()
212+
└─ Recompiler meta.v_entity_profile si le composant porte entity_id / document_id
213+
214+
215+
4. ANALYZE <schema>.<table>
216+
217+
218+
5. SELECT * FROM meta.v_performance_sentinel WHERE component_id = '<schema>.<table>'
219+
└─ Vérifier : hot_blocker_alert, brin_drift_alert, bloat_alert tous à FALSE
220+
```
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
-- ==============================================================================
2+
-- meta.f_compile_entity_profile
3+
-- Architecture ECS/DOD · PostgreSQL 18 · Projet Marius
4+
--
5+
-- Compilateur AOT : genere et execute dynamiquement meta.v_entity_profile.
6+
-- Le catalogue pg_catalog n'est interroge qu'une seule fois, a la compilation.
7+
-- La vue resultante est un UNION ALL pur, sans acces catalogue au runtime.
8+
--
9+
-- Algorithme :
10+
-- 1. Itere sur meta.containment_intent (composants enregistres, resolus via
11+
-- to_regclass).
12+
-- 2. Pour chaque composant, detecte la colonne de liaison (spine FK) avec
13+
-- priorite : entity_id > document_id > id.
14+
-- Guard pour 'id' : verifie l'existence d'un pg_constraint contype='f'
15+
-- sur cette colonne -- exclut les PK autogeneres (ex: commerce.product_core).
16+
-- 3. Composants sans spine FK detecte : ignores silencieusement.
17+
-- 4. Assemble le DDL : UNION ALL des SELECT spine_id, spine_type, component_name,
18+
-- suivi d'un GROUP BY spine_id, spine_type.
19+
-- 5. EXECUTE le DDL (CREATE OR REPLACE VIEW).
20+
-- 6. Retourne le DDL compile (inspecter avant deploiement, loggable, auditee).
21+
--
22+
-- Note sur spine_type :
23+
-- Le UNION ALL melange les sequences de spines distinctes (identity.entity,
24+
-- content.document, ...). Un spine_id=7 peut designer simultanement une entite
25+
-- et un document -- les namespaces sont disjoints. La colonne spine_type
26+
-- ('entity_id', 'document_id', 'id') disambigue la cle composite.
27+
--
28+
-- Usage :
29+
-- SELECT meta.f_compile_entity_profile(); -- compile + execute
30+
-- \gset -- puis inspecter la vue :
31+
-- SELECT * FROM meta.v_entity_profile WHERE spine_id = 1;
32+
-- ==============================================================================
33+
34+
CREATE OR REPLACE FUNCTION meta.f_compile_entity_profile()
35+
RETURNS TEXT
36+
LANGUAGE plpgsql
37+
SECURITY DEFINER
38+
SET search_path = 'meta', 'pg_catalog'
39+
AS $$
40+
DECLARE
41+
v_union_parts TEXT := '';
42+
v_sep TEXT := '';
43+
v_ddl TEXT;
44+
v_spine_col TEXT;
45+
r RECORD;
46+
BEGIN
47+
-- ---- 1. Iteration sur les composants enregistres et resolus ---------------
48+
FOR r IN
49+
SELECT ci.component_id,
50+
to_regclass(ci.component_id)::oid AS reloid
51+
FROM meta.containment_intent ci
52+
WHERE to_regclass(ci.component_id) IS NOT NULL
53+
ORDER BY ci.component_id -- ordre deterministe -> DDL reproductible
54+
LOOP
55+
-- ---- 2. Detection de la colonne spine FK (priorite stricte) -----------
56+
-- Priorite : entity_id(1) > document_id(2) > id(3).
57+
-- Pour 'id' : guard pg_constraint -- la colonne doit etre contrainte par
58+
-- une FK (contype='f') sur ce composant. Cela exclut les PK autogeneres
59+
-- (GENERATED ALWAYS AS IDENTITY sans FK sortante) comme product_core.id.
60+
SELECT candidates.col
61+
INTO v_spine_col
62+
FROM (VALUES ('entity_id', 1), ('document_id', 2), ('id', 3))
63+
AS candidates(col, prio)
64+
WHERE EXISTS (
65+
SELECT 1
66+
FROM pg_attribute a
67+
WHERE a.attrelid = r.reloid
68+
AND a.attname = candidates.col
69+
AND a.attnum > 0
70+
AND NOT a.attisdropped
71+
)
72+
AND (
73+
-- entity_id et document_id : presence suffit (convention FK du projet)
74+
candidates.col <> 'id'
75+
OR
76+
-- 'id' : verification pg_constraint -- FK obligatoire
77+
EXISTS (
78+
SELECT 1
79+
FROM pg_constraint c
80+
JOIN pg_attribute ca ON ca.attrelid = c.conrelid
81+
AND ca.attnum = ANY(c.conkey)
82+
WHERE c.conrelid = r.reloid
83+
AND c.contype = 'f'
84+
AND ca.attname = 'id'
85+
)
86+
)
87+
ORDER BY prio
88+
LIMIT 1;
89+
90+
-- Aucune spine FK : composant ignore (transaction_item, tag_hierarchy, etc.)
91+
IF v_spine_col IS NULL THEN
92+
CONTINUE;
93+
END IF;
94+
95+
-- ---- 3. Assemblage du bras UNION ALL ----------------------------------
96+
v_union_parts := v_union_parts
97+
|| v_sep
98+
|| format(
99+
' SELECT %I AS spine_id, %L AS spine_type, %L AS component_name'
100+
' FROM %s',
101+
v_spine_col,
102+
v_spine_col, -- spine_type = nom de la colonne source
103+
r.component_id,
104+
r.component_id
105+
);
106+
v_sep := E'\n UNION ALL\n';
107+
v_spine_col := NULL;
108+
END LOOP;
109+
110+
-- ---- Guard : aucun composant eligible ------------------------------------
111+
IF v_union_parts = '' THEN
112+
RAISE EXCEPTION
113+
'f_compile_entity_profile: aucun composant eligible trouve dans '
114+
'meta.containment_intent. Aucune colonne entity_id / document_id / '
115+
'id (FK) n''a ete detectee sur les composants enregistres.';
116+
END IF;
117+
118+
-- ---- 4. Assemblage DDL complet -------------------------------------------
119+
v_ddl :=
120+
'CREATE OR REPLACE VIEW meta.v_entity_profile AS' || E'\n'
121+
'SELECT' || E'\n'
122+
' spine_id,' || E'\n'
123+
' spine_type,' || E'\n'
124+
' array_agg(component_name ORDER BY component_name)'
125+
' AS active_components' || E'\n'
126+
'FROM (' || E'\n'
127+
|| v_union_parts || E'\n'
128+
|| ') AS raw_components' || E'\n'
129+
'GROUP BY spine_id, spine_type';
130+
131+
-- ---- 5. Execution --------------------------------------------------------
132+
EXECUTE v_ddl;
133+
134+
-- ---- 6. Retour du DDL compile (auditabilite AOT) -------------------------
135+
RETURN v_ddl;
136+
END;
137+
$$;
138+
139+
COMMENT ON FUNCTION meta.f_compile_entity_profile() IS
140+
'Compilateur AOT : genere et execute meta.v_entity_profile. '
141+
'Detecte les colonnes spine FK (entity_id > document_id > id+FK-guard) '
142+
'sur les composants enregistres dans meta.containment_intent. '
143+
'Retourne le DDL compile pour inspection et audit. '
144+
'ADR-001 / ADR-013 -- meta_registry v2.';
145+
146+
-- ---- DCL : verrouillage des privileges d''execution -------------------------
147+
-- La fonction execute un CREATE OR REPLACE VIEW sous SECURITY DEFINER (postgres).
148+
-- PUBLIC ne doit pas pouvoir recompiler le profil depuis un contexte applicatif.
149+
REVOKE ALL ON FUNCTION meta.f_compile_entity_profile() FROM PUBLIC;
150+
GRANT EXECUTE ON FUNCTION meta.f_compile_entity_profile() TO marius_admin;
File renamed without changes.

0 commit comments

Comments
 (0)