test_data_rule_polars_execution.py 98 KB

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