20260731_400_quality_operations.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. """Add generic deterministic quality profiling, findings and SLA events."""
  2. from alembic import op
  3. revision = "20260731_400"
  4. down_revision = "20260731_390"
  5. branch_labels = None
  6. depends_on = None
  7. def upgrade() -> None:
  8. op.execute(
  9. """
  10. CREATE TABLE public.quality_templates (
  11. uid UUID PRIMARY KEY,
  12. code VARCHAR(120) NOT NULL UNIQUE,
  13. name VARCHAR(300) NOT NULL,
  14. owner_uid UUID NOT NULL REFERENCES public.users(id),
  15. status VARCHAR(20) NOT NULL CHECK (
  16. status IN ('draft','published','retired')
  17. ),
  18. current_version INTEGER NOT NULL CHECK (current_version > 0),
  19. active_version_uid UUID,
  20. created_by UUID NOT NULL REFERENCES public.users(id),
  21. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  22. updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
  23. );
  24. CREATE TABLE public.quality_template_versions (
  25. uid UUID PRIMARY KEY,
  26. template_uid UUID NOT NULL
  27. REFERENCES public.quality_templates(uid),
  28. version INTEGER NOT NULL CHECK (version > 0),
  29. status VARCHAR(20) NOT NULL CHECK (
  30. status IN ('draft','published','superseded')
  31. ),
  32. definition JSONB NOT NULL,
  33. content_hash CHAR(64) NOT NULL,
  34. created_by UUID NOT NULL REFERENCES public.users(id),
  35. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  36. published_by UUID REFERENCES public.users(id),
  37. published_at TIMESTAMPTZ,
  38. UNIQUE (template_uid, version),
  39. UNIQUE (template_uid, content_hash),
  40. CHECK (jsonb_typeof(definition) = 'object')
  41. );
  42. ALTER TABLE public.quality_templates
  43. ADD CONSTRAINT fk_quality_template_active_version
  44. FOREIGN KEY (active_version_uid)
  45. REFERENCES public.quality_template_versions(uid);
  46. CREATE UNIQUE INDEX uq_quality_template_published_version
  47. ON public.quality_template_versions(template_uid)
  48. WHERE status = 'published';
  49. CREATE TABLE public.quality_profile_runs (
  50. uid UUID PRIMARY KEY,
  51. template_uid UUID NOT NULL
  52. REFERENCES public.quality_templates(uid),
  53. template_version_uid UUID NOT NULL
  54. REFERENCES public.quality_template_versions(uid),
  55. template_hash CHAR(64) NOT NULL,
  56. asset_uid UUID NOT NULL
  57. REFERENCES public.active_metadata_assets(uid),
  58. source_uid UUID NOT NULL
  59. REFERENCES public.ingestion_sources(uid),
  60. business_domain_uid VARCHAR(200),
  61. batch_key VARCHAR(160) NOT NULL,
  62. status VARCHAR(20) NOT NULL CHECK (status IN ('success','failed')),
  63. row_count INTEGER NOT NULL CHECK (row_count > 0),
  64. score NUMERIC(6,2) NOT NULL CHECK (
  65. score >= 0 AND score <= 100
  66. ),
  67. source_observed_at TIMESTAMPTZ NOT NULL,
  68. previous_run_uid UUID
  69. REFERENCES public.quality_profile_runs(uid),
  70. comparison JSONB NOT NULL,
  71. profile JSONB NOT NULL,
  72. field_bindings JSONB NOT NULL,
  73. finding_count INTEGER NOT NULL CHECK (finding_count >= 0),
  74. deterministic BOOLEAN NOT NULL DEFAULT TRUE CHECK (deterministic),
  75. created_by UUID NOT NULL REFERENCES public.users(id),
  76. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  77. UNIQUE (asset_uid, batch_key),
  78. CHECK (jsonb_typeof(comparison) = 'object'),
  79. CHECK (jsonb_typeof(profile) = 'object'),
  80. CHECK (jsonb_typeof(field_bindings) = 'object')
  81. );
  82. CREATE TABLE public.quality_profile_metrics (
  83. uid UUID PRIMARY KEY,
  84. run_uid UUID NOT NULL
  85. REFERENCES public.quality_profile_runs(uid) ON DELETE CASCADE,
  86. field_name VARCHAR(200) NOT NULL,
  87. metric_value JSONB NOT NULL,
  88. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  89. UNIQUE (run_uid, field_name),
  90. CHECK (jsonb_typeof(metric_value) = 'object')
  91. );
  92. CREATE TABLE public.quality_findings (
  93. uid UUID PRIMARY KEY,
  94. run_uid UUID NOT NULL
  95. REFERENCES public.quality_profile_runs(uid) ON DELETE CASCADE,
  96. asset_uid UUID NOT NULL
  97. REFERENCES public.active_metadata_assets(uid),
  98. finding_type VARCHAR(30) NOT NULL CHECK (
  99. finding_type IN (
  100. 'completeness','uniqueness','pattern','duplicate',
  101. 'outlier','volume','distribution','schema','freshness'
  102. )
  103. ),
  104. field_name VARCHAR(200),
  105. severity VARCHAR(20) NOT NULL CHECK (
  106. severity IN ('info','warning','error','critical')
  107. ),
  108. status VARCHAR(20) NOT NULL CHECK (
  109. status IN ('open','acknowledged','resolved')
  110. ),
  111. actual JSONB NOT NULL,
  112. expected JSONB NOT NULL,
  113. recurrence_key CHAR(64) NOT NULL,
  114. occurrence_number INTEGER NOT NULL CHECK (occurrence_number > 0),
  115. evidence JSONB NOT NULL,
  116. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  117. CHECK (jsonb_typeof(actual) IN ('object','number','string')),
  118. CHECK (jsonb_typeof(expected) IN ('object','number','string')),
  119. CHECK (jsonb_typeof(evidence) = 'object')
  120. );
  121. CREATE TABLE public.quality_sla_events (
  122. uid UUID PRIMARY KEY,
  123. run_uid UUID NOT NULL
  124. REFERENCES public.quality_profile_runs(uid) ON DELETE CASCADE,
  125. asset_uid UUID NOT NULL
  126. REFERENCES public.active_metadata_assets(uid),
  127. sla_type VARCHAR(30) NOT NULL CHECK (
  128. sla_type IN ('freshness','quality_score')
  129. ),
  130. status VARCHAR(20) NOT NULL CHECK (
  131. status IN ('met','violated','recovered')
  132. ),
  133. severity VARCHAR(20) NOT NULL CHECK (
  134. severity IN ('info','warning','error','critical')
  135. ),
  136. actual NUMERIC NOT NULL,
  137. threshold NUMERIC NOT NULL,
  138. owner_uid UUID NOT NULL REFERENCES public.users(id),
  139. escalation_level INTEGER NOT NULL CHECK (
  140. escalation_level BETWEEN 0 AND 3
  141. ),
  142. evidence JSONB NOT NULL,
  143. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  144. UNIQUE (run_uid, sla_type),
  145. CHECK (jsonb_typeof(evidence) = 'object')
  146. );
  147. CREATE INDEX idx_quality_runs_asset_created
  148. ON public.quality_profile_runs(asset_uid, created_at DESC);
  149. CREATE INDEX idx_quality_runs_domain_created
  150. ON public.quality_profile_runs(
  151. business_domain_uid, created_at DESC
  152. );
  153. CREATE INDEX idx_quality_findings_recurrence
  154. ON public.quality_findings(
  155. recurrence_key, occurrence_number DESC
  156. );
  157. CREATE INDEX idx_quality_findings_asset_status
  158. ON public.quality_findings(asset_uid, status, created_at DESC);
  159. CREATE INDEX idx_quality_sla_asset_status
  160. ON public.quality_sla_events(
  161. asset_uid, sla_type, status, created_at DESC
  162. );
  163. """
  164. )
  165. def downgrade() -> None:
  166. raise RuntimeError(
  167. "quality operation evidence is append-only; "
  168. "downgrade requires an approved archival migration"
  169. )