|
14 | 14 |
|
15 | 15 | BEGIN; |
16 | 16 |
|
17 | | -SELECT plan(46); |
| 17 | +SELECT plan(63); |
18 | 18 |
|
19 | 19 |
|
20 | 20 | -- ============================================================ |
@@ -97,6 +97,43 @@ SELECT ok( |
97 | 97 | ); |
98 | 98 |
|
99 | 99 |
|
| 100 | +-- ============================================================ |
| 101 | +-- TYPES PHYSIQUES — Codes ISO pass-by-value (ADR-028) |
| 102 | +-- |
| 103 | +-- identity.person_identity.nationality : SMALLINT ISO 3166-1 numérique. |
| 104 | +-- Avant audit DOD : CHAR(2) alpha-2 → varlena, violait ADR-026 + ADR-028 |
| 105 | +-- et laissait un gap de 2B à l'offset 6-7 (après gender SMALLINT). |
| 106 | +-- Après correction : 2×SMALLINT (gender offset 4, nationality offset 6) |
| 107 | +-- consomment exactement les 4B post-INT4, zéro gap structurel. |
| 108 | +-- |
| 109 | +-- identity.account_core.language : VARCHAR(5) — pas CHAR(5). |
| 110 | +-- CHAR(5) est bpchar dans PostgreSQL : varlena avec padding/stripping CPU. |
| 111 | +-- ADR-026 : l'invariant de longueur est garanti par la DEFAULT 'fr_FR' et |
| 112 | +-- la contrainte applicative, pas par le type. |
| 113 | +-- ============================================================ |
| 114 | + |
| 115 | +SELECT col_type_is( |
| 116 | + 'identity', 'person_identity', 'nationality', |
| 117 | + 'smallint', |
| 118 | + 'person_identity.nationality : smallint ISO 3166-1 numérique (ADR-028 — gap offset 6-7 absorbé)' |
| 119 | +); |
| 120 | + |
| 121 | +SELECT col_type_is( |
| 122 | + 'identity', 'account_core', 'language', |
| 123 | + 'character varying(5)', |
| 124 | + 'account_core.language : character varying(5), pas bpchar (ADR-026)' |
| 125 | +); |
| 126 | + |
| 127 | +SELECT ok( |
| 128 | + EXISTS ( |
| 129 | + SELECT 1 FROM information_schema.check_constraints |
| 130 | + WHERE constraint_schema = 'identity' |
| 131 | + AND constraint_name = 'nationality_range' |
| 132 | + ), |
| 133 | + 'person_identity.nationality_range : contrainte CHECK 1-999 présente (ADR-028)' |
| 134 | +); |
| 135 | + |
| 136 | + |
100 | 137 | -- ============================================================ |
101 | 138 | -- ADR-024 — Colonnes de snapshot complètes dans content.revision |
102 | 139 | -- |
@@ -432,5 +469,181 @@ SELECT ok( |
432 | 469 | ); |
433 | 470 |
|
434 | 471 |
|
| 472 | +-- ============================================================ |
| 473 | +-- TOAST TUPLE TARGET — composants basse fréquence (Audit 1.2) |
| 474 | +-- |
| 475 | +-- toast_tuple_target = 128 force le moteur à TOASTer les varlena dès 128 B |
| 476 | +-- au lieu du seuil par défaut (~2 kB). Effet : les composants "cold" (textes longs, |
| 477 | +-- descriptions, corps HTML) ne chargent jamais de données textuelles lors des |
| 478 | +-- scans de métadonnées sur les tables "hot" associées. |
| 479 | +-- |
| 480 | +-- Invariant : seuls les composants explicitement déclarés basse fréquence portent |
| 481 | +-- ce paramètre. Les tables hot path (identity.auth, content.core, product_core) |
| 482 | +-- ne doivent PAS l'avoir — leur densité de tuple dépend de l'inline des varlena. |
| 483 | +-- ============================================================ |
| 484 | + |
| 485 | +SELECT ok( |
| 486 | + COALESCE( |
| 487 | + (SELECT reloptions @> ARRAY['toast_tuple_target=128'] |
| 488 | + FROM pg_class WHERE relname = 'place_content'), |
| 489 | + false |
| 490 | + ), |
| 491 | + 'TOAST geo.place_content : toast_tuple_target = 128 (corps textuel isolé)' |
| 492 | +); |
| 493 | + |
| 494 | +SELECT ok( |
| 495 | + COALESCE( |
| 496 | + (SELECT reloptions @> ARRAY['toast_tuple_target=128'] |
| 497 | + FROM pg_class WHERE relname = 'person_content'), |
| 498 | + false |
| 499 | + ), |
| 500 | + 'TOAST identity.person_content : toast_tuple_target = 128 (biographie, TRÈS BASSE fréquence)' |
| 501 | +); |
| 502 | + |
| 503 | +SELECT ok( |
| 504 | + COALESCE( |
| 505 | + (SELECT reloptions @> ARRAY['toast_tuple_target=128'] |
| 506 | + FROM pg_class WHERE relname = 'product_content'), |
| 507 | + false |
| 508 | + ), |
| 509 | + 'TOAST commerce.product_content : toast_tuple_target = 128 (description catalogue)' |
| 510 | +); |
| 511 | + |
| 512 | +SELECT ok( |
| 513 | + COALESCE( |
| 514 | + (SELECT reloptions @> ARRAY['toast_tuple_target=128'] |
| 515 | + FROM pg_class WHERE relname = 'body'), |
| 516 | + false |
| 517 | + ), |
| 518 | + 'TOAST content.body : toast_tuple_target = 128 (corps HTML, BASSE fréquence)' |
| 519 | +); |
| 520 | + |
| 521 | +SELECT ok( |
| 522 | + COALESCE( |
| 523 | + (SELECT reloptions @> ARRAY['toast_tuple_target=128'] |
| 524 | + FROM pg_class WHERE relname = 'revision'), |
| 525 | + false |
| 526 | + ), |
| 527 | + 'TOAST content.revision : toast_tuple_target = 128 (snapshots cold storage)' |
| 528 | +); |
| 529 | + |
| 530 | + |
| 531 | +-- ============================================================ |
| 532 | +-- FILLFACTOR — tables à HOT updates fréquents (Audit 2) |
| 533 | +-- |
| 534 | +-- HOT update (Heap Only Tuple) : PostgreSQL réutilise l'espace libre de la |
| 535 | +-- même page pour la nouvelle version du tuple, sans créer d'entrée d'index |
| 536 | +-- supplémentaire. Condition : la nouvelle version du tuple doit tenir dans la |
| 537 | +-- même page que l'ancienne → fillfactor réserve cet espace à l'avance. |
| 538 | +-- |
| 539 | +-- Sans fillfactor calibré, les HOT updates se dégradent en full updates : |
| 540 | +-- chaque mutation crée une nouvelle entrée dans tous les index de la table |
| 541 | +-- (dead tuple structurel + bloat index systématique). |
| 542 | +-- |
| 543 | +-- identity.auth=70 : last_login_at mis à jour à chaque connexion (hot path ADR-008). |
| 544 | +-- 30 % de marge = ~2,4 kB libre/page → absorbe ~15 connexions avant vacuum. |
| 545 | +-- content.core=75 : status, modified_at, is_commentable mutés fréquemment. |
| 546 | +-- commerce.product_core=80 : stock décrémenté à chaque transaction. |
| 547 | +-- ============================================================ |
| 548 | + |
| 549 | +SELECT ok( |
| 550 | + COALESCE( |
| 551 | + (SELECT reloptions @> ARRAY['fillfactor=70'] |
| 552 | + FROM pg_class WHERE relname = 'auth'), |
| 553 | + false |
| 554 | + ), |
| 555 | + 'fillfactor identity.auth = 70 (HOT updates last_login_at, ADR-008)' |
| 556 | +); |
| 557 | + |
| 558 | +SELECT ok( |
| 559 | + -- Audit 3 : fillfactor retiré de content.core (zero HOT benefit démontré). |
| 560 | + -- Tous les chemins d'UPDATE touchent des colonnes indexées (published_at, modified_at) |
| 561 | + -- ou des conditions de partial index (status). Le fillfactor<100 dégradait la densité |
| 562 | + -- sans aucune contrepartie HOT. On vérifie l'absence du paramètre. |
| 563 | + NOT COALESCE( |
| 564 | + (SELECT reloptions @> ARRAY['fillfactor=75'] |
| 565 | + FROM pg_class WHERE relname = 'core' |
| 566 | + AND relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'content')), |
| 567 | + false |
| 568 | + ), |
| 569 | + 'fillfactor content.core : paramètre absent (Audit 3 — zero HOT benefit, densité +25%)' |
| 570 | +); |
| 571 | + |
| 572 | +SELECT ok( |
| 573 | + COALESCE( |
| 574 | + (SELECT reloptions @> ARRAY['fillfactor=80'] |
| 575 | + FROM pg_class WHERE relname = 'product_core'), |
| 576 | + false |
| 577 | + ), |
| 578 | + 'fillfactor commerce.product_core = 80 (HOT updates stock, ADR-024 FOR UPDATE)' |
| 579 | +); |
| 580 | + |
| 581 | + |
| 582 | +-- ============================================================ |
| 583 | +-- INDEX PARTIELS — couverture des hot scans (Audit 2) |
| 584 | +-- |
| 585 | +-- product_core_catalog (Audit 2 — gap identifié) : |
| 586 | +-- Avant correction : aucun index sur is_available. Un listing catalogue |
| 587 | +-- déclenchait un seq scan sur product_core entier, y compris les produits |
| 588 | +-- désactivés (cold data). L'index partial filtre ce segment dès le parcours. |
| 589 | +-- |
| 590 | +-- core_modified (Audit 2 — gap identifié) : |
| 591 | +-- Avant correction : seul BRIN(created_at) était présent — optimisé pour les |
| 592 | +-- scans par plage de création, pas pour ORDER BY modified_at DESC (dashboard |
| 593 | +-- éditorial "derniers articles modifiés"). L'index partial exclut les |
| 594 | +-- brouillons jamais modifiés (modified_at IS NOT NULL), réduisant sa surface. |
| 595 | +-- ============================================================ |
| 596 | + |
| 597 | +SELECT ok( |
| 598 | + EXISTS ( |
| 599 | + SELECT 1 FROM pg_indexes |
| 600 | + WHERE schemaname = 'commerce' |
| 601 | + AND tablename = 'product_core' |
| 602 | + AND indexname = 'product_core_catalog' |
| 603 | + ), |
| 604 | + 'Index product_core_catalog présent : scan catalogue sans seq scan (Audit 2)' |
| 605 | +); |
| 606 | + |
| 607 | +SELECT ok( |
| 608 | + EXISTS ( |
| 609 | + SELECT 1 FROM pg_indexes |
| 610 | + WHERE schemaname = 'content' |
| 611 | + AND tablename = 'core' |
| 612 | + AND indexname = 'core_modified' |
| 613 | + ), |
| 614 | + 'Index core_modified présent : dashboard éditorial ORDER BY modified_at (Audit 2)' |
| 615 | +); |
| 616 | + |
| 617 | + |
| 618 | +-- ============================================================ |
| 619 | +-- BRIN IMMUTABILITÉ — created_at ne doit jamais être modifié (Audit 3) |
| 620 | +-- |
| 621 | +-- Un UPDATE sur created_at invalide la corrélation physique/logique de l'index |
| 622 | +-- BRIN et peut produire des faux négatifs (lignes exclues à tort lors d'un |
| 623 | +-- scan de zone). Les quatre triggers ci-dessous lèvent SQLSTATE 55000 |
| 624 | +-- si OLD.created_at IS DISTINCT FROM NEW.created_at. |
| 625 | +-- ============================================================ |
| 626 | + |
| 627 | +SELECT has_trigger( |
| 628 | + 'identity', 'auth', 'auth_deny_created_at_update', |
| 629 | + 'Trigger auth_deny_created_at_update : created_at immuable sur identity.auth (Audit 3)' |
| 630 | +); |
| 631 | + |
| 632 | +SELECT has_trigger( |
| 633 | + 'content', 'core', 'core_deny_created_at_update', |
| 634 | + 'Trigger core_deny_created_at_update : created_at immuable sur content.core (Audit 3)' |
| 635 | +); |
| 636 | + |
| 637 | +SELECT has_trigger( |
| 638 | + 'commerce', 'transaction_core', 'transaction_deny_created_at_update', |
| 639 | + 'Trigger transaction_deny_created_at_update : created_at immuable sur commerce.transaction_core (Audit 3)' |
| 640 | +); |
| 641 | + |
| 642 | +SELECT has_trigger( |
| 643 | + 'org', 'org_core', 'org_core_deny_created_at_update', |
| 644 | + 'Trigger org_core_deny_created_at_update : created_at immuable sur org.org_core (Audit 3)' |
| 645 | +); |
| 646 | + |
| 647 | + |
435 | 648 | SELECT * FROM finish(); |
436 | 649 | ROLLBACK; |
0 commit comments