test_data_rule_polars_execution.py 75 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003
  1. from __future__ import annotations
  2. import json
  3. import re
  4. from datetime import UTC, datetime, timedelta
  5. from pathlib import Path
  6. import polars as pl
  7. import pytest
  8. from minio import Minio
  9. from sqlalchemy import create_engine, text
  10. from app.core.common.identifiers import new_governance_uid
  11. from app.core.data_rules.contracts import rule_spec_hash, validate_rule_spec
  12. from app.core.data_rules.execution_contracts import canonical_schema_hash
  13. COMPOSE = (
  14. Path(__file__).resolve().parents[2]
  15. / "deploy"
  16. / "docker"
  17. / "docker-compose.yml"
  18. )
  19. def _compose_value(pattern):
  20. source = COMPOSE.read_text(encoding="utf-8")
  21. match = re.search(pattern, source, flags=re.DOTALL)
  22. assert match is not None
  23. return match.group(1)
  24. def _schema(schema_ref, fields):
  25. normalized = [
  26. {"name": name, "type": field_type, "nullable": nullable}
  27. for name, field_type, nullable in fields
  28. ]
  29. return {
  30. "id": new_governance_uid(),
  31. "schema_ref": schema_ref,
  32. "schema_hash": canonical_schema_hash(normalized),
  33. "fields": normalized,
  34. "source_revision": "task5:real-cross-source",
  35. }
  36. def _binding(schema, *, source_uid, access_mode, object_ref):
  37. return {
  38. "id": new_governance_uid(),
  39. "data_source_uid": source_uid,
  40. "object_kind": "parquet_artifact",
  41. "object_ref": object_ref,
  42. "schema_snapshot_id": schema["id"],
  43. "access_mode": access_mode,
  44. "dialect": "parquet",
  45. "write_mode": "append",
  46. }
  47. def test_real_postgres_mysql_minio_polars_cross_source_execution(tmp_path):
  48. from app.core.data_rules.compilers.polars import PolarsRuleCompiler
  49. from app.runner.artifacts import ArtifactStore, PostgresArtifactResolver
  50. from app.runner.rule_polars import PolarsRulePlanAdapter
  51. from app.runner.rules import PostgresRulePlanRepository, RulePlanExecutor
  52. source_user = _compose_value(
  53. r"source-postgres:.*?POSTGRES_USER:\s*([^\s]+)"
  54. )
  55. source_password = _compose_value(
  56. r"source-postgres:.*?POSTGRES_PASSWORD:\s*([^\s]+)"
  57. )
  58. platform_user = _compose_value(
  59. r"\n postgres:.*?POSTGRES_USER:\s*([^\s]+)"
  60. )
  61. platform_password = _compose_value(
  62. r"\n postgres:.*?POSTGRES_PASSWORD:\s*([^\s]+)"
  63. )
  64. postgres_port = _compose_value(r'"(25432):5432"')
  65. mysql_port = _compose_value(r'"(23306):3306"')
  66. minio_user = _compose_value(r"MINIO_ROOT_USER:\s*([^\s]+)")
  67. minio_password = _compose_value(r"MINIO_ROOT_PASSWORD:\s*([^\s]+)")
  68. minio_port = _compose_value(r'"(19000):9000"')
  69. platform_port = _compose_value(r'"(15432):5432"')
  70. bucket = _compose_value(r"mc mb --ignore-existing local/([^\s]+)")
  71. postgres = create_engine(
  72. f"postgresql+psycopg2://{source_user}:{source_password}"
  73. f"@127.0.0.1:{postgres_port}/acceptance",
  74. pool_pre_ping=True,
  75. )
  76. mysql = create_engine(
  77. f"mysql+pymysql://{source_user}:{source_password}"
  78. f"@127.0.0.1:{mysql_port}/acceptance",
  79. pool_pre_ping=True,
  80. )
  81. platform = create_engine(
  82. f"postgresql+psycopg2://{platform_user}:{platform_password}"
  83. f"@127.0.0.1:{platform_port}/dataops",
  84. pool_pre_ping=True,
  85. )
  86. minio = Minio(
  87. f"127.0.0.1:{minio_port}",
  88. access_key=minio_user,
  89. secret_key=minio_password,
  90. secure=False,
  91. )
  92. store = ArtifactStore(
  93. minio,
  94. bucket=bucket,
  95. max_artifact_bytes=4 * 1024 * 1024,
  96. max_rows=1_000,
  97. memory_limit_bytes=256 * 1024 * 1024,
  98. max_ttl_seconds=3600,
  99. )
  100. correlation_id = new_governance_uid()
  101. failure_correlation_id = new_governance_uid()
  102. unknown_correlation_id = new_governance_uid()
  103. lease_correlation_id = new_governance_uid()
  104. sample_crash_correlation_id = new_governance_uid()
  105. receipt_correlation_id = new_governance_uid()
  106. failed_receipt_correlation_id = new_governance_uid()
  107. no_run_crash_correlation_id = new_governance_uid()
  108. evidence_crash_correlation_id = new_governance_uid()
  109. sql_binding_id = new_governance_uid()
  110. prefix = f"rules/{correlation_id}/"
  111. customer_table = "task5_polars_customers"
  112. segment_table = "task5_polars_segments"
  113. rule_uid = new_governance_uid()
  114. rule_id = new_governance_uid()
  115. downstream_rule_uid = new_governance_uid()
  116. downstream_rule_id = new_governance_uid()
  117. dataflow_uid = new_governance_uid()
  118. dataflow_version_id = new_governance_uid()
  119. deployment_id = new_governance_uid()
  120. component_binding_id = new_governance_uid()
  121. plan_id = new_governance_uid()
  122. downstream_component_binding_id = new_governance_uid()
  123. downstream_plan_id = new_governance_uid()
  124. ledger_jti = None
  125. retry_ledger_jti = None
  126. no_run_crash_jti = None
  127. evidence_crash_jti = None
  128. gateway_candidate_id = None
  129. try:
  130. with postgres.begin() as connection:
  131. connection.execute(text(f"DROP TABLE IF EXISTS {customer_table}"))
  132. connection.execute(
  133. text(
  134. f"CREATE TABLE {customer_table} ("
  135. "customer_id BIGINT NOT NULL, "
  136. "name VARCHAR(100), mobile VARCHAR(30), "
  137. "segment_code VARCHAR(20), version_no BIGINT NOT NULL)"
  138. )
  139. )
  140. connection.execute(
  141. text(
  142. f"INSERT INTO {customer_table} "
  143. "(customer_id, name, mobile, segment_code, version_no) "
  144. "VALUES "
  145. "(1, ' Alice ', '13800138000', 'A', 1), "
  146. "(1, ' Alice Updated ', '13800138000', 'A', 2), "
  147. "(2, ' Bad ', 'invalid', 'B', 1), "
  148. "(3, ' Carol ', '13900139000', 'C', 1)"
  149. )
  150. )
  151. with mysql.begin() as connection:
  152. connection.execute(text(f"DROP TABLE IF EXISTS {segment_table}"))
  153. connection.execute(
  154. text(
  155. f"CREATE TABLE {segment_table} ("
  156. "code VARCHAR(20) PRIMARY KEY, "
  157. "segment_name VARCHAR(100) NOT NULL)"
  158. )
  159. )
  160. connection.execute(
  161. text(
  162. f"INSERT INTO {segment_table} (code, segment_name) "
  163. "VALUES ('A', 'Gold'), ('B', 'Basic'), ('C', 'Silver')"
  164. )
  165. )
  166. with postgres.connect() as connection:
  167. customer_rows = [
  168. dict(row)
  169. for row in connection.execute(
  170. text(
  171. f"SELECT customer_id, name, mobile, "
  172. f"segment_code, version_no FROM {customer_table}"
  173. )
  174. ).mappings()
  175. ]
  176. with mysql.connect() as connection:
  177. segment_rows = [
  178. dict(row)
  179. for row in connection.execute(
  180. text(
  181. f"SELECT code, segment_name FROM {segment_table}"
  182. )
  183. ).mappings()
  184. ]
  185. input_schema = _schema(
  186. "bd:task5:customer:raw",
  187. [
  188. ("customer_id", "integer", False),
  189. ("name", "string", True),
  190. ("mobile", "string", True),
  191. ("segment_code", "string", True),
  192. ("version_no", "integer", False),
  193. ],
  194. )
  195. lookup_schema = _schema(
  196. "bd:task5:segment:lookup",
  197. [
  198. ("code", "string", False),
  199. ("segment_name", "string", False),
  200. ],
  201. )
  202. output_schema = _schema(
  203. "bd:task5:customer:enriched",
  204. [
  205. ("customer_id", "integer", False),
  206. ("name", "string", True),
  207. ("mobile", "string", True),
  208. ("segment_code", "string", True),
  209. ("version_no", "integer", False),
  210. ("segment_name", "string", True),
  211. ],
  212. )
  213. input_binding = _binding(
  214. input_schema,
  215. source_uid=new_governance_uid(),
  216. access_mode="read",
  217. object_ref="postgres-customer-artifact",
  218. )
  219. lookup_binding = _binding(
  220. lookup_schema,
  221. source_uid=new_governance_uid(),
  222. access_mode="read",
  223. object_ref="mysql-segment-artifact",
  224. )
  225. output_binding = _binding(
  226. output_schema,
  227. source_uid=new_governance_uid(),
  228. access_mode="read_write",
  229. object_ref="polars-output-artifact",
  230. )
  231. downstream_output_binding = _binding(
  232. output_schema,
  233. source_uid=new_governance_uid(),
  234. access_mode="write",
  235. object_ref="polars-downstream-output-artifact",
  236. )
  237. spec = validate_rule_spec(
  238. {
  239. "schema_version": "2.0",
  240. "rule_uid": new_governance_uid(),
  241. "name": "task5_real_cross_source",
  242. "input_schema_ref": input_schema["schema_ref"],
  243. "output_schema_ref": output_schema["schema_ref"],
  244. "steps": [
  245. {
  246. "id": "normalize_name",
  247. "op": "normalize_text",
  248. "column": "name",
  249. "trim": True,
  250. },
  251. {
  252. "id": "join_segment",
  253. "op": "lookup_join",
  254. "lookup": {
  255. "binding_id": lookup_binding["id"],
  256. "left_on": ["segment_code"],
  257. "right_on": ["code"],
  258. "select": {
  259. "segment_name": "segment_name"
  260. },
  261. "how": "left",
  262. },
  263. },
  264. {
  265. "id": "valid_mobile",
  266. "op": "assert",
  267. "expression": "matches(mobile, '^[0-9]{11}$')",
  268. "on_failure": "reject",
  269. "severity": "error",
  270. },
  271. {
  272. "id": "latest_customer",
  273. "op": "deduplicate",
  274. "keys": ["customer_id"],
  275. "order_by": ["version_no"],
  276. "keep": "last",
  277. },
  278. ],
  279. "null_policy": "explicit",
  280. "timezone": "Asia/Shanghai",
  281. }
  282. )
  283. rule = {
  284. "id": rule_id,
  285. "status": "published",
  286. "rule_spec": spec,
  287. "spec_hash": rule_spec_hash(spec),
  288. }
  289. compiled = PolarsRuleCompiler().compile(
  290. rule_version=rule,
  291. input_schema=input_schema,
  292. output_schema=output_schema,
  293. input_binding=input_binding,
  294. output_binding=output_binding,
  295. backend={
  296. "max_rows": 1_000,
  297. "max_artifact_bytes": 4 * 1024 * 1024,
  298. "memory_limit_bytes": 256 * 1024 * 1024,
  299. "masking_policies": {},
  300. "lookup_bindings": {
  301. lookup_binding["id"]: {
  302. "binding": lookup_binding,
  303. "schema": lookup_schema,
  304. }
  305. },
  306. },
  307. )
  308. downstream_spec = validate_rule_spec(
  309. {
  310. "schema_version": "2.0",
  311. "rule_uid": new_governance_uid(),
  312. "name": "task6_real_artifact_handoff",
  313. "input_schema_ref": output_schema["schema_ref"],
  314. "output_schema_ref": output_schema["schema_ref"],
  315. "steps": [
  316. {
  317. "id": "normalize_downstream_name",
  318. "op": "normalize_text",
  319. "column": "name",
  320. "trim": True,
  321. }
  322. ],
  323. "null_policy": "explicit",
  324. "timezone": "Asia/Shanghai",
  325. }
  326. )
  327. downstream_rule = {
  328. "id": downstream_rule_id,
  329. "status": "published",
  330. "rule_spec": downstream_spec,
  331. "spec_hash": rule_spec_hash(downstream_spec),
  332. }
  333. downstream_compiled = PolarsRuleCompiler().compile(
  334. rule_version=downstream_rule,
  335. input_schema=output_schema,
  336. output_schema=output_schema,
  337. input_binding=output_binding,
  338. output_binding=downstream_output_binding,
  339. backend={
  340. "max_rows": 1_000,
  341. "max_artifact_bytes": 4 * 1024 * 1024,
  342. "memory_limit_bytes": 256 * 1024 * 1024,
  343. "masking_policies": {},
  344. "lookup_bindings": {},
  345. },
  346. )
  347. lookup_operation = compiled["plan"]["operations"][1]
  348. schema_hashes = {
  349. "rule_spec_hash": compiled["plan"]["rule_spec_hash"],
  350. "input_schema_snapshot_id": input_schema["id"],
  351. "input_schema_hash": input_schema["schema_hash"],
  352. "output_schema_snapshot_id": output_schema["id"],
  353. "output_schema_hash": output_schema["schema_hash"],
  354. }
  355. with platform.begin() as connection:
  356. for schema in (input_schema, lookup_schema, output_schema):
  357. connection.execute(
  358. text(
  359. """
  360. INSERT INTO public.data_schema_snapshots
  361. (id, schema_ref, schema_hash, fields, source_revision)
  362. VALUES (CAST(:id AS uuid), :schema_ref, :schema_hash,
  363. CAST(:fields AS jsonb), :source_revision)
  364. """
  365. ),
  366. {**schema, "fields": json.dumps(schema["fields"])},
  367. )
  368. connection.execute(
  369. text(
  370. """
  371. INSERT INTO public.data_rules
  372. (id, rule_uid, name, category, status)
  373. VALUES (CAST(:id AS uuid), CAST(:rule_uid AS uuid),
  374. :name, 'general', 'active')
  375. """
  376. ),
  377. {
  378. "id": new_governance_uid(),
  379. "rule_uid": rule_uid,
  380. "name": spec["name"],
  381. },
  382. )
  383. connection.execute(
  384. text(
  385. """
  386. INSERT INTO public.data_rules
  387. (id, rule_uid, name, category, status)
  388. VALUES (CAST(:id AS uuid), CAST(:rule_uid AS uuid),
  389. :name, 'general', 'active')
  390. """
  391. ),
  392. {
  393. "id": new_governance_uid(),
  394. "rule_uid": downstream_rule_uid,
  395. "name": downstream_spec["name"],
  396. },
  397. )
  398. connection.execute(
  399. text(
  400. """
  401. INSERT INTO public.data_rule_versions
  402. (id, rule_uid, version_no, source_text, source_language,
  403. rule_spec, spec_hash, generated_kind, status, published_at)
  404. VALUES (CAST(:id AS uuid), CAST(:rule_uid AS uuid), 1,
  405. :source_text, 'en', CAST(:rule_spec AS jsonb),
  406. :spec_hash, 'polars', 'published',
  407. CURRENT_TIMESTAMP)
  408. """
  409. ),
  410. {
  411. "id": rule_id,
  412. "rule_uid": rule_uid,
  413. "source_text": "Task 5 real cross-source integration",
  414. "rule_spec": json.dumps(spec),
  415. "spec_hash": rule["spec_hash"],
  416. },
  417. )
  418. connection.execute(
  419. text(
  420. """
  421. INSERT INTO public.data_rule_versions
  422. (id, rule_uid, version_no, source_text,
  423. source_language, rule_spec, spec_hash,
  424. generated_kind, status, published_at)
  425. VALUES (
  426. CAST(:id AS uuid), CAST(:rule_uid AS uuid), 1,
  427. :source_text, 'en', CAST(:rule_spec AS jsonb),
  428. :spec_hash, 'polars', 'published',
  429. CURRENT_TIMESTAMP
  430. )
  431. """
  432. ),
  433. {
  434. "id": downstream_rule_id,
  435. "rule_uid": downstream_rule_uid,
  436. "source_text": (
  437. "Task 6 real two-node artifact handoff"
  438. ),
  439. "rule_spec": json.dumps(downstream_spec),
  440. "spec_hash": downstream_rule["spec_hash"],
  441. },
  442. )
  443. connection.execute(
  444. text(
  445. """
  446. INSERT INTO public.dataflow_versions
  447. (id, dataflow_uid, version_no, name, dataflow_spec,
  448. input_schema_hashes, output_schema_hash, status,
  449. released_at)
  450. VALUES (CAST(:id AS uuid), CAST(:dataflow_uid AS uuid), 1,
  451. :name, '{}'::jsonb,
  452. CAST(:input_schema_hashes AS jsonb),
  453. :output_schema_hash, 'released', CURRENT_TIMESTAMP)
  454. """
  455. ),
  456. {
  457. "id": dataflow_version_id,
  458. "dataflow_uid": dataflow_uid,
  459. "name": "Task 5 real cross-source integration",
  460. "input_schema_hashes": json.dumps(
  461. [
  462. input_schema["schema_hash"],
  463. lookup_schema["schema_hash"],
  464. ]
  465. ),
  466. "output_schema_hash": output_schema["schema_hash"],
  467. },
  468. )
  469. connection.execute(
  470. text(
  471. """
  472. INSERT INTO public.dataflow_deployments
  473. (id, dataflow_version_id, environment, deployment_config,
  474. status)
  475. VALUES (CAST(:id AS uuid),
  476. CAST(:dataflow_version_id AS uuid), 'test',
  477. '{}'::jsonb, 'disabled')
  478. """
  479. ),
  480. {
  481. "id": deployment_id,
  482. "dataflow_version_id": dataflow_version_id,
  483. },
  484. )
  485. for logical_ref, binding, binding_hash in (
  486. (
  487. "customers",
  488. input_binding,
  489. compiled["plan"]["input_binding_hash"],
  490. ),
  491. (
  492. "segments",
  493. lookup_binding,
  494. lookup_operation["lookup_binding_hash"],
  495. ),
  496. (
  497. "enriched",
  498. output_binding,
  499. compiled["plan"]["output_binding_hash"],
  500. ),
  501. (
  502. "downstream",
  503. downstream_output_binding,
  504. downstream_compiled["plan"][
  505. "output_binding_hash"
  506. ],
  507. ),
  508. ):
  509. connection.execute(
  510. text(
  511. """
  512. INSERT INTO public.dataflow_dataset_bindings
  513. (id, dataflow_deployment_id, logical_ref,
  514. data_source_uid, object_kind, object_ref,
  515. schema_snapshot_id, dialect, access_mode, write_mode,
  516. binding_hash)
  517. VALUES (CAST(:id AS uuid), CAST(:deployment_id AS uuid),
  518. :logical_ref, CAST(:source_uid AS uuid),
  519. 'parquet_artifact', :object_ref,
  520. CAST(:schema_snapshot_id AS uuid), 'parquet',
  521. :access_mode, 'append', :binding_hash)
  522. """
  523. ),
  524. {
  525. "id": binding["id"],
  526. "deployment_id": deployment_id,
  527. "logical_ref": logical_ref,
  528. "source_uid": binding["data_source_uid"],
  529. "object_ref": binding["object_ref"],
  530. "schema_snapshot_id": binding["schema_snapshot_id"],
  531. "access_mode": binding["access_mode"],
  532. "binding_hash": binding_hash,
  533. },
  534. )
  535. connection.execute(
  536. text(
  537. """
  538. INSERT INTO public.dataflow_component_bindings
  539. (id, dataflow_version_id, component_id, component_kind,
  540. rule_version_id, stage, order_no, idempotency, provenance)
  541. VALUES (CAST(:id AS uuid),
  542. CAST(:dataflow_version_id AS uuid),
  543. 'task5_real_polars', 'rule.apply',
  544. CAST(:rule_version_id AS uuid), 'transform', 0,
  545. CAST(:idempotency AS jsonb), '{}'::jsonb)
  546. """
  547. ),
  548. {
  549. "id": component_binding_id,
  550. "dataflow_version_id": dataflow_version_id,
  551. "rule_version_id": rule_id,
  552. "idempotency": json.dumps(
  553. {
  554. "strategy": "deduplication_key",
  555. "key": "customer_id",
  556. }
  557. ),
  558. },
  559. )
  560. connection.execute(
  561. text(
  562. """
  563. INSERT INTO public.dataflow_component_bindings
  564. (id, dataflow_version_id, component_id,
  565. component_kind, rule_version_id, stage, order_no,
  566. idempotency, provenance)
  567. VALUES (
  568. CAST(:id AS uuid),
  569. CAST(:dataflow_version_id AS uuid),
  570. 'task6_real_handoff', 'rule.apply',
  571. CAST(:rule_version_id AS uuid), 'transform', 1,
  572. CAST(:idempotency AS jsonb), '{}'::jsonb
  573. )
  574. """
  575. ),
  576. {
  577. "id": downstream_component_binding_id,
  578. "dataflow_version_id": dataflow_version_id,
  579. "rule_version_id": downstream_rule_id,
  580. "idempotency": json.dumps(
  581. {
  582. "strategy": "deduplication_key",
  583. "key": "customer_id",
  584. }
  585. ),
  586. },
  587. )
  588. connection.execute(
  589. text(
  590. """
  591. INSERT INTO public.rule_execution_plans
  592. (id, component_binding_id, backend, compiler_version, plan,
  593. plan_hash, schema_hashes, status)
  594. VALUES (CAST(:id AS uuid),
  595. CAST(:component_binding_id AS uuid),
  596. 'polars_batch', :compiler_version,
  597. CAST(:plan AS jsonb), :plan_hash,
  598. CAST(:schema_hashes AS jsonb), 'published')
  599. """
  600. ),
  601. {
  602. "id": plan_id,
  603. "component_binding_id": component_binding_id,
  604. "compiler_version": compiled["compiler_version"],
  605. "plan": json.dumps(compiled["plan"]),
  606. "plan_hash": compiled["plan_hash"],
  607. "schema_hashes": json.dumps(schema_hashes),
  608. },
  609. )
  610. connection.execute(
  611. text(
  612. """
  613. INSERT INTO public.rule_execution_plans
  614. (id, component_binding_id, backend,
  615. compiler_version, plan, plan_hash,
  616. schema_hashes, status)
  617. VALUES (
  618. CAST(:id AS uuid),
  619. CAST(:component_binding_id AS uuid),
  620. 'polars_batch', :compiler_version,
  621. CAST(:plan AS jsonb), :plan_hash,
  622. CAST(:schema_hashes AS jsonb), 'published'
  623. )
  624. """
  625. ),
  626. {
  627. "id": downstream_plan_id,
  628. "component_binding_id": (
  629. downstream_component_binding_id
  630. ),
  631. "compiler_version": downstream_compiled[
  632. "compiler_version"
  633. ],
  634. "plan": json.dumps(downstream_compiled["plan"]),
  635. "plan_hash": downstream_compiled["plan_hash"],
  636. "schema_hashes": json.dumps(
  637. {
  638. "rule_spec_hash": downstream_compiled[
  639. "plan"
  640. ]["rule_spec_hash"],
  641. "input_schema_snapshot_id": output_schema[
  642. "id"
  643. ],
  644. "input_schema_hash": output_schema[
  645. "schema_hash"
  646. ],
  647. "output_schema_snapshot_id": output_schema[
  648. "id"
  649. ],
  650. "output_schema_hash": output_schema[
  651. "schema_hash"
  652. ],
  653. }
  654. ),
  655. },
  656. )
  657. customer_path = tmp_path / "customers.parquet"
  658. segment_path = tmp_path / "segments.parquet"
  659. pl.DataFrame(customer_rows).write_parquet(customer_path)
  660. pl.DataFrame(segment_rows).write_parquet(segment_path)
  661. resolver = PostgresArtifactResolver(platform, store)
  662. customer_artifact = resolver.publish_path(
  663. str(customer_path),
  664. binding_id=input_binding["id"],
  665. binding_hash=compiled["plan"]["input_binding_hash"],
  666. correlation_id=correlation_id,
  667. kind="input",
  668. ttl_seconds=900,
  669. schema_fields=input_schema["fields"],
  670. limits=compiled["plan"]["resource_limits"],
  671. )
  672. segment_artifact = resolver.publish_path(
  673. str(segment_path),
  674. binding_id=lookup_binding["id"],
  675. binding_hash=lookup_operation["lookup_binding_hash"],
  676. correlation_id=correlation_id,
  677. kind="lookup",
  678. ttl_seconds=900,
  679. schema_fields=lookup_schema["fields"],
  680. limits=compiled["plan"]["resource_limits"],
  681. )
  682. node = {
  683. "id": "task5_real_polars",
  684. "type": "rule.apply",
  685. "purpose": "write",
  686. "idempotency": {
  687. "strategy": "deduplication_key",
  688. "key": "customer_id",
  689. },
  690. "config": {
  691. "component_binding_id": component_binding_id,
  692. "rule_version_id": rule["id"],
  693. "execution_plan_hash": compiled["plan_hash"],
  694. },
  695. }
  696. from app.core.mcp.gateway import SchedulingGateway
  697. from app.core.mcp.identity import AgentIdentity
  698. from app.core.mcp.persistence import PostgresSchedulingPlanStore
  699. from app.runner.api import create_runner_app
  700. from app.runner.auth import TaskTokenIssuer, TaskTokenVerifier
  701. from app.runner.ledger import PostgresTaskLedger
  702. from app.runner.nodes import NodeRegistry
  703. from app.runner.rule_evidence import PostgresRuleEvidenceWriter
  704. class Audit:
  705. def __init__(self):
  706. self.events = []
  707. def record(self, event):
  708. self.events.append(event)
  709. class CanaryEngine:
  710. def __init__(self):
  711. self.calls = []
  712. def deploy_disabled(self, _definition):
  713. return {"revision": "task6-real-gateway"}
  714. def activate(self, namespace, flow_id):
  715. self.calls.append(("activate", namespace, flow_id))
  716. def execute(self, namespace, flow_id, inputs=None, **_options):
  717. self.calls.append(
  718. ("execute", namespace, flow_id, dict(inputs or {}))
  719. )
  720. return {"id": f"task6-canary-{correlation_id}"}
  721. def deactivate(self, namespace, flow_id):
  722. self.calls.append(("deactivate", namespace, flow_id))
  723. task_secret = "task5-real-http-secret-value-32-bytes"
  724. verifier = TaskTokenVerifier(task_secret)
  725. gateway_engine = CanaryEngine()
  726. gateway_store = PostgresSchedulingPlanStore(platform)
  727. gateway = SchedulingGateway(
  728. plans=gateway_store,
  729. audit=Audit(),
  730. engine=gateway_engine,
  731. token_issuer=TaskTokenIssuer(task_secret),
  732. )
  733. gateway_identity = AgentIdentity(
  734. subject="task6-real-scheduler",
  735. roles=frozenset({"scheduler"}),
  736. business_domains=frozenset({"sales"}),
  737. environments=frozenset({"test"}),
  738. correlation_id=correlation_id,
  739. )
  740. candidate = gateway.create_candidate_plan(
  741. gateway_identity,
  742. business_domain="sales",
  743. environment="test",
  744. workflow_spec={
  745. "schema_version": "1.0",
  746. "dataflow_uid": dataflow_uid,
  747. "name": "Task 6 governed rule canary",
  748. "nodes": [node],
  749. "edges": [],
  750. "parameters": {},
  751. },
  752. schedule_plan={
  753. "schema_version": "1.0",
  754. "timezone": "Asia/Shanghai",
  755. "triggers": [{"type": "manual"}],
  756. "max_concurrency": 1,
  757. "conflict_policy": "skip",
  758. "timeout_seconds": 600,
  759. "retry": {
  760. "max_attempts": 1,
  761. "delay_seconds": 1,
  762. },
  763. "backfill": {"max_days": 1, "max_runs": 1},
  764. },
  765. )
  766. gateway_candidate_id = candidate["candidate_id"]
  767. with platform.begin() as connection:
  768. connection.execute(
  769. text(
  770. """
  771. UPDATE public.dataflow_workflow_versions
  772. SET write_authorized = TRUE
  773. WHERE id = CAST(:id AS uuid)
  774. """
  775. ),
  776. {"id": gateway_candidate_id},
  777. )
  778. deployed = gateway.deploy_disabled_version(
  779. gateway_identity,
  780. candidate_id=gateway_candidate_id,
  781. business_domain="sales",
  782. environment="test",
  783. )
  784. assert deployed["deployment_id"] == deployment_id
  785. assert deployed["candidate_id"] != deployed["deployment_id"]
  786. gateway.run_canary(
  787. gateway_identity,
  788. candidate_id=gateway_candidate_id,
  789. business_domain="sales",
  790. environment="test",
  791. inputs={},
  792. )
  793. execution_call = next(
  794. call for call in gateway_engine.calls if call[0] == "execute"
  795. )
  796. task_token = execution_call[3]["dataops_task_tokens"][node["id"]]
  797. issued_claims = verifier.verify(task_token, node=node)
  798. assert issued_claims.deployment_id == deployment_id
  799. assert issued_claims.workflow_version == 1
  800. assert issued_claims.environment == "test"
  801. executor = RulePlanExecutor(
  802. PostgresRulePlanRepository(platform),
  803. adapters={
  804. "polars_batch": PolarsRulePlanAdapter(
  805. artifact_store=store,
  806. artifact_resolver=resolver,
  807. artifact_ttl_seconds=900,
  808. )
  809. },
  810. )
  811. result = executor.execute(
  812. node,
  813. {},
  814. write_authorized=True,
  815. correlation_id=correlation_id,
  816. )
  817. repeated = executor.execute(
  818. node,
  819. {},
  820. write_authorized=True,
  821. correlation_id=correlation_id,
  822. )
  823. assert result["rows_in"] == 4
  824. assert result["rows_out"] == 2
  825. assert result["rows_rejected"] == 1
  826. assert result["rows_deduplicated"] == 1
  827. assert result["rows_filtered"] == 0
  828. assert result["rows_join_dropped"] == 0
  829. assert result["rows_aggregated"] == 0
  830. assert result["violation_count"] == 1
  831. assert result["violations"] == [
  832. {"step_id": "valid_mobile", "count": 1}
  833. ]
  834. output = store.read(
  835. result["artifact_ref"],
  836. result["digest"],
  837. expected_schema_fields=output_schema["fields"],
  838. limits=compiled["plan"]["resource_limits"],
  839. ).collect()
  840. assert output.sort("customer_id").to_dicts() == [
  841. {
  842. "customer_id": 1,
  843. "mobile": "13800138000",
  844. "name": "Alice Updated",
  845. "segment_code": "A",
  846. "segment_name": "Gold",
  847. "version_no": 2,
  848. },
  849. {
  850. "customer_id": 3,
  851. "mobile": "13900139000",
  852. "name": "Carol",
  853. "segment_code": "C",
  854. "segment_name": "Silver",
  855. "version_no": 1,
  856. },
  857. ]
  858. assert all(
  859. item.object_name.startswith(prefix)
  860. for item in minio.list_objects(
  861. bucket, prefix=prefix, recursive=True
  862. )
  863. )
  864. assert repeated["rows_out"] == 2
  865. assert repeated["artifact_ref"] == result["artifact_ref"]
  866. assert "schema_fields" not in result
  867. ledger_jti = verifier.verify(task_token, node=node).jti
  868. retry_token = TaskTokenIssuer(task_secret).issue(
  869. task_uid=new_governance_uid(),
  870. dataflow_uid=dataflow_uid,
  871. deployment_id=deployment_id,
  872. environment="test",
  873. workflow_version=1,
  874. correlation_id=correlation_id,
  875. node=node,
  876. write_authorized=True,
  877. )
  878. retry_ledger_jti = verifier.verify(
  879. retry_token, node=node
  880. ).jti
  881. real_evidence_writer = PostgresRuleEvidenceWriter(
  882. platform,
  883. store,
  884. sample_ttl_seconds=900,
  885. )
  886. evidenced_executor = RulePlanExecutor(
  887. PostgresRulePlanRepository(platform),
  888. adapters={
  889. "polars_batch": PolarsRulePlanAdapter(
  890. artifact_store=store,
  891. artifact_resolver=resolver,
  892. artifact_ttl_seconds=900,
  893. )
  894. },
  895. evidence_writer=real_evidence_writer,
  896. )
  897. real_ledger = PostgresTaskLedger(platform, lease_seconds=30)
  898. runner_app = create_runner_app(
  899. verifier=verifier,
  900. ledger=real_ledger,
  901. registry=NodeRegistry({"rule.apply": evidenced_executor}),
  902. )
  903. no_run_crash_token = TaskTokenIssuer(task_secret).issue(
  904. task_uid=new_governance_uid(),
  905. dataflow_uid=dataflow_uid,
  906. deployment_id=deployment_id,
  907. environment="test",
  908. workflow_version=1,
  909. correlation_id=no_run_crash_correlation_id,
  910. node=node,
  911. write_authorized=True,
  912. )
  913. no_run_claims = verifier.verify(no_run_crash_token, node=node)
  914. no_run_crash_jti = no_run_claims.jti
  915. evidence_crash_token = TaskTokenIssuer(task_secret).issue(
  916. task_uid=new_governance_uid(),
  917. dataflow_uid=dataflow_uid,
  918. deployment_id=deployment_id,
  919. environment="test",
  920. workflow_version=1,
  921. correlation_id=evidence_crash_correlation_id,
  922. node=node,
  923. write_authorized=True,
  924. )
  925. evidence_crash_claims = verifier.verify(
  926. evidence_crash_token,
  927. node=node,
  928. )
  929. evidence_crash_jti = evidence_crash_claims.jti
  930. def task_binding(claims):
  931. return {
  932. "task_uid": claims.task_uid,
  933. "dataflow_uid": claims.dataflow_uid,
  934. "deployment_id": claims.deployment_id,
  935. "environment": claims.environment,
  936. "workflow_version": claims.workflow_version,
  937. "correlation_id": claims.correlation_id,
  938. "node_id": claims.node_id,
  939. "node_type": claims.node_type,
  940. "data_source_uid": None,
  941. "idempotency_key": "customer_id",
  942. }
  943. assert real_ledger.claim(
  944. no_run_crash_jti,
  945. task_binding(no_run_claims),
  946. expires_at=no_run_claims.expires_at,
  947. )
  948. assert real_ledger.claim(
  949. evidence_crash_jti,
  950. task_binding(evidence_crash_claims),
  951. expires_at=evidence_crash_claims.expires_at,
  952. )
  953. real_evidence_writer.start(
  954. component_binding_id=component_binding_id,
  955. rule_version_id=rule_id,
  956. plan_hash=compiled["plan_hash"],
  957. correlation_id=evidence_crash_correlation_id,
  958. dataflow_uid=dataflow_uid,
  959. deployment_id=deployment_id,
  960. environment="test",
  961. workflow_version=1,
  962. node_id=node["id"],
  963. lease_owner=evidence_crash_jti,
  964. )
  965. with platform.begin() as connection:
  966. connection.execute(
  967. text(
  968. """
  969. UPDATE public.runner_task_executions
  970. SET lease_expires_at =
  971. CURRENT_TIMESTAMP - INTERVAL '1 second'
  972. WHERE token_jti IN (
  973. CAST(:no_run_jti AS uuid),
  974. CAST(:evidence_jti AS uuid)
  975. )
  976. """
  977. ),
  978. {
  979. "no_run_jti": no_run_crash_jti,
  980. "evidence_jti": evidence_crash_jti,
  981. },
  982. )
  983. connection.execute(
  984. text(
  985. """
  986. UPDATE public.rule_runs
  987. SET lease_expires_at =
  988. CURRENT_TIMESTAMP - INTERVAL '1 second'
  989. WHERE lease_owner = CAST(:jti AS uuid)
  990. """
  991. ),
  992. {"jti": evidence_crash_jti},
  993. )
  994. with runner_app.test_client() as client:
  995. http_result = client.post(
  996. "/v1/tasks/execute",
  997. json={
  998. "task_token": task_token,
  999. "node": node,
  1000. "parameters": {},
  1001. },
  1002. )
  1003. replay = client.post(
  1004. "/v1/tasks/execute",
  1005. json={
  1006. "task_token": task_token,
  1007. "node": node,
  1008. "parameters": {},
  1009. },
  1010. )
  1011. retried = client.post(
  1012. "/v1/tasks/execute",
  1013. json={
  1014. "task_token": retry_token,
  1015. "node": node,
  1016. "parameters": {},
  1017. },
  1018. )
  1019. no_run_crash = client.post(
  1020. "/v1/tasks/execute",
  1021. json={
  1022. "task_token": no_run_crash_token,
  1023. "node": node,
  1024. "parameters": {},
  1025. },
  1026. )
  1027. evidence_crash = client.post(
  1028. "/v1/tasks/execute",
  1029. json={
  1030. "task_token": evidence_crash_token,
  1031. "node": node,
  1032. "parameters": {},
  1033. },
  1034. )
  1035. assert http_result.status_code == 200, http_result.get_json()
  1036. assert http_result.get_json()["output_artifact"] == result[
  1037. "artifact_ref"
  1038. ]
  1039. assert http_result.get_json()["result"]["artifact_ref"] == result[
  1040. "artifact_ref"
  1041. ]
  1042. assert replay.status_code == 200
  1043. assert replay.headers["X-Idempotent-Replay"] == "true"
  1044. assert replay.get_json() == http_result.get_json()
  1045. assert retried.status_code == 200
  1046. assert retried.get_json()["output_artifact"] == result[
  1047. "artifact_ref"
  1048. ]
  1049. assert no_run_crash.status_code == 409
  1050. assert no_run_crash.get_json() == {
  1051. "error": "task execution outcome is unknown"
  1052. }
  1053. assert evidence_crash.status_code == 409
  1054. assert evidence_crash.get_json() == {
  1055. "error": "task execution outcome is unknown"
  1056. }
  1057. assert real_ledger.get(no_run_crash_jti).status == "unknown"
  1058. assert real_ledger.get(evidence_crash_jti).status == "unknown"
  1059. with platform.connect() as connection:
  1060. assert connection.execute(
  1061. text(
  1062. """
  1063. SELECT COUNT(*)
  1064. FROM public.rule_runs
  1065. WHERE correlation_id =
  1066. CAST(:correlation_id AS uuid)
  1067. """
  1068. ),
  1069. {"correlation_id": no_run_crash_correlation_id},
  1070. ).scalar_one() == 0
  1071. crash_evidence = connection.execute(
  1072. text(
  1073. """
  1074. SELECT status, commit_outcome
  1075. FROM public.rule_runs
  1076. WHERE correlation_id =
  1077. CAST(:correlation_id AS uuid)
  1078. """
  1079. ),
  1080. {"correlation_id": evidence_crash_correlation_id},
  1081. ).mappings().one()
  1082. assert dict(crash_evidence) == {
  1083. "status": "unknown",
  1084. "commit_outcome": "unknown",
  1085. }
  1086. ledger_record = PostgresTaskLedger(platform).get(ledger_jti)
  1087. assert ledger_record is not None
  1088. assert ledger_record.status == "success"
  1089. assert ledger_record.commit_outcome == "committed"
  1090. with platform.connect() as connection:
  1091. run_evidence = connection.execute(
  1092. text(
  1093. """
  1094. SELECT status, commit_outcome, rows_in, rows_out,
  1095. rows_rejected, rows_quarantined, public_result
  1096. FROM public.rule_runs
  1097. WHERE correlation_id = CAST(:correlation_id AS uuid)
  1098. AND component_binding_id =
  1099. CAST(:component_binding_id AS uuid)
  1100. """
  1101. ),
  1102. {
  1103. "correlation_id": correlation_id,
  1104. "component_binding_id": component_binding_id,
  1105. },
  1106. ).mappings().one()
  1107. sample_evidence = connection.execute(
  1108. text(
  1109. """
  1110. SELECT s.artifact_ref, s.artifact_digest,
  1111. s.sample_count, s.redaction_policy,
  1112. s.expires_at, s.handoff_status
  1113. FROM public.rule_violation_samples s
  1114. JOIN public.rule_runs r ON r.id = s.rule_run_id
  1115. WHERE r.correlation_id =
  1116. CAST(:correlation_id AS uuid)
  1117. """
  1118. ),
  1119. {"correlation_id": correlation_id},
  1120. ).mappings().one()
  1121. assert run_evidence["status"] == "success"
  1122. assert run_evidence["commit_outcome"] == "committed"
  1123. assert run_evidence["rows_in"] == 4
  1124. assert run_evidence["rows_out"] == 2
  1125. assert run_evidence["rows_rejected"] == 1
  1126. assert run_evidence["rows_quarantined"] == 0
  1127. assert run_evidence["public_result"]["output_artifact"] == result[
  1128. "artifact_ref"
  1129. ]
  1130. assert sample_evidence["artifact_digest"]
  1131. assert sample_evidence["sample_count"] == 1
  1132. assert (
  1133. sample_evidence["redaction_policy"]
  1134. == "rule-violation-default-v1"
  1135. )
  1136. assert sample_evidence["handoff_status"] == "ready"
  1137. assert store.read(
  1138. sample_evidence["artifact_ref"],
  1139. sample_evidence["artifact_digest"],
  1140. expected_schema_fields=[
  1141. {
  1142. "name": name,
  1143. "type": "string",
  1144. "nullable": True,
  1145. }
  1146. for name in (
  1147. "customer_id",
  1148. "mobile",
  1149. "name",
  1150. "segment_code",
  1151. "segment_name",
  1152. "version_no",
  1153. )
  1154. ],
  1155. ).collect().to_dicts() == [
  1156. {
  1157. "customer_id": "[REDACTED]",
  1158. "mobile": "[REDACTED]",
  1159. "name": "[REDACTED]",
  1160. "segment_code": "[REDACTED]",
  1161. "segment_name": "[REDACTED]",
  1162. "version_no": "[REDACTED]",
  1163. }
  1164. ]
  1165. orphan = store.write(
  1166. pl.DataFrame({"value": ["orphan"]}),
  1167. correlation_id,
  1168. 900,
  1169. schema_fields=[
  1170. {
  1171. "name": "value",
  1172. "type": "string",
  1173. "nullable": True,
  1174. }
  1175. ],
  1176. )
  1177. original_clock = store.clock
  1178. store.clock = lambda: datetime.now(UTC) + timedelta(seconds=60)
  1179. try:
  1180. reconciliation = resolver.reconcile(
  1181. limit=20,
  1182. grace_seconds=30,
  1183. )
  1184. finally:
  1185. store.clock = original_clock
  1186. assert reconciliation["orphans_deleted"] >= 1
  1187. assert store.describe_optional(orphan["artifact_ref"]) is None
  1188. assert store.describe(sample_evidence["artifact_ref"])[
  1189. "digest"
  1190. ] == sample_evidence["artifact_digest"]
  1191. downstream_node = {
  1192. "id": "task6_real_handoff",
  1193. "type": "rule.apply",
  1194. "purpose": "write",
  1195. "idempotency": {
  1196. "strategy": "deduplication_key",
  1197. "key": "customer_id",
  1198. },
  1199. "config": {
  1200. "component_binding_id": (
  1201. downstream_component_binding_id
  1202. ),
  1203. "rule_version_id": downstream_rule_id,
  1204. "execution_plan_hash": downstream_compiled[
  1205. "plan_hash"
  1206. ],
  1207. },
  1208. }
  1209. downstream_result = evidenced_executor.execute(
  1210. downstream_node,
  1211. {"input_artifact": result["artifact_ref"]},
  1212. write_authorized=True,
  1213. correlation_id=correlation_id,
  1214. dataflow_uid=dataflow_uid,
  1215. deployment_id=deployment_id,
  1216. environment="test",
  1217. workflow_version=1,
  1218. node_id="task6_real_handoff",
  1219. task_jti=new_governance_uid(),
  1220. )
  1221. assert downstream_result["rows_in"] == 2
  1222. assert downstream_result["rows_out"] == 2
  1223. assert downstream_result["output_artifact"] != result[
  1224. "artifact_ref"
  1225. ]
  1226. assert store.read(
  1227. downstream_result["artifact_ref"],
  1228. downstream_result["digest"],
  1229. expected_schema_fields=output_schema["fields"],
  1230. limits=downstream_compiled["plan"]["resource_limits"],
  1231. ).collect().sort("customer_id").to_dicts() == (
  1232. output.sort("customer_id").to_dicts()
  1233. )
  1234. replayed_downstream = evidenced_executor.execute(
  1235. downstream_node,
  1236. {"input_artifact": result["artifact_ref"]},
  1237. write_authorized=True,
  1238. correlation_id=correlation_id,
  1239. dataflow_uid=dataflow_uid,
  1240. deployment_id=deployment_id,
  1241. environment="test",
  1242. workflow_version=1,
  1243. node_id="task6_real_handoff",
  1244. task_jti=new_governance_uid(),
  1245. )
  1246. assert replayed_downstream["artifact_ref"] == downstream_result[
  1247. "artifact_ref"
  1248. ]
  1249. with platform.connect() as connection:
  1250. assert connection.execute(
  1251. text(
  1252. """
  1253. SELECT COUNT(*)
  1254. FROM public.rule_runs
  1255. WHERE correlation_id =
  1256. CAST(:correlation_id AS uuid)
  1257. """
  1258. ),
  1259. {"correlation_id": correlation_id},
  1260. ).scalar_one() == 2
  1261. from app.runner.nodes import NodeExecutionError
  1262. class FailingAdapter:
  1263. def execute(self, **_kwargs):
  1264. raise NodeExecutionError(
  1265. "safe downstream failure",
  1266. commit_outcome="not_committed",
  1267. )
  1268. failed_executor = RulePlanExecutor(
  1269. PostgresRulePlanRepository(platform),
  1270. adapters={"polars_batch": FailingAdapter()},
  1271. evidence_writer=PostgresRuleEvidenceWriter(
  1272. platform,
  1273. store,
  1274. sample_ttl_seconds=900,
  1275. ),
  1276. )
  1277. with pytest.raises(NodeExecutionError, match="safe downstream"):
  1278. failed_executor.execute(
  1279. node,
  1280. {},
  1281. write_authorized=True,
  1282. correlation_id=failure_correlation_id,
  1283. dataflow_uid=dataflow_uid,
  1284. deployment_id=deployment_id,
  1285. environment="test",
  1286. workflow_version=1,
  1287. node_id="task5_real_polars",
  1288. task_jti=new_governance_uid(),
  1289. )
  1290. with platform.connect() as connection:
  1291. failed_evidence = connection.execute(
  1292. text(
  1293. """
  1294. SELECT status, commit_outcome
  1295. FROM public.rule_runs
  1296. WHERE correlation_id =
  1297. CAST(:correlation_id AS uuid)
  1298. """
  1299. ),
  1300. {"correlation_id": failure_correlation_id},
  1301. ).mappings().one()
  1302. assert dict(failed_evidence) == {
  1303. "status": "failed",
  1304. "commit_outcome": "not_committed",
  1305. }
  1306. class UnknownAdapter:
  1307. def execute(self, **_kwargs):
  1308. raise NodeExecutionError(
  1309. "safe uncertain commit",
  1310. commit_outcome="unknown",
  1311. )
  1312. unknown_executor = RulePlanExecutor(
  1313. PostgresRulePlanRepository(platform),
  1314. adapters={"polars_batch": UnknownAdapter()},
  1315. evidence_writer=PostgresRuleEvidenceWriter(
  1316. platform,
  1317. store,
  1318. sample_ttl_seconds=900,
  1319. ),
  1320. )
  1321. with pytest.raises(NodeExecutionError, match="uncertain commit"):
  1322. unknown_executor.execute(
  1323. node,
  1324. {},
  1325. write_authorized=True,
  1326. correlation_id=unknown_correlation_id,
  1327. dataflow_uid=dataflow_uid,
  1328. deployment_id=deployment_id,
  1329. environment="test",
  1330. workflow_version=1,
  1331. node_id="task5_real_polars",
  1332. task_jti=new_governance_uid(),
  1333. )
  1334. with platform.connect() as connection:
  1335. unknown_evidence = connection.execute(
  1336. text(
  1337. """
  1338. SELECT status, commit_outcome
  1339. FROM public.rule_runs
  1340. WHERE correlation_id =
  1341. CAST(:correlation_id AS uuid)
  1342. """
  1343. ),
  1344. {"correlation_id": unknown_correlation_id},
  1345. ).mappings().one()
  1346. assert dict(unknown_evidence) == {
  1347. "status": "unknown",
  1348. "commit_outcome": "unknown",
  1349. }
  1350. evidence_writer = PostgresRuleEvidenceWriter(
  1351. platform,
  1352. store,
  1353. sample_ttl_seconds=900,
  1354. lease_seconds=30,
  1355. )
  1356. lease_owner = new_governance_uid()
  1357. lease_run_id = evidence_writer.start(
  1358. component_binding_id=component_binding_id,
  1359. rule_version_id=rule_id,
  1360. plan_hash=compiled["plan_hash"],
  1361. correlation_id=lease_correlation_id,
  1362. dataflow_uid=dataflow_uid,
  1363. deployment_id=deployment_id,
  1364. environment="test",
  1365. workflow_version=1,
  1366. node_id="task5_real_polars",
  1367. lease_owner=lease_owner,
  1368. )
  1369. with pytest.raises(ValueError, match="not owned"):
  1370. evidence_writer.heartbeat(
  1371. lease_run_id,
  1372. new_governance_uid(),
  1373. )
  1374. evidence_writer.heartbeat(lease_run_id, lease_owner)
  1375. with platform.begin() as connection:
  1376. connection.execute(
  1377. text(
  1378. """
  1379. UPDATE public.rule_runs
  1380. SET lease_expires_at =
  1381. CURRENT_TIMESTAMP - INTERVAL '1 second'
  1382. WHERE id = CAST(:id AS uuid)
  1383. """
  1384. ),
  1385. {"id": lease_run_id},
  1386. )
  1387. assert evidence_writer.start(
  1388. component_binding_id=component_binding_id,
  1389. rule_version_id=rule_id,
  1390. plan_hash=compiled["plan_hash"],
  1391. correlation_id=lease_correlation_id,
  1392. dataflow_uid=dataflow_uid,
  1393. deployment_id=deployment_id,
  1394. environment="test",
  1395. workflow_version=1,
  1396. node_id="task5_real_polars",
  1397. lease_owner=new_governance_uid(),
  1398. ) == lease_run_id
  1399. assert evidence_writer.replay(lease_run_id)["status"] == "unknown"
  1400. class FailOnceConnection:
  1401. def __init__(self, connection, state):
  1402. self.connection = connection
  1403. self.state = state
  1404. def execute(self, statement, parameters=None):
  1405. sql = str(statement)
  1406. if (
  1407. self.state["armed"]
  1408. and "UPDATE public.rule_runs" in sql
  1409. and "rows_in = :rows_in" in sql
  1410. ):
  1411. self.state["armed"] = False
  1412. raise RuntimeError("simulated response loss")
  1413. return self.connection.execute(statement, parameters)
  1414. class FailOnceBegin:
  1415. def __init__(self, context, state):
  1416. self.context = context
  1417. self.state = state
  1418. def __enter__(self):
  1419. return FailOnceConnection(
  1420. self.context.__enter__(),
  1421. self.state,
  1422. )
  1423. def __exit__(self, *args):
  1424. return self.context.__exit__(*args)
  1425. class FailOnceEngine:
  1426. def __init__(self, engine):
  1427. self.engine = engine
  1428. self.state = {"armed": True}
  1429. def connect(self):
  1430. return self.engine.connect()
  1431. def begin(self):
  1432. return FailOnceBegin(
  1433. self.engine.begin(),
  1434. self.state,
  1435. )
  1436. sample_crash_writer = PostgresRuleEvidenceWriter(
  1437. FailOnceEngine(platform),
  1438. store,
  1439. sample_ttl_seconds=900,
  1440. )
  1441. sample_crash_run_id = sample_crash_writer.start(
  1442. component_binding_id=component_binding_id,
  1443. rule_version_id=rule_id,
  1444. plan_hash=compiled["plan_hash"],
  1445. correlation_id=sample_crash_correlation_id,
  1446. dataflow_uid=dataflow_uid,
  1447. deployment_id=deployment_id,
  1448. environment="test",
  1449. workflow_version=1,
  1450. node_id="task5_real_polars",
  1451. lease_owner=new_governance_uid(),
  1452. )
  1453. sample_crash_evidence = {
  1454. "status": "success",
  1455. "rows_in": 1,
  1456. "rows_out": 0,
  1457. "rows_rejected": 1,
  1458. "rows_quarantined": 0,
  1459. "commit_outcome": "committed",
  1460. "timings": {"duration_ms": 1},
  1461. "violation_sample": [{"mobile": "[REDACTED]"}],
  1462. "sample_count": 1,
  1463. "redaction_policy": "rule-violation-default-v1",
  1464. }
  1465. with pytest.raises(RuntimeError, match="outcome is unknown"):
  1466. sample_crash_writer.finish(
  1467. sample_crash_run_id,
  1468. sample_crash_evidence,
  1469. )
  1470. with platform.connect() as connection:
  1471. assert connection.execute(
  1472. text(
  1473. """
  1474. SELECT handoff_status
  1475. FROM public.rule_violation_samples
  1476. WHERE rule_run_id = CAST(:id AS uuid)
  1477. """
  1478. ),
  1479. {"id": sample_crash_run_id},
  1480. ).scalar_one() == "ready"
  1481. with platform.begin() as connection:
  1482. connection.execute(
  1483. text(
  1484. """
  1485. UPDATE public.rule_violation_samples
  1486. SET handoff_status = 'unknown'
  1487. WHERE rule_run_id = CAST(:id AS uuid)
  1488. """
  1489. ),
  1490. {"id": sample_crash_run_id},
  1491. )
  1492. sample_reconciliation = evidence_writer.reconcile_samples(
  1493. limit=10
  1494. )
  1495. assert sample_reconciliation["ready"] >= 1
  1496. evidence_writer.finish(
  1497. sample_crash_run_id,
  1498. sample_crash_evidence,
  1499. )
  1500. assert evidence_writer.replay(sample_crash_run_id)[
  1501. "status"
  1502. ] == "success"
  1503. with platform.begin() as connection:
  1504. connection.execute(
  1505. text(
  1506. """
  1507. INSERT INTO public.dataflow_dataset_bindings
  1508. (id, dataflow_deployment_id, logical_ref,
  1509. data_source_uid, object_kind, object_ref,
  1510. schema_snapshot_id, dialect, access_mode, write_mode,
  1511. binding_hash)
  1512. VALUES (
  1513. CAST(:id AS uuid), CAST(:deployment_id AS uuid),
  1514. 'sql_receipt', CAST(:source_uid AS uuid), 'table',
  1515. 'public.task6_sql_receipt',
  1516. CAST(:schema_snapshot_id AS uuid), 'postgresql',
  1517. 'read_write', 'upsert', :binding_hash
  1518. )
  1519. """
  1520. ),
  1521. {
  1522. "id": sql_binding_id,
  1523. "deployment_id": deployment_id,
  1524. "source_uid": output_binding["data_source_uid"],
  1525. "schema_snapshot_id": output_schema["id"],
  1526. "binding_hash": "b" * 64,
  1527. },
  1528. )
  1529. receipt_run_id = evidence_writer.start(
  1530. component_binding_id=component_binding_id,
  1531. rule_version_id=rule_id,
  1532. plan_hash=compiled["plan_hash"],
  1533. correlation_id=receipt_correlation_id,
  1534. dataflow_uid=dataflow_uid,
  1535. deployment_id=deployment_id,
  1536. environment="test",
  1537. workflow_version=1,
  1538. node_id="task5_real_polars",
  1539. lease_owner=new_governance_uid(),
  1540. )
  1541. receipt = evidence_writer.stage_sql_output(
  1542. receipt_run_id,
  1543. output_binding_id=sql_binding_id,
  1544. )
  1545. assert re.fullmatch(
  1546. r"dataops-staging://[0-9a-f-]{36}",
  1547. receipt,
  1548. )
  1549. with pytest.raises(ValueError, match="not executable"):
  1550. evidence_writer.resolve_sql_staging(
  1551. receipt,
  1552. deployment_id=deployment_id,
  1553. correlation_id=receipt_correlation_id,
  1554. input_binding_id=sql_binding_id,
  1555. )
  1556. receipt_evidence = {
  1557. "status": "success",
  1558. "rows_in": 2,
  1559. "rows_out": 2,
  1560. "rows_rejected": 0,
  1561. "rows_quarantined": 0,
  1562. "commit_outcome": "committed",
  1563. "timings": {"duration_ms": 1},
  1564. "public_result": {
  1565. "rows_in": 2,
  1566. "rows_out": 2,
  1567. "rows_rejected": 0,
  1568. "rows_quarantined": 0,
  1569. "commit_outcome": "committed",
  1570. },
  1571. }
  1572. evidence_writer.finish(receipt_run_id, receipt_evidence)
  1573. evidence_writer.finish(receipt_run_id, receipt_evidence)
  1574. with pytest.raises(ValueError, match="immutable"):
  1575. evidence_writer.finish(
  1576. receipt_run_id,
  1577. {
  1578. **receipt_evidence,
  1579. "rows_out": 1,
  1580. },
  1581. )
  1582. resolved_receipt = evidence_writer.resolve_sql_staging(
  1583. receipt,
  1584. deployment_id=deployment_id,
  1585. correlation_id=receipt_correlation_id,
  1586. input_binding_id=sql_binding_id,
  1587. )
  1588. assert resolved_receipt["relation_ref"] == (
  1589. "public.task6_sql_receipt"
  1590. )
  1591. assert len(resolved_receipt["relation_digest"]) == 64
  1592. with pytest.raises(ValueError, match="not executable"):
  1593. evidence_writer.resolve_sql_staging(
  1594. receipt,
  1595. deployment_id=deployment_id,
  1596. correlation_id=new_governance_uid(),
  1597. input_binding_id=sql_binding_id,
  1598. )
  1599. with pytest.raises(ValueError, match="invalid"):
  1600. evidence_writer.resolve_sql_staging(
  1601. "dataops-staging://public.task6_sql_receipt",
  1602. deployment_id=deployment_id,
  1603. correlation_id=receipt_correlation_id,
  1604. input_binding_id=sql_binding_id,
  1605. )
  1606. failed_receipt_run_id = evidence_writer.start(
  1607. component_binding_id=component_binding_id,
  1608. rule_version_id=rule_id,
  1609. plan_hash=compiled["plan_hash"],
  1610. correlation_id=failed_receipt_correlation_id,
  1611. dataflow_uid=dataflow_uid,
  1612. deployment_id=deployment_id,
  1613. environment="test",
  1614. workflow_version=1,
  1615. node_id="task5_real_polars",
  1616. lease_owner=new_governance_uid(),
  1617. )
  1618. failed_receipt = evidence_writer.stage_sql_output(
  1619. failed_receipt_run_id,
  1620. output_binding_id=sql_binding_id,
  1621. )
  1622. evidence_writer.finish(
  1623. failed_receipt_run_id,
  1624. {
  1625. "status": "failed",
  1626. "commit_outcome": "not_committed",
  1627. "timings": {"duration_ms": 1},
  1628. },
  1629. )
  1630. with pytest.raises(ValueError, match="not executable"):
  1631. evidence_writer.resolve_sql_staging(
  1632. failed_receipt,
  1633. deployment_id=deployment_id,
  1634. correlation_id=failed_receipt_correlation_id,
  1635. input_binding_id=sql_binding_id,
  1636. )
  1637. with platform.begin() as connection:
  1638. connection.execute(
  1639. text(
  1640. """
  1641. UPDATE public.rule_sql_staging_receipts
  1642. SET expires_at =
  1643. CURRENT_TIMESTAMP - INTERVAL '1 second'
  1644. WHERE correlation_id IN (
  1645. CAST(:ready AS uuid),
  1646. CAST(:failed AS uuid)
  1647. )
  1648. """
  1649. ),
  1650. {
  1651. "ready": receipt_correlation_id,
  1652. "failed": failed_receipt_correlation_id,
  1653. },
  1654. )
  1655. assert evidence_writer.cleanup_sql_staging(limit=10) == 2
  1656. with platform.connect() as connection:
  1657. assert connection.execute(
  1658. text(
  1659. """
  1660. SELECT COUNT(*)
  1661. FROM public.rule_sql_staging_receipts
  1662. WHERE correlation_id IN (
  1663. CAST(:ready AS uuid),
  1664. CAST(:failed AS uuid)
  1665. )
  1666. AND status = 'expired'
  1667. """
  1668. ),
  1669. {
  1670. "ready": receipt_correlation_id,
  1671. "failed": failed_receipt_correlation_id,
  1672. },
  1673. ).scalar_one() == 2
  1674. conflict_path = tmp_path / "conflict.parquet"
  1675. pl.DataFrame(
  1676. {
  1677. "customer_id": [99],
  1678. "mobile": ["13800138000"],
  1679. "name": ["Conflict"],
  1680. "segment_code": ["A"],
  1681. "segment_name": ["Gold"],
  1682. "version_no": [1],
  1683. }
  1684. ).write_parquet(conflict_path)
  1685. object_count_before_conflict = len(
  1686. list(minio.list_objects(bucket, prefix=prefix, recursive=True))
  1687. )
  1688. with pytest.raises(ValueError, match="immutable|digest"):
  1689. resolver.publish_path(
  1690. str(conflict_path),
  1691. binding_id=output_binding["id"],
  1692. binding_hash=compiled["plan"]["output_binding_hash"],
  1693. correlation_id=correlation_id,
  1694. kind="output",
  1695. ttl_seconds=900,
  1696. schema_fields=output_schema["fields"],
  1697. limits=compiled["plan"]["resource_limits"],
  1698. )
  1699. assert len(
  1700. list(minio.list_objects(bucket, prefix=prefix, recursive=True))
  1701. ) == object_count_before_conflict
  1702. assert customer_artifact["digest"]
  1703. assert segment_artifact["digest"]
  1704. with platform.connect() as connection:
  1705. catalog_rows = connection.execute(
  1706. text(
  1707. """
  1708. SELECT artifact_kind, artifact_ref, handoff_status,
  1709. binding_hash
  1710. FROM public.rule_run_artifacts
  1711. WHERE correlation_id = CAST(:correlation_id AS uuid)
  1712. ORDER BY artifact_kind
  1713. """
  1714. ),
  1715. {"correlation_id": correlation_id},
  1716. ).mappings().all()
  1717. # The repeated deterministic output has the same digest and is
  1718. # idempotently retained as one stable catalog handoff.
  1719. assert len(catalog_rows) == 4
  1720. assert all(row["handoff_status"] == "ready" for row in catalog_rows)
  1721. assert all(len(row["binding_hash"]) == 64 for row in catalog_rows)
  1722. assert {
  1723. row["artifact_ref"]
  1724. for row in catalog_rows
  1725. if row["artifact_kind"] == "output"
  1726. } == {
  1727. result["artifact_ref"],
  1728. downstream_result["artifact_ref"],
  1729. }
  1730. assert len(
  1731. list(minio.list_objects(bucket, prefix=prefix, recursive=True))
  1732. ) == 5
  1733. finally:
  1734. for item in list(
  1735. minio.list_objects(bucket, prefix=prefix, recursive=True)
  1736. ):
  1737. minio.remove_object(bucket, item.object_name)
  1738. assert list(
  1739. minio.list_objects(bucket, prefix=prefix, recursive=True)
  1740. ) == []
  1741. with postgres.begin() as connection:
  1742. connection.execute(text(f"DROP TABLE IF EXISTS {customer_table}"))
  1743. with mysql.begin() as connection:
  1744. connection.execute(text(f"DROP TABLE IF EXISTS {segment_table}"))
  1745. with platform.begin() as connection:
  1746. if ledger_jti is not None:
  1747. connection.execute(
  1748. text(
  1749. "DELETE FROM public.runner_task_executions "
  1750. "WHERE token_jti = CAST(:jti AS uuid)"
  1751. ),
  1752. {"jti": ledger_jti},
  1753. )
  1754. if retry_ledger_jti is not None:
  1755. connection.execute(
  1756. text(
  1757. "DELETE FROM public.runner_task_executions "
  1758. "WHERE token_jti = CAST(:jti AS uuid)"
  1759. ),
  1760. {"jti": retry_ledger_jti},
  1761. )
  1762. for crash_jti in (
  1763. no_run_crash_jti,
  1764. evidence_crash_jti,
  1765. ):
  1766. if crash_jti is not None:
  1767. connection.execute(
  1768. text(
  1769. "DELETE FROM public.runner_task_executions "
  1770. "WHERE token_jti = CAST(:jti AS uuid)"
  1771. ),
  1772. {"jti": crash_jti},
  1773. )
  1774. connection.execute(
  1775. text(
  1776. """
  1777. DELETE FROM public.rule_sql_staging_receipts
  1778. WHERE correlation_id IN (
  1779. CAST(:receipt_correlation_id AS uuid),
  1780. CAST(:failed_receipt_correlation_id AS uuid)
  1781. )
  1782. """
  1783. ),
  1784. {
  1785. "receipt_correlation_id": receipt_correlation_id,
  1786. "failed_receipt_correlation_id": (
  1787. failed_receipt_correlation_id
  1788. ),
  1789. },
  1790. )
  1791. connection.execute(
  1792. text(
  1793. """
  1794. DELETE FROM public.rule_violation_samples s
  1795. USING public.rule_runs r
  1796. WHERE s.rule_run_id = r.id
  1797. AND r.correlation_id IN (
  1798. CAST(:correlation_id AS uuid),
  1799. CAST(:sample_crash_correlation_id AS uuid)
  1800. )
  1801. """
  1802. ),
  1803. {
  1804. "correlation_id": correlation_id,
  1805. "sample_crash_correlation_id": (
  1806. sample_crash_correlation_id
  1807. ),
  1808. },
  1809. )
  1810. connection.execute(
  1811. text(
  1812. "DELETE FROM public.rule_runs "
  1813. "WHERE correlation_id IN ("
  1814. "CAST(:correlation_id AS uuid), "
  1815. "CAST(:failure_correlation_id AS uuid), "
  1816. "CAST(:unknown_correlation_id AS uuid), "
  1817. "CAST(:lease_correlation_id AS uuid), "
  1818. "CAST(:sample_crash_correlation_id AS uuid), "
  1819. "CAST(:receipt_correlation_id AS uuid), "
  1820. "CAST(:failed_receipt_correlation_id AS uuid), "
  1821. "CAST(:no_run_crash_correlation_id AS uuid), "
  1822. "CAST(:evidence_crash_correlation_id AS uuid))"
  1823. ),
  1824. {
  1825. "correlation_id": correlation_id,
  1826. "failure_correlation_id": failure_correlation_id,
  1827. "unknown_correlation_id": unknown_correlation_id,
  1828. "lease_correlation_id": lease_correlation_id,
  1829. "sample_crash_correlation_id": (
  1830. sample_crash_correlation_id
  1831. ),
  1832. "receipt_correlation_id": receipt_correlation_id,
  1833. "failed_receipt_correlation_id": (
  1834. failed_receipt_correlation_id
  1835. ),
  1836. "no_run_crash_correlation_id": (
  1837. no_run_crash_correlation_id
  1838. ),
  1839. "evidence_crash_correlation_id": (
  1840. evidence_crash_correlation_id
  1841. ),
  1842. },
  1843. )
  1844. connection.execute(
  1845. text(
  1846. "DELETE FROM public.dataflow_dataset_bindings "
  1847. "WHERE id = CAST(:id AS uuid)"
  1848. ),
  1849. {"id": sql_binding_id},
  1850. )
  1851. connection.execute(
  1852. text(
  1853. "DELETE FROM public.rule_run_artifacts "
  1854. "WHERE correlation_id = CAST(:correlation_id AS uuid)"
  1855. ),
  1856. {"correlation_id": correlation_id},
  1857. )
  1858. connection.execute(
  1859. text(
  1860. "DELETE FROM public.rule_execution_plans "
  1861. "WHERE id IN (CAST(:id AS uuid), "
  1862. "CAST(:downstream_id AS uuid))"
  1863. ),
  1864. {
  1865. "id": plan_id,
  1866. "downstream_id": downstream_plan_id,
  1867. },
  1868. )
  1869. connection.execute(
  1870. text(
  1871. "DELETE FROM public.dataflow_dataset_bindings "
  1872. "WHERE dataflow_deployment_id = CAST(:id AS uuid)"
  1873. ),
  1874. {"id": deployment_id},
  1875. )
  1876. connection.execute(
  1877. text(
  1878. "DELETE FROM public.dataflow_component_bindings "
  1879. "WHERE id IN (CAST(:id AS uuid), "
  1880. "CAST(:downstream_id AS uuid))"
  1881. ),
  1882. {
  1883. "id": component_binding_id,
  1884. "downstream_id": downstream_component_binding_id,
  1885. },
  1886. )
  1887. connection.execute(
  1888. text(
  1889. "DELETE FROM public.dataflow_deployments "
  1890. "WHERE id = CAST(:id AS uuid)"
  1891. ),
  1892. {"id": deployment_id},
  1893. )
  1894. if gateway_candidate_id is not None:
  1895. connection.execute(
  1896. text(
  1897. "DELETE FROM public.workflow_canary_evidence "
  1898. "WHERE workflow_version_id = CAST(:id AS uuid)"
  1899. ),
  1900. {"id": gateway_candidate_id},
  1901. )
  1902. connection.execute(
  1903. text(
  1904. "DELETE FROM public.workflow_schedules "
  1905. "WHERE workflow_version_id = CAST(:id AS uuid)"
  1906. ),
  1907. {"id": gateway_candidate_id},
  1908. )
  1909. connection.execute(
  1910. text(
  1911. "DELETE FROM public.workflow_engine_bindings "
  1912. "WHERE workflow_version_id = CAST(:id AS uuid)"
  1913. ),
  1914. {"id": gateway_candidate_id},
  1915. )
  1916. connection.execute(
  1917. text(
  1918. "DELETE FROM public.dataflow_workflow_versions "
  1919. "WHERE id = CAST(:id AS uuid)"
  1920. ),
  1921. {"id": gateway_candidate_id},
  1922. )
  1923. connection.execute(
  1924. text(
  1925. "DELETE FROM public.dataflow_versions "
  1926. "WHERE id = CAST(:id AS uuid)"
  1927. ),
  1928. {"id": dataflow_version_id},
  1929. )
  1930. connection.execute(
  1931. text(
  1932. "DELETE FROM public.data_rule_versions "
  1933. "WHERE id IN (CAST(:id AS uuid), "
  1934. "CAST(:downstream_id AS uuid))"
  1935. ),
  1936. {
  1937. "id": rule_id,
  1938. "downstream_id": downstream_rule_id,
  1939. },
  1940. )
  1941. connection.execute(
  1942. text(
  1943. "DELETE FROM public.data_rules "
  1944. "WHERE rule_uid IN (CAST(:rule_uid AS uuid), "
  1945. "CAST(:downstream_rule_uid AS uuid))"
  1946. ),
  1947. {
  1948. "rule_uid": rule_uid,
  1949. "downstream_rule_uid": downstream_rule_uid,
  1950. },
  1951. )
  1952. for schema in (input_schema, lookup_schema, output_schema):
  1953. connection.execute(
  1954. text(
  1955. "DELETE FROM public.data_schema_snapshots "
  1956. "WHERE id = CAST(:id AS uuid)"
  1957. ),
  1958. {"id": schema["id"]},
  1959. )
  1960. postgres.dispose()
  1961. mysql.dispose()
  1962. platform.dispose()