test_data_rule_polars_execution.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685
  1. from __future__ import annotations
  2. import json
  3. import re
  4. from pathlib import Path
  5. import polars as pl
  6. from minio import Minio
  7. from sqlalchemy import create_engine, text
  8. from app.core.common.identifiers import new_governance_uid
  9. from app.core.data_rules.contracts import rule_spec_hash, validate_rule_spec
  10. from app.core.data_rules.execution_contracts import canonical_schema_hash
  11. COMPOSE = (
  12. Path(__file__).resolve().parents[2]
  13. / "deploy"
  14. / "docker"
  15. / "docker-compose.yml"
  16. )
  17. def _compose_value(pattern):
  18. source = COMPOSE.read_text(encoding="utf-8")
  19. match = re.search(pattern, source, flags=re.DOTALL)
  20. assert match is not None
  21. return match.group(1)
  22. def _schema(schema_ref, fields):
  23. normalized = [
  24. {"name": name, "type": field_type, "nullable": nullable}
  25. for name, field_type, nullable in fields
  26. ]
  27. return {
  28. "id": new_governance_uid(),
  29. "schema_ref": schema_ref,
  30. "schema_hash": canonical_schema_hash(normalized),
  31. "fields": normalized,
  32. "source_revision": "task5:real-cross-source",
  33. }
  34. def _binding(schema, *, source_uid, access_mode, object_ref):
  35. return {
  36. "id": new_governance_uid(),
  37. "data_source_uid": source_uid,
  38. "object_kind": "parquet_artifact",
  39. "object_ref": object_ref,
  40. "schema_snapshot_id": schema["id"],
  41. "access_mode": access_mode,
  42. "dialect": "parquet",
  43. "write_mode": "append",
  44. }
  45. def test_real_postgres_mysql_minio_polars_cross_source_execution():
  46. from app.core.data_rules.compilers.polars import PolarsRuleCompiler
  47. from app.runner.artifacts import ArtifactStore, PostgresArtifactResolver
  48. from app.runner.rule_polars import PolarsRulePlanAdapter
  49. from app.runner.rules import PostgresRulePlanRepository, RulePlanExecutor
  50. source_user = _compose_value(
  51. r"source-postgres:.*?POSTGRES_USER:\s*([^\s]+)"
  52. )
  53. source_password = _compose_value(
  54. r"source-postgres:.*?POSTGRES_PASSWORD:\s*([^\s]+)"
  55. )
  56. platform_user = _compose_value(
  57. r"\n postgres:.*?POSTGRES_USER:\s*([^\s]+)"
  58. )
  59. platform_password = _compose_value(
  60. r"\n postgres:.*?POSTGRES_PASSWORD:\s*([^\s]+)"
  61. )
  62. postgres_port = _compose_value(r'"(25432):5432"')
  63. mysql_port = _compose_value(r'"(23306):3306"')
  64. minio_user = _compose_value(r"MINIO_ROOT_USER:\s*([^\s]+)")
  65. minio_password = _compose_value(r"MINIO_ROOT_PASSWORD:\s*([^\s]+)")
  66. minio_port = _compose_value(r'"(19000):9000"')
  67. platform_port = _compose_value(r'"(15432):5432"')
  68. bucket = _compose_value(r"mc mb --ignore-existing local/([^\s]+)")
  69. postgres = create_engine(
  70. f"postgresql+psycopg2://{source_user}:{source_password}"
  71. f"@127.0.0.1:{postgres_port}/acceptance",
  72. pool_pre_ping=True,
  73. )
  74. mysql = create_engine(
  75. f"mysql+pymysql://{source_user}:{source_password}"
  76. f"@127.0.0.1:{mysql_port}/acceptance",
  77. pool_pre_ping=True,
  78. )
  79. platform = create_engine(
  80. f"postgresql+psycopg2://{platform_user}:{platform_password}"
  81. f"@127.0.0.1:{platform_port}/dataops",
  82. pool_pre_ping=True,
  83. )
  84. minio = Minio(
  85. f"127.0.0.1:{minio_port}",
  86. access_key=minio_user,
  87. secret_key=minio_password,
  88. secure=False,
  89. )
  90. store = ArtifactStore(
  91. minio,
  92. bucket=bucket,
  93. max_artifact_bytes=4 * 1024 * 1024,
  94. max_rows=1_000,
  95. memory_limit_bytes=16 * 1024 * 1024,
  96. max_ttl_seconds=3600,
  97. )
  98. correlation_id = new_governance_uid()
  99. prefix = f"rules/{correlation_id}/"
  100. customer_table = "task5_polars_customers"
  101. segment_table = "task5_polars_segments"
  102. rule_uid = new_governance_uid()
  103. rule_id = new_governance_uid()
  104. dataflow_uid = new_governance_uid()
  105. dataflow_version_id = new_governance_uid()
  106. deployment_id = new_governance_uid()
  107. component_binding_id = new_governance_uid()
  108. plan_id = new_governance_uid()
  109. try:
  110. with postgres.begin() as connection:
  111. connection.execute(text(f"DROP TABLE IF EXISTS {customer_table}"))
  112. connection.execute(
  113. text(
  114. f"CREATE TABLE {customer_table} ("
  115. "customer_id BIGINT NOT NULL, "
  116. "name VARCHAR(100), mobile VARCHAR(30), "
  117. "segment_code VARCHAR(20), version_no BIGINT NOT NULL)"
  118. )
  119. )
  120. connection.execute(
  121. text(
  122. f"INSERT INTO {customer_table} "
  123. "(customer_id, name, mobile, segment_code, version_no) "
  124. "VALUES "
  125. "(1, ' Alice ', '13800138000', 'A', 1), "
  126. "(1, ' Alice Updated ', '13800138000', 'A', 2), "
  127. "(2, ' Bad ', 'invalid', 'B', 1), "
  128. "(3, ' Carol ', '13900139000', 'C', 1)"
  129. )
  130. )
  131. with mysql.begin() as connection:
  132. connection.execute(text(f"DROP TABLE IF EXISTS {segment_table}"))
  133. connection.execute(
  134. text(
  135. f"CREATE TABLE {segment_table} ("
  136. "code VARCHAR(20) PRIMARY KEY, "
  137. "segment_name VARCHAR(100) NOT NULL)"
  138. )
  139. )
  140. connection.execute(
  141. text(
  142. f"INSERT INTO {segment_table} (code, segment_name) "
  143. "VALUES ('A', 'Gold'), ('B', 'Basic'), ('C', 'Silver')"
  144. )
  145. )
  146. with postgres.connect() as connection:
  147. customer_rows = [
  148. dict(row)
  149. for row in connection.execute(
  150. text(
  151. f"SELECT customer_id, name, mobile, "
  152. f"segment_code, version_no FROM {customer_table}"
  153. )
  154. ).mappings()
  155. ]
  156. with mysql.connect() as connection:
  157. segment_rows = [
  158. dict(row)
  159. for row in connection.execute(
  160. text(
  161. f"SELECT code, segment_name FROM {segment_table}"
  162. )
  163. ).mappings()
  164. ]
  165. input_schema = _schema(
  166. "bd:task5:customer:raw",
  167. [
  168. ("customer_id", "integer", False),
  169. ("name", "string", True),
  170. ("mobile", "string", True),
  171. ("segment_code", "string", True),
  172. ("version_no", "integer", False),
  173. ],
  174. )
  175. lookup_schema = _schema(
  176. "bd:task5:segment:lookup",
  177. [
  178. ("code", "string", False),
  179. ("segment_name", "string", False),
  180. ],
  181. )
  182. output_schema = _schema(
  183. "bd:task5:customer:enriched",
  184. [
  185. ("customer_id", "integer", False),
  186. ("name", "string", True),
  187. ("mobile", "string", True),
  188. ("segment_code", "string", True),
  189. ("version_no", "integer", False),
  190. ("segment_name", "string", True),
  191. ],
  192. )
  193. input_binding = _binding(
  194. input_schema,
  195. source_uid=new_governance_uid(),
  196. access_mode="read",
  197. object_ref="postgres-customer-artifact",
  198. )
  199. lookup_binding = _binding(
  200. lookup_schema,
  201. source_uid=new_governance_uid(),
  202. access_mode="read",
  203. object_ref="mysql-segment-artifact",
  204. )
  205. output_binding = _binding(
  206. output_schema,
  207. source_uid=new_governance_uid(),
  208. access_mode="write",
  209. object_ref="polars-output-artifact",
  210. )
  211. spec = validate_rule_spec(
  212. {
  213. "schema_version": "2.0",
  214. "rule_uid": new_governance_uid(),
  215. "name": "task5_real_cross_source",
  216. "input_schema_ref": input_schema["schema_ref"],
  217. "output_schema_ref": output_schema["schema_ref"],
  218. "steps": [
  219. {
  220. "id": "normalize_name",
  221. "op": "normalize_text",
  222. "column": "name",
  223. "trim": True,
  224. },
  225. {
  226. "id": "join_segment",
  227. "op": "lookup_join",
  228. "lookup": {
  229. "binding_id": lookup_binding["id"],
  230. "left_on": ["segment_code"],
  231. "right_on": ["code"],
  232. "select": {
  233. "segment_name": "segment_name"
  234. },
  235. "how": "left",
  236. },
  237. },
  238. {
  239. "id": "valid_mobile",
  240. "op": "assert",
  241. "expression": "matches(mobile, '^[0-9]{11}$')",
  242. "on_failure": "reject",
  243. "severity": "error",
  244. },
  245. {
  246. "id": "latest_customer",
  247. "op": "deduplicate",
  248. "keys": ["customer_id"],
  249. "order_by": ["version_no"],
  250. "keep": "last",
  251. },
  252. ],
  253. "null_policy": "explicit",
  254. "timezone": "Asia/Shanghai",
  255. }
  256. )
  257. rule = {
  258. "id": rule_id,
  259. "status": "published",
  260. "rule_spec": spec,
  261. "spec_hash": rule_spec_hash(spec),
  262. }
  263. compiled = PolarsRuleCompiler().compile(
  264. rule_version=rule,
  265. input_schema=input_schema,
  266. output_schema=output_schema,
  267. input_binding=input_binding,
  268. output_binding=output_binding,
  269. backend={
  270. "max_rows": 1_000,
  271. "max_artifact_bytes": 4 * 1024 * 1024,
  272. "memory_limit_bytes": 16 * 1024 * 1024,
  273. "masking_policies": {},
  274. "lookup_bindings": {
  275. lookup_binding["id"]: {
  276. "binding": lookup_binding,
  277. "schema": lookup_schema,
  278. }
  279. },
  280. },
  281. )
  282. lookup_operation = compiled["plan"]["operations"][1]
  283. schema_hashes = {
  284. "rule_spec_hash": compiled["plan"]["rule_spec_hash"],
  285. "input_schema_snapshot_id": input_schema["id"],
  286. "input_schema_hash": input_schema["schema_hash"],
  287. "output_schema_snapshot_id": output_schema["id"],
  288. "output_schema_hash": output_schema["schema_hash"],
  289. }
  290. with platform.begin() as connection:
  291. for schema in (input_schema, lookup_schema, output_schema):
  292. connection.execute(
  293. text(
  294. """
  295. INSERT INTO public.data_schema_snapshots
  296. (id, schema_ref, schema_hash, fields, source_revision)
  297. VALUES (CAST(:id AS uuid), :schema_ref, :schema_hash,
  298. CAST(:fields AS jsonb), :source_revision)
  299. """
  300. ),
  301. {**schema, "fields": json.dumps(schema["fields"])},
  302. )
  303. connection.execute(
  304. text(
  305. """
  306. INSERT INTO public.data_rules
  307. (id, rule_uid, name, category, status)
  308. VALUES (CAST(:id AS uuid), CAST(:rule_uid AS uuid),
  309. :name, 'general', 'active')
  310. """
  311. ),
  312. {
  313. "id": new_governance_uid(),
  314. "rule_uid": rule_uid,
  315. "name": spec["name"],
  316. },
  317. )
  318. connection.execute(
  319. text(
  320. """
  321. INSERT INTO public.data_rule_versions
  322. (id, rule_uid, version_no, source_text, source_language,
  323. rule_spec, spec_hash, generated_kind, status, published_at)
  324. VALUES (CAST(:id AS uuid), CAST(:rule_uid AS uuid), 1,
  325. :source_text, 'en', CAST(:rule_spec AS jsonb),
  326. :spec_hash, 'polars', 'published',
  327. CURRENT_TIMESTAMP)
  328. """
  329. ),
  330. {
  331. "id": rule_id,
  332. "rule_uid": rule_uid,
  333. "source_text": "Task 5 real cross-source integration",
  334. "rule_spec": json.dumps(spec),
  335. "spec_hash": rule["spec_hash"],
  336. },
  337. )
  338. connection.execute(
  339. text(
  340. """
  341. INSERT INTO public.dataflow_versions
  342. (id, dataflow_uid, version_no, name, dataflow_spec,
  343. input_schema_hashes, output_schema_hash, status,
  344. released_at)
  345. VALUES (CAST(:id AS uuid), CAST(:dataflow_uid AS uuid), 1,
  346. :name, '{}'::jsonb,
  347. CAST(:input_schema_hashes AS jsonb),
  348. :output_schema_hash, 'released', CURRENT_TIMESTAMP)
  349. """
  350. ),
  351. {
  352. "id": dataflow_version_id,
  353. "dataflow_uid": dataflow_uid,
  354. "name": "Task 5 real cross-source integration",
  355. "input_schema_hashes": json.dumps(
  356. [
  357. input_schema["schema_hash"],
  358. lookup_schema["schema_hash"],
  359. ]
  360. ),
  361. "output_schema_hash": output_schema["schema_hash"],
  362. },
  363. )
  364. connection.execute(
  365. text(
  366. """
  367. INSERT INTO public.dataflow_deployments
  368. (id, dataflow_version_id, environment, deployment_config,
  369. status, activated_at)
  370. VALUES (CAST(:id AS uuid),
  371. CAST(:dataflow_version_id AS uuid), 'test',
  372. '{}'::jsonb, 'active', CURRENT_TIMESTAMP)
  373. """
  374. ),
  375. {
  376. "id": deployment_id,
  377. "dataflow_version_id": dataflow_version_id,
  378. },
  379. )
  380. for logical_ref, binding, binding_hash in (
  381. (
  382. "customers",
  383. input_binding,
  384. compiled["plan"]["input_binding_hash"],
  385. ),
  386. (
  387. "segments",
  388. lookup_binding,
  389. lookup_operation["lookup_binding_hash"],
  390. ),
  391. (
  392. "enriched",
  393. output_binding,
  394. compiled["plan"]["output_binding_hash"],
  395. ),
  396. ):
  397. connection.execute(
  398. text(
  399. """
  400. INSERT INTO public.dataflow_dataset_bindings
  401. (id, dataflow_deployment_id, logical_ref,
  402. data_source_uid, object_kind, object_ref,
  403. schema_snapshot_id, dialect, access_mode, write_mode,
  404. binding_hash)
  405. VALUES (CAST(:id AS uuid), CAST(:deployment_id AS uuid),
  406. :logical_ref, CAST(:source_uid AS uuid),
  407. 'parquet_artifact', :object_ref,
  408. CAST(:schema_snapshot_id AS uuid), 'parquet',
  409. :access_mode, 'append', :binding_hash)
  410. """
  411. ),
  412. {
  413. "id": binding["id"],
  414. "deployment_id": deployment_id,
  415. "logical_ref": logical_ref,
  416. "source_uid": binding["data_source_uid"],
  417. "object_ref": binding["object_ref"],
  418. "schema_snapshot_id": binding["schema_snapshot_id"],
  419. "access_mode": binding["access_mode"],
  420. "binding_hash": binding_hash,
  421. },
  422. )
  423. connection.execute(
  424. text(
  425. """
  426. INSERT INTO public.dataflow_component_bindings
  427. (id, dataflow_version_id, component_id, component_kind,
  428. rule_version_id, stage, order_no, idempotency, provenance)
  429. VALUES (CAST(:id AS uuid),
  430. CAST(:dataflow_version_id AS uuid),
  431. 'task5_real_polars', 'rule.apply',
  432. CAST(:rule_version_id AS uuid), 'transform', 0,
  433. CAST(:idempotency AS jsonb), '{}'::jsonb)
  434. """
  435. ),
  436. {
  437. "id": component_binding_id,
  438. "dataflow_version_id": dataflow_version_id,
  439. "rule_version_id": rule_id,
  440. "idempotency": json.dumps(
  441. {
  442. "strategy": "deduplication_key",
  443. "key": "customer_id",
  444. }
  445. ),
  446. },
  447. )
  448. connection.execute(
  449. text(
  450. """
  451. INSERT INTO public.rule_execution_plans
  452. (id, component_binding_id, backend, compiler_version, plan,
  453. plan_hash, schema_hashes, status)
  454. VALUES (CAST(:id AS uuid),
  455. CAST(:component_binding_id AS uuid),
  456. 'polars_batch', :compiler_version,
  457. CAST(:plan AS jsonb), :plan_hash,
  458. CAST(:schema_hashes AS jsonb), 'published')
  459. """
  460. ),
  461. {
  462. "id": plan_id,
  463. "component_binding_id": component_binding_id,
  464. "compiler_version": compiled["compiler_version"],
  465. "plan": json.dumps(compiled["plan"]),
  466. "plan_hash": compiled["plan_hash"],
  467. "schema_hashes": json.dumps(schema_hashes),
  468. },
  469. )
  470. customer_artifact = store.write(
  471. pl.DataFrame(customer_rows),
  472. correlation_id,
  473. 900,
  474. schema_fields=input_schema["fields"],
  475. limits=compiled["plan"]["resource_limits"],
  476. )
  477. segment_artifact = store.write(
  478. pl.DataFrame(segment_rows),
  479. correlation_id,
  480. 900,
  481. schema_fields=lookup_schema["fields"],
  482. limits=compiled["plan"]["resource_limits"],
  483. )
  484. resolver = PostgresArtifactResolver(platform, store)
  485. resolver.register(
  486. binding_id=input_binding["id"],
  487. binding_hash=compiled["plan"]["input_binding_hash"],
  488. correlation_id=correlation_id,
  489. artifact=customer_artifact,
  490. kind="input",
  491. )
  492. resolver.register(
  493. binding_id=lookup_binding["id"],
  494. binding_hash=lookup_operation["lookup_binding_hash"],
  495. correlation_id=correlation_id,
  496. artifact=segment_artifact,
  497. kind="lookup",
  498. )
  499. node = {
  500. "id": "task5_real_polars",
  501. "type": "rule.apply",
  502. "purpose": "write",
  503. "idempotency": {
  504. "strategy": "deduplication_key",
  505. "key": "customer_id",
  506. },
  507. "config": {
  508. "component_binding_id": component_binding_id,
  509. "rule_version_id": rule["id"],
  510. "execution_plan_hash": compiled["plan_hash"],
  511. },
  512. }
  513. executor = RulePlanExecutor(
  514. PostgresRulePlanRepository(platform),
  515. adapters={
  516. "polars_batch": PolarsRulePlanAdapter(
  517. artifact_store=store,
  518. artifact_resolver=resolver,
  519. artifact_ttl_seconds=900,
  520. )
  521. },
  522. )
  523. result = executor.execute(
  524. node,
  525. {},
  526. write_authorized=True,
  527. correlation_id=correlation_id,
  528. )
  529. repeated = executor.execute(
  530. node,
  531. {},
  532. write_authorized=True,
  533. correlation_id=correlation_id,
  534. )
  535. assert result["rows_in"] == 4
  536. assert result["rows_out"] == 2
  537. assert result["rows_rejected"] == 1
  538. assert result["rows_deduplicated"] == 1
  539. assert result["rows_filtered"] == 0
  540. assert result["rows_join_dropped"] == 0
  541. assert result["rows_aggregated"] == 0
  542. assert result["violation_count"] == 1
  543. assert result["violations"] == [
  544. {"step_id": "valid_mobile", "count": 1}
  545. ]
  546. output = store.read(
  547. result["artifact_ref"],
  548. result["digest"],
  549. expected_schema_fields=output_schema["fields"],
  550. limits=compiled["plan"]["resource_limits"],
  551. ).collect()
  552. assert output.sort("customer_id").to_dicts() == [
  553. {
  554. "customer_id": 1,
  555. "mobile": "13800138000",
  556. "name": "Alice Updated",
  557. "segment_code": "A",
  558. "segment_name": "Gold",
  559. "version_no": 2,
  560. },
  561. {
  562. "customer_id": 3,
  563. "mobile": "13900139000",
  564. "name": "Carol",
  565. "segment_code": "C",
  566. "segment_name": "Silver",
  567. "version_no": 1,
  568. },
  569. ]
  570. assert all(
  571. item.object_name.startswith(prefix)
  572. for item in minio.list_objects(
  573. bucket, prefix=prefix, recursive=True
  574. )
  575. )
  576. assert repeated["rows_out"] == 2
  577. with platform.connect() as connection:
  578. catalog_count = connection.execute(
  579. text(
  580. """
  581. SELECT COUNT(*)
  582. FROM public.rule_run_artifacts
  583. WHERE correlation_id = CAST(:correlation_id AS uuid)
  584. """
  585. ),
  586. {"correlation_id": correlation_id},
  587. ).scalar_one()
  588. # The repeated deterministic output has the same digest and is
  589. # idempotently retained as one stable catalog handoff.
  590. assert catalog_count == 3
  591. finally:
  592. for item in list(
  593. minio.list_objects(bucket, prefix=prefix, recursive=True)
  594. ):
  595. minio.remove_object(bucket, item.object_name)
  596. assert list(
  597. minio.list_objects(bucket, prefix=prefix, recursive=True)
  598. ) == []
  599. with postgres.begin() as connection:
  600. connection.execute(text(f"DROP TABLE IF EXISTS {customer_table}"))
  601. with mysql.begin() as connection:
  602. connection.execute(text(f"DROP TABLE IF EXISTS {segment_table}"))
  603. with platform.begin() as connection:
  604. connection.execute(
  605. text(
  606. "DELETE FROM public.rule_run_artifacts "
  607. "WHERE correlation_id = CAST(:correlation_id AS uuid)"
  608. ),
  609. {"correlation_id": correlation_id},
  610. )
  611. connection.execute(
  612. text(
  613. "DELETE FROM public.rule_execution_plans "
  614. "WHERE id = CAST(:id AS uuid)"
  615. ),
  616. {"id": plan_id},
  617. )
  618. connection.execute(
  619. text(
  620. "DELETE FROM public.dataflow_dataset_bindings "
  621. "WHERE dataflow_deployment_id = CAST(:id AS uuid)"
  622. ),
  623. {"id": deployment_id},
  624. )
  625. connection.execute(
  626. text(
  627. "DELETE FROM public.dataflow_component_bindings "
  628. "WHERE id = CAST(:id AS uuid)"
  629. ),
  630. {"id": component_binding_id},
  631. )
  632. connection.execute(
  633. text(
  634. "DELETE FROM public.dataflow_deployments "
  635. "WHERE id = CAST(:id AS uuid)"
  636. ),
  637. {"id": deployment_id},
  638. )
  639. connection.execute(
  640. text(
  641. "DELETE FROM public.dataflow_versions "
  642. "WHERE id = CAST(:id AS uuid)"
  643. ),
  644. {"id": dataflow_version_id},
  645. )
  646. connection.execute(
  647. text(
  648. "DELETE FROM public.data_rule_versions "
  649. "WHERE id = CAST(:id AS uuid)"
  650. ),
  651. {"id": rule_id},
  652. )
  653. connection.execute(
  654. text(
  655. "DELETE FROM public.data_rules "
  656. "WHERE rule_uid = CAST(:rule_uid AS uuid)"
  657. ),
  658. {"rule_uid": rule_uid},
  659. )
  660. for schema in (input_schema, lookup_schema, output_schema):
  661. connection.execute(
  662. text(
  663. "DELETE FROM public.data_schema_snapshots "
  664. "WHERE id = CAST(:id AS uuid)"
  665. ),
  666. {"id": schema["id"]},
  667. )
  668. postgres.dispose()
  669. mysql.dispose()
  670. platform.dispose()