0018_sub_plan_binding.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. """子案跨主案挂载:新表 doc_sub_plan_binding。
  2. Revision ID: 0018_sub_plan_binding
  3. Revises: 0017_category_type_display_name
  4. Create Date: 2026-08-24
  5. 变更要点:
  6. 1. 新建 doc_sub_plan_binding:主案对其他主案子案的挂载关系;
  7. 2. 软删 + 生成列唯一约束(uq_doc_sub_plan_binding_active)保证同一主案
  8. 对同一子案至多一条有效挂载;主案自有子案(parent_document_id 指向
  9. 该主案)不允许建立挂载,由服务层校验;
  10. 3. 审计复用 BIND_ATTACHMENT / UNBIND_ATTACHMENT 动作(detail 标注
  11. scope=subPlanMount),不扩展 sys_audit_log 的 CHECK 约束;
  12. 4. 降级保护:已产生挂载数据时拒绝有损降级。
  13. """
  14. from __future__ import annotations
  15. from collections.abc import Sequence
  16. from alembic import op
  17. import sqlalchemy as sa
  18. from sqlalchemy.dialects import mysql
  19. revision: str = "0018_sub_plan_binding"
  20. down_revision: str | None = "0017_category_type_display_name"
  21. branch_labels: str | Sequence[str] | None = None
  22. depends_on: str | Sequence[str] | None = None
  23. def _business_columns() -> list[sa.Column]:
  24. return [
  25. sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
  26. sa.Column("created_by", sa.BigInteger(), nullable=True),
  27. sa.Column("updated_by", sa.BigInteger(), nullable=True),
  28. sa.Column(
  29. "created_at",
  30. mysql.DATETIME(fsp=3),
  31. server_default=sa.text("CURRENT_TIMESTAMP(3)"),
  32. nullable=False,
  33. ),
  34. sa.Column(
  35. "updated_at",
  36. mysql.DATETIME(fsp=3),
  37. server_default=sa.text("CURRENT_TIMESTAMP(3)"),
  38. nullable=False,
  39. ),
  40. sa.Column("deleted_at", mysql.DATETIME(fsp=3), nullable=True),
  41. sa.Column(
  42. "is_deleted",
  43. sa.Boolean(),
  44. server_default=sa.text("0"),
  45. nullable=False,
  46. ),
  47. sa.Column(
  48. "row_version",
  49. sa.Integer(),
  50. server_default=sa.text("0"),
  51. nullable=False,
  52. ),
  53. sa.PrimaryKeyConstraint("id"),
  54. ]
  55. def _existing_tables() -> set[str]:
  56. inspector = sa.inspect(op.get_bind())
  57. return set(inspector.get_table_names())
  58. def upgrade() -> None:
  59. if "doc_sub_plan_binding" in _existing_tables():
  60. return
  61. op.create_table(
  62. "doc_sub_plan_binding",
  63. *_business_columns(),
  64. sa.Column("main_document_id", sa.BigInteger(), nullable=False),
  65. sa.Column("sub_document_id", sa.BigInteger(), nullable=False),
  66. sa.Column(
  67. "sort_no",
  68. sa.Integer(),
  69. server_default=sa.text("0"),
  70. nullable=False,
  71. ),
  72. sa.Column(
  73. "active_marker",
  74. sa.Integer(),
  75. sa.Computed("IF(is_deleted = 0, 1, NULL)", persisted=False),
  76. nullable=True,
  77. ),
  78. sa.ForeignKeyConstraint(
  79. ["main_document_id"],
  80. ["doc_document.id"],
  81. name="fk_doc_sub_plan_binding_main_id_doc_document",
  82. ondelete="RESTRICT",
  83. ),
  84. sa.ForeignKeyConstraint(
  85. ["sub_document_id"],
  86. ["doc_document.id"],
  87. name="fk_doc_sub_plan_binding_sub_id_doc_document",
  88. ondelete="RESTRICT",
  89. ),
  90. sa.UniqueConstraint(
  91. "main_document_id",
  92. "sub_document_id",
  93. "active_marker",
  94. name="uq_doc_sub_plan_binding_active",
  95. ),
  96. )
  97. op.create_index(
  98. "ix_doc_sub_plan_binding_main_active_sort",
  99. "doc_sub_plan_binding",
  100. ["main_document_id", "is_deleted", "sort_no"],
  101. )
  102. op.create_index(
  103. "ix_doc_sub_plan_binding_sub_active",
  104. "doc_sub_plan_binding",
  105. ["sub_document_id", "is_deleted"],
  106. )
  107. def downgrade() -> None:
  108. if "doc_sub_plan_binding" not in _existing_tables():
  109. return
  110. bind = op.get_bind()
  111. binding_rows = bind.scalar(
  112. sa.text("SELECT COUNT(*) FROM doc_sub_plan_binding")
  113. )
  114. if binding_rows:
  115. raise RuntimeError(
  116. "0018已产生子案挂载数据,拒绝有损降级;请先备份并按数据治理流程处理"
  117. )
  118. op.drop_table("doc_sub_plan_binding")