20260802_440_product_governance.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. """Add governed data products, applications, contracts and certificates."""
  2. from alembic import op
  3. revision = "20260802_440"
  4. down_revision = "20260802_430"
  5. branch_labels = None
  6. depends_on = None
  7. def upgrade() -> None:
  8. op.execute(
  9. """
  10. CREATE TABLE public.governed_data_products (
  11. uid UUID PRIMARY KEY,
  12. legacy_product_id INTEGER NOT NULL UNIQUE
  13. REFERENCES public.data_products(id) ON DELETE RESTRICT,
  14. product_code VARCHAR(120) NOT NULL UNIQUE,
  15. name VARCHAR(300) NOT NULL,
  16. product_type VARCHAR(30) NOT NULL CHECK (
  17. product_type IN ('database','api','file','data_product')
  18. ),
  19. owner_uid UUID NOT NULL REFERENCES public.users(id),
  20. business_domain_uid UUID NOT NULL,
  21. description VARCHAR(2000) NOT NULL,
  22. quality_target NUMERIC(6,2) NOT NULL CHECK (
  23. quality_target >= 0 AND quality_target <= 100
  24. ),
  25. sla JSONB NOT NULL,
  26. status VARCHAR(20) NOT NULL CHECK (
  27. status IN ('draft','in_review','active','suspended','retired')
  28. ),
  29. current_version INTEGER NOT NULL DEFAULT 1 CHECK (current_version > 0),
  30. created_by UUID NOT NULL REFERENCES public.users(id),
  31. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  32. updated_by UUID NOT NULL REFERENCES public.users(id),
  33. updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  34. retired_at TIMESTAMPTZ,
  35. CHECK (jsonb_typeof(sla) = 'object')
  36. );
  37. CREATE INDEX idx_governed_product_domain_status
  38. ON public.governed_data_products(
  39. business_domain_uid, status, updated_at DESC
  40. );
  41. CREATE INDEX idx_governed_product_owner_status
  42. ON public.governed_data_products(owner_uid, status, updated_at DESC);
  43. CREATE TABLE public.data_product_governance_events (
  44. uid UUID PRIMARY KEY,
  45. product_uid UUID NOT NULL
  46. REFERENCES public.governed_data_products(uid) ON DELETE CASCADE,
  47. product_version INTEGER NOT NULL CHECK (product_version > 0),
  48. action VARCHAR(50) NOT NULL,
  49. actor_uid UUID NOT NULL REFERENCES public.users(id),
  50. payload JSONB NOT NULL DEFAULT '{}'::jsonb,
  51. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  52. CHECK (jsonb_typeof(payload) = 'object')
  53. );
  54. CREATE INDEX idx_product_governance_timeline
  55. ON public.data_product_governance_events(product_uid, created_at, uid);
  56. CREATE TABLE public.data_product_applications (
  57. uid UUID PRIMARY KEY,
  58. application_code VARCHAR(40) NOT NULL UNIQUE,
  59. title VARCHAR(300) NOT NULL,
  60. source_type VARCHAR(30) NOT NULL CHECK (
  61. source_type IN ('database','api','file','data_product')
  62. ),
  63. source_ref JSONB NOT NULL,
  64. business_domain_uid UUID NOT NULL,
  65. purpose VARCHAR(1000) NOT NULL,
  66. requested_fields JSONB NOT NULL,
  67. status VARCHAR(30) NOT NULL CHECK (
  68. status IN (
  69. 'draft','pending_approval','approved','rejected',
  70. 'fulfilled','cancelled'
  71. )
  72. ),
  73. approval_task_uid UUID REFERENCES public.governance_tasks(uid),
  74. product_uid UUID REFERENCES public.governed_data_products(uid),
  75. current_version INTEGER NOT NULL DEFAULT 1 CHECK (current_version > 0),
  76. created_by UUID NOT NULL REFERENCES public.users(id),
  77. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  78. updated_by UUID NOT NULL REFERENCES public.users(id),
  79. updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  80. CHECK (jsonb_typeof(source_ref) = 'object'),
  81. CHECK (jsonb_typeof(requested_fields) = 'array')
  82. );
  83. CREATE INDEX idx_product_application_worklist
  84. ON public.data_product_applications(
  85. created_by, status, updated_at DESC
  86. );
  87. CREATE INDEX idx_product_application_domain
  88. ON public.data_product_applications(
  89. business_domain_uid, status, updated_at DESC
  90. );
  91. CREATE TABLE public.data_product_application_events (
  92. uid UUID PRIMARY KEY,
  93. application_uid UUID NOT NULL
  94. REFERENCES public.data_product_applications(uid) ON DELETE CASCADE,
  95. application_version INTEGER NOT NULL CHECK (application_version > 0),
  96. action VARCHAR(50) NOT NULL,
  97. actor_uid UUID NOT NULL REFERENCES public.users(id),
  98. payload JSONB NOT NULL DEFAULT '{}'::jsonb,
  99. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  100. CHECK (jsonb_typeof(payload) = 'object')
  101. );
  102. CREATE INDEX idx_product_application_timeline
  103. ON public.data_product_application_events(
  104. application_uid, created_at, uid
  105. );
  106. CREATE TABLE public.data_product_contracts (
  107. uid UUID PRIMARY KEY,
  108. product_uid UUID NOT NULL UNIQUE
  109. REFERENCES public.governed_data_products(uid) ON DELETE RESTRICT,
  110. contract_code VARCHAR(180) NOT NULL UNIQUE,
  111. status VARCHAR(20) NOT NULL CHECK (
  112. status IN ('draft','active','terminated')
  113. ),
  114. current_version INTEGER NOT NULL CHECK (current_version > 0),
  115. active_version_uid UUID,
  116. created_by UUID NOT NULL REFERENCES public.users(id),
  117. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  118. updated_by UUID NOT NULL REFERENCES public.users(id),
  119. updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  120. terminated_at TIMESTAMPTZ,
  121. termination_reason VARCHAR(1000)
  122. );
  123. CREATE TABLE public.data_product_contract_versions (
  124. uid UUID PRIMARY KEY,
  125. contract_uid UUID NOT NULL
  126. REFERENCES public.data_product_contracts(uid) ON DELETE RESTRICT,
  127. version INTEGER NOT NULL CHECK (version > 0),
  128. status VARCHAR(20) NOT NULL CHECK (
  129. status IN ('draft','published','superseded')
  130. ),
  131. definition JSONB NOT NULL,
  132. content_hash CHAR(64) NOT NULL,
  133. compatibility JSONB NOT NULL,
  134. created_by UUID NOT NULL REFERENCES public.users(id),
  135. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  136. published_by UUID REFERENCES public.users(id),
  137. published_at TIMESTAMPTZ,
  138. UNIQUE (contract_uid, version),
  139. UNIQUE (contract_uid, content_hash),
  140. CHECK (jsonb_typeof(definition) = 'object'),
  141. CHECK (jsonb_typeof(compatibility) = 'object')
  142. );
  143. ALTER TABLE public.data_product_contracts
  144. ADD CONSTRAINT data_product_contract_active_version_fk
  145. FOREIGN KEY (active_version_uid)
  146. REFERENCES public.data_product_contract_versions(uid);
  147. CREATE UNIQUE INDEX uq_data_product_contract_published_version
  148. ON public.data_product_contract_versions(contract_uid)
  149. WHERE status = 'published';
  150. CREATE TABLE public.data_product_certificates (
  151. uid UUID PRIMARY KEY,
  152. certificate_code VARCHAR(50) NOT NULL UNIQUE,
  153. product_uid UUID NOT NULL
  154. REFERENCES public.governed_data_products(uid) ON DELETE RESTRICT,
  155. contract_uid UUID NOT NULL
  156. REFERENCES public.data_product_contracts(uid) ON DELETE RESTRICT,
  157. contract_version INTEGER NOT NULL CHECK (contract_version > 0),
  158. approval_task_uid UUID NOT NULL REFERENCES public.governance_tasks(uid),
  159. evidence_snapshot JSONB NOT NULL,
  160. status VARCHAR(20) NOT NULL CHECK (
  161. status IN ('qualified','unqualified')
  162. ),
  163. content_hash CHAR(64) NOT NULL,
  164. issued_by UUID NOT NULL REFERENCES public.users(id),
  165. issued_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  166. UNIQUE (product_uid, content_hash),
  167. CHECK (jsonb_typeof(evidence_snapshot) = 'object')
  168. );
  169. CREATE INDEX idx_product_certificate_latest
  170. ON public.data_product_certificates(product_uid, issued_at DESC);
  171. CREATE TABLE public.data_product_feedback (
  172. uid UUID PRIMARY KEY,
  173. product_uid UUID NOT NULL
  174. REFERENCES public.governed_data_products(uid) ON DELETE CASCADE,
  175. category VARCHAR(30) NOT NULL CHECK (
  176. category IN (
  177. 'quality','freshness','usability',
  178. 'documentation','service'
  179. )
  180. ),
  181. rating INTEGER NOT NULL CHECK (rating BETWEEN 1 AND 5),
  182. summary VARCHAR(300) NOT NULL,
  183. details VARCHAR(2000) NOT NULL,
  184. status VARCHAR(20) NOT NULL CHECK (
  185. status IN ('open','triaged','in_progress','resolved','closed')
  186. ),
  187. assignee_uid UUID REFERENCES public.users(id),
  188. resolution VARCHAR(2000),
  189. evidence_refs JSONB NOT NULL DEFAULT '[]'::jsonb,
  190. current_version INTEGER NOT NULL DEFAULT 1 CHECK (current_version > 0),
  191. created_by UUID NOT NULL REFERENCES public.users(id),
  192. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  193. updated_by UUID NOT NULL REFERENCES public.users(id),
  194. updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  195. closed_at TIMESTAMPTZ,
  196. CHECK (jsonb_typeof(evidence_refs) = 'array')
  197. );
  198. CREATE INDEX idx_product_feedback_worklist
  199. ON public.data_product_feedback(
  200. product_uid, status, updated_at DESC
  201. );
  202. CREATE INDEX idx_product_feedback_assignee
  203. ON public.data_product_feedback(
  204. assignee_uid, status, updated_at DESC
  205. );
  206. """
  207. )
  208. def downgrade() -> None:
  209. raise RuntimeError(
  210. "governed product contracts, certificates and feedback are retained; "
  211. "downgrade requires an approved archival migration"
  212. )