test_data_rule_sql_execution.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668
  1. from __future__ import annotations
  2. from contextlib import contextmanager
  3. import pytest
  4. from sqlalchemy import create_engine, text
  5. from app.core.common.identifiers import new_governance_uid
  6. from app.core.data_rules.contracts import rule_spec_hash, validate_rule_spec
  7. from app.core.data_rules.execution_contracts import canonical_schema_hash
  8. from app.runner.nodes import NodeExecutionError
  9. CASES = [
  10. (
  11. "postgresql",
  12. "postgresql+psycopg2://source_reader:source-test-password@127.0.0.1:25432/acceptance",
  13. "public",
  14. "C",
  15. "posix",
  16. ),
  17. (
  18. "mysql",
  19. "mysql+pymysql://source_reader:source-test-password@127.0.0.1:23306/acceptance",
  20. "acceptance",
  21. "utf8mb4_0900_bin",
  22. "icu",
  23. ),
  24. ]
  25. class Definition:
  26. def __init__(self, dialect, capabilities):
  27. self.database_type = dialect
  28. self.extra_properties = {"sql_rule_capabilities": capabilities}
  29. class Definitions:
  30. def __init__(self, definition):
  31. self.definition = definition
  32. def get(self, _uid):
  33. return self.definition
  34. class DirectManager:
  35. def __init__(self, engine, definition):
  36. self.engine = engine
  37. self.definitions = Definitions(definition)
  38. @contextmanager
  39. def connect(self, _uid, purpose):
  40. assert purpose == "dataflow_write"
  41. with self.engine.connect() as connection:
  42. transaction = connection.begin()
  43. try:
  44. yield connection
  45. transaction.commit()
  46. except Exception:
  47. transaction.rollback()
  48. raise
  49. class ReadOnlyPreflightManager:
  50. def __init__(self, engine):
  51. self.engine = engine
  52. @contextmanager
  53. def connect(self, _uid, purpose):
  54. assert purpose == "dataflow_read"
  55. with self.engine.connect() as connection:
  56. transaction = connection.begin()
  57. try:
  58. yield connection
  59. finally:
  60. transaction.rollback()
  61. class PlanRepository:
  62. def __init__(self, idempotency, context):
  63. self.idempotency = idempotency
  64. self.context = context
  65. self.record = None
  66. def load_bound_compile_context(self, **_ids):
  67. return self.context
  68. def persist_bound_component_plan(self, **kwargs):
  69. compiled = kwargs["compiled"]
  70. plan = compiled["plan"]
  71. self.record = {
  72. "component_binding_id": kwargs["component_binding_id"],
  73. "rule_version_id": kwargs["rule_version_id"],
  74. "backend": compiled["backend"],
  75. "compiler_version": compiled["compiler_version"],
  76. "plan": compiled["plan"],
  77. "plan_hash": compiled["plan_hash"],
  78. "schema_hashes": {
  79. "rule_spec_hash": plan["rule_spec_hash"],
  80. "input_schema_snapshot_id": plan["input_schema_snapshot_id"],
  81. "input_schema_hash": plan["input_schema_hash"],
  82. "output_schema_snapshot_id": plan["output_schema_snapshot_id"],
  83. "output_schema_hash": plan["output_schema_hash"],
  84. },
  85. "canonical_rule_spec_hash": plan["rule_spec_hash"],
  86. "canonical_input_schema_snapshot_id": plan[
  87. "input_schema_snapshot_id"
  88. ],
  89. "canonical_input_schema_hash": plan["input_schema_hash"],
  90. "canonical_input_data_source_uid": self.context[
  91. "input_binding"
  92. ]["data_source_uid"],
  93. "canonical_input_object_ref": self.context["input_binding"][
  94. "object_ref"
  95. ],
  96. "canonical_input_dialect": self.context["input_binding"][
  97. "dialect"
  98. ],
  99. "canonical_output_schema_snapshot_id": plan[
  100. "output_schema_snapshot_id"
  101. ],
  102. "canonical_output_schema_hash": plan["output_schema_hash"],
  103. "canonical_output_data_source_uid": self.context[
  104. "output_binding"
  105. ]["data_source_uid"],
  106. "canonical_output_object_ref": self.context["output_binding"][
  107. "object_ref"
  108. ],
  109. "canonical_output_dialect": self.context["output_binding"][
  110. "dialect"
  111. ],
  112. "plan_status": kwargs["status"],
  113. "rule_status": "published",
  114. "component_kind": "rule.apply",
  115. "binding_idempotency": self.idempotency,
  116. }
  117. return {
  118. "id": new_governance_uid(),
  119. "status": kwargs["status"],
  120. "plan_hash": compiled["plan_hash"],
  121. }
  122. def trust_test_only_preflight_and_publish(self, plan_hash, evidence):
  123. assert self.record is not None
  124. assert self.record["plan_status"] == "compiled"
  125. assert self.record["plan_hash"] == plan_hash
  126. assert evidence["commit_outcome"] == "committed"
  127. assert evidence["rows_in"] >= evidence["rows_out"]
  128. self.record["plan_status"] = "published"
  129. self.record["publication_audit_trusted"] = True
  130. self.record["logical_evidence_trusted"] = True
  131. self.record["physical_evidence_trusted"] = True
  132. def load(self, **_kwargs):
  133. return dict(self.record)
  134. def _snapshot(schema_ref):
  135. fields = [
  136. {"name": "customer_id", "type": "integer", "nullable": False},
  137. {"name": "name", "type": "string", "nullable": True},
  138. {"name": "mobile", "type": "string", "nullable": True},
  139. ]
  140. return {
  141. "id": new_governance_uid(),
  142. "schema_ref": schema_ref,
  143. "schema_hash": canonical_schema_hash(fields),
  144. "fields": fields,
  145. "source_revision": "task4:integration",
  146. }
  147. @pytest.mark.parametrize(
  148. ("dialect", "url", "schema_name", "collation", "regex_engine"), CASES
  149. )
  150. def test_server_owned_sql_preflight_explains_without_writing(
  151. dialect, url, schema_name, collation, regex_engine
  152. ):
  153. from app.core.data_rules.compilers.sql import SqlGlotRuleCompiler
  154. from app.core.data_rules.publication import (
  155. ServerOwnedPhysicalPreflightRunner,
  156. )
  157. engine = create_engine(url, pool_pre_ping=True)
  158. suffix = dialect.replace("postgresql", "pg")
  159. source_name = f"task7_preflight_source_{suffix}"
  160. target_name = f"task7_preflight_target_{suffix}"
  161. capabilities = {
  162. "dialect": dialect,
  163. "timezone": "Asia/Shanghai",
  164. "collation": collation,
  165. "rounding_mode": "half_away_from_zero",
  166. "regex_engine": regex_engine,
  167. }
  168. datasource_uid = new_governance_uid()
  169. input_schema = _snapshot(f"bd:task7:{dialect}:raw")
  170. output_schema = _snapshot(f"bd:task7:{dialect}:clean")
  171. input_binding = {
  172. "id": new_governance_uid(),
  173. "data_source_uid": datasource_uid,
  174. "object_kind": "table",
  175. "object_ref": f"{schema_name}.{source_name}",
  176. "schema_snapshot_id": input_schema["id"],
  177. "access_mode": "read",
  178. "dialect": dialect,
  179. "write_mode": "append",
  180. }
  181. output_binding = {
  182. "id": new_governance_uid(),
  183. "data_source_uid": datasource_uid,
  184. "object_kind": "table",
  185. "object_ref": f"{schema_name}.{target_name}",
  186. "schema_snapshot_id": output_schema["id"],
  187. "access_mode": "write",
  188. "dialect": dialect,
  189. "write_mode": "append",
  190. }
  191. spec = validate_rule_spec(
  192. {
  193. "schema_version": "2.0",
  194. "rule_uid": new_governance_uid(),
  195. "name": f"task7_{dialect}_safe_preflight",
  196. "input_schema_ref": input_schema["schema_ref"],
  197. "output_schema_ref": output_schema["schema_ref"],
  198. "steps": [
  199. {
  200. "id": "trim_name",
  201. "op": "normalize_text",
  202. "column": "name",
  203. "trim": True,
  204. }
  205. ],
  206. "null_policy": "explicit",
  207. "timezone": "Asia/Shanghai",
  208. }
  209. )
  210. rule = {
  211. "id": new_governance_uid(),
  212. "status": "published",
  213. "rule_spec": spec,
  214. "spec_hash": rule_spec_hash(spec),
  215. }
  216. try:
  217. with engine.begin() as connection:
  218. connection.execute(text(f"DROP TABLE IF EXISTS {target_name}"))
  219. connection.execute(text(f"DROP TABLE IF EXISTS {source_name}"))
  220. connection.execute(
  221. text(
  222. f"CREATE TABLE {source_name} ("
  223. "customer_id BIGINT PRIMARY KEY, "
  224. "name VARCHAR(100), mobile VARCHAR(30))"
  225. )
  226. )
  227. connection.execute(
  228. text(
  229. f"CREATE TABLE {target_name} ("
  230. "customer_id BIGINT PRIMARY KEY, "
  231. "name VARCHAR(100), mobile VARCHAR(30))"
  232. )
  233. )
  234. connection.execute(
  235. text(
  236. f"INSERT INTO {source_name} "
  237. "(customer_id, name, mobile) "
  238. "VALUES (1, ' Alice ', '13800138000')"
  239. )
  240. )
  241. compiled = SqlGlotRuleCompiler(dialect).compile(
  242. rule_version=rule,
  243. input_schema=input_schema,
  244. output_schema=output_schema,
  245. input_binding=input_binding,
  246. output_binding=output_binding,
  247. backend=capabilities,
  248. )
  249. schema_hashes = {
  250. "input_schema_hash": input_schema["schema_hash"],
  251. "output_schema_hash": output_schema["schema_hash"],
  252. }
  253. binding_hashes = {
  254. "input": "a" * 64,
  255. "output": "b" * 64,
  256. }
  257. result = ServerOwnedPhysicalPreflightRunner(
  258. artifact_store=None,
  259. datasource_manager=ReadOnlyPreflightManager(engine),
  260. ).run(
  261. {
  262. "backend": "sql_pushdown",
  263. "plan": compiled["plan"],
  264. "plan_hash": compiled["plan_hash"],
  265. "schema_hashes": schema_hashes,
  266. "binding_hashes": binding_hashes,
  267. }
  268. )
  269. assert result["status"] == "success"
  270. assert result["plan_hash"] == compiled["plan_hash"]
  271. assert result["attestation"]["dialect"] == dialect
  272. with engine.connect() as connection:
  273. assert connection.execute(
  274. text(f"SELECT COUNT(*) FROM {target_name}")
  275. ).scalar_one() == 0
  276. finally:
  277. with engine.begin() as connection:
  278. connection.execute(text(f"DROP TABLE IF EXISTS {target_name}"))
  279. connection.execute(text(f"DROP TABLE IF EXISTS {source_name}"))
  280. engine.dispose()
  281. @pytest.mark.parametrize(
  282. ("dialect", "url", "schema_name", "collation", "regex_engine"), CASES
  283. )
  284. def test_bound_rule_compiles_publishes_executes_and_rejects_tampering(
  285. dialect, url, schema_name, collation, regex_engine
  286. ):
  287. from app.core.data_rules.compilers import CompilerRegistry
  288. from app.core.data_rules.compilers.sql import SqlGlotRuleCompiler
  289. from app.core.data_rules.release import BoundSqlPlanService
  290. from app.runner.rule_sql import SqlGlotRulePlanAdapter
  291. from app.runner.rules import RulePlanExecutor
  292. engine = create_engine(url, pool_pre_ping=True)
  293. source_name = "task4_rule_source"
  294. target_name = "task4_rule_target"
  295. source_ref = f"{schema_name}.{source_name}"
  296. target_ref = f"{schema_name}.{target_name}"
  297. capabilities = {
  298. "dialect": dialect,
  299. "timezone": "Asia/Shanghai",
  300. "collation": collation,
  301. "rounding_mode": "half_away_from_zero",
  302. "regex_engine": regex_engine,
  303. }
  304. datasource_uid = new_governance_uid()
  305. input_schema = _snapshot("bd:task4:raw")
  306. output_schema = _snapshot("bd:task4:clean")
  307. input_binding = {
  308. "id": new_governance_uid(),
  309. "data_source_uid": datasource_uid,
  310. "object_kind": "table",
  311. "object_ref": source_ref,
  312. "schema_snapshot_id": input_schema["id"],
  313. "access_mode": "read",
  314. "dialect": dialect,
  315. "write_mode": "append",
  316. }
  317. output_binding = {
  318. "id": new_governance_uid(),
  319. "data_source_uid": datasource_uid,
  320. "object_kind": "table",
  321. "object_ref": target_ref,
  322. "schema_snapshot_id": output_schema["id"],
  323. "access_mode": "write",
  324. "dialect": dialect,
  325. "write_mode": "append",
  326. }
  327. spec = validate_rule_spec(
  328. {
  329. "schema_version": "2.0",
  330. "rule_uid": new_governance_uid(),
  331. "name": "task4_real_sql",
  332. "input_schema_ref": input_schema["schema_ref"],
  333. "output_schema_ref": output_schema["schema_ref"],
  334. "steps": [
  335. {
  336. "id": "trim_name",
  337. "op": "normalize_text",
  338. "column": "name",
  339. "trim": True,
  340. },
  341. {
  342. "id": "mobile_format",
  343. "op": "assert",
  344. "expression": "matches(mobile, '^[0-9]{11}$')",
  345. "on_failure": "reject",
  346. "severity": "error",
  347. },
  348. ],
  349. "null_policy": "explicit",
  350. "timezone": "Asia/Shanghai",
  351. }
  352. )
  353. rule = {
  354. "id": new_governance_uid(),
  355. "status": "published",
  356. "rule_spec": spec,
  357. "spec_hash": rule_spec_hash(spec),
  358. }
  359. try:
  360. with engine.begin() as connection:
  361. connection.execute(text(f"DROP TABLE IF EXISTS {target_name}"))
  362. connection.execute(text(f"DROP TABLE IF EXISTS {source_name}"))
  363. connection.execute(
  364. text(
  365. f"CREATE TABLE {source_name} ("
  366. "customer_id BIGINT PRIMARY KEY, "
  367. "name VARCHAR(100), mobile VARCHAR(30))"
  368. )
  369. )
  370. connection.execute(
  371. text(
  372. f"CREATE TABLE {target_name} ("
  373. "customer_id BIGINT PRIMARY KEY, "
  374. "name VARCHAR(100), mobile VARCHAR(30))"
  375. )
  376. )
  377. connection.execute(
  378. text(
  379. f"INSERT INTO {source_name} "
  380. "(customer_id, name, mobile) VALUES "
  381. "(1, ' Alice ', '13800138000'), "
  382. "(2, ' Bad ', 'not-a-mobile')"
  383. )
  384. )
  385. component_binding_id = new_governance_uid()
  386. idempotency = {
  387. "strategy": "upsert",
  388. "key": "customer_id",
  389. }
  390. repository = PlanRepository(
  391. idempotency,
  392. {
  393. "component_binding": {
  394. "id": component_binding_id,
  395. "rule_version_id": rule["id"],
  396. },
  397. "rule_version": rule,
  398. "input_schema": input_schema,
  399. "output_schema": output_schema,
  400. "input_binding": input_binding,
  401. "output_binding": output_binding,
  402. "backend": capabilities,
  403. },
  404. )
  405. BoundSqlPlanService(
  406. repository,
  407. CompilerRegistry(
  408. {dialect: SqlGlotRuleCompiler(dialect)}
  409. ),
  410. ).compile_and_persist(
  411. component_binding_id=component_binding_id,
  412. rule_version_id=rule["id"],
  413. input_schema_snapshot_id=input_schema["id"],
  414. output_schema_snapshot_id=output_schema["id"],
  415. input_binding_id=input_binding["id"],
  416. output_binding_id=output_binding["id"],
  417. )
  418. record = repository.record
  419. assert record["plan_status"] == "compiled"
  420. compiled = {
  421. "plan": record["plan"],
  422. "plan_hash": record["plan_hash"],
  423. }
  424. adapter = SqlGlotRulePlanAdapter(
  425. DirectManager(
  426. engine,
  427. Definition(dialect, capabilities),
  428. )
  429. )
  430. node = {
  431. "id": "task4_real_rule",
  432. "type": "rule.apply",
  433. "purpose": "write",
  434. "idempotency": idempotency,
  435. "config": {
  436. "component_binding_id": component_binding_id,
  437. "rule_version_id": rule["id"],
  438. "execution_plan_hash": compiled["plan_hash"],
  439. },
  440. }
  441. preflight_evidence = adapter.execute(
  442. plan=compiled["plan"],
  443. node=node,
  444. parameters={},
  445. write_authorized=True,
  446. )
  447. with engine.begin() as connection:
  448. connection.execute(text(f"DELETE FROM {target_name}"))
  449. repository.trust_test_only_preflight_and_publish(
  450. compiled["plan_hash"],
  451. preflight_evidence,
  452. )
  453. executor = RulePlanExecutor(
  454. repository,
  455. adapters={"sql_pushdown": adapter},
  456. )
  457. result = executor.execute(node, {}, write_authorized=True)
  458. assert result["rows_in"] == 2
  459. assert result["rows_out"] == 1
  460. assert result["rows_rejected"] == 1
  461. with engine.connect() as connection:
  462. rows = connection.execute(
  463. text(
  464. f"SELECT customer_id, name, mobile "
  465. f"FROM {target_name} ORDER BY customer_id"
  466. )
  467. ).tuples().all()
  468. assert rows == [(1, "Alice", "13800138000")]
  469. repeated = executor.execute(node, {}, write_authorized=True)
  470. assert repeated["rows_out"] == 1
  471. assert repeated["rows_rejected"] == 1
  472. with engine.connect() as connection:
  473. assert connection.execute(
  474. text(f"SELECT COUNT(*) FROM {target_name}")
  475. ).scalar_one() == 1
  476. repository.record["plan"] = {
  477. **repository.record["plan"],
  478. "result_contract": {
  479. **repository.record["plan"]["result_contract"],
  480. "rows_rejected": "unknown",
  481. },
  482. }
  483. with pytest.raises(NodeExecutionError, match="not executable"):
  484. executor.execute(node, {}, write_authorized=True)
  485. finally:
  486. with engine.begin() as connection:
  487. connection.execute(text(f"DROP TABLE IF EXISTS {target_name}"))
  488. connection.execute(text(f"DROP TABLE IF EXISTS {source_name}"))
  489. engine.dispose()
  490. def test_mysql_upsert_rejects_real_nonunique_and_alternate_unique_targets():
  491. from app.core.data_rules.compilers.sql import SqlGlotRuleCompiler
  492. from app.runner.rule_sql import SqlGlotRulePlanAdapter
  493. dialect, url, schema_name, collation, regex_engine = CASES[1]
  494. engine = create_engine(url, pool_pre_ping=True)
  495. source_name = "task4_unique_source"
  496. nonunique_name = "task4_nonunique_target"
  497. alternate_name = "task4_alternate_target"
  498. capabilities = {
  499. "dialect": dialect,
  500. "timezone": "Asia/Shanghai",
  501. "collation": collation,
  502. "rounding_mode": "half_away_from_zero",
  503. "regex_engine": regex_engine,
  504. }
  505. datasource_uid = new_governance_uid()
  506. input_schema = _snapshot("bd:task4:unique:raw")
  507. output_schema = _snapshot("bd:task4:unique:clean")
  508. spec = validate_rule_spec(
  509. {
  510. "schema_version": "2.0",
  511. "rule_uid": new_governance_uid(),
  512. "name": "task4_unique_attestation",
  513. "input_schema_ref": input_schema["schema_ref"],
  514. "output_schema_ref": output_schema["schema_ref"],
  515. "steps": [
  516. {
  517. "id": "trim_name",
  518. "op": "normalize_text",
  519. "column": "name",
  520. "trim": True,
  521. }
  522. ],
  523. "null_policy": "explicit",
  524. "timezone": "Asia/Shanghai",
  525. }
  526. )
  527. rule = {
  528. "id": new_governance_uid(),
  529. "status": "published",
  530. "rule_spec": spec,
  531. "spec_hash": rule_spec_hash(spec),
  532. }
  533. input_binding = {
  534. "id": new_governance_uid(),
  535. "data_source_uid": datasource_uid,
  536. "object_kind": "table",
  537. "object_ref": f"{schema_name}.{source_name}",
  538. "schema_snapshot_id": input_schema["id"],
  539. "access_mode": "read",
  540. "dialect": dialect,
  541. "write_mode": "append",
  542. }
  543. try:
  544. with engine.begin() as connection:
  545. for name in (alternate_name, nonunique_name, source_name):
  546. connection.execute(text(f"DROP TABLE IF EXISTS {name}"))
  547. connection.execute(
  548. text(
  549. f"CREATE TABLE {source_name} ("
  550. "customer_id BIGINT PRIMARY KEY, "
  551. "name VARCHAR(100), mobile VARCHAR(30))"
  552. )
  553. )
  554. connection.execute(
  555. text(
  556. f"CREATE TABLE {nonunique_name} ("
  557. "customer_id BIGINT, name VARCHAR(100), mobile VARCHAR(30))"
  558. )
  559. )
  560. connection.execute(
  561. text(
  562. f"CREATE TABLE {alternate_name} ("
  563. "customer_id BIGINT PRIMARY KEY, "
  564. "name VARCHAR(100), mobile VARCHAR(30) UNIQUE)"
  565. )
  566. )
  567. connection.execute(
  568. text(
  569. f"INSERT INTO {source_name} "
  570. "(customer_id, name, mobile) "
  571. "VALUES (1, ' Alice ', '13800138000')"
  572. )
  573. )
  574. adapter = SqlGlotRulePlanAdapter(
  575. DirectManager(engine, Definition(dialect, capabilities))
  576. )
  577. for target_name, error in (
  578. (nonunique_name, "exact unique key"),
  579. (alternate_name, "alternate unique"),
  580. ):
  581. output_binding = {
  582. "id": new_governance_uid(),
  583. "data_source_uid": datasource_uid,
  584. "object_kind": "table",
  585. "object_ref": f"{schema_name}.{target_name}",
  586. "schema_snapshot_id": output_schema["id"],
  587. "access_mode": "write",
  588. "dialect": dialect,
  589. "write_mode": "append",
  590. }
  591. compiled = SqlGlotRuleCompiler(dialect).compile(
  592. rule_version=rule,
  593. input_schema=input_schema,
  594. output_schema=output_schema,
  595. input_binding=input_binding,
  596. output_binding=output_binding,
  597. backend=capabilities,
  598. )
  599. node = {
  600. "id": "task4_unique_rule",
  601. "type": "rule.apply",
  602. "purpose": "write",
  603. "idempotency": {
  604. "strategy": "upsert",
  605. "key": "customer_id",
  606. },
  607. "config": {
  608. "component_binding_id": new_governance_uid(),
  609. "rule_version_id": rule["id"],
  610. "execution_plan_hash": compiled["plan_hash"],
  611. },
  612. }
  613. with pytest.raises(NodeExecutionError, match=error):
  614. adapter.execute(
  615. plan=compiled["plan"],
  616. node=node,
  617. parameters={},
  618. write_authorized=True,
  619. )
  620. with engine.connect() as connection:
  621. assert connection.execute(
  622. text(f"SELECT COUNT(*) FROM {target_name}")
  623. ).scalar_one() == 0
  624. finally:
  625. with engine.begin() as connection:
  626. for name in (alternate_name, nonunique_name, source_name):
  627. connection.execute(text(f"DROP TABLE IF EXISTS {name}"))
  628. engine.dispose()