test_catalog_ingestion_databases.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. from __future__ import annotations
  2. import os
  3. import uuid
  4. import pytest
  5. from sqlalchemy.engine import make_url
  6. pytestmark = pytest.mark.integration
  7. class DefinitionRepository:
  8. def __init__(self, definition):
  9. self.definition = definition
  10. def get(self, uid):
  11. return self.definition if str(uid) == self.definition.uid else None
  12. class CredentialRepository:
  13. def __init__(self, credential):
  14. self.credential = credential
  15. def get_active(self, _session, _uid, _version):
  16. return self.credential
  17. @pytest.mark.parametrize(
  18. ("database_type", "source_environment"),
  19. [
  20. ("postgresql", "TEST_SOURCE_POSTGRES_URL"),
  21. ("mysql", "TEST_SOURCE_MYSQL_URL"),
  22. ],
  23. )
  24. def test_real_database_catalog_collection_persists_attempt_and_evidence(
  25. monkeypatch,
  26. database_type,
  27. source_environment,
  28. ):
  29. platform_url = os.environ.get("TEST_DATABASE_URL")
  30. source_url = os.environ.get(source_environment)
  31. if not platform_url or not source_url:
  32. pytest.skip(
  33. f"TEST_DATABASE_URL and {source_environment} are required"
  34. )
  35. monkeypatch.setenv("DATABASE_URL", platform_url)
  36. from app import create_app, db
  37. from app.config.config import datasource_pool_settings
  38. from app.core.data_research.catalog.execution import (
  39. CatalogIngestionExecutor,
  40. )
  41. from app.core.data_research.catalog.models import CatalogScope
  42. from app.core.data_research.catalog.mysql import MySqlCatalogCollector
  43. from app.core.data_research.catalog.postgresql import (
  44. PostgreSqlCatalogCollector,
  45. )
  46. from app.core.data_research.catalog.service import CatalogCollectionService
  47. from app.core.data_research.ingestion import IngestionService
  48. from app.core.data_research.repository import (
  49. SqlAlchemyCatalogSnapshotRepository,
  50. SqlAlchemyIngestionJobRepository,
  51. )
  52. from app.core.data_source.adapters import adapter_for
  53. from app.core.data_source.manager import DataSourceConnectionManager
  54. from app.core.data_source.models import (
  55. DataSourceCredential,
  56. DataSourceDefinition,
  57. )
  58. from app.core.data_source.pool_registry import PoolRegistry
  59. from app.models.data_research import (
  60. CatalogSnapshot,
  61. EvidenceFragment,
  62. IngestionJob,
  63. IngestionSource,
  64. )
  65. parsed = make_url(source_url)
  66. source_uid = str(uuid.uuid4())
  67. definition = DataSourceDefinition(
  68. uid=source_uid,
  69. name_en=f"acceptance-{database_type}",
  70. database_type=database_type,
  71. host=parsed.host,
  72. port=parsed.port,
  73. database=parsed.database,
  74. schema="public",
  75. credential_ref=source_uid,
  76. credential_version=1,
  77. )
  78. credential = DataSourceCredential(
  79. username=parsed.username,
  80. password=parsed.password,
  81. )
  82. settings = datasource_pool_settings()
  83. manager = DataSourceConnectionManager(
  84. definitions=DefinitionRepository(definition),
  85. credentials=CredentialRepository(credential),
  86. platform_session=lambda: object(),
  87. adapter_resolver=adapter_for,
  88. registry=PoolRegistry({**settings, "drain_timeout": 30}),
  89. settings_resolver=datasource_pool_settings,
  90. )
  91. app = create_app()
  92. app.config.update(TESTING=True)
  93. job_uid = None
  94. try:
  95. with app.app_context():
  96. db.session.add(
  97. IngestionSource(
  98. uid=source_uid,
  99. source_type="database",
  100. name=f"验收 {database_type}",
  101. config={
  102. "database_type": database_type,
  103. "database": parsed.database,
  104. "schema": "public",
  105. },
  106. permission_scope={},
  107. status="active",
  108. created_by="integration-test",
  109. )
  110. )
  111. db.session.commit()
  112. ingestion = IngestionService(
  113. SqlAlchemyIngestionJobRepository(db.session),
  114. commit=db.session.commit,
  115. rollback=db.session.rollback,
  116. )
  117. job, _created = ingestion.create_job(
  118. {
  119. "source_uid": source_uid,
  120. "job_type": "catalog_collect",
  121. "parser_version": "catalog-v1",
  122. "parameters": {
  123. "include_schemas": (
  124. ["public"]
  125. if database_type == "postgresql"
  126. else []
  127. ),
  128. "exclude_schemas": [],
  129. "include_tables": ["acceptance_customers"],
  130. "exclude_tables": [],
  131. },
  132. "force_rerun": True,
  133. },
  134. actor_uid="integration-test",
  135. )
  136. job_uid = job.uid
  137. collection = CatalogCollectionService(
  138. manager,
  139. definition_resolver=lambda _uid: definition,
  140. collector_resolver=lambda _type: (
  141. PostgreSqlCatalogCollector()
  142. if database_type == "postgresql"
  143. else MySqlCatalogCollector()
  144. ),
  145. )
  146. executor = CatalogIngestionExecutor(
  147. ingestion,
  148. collection,
  149. SqlAlchemyCatalogSnapshotRepository(db.session),
  150. commit=db.session.commit,
  151. rollback=db.session.rollback,
  152. )
  153. completed = executor.execute(job.uid)
  154. assert completed.status == "awaiting_review"
  155. assert completed.attempt_count == 1
  156. assert completed.statistics["asset_count"] == 1
  157. assert completed.statistics["field_count"] == 2
  158. snapshot = db.session.query(CatalogSnapshot).filter_by(
  159. job_uid=job.uid
  160. ).one()
  161. assert snapshot.attempt == 1
  162. assert snapshot.snapshot["assets"][0]["name"] == (
  163. "acceptance_customers"
  164. )
  165. evidence = db.session.query(EvidenceFragment).filter_by(
  166. job_uid=job.uid
  167. ).all()
  168. assert {
  169. item.locator["column"]
  170. for item in evidence
  171. } == {"id", "customer_name"}
  172. assert all(
  173. "password" not in str(item.locator).lower()
  174. for item in evidence
  175. )
  176. finally:
  177. manager.close()
  178. with app.app_context():
  179. if job_uid:
  180. db.session.query(IngestionJob).filter_by(
  181. uid=job_uid
  182. ).delete(synchronize_session=False)
  183. db.session.query(IngestionSource).filter_by(
  184. uid=source_uid
  185. ).delete(synchronize_session=False)
  186. db.session.commit()