test_data_rule_control_plane.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. from __future__ import annotations
  2. import copy
  3. import os
  4. import pytest
  5. from sqlalchemy import create_engine, text
  6. from sqlalchemy.orm import Session
  7. from app.core.data_rules.schema_resolver import SchemaResolver
  8. from tests.core.data_rules.test_contracts import (
  9. valid_dataflow_spec,
  10. valid_rule_spec,
  11. valid_standard_spec,
  12. )
  13. pytestmark = pytest.mark.integration
  14. class FakeMetadataCatalog:
  15. def load_schema(self, schema_ref):
  16. return {
  17. "source_revision": "integration:1",
  18. "fields": [
  19. {
  20. "name": "customer_id",
  21. "type": "string",
  22. "nullable": False,
  23. },
  24. {
  25. "name": "name",
  26. "type": "string",
  27. "nullable": True,
  28. },
  29. {
  30. "name": "mobile",
  31. "type": "string",
  32. "nullable": True,
  33. },
  34. ],
  35. }
  36. @pytest.fixture()
  37. def database_url():
  38. value = os.environ.get("TEST_DATABASE_URL")
  39. if not value:
  40. pytest.skip("TEST_DATABASE_URL is not configured")
  41. return value
  42. def test_postgres_rule_standard_and_production_line_release_is_atomic(
  43. database_url,
  44. ):
  45. from app.core.data_rules.release import ProductionLineReleaseService
  46. from app.core.data_rules.repository import DataRuleRepository
  47. engine = create_engine(database_url)
  48. with engine.connect() as connection:
  49. transaction = connection.begin()
  50. try:
  51. actor = connection.execute(
  52. text(
  53. "SELECT id::text FROM public.users "
  54. "WHERE status = 'active' ORDER BY created_at LIMIT 1"
  55. )
  56. ).scalar_one()
  57. session = Session(bind=connection)
  58. repository = DataRuleRepository(session)
  59. quality_spec = valid_rule_spec()
  60. quality_spec["steps"] = [copy.deepcopy(quality_spec["steps"][1])]
  61. quality_created = repository.create_rule_version(
  62. rule_spec=quality_spec,
  63. source_text="手机号必须为11位数字",
  64. category="standard_clause",
  65. created_by=actor,
  66. )
  67. quality = repository.publish_rule_version(
  68. version_id=quality_created["id"],
  69. published_by=actor,
  70. )
  71. transform_spec = valid_rule_spec()
  72. transform_created = repository.create_rule_version(
  73. rule_spec=transform_spec,
  74. source_text="清洗姓名并校验手机号",
  75. category="flow_scoped",
  76. created_by=actor,
  77. )
  78. transform = repository.publish_rule_version(
  79. version_id=transform_created["id"],
  80. published_by=actor,
  81. )
  82. standard_spec = valid_standard_spec(quality["id"])
  83. standard_created = repository.create_standard_version(
  84. standard_spec=standard_spec,
  85. source_text="客户手机号遵循统一格式",
  86. created_by=actor,
  87. )
  88. standard = repository.publish_standard_version(
  89. version_id=standard_created["id"],
  90. published_by=actor,
  91. )
  92. flow = valid_dataflow_spec(standard["id"], transform["id"])
  93. released = ProductionLineReleaseService(
  94. repository,
  95. schema_resolver=SchemaResolver(FakeMetadataCatalog(), repository),
  96. ).release(
  97. dataflow_uid=flow["dataflow_uid"],
  98. dataflow_spec=flow,
  99. source_text="清洗客户数据并执行客户数据标准",
  100. created_by=actor,
  101. )
  102. assert released["status"] == "released"
  103. assert released["package"]["standard_version_ids"] == [standard["id"]]
  104. assert released["package"]["rule_version_ids"] == sorted(
  105. [quality["id"], transform["id"]]
  106. )
  107. counts = connection.execute(
  108. text(
  109. "SELECT "
  110. "(SELECT COUNT(*) FROM public.dataflow_component_bindings "
  111. " WHERE dataflow_version_id = CAST(:id AS uuid)) AS bindings, "
  112. "(SELECT COUNT(*) FROM public.rule_execution_plans p "
  113. " JOIN public.dataflow_component_bindings b "
  114. " ON b.id = p.component_binding_id "
  115. " WHERE b.dataflow_version_id = CAST(:id AS uuid) "
  116. " AND p.status = 'compiled') AS plans"
  117. ),
  118. {"id": released["id"]},
  119. ).one()
  120. assert counts.bindings == 2
  121. assert counts.plans == 2
  122. finally:
  123. transaction.rollback()
  124. engine.dispose()