20260729_310_device_entity_resolution.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. """Add governed device entity candidates, merges, and rollbacks."""
  2. from alembic import op
  3. revision = "20260729_310"
  4. down_revision = "20260729_300"
  5. branch_labels = None
  6. depends_on = None
  7. def upgrade() -> None:
  8. op.execute(
  9. """
  10. CREATE TABLE public.device_entity_match_candidates (
  11. uid UUID PRIMARY KEY,
  12. left_asset_uid UUID NOT NULL
  13. REFERENCES public.device_assets(uid) ON DELETE RESTRICT,
  14. right_asset_uid UUID NOT NULL
  15. REFERENCES public.device_assets(uid) ON DELETE RESTRICT,
  16. canonical_asset_uid UUID
  17. REFERENCES public.device_assets(uid) ON DELETE RESTRICT,
  18. status VARCHAR(20) NOT NULL DEFAULT 'pending'
  19. CHECK (
  20. status IN (
  21. 'pending','merged','rejected','rolled_back'
  22. )
  23. ),
  24. suggestion_source VARCHAR(20) NOT NULL
  25. CHECK (suggestion_source IN ('rule','ai','manual')),
  26. confidence DOUBLE PRECISION NOT NULL
  27. CHECK (confidence >= 0 AND confidence <= 1),
  28. explanation JSONB NOT NULL DEFAULT '[]'::jsonb,
  29. evidence_uids JSONB NOT NULL DEFAULT '[]'::jsonb,
  30. model_provider VARCHAR(200),
  31. model_name VARCHAR(200),
  32. current_version INTEGER NOT NULL DEFAULT 1
  33. CHECK (current_version > 0),
  34. created_by VARCHAR(100),
  35. reviewed_by VARCHAR(100),
  36. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  37. updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  38. CHECK (left_asset_uid <> right_asset_uid)
  39. );
  40. CREATE UNIQUE INDEX uq_device_entity_open_pair
  41. ON public.device_entity_match_candidates(
  42. left_asset_uid, right_asset_uid
  43. )
  44. WHERE status IN ('pending','merged');
  45. CREATE INDEX idx_device_entity_candidates_status
  46. ON public.device_entity_match_candidates(
  47. status, suggestion_source, updated_at DESC
  48. );
  49. CREATE TABLE public.device_entity_match_reviews (
  50. uid UUID PRIMARY KEY,
  51. candidate_uid UUID NOT NULL
  52. REFERENCES public.device_entity_match_candidates(uid)
  53. ON DELETE CASCADE,
  54. version INTEGER NOT NULL CHECK (version > 0),
  55. decision VARCHAR(20) NOT NULL
  56. CHECK (decision IN ('approve','reject','auto_approve')),
  57. reason VARCHAR(1000) NOT NULL,
  58. actor_uid VARCHAR(100) NOT NULL,
  59. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
  60. );
  61. CREATE INDEX idx_device_entity_reviews_candidate
  62. ON public.device_entity_match_reviews(
  63. candidate_uid, created_at DESC
  64. );
  65. CREATE TABLE public.device_entity_merge_events (
  66. uid UUID PRIMARY KEY,
  67. candidate_uid UUID NOT NULL UNIQUE
  68. REFERENCES public.device_entity_match_candidates(uid)
  69. ON DELETE CASCADE,
  70. canonical_asset_uid UUID NOT NULL
  71. REFERENCES public.device_assets(uid) ON DELETE RESTRICT,
  72. member_asset_uid UUID NOT NULL
  73. REFERENCES public.device_assets(uid) ON DELETE RESTRICT,
  74. review_uid UUID NOT NULL
  75. REFERENCES public.device_entity_match_reviews(uid)
  76. ON DELETE RESTRICT,
  77. snapshot JSONB NOT NULL,
  78. actor_uid VARCHAR(100) NOT NULL,
  79. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
  80. );
  81. CREATE INDEX idx_device_entity_merge_canonical
  82. ON public.device_entity_merge_events(
  83. canonical_asset_uid, created_at DESC
  84. );
  85. CREATE INDEX idx_device_entity_merge_member
  86. ON public.device_entity_merge_events(
  87. member_asset_uid, created_at DESC
  88. );
  89. CREATE TABLE public.device_entity_merge_rollbacks (
  90. uid UUID PRIMARY KEY,
  91. merge_uid UUID NOT NULL UNIQUE
  92. REFERENCES public.device_entity_merge_events(uid)
  93. ON DELETE CASCADE,
  94. candidate_uid UUID NOT NULL
  95. REFERENCES public.device_entity_match_candidates(uid)
  96. ON DELETE CASCADE,
  97. reason VARCHAR(1000) NOT NULL,
  98. snapshot JSONB NOT NULL,
  99. actor_uid VARCHAR(100) NOT NULL,
  100. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
  101. );
  102. CREATE INDEX idx_device_entity_rollbacks_candidate
  103. ON public.device_entity_merge_rollbacks(
  104. candidate_uid, created_at DESC
  105. );
  106. """
  107. )
  108. def downgrade() -> None:
  109. # Candidate, review, merge, and rollback evidence is retained.
  110. pass