test_data_rule_sql_execution.py 24 KB

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